fontname_test.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. package transcoder
  2. import (
  3. "encoding/binary"
  4. "strings"
  5. "testing"
  6. "unicode/utf16"
  7. )
  8. // buildFont assembles a minimal sfnt carrying just a name table, which is all
  9. // FontFamilyName needs. Records are given as (platformID, nameID, value).
  10. type nameRec struct {
  11. platformID uint16
  12. nameID uint16
  13. value string
  14. }
  15. func buildFont(tag string, records []nameRec) []byte {
  16. // Encode the string storage and the record array
  17. var storage []byte
  18. recs := make([]byte, 0, len(records)*nameRecordSize)
  19. for _, r := range records {
  20. var encoded []byte
  21. if r.platformID == platformIDMicrosoft {
  22. units := utf16.Encode([]rune(r.value))
  23. encoded = make([]byte, len(units)*2)
  24. for i, u := range units {
  25. binary.BigEndian.PutUint16(encoded[i*2:], u)
  26. }
  27. } else {
  28. encoded = []byte(r.value)
  29. }
  30. rec := make([]byte, nameRecordSize)
  31. binary.BigEndian.PutUint16(rec[0:2], r.platformID)
  32. binary.BigEndian.PutUint16(rec[2:4], 0)
  33. binary.BigEndian.PutUint16(rec[4:6], 0)
  34. binary.BigEndian.PutUint16(rec[6:8], r.nameID)
  35. binary.BigEndian.PutUint16(rec[8:10], uint16(len(encoded)))
  36. binary.BigEndian.PutUint16(rec[10:12], uint16(len(storage)))
  37. recs = append(recs, rec...)
  38. storage = append(storage, encoded...)
  39. }
  40. stringOffset := nameTableHeaderSize + len(recs)
  41. nameTable := make([]byte, 0, stringOffset+len(storage))
  42. header := make([]byte, nameTableHeaderSize)
  43. binary.BigEndian.PutUint16(header[0:2], 0)
  44. binary.BigEndian.PutUint16(header[2:4], uint16(len(records)))
  45. binary.BigEndian.PutUint16(header[4:6], uint16(stringOffset))
  46. nameTable = append(nameTable, header...)
  47. nameTable = append(nameTable, recs...)
  48. nameTable = append(nameTable, storage...)
  49. // One-table sfnt: 12-byte header, one 16-byte record, then the table
  50. tableOffset := sfntTableDirOffset + sfntTableRecordSize
  51. font := make([]byte, tableOffset)
  52. copy(font[0:4], []byte{0x00, 0x01, 0x00, 0x00}) // TrueType version
  53. binary.BigEndian.PutUint16(font[sfntNumTablesOffset:], 1)
  54. copy(font[sfntTableDirOffset:], []byte(tag))
  55. binary.BigEndian.PutUint32(font[sfntTableDirOffset+8:], uint32(tableOffset))
  56. binary.BigEndian.PutUint32(font[sfntTableDirOffset+12:], uint32(len(nameTable)))
  57. return append(font, nameTable...)
  58. }
  59. // TestFontFamilyName_Microsoft covers the common case: a Windows platform
  60. // record holding a UTF-16BE family name.
  61. func TestFontFamilyName_Microsoft(t *testing.T) {
  62. font := buildFont("name", []nameRec{
  63. {platformIDMicrosoft, nameIDFontFamily, "UUZHUQHH"},
  64. })
  65. got, err := FontFamilyName(font)
  66. if err != nil {
  67. t.Fatalf("unexpected error: %v", err)
  68. }
  69. if got != "UUZHUQHH" {
  70. t.Errorf("expected UUZHUQHH, got %q", got)
  71. }
  72. }
  73. // TestFontFamilyName_NonASCII verifies CJK family names survive the UTF-16
  74. // decode, since those are exactly what fansub releases attach.
  75. func TestFontFamilyName_NonASCII(t *testing.T) {
  76. font := buildFont("name", []nameRec{
  77. {platformIDMicrosoft, nameIDFontFamily, "方正准雅宋_GBK"},
  78. })
  79. got, err := FontFamilyName(font)
  80. if err != nil {
  81. t.Fatalf("unexpected error: %v", err)
  82. }
  83. if got != "方正准雅宋_GBK" {
  84. t.Errorf("expected the CJK family name, got %q", got)
  85. }
  86. }
  87. // TestFontFamilyName_PrefersMicrosoft verifies the Windows record wins when a
  88. // font carries both, since that is the one matching what ASS scripts reference.
  89. func TestFontFamilyName_PrefersMicrosoft(t *testing.T) {
  90. font := buildFont("name", []nameRec{
  91. {platformIDMacintosh, nameIDFontFamily, "MacName"},
  92. {platformIDMicrosoft, nameIDFontFamily, "WindowsName"},
  93. })
  94. got, err := FontFamilyName(font)
  95. if err != nil {
  96. t.Fatalf("unexpected error: %v", err)
  97. }
  98. if got != "WindowsName" {
  99. t.Errorf("expected WindowsName, got %q", got)
  100. }
  101. }
  102. // TestFontFamilyName_MacintoshFallback verifies a font with only a Macintosh
  103. // record still resolves.
  104. func TestFontFamilyName_MacintoshFallback(t *testing.T) {
  105. font := buildFont("name", []nameRec{
  106. {platformIDMacintosh, nameIDFontFamily, "OnlyMac"},
  107. })
  108. got, err := FontFamilyName(font)
  109. if err != nil {
  110. t.Fatalf("unexpected error: %v", err)
  111. }
  112. if got != "OnlyMac" {
  113. t.Errorf("expected OnlyMac, got %q", got)
  114. }
  115. }
  116. // TestFontFamilyName_IgnoresOtherNameIDs verifies only the family record (ID 1)
  117. // is used, not the style or full-name records.
  118. func TestFontFamilyName_IgnoresOtherNameIDs(t *testing.T) {
  119. font := buildFont("name", []nameRec{
  120. {platformIDMicrosoft, 2, "Bold"},
  121. {platformIDMicrosoft, 4, "Family Bold"},
  122. {platformIDMicrosoft, nameIDFontFamily, "Family"},
  123. })
  124. got, err := FontFamilyName(font)
  125. if err != nil {
  126. t.Fatalf("unexpected error: %v", err)
  127. }
  128. if got != "Family" {
  129. t.Errorf("expected Family, got %q", got)
  130. }
  131. }
  132. // TestFontFamilyName_Rejects covers the malformed inputs the parser must not
  133. // panic on, since attachment bytes come straight from an untrusted container.
  134. func TestFontFamilyName_Rejects(t *testing.T) {
  135. cases := []struct {
  136. name string
  137. data []byte
  138. }{
  139. {"empty", []byte{}},
  140. {"too short for a header", []byte{0, 1, 0, 0, 0}},
  141. {"no name table", buildFont("glyf", []nameRec{{platformIDMicrosoft, nameIDFontFamily, "X"}})},
  142. {"no family record", buildFont("name", []nameRec{{platformIDMicrosoft, 4, "Full Name Only"}})},
  143. {"font collection", append([]byte("ttcf"), make([]byte, 32)...)},
  144. {"random bytes", []byte("this is definitely not a font at all")},
  145. }
  146. for _, tc := range cases {
  147. t.Run(tc.name, func(t *testing.T) {
  148. if _, err := FontFamilyName(tc.data); err == nil {
  149. t.Error("expected an error, got nil")
  150. }
  151. })
  152. }
  153. }
  154. // TestFontFamilyName_TruncatedTableOffset verifies a table directory pointing
  155. // past the end of the data is rejected rather than slicing out of range.
  156. func TestFontFamilyName_TruncatedTableOffset(t *testing.T) {
  157. font := buildFont("name", []nameRec{{platformIDMicrosoft, nameIDFontFamily, "X"}})
  158. // Point the name table beyond the buffer
  159. binary.BigEndian.PutUint32(font[sfntTableDirOffset+8:], uint32(len(font)+500))
  160. if _, err := FontFamilyName(font); err == nil {
  161. t.Error("expected an error for a truncated table, got nil")
  162. }
  163. }
  164. // TestDecodeUTF16BE checks the decoder handles an odd trailing byte instead of
  165. // reading past the slice.
  166. func TestDecodeUTF16BE(t *testing.T) {
  167. units := utf16.Encode([]rune("Hi"))
  168. raw := make([]byte, len(units)*2)
  169. for i, u := range units {
  170. binary.BigEndian.PutUint16(raw[i*2:], u)
  171. }
  172. if got := decodeUTF16BE(raw); got != "Hi" {
  173. t.Errorf("expected Hi, got %q", got)
  174. }
  175. if got := decodeUTF16BE(append(raw, 0x00)); got != "Hi" {
  176. t.Errorf("expected Hi from odd-length input, got %q", got)
  177. }
  178. if got := decodeUTF16BE(nil); got != "" {
  179. t.Errorf("expected empty string, got %q", got)
  180. }
  181. }
  182. // TestFontFamilyName_TrimsWhitespace verifies padded names are cleaned up, as
  183. // some tools pad the record.
  184. func TestFontFamilyName_TrimsWhitespace(t *testing.T) {
  185. font := buildFont("name", []nameRec{
  186. {platformIDMicrosoft, nameIDFontFamily, " Padded Name "},
  187. })
  188. got, err := FontFamilyName(font)
  189. if err != nil {
  190. t.Fatalf("unexpected error: %v", err)
  191. }
  192. if got != "Padded Name" || strings.HasPrefix(got, " ") {
  193. t.Errorf("expected trimmed name, got %q", got)
  194. }
  195. }