fonttools 4.60.2__cp311-cp311-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. fontTools/__init__.py +8 -0
  2. fontTools/__main__.py +35 -0
  3. fontTools/afmLib.py +439 -0
  4. fontTools/agl.py +5233 -0
  5. fontTools/annotations.py +30 -0
  6. fontTools/cffLib/CFF2ToCFF.py +258 -0
  7. fontTools/cffLib/CFFToCFF2.py +305 -0
  8. fontTools/cffLib/__init__.py +3694 -0
  9. fontTools/cffLib/specializer.py +927 -0
  10. fontTools/cffLib/transforms.py +495 -0
  11. fontTools/cffLib/width.py +210 -0
  12. fontTools/colorLib/__init__.py +0 -0
  13. fontTools/colorLib/builder.py +664 -0
  14. fontTools/colorLib/errors.py +2 -0
  15. fontTools/colorLib/geometry.py +143 -0
  16. fontTools/colorLib/table_builder.py +223 -0
  17. fontTools/colorLib/unbuilder.py +81 -0
  18. fontTools/config/__init__.py +90 -0
  19. fontTools/cu2qu/__init__.py +15 -0
  20. fontTools/cu2qu/__main__.py +6 -0
  21. fontTools/cu2qu/benchmark.py +54 -0
  22. fontTools/cu2qu/cli.py +198 -0
  23. fontTools/cu2qu/cu2qu.c +15817 -0
  24. fontTools/cu2qu/cu2qu.cp311-win32.pyd +0 -0
  25. fontTools/cu2qu/cu2qu.py +563 -0
  26. fontTools/cu2qu/errors.py +77 -0
  27. fontTools/cu2qu/ufo.py +363 -0
  28. fontTools/designspaceLib/__init__.py +3343 -0
  29. fontTools/designspaceLib/__main__.py +6 -0
  30. fontTools/designspaceLib/split.py +475 -0
  31. fontTools/designspaceLib/statNames.py +260 -0
  32. fontTools/designspaceLib/types.py +147 -0
  33. fontTools/encodings/MacRoman.py +258 -0
  34. fontTools/encodings/StandardEncoding.py +258 -0
  35. fontTools/encodings/__init__.py +1 -0
  36. fontTools/encodings/codecs.py +135 -0
  37. fontTools/feaLib/__init__.py +4 -0
  38. fontTools/feaLib/__main__.py +78 -0
  39. fontTools/feaLib/ast.py +2143 -0
  40. fontTools/feaLib/builder.py +1814 -0
  41. fontTools/feaLib/error.py +22 -0
  42. fontTools/feaLib/lexer.c +17029 -0
  43. fontTools/feaLib/lexer.cp311-win32.pyd +0 -0
  44. fontTools/feaLib/lexer.py +287 -0
  45. fontTools/feaLib/location.py +12 -0
  46. fontTools/feaLib/lookupDebugInfo.py +12 -0
  47. fontTools/feaLib/parser.py +2394 -0
  48. fontTools/feaLib/variableScalar.py +118 -0
  49. fontTools/fontBuilder.py +1014 -0
  50. fontTools/help.py +36 -0
  51. fontTools/merge/__init__.py +248 -0
  52. fontTools/merge/__main__.py +6 -0
  53. fontTools/merge/base.py +81 -0
  54. fontTools/merge/cmap.py +173 -0
  55. fontTools/merge/layout.py +526 -0
  56. fontTools/merge/options.py +85 -0
  57. fontTools/merge/tables.py +352 -0
  58. fontTools/merge/unicode.py +78 -0
  59. fontTools/merge/util.py +143 -0
  60. fontTools/misc/__init__.py +1 -0
  61. fontTools/misc/arrayTools.py +424 -0
  62. fontTools/misc/bezierTools.c +39731 -0
  63. fontTools/misc/bezierTools.cp311-win32.pyd +0 -0
  64. fontTools/misc/bezierTools.py +1500 -0
  65. fontTools/misc/classifyTools.py +170 -0
  66. fontTools/misc/cliTools.py +53 -0
  67. fontTools/misc/configTools.py +349 -0
  68. fontTools/misc/cython.py +27 -0
  69. fontTools/misc/dictTools.py +83 -0
  70. fontTools/misc/eexec.py +119 -0
  71. fontTools/misc/encodingTools.py +72 -0
  72. fontTools/misc/enumTools.py +23 -0
  73. fontTools/misc/etree.py +456 -0
  74. fontTools/misc/filenames.py +245 -0
  75. fontTools/misc/filesystem/__init__.py +68 -0
  76. fontTools/misc/filesystem/_base.py +134 -0
  77. fontTools/misc/filesystem/_copy.py +45 -0
  78. fontTools/misc/filesystem/_errors.py +54 -0
  79. fontTools/misc/filesystem/_info.py +75 -0
  80. fontTools/misc/filesystem/_osfs.py +164 -0
  81. fontTools/misc/filesystem/_path.py +67 -0
  82. fontTools/misc/filesystem/_subfs.py +92 -0
  83. fontTools/misc/filesystem/_tempfs.py +34 -0
  84. fontTools/misc/filesystem/_tools.py +34 -0
  85. fontTools/misc/filesystem/_walk.py +55 -0
  86. fontTools/misc/filesystem/_zipfs.py +204 -0
  87. fontTools/misc/fixedTools.py +253 -0
  88. fontTools/misc/intTools.py +25 -0
  89. fontTools/misc/iterTools.py +12 -0
  90. fontTools/misc/lazyTools.py +42 -0
  91. fontTools/misc/loggingTools.py +543 -0
  92. fontTools/misc/macCreatorType.py +56 -0
  93. fontTools/misc/macRes.py +261 -0
  94. fontTools/misc/plistlib/__init__.py +681 -0
  95. fontTools/misc/plistlib/py.typed +0 -0
  96. fontTools/misc/psCharStrings.py +1511 -0
  97. fontTools/misc/psLib.py +398 -0
  98. fontTools/misc/psOperators.py +572 -0
  99. fontTools/misc/py23.py +96 -0
  100. fontTools/misc/roundTools.py +110 -0
  101. fontTools/misc/sstruct.py +227 -0
  102. fontTools/misc/symfont.py +242 -0
  103. fontTools/misc/testTools.py +233 -0
  104. fontTools/misc/textTools.py +156 -0
  105. fontTools/misc/timeTools.py +88 -0
  106. fontTools/misc/transform.py +516 -0
  107. fontTools/misc/treeTools.py +45 -0
  108. fontTools/misc/vector.py +147 -0
  109. fontTools/misc/visitor.py +158 -0
  110. fontTools/misc/xmlReader.py +188 -0
  111. fontTools/misc/xmlWriter.py +231 -0
  112. fontTools/mtiLib/__init__.py +1400 -0
  113. fontTools/mtiLib/__main__.py +5 -0
  114. fontTools/otlLib/__init__.py +1 -0
  115. fontTools/otlLib/builder.py +3465 -0
  116. fontTools/otlLib/error.py +11 -0
  117. fontTools/otlLib/maxContextCalc.py +96 -0
  118. fontTools/otlLib/optimize/__init__.py +53 -0
  119. fontTools/otlLib/optimize/__main__.py +6 -0
  120. fontTools/otlLib/optimize/gpos.py +439 -0
  121. fontTools/pens/__init__.py +1 -0
  122. fontTools/pens/areaPen.py +52 -0
  123. fontTools/pens/basePen.py +475 -0
  124. fontTools/pens/boundsPen.py +98 -0
  125. fontTools/pens/cairoPen.py +26 -0
  126. fontTools/pens/cocoaPen.py +26 -0
  127. fontTools/pens/cu2quPen.py +325 -0
  128. fontTools/pens/explicitClosingLinePen.py +101 -0
  129. fontTools/pens/filterPen.py +433 -0
  130. fontTools/pens/freetypePen.py +462 -0
  131. fontTools/pens/hashPointPen.py +89 -0
  132. fontTools/pens/momentsPen.c +13378 -0
  133. fontTools/pens/momentsPen.cp311-win32.pyd +0 -0
  134. fontTools/pens/momentsPen.py +879 -0
  135. fontTools/pens/perimeterPen.py +69 -0
  136. fontTools/pens/pointInsidePen.py +192 -0
  137. fontTools/pens/pointPen.py +643 -0
  138. fontTools/pens/qtPen.py +29 -0
  139. fontTools/pens/qu2cuPen.py +105 -0
  140. fontTools/pens/quartzPen.py +43 -0
  141. fontTools/pens/recordingPen.py +335 -0
  142. fontTools/pens/reportLabPen.py +79 -0
  143. fontTools/pens/reverseContourPen.py +96 -0
  144. fontTools/pens/roundingPen.py +130 -0
  145. fontTools/pens/statisticsPen.py +312 -0
  146. fontTools/pens/svgPathPen.py +310 -0
  147. fontTools/pens/t2CharStringPen.py +88 -0
  148. fontTools/pens/teePen.py +55 -0
  149. fontTools/pens/transformPen.py +115 -0
  150. fontTools/pens/ttGlyphPen.py +335 -0
  151. fontTools/pens/wxPen.py +29 -0
  152. fontTools/qu2cu/__init__.py +15 -0
  153. fontTools/qu2cu/__main__.py +7 -0
  154. fontTools/qu2cu/benchmark.py +56 -0
  155. fontTools/qu2cu/cli.py +125 -0
  156. fontTools/qu2cu/qu2cu.c +16682 -0
  157. fontTools/qu2cu/qu2cu.cp311-win32.pyd +0 -0
  158. fontTools/qu2cu/qu2cu.py +405 -0
  159. fontTools/subset/__init__.py +4096 -0
  160. fontTools/subset/__main__.py +6 -0
  161. fontTools/subset/cff.py +184 -0
  162. fontTools/subset/svg.py +253 -0
  163. fontTools/subset/util.py +25 -0
  164. fontTools/svgLib/__init__.py +3 -0
  165. fontTools/svgLib/path/__init__.py +65 -0
  166. fontTools/svgLib/path/arc.py +154 -0
  167. fontTools/svgLib/path/parser.py +322 -0
  168. fontTools/svgLib/path/shapes.py +183 -0
  169. fontTools/t1Lib/__init__.py +648 -0
  170. fontTools/tfmLib.py +460 -0
  171. fontTools/ttLib/__init__.py +30 -0
  172. fontTools/ttLib/__main__.py +148 -0
  173. fontTools/ttLib/macUtils.py +54 -0
  174. fontTools/ttLib/removeOverlaps.py +395 -0
  175. fontTools/ttLib/reorderGlyphs.py +285 -0
  176. fontTools/ttLib/scaleUpem.py +436 -0
  177. fontTools/ttLib/sfnt.py +661 -0
  178. fontTools/ttLib/standardGlyphOrder.py +271 -0
  179. fontTools/ttLib/tables/B_A_S_E_.py +14 -0
  180. fontTools/ttLib/tables/BitmapGlyphMetrics.py +64 -0
  181. fontTools/ttLib/tables/C_B_D_T_.py +113 -0
  182. fontTools/ttLib/tables/C_B_L_C_.py +19 -0
  183. fontTools/ttLib/tables/C_F_F_.py +61 -0
  184. fontTools/ttLib/tables/C_F_F__2.py +26 -0
  185. fontTools/ttLib/tables/C_O_L_R_.py +165 -0
  186. fontTools/ttLib/tables/C_P_A_L_.py +305 -0
  187. fontTools/ttLib/tables/D_S_I_G_.py +158 -0
  188. fontTools/ttLib/tables/D__e_b_g.py +35 -0
  189. fontTools/ttLib/tables/DefaultTable.py +49 -0
  190. fontTools/ttLib/tables/E_B_D_T_.py +835 -0
  191. fontTools/ttLib/tables/E_B_L_C_.py +718 -0
  192. fontTools/ttLib/tables/F_F_T_M_.py +52 -0
  193. fontTools/ttLib/tables/F__e_a_t.py +149 -0
  194. fontTools/ttLib/tables/G_D_E_F_.py +13 -0
  195. fontTools/ttLib/tables/G_M_A_P_.py +148 -0
  196. fontTools/ttLib/tables/G_P_K_G_.py +133 -0
  197. fontTools/ttLib/tables/G_P_O_S_.py +14 -0
  198. fontTools/ttLib/tables/G_S_U_B_.py +13 -0
  199. fontTools/ttLib/tables/G_V_A_R_.py +5 -0
  200. fontTools/ttLib/tables/G__l_a_t.py +235 -0
  201. fontTools/ttLib/tables/G__l_o_c.py +85 -0
  202. fontTools/ttLib/tables/H_V_A_R_.py +13 -0
  203. fontTools/ttLib/tables/J_S_T_F_.py +13 -0
  204. fontTools/ttLib/tables/L_T_S_H_.py +58 -0
  205. fontTools/ttLib/tables/M_A_T_H_.py +13 -0
  206. fontTools/ttLib/tables/M_E_T_A_.py +352 -0
  207. fontTools/ttLib/tables/M_V_A_R_.py +13 -0
  208. fontTools/ttLib/tables/O_S_2f_2.py +752 -0
  209. fontTools/ttLib/tables/S_I_N_G_.py +99 -0
  210. fontTools/ttLib/tables/S_T_A_T_.py +15 -0
  211. fontTools/ttLib/tables/S_V_G_.py +223 -0
  212. fontTools/ttLib/tables/S__i_l_f.py +1040 -0
  213. fontTools/ttLib/tables/S__i_l_l.py +92 -0
  214. fontTools/ttLib/tables/T_S_I_B_.py +13 -0
  215. fontTools/ttLib/tables/T_S_I_C_.py +14 -0
  216. fontTools/ttLib/tables/T_S_I_D_.py +13 -0
  217. fontTools/ttLib/tables/T_S_I_J_.py +13 -0
  218. fontTools/ttLib/tables/T_S_I_P_.py +13 -0
  219. fontTools/ttLib/tables/T_S_I_S_.py +13 -0
  220. fontTools/ttLib/tables/T_S_I_V_.py +26 -0
  221. fontTools/ttLib/tables/T_S_I__0.py +70 -0
  222. fontTools/ttLib/tables/T_S_I__1.py +163 -0
  223. fontTools/ttLib/tables/T_S_I__2.py +17 -0
  224. fontTools/ttLib/tables/T_S_I__3.py +22 -0
  225. fontTools/ttLib/tables/T_S_I__5.py +60 -0
  226. fontTools/ttLib/tables/T_T_F_A_.py +14 -0
  227. fontTools/ttLib/tables/TupleVariation.py +884 -0
  228. fontTools/ttLib/tables/V_A_R_C_.py +12 -0
  229. fontTools/ttLib/tables/V_D_M_X_.py +249 -0
  230. fontTools/ttLib/tables/V_O_R_G_.py +165 -0
  231. fontTools/ttLib/tables/V_V_A_R_.py +13 -0
  232. fontTools/ttLib/tables/__init__.py +98 -0
  233. fontTools/ttLib/tables/_a_n_k_r.py +15 -0
  234. fontTools/ttLib/tables/_a_v_a_r.py +193 -0
  235. fontTools/ttLib/tables/_b_s_l_n.py +15 -0
  236. fontTools/ttLib/tables/_c_i_d_g.py +24 -0
  237. fontTools/ttLib/tables/_c_m_a_p.py +1591 -0
  238. fontTools/ttLib/tables/_c_v_a_r.py +94 -0
  239. fontTools/ttLib/tables/_c_v_t.py +56 -0
  240. fontTools/ttLib/tables/_f_e_a_t.py +15 -0
  241. fontTools/ttLib/tables/_f_p_g_m.py +62 -0
  242. fontTools/ttLib/tables/_f_v_a_r.py +261 -0
  243. fontTools/ttLib/tables/_g_a_s_p.py +63 -0
  244. fontTools/ttLib/tables/_g_c_i_d.py +13 -0
  245. fontTools/ttLib/tables/_g_l_y_f.py +2311 -0
  246. fontTools/ttLib/tables/_g_v_a_r.py +340 -0
  247. fontTools/ttLib/tables/_h_d_m_x.py +127 -0
  248. fontTools/ttLib/tables/_h_e_a_d.py +130 -0
  249. fontTools/ttLib/tables/_h_h_e_a.py +147 -0
  250. fontTools/ttLib/tables/_h_m_t_x.py +164 -0
  251. fontTools/ttLib/tables/_k_e_r_n.py +289 -0
  252. fontTools/ttLib/tables/_l_c_a_r.py +13 -0
  253. fontTools/ttLib/tables/_l_o_c_a.py +70 -0
  254. fontTools/ttLib/tables/_l_t_a_g.py +72 -0
  255. fontTools/ttLib/tables/_m_a_x_p.py +147 -0
  256. fontTools/ttLib/tables/_m_e_t_a.py +112 -0
  257. fontTools/ttLib/tables/_m_o_r_t.py +14 -0
  258. fontTools/ttLib/tables/_m_o_r_x.py +15 -0
  259. fontTools/ttLib/tables/_n_a_m_e.py +1242 -0
  260. fontTools/ttLib/tables/_o_p_b_d.py +14 -0
  261. fontTools/ttLib/tables/_p_o_s_t.py +319 -0
  262. fontTools/ttLib/tables/_p_r_e_p.py +16 -0
  263. fontTools/ttLib/tables/_p_r_o_p.py +12 -0
  264. fontTools/ttLib/tables/_s_b_i_x.py +129 -0
  265. fontTools/ttLib/tables/_t_r_a_k.py +332 -0
  266. fontTools/ttLib/tables/_v_h_e_a.py +139 -0
  267. fontTools/ttLib/tables/_v_m_t_x.py +19 -0
  268. fontTools/ttLib/tables/asciiTable.py +20 -0
  269. fontTools/ttLib/tables/grUtils.py +92 -0
  270. fontTools/ttLib/tables/otBase.py +1458 -0
  271. fontTools/ttLib/tables/otConverters.py +2068 -0
  272. fontTools/ttLib/tables/otData.py +6400 -0
  273. fontTools/ttLib/tables/otTables.py +2703 -0
  274. fontTools/ttLib/tables/otTraverse.py +163 -0
  275. fontTools/ttLib/tables/sbixGlyph.py +149 -0
  276. fontTools/ttLib/tables/sbixStrike.py +177 -0
  277. fontTools/ttLib/tables/table_API_readme.txt +91 -0
  278. fontTools/ttLib/tables/ttProgram.py +594 -0
  279. fontTools/ttLib/ttCollection.py +125 -0
  280. fontTools/ttLib/ttFont.py +1148 -0
  281. fontTools/ttLib/ttGlyphSet.py +490 -0
  282. fontTools/ttLib/ttVisitor.py +32 -0
  283. fontTools/ttLib/woff2.py +1680 -0
  284. fontTools/ttx.py +479 -0
  285. fontTools/ufoLib/__init__.py +2575 -0
  286. fontTools/ufoLib/converters.py +407 -0
  287. fontTools/ufoLib/errors.py +30 -0
  288. fontTools/ufoLib/etree.py +6 -0
  289. fontTools/ufoLib/filenames.py +356 -0
  290. fontTools/ufoLib/glifLib.py +2120 -0
  291. fontTools/ufoLib/kerning.py +141 -0
  292. fontTools/ufoLib/plistlib.py +47 -0
  293. fontTools/ufoLib/pointPen.py +6 -0
  294. fontTools/ufoLib/utils.py +107 -0
  295. fontTools/ufoLib/validators.py +1208 -0
  296. fontTools/unicode.py +50 -0
  297. fontTools/unicodedata/Blocks.py +817 -0
  298. fontTools/unicodedata/Mirrored.py +446 -0
  299. fontTools/unicodedata/OTTags.py +50 -0
  300. fontTools/unicodedata/ScriptExtensions.py +832 -0
  301. fontTools/unicodedata/Scripts.py +3639 -0
  302. fontTools/unicodedata/__init__.py +306 -0
  303. fontTools/varLib/__init__.py +1600 -0
  304. fontTools/varLib/__main__.py +6 -0
  305. fontTools/varLib/avar/__init__.py +0 -0
  306. fontTools/varLib/avar/__main__.py +72 -0
  307. fontTools/varLib/avar/build.py +79 -0
  308. fontTools/varLib/avar/map.py +108 -0
  309. fontTools/varLib/avar/plan.py +1004 -0
  310. fontTools/varLib/avar/unbuild.py +271 -0
  311. fontTools/varLib/avarPlanner.py +8 -0
  312. fontTools/varLib/builder.py +215 -0
  313. fontTools/varLib/cff.py +631 -0
  314. fontTools/varLib/errors.py +219 -0
  315. fontTools/varLib/featureVars.py +703 -0
  316. fontTools/varLib/hvar.py +113 -0
  317. fontTools/varLib/instancer/__init__.py +2052 -0
  318. fontTools/varLib/instancer/__main__.py +5 -0
  319. fontTools/varLib/instancer/featureVars.py +190 -0
  320. fontTools/varLib/instancer/names.py +388 -0
  321. fontTools/varLib/instancer/solver.py +309 -0
  322. fontTools/varLib/interpolatable.py +1209 -0
  323. fontTools/varLib/interpolatableHelpers.py +399 -0
  324. fontTools/varLib/interpolatablePlot.py +1269 -0
  325. fontTools/varLib/interpolatableTestContourOrder.py +82 -0
  326. fontTools/varLib/interpolatableTestStartingPoint.py +107 -0
  327. fontTools/varLib/interpolate_layout.py +124 -0
  328. fontTools/varLib/iup.c +19815 -0
  329. fontTools/varLib/iup.cp311-win32.pyd +0 -0
  330. fontTools/varLib/iup.py +490 -0
  331. fontTools/varLib/merger.py +1717 -0
  332. fontTools/varLib/models.py +642 -0
  333. fontTools/varLib/multiVarStore.py +253 -0
  334. fontTools/varLib/mutator.py +529 -0
  335. fontTools/varLib/mvar.py +40 -0
  336. fontTools/varLib/plot.py +238 -0
  337. fontTools/varLib/stat.py +149 -0
  338. fontTools/varLib/varStore.py +739 -0
  339. fontTools/voltLib/__init__.py +5 -0
  340. fontTools/voltLib/__main__.py +206 -0
  341. fontTools/voltLib/ast.py +452 -0
  342. fontTools/voltLib/error.py +12 -0
  343. fontTools/voltLib/lexer.py +99 -0
  344. fontTools/voltLib/parser.py +664 -0
  345. fontTools/voltLib/voltToFea.py +911 -0
  346. fonttools-4.60.2.data/data/share/man/man1/ttx.1 +225 -0
  347. fonttools-4.60.2.dist-info/METADATA +2250 -0
  348. fonttools-4.60.2.dist-info/RECORD +353 -0
  349. fonttools-4.60.2.dist-info/WHEEL +5 -0
  350. fonttools-4.60.2.dist-info/entry_points.txt +5 -0
  351. fonttools-4.60.2.dist-info/licenses/LICENSE +21 -0
  352. fonttools-4.60.2.dist-info/licenses/LICENSE.external +388 -0
  353. fonttools-4.60.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1209 @@
