Skip to content

Commit

Permalink
Add support for parsing WOFF2 fonts.
Browse files Browse the repository at this point in the history
This change uses the font/woff2 package for parsing WOFF2 font files.

Update all parts of the documentation, usage, etc., to say that WOFF2
fonts are now supported.

Add test .woff2 file to testdata and run smoke test on it.

Add benchmarks for parsing WOFF2 font files.

Fix minor Go style and documentation issues.

Resolves #1.
  • Loading branch information
dmitshur committed Feb 7, 2018
1 parent 1123599 commit 6d36219
Show file tree
Hide file tree
Showing 12 changed files with 86 additions and 37 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
A collection of Go packages for parsing and encoding OpenType fonts.

The main contribution of this repository is the [SFNT](https://godoc.org/github.com/ConradIrwin/font/sfnt) library which provides support for parsing OpenType, TrueType and wOFF fonts.
The main contribution of this repository is the [SFNT](https://godoc.org/github.com/ConradIrwin/font/sfnt) library which provides support for parsing OpenType, TrueType, wOFF, and WOFF2 fonts.

Also included is a utility called `font` that can do various useful things with fonts:

Expand Down Expand Up @@ -29,12 +29,12 @@ font stats ~/Downloads/Fanwood.ttf
TODO
====

Still missing is support for parsing EOT files (which should be easy to add) and for parsing wOFF2 files (which might be more time consuming, as that uses custom compression algorithm). Also support for generating wOFF files (which is annoyingly fiddly due to the checksum calculation), and a whole load of code around dealing with the hundreds of other SFNT table formats.
Still missing is support for parsing EOT files (which should be easy to add). Also support for generating wOFF files (which is annoyingly fiddly due to the checksum calculation) and WOFF2 files (needs a Brotli encoder), and a whole load of code around dealing with the hundreds of other SFNT table formats.

Font file formats
=================

On the web there are four main types of font file, TrueType, OpenType, wOFF, and EOT. They all represent the same SFNT information, but are encoded slightly differently. You may also come across SVG fonts, which are a totally different beast.
On the web there are four main types of font file, TrueType, OpenType, wOFF, WOFF2, and EOT. They all represent the same SFNT information, but are encoded slightly differently. You may also come across SVG fonts, which are a totally different beast.

Inside one of these files, there are two main types of glyphs, TrueType and
OpenType (also known as PostScript Type 2, or CFF). There are also a series of supporting
Expand Down
2 changes: 1 addition & 1 deletion cmd/font/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

func usage() {
fmt.Println(`
Usage: font [features|info|metrics|scrub|stats] font.[otf,ttf,woff]
Usage: font [features|info|metrics|scrub|stats] font.[otf,ttf,woff,woff2]
features: prints the gpos/gsub tables (contains font features)
info: prints the name table (contains metadata)
Expand Down
7 changes: 7 additions & 0 deletions sfnt/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Package sfnt provides support for sfnt based font formats.
//
// This includes OpenType, TrueType, wOFF, WOFF2, and EOT (though EOT is currently unimplemented).
//
// Usually you will want to parse a font, make modifications, and then output the modified
// font. If you're really brave, you can build a new font from scratch.
package sfnt
24 changes: 13 additions & 11 deletions sfnt/font.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ var ErrUnsupportedFormat = errors.New("unsupported font format")
var ErrMissingTable = errors.New("missing table")

// Font represents a SFNT font, which is the underlying representation found
// in .otf and .ttf files (and .woff and .eot files)
// in .otf and .ttf files (and .woff, .woff2, .eot files)
// SFNT is a container format, which contains a number of tables identified by
// Tags. Depending on the type of glyphs embedded in the file which tables will
// exist. In particular, there's a big different between TrueType glyphs (usually .ttf)
Expand All @@ -50,8 +50,8 @@ type tableSection struct {
table Table

offset uint32 // Offset into the file this table starts.
length uint32 // (Uncompressed) Length of this table
zLength uint32 // Compressed length of this table
length uint32 // Uncompressed length of this table.
zLength uint32 // Compressed (using zlib) length of this table, or 0 if uncompressed.
}

// Tags is the list of tags that are defined in this font, sorted by numeric value.
Expand Down Expand Up @@ -202,7 +202,7 @@ type File interface {
Seek(int64, int) (int64, error)
}

// Parse parses an OpenType, TrueType or wOFF File and returns a Font.
// Parse parses an OpenType, TrueType, wOFF, or WOFF2 file and returns a Font.
// If parsing fails, an error is returned and *Font will be nil.
func Parse(file File) (*Font, error) {
var magic Tag
Expand All @@ -212,17 +212,19 @@ func Parse(file File) (*Font, error) {

file.Seek(0, 0)

if magic == SignatureWoff {
return parseWoff(file)
}
if magic == TypeTrueType || magic == TypeOpenType || magic == TypePostScript1 || magic == TypeAppleTrueType {
switch magic {
case SignatureWOFF:
return parseWOFF(file)
case SignatureWOFF2:
return parseWOFF2(file)
case TypeTrueType, TypeOpenType, TypePostScript1, TypeAppleTrueType:
return parseOTF(file)
default:
return nil, ErrUnsupportedFormat
}

return nil, ErrUnsupportedFormat
}

// Parse parses an OpenType, TrueType or wOFF File and returns a Font.
// StrictParse parses an OpenType, TrueType, wOFF or WOFF2 file and returns a Font.
// Each table will be fully parsed and an error is returned if any fail.
func StrictParse(file File) (*Font, error) {
font, err := Parse(file)
Expand Down
29 changes: 20 additions & 9 deletions sfnt/font_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func TestSmokeTest(t *testing.T) {
{filename: "Roboto-BoldItalic.ttf"},
{filename: "Raleway-v4020-Regular.otf"},
{filename: "open-sans-v15-latin-regular.woff"},
{filename: "Go-Regular.woff2"},
}

for _, test := range tests {
Expand All @@ -44,13 +45,15 @@ func TestSmokeTest(t *testing.T) {

// benchmarkParse tests the performance of a simple Parse.
// Example run:
// go test -cpuprofile cpu.prof -bench . -run=^$ -benchtime=30s
// go test -cpuprofile cpu.prof -bench . -benchmem -run=^$ -benchtime=30s
// go tool pprof cpu.prof
//
// BenchmarkParseOtf-8 5000000 2784 ns/op 1440 B/op 33 allocs/op
// BenchmarkStrictParseOtf-8 100000 185088 ns/op 372422 B/op 1615 allocs/op
// BenchmarkParseWoff-8 5000000 3573 ns/op 2005 B/op 41 allocs/op
// BenchmarkStrictParseWoff-8 20000 615948 ns/op 543514 B/op 484 allocs/op
// BenchmarkParseOTF-8 1000000 10524 ns/op 2039 B/op 106 allocs/op
// BenchmarkStrictParseOTF-8 50000 302520 ns/op 934513 B/op 1744 allocs/op
// BenchmarkParseWOFF-8 1000000 16108 ns/op 3835 B/op 161 allocs/op
// BenchmarkStrictParseWOFF-8 20000 722079 ns/op 637524 B/op 645 allocs/op
// BenchmarkParseWOFF2-8 10000 1943778 ns/op 742968 B/op 456 allocs/op
// BenchmarkStrictParseWOFF2-8 10000 2098681 ns/op 1076108 B/op 852 allocs/op
func benchmarkParse(b *testing.B, filename string) {
buf, err := ioutil.ReadFile(filepath.Join("testdata", filename))
if err != nil {
Expand Down Expand Up @@ -82,18 +85,26 @@ func benchmarkStrictParse(b *testing.B, filename string) {
}
}

func BenchmarkParseOtf(b *testing.B) {
func BenchmarkParseOTF(b *testing.B) {
benchmarkParse(b, "Roboto-BoldItalic.ttf")
}

func BenchmarkStrictParseOtf(b *testing.B) {
func BenchmarkStrictParseOTF(b *testing.B) {
benchmarkStrictParse(b, "Roboto-BoldItalic.ttf")
}

func BenchmarkParseWoff(b *testing.B) {
func BenchmarkParseWOFF(b *testing.B) {
benchmarkParse(b, "open-sans-v15-latin-regular.woff")
}

func BenchmarkStrictParseWoff(b *testing.B) {
func BenchmarkStrictParseWOFF(b *testing.B) {
benchmarkStrictParse(b, "open-sans-v15-latin-regular.woff")
}

func BenchmarkParseWOFF2(b *testing.B) {
benchmarkParse(b, "Go-Regular.woff2")
}

func BenchmarkStrictParseWOFF2(b *testing.B) {
benchmarkStrictParse(b, "Go-Regular.woff2")
}
8 changes: 1 addition & 7 deletions sfnt/parse_otf.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
// Package sfnt provides support for sfnt based font formats.
//
// This includes OpenType, TrueType, wOFF and EOT (though EOT is currently unimplemented).
//
// Usually you will want to Parse a font, make modifications, and then output the modified
// font. If you're really brave, you can build a New Font from scratch.
package sfnt

import (
Expand All @@ -23,7 +17,7 @@ type otfHeader struct {
const otfHeaderLength = 12
const directoryEntryLength = 16

func newOtfHeader(scalerType Tag, numTables uint16) *otfHeader {
func newOTFHeader(scalerType Tag, numTables uint16) *otfHeader {

// http://www.opensource.apple.com/source/ICU/ICU-491.11.3/icuSources/layout/KernTable.cpp?txt
entrySelector := uint16(math.Logb(float64(numTables)))
Expand Down
2 changes: 1 addition & 1 deletion sfnt/parse_woff.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type woffEntry struct {
OrigChecksum uint32
}

func parseWoff(file File) (*Font, error) {
func parseWOFF(file File) (*Font, error) {
var header woffHeader
if err := binary.Read(file, binary.BigEndian, &header); err != nil {
return nil, err
Expand Down
28 changes: 28 additions & 0 deletions sfnt/parse_woff2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package sfnt

import (
"bytes"

"dmitri.shuralyov.com/font/woff2"
)

func parseWOFF2(file File) (*Font, error) {
f, err := woff2.Parse(file)
if err != nil {
return nil, err
}
font := &Font{
file: bytes.NewReader(f.FontData),
scalerType: Tag{f.Header.Flavor},
tables: make(map[Tag]tableSection, f.Header.NumTables),
}
for _, t := range f.TableDirectory.Tables() {
tag := Tag{t.Tag}
font.tables[tag] = tableSection{
tag: tag,
offset: uint32(t.Offset),
length: uint32(t.Length),
}
}
return font, nil
}
7 changes: 5 additions & 2 deletions sfnt/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ var (
// as specified by OpenType
TypeOpenType = MustNamedTag("OTTO")

// SignatureWoff if the magic number at the start of a wOFF file.
SignatureWoff = MustNamedTag("wOFF")
// SignatureWOFF if the magic number at the start of a WOFF file.
SignatureWOFF = MustNamedTag("wOFF")

// SignatureWOFF2 if the magic number at the start of a WOFF2 file.
SignatureWOFF2 = MustNamedTag("wOF2")
)

// Tag represents an open-type table name.
Expand Down
Binary file added sfnt/testdata/Go-Regular.woff2
Binary file not shown.
8 changes: 6 additions & 2 deletions sfnt/testdata/attribution
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Raleway - Raleway-v4020-Regular.otf
SIL Open Font License, 1.1
Copyright (c) 2010 - 2013, Matt McInerney ([email protected]), Pablo Impallari ([email protected]), Rodrigo Fuenzalida ([email protected]) with Reserved Font Name "Raleway"

Roboto - Roboto-BoldItalic.ttf
Roboto - Roboto-BoldItalic.ttf
Apache License, version 2.0
Copyright 2011 Google Inc. All Rights Reserved.
Copyright 2011 Google Inc. All Rights Reserved.

Go-Regular.woff2
BSD 3-Clause License
Copyright (c) 2016 Bigelow & Holmes Inc.. All rights reserved.
2 changes: 1 addition & 1 deletion sfnt/write_otf.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (font *Font) WriteOTF(w io.Writer) (n int, err error) {

headTable.ClearExpectedChecksum()

header := newOtfHeader(font.scalerType, uint16(len(todo)))
header := newOTFHeader(font.scalerType, uint16(len(todo)))

fragments := make([][]byte, len(todo))

Expand Down

0 comments on commit 6d36219

Please sign in to comment.