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,642 @@
1
+ """Variation fonts interpolation models."""
2
+
3
+ __all__ = [
4
+ "normalizeValue",
5
+ "normalizeLocation",
6
+ "supportScalar",
7
+ "piecewiseLinearMap",
8
+ "VariationModel",
9
+ ]
10
+
11
+ from fontTools.misc.roundTools import noRound
12
+ from .errors import VariationModelError
13
+
14
+
15
+ def nonNone(lst):
16
+ return [l for l in lst if l is not None]
17
+
18
+
19
+ def allNone(lst):
20
+ return all(l is None for l in lst)
21
+
22
+
23
+ def allEqualTo(ref, lst, mapper=None):
24
+ if mapper is None:
25
+ return all(ref == item for item in lst)
26
+
27
+ mapped = mapper(ref)
28
+ return all(mapped == mapper(item) for item in lst)
29
+
30
+
31
+ def allEqual(lst, mapper=None):
32
+ if not lst:
33
+ return True
34
+ it = iter(lst)
35
+ try:
36
+ first = next(it)
37
+ except StopIteration:
38
+ return True
39
+ return allEqualTo(first, it, mapper=mapper)
40
+
41
+
42
+ def subList(truth, lst):
43
+ assert len(truth) == len(lst)
44
+ return [l for l, t in zip(lst, truth) if t]
45
+
46
+
47
+ def normalizeValue(v, triple, extrapolate=False):
48
+ """Normalizes value based on a min/default/max triple.
49
+
50
+ >>> normalizeValue(400, (100, 400, 900))
51
+ 0.0
52
+ >>> normalizeValue(100, (100, 400, 900))
53
+ -1.0
54
+ >>> normalizeValue(650, (100, 400, 900))
55
+ 0.5
56
+ """
57
+ lower, default, upper = triple
58
+ if not (lower <= default <= upper):
59
+ raise ValueError(
60
+ f"Invalid axis values, must be minimum, default, maximum: "
61
+ f"{lower:3.3f}, {default:3.3f}, {upper:3.3f}"
62
+ )
63
+ if not extrapolate:
64
+ v = max(min(v, upper), lower)
65
+
66
+ if v == default or lower == upper:
67
+ return 0.0
68
+
69
+ if (v < default and lower != default) or (v > default and upper == default):
70
+ return (v - default) / (default - lower)
71
+ else:
72
+ assert (v > default and upper != default) or (
73
+ v < default and lower == default
74
+ ), f"Ooops... v={v}, triple=({lower}, {default}, {upper})"
75
+ return (v - default) / (upper - default)
76
+
77
+
78
+ def normalizeLocation(location, axes, extrapolate=False, *, validate=False):
79
+ """Normalizes location based on axis min/default/max values from axes.
80
+
81
+ >>> axes = {"wght": (100, 400, 900)}
82
+ >>> normalizeLocation({"wght": 400}, axes)
83
+ {'wght': 0.0}
84
+ >>> normalizeLocation({"wght": 100}, axes)
85
+ {'wght': -1.0}
86
+ >>> normalizeLocation({"wght": 900}, axes)
87
+ {'wght': 1.0}
88
+ >>> normalizeLocation({"wght": 650}, axes)
89
+ {'wght': 0.5}
90
+ >>> normalizeLocation({"wght": 1000}, axes)
91
+ {'wght': 1.0}
92
+ >>> normalizeLocation({"wght": 0}, axes)
93
+ {'wght': -1.0}
94
+ >>> axes = {"wght": (0, 0, 1000)}
95
+ >>> normalizeLocation({"wght": 0}, axes)
96
+ {'wght': 0.0}
97
+ >>> normalizeLocation({"wght": -1}, axes)
98
+ {'wght': 0.0}
99
+ >>> normalizeLocation({"wght": 1000}, axes)
100
+ {'wght': 1.0}
101
+ >>> normalizeLocation({"wght": 500}, axes)
102
+ {'wght': 0.5}
103
+ >>> normalizeLocation({"wght": 1001}, axes)
104
+ {'wght': 1.0}
105
+ >>> axes = {"wght": (0, 1000, 1000)}
106
+ >>> normalizeLocation({"wght": 0}, axes)
107
+ {'wght': -1.0}
108
+ >>> normalizeLocation({"wght": -1}, axes)
109
+ {'wght': -1.0}
110
+ >>> normalizeLocation({"wght": 500}, axes)
111
+ {'wght': -0.5}
112
+ >>> normalizeLocation({"wght": 1000}, axes)
113
+ {'wght': 0.0}
114
+ >>> normalizeLocation({"wght": 1001}, axes)
115
+ {'wght': 0.0}
116
+ """
117
+ if validate:
118
+ assert set(location.keys()) <= set(axes.keys()), set(location.keys()) - set(
119
+ axes.keys()
120
+ )
121
+ out = {}
122
+ for tag, triple in axes.items():
123
+ v = location.get(tag, triple[1])
124
+ out[tag] = normalizeValue(v, triple, extrapolate=extrapolate)
125
+ return out
126
+
127
+
128
+ def supportScalar(location, support, ot=True, extrapolate=False, axisRanges=None):
129
+ """Returns the scalar multiplier at location, for a master
130
+ with support. If ot is True, then a peak value of zero
131
+ for support of an axis means "axis does not participate". That
132
+ is how OpenType Variation Font technology works.
133
+
134
+ If extrapolate is True, axisRanges must be a dict that maps axis
135
+ names to (axisMin, axisMax) tuples.
136
+
137
+ >>> supportScalar({}, {})
138
+ 1.0
139
+ >>> supportScalar({'wght':.2}, {})
140
+ 1.0
141
+ >>> supportScalar({'wght':.2}, {'wght':(0,2,3)})
142
+ 0.1
143
+ >>> supportScalar({'wght':2.5}, {'wght':(0,2,4)})
144
+ 0.75
145
+ >>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
146
+ 0.75
147
+ >>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}, ot=False)
148
+ 0.375
149
+ >>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
150
+ 0.75
151
+ >>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
152
+ 0.75
153
+ >>> supportScalar({'wght':3}, {'wght':(0,1,2)}, extrapolate=True, axisRanges={'wght':(0, 2)})
154
+ -1.0
155
+ >>> supportScalar({'wght':-1}, {'wght':(0,1,2)}, extrapolate=True, axisRanges={'wght':(0, 2)})
156
+ -1.0
157
+ >>> supportScalar({'wght':3}, {'wght':(0,2,2)}, extrapolate=True, axisRanges={'wght':(0, 2)})
158
+ 1.5
159
+ >>> supportScalar({'wght':-1}, {'wght':(0,2,2)}, extrapolate=True, axisRanges={'wght':(0, 2)})
160
+ -0.5
161
+ """
162
+ if extrapolate and axisRanges is None:
163
+ raise TypeError("axisRanges must be passed when extrapolate is True")
164
+ scalar = 1.0
165
+ for axis, (lower, peak, upper) in support.items():
166
+ if ot:
167
+ # OpenType-specific case handling
168
+ if peak == 0.0:
169
+ continue
170
+ if lower > peak or peak > upper:
171
+ continue
172
+ if lower < 0.0 and upper > 0.0:
173
+ continue
174
+ v = location.get(axis, 0.0)
175
+ else:
176
+ assert axis in location
177
+ v = location[axis]
178
+ if v == peak:
179
+ continue
180
+
181
+ if extrapolate:
182
+ axisMin, axisMax = axisRanges[axis]
183
+ if v < axisMin and lower <= axisMin:
184
+ if peak <= axisMin and peak < upper:
185
+ scalar *= (v - upper) / (peak - upper)
186
+ continue
187
+ elif axisMin < peak:
188
+ scalar *= (v - lower) / (peak - lower)
189
+ continue
190
+ elif axisMax < v and axisMax <= upper:
191
+ if axisMax <= peak and lower < peak:
192
+ scalar *= (v - lower) / (peak - lower)
193
+ continue
194
+ elif peak < axisMax:
195
+ scalar *= (v - upper) / (peak - upper)
196
+ continue
197
+
198
+ if v <= lower or upper <= v:
199
+ scalar = 0.0
200
+ break
201
+
202
+ if v < peak:
203
+ scalar *= (v - lower) / (peak - lower)
204
+ else: # v > peak
205
+ scalar *= (v - upper) / (peak - upper)
206
+ return scalar
207
+
208
+
209
+ class VariationModel(object):
210
+ """Locations must have the base master at the origin (ie. 0).
211
+
212
+ If axis-ranges are not provided, values are assumed to be normalized to
213
+ the range [-1, 1].
214
+
215
+ If the extrapolate argument is set to True, then values are extrapolated
216
+ outside the axis range.
217
+
218
+ >>> from pprint import pprint
219
+ >>> axisRanges = {'wght': (-180, +180), 'wdth': (-1, +1)}
220
+ >>> locations = [ \
221
+ {'wght':100}, \
222
+ {'wght':-100}, \
223
+ {'wght':-180}, \
224
+ {'wdth':+.3}, \
225
+ {'wght':+120,'wdth':.3}, \
226
+ {'wght':+120,'wdth':.2}, \
227
+ {}, \
228
+ {'wght':+180,'wdth':.3}, \
229
+ {'wght':+180}, \
230
+ ]
231
+ >>> model = VariationModel(locations, axisOrder=['wght'], axisRanges=axisRanges)
232
+ >>> pprint(model.locations)
233
+ [{},
234
+ {'wght': -100},
235
+ {'wght': -180},
236
+ {'wght': 100},
237
+ {'wght': 180},
238
+ {'wdth': 0.3},
239
+ {'wdth': 0.3, 'wght': 180},
240
+ {'wdth': 0.3, 'wght': 120},
241
+ {'wdth': 0.2, 'wght': 120}]
242
+ >>> pprint(model.deltaWeights)
243
+ [{},
244
+ {0: 1.0},
245
+ {0: 1.0},
246
+ {0: 1.0},
247
+ {0: 1.0},
248
+ {0: 1.0},
249
+ {0: 1.0, 4: 1.0, 5: 1.0},
250
+ {0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.6666666666666666},
251
+ {0: 1.0,
252
+ 3: 0.75,
253
+ 4: 0.25,
254
+ 5: 0.6666666666666667,
255
+ 6: 0.4444444444444445,
256
+ 7: 0.6666666666666667}]
257
+ """
258
+
259
+ def __init__(
260
+ self, locations, axisOrder=None, extrapolate=False, *, axisRanges=None
261
+ ):
262
+ if len(set(tuple(sorted(l.items())) for l in locations)) != len(locations):
263
+ raise VariationModelError("Locations must be unique.")
264
+
265
+ self.origLocations = locations
266
+ self.axisOrder = axisOrder if axisOrder is not None else []
267
+ self.extrapolate = extrapolate
268
+ if axisRanges is None:
269
+ if extrapolate:
270
+ axisRanges = self.computeAxisRanges(locations)
271
+ else:
272
+ allAxes = {axis for loc in locations for axis in loc.keys()}
273
+ axisRanges = {axis: (-1, 1) for axis in allAxes}
274
+ self.axisRanges = axisRanges
275
+
276
+ locations = [{k: v for k, v in loc.items() if v != 0.0} for loc in locations]
277
+ keyFunc = self.getMasterLocationsSortKeyFunc(
278
+ locations, axisOrder=self.axisOrder
279
+ )
280
+ self.locations = sorted(locations, key=keyFunc)
281
+
282
+ # Mapping from user's master order to our master order
283
+ self.mapping = [self.locations.index(l) for l in locations]
284
+ self.reverseMapping = [locations.index(l) for l in self.locations]
285
+
286
+ self._computeMasterSupports()
287
+ self._subModels = {}
288
+
289
+ def getSubModel(self, items):
290
+ """Return a sub-model and the items that are not None.
291
+
292
+ The sub-model is necessary for working with the subset
293
+ of items when some are None.
294
+
295
+ The sub-model is cached."""
296
+ if None not in items:
297
+ return self, items
298
+ key = tuple(v is not None for v in items)
299
+ subModel = self._subModels.get(key)
300
+ if subModel is None:
301
+ subModel = VariationModel(subList(key, self.origLocations), self.axisOrder)
302
+ self._subModels[key] = subModel
303
+ return subModel, subList(key, items)
304
+
305
+ @staticmethod
306
+ def computeAxisRanges(locations):
307
+ axisRanges = {}
308
+ allAxes = {axis for loc in locations for axis in loc.keys()}
309
+ for loc in locations:
310
+ for axis in allAxes:
311
+ value = loc.get(axis, 0)
312
+ axisMin, axisMax = axisRanges.get(axis, (value, value))
313
+ axisRanges[axis] = min(value, axisMin), max(value, axisMax)
314
+ return axisRanges
315
+
316
+ @staticmethod
317
+ def getMasterLocationsSortKeyFunc(locations, axisOrder=[]):
318
+ if {} not in locations:
319
+ raise VariationModelError("Base master not found.")
320
+ axisPoints = {}
321
+ for loc in locations:
322
+ if len(loc) != 1:
323
+ continue
324
+ axis = next(iter(loc))
325
+ value = loc[axis]
326
+ if axis not in axisPoints:
327
+ axisPoints[axis] = {0.0}
328
+ assert (
329
+ value not in axisPoints[axis]
330
+ ), 'Value "%s" in axisPoints["%s"] --> %s' % (value, axis, axisPoints)
331
+ axisPoints[axis].add(value)
332
+
333
+ def getKey(axisPoints, axisOrder):
334
+ def sign(v):
335
+ return -1 if v < 0 else +1 if v > 0 else 0
336
+
337
+ def key(loc):
338
+ rank = len(loc)
339
+ onPointAxes = [
340
+ axis
341
+ for axis, value in loc.items()
342
+ if axis in axisPoints and value in axisPoints[axis]
343
+ ]
344
+ orderedAxes = [axis for axis in axisOrder if axis in loc]
345
+ orderedAxes.extend(
346
+ [axis for axis in sorted(loc.keys()) if axis not in axisOrder]
347
+ )
348
+ return (
349
+ rank, # First, order by increasing rank
350
+ -len(onPointAxes), # Next, by decreasing number of onPoint axes
351
+ tuple(
352
+ axisOrder.index(axis) if axis in axisOrder else 0x10000
353
+ for axis in orderedAxes
354
+ ), # Next, by known axes
355
+ tuple(orderedAxes), # Next, by all axes
356
+ tuple(
357
+ sign(loc[axis]) for axis in orderedAxes
358
+ ), # Next, by signs of axis values
359
+ tuple(
360
+ abs(loc[axis]) for axis in orderedAxes
361
+ ), # Next, by absolute value of axis values
362
+ )
363
+
364
+ return key
365
+
366
+ ret = getKey(axisPoints, axisOrder)
367
+ return ret
368
+
369
+ def reorderMasters(self, master_list, mapping):
370
+ # For changing the master data order without
371
+ # recomputing supports and deltaWeights.
372
+ new_list = [master_list[idx] for idx in mapping]
373
+ self.origLocations = [self.origLocations[idx] for idx in mapping]
374
+ locations = [
375
+ {k: v for k, v in loc.items() if v != 0.0} for loc in self.origLocations
376
+ ]
377
+ self.mapping = [self.locations.index(l) for l in locations]
378
+ self.reverseMapping = [locations.index(l) for l in self.locations]
379
+ self._subModels = {}
380
+ return new_list
381
+
382
+ def _computeMasterSupports(self):
383
+ self.supports = []
384
+ regions = self._locationsToRegions()
385
+ for i, region in enumerate(regions):
386
+ locAxes = set(region.keys())
387
+ # Walk over previous masters now
388
+ for prev_region in regions[:i]:
389
+ # Master with different axes do not participte
390
+ if set(prev_region.keys()) != locAxes:
391
+ continue
392
+ # If it's NOT in the current box, it does not participate
393
+ relevant = True
394
+ for axis, (lower, peak, upper) in region.items():
395
+ if not (
396
+ prev_region[axis][1] == peak
397
+ or lower < prev_region[axis][1] < upper
398
+ ):
399
+ relevant = False
400
+ break
401
+ if not relevant:
402
+ continue
403
+
404
+ # Split the box for new master; split in whatever direction
405
+ # that has largest range ratio.
406
+ #
407
+ # For symmetry, we actually cut across multiple axes
408
+ # if they have the largest, equal, ratio.
409
+ # https://github.com/fonttools/fonttools/commit/7ee81c8821671157968b097f3e55309a1faa511e#commitcomment-31054804
410
+
411
+ bestAxes = {}
412
+ bestRatio = -1
413
+ for axis in prev_region.keys():
414
+ val = prev_region[axis][1]
415
+ assert axis in region
416
+ lower, locV, upper = region[axis]
417
+ newLower, newUpper = lower, upper
418
+ if val < locV:
419
+ newLower = val
420
+ ratio = (val - locV) / (lower - locV)
421
+ elif locV < val:
422
+ newUpper = val
423
+ ratio = (val - locV) / (upper - locV)
424
+ else: # val == locV
425
+ # Can't split box in this direction.
426
+ continue
427
+ if ratio > bestRatio:
428
+ bestAxes = {}
429
+ bestRatio = ratio
430
+ if ratio == bestRatio:
431
+ bestAxes[axis] = (newLower, locV, newUpper)
432
+
433
+ for axis, triple in bestAxes.items():
434
+ region[axis] = triple
435
+ self.supports.append(region)
436
+ self._computeDeltaWeights()
437
+
438
+ def _locationsToRegions(self):
439
+ locations = self.locations
440
+ axisRanges = self.axisRanges
441
+
442
+ regions = []
443
+ for loc in locations:
444
+ region = {}
445
+ for axis, locV in loc.items():
446
+ if locV > 0:
447
+ region[axis] = (0, locV, axisRanges[axis][1])
448
+ else:
449
+ region[axis] = (axisRanges[axis][0], locV, 0)
450
+ regions.append(region)
451
+ return regions
452
+
453
+ def _computeDeltaWeights(self):
454
+ self.deltaWeights = []
455
+ for i, loc in enumerate(self.locations):
456
+ deltaWeight = {}
457
+ # Walk over previous masters now, populate deltaWeight
458
+ for j, support in enumerate(self.supports[:i]):
459
+ scalar = supportScalar(loc, support)
460
+ if scalar:
461
+ deltaWeight[j] = scalar
462
+ self.deltaWeights.append(deltaWeight)
463
+
464
+ def getDeltas(self, masterValues, *, round=noRound):
465
+ assert len(masterValues) == len(self.deltaWeights), (
466
+ len(masterValues),
467
+ len(self.deltaWeights),
468
+ )
469
+ mapping = self.reverseMapping
470
+ out = []
471
+ for i, weights in enumerate(self.deltaWeights):
472
+ delta = masterValues[mapping[i]]
473
+ for j, weight in weights.items():
474
+ if weight == 1:
475
+ delta -= out[j]
476
+ else:
477
+ delta -= out[j] * weight
478
+ out.append(round(delta))
479
+ return out
480
+
481
+ def getDeltasAndSupports(self, items, *, round=noRound):
482
+ model, items = self.getSubModel(items)
483
+ return model.getDeltas(items, round=round), model.supports
484
+
485
+ def getScalars(self, loc):
486
+ """Return scalars for each delta, for the given location.
487
+ If interpolating many master-values at the same location,
488
+ this function allows speed up by fetching the scalars once
489
+ and using them with interpolateFromMastersAndScalars()."""
490
+ return [
491
+ supportScalar(
492
+ loc, support, extrapolate=self.extrapolate, axisRanges=self.axisRanges
493
+ )
494
+ for support in self.supports
495
+ ]
496
+
497
+ def getMasterScalars(self, targetLocation):
498
+ """Return multipliers for each master, for the given location.
499
+ If interpolating many master-values at the same location,
500
+ this function allows speed up by fetching the scalars once
501
+ and using them with interpolateFromValuesAndScalars().
502
+
503
+ Note that the scalars used in interpolateFromMastersAndScalars(),
504
+ are *not* the same as the ones returned here. They are the result
505
+ of getScalars()."""
506
+ out = self.getScalars(targetLocation)
507
+ for i, weights in reversed(list(enumerate(self.deltaWeights))):
508
+ for j, weight in weights.items():
509
+ out[j] -= out[i] * weight
510
+
511
+ out = [out[self.mapping[i]] for i in range(len(out))]
512
+ return out
513
+
514
+ @staticmethod
515
+ def interpolateFromValuesAndScalars(values, scalars):
516
+ """Interpolate from values and scalars coefficients.
517
+
518
+ If the values are master-values, then the scalars should be
519
+ fetched from getMasterScalars().
520
+
521
+ If the values are deltas, then the scalars should be fetched
522
+ from getScalars(); in which case this is the same as
523
+ interpolateFromDeltasAndScalars().
524
+ """
525
+ v = None
526
+ assert len(values) == len(scalars)
527
+ for value, scalar in zip(values, scalars):
528
+ if not scalar:
529
+ continue
530
+ contribution = value * scalar
531
+ if v is None:
532
+ v = contribution
533
+ else:
534
+ v += contribution
535
+ return v
536
+
537
+ @staticmethod
538
+ def interpolateFromDeltasAndScalars(deltas, scalars):
539
+ """Interpolate from deltas and scalars fetched from getScalars()."""
540
+ return VariationModel.interpolateFromValuesAndScalars(deltas, scalars)
541
+
542
+ def interpolateFromDeltas(self, loc, deltas):
543
+ """Interpolate from deltas, at location loc."""
544
+ scalars = self.getScalars(loc)
545
+ return self.interpolateFromDeltasAndScalars(deltas, scalars)
546
+
547
+ def interpolateFromMasters(self, loc, masterValues, *, round=noRound):
548
+ """Interpolate from master-values, at location loc."""
549
+ scalars = self.getMasterScalars(loc)
550
+ return self.interpolateFromValuesAndScalars(masterValues, scalars)
551
+
552
+ def interpolateFromMastersAndScalars(self, masterValues, scalars, *, round=noRound):
553
+ """Interpolate from master-values, and scalars fetched from
554
+ getScalars(), which is useful when you want to interpolate
555
+ multiple master-values with the same location."""
556
+ deltas = self.getDeltas(masterValues, round=round)
557
+ return self.interpolateFromDeltasAndScalars(deltas, scalars)
558
+
559
+
560
+ def piecewiseLinearMap(v, mapping):
561
+ keys = mapping.keys()
562
+ if not keys:
563
+ return v
564
+ if v in keys:
565
+ return mapping[v]
566
+ k = min(keys)
567
+ if v < k:
568
+ return v + mapping[k] - k
569
+ k = max(keys)
570
+ if v > k:
571
+ return v + mapping[k] - k
572
+ # Interpolate
573
+ a = max(k for k in keys if k < v)
574
+ b = min(k for k in keys if k > v)
575
+ va = mapping[a]
576
+ vb = mapping[b]
577
+ return va + (vb - va) * (v - a) / (b - a)
578
+
579
+
580
+ def main(args=None):
581
+ """Normalize locations on a given designspace"""
582
+ from fontTools import configLogger
583
+ import argparse
584
+
585
+ parser = argparse.ArgumentParser(
586
+ "fonttools varLib.models",
587
+ description=main.__doc__,
588
+ )
589
+ parser.add_argument(
590
+ "--loglevel",
591
+ metavar="LEVEL",
592
+ default="INFO",
593
+ help="Logging level (defaults to INFO)",
594
+ )
595
+
596
+ group = parser.add_mutually_exclusive_group(required=True)
597
+ group.add_argument("-d", "--designspace", metavar="DESIGNSPACE", type=str)
598
+ group.add_argument(
599
+ "-l",
600
+ "--locations",
601
+ metavar="LOCATION",
602
+ nargs="+",
603
+ help="Master locations as comma-separate coordinates. One must be all zeros.",
604
+ )
605
+
606
+ args = parser.parse_args(args)
607
+
608
+ configLogger(level=args.loglevel)
609
+ from pprint import pprint
610
+
611
+ if args.designspace:
612
+ from fontTools.designspaceLib import DesignSpaceDocument
613
+
614
+ doc = DesignSpaceDocument()
615
+ doc.read(args.designspace)
616
+ locs = [s.location for s in doc.sources]
617
+ print("Original locations:")
618
+ pprint(locs)
619
+ doc.normalize()
620
+ print("Normalized locations:")
621
+ locs = [s.location for s in doc.sources]
622
+ pprint(locs)
623
+ else:
624
+ axes = [chr(c) for c in range(ord("A"), ord("Z") + 1)]
625
+ locs = [
626
+ dict(zip(axes, (float(v) for v in s.split(",")))) for s in args.locations
627
+ ]
628
+
629
+ model = VariationModel(locs)
630
+ print("Sorted locations:")
631
+ pprint(model.locations)
632
+ print("Supports:")
633
+ pprint(model.supports)
634
+
635
+
636
+ if __name__ == "__main__":
637
+ import doctest, sys
638
+
639
+ if len(sys.argv) > 1:
640
+ sys.exit(main())
641
+
642
+ sys.exit(doctest.testmod().failed)