1
+ """
2
+ Tool to find wrong contour order between different masters, and
3
+ other interpolatability (or lack thereof) issues.
4
+
5
+ Call as:
6
+ $ fonttools varLib.interpolatable font1 font2 ...
7
+ """
8
+
9
+ from .interpolatableHelpers import *
10
+ from .interpolatableTestContourOrder import test_contour_order
11
+ from .interpolatableTestStartingPoint import test_starting_point
12
+ from fontTools.pens.recordingPen import (
13
+ RecordingPen,
14
+ DecomposingRecordingPen,
15
+ lerpRecordings,
16
+ )
17
+ from fontTools.pens.transformPen import TransformPen
18
+ from fontTools.pens.statisticsPen import StatisticsPen, StatisticsControlPen
19
+ from fontTools.pens.momentsPen import OpenContourError
20
+ from fontTools.varLib.models import piecewiseLinearMap, normalizeLocation
21
+ from fontTools.misc.fixedTools import floatToFixedToStr
22
+ from fontTools.misc.transform import Transform
23
+ from collections import defaultdict
24
+ from types import SimpleNamespace
25
+ from functools import wraps
26
+ from pprint import pformat
27
+ from math import sqrt, atan2, pi
28
+ import logging
29
+ import os
30
+
31
+ log = logging.getLogger("fontTools.varLib.interpolatable")
32
+
33
+ DEFAULT_TOLERANCE = 0.95
34
+ DEFAULT_KINKINESS = 0.5
35
+ DEFAULT_KINKINESS_LENGTH = 0.002 # ratio of UPEM
36
+ DEFAULT_UPEM = 1000
37
+
38
+
39
+ class Glyph:
40
+ ITEMS = (
41
+ "recordings",
42
+ "greenStats",
43
+ "controlStats",
44
+ "greenVectors",
45
+ "controlVectors",
46
+ "nodeTypes",
47
+ "isomorphisms",
48
+ "points",
49
+ "openContours",
50
+ )
51
+
52
+ def __init__(self, glyphname, glyphset):
53
+ self.name = glyphname
54
+ for item in self.ITEMS:
55
+ setattr(self, item, [])
56
+ self._populate(glyphset)
57
+
58
+ def _fill_in(self, ix):
59
+ for item in self.ITEMS:
60
+ if len(getattr(self, item)) == ix:
61
+ getattr(self, item).append(None)
62
+
63
+ def _populate(self, glyphset):
64
+ glyph = glyphset[self.name]
65
+ self.doesnt_exist = glyph is None
66
+ if self.doesnt_exist:
67
+ return
68
+
69
+ perContourPen = PerContourOrComponentPen(RecordingPen, glyphset=glyphset)
70
+ try:
71
+ glyph.draw(perContourPen, outputImpliedClosingLine=True)
72
+ except TypeError:
73
+ glyph.draw(perContourPen)
74
+ self.recordings = perContourPen.value
75
+ del perContourPen
76
+
77
+ for ix, contour in enumerate(self.recordings):
78
+ nodeTypes = [op for op, arg in contour.value]
79
+ self.nodeTypes.append(nodeTypes)
80
+
81
+ greenStats = StatisticsPen(glyphset=glyphset)
82
+ controlStats = StatisticsControlPen(glyphset=glyphset)
83
+ try:
84
+ contour.replay(greenStats)
85
+ contour.replay(controlStats)
86
+ self.openContours.append(False)
87
+ except OpenContourError as e:
88
+ self.openContours.append(True)
89
+ self._fill_in(ix)
90
+ continue
91
+ self.greenStats.append(greenStats)
92
+ self.controlStats.append(controlStats)
93
+ self.greenVectors.append(contour_vector_from_stats(greenStats))
94
+ self.controlVectors.append(contour_vector_from_stats(controlStats))
95
+
96
+ # Check starting point
97
+ if nodeTypes[0] == "addComponent":
98
+ self._fill_in(ix)
99
+ continue
100
+
101
+ assert nodeTypes[0] == "moveTo"
102
+ assert nodeTypes[-1] in ("closePath", "endPath")
103
+ points = SimpleRecordingPointPen()
104
+ converter = SegmentToPointPen(points, False)
105
+ contour.replay(converter)
106
+ # points.value is a list of pt,bool where bool is true if on-curve and false if off-curve;
107
+ # now check all rotations and mirror-rotations of the contour and build list of isomorphic
108
+ # possible starting points.
109
+ self.points.append(points.value)
110
+
111
+ isomorphisms = []
112
+ self.isomorphisms.append(isomorphisms)
113
+
114
+ # Add rotations
115
+ add_isomorphisms(points.value, isomorphisms, False)
116
+ # Add mirrored rotations
117
+ add_isomorphisms(points.value, isomorphisms, True)
118
+
119
+ def draw(self, pen, countor_idx=None):
120
+ if countor_idx is None:
121
+ for contour in self.recordings:
122
+ contour.draw(pen)
123
+ else:
124
+ self.recordings[countor_idx].draw(pen)
125
+
126
+
127
+ def test_gen(
128
+ glyphsets,
129
+ glyphs=None,
130
+ names=None,
131
+ ignore_missing=False,
132
+ *,
133
+ locations=None,
134
+ tolerance=DEFAULT_TOLERANCE,
135
+ kinkiness=DEFAULT_KINKINESS,
136
+ upem=DEFAULT_UPEM,
137
+ show_all=False,
138
+ discrete_axes=[],
139
+ ):
140
+ if tolerance >= 10:
141
+ tolerance *= 0.01
142
+ assert 0 <= tolerance <= 1
143
+ if kinkiness >= 10:
144
+ kinkiness *= 0.01
145
+ assert 0 <= kinkiness
146
+
147
+ names = names or [repr(g) for g in glyphsets]
148
+
149
+ if glyphs is None:
150
+ # `glyphs = glyphsets[0].keys()` is faster, certainly, but doesn't allow for sparse TTFs/OTFs given out of order
151
+ # ... risks the sparse master being the first one, and only processing a subset of the glyphs
152
+ glyphs = {g for glyphset in glyphsets for g in glyphset.keys()}
153
+
154
+ parents, order = find_parents_and_order(
155
+ glyphsets, locations, discrete_axes=discrete_axes
156
+ )
157
+
158
+ def grand_parent(i, glyphname):
159
+ if i is None:
160
+ return None
161
+ i = parents[i]
162
+ if i is None:
163
+ return None
164
+ while parents[i] is not None and glyphsets[i][glyphname] is None:
165
+ i = parents[i]
166
+ return i
167
+
168
+ for glyph_name in glyphs:
169
+ log.info("Testing glyph %s", glyph_name)
170
+ allGlyphs = [Glyph(glyph_name, glyphset) for glyphset in glyphsets]
171
+ if len([1 for glyph in allGlyphs if glyph is not None]) <= 1:
172
+ continue
173
+ for master_idx, (glyph, glyphset, name) in enumerate(
174
+ zip(allGlyphs, glyphsets, names)
175
+ ):
176
+ if glyph.doesnt_exist:
177
+ if not ignore_missing:
178
+ yield (
179
+ glyph_name,
180
+ {
181
+ "type": InterpolatableProblem.MISSING,
182
+ "master": name,
183
+ "master_idx": master_idx,
184
+ },
185
+ )
186
+ continue
187
+
188
+ has_open = False
189
+ for ix, open in enumerate(glyph.openContours):
190
+ if not open:
191
+ continue
192
+ has_open = True
193
+ yield (
194
+ glyph_name,
195
+ {
196
+ "type": InterpolatableProblem.OPEN_PATH,
197
+ "master": name,
198
+ "master_idx": master_idx,
199
+ "contour": ix,
200
+ },
201
+ )
202
+ if has_open:
203
+ continue
204
+
205
+ matchings = [None] * len(glyphsets)
206
+
207
+ for m1idx in order:
208
+ glyph1 = allGlyphs[m1idx]
209
+ if glyph1 is None or not glyph1.nodeTypes:
210
+ continue
211
+ m0idx = grand_parent(m1idx, glyph_name)
212
+ if m0idx is None:
213
+ continue
214
+ glyph0 = allGlyphs[m0idx]
215
+ if glyph0 is None or not glyph0.nodeTypes:
216
+ continue
217
+
218
+ #
219
+ # Basic compatibility checks
220
+ #
221
+
222
+ m1 = glyph0.nodeTypes
223
+ m0 = glyph1.nodeTypes
224
+ if len(m0) != len(m1):
225
+ yield (
226
+ glyph_name,
227
+ {
228
+ "type": InterpolatableProblem.PATH_COUNT,
229
+ "master_1": names[m0idx],
230
+ "master_2": names[m1idx],
231
+ "master_1_idx": m0idx,
232
+ "master_2_idx": m1idx,
233
+ "value_1": len(m0),
234
+ "value_2": len(m1),
235
+ },
236
+ )
237
+ continue
238
+
239
+ if m0 != m1:
240
+ for pathIx, (nodes1, nodes2) in enumerate(zip(m0, m1)):
241
+ if nodes1 == nodes2:
242
+ continue
243
+ if len(nodes1) != len(nodes2):
244
+ yield (
245
+ glyph_name,
246
+ {
247
+ "type": InterpolatableProblem.NODE_COUNT,
248
+ "path": pathIx,
249
+ "master_1": names[m0idx],
250
+ "master_2": names[m1idx],
251
+ "master_1_idx": m0idx,
252
+ "master_2_idx": m1idx,
253
+ "value_1": len(nodes1),
254
+ "value_2": len(nodes2),
255
+ },
256
+ )
257
+ continue
258
+ for nodeIx, (n1, n2) in enumerate(zip(nodes1, nodes2)):
259
+ if n1 != n2:
260
+ yield (
261
+ glyph_name,
262
+ {
263
+ "type": InterpolatableProblem.NODE_INCOMPATIBILITY,
264
+ "path": pathIx,
265
+ "node": nodeIx,
266
+ "master_1": names[m0idx],
267
+ "master_2": names[m1idx],
268
+ "master_1_idx": m0idx,
269
+ "master_2_idx": m1idx,
270
+ "value_1": n1,
271
+ "value_2": n2,
272
+ },
273
+ )
274
+ continue
275
+
276
+ #
277
+ # InterpolatableProblem.CONTOUR_ORDER check
278
+ #
279
+
280
+ this_tolerance, matching = test_contour_order(glyph0, glyph1)
281
+ if this_tolerance < tolerance:
282
+ yield (
283
+ glyph_name,
284
+ {
285
+ "type": InterpolatableProblem.CONTOUR_ORDER,
286
+ "master_1": names[m0idx],
287
+ "master_2": names[m1idx],
288
+ "master_1_idx": m0idx,
289
+ "master_2_idx": m1idx,
290
+ "value_1": list(range(len(matching))),
291
+ "value_2": matching,
292
+ "tolerance": this_tolerance,
293
+ },
294
+ )
295
+ matchings[m1idx] = matching
296
+
297
+ #
298
+ # wrong-start-point / weight check
299
+ #
300
+
301
+ m0Isomorphisms = glyph0.isomorphisms
302
+ m1Isomorphisms = glyph1.isomorphisms
303
+ m0Vectors = glyph0.greenVectors
304
+ m1Vectors = glyph1.greenVectors
305
+ recording0 = glyph0.recordings
306
+ recording1 = glyph1.recordings
307
+
308
+ # If contour-order is wrong, adjust it
309
+ matching = matchings[m1idx]
310
+ if (
311
+ matching is not None and m1Isomorphisms
312
+ ): # m1 is empty for composite glyphs
313
+ m1Isomorphisms = [m1Isomorphisms[i] for i in matching]
314
+ m1Vectors = [m1Vectors[i] for i in matching]
315
+ recording1 = [recording1[i] for i in matching]
316
+
317
+ midRecording = []
318
+ for c0, c1 in zip(recording0, recording1):
319
+ try:
320
+ r = RecordingPen()
321
+ r.value = list(lerpRecordings(c0.value, c1.value))
322
+ midRecording.append(r)
323
+ except ValueError:
324
+ # Mismatch because of the reordering above
325
+ midRecording.append(None)
326
+
327
+ for ix, (contour0, contour1) in enumerate(
328
+ zip(m0Isomorphisms, m1Isomorphisms)
329
+ ):
330
+ if (
331
+ contour0 is None
332
+ or contour1 is None
333
+ or len(contour0) == 0
334
+ or len(contour0) != len(contour1)
335
+ ):
336
+ # We already reported this; or nothing to do; or not compatible
337
+ # after reordering above.
338
+ continue
339
+
340
+ this_tolerance, proposed_point, reverse = test_starting_point(
341
+ glyph0, glyph1, ix, tolerance, matching
342
+ )
343
+
344
+ if this_tolerance < tolerance:
345
+ yield (
346
+ glyph_name,
347
+ {
348
+ "type": InterpolatableProblem.WRONG_START_POINT,
349
+ "contour": ix,
350
+ "master_1": names[m0idx],
351
+ "master_2": names[m1idx],
352
+ "master_1_idx": m0idx,
353
+ "master_2_idx": m1idx,
354
+ "value_1": 0,
355
+ "value_2": proposed_point,
356
+ "reversed": reverse,
357
+ "tolerance": this_tolerance,
358
+ },
359
+ )
360
+
361
+ # Weight check.
362
+ #
363
+ # If contour could be mid-interpolated, and the two
364
+ # contours have the same area sign, proceeed.
365
+ #
366
+ # The sign difference can happen if it's a weirdo
367
+ # self-intersecting contour; ignore it.
368
+ contour = midRecording[ix]
369
+
370
+ if contour and (m0Vectors[ix][0] < 0) == (m1Vectors[ix][0] < 0):
371
+ midStats = StatisticsPen(glyphset=None)
372
+ contour.replay(midStats)
373
+
374
+ midVector = contour_vector_from_stats(midStats)
375
+
376
+ m0Vec = m0Vectors[ix]
377
+ m1Vec = m1Vectors[ix]
378
+ size0 = m0Vec[0] * m0Vec[0]
379
+ size1 = m1Vec[0] * m1Vec[0]
380
+ midSize = midVector[0] * midVector[0]
381
+
382
+ for overweight, problem_type in enumerate(
383
+ (
384
+ InterpolatableProblem.UNDERWEIGHT,
385
+ InterpolatableProblem.OVERWEIGHT,
386
+ )
387
+ ):
388
+ if overweight:
389
+ expectedSize = max(size0, size1)
390
+ continue
391
+ else:
392
+ expectedSize = sqrt(size0 * size1)
393
+
394
+ log.debug(
395
+ "%s: actual size %g; threshold size %g, master sizes: %g, %g",
396
+ problem_type,
397
+ midSize,
398
+ expectedSize,
399
+ size0,
400
+ size1,
401
+ )
402
+
403
+ if (
404
+ not overweight and expectedSize * tolerance > midSize + 1e-5
405
+ ) or (overweight and 1e-5 + expectedSize / tolerance < midSize):
406
+ try:
407
+ if overweight:
408
+ this_tolerance = expectedSize / midSize
409
+ else:
410
+ this_tolerance = midSize / expectedSize
411
+ except ZeroDivisionError:
412
+ this_tolerance = 0
413
+ log.debug("tolerance %g", this_tolerance)
414
+ yield (
415
+ glyph_name,
416
+ {
417
+ "type": problem_type,
418
+ "contour": ix,
419
+ "master_1": names[m0idx],
420
+ "master_2": names[m1idx],
421
+ "master_1_idx": m0idx,
422
+ "master_2_idx": m1idx,
423
+ "tolerance": this_tolerance,
424
+ },
425
+ )
426
+
427
+ #
428
+ # "kink" detector
429
+ #
430
+ m0 = glyph0.points
431
+ m1 = glyph1.points
432
+
433
+ # If contour-order is wrong, adjust it
434
+ if matchings[m1idx] is not None and m1: # m1 is empty for composite glyphs
435
+ m1 = [m1[i] for i in matchings[m1idx]]
436
+
437
+ t = 0.1 # ~sin(radian(6)) for tolerance 0.95
438
+ deviation_threshold = (
439
+ upem * DEFAULT_KINKINESS_LENGTH * DEFAULT_KINKINESS / kinkiness
440
+ )
441
+
442
+ for ix, (contour0, contour1) in enumerate(zip(m0, m1)):
443
+ if (
444
+ contour0 is None
445
+ or contour1 is None
446
+ or len(contour0) == 0
447
+ or len(contour0) != len(contour1)
448
+ ):
449
+ # We already reported this; or nothing to do; or not compatible
450
+ # after reordering above.
451
+ continue
452
+
453
+ # Walk the contour, keeping track of three consecutive points, with
454
+ # middle one being an on-curve. If the three are co-linear then
455
+ # check for kinky-ness.
456
+ for i in range(len(contour0)):
457
+ pt0 = contour0[i]
458
+ pt1 = contour1[i]
459
+ if not pt0[1] or not pt1[1]:
460
+ # Skip off-curves
461
+ continue
462
+ pt0_prev = contour0[i - 1]
463
+ pt1_prev = contour1[i - 1]
464
+ pt0_next = contour0[(i + 1) % len(contour0)]
465
+ pt1_next = contour1[(i + 1) % len(contour1)]
466
+
467
+ if pt0_prev[1] and pt1_prev[1]:
468
+ # At least one off-curve is required
469
+ continue
470
+ if pt0_prev[1] and pt1_prev[1]:
471
+ # At least one off-curve is required
472
+ continue
473
+
474
+ pt0 = complex(*pt0[0])
475
+ pt1 = complex(*pt1[0])
476
+ pt0_prev = complex(*pt0_prev[0])
477
+ pt1_prev = complex(*pt1_prev[0])
478
+ pt0_next = complex(*pt0_next[0])
479
+ pt1_next = complex(*pt1_next[0])
480
+
481
+ # We have three consecutive points. Check whether
482
+ # they are colinear.
483
+ d0_prev = pt0 - pt0_prev
484
+ d0_next = pt0_next - pt0
485
+ d1_prev = pt1 - pt1_prev
486
+ d1_next = pt1_next - pt1
487
+
488
+ sin0 = d0_prev.real * d0_next.imag - d0_prev.imag * d0_next.real
489
+ sin1 = d1_prev.real * d1_next.imag - d1_prev.imag * d1_next.real
490
+ try:
491
+ sin0 /= abs(d0_prev) * abs(d0_next)
492
+ sin1 /= abs(d1_prev) * abs(d1_next)
493
+ except ZeroDivisionError:
494
+ continue
495
+
496
+ if abs(sin0) > t or abs(sin1) > t:
497
+ # Not colinear / not smooth.
498
+ continue
499
+
500
+ # Check the mid-point is actually, well, in the middle.
501
+ dot0 = d0_prev.real * d0_next.real + d0_prev.imag * d0_next.imag
502
+ dot1 = d1_prev.real * d1_next.real + d1_prev.imag * d1_next.imag
503
+ if dot0 < 0 or dot1 < 0:
504
+ # Sharp corner.
505
+ continue
506
+
507
+ # Fine, if handle ratios are similar...
508
+ r0 = abs(d0_prev) / (abs(d0_prev) + abs(d0_next))
509
+ r1 = abs(d1_prev) / (abs(d1_prev) + abs(d1_next))
510
+ r_diff = abs(r0 - r1)
511
+ if abs(r_diff) < t:
512
+ # Smooth enough.
513
+ continue
514
+
515
+ mid = (pt0 + pt1) / 2
516
+ mid_prev = (pt0_prev + pt1_prev) / 2
517
+ mid_next = (pt0_next + pt1_next) / 2
518
+
519
+ mid_d0 = mid - mid_prev
520
+ mid_d1 = mid_next - mid
521
+
522
+ sin_mid = mid_d0.real * mid_d1.imag - mid_d0.imag * mid_d1.real
523
+ try:
524
+ sin_mid /= abs(mid_d0) * abs(mid_d1)
525
+ except ZeroDivisionError:
526
+ continue
527
+
528
+ # ...or if the angles are similar.
529
+ if abs(sin_mid) * (tolerance * kinkiness) <= t:
530
+ # Smooth enough.
531
+ continue
532
+
533
+ # How visible is the kink?
534
+
535
+ cross = sin_mid * abs(mid_d0) * abs(mid_d1)
536
+ arc_len = abs(mid_d0 + mid_d1)
537
+ deviation = abs(cross / arc_len)
538
+ if deviation < deviation_threshold:
539
+ continue
540
+ deviation_ratio = deviation / arc_len
541
+ if deviation_ratio > t:
542
+ continue
543
+
544
+ this_tolerance = t / (abs(sin_mid) * kinkiness)
545
+
546
+ log.debug(
547
+ "kink: deviation %g; deviation_ratio %g; sin_mid %g; r_diff %g",
548
+ deviation,
549
+ deviation_ratio,
550
+ sin_mid,
551
+ r_diff,
552
+ )
553
+ log.debug("tolerance %g", this_tolerance)
554
+ yield (
555
+ glyph_name,
556
+ {
557
+ "type": InterpolatableProblem.KINK,
558
+ "contour": ix,
559
+ "master_1": names[m0idx],
560
+ "master_2": names[m1idx],
561
+ "master_1_idx": m0idx,
562
+ "master_2_idx": m1idx,
563
+ "value": i,
564
+ "tolerance": this_tolerance,
565
+ },
566
+ )
567
+
568
+ #
569
+ # --show-all
570
+ #
571
+
572
+ if show_all:
573
+ yield (
574
+ glyph_name,
575
+ {
576
+ "type": InterpolatableProblem.NOTHING,
577
+ "master_1": names[m0idx],
578
+ "master_2": names[m1idx],
579
+ "master_1_idx": m0idx,
580
+ "master_2_idx": m1idx,
581
+ },
582
+ )
583
+
584
+
585
+ @wraps(test_gen)
586
+ def test(*args, **kwargs):
587
+ problems = defaultdict(list)
588
+ for glyphname, problem in test_gen(*args, **kwargs):
589
+ problems[glyphname].append(problem)
590
+ return problems
591
+
592
+
593
+ def recursivelyAddGlyph(glyphname, glyphset, ttGlyphSet, glyf):
594
+ if glyphname in glyphset:
595
+ return
596
+ glyphset[glyphname] = ttGlyphSet[glyphname]
597
+
598
+ for component in getattr(glyf[glyphname], "components", []):
599
+ recursivelyAddGlyph(component.glyphName, glyphset, ttGlyphSet, glyf)
600
+
601
+
602
+ def ensure_parent_dir(path):
603
+ dirname = os.path.dirname(path)
604
+ if dirname:
605
+ os.makedirs(dirname, exist_ok=True)
606
+ return path
607
+
608
+
609
+ def main(args=None):
610
+ """Test for interpolatability issues between fonts"""
611
+ import argparse
612
+ import sys
613
+
614
+ parser = argparse.ArgumentParser(
615
+ "fonttools varLib.interpolatable",
616
+ description=main.__doc__,
617
+ )
618
+ parser.add_argument(
619
+ "--glyphs",
620
+ action="store",
621
+ help="Space-separate name of glyphs to check",
622
+ )
623
+ parser.add_argument(
624
+ "--show-all",
625
+ action="store_true",
626
+ help="Show all glyph pairs, even if no problems are found",
627
+ )
628
+ parser.add_argument(
629
+ "--tolerance",
630
+ action="store",
631
+ type=float,
632
+ help="Error tolerance. Between 0 and 1. Default %s" % DEFAULT_TOLERANCE,
633
+ )
634
+ parser.add_argument(
635
+ "--kinkiness",
636
+ action="store",
637
+ type=float,
638
+ help="How aggressively report kinks. Default %s" % DEFAULT_KINKINESS,
639
+ )
640
+ parser.add_argument(
641
+ "--json",
642
+ action="store_true",
643
+ help="Output report in JSON format",
644
+ )
645
+ parser.add_argument(
646
+ "--pdf",
647
+ action="store",
648
+ help="Output report in PDF format",
649
+ )
650
+ parser.add_argument(
651
+ "--ps",
652
+ action="store",
653
+ help="Output report in PostScript format",
654
+ )
655
+ parser.add_argument(
656
+ "--html",
657
+ action="store",
658
+ help="Output report in HTML format",
659
+ )
660
+ parser.add_argument(
661
+ "--quiet",
662
+ action="store_true",
663
+ help="Only exit with code 1 or 0, no output",
664
+ )
665
+ parser.add_argument(
666
+ "--output",
667
+ action="store",
668
+ help="Output file for the problem report; Default: stdout",
669
+ )
670
+ parser.add_argument(
671
+ "--ignore-missing",
672
+ action="store_true",
673
+ help="Will not report glyphs missing from sparse masters as errors",
674
+ )
675
+ parser.add_argument(
676
+ "inputs",
677
+ metavar="FILE",
678
+ type=str,
679
+ nargs="+",
680
+ help="Input a single variable font / DesignSpace / Glyphs file, or multiple TTF/UFO files",
681
+ )
682
+ parser.add_argument(
683
+ "--name",
684
+ metavar="NAME",
685
+ type=str,
686
+ action="append",
687
+ help="Name of the master to use in the report. If not provided, all are used.",
688
+ )
689
+ parser.add_argument("-v", "--verbose", action="store_true", help="Run verbosely.")
690
+ parser.add_argument("--debug", action="store_true", help="Run with debug output.")
691
+
692
+ args = parser.parse_args(args)
693
+
694
+ from fontTools import configLogger
695
+
696
+ configLogger(level=("INFO" if args.verbose else "WARNING"))
697
+ if args.debug:
698
+ configLogger(level="DEBUG")
699
+
700
+ glyphs = args.glyphs.split() if args.glyphs else None
701
+
702
+ from os.path import basename
703
+
704
+ fonts = []
705
+ names = []
706
+ locations = []
707
+ discrete_axes = set()
708
+ upem = DEFAULT_UPEM
709
+
710
+ original_args_inputs = tuple(args.inputs)
711
+
712
+ if len(args.inputs) == 1:
713
+ designspace = None
714
+ if args.inputs[0].endswith(".designspace"):
715
+ from fontTools.designspaceLib import DesignSpaceDocument
716
+
717
+ designspace = DesignSpaceDocument.fromfile(args.inputs[0])
718
+ args.inputs = [master.path for master in designspace.sources]
719
+ locations = [master.location for master in designspace.sources]
720
+ discrete_axes = {
721
+ a.name for a in designspace.axes if not hasattr(a, "minimum")
722
+ }
723
+ axis_triples = {
724
+ a.name: (a.minimum, a.default, a.maximum)
725
+ for a in designspace.axes
726
+ if a.name not in discrete_axes
727
+ }
728
+ axis_mappings = {a.name: a.map for a in designspace.axes}
729
+ axis_triples = {
730
+ k: tuple(piecewiseLinearMap(v, dict(axis_mappings[k])) for v in vv)
731
+ for k, vv in axis_triples.items()
732
+ }
733
+
734
+ elif args.inputs[0].endswith((".glyphs", ".glyphspackage")):
735
+ from glyphsLib import GSFont, to_designspace
736
+
737
+ gsfont = GSFont(args.inputs[0])
738
+ upem = gsfont.upm
739
+ designspace = to_designspace(gsfont)
740
+ fonts = [source.font for source in designspace.sources]
741
+ names = ["%s-%s" % (f.info.familyName, f.info.styleName) for f in fonts]
742
+ args.inputs = []
743
+ locations = [master.location for master in designspace.sources]
744
+ axis_triples = {
745
+ a.name: (a.minimum, a.default, a.maximum) for a in designspace.axes
746
+ }
747
+ axis_mappings = {a.name: a.map for a in designspace.axes}
748
+ axis_triples = {
749
+ k: tuple(piecewiseLinearMap(v, dict(axis_mappings[k])) for v in vv)
750
+ for k, vv in axis_triples.items()
751
+ }
752
+
753
+ elif args.inputs[0].endswith(".ttf") or args.inputs[0].endswith(".otf"):
754
+ from fontTools.ttLib import TTFont
755
+
756
+ # Is variable font?
757
+
758
+ font = TTFont(args.inputs[0])
759
+ upem = font["head"].unitsPerEm
760
+
761
+ fvar = font["fvar"]
762
+ axisMapping = {}
763
+ for axis in fvar.axes:
764
+ axisMapping[axis.axisTag] = {
765
+ -1: axis.minValue,
766
+ 0: axis.defaultValue,
767
+ 1: axis.maxValue,
768
+ }
769
+ normalized = False
770
+ if "avar" in font:
771
+ avar = font["avar"]
772
+ if getattr(avar.table, "VarStore", None):
773
+ axisMapping = {tag: {-1: -1, 0: 0, 1: 1} for tag in axisMapping}
774
+ normalized = True
775
+ else:
776
+ for axisTag, segments in avar.segments.items():
777
+ fvarMapping = axisMapping[axisTag].copy()
778
+ for location, value in segments.items():
779
+ axisMapping[axisTag][value] = piecewiseLinearMap(
780
+ location, fvarMapping
781
+ )
782
+
783
+ # Gather all glyphs at their "master" locations
784
+ ttGlyphSets = {}
785
+ glyphsets = defaultdict(dict)
786
+
787
+ if "gvar" in font:
788
+ gvar = font["gvar"]
789
+ glyf = font["glyf"]
790
+
791
+ if glyphs is None:
792
+ glyphs = sorted(gvar.variations.keys())
793
+ for glyphname in glyphs:
794
+ for var in gvar.variations[glyphname]:
795
+ locDict = {}
796
+ loc = []
797
+ for tag, val in sorted(var.axes.items()):
798
+ locDict[tag] = val[1]
799
+ loc.append((tag, val[1]))
800
+
801
+ locTuple = tuple(loc)
802
+ if locTuple not in ttGlyphSets:
803
+ ttGlyphSets[locTuple] = font.getGlyphSet(
804
+ location=locDict, normalized=True, recalcBounds=False
805
+ )
806
+
807
+ recursivelyAddGlyph(
808
+ glyphname, glyphsets[locTuple], ttGlyphSets[locTuple], glyf
809
+ )
810
+
811
+ elif "CFF2" in font:
812
+ fvarAxes = font["fvar"].axes
813
+ cff2 = font["CFF2"].cff.topDictIndex[0]
814
+ charstrings = cff2.CharStrings
815
+
816
+ if glyphs is None:
817
+ glyphs = sorted(charstrings.keys())
818
+ for glyphname in glyphs:
819
+ cs = charstrings[glyphname]
820
+ private = cs.private
821
+
822
+ # Extract vsindex for the glyph
823
+ vsindices = {getattr(private, "vsindex", 0)}
824
+ vsindex = getattr(private, "vsindex", 0)
825
+ last_op = 0
826
+ # The spec says vsindex can only appear once and must be the first
827
+ # operator in the charstring, but we support multiple.
828
+ # https://github.com/harfbuzz/boring-expansion-spec/issues/158
829
+ for op in enumerate(cs.program):
830
+ if op == "blend":
831
+ vsindices.add(vsindex)
832
+ elif op == "vsindex":
833
+ assert isinstance(last_op, int)
834
+ vsindex = last_op
835
+ last_op = op
836
+
837
+ if not hasattr(private, "vstore"):
838
+ continue
839
+
840
+ varStore = private.vstore.otVarStore
841
+ for vsindex in vsindices:
842
+ varData = varStore.VarData[vsindex]
843
+ for regionIndex in varData.VarRegionIndex:
844
+ region = varStore.VarRegionList.Region[regionIndex]
845
+
846
+ locDict = {}
847
+ loc = []
848
+ for axisIndex, axis in enumerate(region.VarRegionAxis):
849
+ tag = fvarAxes[axisIndex].axisTag
850
+ val = axis.PeakCoord
851
+ locDict[tag] = val
852
+ loc.append((tag, val))
853
+
854
+ locTuple = tuple(loc)
855
+ if locTuple not in ttGlyphSets:
856
+ ttGlyphSets[locTuple] = font.getGlyphSet(
857
+ location=locDict,
858
+ normalized=True,
859
+ recalcBounds=False,
860
+ )
861
+
862
+ glyphset = glyphsets[locTuple]
863
+ glyphset[glyphname] = ttGlyphSets[locTuple][glyphname]
864
+
865
+ names = ["''"]
866
+ fonts = [font.getGlyphSet()]
867
+ locations = [{}]
868
+ axis_triples = {a: (-1, 0, +1) for a in sorted(axisMapping.keys())}
869
+ for locTuple in sorted(glyphsets.keys(), key=lambda v: (len(v), v)):
870
+ name = (
871
+ "'"
872
+ + " ".join(
873
+ "%s=%s"
874
+ % (
875
+ k,
876
+ floatToFixedToStr(
877
+ piecewiseLinearMap(v, axisMapping[k]), 14
878
+ ),
879
+ )
880
+ for k, v in locTuple
881
+ )
882
+ + "'"
883
+ )
884
+ if normalized:
885
+ name += " (normalized)"
886
+ names.append(name)
887
+ fonts.append(glyphsets[locTuple])
888
+ locations.append(dict(locTuple))
889
+
890
+ args.ignore_missing = True
891
+ args.inputs = []
892
+
893
+ if not locations:
894
+ locations = [{} for _ in fonts]
895
+
896
+ for filename in args.inputs:
897
+ if filename.endswith(".ufo"):
898
+ from fontTools.ufoLib import UFOReader
899
+
900
+ font = UFOReader(filename)
901
+ info = SimpleNamespace()
902
+ font.readInfo(info)
903
+ upem = info.unitsPerEm
904
+ fonts.append(font)
905
+ else:
906
+ from fontTools.ttLib import TTFont
907
+
908
+ font = TTFont(filename)
909
+ upem = font["head"].unitsPerEm
910
+ fonts.append(font)
911
+
912
+ names.append(basename(filename).rsplit(".", 1)[0])
913
+
914
+ if len(fonts) < 2:
915
+ log.warning("Font file does not seem to be variable. Nothing to check.")
916
+ return
917
+
918
+ glyphsets = []
919
+ for font in fonts:
920
+ if hasattr(font, "getGlyphSet"):
921
+ glyphset = font.getGlyphSet()
922
+ else:
923
+ glyphset = font
924
+ glyphsets.append({k: glyphset[k] for k in glyphset.keys()})
925
+
926
+ if args.name:
927
+ accepted_names = set(args.name)
928
+ glyphsets = [
929
+ glyphset
930
+ for name, glyphset in zip(names, glyphsets)
931
+ if name in accepted_names
932
+ ]
933
+ locations = [
934
+ location
935
+ for name, location in zip(names, locations)
936
+ if name in accepted_names
937
+ ]
938
+ names = [name for name in names if name in accepted_names]
939
+
940
+ if not glyphs:
941
+ glyphs = sorted(set([gn for glyphset in glyphsets for gn in glyphset.keys()]))
942
+
943
+ glyphsSet = set(glyphs)
944
+ for glyphset in glyphsets:
945
+ glyphSetGlyphNames = set(glyphset.keys())
946
+ diff = glyphsSet - glyphSetGlyphNames
947
+ if diff:
948
+ for gn in diff:
949
+ glyphset[gn] = None
950
+
951
+ # Normalize locations
952
+ locations = [
953
+ {
954
+ **normalizeLocation(loc, axis_triples),
955
+ **{k: v for k, v in loc.items() if k in discrete_axes},
956
+ }
957
+ for loc in locations
958
+ ]
959
+ tolerance = args.tolerance or DEFAULT_TOLERANCE
960
+ kinkiness = args.kinkiness if args.kinkiness is not None else DEFAULT_KINKINESS
961
+
962
+ try:
963
+ log.info("Running on %d glyphsets", len(glyphsets))
964
+ log.info("Locations: %s", pformat(locations))
965
+ problems_gen = test_gen(
966
+ glyphsets,
967
+ glyphs=glyphs,
968
+ names=names,
969
+ locations=locations,
970
+ upem=upem,
971
+ ignore_missing=args.ignore_missing,
972
+ tolerance=tolerance,
973
+ kinkiness=kinkiness,
974
+ show_all=args.show_all,
975
+ discrete_axes=discrete_axes,
976
+ )
977
+ problems = defaultdict(list)
978
+
979
+ f = (
980
+ sys.stdout
981
+ if args.output is None
982
+ else open(ensure_parent_dir(args.output), "w")
983
+ )
984
+
985
+ if not args.quiet:
986
+ if args.json:
987
+ import json
988
+
989
+ for glyphname, problem in problems_gen:
990
+ problems[glyphname].append(problem)
991
+
992
+ print(json.dumps(problems), file=f)
993
+ else:
994
+ last_glyphname = None
995
+ for glyphname, p in problems_gen:
996
+ problems[glyphname].append(p)
997
+
998
+ if glyphname != last_glyphname:
999
+ print(f"Glyph {glyphname} was not compatible:", file=f)
1000
+ last_glyphname = glyphname
1001
+ last_master_idxs = None
1002
+
1003
+ master_idxs = (
1004
+ (p["master_idx"],)
1005
+ if "master_idx" in p
1006
+ else (p["master_1_idx"], p["master_2_idx"])
1007
+ )
1008
+ if master_idxs != last_master_idxs:
1009
+ master_names = (
1010
+ (p["master"],)
1011
+ if "master" in p
1012
+ else (p["master_1"], p["master_2"])
1013
+ )
1014
+ print(f" Masters: %s:" % ", ".join(master_names), file=f)
1015
+ last_master_idxs = master_idxs
1016
+
1017
+ if p["type"] == InterpolatableProblem.MISSING:
1018
+ print(
1019
+ " Glyph was missing in master %s" % p["master"], file=f
1020
+ )
1021
+ elif p["type"] == InterpolatableProblem.OPEN_PATH:
1022
+ print(
1023
+ " Glyph has an open path in master %s" % p["master"],
1024
+ file=f,
1025
+ )
1026
+ elif p["type"] == InterpolatableProblem.PATH_COUNT:
1027
+ print(
1028
+ " Path count differs: %i in %s, %i in %s"
1029
+ % (
1030
+ p["value_1"],
1031
+ p["master_1"],
1032
+ p["value_2"],
1033
+ p["master_2"],
1034
+ ),
1035
+ file=f,
1036
+ )
1037
+ elif p["type"] == InterpolatableProblem.NODE_COUNT:
1038
+ print(
1039
+ " Node count differs in path %i: %i in %s, %i in %s"
1040
+ % (
1041
+ p["path"],
1042
+ p["value_1"],
1043
+ p["master_1"],
1044
+ p["value_2"],
1045
+ p["master_2"],
1046
+ ),
1047
+ file=f,
1048
+ )
1049
+ elif p["type"] == InterpolatableProblem.NODE_INCOMPATIBILITY:
1050
+ print(
1051
+ " Node %o incompatible in path %i: %s in %s, %s in %s"
1052
+ % (
1053
+ p["node"],
1054
+ p["path"],
1055
+ p["value_1"],
1056
+ p["master_1"],
1057
+ p["value_2"],
1058
+ p["master_2"],
1059
+ ),
1060
+ file=f,
1061
+ )
1062
+ elif p["type"] == InterpolatableProblem.CONTOUR_ORDER:
1063
+ print(
1064
+ " Contour order differs: %s in %s, %s in %s"
1065
+ % (
1066
+ p["value_1"],
1067
+ p["master_1"],
1068
+ p["value_2"],
1069
+ p["master_2"],
1070
+ ),
1071
+ file=f,
1072
+ )
1073
+ elif p["type"] == InterpolatableProblem.WRONG_START_POINT:
1074
+ print(
1075
+ " Contour %d start point differs: %s in %s, %s in %s; reversed: %s"
1076
+ % (
1077
+ p["contour"],
1078
+ p["value_1"],
1079
+ p["master_1"],
1080
+ p["value_2"],
1081
+ p["master_2"],
1082
+ p["reversed"],
1083
+ ),
1084
+ file=f,
1085
+ )
1086
+ elif p["type"] == InterpolatableProblem.UNDERWEIGHT:
1087
+ print(
1088
+ " Contour %d interpolation is underweight: %s, %s"
1089
+ % (
1090
+ p["contour"],
1091
+ p["master_1"],
1092
+ p["master_2"],
1093
+ ),
1094
+ file=f,
1095
+ )
1096
+ elif p["type"] == InterpolatableProblem.OVERWEIGHT:
1097
+ print(
1098
+ " Contour %d interpolation is overweight: %s, %s"
1099
+ % (
1100
+ p["contour"],
1101
+ p["master_1"],
1102
+ p["master_2"],
1103
+ ),
1104
+ file=f,
1105
+ )
1106
+ elif p["type"] == InterpolatableProblem.KINK:
1107
+ print(
1108
+ " Contour %d has a kink at %s: %s, %s"
1109
+ % (
1110
+ p["contour"],
1111
+ p["value"],
1112
+ p["master_1"],
1113
+ p["master_2"],
1114
+ ),
1115
+ file=f,
1116
+ )
1117
+ elif p["type"] == InterpolatableProblem.NOTHING:
1118
+ print(
1119
+ " Showing %s and %s"
1120
+ % (
1121
+ p["master_1"],
1122
+ p["master_2"],
1123
+ ),
1124
+ file=f,
1125
+ )
1126
+ else:
1127
+ for glyphname, problem in problems_gen:
1128
+ problems[glyphname].append(problem)
1129
+
1130
+ problems = sort_problems(problems)
1131
+
1132
+ for p in "ps", "pdf":
1133
+ arg = getattr(args, p)
1134
+ if arg is None:
1135
+ continue
1136
+ log.info("Writing %s to %s", p.upper(), arg)
1137
+ from .interpolatablePlot import InterpolatablePS, InterpolatablePDF
1138
+
1139
+ PlotterClass = InterpolatablePS if p == "ps" else InterpolatablePDF
1140
+
1141
+ with PlotterClass(
1142
+ ensure_parent_dir(arg), glyphsets=glyphsets, names=names
1143
+ ) as doc:
1144
+ doc.add_title_page(
1145
+ original_args_inputs, tolerance=tolerance, kinkiness=kinkiness
1146
+ )
1147
+ if problems:
1148
+ doc.add_summary(problems)
1149
+ doc.add_problems(problems)
1150
+ if not problems and not args.quiet:
1151
+ doc.draw_cupcake()
1152
+ if problems:
1153
+ doc.add_index()
1154
+ doc.add_table_of_contents()
1155
+
1156
+ if args.html:
1157
+ log.info("Writing HTML to %s", args.html)
1158
+ from .interpolatablePlot import InterpolatableSVG
1159
+
1160
+ svgs = []
1161
+ glyph_starts = {}
1162
+ with InterpolatableSVG(svgs, glyphsets=glyphsets, names=names) as svg:
1163
+ svg.add_title_page(
1164
+ original_args_inputs,
1165
+ show_tolerance=False,
1166
+ tolerance=tolerance,
1167
+ kinkiness=kinkiness,
1168
+ )
1169
+ for glyph, glyph_problems in problems.items():
1170
+ glyph_starts[len(svgs)] = glyph
1171
+ svg.add_problems(
1172
+ {glyph: glyph_problems},
1173
+ show_tolerance=False,
1174
+ show_page_number=False,
1175
+ )
1176
+ if not problems and not args.quiet:
1177
+ svg.draw_cupcake()
1178
+
1179
+ import base64
1180
+
1181
+ with open(ensure_parent_dir(args.html), "wb") as f:
1182
+ f.write(b"<!DOCTYPE html>\n")
1183
+ f.write(
1184
+ b'<html><body align="center" style="font-family: sans-serif; text-color: #222">\n'
1185
+ )
1186
+ f.write(b"<title>fonttools varLib.interpolatable report</title>\n")
1187
+ for i, svg in enumerate(svgs):
1188
+ if i in glyph_starts:
1189
+ f.write(f"<h1>Glyph {glyph_starts[i]}</h1>\n".encode("utf-8"))
1190
+ f.write("<img src='data:image/svg+xml;base64,".encode("utf-8"))
1191
+ f.write(base64.b64encode(svg))
1192
+ f.write(b"' />\n")
1193
+ f.write(b"<hr>\n")
1194
+ f.write(b"</body></html>\n")
1195
+
1196
+ except Exception as e:
1197
+ e.args += original_args_inputs
1198
+ log.error(e)
1199
+ raise
1200
+
1201
+ if problems:
1202
+ return problems
1203
+
1204
+
1205
+ if __name__ == "__main__":
1206
+ import sys
1207
+
1208
+ problems = main()
1209
+ sys.exit(int(bool(problems)))