@@ -0,0 +1,841 @@ package fs import ( "context" "fmt" "os" "time" "dmitri.shuralyov.com/changes" "github.com/shurcooL/users" ) type Service struct{} var s = struct { changes []struct { changes.Change Timeline []interface{} Commits []changes.Commit Diff []byte } }{ changes: []struct { changes.Change Timeline []interface{} Commits []changes.Commit Diff []byte }{ { Change: changes.Change{ ID: 1, State: changes.OpenState, Title: "Initial implementation of woff2.", Labels: nil, Author: shurcool, CreatedAt: time.Now().Add(-5 * time.Minute), Replies: 0, Commits: 1, }, Timeline: nil, Commits: []changes.Commit{{ SHA: "4a911c4a1eabcc20a66ccc5c983dede401da2796", Message: "Initial implementation of woff2.\n\nMaybe some additional details here.", Author: shurcool, AuthorTime: time.Now().Add(-10 * time.Minute), }}, Diff: []byte(diff), }, }, } // List changes. func (*Service) List(ctx context.Context, repo string, opt changes.ListOptions) ([]changes.Change, error) { if repo != "dmitri.shuralyov.com/font/woff2" { return nil, os.ErrNotExist } var cs []changes.Change for _, c := range s.changes { cs = append(cs, c.Change) } return cs, nil } // Count changes. func (*Service) Count(ctx context.Context, repo string, opt changes.ListOptions) (uint64, error) { if repo != "dmitri.shuralyov.com/font/woff2" { return 0, os.ErrNotExist } var counts func(s changes.State) bool switch opt.Filter { case changes.FilterOpen: counts = func(s changes.State) bool { return s == changes.OpenState } case changes.FilterClosedMerged: counts = func(s changes.State) bool { return s == changes.ClosedState || s == changes.MergedState } case changes.FilterAll: counts = func(s changes.State) bool { return true } default: // TODO: Map to 400 Bad Request HTTP error. return 0, fmt.Errorf("opt.State has unsupported value %q", opt.Filter) } var count uint64 for _, c := range s.changes { if counts(c.State) { count++ } } return count, nil } // Get a change. func (*Service) Get(ctx context.Context, repo string, id uint64) (changes.Change, error) { if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 { return changes.Change{}, os.ErrNotExist } return s.changes[0].Change, nil } // ListTimeline lists timeline items (changes.Comment, changes.TimelineItem) for specified change id. func (*Service) ListTimeline(ctx context.Context, repo string, id uint64, opt *changes.ListTimelineOptions) ([]interface{}, error) { if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 { return nil, os.ErrNotExist } return s.changes[0].Timeline, nil } // ListCommits lists change commits. func (*Service) ListCommits(ctx context.Context, repo string, id uint64) ([]changes.Commit, error) { if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 { return nil, os.ErrNotExist } return s.changes[0].Commits, nil } // Get a change diff. func (*Service) GetDiff(ctx context.Context, repo string, id uint64, opt *changes.GetDiffOptions) ([]byte, error) { if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 { return nil, os.ErrNotExist } return s.changes[0].Diff, nil } var shurcool = users.User{ UserSpec: users.UserSpec{ ID: 1924134, Domain: "github.com", }, Login: "shurcooL", Name: "Dmitri Shuralyov", Email: "dmitri@shuralyov.com", AvatarURL: "https://dmitri.shuralyov.com/avatar.jpg", HTMLURL: "https://dmitri.shuralyov.com", SiteAdmin: true, } const diff = `diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..49ea0f9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2018 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..a751214 --- /dev/null +++ b/doc.go @@ -0,0 +1,4 @@ +// Package woff2 implements a WOFF2 font decoder. +// +// The WOFF2 font packaging format is specified at https://www.w3.org/TR/WOFF2/. +package woff2 diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..498a4a8 --- /dev/null +++ b/parse.go @@ -0,0 +1,438 @@ +package woff2 + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + + "github.com/dsnet/compress/brotli" +) + +// File represents a parsed WOFF2 file. +type File struct { + Header Header + TableDirectory TableDirectory + // CollectionDirectory is present only if the font is a collection, + // as reported by Header.IsCollection. + CollectionDirectory *CollectionDirectory + + // FontData is the concatenation of data for each table in the font. + // During storage, it's compressed using Brotli. + FontData []byte + + ExtendedMetadata *ExtendedMetadata + + // PrivateData is an optional block of private data for the font designer, + // foundry, or vendor to use. + PrivateData []byte +} + +// Parse parses the WOFF2 data from r. +func Parse(r io.Reader) (File, error) { + hdr, err := parseHeader(r) + if err != nil { + return File{}, err + } + td, err := parseTableDirectory(r, hdr) + if err != nil { + return File{}, err + } + cd, err := parseCollectionDirectory(r, hdr) + if err != nil { + return File{}, err + } + fd, err := parseCompressedFontData(r, hdr, td) + if err != nil { + return File{}, err + } + em, err := parseExtendedMetadata(r, hdr) + if err != nil { + return File{}, err + } + pd, err := parsePrivateData(r, hdr) + if err != nil { + return File{}, err + } + + n, err := io.Copy(discardZeroes{}, r) + if err != nil { + return File{}, fmt.Errorf("Parse: %v", err) + } + if n > 3 { + return File{}, fmt.Errorf("Parse: %d bytes left remaining, want no more than 3", n) + } + + return File{ + Header: hdr, + TableDirectory: td, + CollectionDirectory: cd, + FontData: fd, + ExtendedMetadata: em, + PrivateData: pd, + }, nil +} + +// discardZeroes is an io.Writer that returns an error if any non-zero bytes are written to it. +type discardZeroes struct{} + +func (discardZeroes) Write(p []byte) (int, error) { + for _, b := range p { + if b != 0 { + return 0, fmt.Errorf("encountered non-zero byte %d", b) + } + } + return len(p), nil +} + +// Header is the file header with basic font type and version, +// along with offsets to metadata and private data blocks. +type Header struct { + Signature uint32 // The identifying signature; must be 0x774F4632 ('wOF2'). + Flavor uint32 // The "sfnt version" of the input font. + Length uint32 // Total size of the WOFF file. + NumTables uint16 // Number of entries in directory of font tables. + Reserved uint16 // Reserved; set to 0. + TotalSfntSize uint32 // Total size needed for the uncompressed font data, including the sfnt header, directory, and font tables (including padding). + TotalCompressedSize uint32 // Total length of the compressed data block. + MajorVersion uint16 // Major version of the WOFF file. + MinorVersion uint16 // Minor version of the WOFF file. + MetaOffset uint32 // Offset to metadata block, from beginning of WOFF file. + MetaLength uint32 // Length of compressed metadata block. + MetaOrigLength uint32 // Uncompressed size of metadata block. + PrivOffset uint32 // Offset to private data block, from beginning of WOFF file. + PrivLength uint32 // Length of private data block. +} + +func parseHeader(r io.Reader) (Header, error) { + var hdr Header + err := binary.Read(r, order, &hdr) + if err != nil { + return Header{}, err + } + if hdr.Signature != signature { + return Header{}, fmt.Errorf("parseHeader: invalid signature: got %#08x, want %#08x", hdr.Signature, signature) + } + return hdr, nil +} + +// IsCollection reports whether this is a font collection, i.e., +// if the value of Flavor field is set to the TrueType Collection flavor 'ttcf'. +func (hdr Header) IsCollection() bool { + return hdr.Flavor == ttcfFlavor +} + +// TableDirectory is the directory of font tables, containing size and other info. +type TableDirectory []TableDirectoryEntry + +func parseTableDirectory(r io.Reader, hdr Header) (TableDirectory, error) { + var td TableDirectory + for i := 0; i < int(hdr.NumTables); i++ { + var e TableDirectoryEntry + + err := readU8(r, &e.Flags) + if err != nil { + return nil, err + } + if e.Flags&0x3f == 0x3f { + e.Tag = new(uint32) + err := readU32(r, e.Tag) + if err != nil { + return nil, err + } + } + err = readBase128(r, &e.OrigLength) + if err != nil { + return nil, err + } + + switch tag, transformVersion := e.tag(), e.transformVersion(); tag { + case glyfTable, locaTable: + // 0 means transform for glyf/loca tables. + if transformVersion == 0 { + e.TransformLength = new(uint32) + err := readBase128(r, e.TransformLength) + if err != nil { + return nil, err + } + + // The transform length of the transformed loca table MUST always be zero. + if tag == locaTable && *e.TransformLength != 0 { + return nil, fmt.Errorf("parseTableDirectory: 'loca' table has non-zero transform length %d", *e.TransformLength) + } + } + default: + // Non-0 means transform for other tables. + if transformVersion != 0 { + e.TransformLength = new(uint32) + err := readBase128(r, e.TransformLength) + if err != nil { + return nil, err + } + } + } + + td = append(td, e) + } + return td, nil +} + +// Table is a high-level representation of a table. +type Table struct { + Tag uint32 + Offset int + Length int +} + +// Tables returns the derived high-level information +// about the tables in the table directory. +func (td TableDirectory) Tables() []Table { + var ts []Table + var offset int + for _, t := range td { + length := int(t.length()) + ts = append(ts, Table{ + Tag: t.tag(), + Offset: offset, + Length: length, + }) + offset += length + } + return ts +} + +// uncompressedSize computes the total uncompressed size +// of the tables in the table directory. +func (td TableDirectory) uncompressedSize() int64 { + var n int64 + for _, t := range td { + n += int64(t.length()) + } + return n +} + +// TableDirectoryEntry is a table directory entry. +type TableDirectoryEntry struct { + Flags uint8 // Table type and flags. + Tag *uint32 // 4-byte tag (optional). + OrigLength uint32 // Length of original table. + TransformLength *uint32 // Transformed length (optional). +} + +func (e TableDirectoryEntry) tag() uint32 { + switch e.Tag { + case nil: + return knownTableTags[e.Flags&0x3f] // Bits [0..5]. + default: + return *e.Tag + } +} + +func (e TableDirectoryEntry) transformVersion() uint8 { + return e.Flags >> 6 // Bits [6..7]. +} + +func (e TableDirectoryEntry) length() uint32 { + switch e.TransformLength { + case nil: + return e.OrigLength + default: + return *e.TransformLength + } +} + +// CollectionDirectory is an optional table containing the font fragment descriptions +// of font collection entries. +type CollectionDirectory struct { + Header CollectionHeader + Entries []CollectionFontEntry +} + +// CollectionHeader is a part of CollectionDirectory. +type CollectionHeader struct { + Version uint32 + NumFonts uint16 +} + +// CollectionFontEntry represents a CollectionFontEntry record. +type CollectionFontEntry struct { + NumTables uint16 // The number of tables in this font. + Flavor uint32 // The "sfnt version" of the font. + TableIndices []uint16 // The indicies identifying an entry in the Table Directory for each table in this font. +} + +func parseCollectionDirectory(r io.Reader, hdr Header) (*CollectionDirectory, error) { + // CollectionDirectory is present only if the input font is a collection. + if !hdr.IsCollection() { + return nil, nil + } + + var cd CollectionDirectory + err := readU32(r, &cd.Header.Version) + if err != nil { + return nil, err + } + err = read255UShort(r, &cd.Header.NumFonts) + if err != nil { + return nil, err + } + for i := 0; i < int(cd.Header.NumFonts); i++ { + var e CollectionFontEntry + + err := read255UShort(r, &e.NumTables) + if err != nil { + return nil, err + } + err = readU32(r, &e.Flavor) + if err != nil { + return nil, err + } + for j := 0; j < int(e.NumTables); j++ { + var tableIndex uint16 + err := read255UShort(r, &tableIndex) + if err != nil { + return nil, err + } + if tableIndex >= hdr.NumTables { + return nil, fmt.Errorf("parseCollectionDirectory: tableIndex >= hdr.NumTables") + } + e.TableIndices = append(e.TableIndices, tableIndex) + } + + cd.Entries = append(cd.Entries, e) + } + return &cd, nil +} + +func parseCompressedFontData(r io.Reader, hdr Header, td TableDirectory) ([]byte, error) { + // Compressed font data. + br, err := brotli.NewReader(io.LimitReader(r, int64(hdr.TotalCompressedSize)), nil) + //br, err := brotli.NewReader(&exactReader{R: r, N: int64(hdr.TotalCompressedSize)}, nil) + if err != nil { + return nil, err + } + var buf bytes.Buffer + n, err := io.Copy(&buf, br) + if err != nil { + return nil, fmt.Errorf("parseCompressedFontData: io.Copy: %v", err) + } + err = br.Close() + if err != nil { + return nil, fmt.Errorf("parseCompressedFontData: br.Close: %v", err) + } + if uncompressedSize := td.uncompressedSize(); n != uncompressedSize { + return nil, fmt.Errorf("parseCompressedFontData: unexpected size of uncompressed data: got %d, want %d", n, uncompressedSize) + } + return buf.Bytes(), nil +} + +// ExtendedMetadata is an optional block of extended metadata, +// represented in XML format and compressed for storage in the WOFF2 file. +type ExtendedMetadata struct{} + +func parseExtendedMetadata(r io.Reader, hdr Header) (*ExtendedMetadata, error) { + if hdr.MetaLength == 0 { + return nil, nil + } + return nil, fmt.Errorf("parseExtendedMetadata: not implemented") +} + +func parsePrivateData(r io.Reader, hdr Header) ([]byte, error) { + if hdr.PrivLength == 0 { + return nil, nil + } + return nil, fmt.Errorf("parsePrivateData: not implemented") +} + +// readU8 reads a UInt8 value. +func readU8(r io.Reader, v *uint8) error { + return binary.Read(r, order, v) +} + +// readU16 reads a UInt16 value. +func readU16(r io.Reader, v *uint16) error { + return binary.Read(r, order, v) +} + +// readU32 reads a UInt32 value. +func readU32(r io.Reader, v *uint32) error { + return binary.Read(r, order, v) +} + +// readBase128 reads a UIntBase128 value. +func readBase128(r io.Reader, v *uint32) error { + var accum uint32 + for i := 0; i < 5; i++ { + var data uint8 + err := binary.Read(r, order, &data) + if err != nil { + return err + } + + // Leading zeros are invalid. + if i == 0 && data == 0x80 { + return fmt.Errorf("leading zero is invalid") + } + + // If any of top 7 bits are set then accum << 7 would overflow. + if accum&0xfe000000 != 0 { + return fmt.Errorf("top seven bits are set, about to overflow") + } + + accum = (accum << 7) | uint32(data)&0x7f + + // Spin until most significant bit of data byte is false. + if (data & 0x80) == 0 { + *v = accum + return nil + } + } + return fmt.Errorf("UIntBase128 sequence exceeds 5 bytes") +} + +// read255UShort reads a 255UInt16 value. +func read255UShort(r io.Reader, v *uint16) error { + const ( + oneMoreByteCode1 = 255 + oneMoreByteCode2 = 254 + wordCode = 253 + lowestUCode = 253 + ) + var code uint8 + err := binary.Read(r, order, &code) + if err != nil { + return err + } + switch code { + case wordCode: + var value uint16 + err := binary.Read(r, order, &value) + if err != nil { + return err + } + *v = value + return nil + case oneMoreByteCode1: + var value uint8 + err := binary.Read(r, order, &value) + if err != nil { + return err + } + *v = uint16(value) + lowestUCode + return nil + case oneMoreByteCode2: + var value uint8 + err := binary.Read(r, order, &value) + if err != nil { + return err + } + *v = uint16(value) + lowestUCode*2 + return nil + default: + *v = uint16(code) + return nil + } +} + +// WOFF2 uses big endian encoding. +var order binary.ByteOrder = binary.BigEndian diff --git a/parse_test.go b/parse_test.go new file mode 100644 index 0000000..87545cf --- /dev/null +++ b/parse_test.go @@ -0,0 +1,109 @@ +package woff2_test + +import ( + "fmt" + "log" + + "dmitri.shuralyov.com/font/woff2" + "github.com/shurcooL/gofontwoff" +) + +func ExampleParse() { + f, err := gofontwoff.Assets.Open("/Go-Regular.woff2") + if err != nil { + log.Fatalln(err) + } + defer f.Close() + + font, err := woff2.Parse(f) + if err != nil { + log.Fatalln(err) + } + Dump(font) + + // Output: + // + // Signature: 0x774f4632 + // Flavor: 0x00010000 + // Length: 46132 + // NumTables: 14 + // Reserved: 0 + // TotalSfntSize: 140308 + // TotalCompressedSize: 46040 + // MajorVersion: 1 + // MinorVersion: 0 + // MetaOffset: 0 + // MetaLength: 0 + // MetaOrigLength: 0 + // PrivOffset: 0 + // PrivLength: 0 + // + // TableDirectory: 14 entries + // {Flags: 0x06, Tag: <nil>, OrigLength: 96, TransformLength: <nil>} + // {Flags: 0x00, Tag: <nil>, OrigLength: 1318, TransformLength: <nil>} + // {Flags: 0x08, Tag: <nil>, OrigLength: 176, TransformLength: <nil>} + // {Flags: 0x09, Tag: <nil>, OrigLength: 3437, TransformLength: <nil>} + // {Flags: 0x11, Tag: <nil>, OrigLength: 8, TransformLength: <nil>} + // {Flags: 0x0a, Tag: <nil>, OrigLength: 118912, TransformLength: 105020} + // {Flags: 0x0b, Tag: <nil>, OrigLength: 1334, TransformLength: 0} + // {Flags: 0x01, Tag: <nil>, OrigLength: 54, TransformLength: <nil>} + // {Flags: 0x02, Tag: <nil>, OrigLength: 36, TransformLength: <nil>} + // {Flags: 0x03, Tag: <nil>, OrigLength: 2662, TransformLength: <nil>} + // {Flags: 0x04, Tag: <nil>, OrigLength: 32, TransformLength: <nil>} + // {Flags: 0x05, Tag: <nil>, OrigLength: 6967, TransformLength: <nil>} + // {Flags: 0x07, Tag: <nil>, OrigLength: 4838, TransformLength: <nil>} + // {Flags: 0x0c, Tag: <nil>, OrigLength: 188, TransformLength: <nil>} + // + // CollectionDirectory: <nil> + // CompressedFontData: 124832 bytes (uncompressed size) + // ExtendedMetadata: <nil> + // PrivateData: [] +} + +func Dump(f woff2.File) { + dumpHeader(f.Header) + fmt.Println() + dumpTableDirectory(f.TableDirectory) + fmt.Println() + fmt.Println("CollectionDirectory:", f.CollectionDirectory) + fmt.Println("CompressedFontData:", len(f.CompressedFontData.Data), "bytes (uncompressed size)") + fmt.Println("ExtendedMetadata:", f.ExtendedMetadata) + fmt.Println("PrivateData:", f.PrivateData) +} + +func dumpHeader(hdr woff2.Header) { + fmt.Printf("Signature: %#08x\n", hdr.Signature) + fmt.Printf("Flavor: %#08x\n", hdr.Flavor) + fmt.Printf("Length: %d\n", hdr.Length) + fmt.Printf("NumTables: %d\n", hdr.NumTables) + fmt.Printf("Reserved: %d\n", hdr.Reserved) + fmt.Printf("TotalSfntSize: %d\n", hdr.TotalSfntSize) + fmt.Printf("TotalCompressedSize: %d\n", hdr.TotalCompressedSize) + fmt.Printf("MajorVersion: %d\n", hdr.MajorVersion) + fmt.Printf("MinorVersion: %d\n", hdr.MinorVersion) + fmt.Printf("MetaOffset: %d\n", hdr.MetaOffset) + fmt.Printf("MetaLength: %d\n", hdr.MetaLength) + fmt.Printf("MetaOrigLength: %d\n", hdr.MetaOrigLength) + fmt.Printf("PrivOffset: %d\n", hdr.PrivOffset) + fmt.Printf("PrivLength: %d\n", hdr.PrivLength) +} + +func dumpTableDirectory(td woff2.TableDirectory) { + fmt.Println("TableDirectory:", len(td), "entries") + for _, t := range td { + fmt.Printf("\t{") + fmt.Printf("Flags: %#02x, ", t.Flags) + if t.Tag != nil { + fmt.Printf("Tag: %v, ", *t.Tag) + } else { + fmt.Printf("Tag: <nil>, ") + } + fmt.Printf("OrigLength: %v, ", t.OrigLength) + if t.TransformLength != nil { + fmt.Printf("TransformLength: %v", *t.TransformLength) + } else { + fmt.Printf("TransformLength: <nil>") + } + fmt.Printf("}\n") + } +} diff --git a/tags.go b/tags.go new file mode 100644 index 0000000..3c13a55 --- /dev/null +++ b/tags.go @@ -0,0 +1,79 @@ +package woff2 + +const ( + // signature is the WOFF 2.0 file identifying signature 'wOF2'. + signature = uint32('w'<<24 | 'O'<<16 | 'F'<<8 | '2') + + // ttcfFlavor is the TrueType Collection flavor 'ttcf'. + ttcfFlavor = uint32('t'<<24 | 't'<<16 | 'c'<<8 | 'f') + + glyfTable = uint32('g'<<24 | 'l'<<16 | 'y'<<8 | 'f') + locaTable = uint32('l'<<24 | 'o'<<16 | 'c'<<8 | 'a') +) + +// knownTableTags is the "Known Table Tags" table. +var knownTableTags = [...]uint32{ + 0: uint32('c'<<24 | 'm'<<16 | 'a'<<8 | 'p'), + 1: uint32('h'<<24 | 'e'<<16 | 'a'<<8 | 'd'), + 2: uint32('h'<<24 | 'h'<<16 | 'e'<<8 | 'a'), + 3: uint32('h'<<24 | 'm'<<16 | 't'<<8 | 'x'), + 4: uint32('m'<<24 | 'a'<<16 | 'x'<<8 | 'p'), + 5: uint32('n'<<24 | 'a'<<16 | 'm'<<8 | 'e'), + 6: uint32('O'<<24 | 'S'<<16 | '/'<<8 | '2'), + 7: uint32('p'<<24 | 'o'<<16 | 's'<<8 | 't'), + 8: uint32('c'<<24 | 'v'<<16 | 't'<<8 | ' '), + 9: uint32('f'<<24 | 'p'<<16 | 'g'<<8 | 'm'), + 10: uint32('g'<<24 | 'l'<<16 | 'y'<<8 | 'f'), + 11: uint32('l'<<24 | 'o'<<16 | 'c'<<8 | 'a'), + 12: uint32('p'<<24 | 'r'<<16 | 'e'<<8 | 'p'), + 13: uint32('C'<<24 | 'F'<<16 | 'F'<<8 | ' '), + 14: uint32('V'<<24 | 'O'<<16 | 'R'<<8 | 'G'), + 15: uint32('E'<<24 | 'B'<<16 | 'D'<<8 | 'T'), + 16: uint32('E'<<24 | 'B'<<16 | 'L'<<8 | 'C'), + 17: uint32('g'<<24 | 'a'<<16 | 's'<<8 | 'p'), + 18: uint32('h'<<24 | 'd'<<16 | 'm'<<8 | 'x'), + 19: uint32('k'<<24 | 'e'<<16 | 'r'<<8 | 'n'), + 20: uint32('L'<<24 | 'T'<<16 | 'S'<<8 | 'H'), + 21: uint32('P'<<24 | 'C'<<16 | 'L'<<8 | 'T'), + 22: uint32('V'<<24 | 'D'<<16 | 'M'<<8 | 'X'), + 23: uint32('v'<<24 | 'h'<<16 | 'e'<<8 | 'a'), + 24: uint32('v'<<24 | 'm'<<16 | 't'<<8 | 'x'), + 25: uint32('B'<<24 | 'A'<<16 | 'S'<<8 | 'E'), + 26: uint32('G'<<24 | 'D'<<16 | 'E'<<8 | 'F'), + 27: uint32('G'<<24 | 'P'<<16 | 'O'<<8 | 'S'), + 28: uint32('G'<<24 | 'S'<<16 | 'U'<<8 | 'B'), + 29: uint32('E'<<24 | 'B'<<16 | 'S'<<8 | 'C'), + 30: uint32('J'<<24 | 'S'<<16 | 'T'<<8 | 'F'), + 31: uint32('M'<<24 | 'A'<<16 | 'T'<<8 | 'H'), + 32: uint32('C'<<24 | 'B'<<16 | 'D'<<8 | 'T'), + 33: uint32('C'<<24 | 'B'<<16 | 'L'<<8 | 'C'), + 34: uint32('C'<<24 | 'O'<<16 | 'L'<<8 | 'R'), + 35: uint32('C'<<24 | 'P'<<16 | 'A'<<8 | 'L'), + 36: uint32('S'<<24 | 'V'<<16 | 'G'<<8 | ' '), + 37: uint32('s'<<24 | 'b'<<16 | 'i'<<8 | 'x'), + 38: uint32('a'<<24 | 'c'<<16 | 'n'<<8 | 't'), + 39: uint32('a'<<24 | 'v'<<16 | 'a'<<8 | 'r'), + 40: uint32('b'<<24 | 'd'<<16 | 'a'<<8 | 't'), + 41: uint32('b'<<24 | 'l'<<16 | 'o'<<8 | 'c'), + 42: uint32('b'<<24 | 's'<<16 | 'l'<<8 | 'n'), + 43: uint32('c'<<24 | 'v'<<16 | 'a'<<8 | 'r'), + 44: uint32('f'<<24 | 'd'<<16 | 's'<<8 | 'c'), + 45: uint32('f'<<24 | 'e'<<16 | 'a'<<8 | 't'), + 46: uint32('f'<<24 | 'm'<<16 | 't'<<8 | 'x'), + 47: uint32('f'<<24 | 'v'<<16 | 'a'<<8 | 'r'), + 48: uint32('g'<<24 | 'v'<<16 | 'a'<<8 | 'r'), + 49: uint32('h'<<24 | 's'<<16 | 't'<<8 | 'y'), + 50: uint32('j'<<24 | 'u'<<16 | 's'<<8 | 't'), + 51: uint32('l'<<24 | 'c'<<16 | 'a'<<8 | 'r'), + 52: uint32('m'<<24 | 'o'<<16 | 'r'<<8 | 't'), + 53: uint32('m'<<24 | 'o'<<16 | 'r'<<8 | 'x'), + 54: uint32('o'<<24 | 'p'<<16 | 'b'<<8 | 'd'), + 55: uint32('p'<<24 | 'r'<<16 | 'o'<<8 | 'p'), + 56: uint32('t'<<24 | 'r'<<16 | 'a'<<8 | 'k'), + 57: uint32('Z'<<24 | 'a'<<16 | 'p'<<8 | 'f'), + 58: uint32('S'<<24 | 'i'<<16 | 'l'<<8 | 'f'), + 59: uint32('G'<<24 | 'l'<<16 | 'a'<<8 | 't'), + 60: uint32('G'<<24 | 'l'<<16 | 'o'<<8 | 'c'), + 61: uint32('F'<<24 | 'e'<<16 | 'a'<<8 | 't'), + 62: uint32('S'<<24 | 'i'<<16 | 'l'<<8 | 'l'), +} diff --git a/tags_test.go b/tags_test.go new file mode 100644 index 0000000..19eab67 --- /dev/null +++ b/tags_test.go @@ -0,0 +1,12 @@ +package woff2 + +import ( + "testing" +) + +func TestKnownTableTagsLength(t *testing.T) { + const want = 63 + if got := len(knownTableTags); got != want { + t.Errorf("got len(knownTableTags): %v, want: %v", got, want) + } +} `