| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- package transcoder
- import (
- "encoding/binary"
- "strings"
- "testing"
- "unicode/utf16"
- )
- // buildFont assembles a minimal sfnt carrying just a name table, which is all
- // FontFamilyName needs. Records are given as (platformID, nameID, value).
- type nameRec struct {
- platformID uint16
- nameID uint16
- value string
- }
- func buildFont(tag string, records []nameRec) []byte {
- // Encode the string storage and the record array
- var storage []byte
- recs := make([]byte, 0, len(records)*nameRecordSize)
- for _, r := range records {
- var encoded []byte
- if r.platformID == platformIDMicrosoft {
- units := utf16.Encode([]rune(r.value))
- encoded = make([]byte, len(units)*2)
- for i, u := range units {
- binary.BigEndian.PutUint16(encoded[i*2:], u)
- }
- } else {
- encoded = []byte(r.value)
- }
- rec := make([]byte, nameRecordSize)
- binary.BigEndian.PutUint16(rec[0:2], r.platformID)
- binary.BigEndian.PutUint16(rec[2:4], 0)
- binary.BigEndian.PutUint16(rec[4:6], 0)
- binary.BigEndian.PutUint16(rec[6:8], r.nameID)
- binary.BigEndian.PutUint16(rec[8:10], uint16(len(encoded)))
- binary.BigEndian.PutUint16(rec[10:12], uint16(len(storage)))
- recs = append(recs, rec...)
- storage = append(storage, encoded...)
- }
- stringOffset := nameTableHeaderSize + len(recs)
- nameTable := make([]byte, 0, stringOffset+len(storage))
- header := make([]byte, nameTableHeaderSize)
- binary.BigEndian.PutUint16(header[0:2], 0)
- binary.BigEndian.PutUint16(header[2:4], uint16(len(records)))
- binary.BigEndian.PutUint16(header[4:6], uint16(stringOffset))
- nameTable = append(nameTable, header...)
- nameTable = append(nameTable, recs...)
- nameTable = append(nameTable, storage...)
- // One-table sfnt: 12-byte header, one 16-byte record, then the table
- tableOffset := sfntTableDirOffset + sfntTableRecordSize
- font := make([]byte, tableOffset)
- copy(font[0:4], []byte{0x00, 0x01, 0x00, 0x00}) // TrueType version
- binary.BigEndian.PutUint16(font[sfntNumTablesOffset:], 1)
- copy(font[sfntTableDirOffset:], []byte(tag))
- binary.BigEndian.PutUint32(font[sfntTableDirOffset+8:], uint32(tableOffset))
- binary.BigEndian.PutUint32(font[sfntTableDirOffset+12:], uint32(len(nameTable)))
- return append(font, nameTable...)
- }
- // TestFontFamilyName_Microsoft covers the common case: a Windows platform
- // record holding a UTF-16BE family name.
- func TestFontFamilyName_Microsoft(t *testing.T) {
- font := buildFont("name", []nameRec{
- {platformIDMicrosoft, nameIDFontFamily, "UUZHUQHH"},
- })
- got, err := FontFamilyName(font)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if got != "UUZHUQHH" {
- t.Errorf("expected UUZHUQHH, got %q", got)
- }
- }
- // TestFontFamilyName_NonASCII verifies CJK family names survive the UTF-16
- // decode, since those are exactly what fansub releases attach.
- func TestFontFamilyName_NonASCII(t *testing.T) {
- font := buildFont("name", []nameRec{
- {platformIDMicrosoft, nameIDFontFamily, "方正准雅宋_GBK"},
- })
- got, err := FontFamilyName(font)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if got != "方正准雅宋_GBK" {
- t.Errorf("expected the CJK family name, got %q", got)
- }
- }
- // TestFontFamilyName_PrefersMicrosoft verifies the Windows record wins when a
- // font carries both, since that is the one matching what ASS scripts reference.
- func TestFontFamilyName_PrefersMicrosoft(t *testing.T) {
- font := buildFont("name", []nameRec{
- {platformIDMacintosh, nameIDFontFamily, "MacName"},
- {platformIDMicrosoft, nameIDFontFamily, "WindowsName"},
- })
- got, err := FontFamilyName(font)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if got != "WindowsName" {
- t.Errorf("expected WindowsName, got %q", got)
- }
- }
- // TestFontFamilyName_MacintoshFallback verifies a font with only a Macintosh
- // record still resolves.
- func TestFontFamilyName_MacintoshFallback(t *testing.T) {
- font := buildFont("name", []nameRec{
- {platformIDMacintosh, nameIDFontFamily, "OnlyMac"},
- })
- got, err := FontFamilyName(font)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if got != "OnlyMac" {
- t.Errorf("expected OnlyMac, got %q", got)
- }
- }
- // TestFontFamilyName_IgnoresOtherNameIDs verifies only the family record (ID 1)
- // is used, not the style or full-name records.
- func TestFontFamilyName_IgnoresOtherNameIDs(t *testing.T) {
- font := buildFont("name", []nameRec{
- {platformIDMicrosoft, 2, "Bold"},
- {platformIDMicrosoft, 4, "Family Bold"},
- {platformIDMicrosoft, nameIDFontFamily, "Family"},
- })
- got, err := FontFamilyName(font)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if got != "Family" {
- t.Errorf("expected Family, got %q", got)
- }
- }
- // TestFontFamilyName_Rejects covers the malformed inputs the parser must not
- // panic on, since attachment bytes come straight from an untrusted container.
- func TestFontFamilyName_Rejects(t *testing.T) {
- cases := []struct {
- name string
- data []byte
- }{
- {"empty", []byte{}},
- {"too short for a header", []byte{0, 1, 0, 0, 0}},
- {"no name table", buildFont("glyf", []nameRec{{platformIDMicrosoft, nameIDFontFamily, "X"}})},
- {"no family record", buildFont("name", []nameRec{{platformIDMicrosoft, 4, "Full Name Only"}})},
- {"font collection", append([]byte("ttcf"), make([]byte, 32)...)},
- {"random bytes", []byte("this is definitely not a font at all")},
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- if _, err := FontFamilyName(tc.data); err == nil {
- t.Error("expected an error, got nil")
- }
- })
- }
- }
- // TestFontFamilyName_TruncatedTableOffset verifies a table directory pointing
- // past the end of the data is rejected rather than slicing out of range.
- func TestFontFamilyName_TruncatedTableOffset(t *testing.T) {
- font := buildFont("name", []nameRec{{platformIDMicrosoft, nameIDFontFamily, "X"}})
- // Point the name table beyond the buffer
- binary.BigEndian.PutUint32(font[sfntTableDirOffset+8:], uint32(len(font)+500))
- if _, err := FontFamilyName(font); err == nil {
- t.Error("expected an error for a truncated table, got nil")
- }
- }
- // TestDecodeUTF16BE checks the decoder handles an odd trailing byte instead of
- // reading past the slice.
- func TestDecodeUTF16BE(t *testing.T) {
- units := utf16.Encode([]rune("Hi"))
- raw := make([]byte, len(units)*2)
- for i, u := range units {
- binary.BigEndian.PutUint16(raw[i*2:], u)
- }
- if got := decodeUTF16BE(raw); got != "Hi" {
- t.Errorf("expected Hi, got %q", got)
- }
- if got := decodeUTF16BE(append(raw, 0x00)); got != "Hi" {
- t.Errorf("expected Hi from odd-length input, got %q", got)
- }
- if got := decodeUTF16BE(nil); got != "" {
- t.Errorf("expected empty string, got %q", got)
- }
- }
- // TestFontFamilyName_TrimsWhitespace verifies padded names are cleaned up, as
- // some tools pad the record.
- func TestFontFamilyName_TrimsWhitespace(t *testing.T) {
- font := buildFont("name", []nameRec{
- {platformIDMicrosoft, nameIDFontFamily, " Padded Name "},
- })
- got, err := FontFamilyName(font)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if got != "Padded Name" || strings.HasPrefix(got, " ") {
- t.Errorf("expected trimmed name, got %q", got)
- }
- }
|