test_names.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. from fontTools.ttLib import TTFont
  2. from fontTools.misc.testTools import getXML
  3. from axisregistry import (
  4. build_name_table,
  5. build_filename,
  6. build_fvar_instances,
  7. build_stat,
  8. build_variations_ps_name,
  9. _fvar_dflts,
  10. _fvar_instance_collisions,
  11. )
  12. import pytest
  13. import os
  14. CWD = os.path.dirname(__file__)
  15. DATA_DIR = os.path.join(CWD, "data")
  16. roboto_flex_fp = os.path.join(
  17. DATA_DIR,
  18. "RobotoFlex[GRAD,XOPQ,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght].ttf",
  19. )
  20. mavenpro_fp = os.path.join(DATA_DIR, "MavenPro-Regular.ttf")
  21. opensans_roman_fp = os.path.join(DATA_DIR, "OpenSans[wdth,wght].ttf")
  22. opensans_italic_fp = os.path.join(DATA_DIR, "OpenSans-Italic[wdth,wght].ttf")
  23. opensans_cond_roman_fp = os.path.join(DATA_DIR, "OpenSansCondensed[wght].ttf")
  24. opensans_cond_italic_fp = os.path.join(DATA_DIR, "OpenSansCondensed-Italic[wght].ttf")
  25. wonky_fp = os.path.join(DATA_DIR, "Wonky[wdth,wght].ttf")
  26. @pytest.fixture
  27. def static_font():
  28. return TTFont(mavenpro_fp)
  29. def _test_names(ttFont, expected):
  30. name_table = ttFont["name"]
  31. for k, v in expected.items():
  32. record = name_table.getName(*k)
  33. if not record:
  34. assert record == v, f"{k} record is incorrect, expected {v} got {record}"
  35. else:
  36. record_string = record.toUnicode()
  37. assert (
  38. record_string == v
  39. ), f"{k} record is incorrect, expected {v} got {record}"
  40. @pytest.mark.parametrize(
  41. "fp, family_name, style_name, siblings, expected",
  42. [
  43. # Maven Pro Regular
  44. (
  45. mavenpro_fp,
  46. "Maven Pro",
  47. "Regular",
  48. [],
  49. {
  50. (1, 3, 1, 0x409): "Maven Pro",
  51. (2, 3, 1, 0x409): "Regular",
  52. (3, 3, 1, 0x409): "2.003;NONE;MavenPro-Regular",
  53. (4, 3, 1, 0x409): "Maven Pro Regular",
  54. (6, 3, 1, 0x409): "MavenPro-Regular",
  55. (16, 3, 1, 0x409): None,
  56. (17, 3, 1, 0x409): None,
  57. },
  58. ),
  59. # Maven Pro Italic
  60. (
  61. mavenpro_fp,
  62. "Maven Pro",
  63. "Italic",
  64. [],
  65. {
  66. (1, 3, 1, 0x409): "Maven Pro",
  67. (2, 3, 1, 0x409): "Italic",
  68. (3, 3, 1, 0x409): "2.003;NONE;MavenPro-Italic",
  69. (4, 3, 1, 0x409): "Maven Pro Italic",
  70. (6, 3, 1, 0x409): "MavenPro-Italic",
  71. (16, 3, 1, 0x409): None,
  72. (17, 3, 1, 0x409): None,
  73. },
  74. ),
  75. # Maven Pro Bold
  76. (
  77. mavenpro_fp,
  78. "Maven Pro",
  79. "Bold",
  80. [],
  81. {
  82. (1, 3, 1, 0x409): "Maven Pro",
  83. (2, 3, 1, 0x409): "Bold",
  84. (3, 3, 1, 0x409): "2.003;NONE;MavenPro-Bold",
  85. (4, 3, 1, 0x409): "Maven Pro Bold",
  86. (6, 3, 1, 0x409): "MavenPro-Bold",
  87. (16, 3, 1, 0x409): None,
  88. (17, 3, 1, 0x409): None,
  89. },
  90. ),
  91. # Maven Pro Bold Italic
  92. (
  93. mavenpro_fp,
  94. "Maven Pro",
  95. "Bold Italic",
  96. [],
  97. {
  98. (1, 3, 1, 0x409): "Maven Pro",
  99. (2, 3, 1, 0x409): "Bold Italic",
  100. (3, 3, 1, 0x409): "2.003;NONE;MavenPro-BoldItalic",
  101. (4, 3, 1, 0x409): "Maven Pro Bold Italic",
  102. (6, 3, 1, 0x409): "MavenPro-BoldItalic",
  103. (16, 3, 1, 0x409): None,
  104. (17, 3, 1, 0x409): None,
  105. },
  106. ),
  107. # Maven Pro Black
  108. (
  109. mavenpro_fp,
  110. "Maven Pro",
  111. "Black",
  112. [],
  113. {
  114. (1, 3, 1, 0x409): "Maven Pro Black",
  115. (2, 3, 1, 0x409): "Regular",
  116. (3, 3, 1, 0x409): "2.003;NONE;MavenPro-Black",
  117. (4, 3, 1, 0x409): "Maven Pro Black",
  118. (6, 3, 1, 0x409): "MavenPro-Black",
  119. (16, 3, 1, 0x409): "Maven Pro",
  120. (17, 3, 1, 0x409): "Black",
  121. },
  122. ),
  123. # Maven Pro Black Italic
  124. (
  125. mavenpro_fp,
  126. "Maven Pro",
  127. "Black Italic",
  128. [],
  129. {
  130. (1, 3, 1, 0x409): "Maven Pro Black",
  131. (2, 3, 1, 0x409): "Italic",
  132. (3, 3, 1, 0x409): "2.003;NONE;MavenPro-BlackItalic",
  133. (4, 3, 1, 0x409): "Maven Pro Black Italic",
  134. (6, 3, 1, 0x409): "MavenPro-BlackItalic",
  135. (16, 3, 1, 0x409): "Maven Pro",
  136. (17, 3, 1, 0x409): "Black Italic",
  137. },
  138. ),
  139. # Maven Pro ExtraLight Italic
  140. (
  141. mavenpro_fp,
  142. "Maven Pro",
  143. "ExtraLight Italic",
  144. [],
  145. {
  146. (1, 3, 1, 0x409): "Maven Pro ExtraLight",
  147. (2, 3, 1, 0x409): "Italic",
  148. (3, 3, 1, 0x409): "2.003;NONE;MavenPro-ExtraLightItalic",
  149. (4, 3, 1, 0x409): "Maven Pro ExtraLight Italic",
  150. (6, 3, 1, 0x409): "MavenPro-ExtraLightItalic",
  151. (16, 3, 1, 0x409): "Maven Pro",
  152. (17, 3, 1, 0x409): "ExtraLight Italic",
  153. },
  154. ),
  155. # check non-weight styles get appended to family name
  156. # Maven Pro UltraExpanded Regular
  157. (
  158. mavenpro_fp,
  159. "Maven Pro",
  160. "UltraExpanded Regular",
  161. [],
  162. {
  163. (1, 3, 1, 0x409): "Maven Pro UltraExpanded",
  164. (2, 3, 1, 0x409): "Regular",
  165. (3, 3, 1, 0x409): "2.003;NONE;MavenProUltraExpanded-Regular",
  166. (4, 3, 1, 0x409): "Maven Pro UltraExpanded Regular",
  167. (6, 3, 1, 0x409): "MavenProUltraExpanded-Regular",
  168. (16, 3, 1, 0x409): None,
  169. (17, 3, 1, 0x409): None,
  170. },
  171. ),
  172. # Maven Pro Condensed ExtraLight Italic
  173. (
  174. mavenpro_fp,
  175. "Maven Pro",
  176. "Condensed ExtraLight Italic",
  177. [],
  178. {
  179. (1, 3, 1, 0x409): "Maven Pro Condensed ExtraLight",
  180. (2, 3, 1, 0x409): "Italic",
  181. (3, 3, 1, 0x409): "2.003;NONE;MavenProCondensed-ExtraLightItalic",
  182. (4, 3, 1, 0x409): "Maven Pro Condensed ExtraLight Italic",
  183. (6, 3, 1, 0x409): "MavenProCondensed-ExtraLightItalic",
  184. (16, 3, 1, 0x409): "Maven Pro Condensed",
  185. (17, 3, 1, 0x409): "ExtraLight Italic",
  186. },
  187. ),
  188. ## VFs
  189. # Open Sans Roman
  190. (
  191. opensans_roman_fp,
  192. "Open Sans",
  193. None,
  194. [opensans_italic_fp, opensans_cond_roman_fp, opensans_cond_italic_fp],
  195. {
  196. (1, 3, 1, 0x409): "Open Sans",
  197. (2, 3, 1, 0x409): "Regular",
  198. (3, 3, 1, 0x409): "3.000;GOOG;OpenSans-Regular",
  199. (4, 3, 1, 0x409): "Open Sans Regular",
  200. (6, 3, 1, 0x409): "OpenSans-Regular",
  201. (16, 3, 1, 0x409): None,
  202. (17, 3, 1, 0x409): None,
  203. (25, 3, 1, 0x409): "OpenSans",
  204. },
  205. ),
  206. # Open Sans Italic
  207. (
  208. opensans_italic_fp,
  209. "Open Sans",
  210. None,
  211. [opensans_roman_fp, opensans_cond_roman_fp, opensans_cond_italic_fp],
  212. {
  213. (1, 3, 1, 0x409): "Open Sans",
  214. (2, 3, 1, 0x409): "Italic",
  215. (3, 3, 1, 0x409): "3.000;GOOG;OpenSans-Italic",
  216. (4, 3, 1, 0x409): "Open Sans Italic",
  217. (6, 3, 1, 0x409): "OpenSans-Italic",
  218. (16, 3, 1, 0x409): None,
  219. (17, 3, 1, 0x409): None,
  220. (25, 3, 1, 0x409): "OpenSansItalic",
  221. },
  222. ),
  223. # Open Sans Cond Roman
  224. (
  225. opensans_cond_roman_fp,
  226. "Open Sans Condensed",
  227. None,
  228. [opensans_roman_fp, opensans_italic_fp, opensans_cond_italic_fp],
  229. {
  230. (1, 3, 1, 0x409): "Open Sans Condensed",
  231. (2, 3, 1, 0x409): "Regular",
  232. (3, 3, 1, 0x409): "3.000;GOOG;OpenSansCondensed-Regular",
  233. (4, 3, 1, 0x409): "Open Sans Condensed Regular",
  234. (6, 3, 1, 0x409): "OpenSansCondensed-Regular",
  235. (16, 3, 1, 0x409): None,
  236. (17, 3, 1, 0x409): None,
  237. (25, 3, 1, 0x409): "OpenSansCondensed",
  238. },
  239. ),
  240. # Open Sans Cond Italic
  241. (
  242. opensans_cond_italic_fp,
  243. "Open Sans Condensed",
  244. None,
  245. [opensans_roman_fp, opensans_italic_fp, opensans_cond_roman_fp],
  246. {
  247. (1, 3, 1, 0x409): "Open Sans Condensed",
  248. (2, 3, 1, 0x409): "Italic",
  249. (3, 3, 1, 0x409): "3.000;GOOG;OpenSansCondensed-Italic",
  250. (4, 3, 1, 0x409): "Open Sans Condensed Italic",
  251. (6, 3, 1, 0x409): "OpenSansCondensed-Italic",
  252. (16, 3, 1, 0x409): None,
  253. (17, 3, 1, 0x409): None,
  254. (25, 3, 1, 0x409): "OpenSansCondensedItalic",
  255. },
  256. ),
  257. # Bad names
  258. (
  259. mavenpro_fp,
  260. "Maven Pro",
  261. "Fat", # this should get appended to the family name
  262. [],
  263. {
  264. (1, 3, 1, 0x409): "Maven Pro Fat",
  265. (2, 3, 1, 0x409): "Regular",
  266. (3, 3, 1, 0x409): "2.003;NONE;MavenProFat-Regular",
  267. (4, 3, 1, 0x409): "Maven Pro Fat Regular",
  268. (6, 3, 1, 0x409): "MavenProFat-Regular",
  269. (16, 3, 1, 0x409): None,
  270. (17, 3, 1, 0x409): None,
  271. },
  272. ),
  273. (
  274. wonky_fp,
  275. "Wonky", # name exists in axis reg!
  276. None,
  277. [],
  278. {
  279. (1, 3, 1, 0x409): "Wonky",
  280. (2, 3, 1, 0x409): "Regular",
  281. (3, 3, 1, 0x409): "3.000;GOOG;Wonky-Regular",
  282. (4, 3, 1, 0x409): "Wonky Regular",
  283. (6, 3, 1, 0x409): "Wonky-Regular",
  284. (16, 3, 1, 0x409): None,
  285. (17, 3, 1, 0x409): None,
  286. },
  287. ),
  288. ],
  289. )
  290. def test_name_table(fp, family_name, style_name, siblings, expected):
  291. font = TTFont(fp)
  292. siblings = [TTFont(fp) for fp in siblings]
  293. build_name_table(font, family_name, style_name, siblings)
  294. _test_names(font, expected)
  295. @pytest.mark.parametrize(
  296. "font_fp, dflt_coords, expected",
  297. [
  298. # Condensed variants
  299. (
  300. opensans_cond_roman_fp,
  301. {},
  302. [
  303. ("Thin", {"wght": 100.0}),
  304. ("ExtraLight", {"wght": 200.0}),
  305. ("Light", {"wght": 300.0}),
  306. ("Regular", {"wght": 400.0}),
  307. ("Medium", {"wght": 500.0}),
  308. ("SemiBold", {"wght": 600.0}),
  309. ("Bold", {"wght": 700.0}),
  310. ("ExtraBold", {"wght": 800.0}),
  311. ],
  312. ),
  313. (
  314. opensans_cond_italic_fp,
  315. {},
  316. [
  317. ("Thin Italic", {"wght": 100.0}),
  318. ("ExtraLight Italic", {"wght": 200.0}),
  319. ("Light Italic", {"wght": 300.0}),
  320. ("Italic", {"wght": 400.0}),
  321. ("Medium Italic", {"wght": 500.0}),
  322. ("SemiBold Italic", {"wght": 600.0}),
  323. ("Bold Italic", {"wght": 700.0}),
  324. ("ExtraBold Italic", {"wght": 800.0}),
  325. ],
  326. ),
  327. # Multi axis + roman & ital with dflt coords
  328. (
  329. roboto_flex_fp,
  330. {},
  331. [
  332. ("Thin", {"wght": 100.0}),
  333. ("ExtraLight", {"wght": 200.0}),
  334. ("Light", {"wght": 300.0}),
  335. ("Regular", {"wght": 400.0}),
  336. ("Medium", {"wght": 500.0}),
  337. ("SemiBold", {"wght": 600.0}),
  338. ("Bold", {"wght": 700.0}),
  339. ("ExtraBold", {"wght": 800.0}),
  340. ("Black", {"wght": 900.0}),
  341. ("Thin Italic", {"wght": 100.0, "slnt": -10}),
  342. ("ExtraLight Italic", {"wght": 200.0, "slnt": -10}),
  343. ("Light Italic", {"wght": 300.0, "slnt": -10}),
  344. ("Italic", {"wght": 400.0, "slnt": -10}),
  345. ("Medium Italic", {"wght": 500.0, "slnt": -10}),
  346. ("SemiBold Italic", {"wght": 600.0, "slnt": -10}),
  347. ("Bold Italic", {"wght": 700.0, "slnt": -10}),
  348. ("ExtraBold Italic", {"wght": 800.0, "slnt": -10}),
  349. ("Black Italic", {"wght": 900.0, "slnt": -10}),
  350. ],
  351. ),
  352. # multi axis with METADATA.pb registry_default_overrides
  353. # https://github.com/google/fonts/blob/main/ofl/robotoflex/METADATA.pb
  354. (
  355. roboto_flex_fp,
  356. {
  357. "XOPQ": 96.0,
  358. "XTRA": 468.0,
  359. "YOPQ": 79.0,
  360. "YTDE": -203.0,
  361. "YTFI": 738.0,
  362. "YTLC": 514.0,
  363. "YTUC": 712.0,
  364. },
  365. [
  366. ("Thin", {"wght": 100.0}),
  367. ("ExtraLight", {"wght": 200.0}),
  368. ("Light", {"wght": 300.0}),
  369. ("Regular", {"wght": 400.0}),
  370. ("Medium", {"wght": 500.0}),
  371. ("SemiBold", {"wght": 600.0}),
  372. ("Bold", {"wght": 700.0}),
  373. ("ExtraBold", {"wght": 800.0}),
  374. ("Black", {"wght": 900.0}),
  375. ("Thin Italic", {"wght": 100.0, "slnt": -10}),
  376. ("ExtraLight Italic", {"wght": 200.0, "slnt": -10}),
  377. ("Light Italic", {"wght": 300.0, "slnt": -10}),
  378. ("Italic", {"wght": 400.0, "slnt": -10}),
  379. ("Medium Italic", {"wght": 500.0, "slnt": -10}),
  380. ("SemiBold Italic", {"wght": 600.0, "slnt": -10}),
  381. ("Bold Italic", {"wght": 700.0, "slnt": -10}),
  382. ("ExtraBold Italic", {"wght": 800.0, "slnt": -10}),
  383. ("Black Italic", {"wght": 900.0, "slnt": -10}),
  384. ],
  385. ),
  386. ],
  387. )
  388. def test_fvar_instances(font_fp, dflt_coords, expected):
  389. font = TTFont(font_fp)
  390. builder = build_fvar_instances(font, dflt_coords)
  391. fvar = font["fvar"]
  392. wght_axis = next((a for a in fvar.axes if a.axisTag == "wght"), None)
  393. wght_min = wght_axis.minValue
  394. wght_max = wght_axis.maxValue
  395. instances = fvar.instances
  396. if not dflt_coords:
  397. dflt_coords = {k: v["value"] for k, v in _fvar_dflts(font).items()}
  398. assert len(instances) == len(expected)
  399. for inst, (expected_name, coords) in zip(instances, expected):
  400. inst_name = font["name"].getName(inst.subfamilyNameID, 3, 1, 0x409).toUnicode()
  401. assert inst_name == expected_name
  402. for tag, val in coords.items():
  403. dflt_coords[tag] = val
  404. assert inst.coordinates == dflt_coords
  405. def dump(table, ttFont=None):
  406. return "\n".join(getXML(table.toXML, ttFont))
  407. @pytest.mark.parametrize(
  408. "fp, sibling_fps",
  409. [
  410. (roboto_flex_fp, []),
  411. (
  412. opensans_roman_fp,
  413. [opensans_italic_fp, opensans_cond_roman_fp, opensans_cond_italic_fp],
  414. ),
  415. (
  416. opensans_italic_fp,
  417. [opensans_roman_fp, opensans_cond_roman_fp, opensans_cond_italic_fp],
  418. ),
  419. (
  420. opensans_cond_roman_fp,
  421. [opensans_roman_fp, opensans_italic_fp, opensans_cond_italic_fp],
  422. ),
  423. (
  424. opensans_cond_italic_fp,
  425. [opensans_roman_fp, opensans_italic_fp, opensans_cond_roman_fp],
  426. ),
  427. (wonky_fp, []),
  428. ],
  429. )
  430. def test_stat(fp, sibling_fps):
  431. font = TTFont(fp)
  432. siblings = [TTFont(f) for f in sibling_fps]
  433. build_stat(font, siblings)
  434. stat_fp = fp.replace(".ttf", "_STAT.ttx")
  435. ## output good files
  436. # with open(stat_fp, "w") as doc:
  437. # got = dump(font["STAT"], font)
  438. # doc.write(got)
  439. with open(stat_fp) as doc:
  440. expected = doc.read()
  441. got = dump(font["STAT"], font)
  442. assert got == expected
  443. @pytest.mark.parametrize(
  444. "fp, expected",
  445. [
  446. (mavenpro_fp, "MavenPro-Regular.ttf"),
  447. (opensans_roman_fp, "OpenSans[wdth,wght].ttf"),
  448. (opensans_italic_fp, "OpenSans-Italic[wdth,wght].ttf"),
  449. (opensans_cond_roman_fp, "OpenSansCondensed[wght].ttf"),
  450. (opensans_cond_italic_fp, "OpenSansCondensed-Italic[wght].ttf"),
  451. ],
  452. )
  453. def test_filename(fp, expected):
  454. font = TTFont(fp)
  455. assert build_filename(font) == expected
  456. @pytest.mark.parametrize(
  457. "fp, sibling_fps, result",
  458. [
  459. (roboto_flex_fp, [], False),
  460. (
  461. opensans_roman_fp,
  462. [opensans_italic_fp, opensans_cond_roman_fp, opensans_cond_italic_fp],
  463. True,
  464. ),
  465. (
  466. opensans_italic_fp,
  467. [opensans_roman_fp, opensans_cond_roman_fp, opensans_cond_italic_fp],
  468. True,
  469. ),
  470. (
  471. opensans_cond_roman_fp,
  472. [opensans_roman_fp, opensans_italic_fp, opensans_cond_italic_fp],
  473. True,
  474. ),
  475. (
  476. opensans_cond_italic_fp,
  477. [opensans_roman_fp, opensans_italic_fp, opensans_cond_roman_fp],
  478. True,
  479. ),
  480. (opensans_roman_fp, [opensans_cond_roman_fp], True),
  481. (opensans_roman_fp, [opensans_italic_fp], False),
  482. (wonky_fp, [], False),
  483. ],
  484. )
  485. def test_fvar_instance_collisions(fp, sibling_fps, result):
  486. ttFont = TTFont(fp)
  487. siblings = [TTFont(fp) for fp in sibling_fps]
  488. assert _fvar_instance_collisions(ttFont, siblings) == result
  489. @pytest.mark.parametrize(
  490. "fp, result",
  491. [
  492. (roboto_flex_fp, "RobotoFlex"),
  493. (opensans_roman_fp, "OpenSans"),
  494. (opensans_italic_fp, "OpenSansItalic"),
  495. (opensans_cond_roman_fp, "OpenSansCondensed"),
  496. (opensans_cond_italic_fp, "OpenSansCondensedItalic"),
  497. (wonky_fp, "Wonky"),
  498. ],
  499. )
  500. def test_build_variations_ps_name(fp, result):
  501. ttFont = TTFont(fp)
  502. build_variations_ps_name(ttFont)
  503. variation_ps_name = ttFont["name"].getName(25, 3, 1, 0x409).toUnicode()
  504. assert variation_ps_name == result