package fontinfo import ( "bytes" "fmt" "io" "unicode/utf8" "golang.org/x/text/encoding/unicode" ) func read(r io.Reader, length int) ([]byte, error) { buf := make([]byte, length) if n, err := r.Read(buf); err != nil { return nil, err } else if n < length { return nil, fmt.Errorf("invalid length") } return buf, nil } func u16(buf []byte) uint16 { return (uint16(buf[0]) << 8) + uint16(buf[1]) } func u32(buf []byte) uint32 { return (uint32(buf[0]) << 24) + (uint32(buf[1]) << 16) + (uint32(buf[2]) << 8) + uint32(buf[3]) } type FontMetadata struct { FontFamily string FontStyle string } type FontParser interface { Parse(r io.ReadSeeker) (*FontMetadata, error) } // for ttf and otf files type TruetypeParser struct { } func (t *TruetypeParser) Parse(r io.ReadSeeker) (*FontMetadata, error) { buf, err := read(r, 12) if err != nil { return nil, err } tableCount := u16(buf[4:6]) for i := 0; i < int(tableCount); i++ { if _, err := r.Seek(12+(int64(i)*16), 0); err != nil { return nil, err } table, err := read(r, 16) if err != nil { return nil, err } if string(table[0:4]) != "name" { continue } offset := u32(table[8:12]) return t.readNameTable(r, offset) } return nil, fmt.Errorf("name table not found") } func (t *TruetypeParser) readNameTable(r io.ReadSeeker, offset uint32) (*FontMetadata, error) { if _, err := r.Seek(int64(offset), 0); err != nil { return nil, fmt.Errorf("invalid font file") } nameTable, err := read(r, 6) if err != nil { return nil, err } nameCount := u16(nameTable[2:4]) stringOffset := int64(u16(nameTable[4:6])) + int64(offset) var done uint8 var metadata FontMetadata nameRecordStart := offset + 6 for j := 0; j < int(nameCount); j++ { recordOffset := nameRecordStart + uint32(12*j) if _, err := r.Seek(int64(recordOffset), 0); err != nil { return nil, err } buf, err := read(r, 12) if err != nil { return nil, err } language := u16(buf[4:6]) if language != 0 && language != 1033 { // not english or english us continue } nameID := u16(buf[6:8]) switch nameID { case 1, 2: //family, style if _, err := r.Seek(int64(stringOffset)+int64(u16(buf[10:12])), 0); err != nil { return nil, err } raw, err := read(r, int(u16(buf[8:10]))) if err != nil { return nil, err } if nameID == 1 { done |= 1 metadata.FontFamily = lazyUnicode(raw) } else { done |= 2 metadata.FontStyle = lazyUnicode(raw) } if done == 3 { // bail early if we have what we need return &metadata, nil } } } return &metadata, nil } func lazyUnicode(r []byte) string { var back bool if bytes.HasSuffix(r, []byte{0}) { r = bytes.TrimSuffix(r, []byte{0}) back = true } if utf8.Valid(r) && (!bytes.Contains(r, []byte{0})) { return string(r) } if back { r = append(r, 0) } dec := unicode.UTF16(unicode.BigEndian, unicode.UseBOM).NewDecoder() ans, err := dec.String(string(r)) if err != nil { return "Invalid_UTF16_Encoding" } return ans }