diff --git a/font/font.go b/font/font.go index 1eb68362..6807932e 100644 --- a/font/font.go +++ b/font/font.go @@ -107,6 +107,141 @@ func ParseFont(b []byte, index int) (*SFNT, error) { return ParseSFNT(sfntBytes, index) } +func ParseMetadata(b []byte, index int) (*FontMetadata, error) { + sfntBytes, err := ToSFNT(b) + if err != nil { + return nil, err + } + return parseMetadata(sfntBytes, index) +} +func parseMetadata(byt []byte, index int) (*FontMetadata, error) { + if len(byt) < 12 || uint(math.MaxUint32) < uint(len(byt)) { + return nil, ErrInvalidFontData + } + + r := NewBinaryReader(byt) + sfntVersion := r.ReadString(4) + isCollection := sfntVersion == "ttcf" + if isCollection { + majorVersion := r.ReadUint16() + minorVersion := r.ReadUint16() + if majorVersion != 1 && majorVersion != 2 || minorVersion != 0 { + return nil, fmt.Errorf("bad TTC version") + } + + numFonts := r.ReadUint32() + if index < 0 || numFonts <= uint32(index) { + return nil, fmt.Errorf("bad font index %d", index) + } + if r.Len() < 4*numFonts { + return nil, ErrInvalidFontData + } + + _ = r.ReadBytes(uint32(4 * index)) + offset := r.ReadUint32() + var length uint32 + if uint32(index)+1 == numFonts { + length = uint32(len(byt)) - offset + } else { + length = r.ReadUint32() - offset + } + if uint32(len(byt))-8 < offset || uint32(len(byt))-8-offset < length { + return nil, ErrInvalidFontData + } + + r.Seek(offset) + sfntVersion = r.ReadString(4) + } else if index != 0 { + return nil, fmt.Errorf("bad font index %d", index) + } + if sfntVersion != "OTTO" && sfntVersion != "true" && binary.BigEndian.Uint32([]byte(sfntVersion)) != 0x00010000 { + return nil, fmt.Errorf("bad SFNT version") + } + numTables := r.ReadUint16() + _ = r.ReadUint16() // searchRange + _ = r.ReadUint16() // entrySelector + _ = r.ReadUint16() // rangeShift + if r.Len() < 16*uint32(numTables) { // can never exceed uint32 as numTables is uint16 + return nil, ErrInvalidFontData + } + + var nameTableData []byte + for i := 0; i < int(numTables); i++ { + tag := r.ReadString(4) + _ = r.ReadUint32() // checksum + offset := r.ReadUint32() + length := r.ReadUint32() + + padding := (4 - length&3) & 3 + if uint32(len(byt)) <= offset || uint32(len(byt))-offset < length || uint32(len(byt))-offset-length < padding { + return nil, ErrInvalidFontData + } + + if tag == "head" { + if length < 12 { + return nil, ErrInvalidFontData + } + } + + if string(tag) != "name" { + continue + } + + nameTableData = byt[offset : offset+length : offset+length] + break + } + + if nameTableData == nil { + return nil, fmt.Errorf("missing table name") + } + + r = NewBinaryReader(nameTableData) + version := r.ReadUint16() + if version != 0 && version != 1 { + return nil, fmt.Errorf("name: bad version") + } + count := r.ReadUint16() + storageOffset := r.ReadUint16() + if uint32(len(nameTableData)) < 6+12*uint32(count) || uint16(len(nameTableData)) < storageOffset { + return nil, fmt.Errorf("name: bad table") + } + + var names []string + var style string + + for i := 0; i < int(count); i++ { + var record nameRecord + record.Platform = PlatformID(r.ReadUint16()) + record.Encoding = EncodingID(r.ReadUint16()) + record.Language = r.ReadUint16() + record.Name = NameID(r.ReadUint16()) + + length := r.ReadUint16() + offset := r.ReadUint16() + if uint16(len(nameTableData))-storageOffset < offset || uint16(len(nameTableData))-storageOffset-offset < length { + return nil, fmt.Errorf("name: bad table") + } + record.Value = nameTableData[storageOffset+offset : storageOffset+offset+length] + + if record.Name == NameFontFamily || + record.Name == NameFull || + record.Name == NamePostScript { + names = append(names, record.String()) + } + + if record.Name == NameFontSubfamily || + record.Name == NamePreferredSubfamily { + style = record.String() + } + } + + return &FontMetadata{ + Filename: "", + Families: names, + Style: ParseStyle(style), + }, nil +} + // FromGoFreetype parses a structure from truetype.Font to a valid SFNT byte slice. func FromGoFreetype(font *truetype.Font) []byte { v := reflect.ValueOf(*font) diff --git a/font/system.go b/font/system.go index cc45b176..1a38bf54 100644 --- a/font/system.go +++ b/font/system.go @@ -1,7 +1,6 @@ package font import ( - "bytes" "encoding/gob" "fmt" "io" @@ -11,9 +10,6 @@ import ( "runtime" "sort" "strings" - - "golang.org/x/text/encoding/unicode" - "golang.org/x/text/transform" ) func DefaultFontDirs() []string { @@ -270,12 +266,12 @@ func (style Style) String() string { type FontMetadata struct { Filename string - Family string + Families []string Style } func (metadata FontMetadata) String() string { - return fmt.Sprintf("%s (%v): %s", metadata.Family, metadata.Style, metadata.Filename) + return fmt.Sprintf("%s (%v): %s", strings.Join(metadata.Families, ","), metadata.Style, metadata.Filename) } type SystemFonts struct { @@ -307,10 +303,12 @@ func (s *SystemFonts) Save(filename string) error { } func (s *SystemFonts) Add(metadata FontMetadata) { - if _, ok := s.Fonts[metadata.Family]; !ok { - s.Fonts[metadata.Family] = map[Style]FontMetadata{} + for _, family := range metadata.Families { + if _, ok := s.Fonts[family]; !ok { + s.Fonts[family] = map[Style]FontMetadata{} + } + s.Fonts[family][metadata.Style] = metadata } - s.Fonts[metadata.Family][metadata.Style] = metadata } func (s *SystemFonts) Match(name string, style Style) (FontMetadata, bool) { @@ -438,27 +436,18 @@ func FindSystemFonts(dirs []string) (*SystemFonts, error) { return nil } - var getMetadata func(io.ReadSeeker) (FontMetadata, error) - switch filepath.Ext(path) { - case ".ttf", ".otf": - getMetadata = getSFNTMetadata - // TODO: handle .ttc, .woff, .woff2, .eot + fontData, err := os.ReadFile(path) + if err != nil { + return nil } - if getMetadata != nil { - f, err := os.Open(path) - if err != nil { - return nil - } - defer f.Close() - - metadata, err := getMetadata(f) - if err != nil { - return nil - } - metadata.Filename = path - fonts.Add(metadata) + metadata, err := ParseMetadata(fontData, 0) + if err != nil { + return nil } + metadata.Filename = path + fonts.Add(*metadata) + return nil }) } @@ -500,101 +489,3 @@ func u32(b []byte) uint32 { return (uint32(b[0]) << 24) + (uint32(b[1]) << 16) + (uint32(b[2]) << 8) + uint32(b[3]) } -func getSFNTMetadata(r io.ReadSeeker) (FontMetadata, error) { - header, err := read(r, 12) - if err != nil { - return FontMetadata{}, err - } - numTables := u16(header[4:]) - - // read tables list - var offset uint32 - tables, err := read(r, 16*int(numTables)) - if err != nil { - return FontMetadata{}, err - } - for i := 0; i < 16*int(numTables); i += 16 { - if bytes.Equal(tables[i:i+4], []byte("name")) { - offset = u32(tables[i+8:]) - break - } - } - if offset == 0 { - return FontMetadata{}, fmt.Errorf("name table not found") - } - - // read name table - if _, err = r.Seek(int64(offset), io.SeekStart); err != nil { - return FontMetadata{}, err - } - nameTable, err := read(r, 6) - if err != nil { - return FontMetadata{}, err - } - version := u16(nameTable) - count := u16(nameTable[2:]) - storageOffset := int64(offset) + int64(u16(nameTable[4:])) - - metadata := FontMetadata{} - decodeUTF16 := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder() - if version == 0 { - records, err := read(r, 12*int(count)) - if err != nil { - return FontMetadata{}, err - } - - found := 0 - var family, subfamily string - for i := 0; i < 12*int(count); i += 12 { - // TODO: check platform and encoding? - platform := PlatformID(u16(records[i:])) - //encoding := EncodingID(u16(records[i+2:])) - language := u16(records[i+4:]) - if platform != PlatformWindows && (language&0x00FF) != 0x0009 { - continue // not English or not Windows - } - - name := NameID(u16(records[i+6:])) - if name == NameFontFamily || name == NameFontSubfamily || name == NamePreferredFamily || name == NamePreferredSubfamily { - length := u16(records[i+8:]) - offset := u16(records[i+10:]) - if _, err = r.Seek(storageOffset+int64(offset), io.SeekStart); err != nil { - return FontMetadata{}, err - } - val, err := read(r, int(length)) - if err != nil { - return FontMetadata{}, err - } - val, _, err = transform.Bytes(decodeUTF16, val) - if err != nil { - return FontMetadata{}, err - } - if name == NameFontFamily || name == NamePreferredFamily { - family = string(val) - } else if name == NameFontSubfamily || name == NamePreferredSubfamily { - subfamily = string(val) - } - if name == NamePreferredFamily || name == NamePreferredSubfamily { - found++ - //if found == 2 { - // break // break early - //} - } - } - } - if family == "" { - return FontMetadata{}, fmt.Errorf("font family not found") - } - - style := ParseStyle(subfamily) - if style == UnknownStyle { - return FontMetadata{}, fmt.Errorf("unknown subfamily style: %s", subfamily) - } - - metadata.Family = family - metadata.Style = style - } else if version == 1 { - // TODO - } - return metadata, nil -} diff --git a/font/system_test.go b/font/system_test.go new file mode 100755 index 00000000..07d4cc25 --- /dev/null +++ b/font/system_test.go @@ -0,0 +1,19 @@ +package font + +import ( + "fmt" + "testing" + "time" +) + +func TestFindSystemFonts(t *testing.T) { + start := time.Now() + dirs := DefaultFontDirs() + fonts, err := FindSystemFonts(dirs) + if err != nil { + t.Error(err) + return + } + + fmt.Println(fonts, time.Since(start)) +}