diff --git a/font/sfnt/data.go b/font/sfnt/data.go new file mode 100644 index 0000000..ad0c139 --- /dev/null +++ b/font/sfnt/data.go @@ -0,0 +1,68 @@ +// generated by go run gen.go; DO NOT EDIT + +package sfnt + +const numBuiltInPostNames = 258 + +const builtInPostNamesData = "" + + ".notdef.nullnonmarkingreturnspaceexclamquotedblnumbersigndollarp" + + "ercentampersandquotesingleparenleftparenrightasteriskpluscommahy" + + "phenperiodslashzeroonetwothreefourfivesixseveneightninecolonsemi" + + "colonlessequalgreaterquestionatABCDEFGHIJKLMNOPQRSTUVWXYZbracket" + + "leftbackslashbracketrightasciicircumunderscoregraveabcdefghijklm" + + "nopqrstuvwxyzbraceleftbarbracerightasciitildeAdieresisAringCcedi" + + "llaEacuteNtildeOdieresisUdieresisaacuteagraveacircumflexadieresi" + + "satildearingccedillaeacuteegraveecircumflexedieresisiacuteigrave" + + "icircumflexidieresisntildeoacuteograveocircumflexodieresisotilde" + + "uacuteugraveucircumflexudieresisdaggerdegreecentsterlingsectionb" + + "ulletparagraphgermandblsregisteredcopyrighttrademarkacutedieresi" + + "snotequalAEOslashinfinityplusminuslessequalgreaterequalyenmupart" + + "ialdiffsummationproductpiintegralordfeminineordmasculineOmegaaeo" + + "slashquestiondownexclamdownlogicalnotradicalflorinapproxequalDel" + + "taguillemotleftguillemotrightellipsisnonbreakingspaceAgraveAtild" + + "eOtildeOEoeendashemdashquotedblleftquotedblrightquoteleftquoteri" + + "ghtdividelozengeydieresisYdieresisfractioncurrencyguilsinglleftg" + + "uilsinglrightfifldaggerdblperiodcenteredquotesinglbasequotedblba" + + "seperthousandAcircumflexEcircumflexAacuteEdieresisEgraveIacuteIc" + + "ircumflexIdieresisIgraveOacuteOcircumflexappleOgraveUacuteUcircu" + + "mflexUgravedotlessicircumflextildemacronbrevedotaccentringcedill" + + "ahungarumlautogonekcaronLslashlslashScaronscaronZcaronzcaronbrok" + + "enbarEthethYacuteyacuteThornthornminusmultiplyonesuperiortwosupe" + + "riorthreesuperioronehalfonequarterthreequartersfrancGbrevegbreve" + + "IdotaccentScedillascedillaCacutecacuteCcaronccarondcroat" + +var builtInPostNamesOffsets = [...]uint16{ + 0x0000, 0x0007, 0x000c, 0x001c, 0x0021, 0x0027, 0x002f, 0x0039, + 0x003f, 0x0046, 0x004f, 0x005a, 0x0063, 0x006d, 0x0075, 0x0079, + 0x007e, 0x0084, 0x008a, 0x008f, 0x0093, 0x0096, 0x0099, 0x009e, + 0x00a2, 0x00a6, 0x00a9, 0x00ae, 0x00b3, 0x00b7, 0x00bc, 0x00c5, + 0x00c9, 0x00ce, 0x00d5, 0x00dd, 0x00df, 0x00e0, 0x00e1, 0x00e2, + 0x00e3, 0x00e4, 0x00e5, 0x00e6, 0x00e7, 0x00e8, 0x00e9, 0x00ea, + 0x00eb, 0x00ec, 0x00ed, 0x00ee, 0x00ef, 0x00f0, 0x00f1, 0x00f2, + 0x00f3, 0x00f4, 0x00f5, 0x00f6, 0x00f7, 0x00f8, 0x00f9, 0x0104, + 0x010d, 0x0119, 0x0124, 0x012e, 0x0133, 0x0134, 0x0135, 0x0136, + 0x0137, 0x0138, 0x0139, 0x013a, 0x013b, 0x013c, 0x013d, 0x013e, + 0x013f, 0x0140, 0x0141, 0x0142, 0x0143, 0x0144, 0x0145, 0x0146, + 0x0147, 0x0148, 0x0149, 0x014a, 0x014b, 0x014c, 0x014d, 0x0156, + 0x0159, 0x0163, 0x016d, 0x0176, 0x017b, 0x0183, 0x0189, 0x018f, + 0x0198, 0x01a1, 0x01a7, 0x01ad, 0x01b8, 0x01c1, 0x01c7, 0x01cc, + 0x01d4, 0x01da, 0x01e0, 0x01eb, 0x01f4, 0x01fa, 0x0200, 0x020b, + 0x0214, 0x021a, 0x0220, 0x0226, 0x0231, 0x023a, 0x0240, 0x0246, + 0x024c, 0x0257, 0x0260, 0x0266, 0x026c, 0x0270, 0x0278, 0x027f, + 0x0285, 0x028e, 0x0298, 0x02a2, 0x02ab, 0x02b4, 0x02b9, 0x02c1, + 0x02c9, 0x02cb, 0x02d1, 0x02d9, 0x02e2, 0x02eb, 0x02f7, 0x02fa, + 0x02fc, 0x0307, 0x0310, 0x0317, 0x0319, 0x0321, 0x032c, 0x0338, + 0x033d, 0x033f, 0x0345, 0x0351, 0x035b, 0x0365, 0x036c, 0x0372, + 0x037d, 0x0382, 0x038f, 0x039d, 0x03a5, 0x03b5, 0x03bb, 0x03c1, + 0x03c7, 0x03c9, 0x03cb, 0x03d1, 0x03d7, 0x03e3, 0x03f0, 0x03f9, + 0x0403, 0x0409, 0x0410, 0x0419, 0x0422, 0x042a, 0x0432, 0x043f, + 0x044d, 0x044f, 0x0451, 0x045a, 0x0468, 0x0476, 0x0482, 0x048d, + 0x0498, 0x04a3, 0x04a9, 0x04b2, 0x04b8, 0x04be, 0x04c9, 0x04d2, + 0x04d8, 0x04de, 0x04e9, 0x04ee, 0x04f4, 0x04fa, 0x0505, 0x050b, + 0x0513, 0x051d, 0x0522, 0x0528, 0x052d, 0x0536, 0x053a, 0x0541, + 0x054d, 0x0553, 0x0558, 0x055e, 0x0564, 0x056a, 0x0570, 0x0576, + 0x057c, 0x0585, 0x0588, 0x058b, 0x0591, 0x0597, 0x059c, 0x05a1, + 0x05a6, 0x05ae, 0x05b9, 0x05c4, 0x05d1, 0x05d8, 0x05e2, 0x05ef, + 0x05f4, 0x05fa, 0x0600, 0x060a, 0x0612, 0x061a, 0x0620, 0x0626, + 0x062c, 0x0632, 0x0638, +} diff --git a/font/sfnt/gen.go b/font/sfnt/gen.go new file mode 100644 index 0000000..12587d4 --- /dev/null +++ b/font/sfnt/gen.go @@ -0,0 +1,321 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ignore + +package main + +import ( + "bytes" + "fmt" + "go/format" + "io/ioutil" + "log" +) + +func main() { + data, offsets := []byte(nil), []int{0} + for _, name := range names { + data = append(data, name...) + offsets = append(offsets, len(data)) + } + + b := new(bytes.Buffer) + fmt.Fprintf(b, "// generated by go run gen.go; DO NOT EDIT\n\n") + fmt.Fprintf(b, "package sfnt\n\n") + + fmt.Fprintf(b, "const numBuiltInPostNames = %d\n\n", len(names)) + + fmt.Fprintf(b, "const builtInPostNamesData = \"\" +\n") + for s := data; ; { + if len(s) <= 64 { + fmt.Fprintf(b, "%q\n", s) + break + } + fmt.Fprintf(b, "%q +\n", s[:64]) + s = s[64:] + } + fmt.Fprintf(b, "\n") + + fmt.Fprintf(b, "var builtInPostNamesOffsets = [...]uint16{\n") + for i, o := range offsets { + fmt.Fprintf(b, "%#04x,", o) + if i%8 == 7 { + fmt.Fprintf(b, "\n") + } + } + fmt.Fprintf(b, "\n}\n") + + dstUnformatted := b.Bytes() + dst, err := format.Source(dstUnformatted) + if err != nil { + log.Fatalf("format.Source: %v\n\n----\n%s\n----", err, dstUnformatted) + } + if err := ioutil.WriteFile("data.go", dst, 0666); err != nil { + log.Fatalf("ioutil.WriteFile: %v", err) + } +} + +// names is the built-in post table names listed at +// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6post.html +var names = [258]string{ + ".notdef", + ".null", + "nonmarkingreturn", + "space", + "exclam", + "quotedbl", + "numbersign", + "dollar", + "percent", + "ampersand", + "quotesingle", + "parenleft", + "parenright", + "asterisk", + "plus", + "comma", + "hyphen", + "period", + "slash", + "zero", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "colon", + "semicolon", + "less", + "equal", + "greater", + "question", + "at", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "bracketleft", + "backslash", + "bracketright", + "asciicircum", + "underscore", + "grave", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "braceleft", + "bar", + "braceright", + "asciitilde", + "Adieresis", + "Aring", + "Ccedilla", + "Eacute", + "Ntilde", + "Odieresis", + "Udieresis", + "aacute", + "agrave", + "acircumflex", + "adieresis", + "atilde", + "aring", + "ccedilla", + "eacute", + "egrave", + "ecircumflex", + "edieresis", + "iacute", + "igrave", + "icircumflex", + "idieresis", + "ntilde", + "oacute", + "ograve", + "ocircumflex", + "odieresis", + "otilde", + "uacute", + "ugrave", + "ucircumflex", + "udieresis", + "dagger", + "degree", + "cent", + "sterling", + "section", + "bullet", + "paragraph", + "germandbls", + "registered", + "copyright", + "trademark", + "acute", + "dieresis", + "notequal", + "AE", + "Oslash", + "infinity", + "plusminus", + "lessequal", + "greaterequal", + "yen", + "mu", + "partialdiff", + "summation", + "product", + "pi", + "integral", + "ordfeminine", + "ordmasculine", + "Omega", + "ae", + "oslash", + "questiondown", + "exclamdown", + "logicalnot", + "radical", + "florin", + "approxequal", + "Delta", + "guillemotleft", + "guillemotright", + "ellipsis", + "nonbreakingspace", + "Agrave", + "Atilde", + "Otilde", + "OE", + "oe", + "endash", + "emdash", + "quotedblleft", + "quotedblright", + "quoteleft", + "quoteright", + "divide", + "lozenge", + "ydieresis", + "Ydieresis", + "fraction", + "currency", + "guilsinglleft", + "guilsinglright", + "fi", + "fl", + "daggerdbl", + "periodcentered", + "quotesinglbase", + "quotedblbase", + "perthousand", + "Acircumflex", + "Ecircumflex", + "Aacute", + "Edieresis", + "Egrave", + "Iacute", + "Icircumflex", + "Idieresis", + "Igrave", + "Oacute", + "Ocircumflex", + "apple", + "Ograve", + "Uacute", + "Ucircumflex", + "Ugrave", + "dotlessi", + "circumflex", + "tilde", + "macron", + "breve", + "dotaccent", + "ring", + "cedilla", + "hungarumlaut", + "ogonek", + "caron", + "Lslash", + "lslash", + "Scaron", + "scaron", + "Zcaron", + "zcaron", + "brokenbar", + "Eth", + "eth", + "Yacute", + "yacute", + "Thorn", + "thorn", + "minus", + "multiply", + "onesuperior", + "twosuperior", + "threesuperior", + "onehalf", + "onequarter", + "threequarters", + "franc", + "Gbreve", + "gbreve", + "Idotaccent", + "Scedilla", + "scedilla", + "Cacute", + "cacute", + "Ccaron", + "ccaron", + "dcroat", +} diff --git a/font/sfnt/sfnt.go b/font/sfnt/sfnt.go index b7f4bdc..995b287 100644 --- a/font/sfnt/sfnt.go +++ b/font/sfnt/sfnt.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:generate go run gen.go + // Package sfnt implements a decoder for SFNT font file formats, including // TrueType and OpenType. package sfnt // import "golang.org/x/image/font/sfnt" @@ -65,6 +67,7 @@ var ( errInvalidLocationData = errors.New("sfnt: invalid location data") errInvalidMaxpTable = errors.New("sfnt: invalid maxp table") errInvalidNameTable = errors.New("sfnt: invalid name table") + errInvalidPostTable = errors.New("sfnt: invalid post table") errInvalidSourceData = errors.New("sfnt: invalid source data") errInvalidTableOffset = errors.New("sfnt: invalid table offset") errInvalidTableTagOrder = errors.New("sfnt: invalid table tag order") @@ -80,6 +83,7 @@ var ( errUnsupportedNumberOfHints = errors.New("sfnt: unsupported number of hints") errUnsupportedNumberOfTables = errors.New("sfnt: unsupported number of tables") errUnsupportedPlatformEncoding = errors.New("sfnt: unsupported platform encoding") + errUnsupportedPostTable = errors.New("sfnt: unsupported post table") errUnsupportedTableOffsetLength = errors.New("sfnt: unsupported table offset or length") errUnsupportedType2Charstring = errors.New("sfnt: unsupported Type 2 Charstring") ) @@ -217,6 +221,20 @@ func (s *source) u16(buf []byte, t table, i int) (uint16, error) { return u16(buf), nil } +// u32 returns the uint32 in the table t at the relative offset i. +// +// buf is an optional scratch buffer as per the source.view method. +func (s *source) u32(buf []byte, t table, i int) (uint32, error) { + if i < 0 || uint(t.length) < uint(i+4) { + return 0, errInvalidBounds + } + buf, err := s.view(buf, int(t.offset)+i, 4) + if err != nil { + return 0, err + } + return u32(buf), nil +} + // table is a section of the font data. type table struct { offset, length uint32 @@ -298,6 +316,7 @@ type Font struct { glyphIndex func(f *Font, b *Buffer, r rune) (GlyphIndex, error) indexToLocFormat bool // false means short, true means long. isPostScript bool + postTableVersion uint32 unitsPerEm Units // The glyph data for the glyph index i is in @@ -320,6 +339,13 @@ func (f *Font) initialize() error { if err != nil { return err } + + // The order of these parseXxx calls matters. Later calls may depend on + // f.cached state set up by earlier calls, such as the number of glyphs in + // the font being parsed by parseMaxp. + + // TODO: make state dependencies explicit instead of implicit. + buf, err = f.parseHead(buf) if err != nil { return err @@ -332,6 +358,10 @@ func (f *Font) initialize() error { if err != nil { return err } + buf, err = f.parsePost(buf) + if err != nil { + return err + } return nil } @@ -538,6 +568,31 @@ func (f *Font) parseMaxp(buf []byte) ([]byte, error) { return buf, nil } +func (f *Font) parsePost(buf []byte) ([]byte, error) { + // https://www.microsoft.com/typography/otspec/post.htm + + const headerSize = 32 + if f.post.length < headerSize { + return nil, errInvalidPostTable + } + u, err := f.src.u32(buf, f.post, 0) + if err != nil { + return nil, err + } + switch u { + case 0x20000: + if f.post.length < headerSize+2+2*uint32(f.NumGlyphs()) { + return nil, errInvalidPostTable + } + case 0x30000: + // No-op. + default: + return nil, errUnsupportedPostTable + } + f.cached.postTableVersion = u + return buf, nil +} + // TODO: API for looking up glyph variants?? For example, some fonts may // provide both slashed and dotted zero glyphs ('0'), or regular and 'old // style' numerals, and users can direct software to choose a variant. @@ -606,6 +661,78 @@ func (f *Font) LoadGlyph(b *Buffer, x GlyphIndex, opts *LoadGlyphOptions) ([]Seg return b.segments, nil } +// GlyphName returns the name of the x'th glyph. +// +// Not every font contains glyph names. If not present, GlyphName will return +// ("", nil). +// +// If present, the glyph name, provided by the font, is assumed to follow the +// Adobe Glyph List Specification: +// https://github.com/adobe-type-tools/agl-specification/blob/master/README.md +// +// This is also known as the "Adobe Glyph Naming convention", the "Adobe +// document [for] Unicode and Glyph Names" or "PostScript glyph names". +// +// It returns ErrNotFound if the glyph index is out of range. +func (f *Font) GlyphName(b *Buffer, x GlyphIndex) (string, error) { + if int(x) >= f.NumGlyphs() { + return "", ErrNotFound + } + if f.cached.postTableVersion != 0x20000 { + return "", nil + } + if b == nil { + b = &Buffer{} + } + + // The wire format for a Version 2 post table is documented at: + // https://www.microsoft.com/typography/otspec/post.htm + const glyphNameIndexOffset = 34 + + buf, err := b.view(&f.src, int(f.post.offset)+glyphNameIndexOffset+2*int(x), 2) + if err != nil { + return "", err + } + u := u16(buf) + if u < numBuiltInPostNames { + i := builtInPostNamesOffsets[u+0] + j := builtInPostNamesOffsets[u+1] + return builtInPostNamesData[i:j], nil + } + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6post.html + // says that "32768 through 65535 are reserved for future use". + if u > 32767 { + return "", errUnsupportedPostTable + } + u -= numBuiltInPostNames + + // Iterate through the list of Pascal-formatted strings. A linear scan is + // clearly O(u), which isn't great (as the obvious loop, calling + // Font.GlyphName, to get all of the glyph names in a font has quadratic + // complexity), but the wire format doesn't suggest a better alternative. + + offset := glyphNameIndexOffset + 2*f.NumGlyphs() + buf, err = b.view(&f.src, int(f.post.offset)+offset, int(f.post.length)-offset) + if err != nil { + return "", err + } + + for { + if len(buf) == 0 { + return "", errInvalidPostTable + } + n := 1 + int(buf[0]) + if len(buf) < n { + return "", errInvalidPostTable + } + if u == 0 { + return string(buf[1:n]), nil + } + buf = buf[n:] + u-- + } +} + // Name returns the name value keyed by the given NameID. // // It returns ErrNotFound if there is no value for that key. diff --git a/font/sfnt/sfnt_test.go b/font/sfnt/sfnt_test.go index c6d6741..ca30321 100644 --- a/font/sfnt/sfnt_test.go +++ b/font/sfnt/sfnt_test.go @@ -431,3 +431,77 @@ loop: t.Errorf("Name:\ngot %q\nwant %q", name, want) } } + +func TestGlyphName(t *testing.T) { + f, err := Parse(goregular.TTF) + if err != nil { + t.Fatalf("Parse: %v", err) + } + + testCases := []struct { + r rune + want string + }{ + {'\x00', "NULL"}, + {'!', "exclam"}, + {'A', "A"}, + {'{', "braceleft"}, + {'\u00c4', "Adieresis"}, // U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS + {'\u2020', "dagger"}, // U+2020 DAGGER + {'\u2660', "spade"}, // U+2660 BLACK SPADE SUIT + {'\uf800', "gopher"}, // U+F800 + {'\ufffe', ".notdef"}, // Not in the Go Regular font, so GlyphIndex returns (0, nil). + } + + var b Buffer + for _, tc := range testCases { + x, err := f.GlyphIndex(&b, tc.r) + if err != nil { + t.Errorf("r=%q: GlyphIndex: %v", tc.r, err) + continue + } + got, err := f.GlyphName(&b, x) + if err != nil { + t.Errorf("r=%q: GlyphName: %v", tc.r, err) + continue + } + if got != tc.want { + t.Errorf("r=%q: got %q, want %q", tc.r, got, tc.want) + continue + } + } +} + +func TestBuiltInPostNames(t *testing.T) { + testCases := []struct { + x GlyphIndex + want string + }{ + {0, ".notdef"}, + {1, ".null"}, + {2, "nonmarkingreturn"}, + {13, "asterisk"}, + {36, "A"}, + {93, "z"}, + {123, "ocircumflex"}, + {202, "Edieresis"}, + {255, "Ccaron"}, + {256, "ccaron"}, + {257, "dcroat"}, + {258, ""}, + {999, ""}, + {0xffff, ""}, + } + + for _, tc := range testCases { + if tc.x >= numBuiltInPostNames { + continue + } + i := builtInPostNamesOffsets[tc.x+0] + j := builtInPostNamesOffsets[tc.x+1] + got := builtInPostNamesData[i:j] + if got != tc.want { + t.Errorf("x=%d: got %q, want %q", tc.x, got, tc.want) + } + } +}