// SPDX-License-Identifier: Unlicense OR BSD-3-Clause

// Package font provides an high level API to access // Opentype font properties. // See packages [opentype] and [opentype/tables] for a lower level, more detailled API.
package font import ( ot ) type ( // GID is used to identify glyphs in a font. // It is mostly internal to the font and should not be confused with // Unicode code points. // Note that, despite Opentype font files using uint16, we choose to use uint32, // to allow room for future extension. GID = ot.GID // Tag represents an open-type name. // These are technically uint32's, but are usually // displayed in ASCII as they are all acronyms. // See https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6.html#Overview Tag = ot.Tag // VarCoord stores font variation coordinates, // which are real numbers in [-1;1], stored as fixed 2.14 integer. VarCoord = tables.Coord // Resource is a combination of io.Reader, io.Seeker and io.ReaderAt. // This interface is satisfied by most things that you'd want // to parse, for example *os.File, io.SectionReader or *bytes.Reader. Resource = ot.Resource // GlyphExtents exposes extent values, measured in font units. // Note that height is negative in coordinate systems that grow up. GlyphExtents = ot.GlyphExtents ) // ParseTTF parse an Opentype font file (.otf, .ttf). // See ParseTTC for support for collections. func ( Resource) (*Face, error) { , := ot.NewLoader() if != nil { return nil, } , := NewFont() if != nil { return nil, } return NewFace(), nil } // ParseTTC parse an Opentype font file, with support for collections. // Single font files are supported, returning a slice with length 1. func ( Resource) ([]*Face, error) { , := ot.NewLoaders() if != nil { return nil, } := make([]*Face, len()) for , := range { , := NewFont() if != nil { return nil, fmt.Errorf("reading font %d of collection: %s", , ) } [] = NewFace() } return , nil } // EmptyGlyph represents an invisible glyph, which should not be drawn, // but whose advance and offsets should still be accounted for when rendering. const EmptyGlyph GID = math.MaxUint32 // FontExtents exposes font-wide extent values, measured in font units. // Note that typically ascender is positive and descender negative in coordinate systems that grow up. type FontExtents struct { Ascender float32 // Typographic ascender. Descender float32 // Typographic descender. LineGap float32 // Suggested line spacing gap. } // LineMetric identifies one metric about the font. type LineMetric uint8 const ( // Distance above the baseline of the top of the underline. // Since most fonts have underline positions beneath the baseline, this value is typically negative. UnderlinePosition LineMetric = iota // Suggested thickness to draw for the underline. UnderlineThickness // Distance above the baseline of the top of the strikethrough. StrikethroughPosition // Suggested thickness to draw for the strikethrough. StrikethroughThickness SuperscriptEmYSize SuperscriptEmXOffset SubscriptEmYSize SubscriptEmYOffset SubscriptEmXOffset CapHeight XHeight ) // FontID represents an identifier of a font (possibly in a collection), // and an optional variable instance. type FontID struct { File string // The filename or identifier of the font file. // The index of the face in a collection. It is always 0 for // single font files. Index uint16 // For variable fonts, stores 1 + the instance index. // It is set to 0 to ignore variations, or for non variable fonts. Instance uint16 } // Font represents one Opentype font file (or one sub font of a collection). // It is an educated view of the underlying font file, optimized for quick access // to information required by text layout engines. // // All its methods are read-only and a [*Font] object is thus safe for concurrent use. type Font struct { // Cmap is the 'cmap' table Cmap Cmap cmapVar UnicodeVariations hhea *tables.Hhea vhea *tables.Vhea vorg *tables.VORG // optional cff *cff.CFF // optional cff2 *cff.CFF2 // optional post post // optional svg svg // optional glyf tables.Glyf hmtx tables.Hmtx vmtx tables.Vmtx bitmap bitmap sbix sbix STAT *STAT // optional COLR *tables.COLR1 // color glyphs, optional CPAL CPAL // color glyphs, optional os2 os2 names tables.Name head tables.Head // Optional, only present in variable fonts fvar fvar // optional hvar *tables.HVAR // optional vvar *tables.VVAR // optional avar tables.Avar mvar mvar gvar gvar // Advanced layout tables. GDEF tables.GDEF // An absent table has a nil GlyphClassDef Trak tables.Trak Ankr tables.Ankr Feat tables.Feat Ltag tables.Ltag Morx Morx Kern Kernx Kerx Kernx GSUB GSUB // An absent table has a nil slice of lookups GPOS GPOS // An absent table has a nil slice of lookups upem uint16 // cached value nGlyphs int } // NewFont loads all the font tables, sanitizing them. // An error is returned only when required tables 'cmap', 'head', 'maxp' are invalid (or missing). // More control on errors is available by using package [tables]. func ( *ot.Loader) (*Font, error) { var ( Font error ) // 'cmap' handling depend on os2 , := .RawTable(ot.MustNewTag("OS/2")) , , := tables.ParseOs2() := .FontPage() .os2, _ = newOs2() , = .RawTable(ot.MustNewTag("cmap")) if != nil { return nil, } , , := tables.ParseCmap() if != nil { return nil, } .Cmap, .cmapVar, = ProcessCmap(, ) if != nil { return nil, } .head, _, = LoadHeadTable(, nil) if != nil { return nil, } , = .RawTable(ot.MustNewTag("maxp")) if != nil { return nil, } , , := tables.ParseMaxp() if != nil { return nil, } .nGlyphs = int(.NumGlyphs) // We considerer all the following tables as optional, // since, in practice, users won't have much control on the // font files they use // // Ignoring the errors on `RawTable` is OK : it will trigger an error on the next tables.ParseXXX, // which in turn will return a zero value , _ = .RawTable(ot.MustNewTag("fvar")) , , := tables.ParseFvar() .fvar = newFvar() , _ = .RawTable(ot.MustNewTag("avar")) .avar, _, _ = tables.ParseAvar() .upem = .head.Upem() , _ = .RawTable(ot.MustNewTag("glyf")) , := .RawTable(ot.MustNewTag("loca")) , := tables.ParseLoca(, .nGlyphs, .head.IndexToLocFormat == 1) if == nil { // ParseGlyf panics if len(loca) == 0 .glyf, _ = tables.ParseGlyf(, ) } .bitmap = selectBitmapTable() , _ = .RawTable(ot.MustNewTag("sbix")) , , := tables.ParseSbix(, .nGlyphs) .sbix = newSbix() .cff, _ = loadCff(, .nGlyphs) .cff2, _ = loadCff2(, .nGlyphs, len(.fvar)) , _ = .RawTable(ot.MustNewTag("post")) , , := tables.ParsePost() .post, _ = newPost() , _ = .RawTable(ot.MustNewTag("SVG ")) , , := tables.ParseSVG() .svg, _ = newSvg() , _ = .RawTable(ot.MustNewTag("COLR")) if , := tables.ParseCOLR(); == nil { .COLR = & // color table without CPAL is broken , _ = .RawTable(ot.MustNewTag("CPAL")) , , := tables.ParseCPAL() .CPAL, = newCPAL() if != nil { return nil, } } , _ = .RawTable(ot.MustNewTag("STAT")) , , := tables.ParseSTAT() if == nil { .STAT = & } .hhea, .hmtx, _ = loadHmtx(, .nGlyphs) .vhea, .vmtx, _ = loadVmtx(, .nGlyphs) if := len(.fvar); != 0 { , _ = .RawTable(ot.MustNewTag("MVAR")) , , := tables.ParseMVAR() .mvar, _ = newMvar(, ) , _ = .RawTable(ot.MustNewTag("gvar")) , , := tables.ParseGvar() .gvar, _ = newGvar(, .glyf) , _ = .RawTable(ot.MustNewTag("HVAR")) , , := tables.ParseHVAR() if == nil { .hvar = & } , _ = .RawTable(ot.MustNewTag("VVAR")) , , := tables.ParseVVAR() if == nil { .vvar = & } } , _ = .RawTable(ot.MustNewTag("VORG")) , , := tables.ParseVORG() if == nil { .vorg = & } , _ = .RawTable(ot.MustNewTag("name")) .names, _, _ = tables.ParseName() // layout tables , := .RawTable(ot.MustNewTag("GSUB")) , , := tables.ParseLayout() // harfbuzz relies on GSUB.Loookups being nil when the table is absent if == nil { .GSUB, _ = newGSUB() } , := .RawTable(ot.MustNewTag("GPOS")) , _, = tables.ParseLayout() // harfbuzz relies on GPOS.Loookups being nil when the table is absent if == nil { .GPOS, _ = newGPOS() } .GDEF, _ = loadGDEF(, len(.fvar), , ) , _ = .RawTable(ot.MustNewTag("morx")) , , := tables.ParseMorx(, .nGlyphs) .Morx = newMorx() , _ = .RawTable(ot.MustNewTag("kerx")) , , := tables.ParseKerx(, .nGlyphs) .Kerx = newKernxFromKerx() , _ = .RawTable(ot.MustNewTag("kern")) , , := tables.ParseKern() .Kern = newKernxFromKern() , _ = .RawTable(ot.MustNewTag("ankr")) .Ankr, _, _ = tables.ParseAnkr(, .nGlyphs) , _ = .RawTable(ot.MustNewTag("trak")) .Trak, _, _ = tables.ParseTrak() , _ = .RawTable(ot.MustNewTag("feat")) .Feat, _, _ = tables.ParseFeat() , _ = .RawTable(ot.MustNewTag("ltag")) .Ltag, _, _ = tables.ParseLtag() return &, nil } // see harfbuzz/src/hb-ot-layout.cc func isGDEFBlocklisted(, , []byte) bool { := uint64(len())<<42 | uint64(len())<<21 | uint64(len()) switch { /* sha1sum:c5ee92f0bca4bfb7d06c4d03e8cf9f9cf75d2e8a Windows 7? timesi.ttf */ case 442<<42 | 2874<<21 | 42038, /* sha1sum:37fc8c16a0894ab7b749e35579856c73c840867b Windows 7? timesbi.ttf */ 430<<42 | 2874<<21 | 40662, /* sha1sum:19fc45110ea6cd3cdd0a5faca256a3797a069a80 Windows 7 timesi.ttf */ 442<<42 | 2874<<21 | 39116, /* sha1sum:6d2d3c9ed5b7de87bc84eae0df95ee5232ecde26 Windows 7 timesbi.ttf */ 430<<42 | 2874<<21 | 39374, /* sha1sum:8583225a8b49667c077b3525333f84af08c6bcd8 OS X 10.11.3 Times New Roman Italic.ttf */ 490<<42 | 3046<<21 | 41638, /* sha1sum:ec0f5a8751845355b7c3271d11f9918a966cb8c9 OS X 10.11.3 Times New Roman Bold Italic.ttf */ 478<<42 | 3046<<21 | 41902, /* sha1sum:96eda93f7d33e79962451c6c39a6b51ee893ce8c tahoma.ttf from Windows 8 */ 898<<42 | 12554<<21 | 46470, /* sha1sum:20928dc06014e0cd120b6fc942d0c3b1a46ac2bc tahomabd.ttf from Windows 8 */ 910<<42 | 12566<<21 | 47732, /* sha1sum:4f95b7e4878f60fa3a39ca269618dfde9721a79e tahoma.ttf from Windows 8.1 */ 928<<42 | 23298<<21 | 59332, /* sha1sum:6d400781948517c3c0441ba42acb309584b73033 tahomabd.ttf from Windows 8.1 */ 940<<42 | 23310<<21 | 60732, /* tahoma.ttf v6.04 from Windows 8.1 x64, see https://bugzilla.mozilla.org/show_bug.cgi?id=1279925 */ 964<<42 | 23836<<21 | 60072, /* tahomabd.ttf v6.04 from Windows 8.1 x64, see https://bugzilla.mozilla.org/show_bug.cgi?id=1279925 */ 976<<42 | 23832<<21 | 61456, /* sha1sum:e55fa2dfe957a9f7ec26be516a0e30b0c925f846 tahoma.ttf from Windows 10 */ 994<<42 | 24474<<21 | 60336, /* sha1sum:7199385abb4c2cc81c83a151a7599b6368e92343 tahomabd.ttf from Windows 10 */ 1006<<42 | 24470<<21 | 61740, /* tahoma.ttf v6.91 from Windows 10 x64, see https://bugzilla.mozilla.org/show_bug.cgi?id=1279925 */ 1006<<42 | 24576<<21 | 61346, /* tahomabd.ttf v6.91 from Windows 10 x64, see https://bugzilla.mozilla.org/show_bug.cgi?id=1279925 */ 1018<<42 | 24572<<21 | 62828, /* sha1sum:b9c84d820c49850d3d27ec498be93955b82772b5 tahoma.ttf from Windows 10 AU */ 1006<<42 | 24576<<21 | 61352, /* sha1sum:2bdfaab28174bdadd2f3d4200a30a7ae31db79d2 tahomabd.ttf from Windows 10 AU */ 1018<<42 | 24572<<21 | 62834, /* sha1sum:b0d36cf5a2fbe746a3dd277bffc6756a820807a7 Tahoma.ttf from Mac OS X 10.9 */ 832<<42 | 7324<<21 | 47162, /* sha1sum:12fc4538e84d461771b30c18b5eb6bd434e30fba Tahoma Bold.ttf from Mac OS X 10.9 */ 844<<42 | 7302<<21 | 45474, /* sha1sum:eb8afadd28e9cf963e886b23a30b44ab4fd83acc himalaya.ttf from Windows 7 */ 180<<42 | 13054<<21 | 7254, /* sha1sum:73da7f025b238a3f737aa1fde22577a6370f77b0 himalaya.ttf from Windows 8 */ 192<<42 | 12638<<21 | 7254, /* sha1sum:6e80fd1c0b059bbee49272401583160dc1e6a427 himalaya.ttf from Windows 8.1 */ 192<<42 | 12690<<21 | 7254, /* 8d9267aea9cd2c852ecfb9f12a6e834bfaeafe44 cantarell-fonts-0.0.21/otf/Cantarell-Regular.otf */ /* 983988ff7b47439ab79aeaf9a45bd4a2c5b9d371 cantarell-fonts-0.0.21/otf/Cantarell-Oblique.otf */ 188<<42 | 248<<21 | 3852, /* 2c0c90c6f6087ffbfea76589c93113a9cbb0e75f cantarell-fonts-0.0.21/otf/Cantarell-Bold.otf */ /* 55461f5b853c6da88069ffcdf7f4dd3f8d7e3e6b cantarell-fonts-0.0.21/otf/Cantarell-Bold-Oblique.otf */ 188<<42 | 264<<21 | 3426, /* d125afa82a77a6475ac0e74e7c207914af84b37a padauk-2.80/Padauk.ttf RHEL 7.2 */ 1058<<42 | 47032<<21 | 11818, /* 0f7b80437227b90a577cc078c0216160ae61b031 padauk-2.80/Padauk-Bold.ttf RHEL 7.2*/ 1046<<42 | 47030<<21 | 12600, /* d3dde9aa0a6b7f8f6a89ef1002e9aaa11b882290 padauk-2.80/Padauk.ttf Ubuntu 16.04 */ 1058<<42 | 71796<<21 | 16770, /* 5f3c98ccccae8a953be2d122c1b3a77fd805093f padauk-2.80/Padauk-Bold.ttf Ubuntu 16.04 */ 1046<<42 | 71790<<21 | 17862, /* 6c93b63b64e8b2c93f5e824e78caca555dc887c7 padauk-2.80/Padauk-book.ttf */ 1046<<42 | 71788<<21 | 17112, /* d89b1664058359b8ec82e35d3531931125991fb9 padauk-2.80/Padauk-bookbold.ttf */ 1058<<42 | 71794<<21 | 17514, /* 824cfd193aaf6234b2b4dc0cf3c6ef576c0d00ef padauk-3.0/Padauk-book.ttf */ 1330<<42 | 109904<<21 | 57938, /* 91fcc10cf15e012d27571e075b3b4dfe31754a8a padauk-3.0/Padauk-bookbold.ttf */ 1330<<42 | 109904<<21 | 58972, /* sha1sum: c26e41d567ed821bed997e937bc0c41435689e85 Padauk.ttf * "Padauk Regular" "Version 2.5", see https://crbug.com/681813 */ 1004<<42 | 59092<<21 | 14836, /* 88d2006ca084f04af2df1954ed714a8c71e8400f Courier New.ttf from macOS 15 */ 588<<42 | 5078<<21 | 14418, /* 608e3ebb6dd1aee521cff08eb07d500a2c59df68 Courier New Bold.ttf from macOS 15 */ 588<<42 | 5078<<21 | 14238, /* d13221044ff054efd78f1cd8631b853c3ce85676 cour.ttf from Windows 10 */ 894<<42 | 17162<<21 | 33960, /* 68ed4a22d8067fcf1622ac6f6e2f4d3a2e3ec394 courbd.ttf from Windows 10 */ 894<<42 | 17154<<21 | 34472, /* 4cdb0259c96b7fd7c103821bb8f08f7cc6b211d7 cour.ttf from Windows 8.1 */ 816<<42 | 7868<<21 | 17052, /* 920483d8a8ed37f7f0afdabbe7f679aece7c75d8 courbd.ttf from Windows 8.1 */ 816<<42 | 7868<<21 | 17138: return true } return false } var bhedTag = ot.MustNewTag("bhed") // LoadHeadTable loads the 'head' or the 'bhed' table. // // If a 'bhed' Apple table is present, it replaces the 'head' one. // // [buffer] may be provided to reduce allocations; the returned [tables.Head] is guaranteed // not to retain any reference on [buffer]. // If [buffer] is nil or has not enough capacity, a new slice is allocated (and returned). func ( *ot.Loader, []byte) (tables.Head, []byte, error) { var error // check 'bhed' first if .HasTable(bhedTag) { , = .RawTableTo(bhedTag, ) } else { , = .RawTableTo(ot.MustNewTag("head"), ) } if != nil { return tables.Head{}, nil, errors.New("missing required head (or bhed) table") } , , := tables.ParseHead() return , , } // return nil if no table is valid (or present) func selectBitmapTable( *ot.Loader) bitmap { , := loadBitmap(, ot.MustNewTag("CBLC"), ot.MustNewTag("CBDT")) if == nil { return } , := loadBitmap(, ot.MustNewTag("EBLC"), ot.MustNewTag("EBDT")) if == nil { return } , := loadBitmap(, ot.MustNewTag("bloc"), ot.MustNewTag("bdat")) if == nil { return } return nil } // return nil if the table is missing or invalid func loadCff( *ot.Loader, int) (*cff.CFF, error) { , := .RawTable(ot.MustNewTag("CFF ")) if != nil { return nil, } , := cff.Parse() if != nil { return nil, } if := len(.Charstrings); != { return nil, fmt.Errorf("invalid number of glyphs in CFF table (%d != %d)", , ) } return , nil } // return nil if the table is missing or invalid func loadCff2( *ot.Loader, , int) (*cff.CFF2, error) { , := .RawTable(ot.MustNewTag("CFF2")) if != nil { return nil, } , := cff.ParseCFF2() if != nil { return nil, } if := len(.Charstrings); != { return nil, fmt.Errorf("invalid number of glyphs in CFF table (%d != %d)", , ) } if := .VarStore.AxisCount(); != -1 && != { return nil, fmt.Errorf("invalid number of axis in CFF table (%d != %d)", , ) } return , nil } func loadHVtmx(, []byte, int) (*tables.Hhea, tables.Hmtx, error) { , , := tables.ParseHhea() if != nil { return nil, tables.Hmtx{}, } , , := tables.ParseHmtx(, int(.NumOfLongMetrics), -int(.NumOfLongMetrics)) if != nil { return nil, tables.Hmtx{}, } return &, , nil } func loadHmtx( *ot.Loader, int) (*tables.Hhea, tables.Hmtx, error) { , := .RawTable(ot.MustNewTag("hhea")) if != nil { return nil, tables.Hmtx{}, } , := .RawTable(ot.MustNewTag("hmtx")) if != nil { return nil, tables.Hmtx{}, } return loadHVtmx(, , ) } func loadVmtx( *ot.Loader, int) (*tables.Hhea, tables.Hmtx, error) { , := .RawTable(ot.MustNewTag("vhea")) if != nil { return nil, tables.Hmtx{}, } , := .RawTable(ot.MustNewTag("vmtx")) if != nil { return nil, tables.Hmtx{}, } return loadHVtmx(, , ) } func loadGDEF( *ot.Loader, int, , []byte) (tables.GDEF, error) { , := .RawTable(ot.MustNewTag("GDEF")) if != nil { return tables.GDEF{}, } // Nuke the GDEF tables of to avoid unwanted width-zeroing. if isGDEFBlocklisted(, , ) { return tables.GDEF{}, nil } , , := tables.ParseGDEF() if != nil { return tables.GDEF{}, } = sanitizeGDEF(, ) if != nil { return tables.GDEF{}, } return , nil } // Face is a font with user-provided settings. // Contrary to the [*Font] objects, Faces are NOT safe for concurrent use. // A Face caches glyph extents and rune to glyph mapping, and should be reused when possible. // // Also note that an empty [Face] is invalid : the [NewFace] constructor is required to properly init caches. type Face struct { *Font extentsCache extentsCache cmapCache cache21_19_8 coords []tables.Coord xPpem, yPpem uint16 } // NewFace wraps [font] and initializes glyph caches. func ( *Font) *Face { := &Face{Font: , extentsCache: make(extentsCache, .nGlyphs)} .cmapCache.clear() return } // NominalGlyph returns the glyph used to represent the given rune, // or false if not found. // Note that it only looks into the cmap, without taking account substitutions // nor variation selectors. func ( *Face) ( rune) (GID, bool) { if , := .cmapCache.get(uint32()); { return GID(), } , := .Cmap.Lookup() if { .cmapCache.set(uint32(), uint32()) } return , } // Ppem returns the horizontal and vertical pixels-per-em (ppem), used to select bitmap sizes. func ( *Face) () (, uint16) { return .xPpem, .yPpem } // SetPpem applies horizontal and vertical pixels-per-em (ppem). func ( *Face) (, uint16) { .xPpem, .yPpem = , // invalid the cache .extentsCache.reset() } // Coords return a read-only slice of the current variable coordinates, expressed in normalized units. // It is empty for non variable fonts. func ( *Face) () []tables.Coord { return .coords } // SetCoords applies a list of variation coordinates, expressed in normalized units. // Use [NormalizeVariations] to convert from design (user) space units. func ( *Face) ( []tables.Coord) { .coords = // invalid the cache .extentsCache.reset() }