Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions font/font.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
141 changes: 16 additions & 125 deletions font/system.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package font

import (
"bytes"
"encoding/gob"
"fmt"
"io"
Expand All @@ -11,9 +10,6 @@ import (
"runtime"
"sort"
"strings"

"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
)

func DefaultFontDirs() []string {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
})
}
Expand Down Expand Up @@ -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
}
19 changes: 19 additions & 0 deletions font/system_test.go
Original file line number Diff line number Diff line change
@@ -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))
}