dmitri.shuralyov.com/service/change/...

Start on a fs service implementation; use mock data.
dmitshur committed 6 years ago commit bc830c6484e477da14b5ac3e373319eb4468785b
Collapse all
fs/fs.go
@@ -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)
+	}
+}
`