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,703 @@
1
+ """Module to build FeatureVariation tables:
2
+ https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#featurevariations-table
3
+
4
+ NOTE: The API is experimental and subject to change.
5
+ """
6
+
7
+ from fontTools.misc.dictTools import hashdict
8
+ from fontTools.misc.intTools import bit_count
9
+ from fontTools.ttLib import newTable
10
+ from fontTools.ttLib.tables import otTables as ot
11
+ from fontTools.ttLib.ttVisitor import TTVisitor
12
+ from fontTools.otlLib.builder import buildLookup, buildSingleSubstSubtable
13
+ from collections import OrderedDict
14
+
15
+ from .errors import VarLibError, VarLibValidationError
16
+
17
+
18
+ def addFeatureVariations(font, conditionalSubstitutions, featureTag="rvrn"):
19
+ """Add conditional substitutions to a Variable Font.
20
+
21
+ The `conditionalSubstitutions` argument is a list of (Region, Substitutions)
22
+ tuples.
23
+
24
+ A Region is a list of Boxes. A Box is a dict mapping axisTags to
25
+ (minValue, maxValue) tuples. Irrelevant axes may be omitted and they are
26
+ interpretted as extending to end of axis in each direction. A Box represents
27
+ an orthogonal 'rectangular' subset of an N-dimensional design space.
28
+ A Region represents a more complex subset of an N-dimensional design space,
29
+ ie. the union of all the Boxes in the Region.
30
+ For efficiency, Boxes within a Region should ideally not overlap, but
31
+ functionality is not compromised if they do.
32
+
33
+ The minimum and maximum values are expressed in normalized coordinates.
34
+
35
+ A Substitution is a dict mapping source glyph names to substitute glyph names.
36
+
37
+ Example:
38
+
39
+ # >>> f = TTFont(srcPath)
40
+ # >>> condSubst = [
41
+ # ... # A list of (Region, Substitution) tuples.
42
+ # ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}),
43
+ # ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}),
44
+ # ... ]
45
+ # >>> addFeatureVariations(f, condSubst)
46
+ # >>> f.save(dstPath)
47
+
48
+ The `featureTag` parameter takes either a str or a iterable of str (the single str
49
+ is kept for backwards compatibility), and defines which feature(s) will be
50
+ associated with the feature variations.
51
+ Note, if this is "rvrn", then the substitution lookup will be inserted at the
52
+ beginning of the lookup list so that it is processed before others, otherwise
53
+ for any other feature tags it will be appended last.
54
+ """
55
+
56
+ # process first when "rvrn" is the only listed tag
57
+ featureTags = [featureTag] if isinstance(featureTag, str) else sorted(featureTag)
58
+ processLast = "rvrn" not in featureTags or len(featureTags) > 1
59
+
60
+ _checkSubstitutionGlyphsExist(
61
+ glyphNames=set(font.getGlyphOrder()),
62
+ substitutions=conditionalSubstitutions,
63
+ )
64
+
65
+ substitutions = overlayFeatureVariations(conditionalSubstitutions)
66
+
67
+ # turn substitution dicts into tuples of tuples, so they are hashable
68
+ conditionalSubstitutions, allSubstitutions = makeSubstitutionsHashable(
69
+ substitutions
70
+ )
71
+ if "GSUB" not in font:
72
+ font["GSUB"] = buildGSUB()
73
+ else:
74
+ existingTags = _existingVariableFeatures(font["GSUB"].table).intersection(
75
+ featureTags
76
+ )
77
+ if existingTags:
78
+ raise VarLibError(
79
+ f"FeatureVariations already exist for feature tag(s): {existingTags}"
80
+ )
81
+
82
+ # setup lookups
83
+ lookupMap = buildSubstitutionLookups(
84
+ font["GSUB"].table, allSubstitutions, processLast
85
+ )
86
+
87
+ # addFeatureVariationsRaw takes a list of
88
+ # ( {condition}, [ lookup indices ] )
89
+ # so rearrange our lookups to match
90
+ conditionsAndLookups = []
91
+ for conditionSet, substitutions in conditionalSubstitutions:
92
+ conditionsAndLookups.append(
93
+ (conditionSet, [lookupMap[s] for s in substitutions])
94
+ )
95
+
96
+ addFeatureVariationsRaw(font, font["GSUB"].table, conditionsAndLookups, featureTags)
97
+
98
+ # Update OS/2.usMaxContext in case the font didn't have features before, but
99
+ # does now, if the OS/2 table exists. The table may be required, but
100
+ # fontTools needs to be able to deal with non-standard fonts. Since feature
101
+ # variations are always 1:1 mappings, we can set the value to at least 1
102
+ # instead of recomputing it with `otlLib.maxContextCalc.maxCtxFont()`.
103
+ if (os2 := font.get("OS/2")) is not None:
104
+ os2.usMaxContext = max(1, os2.usMaxContext)
105
+
106
+
107
+ def _existingVariableFeatures(table):
108
+ existingFeatureVarsTags = set()
109
+ if hasattr(table, "FeatureVariations") and table.FeatureVariations is not None:
110
+ features = table.FeatureList.FeatureRecord
111
+ for fvr in table.FeatureVariations.FeatureVariationRecord:
112
+ for ftsr in fvr.FeatureTableSubstitution.SubstitutionRecord:
113
+ existingFeatureVarsTags.add(features[ftsr.FeatureIndex].FeatureTag)
114
+ return existingFeatureVarsTags
115
+
116
+
117
+ def _checkSubstitutionGlyphsExist(glyphNames, substitutions):
118
+ referencedGlyphNames = set()
119
+ for _, substitution in substitutions:
120
+ referencedGlyphNames |= substitution.keys()
121
+ referencedGlyphNames |= set(substitution.values())
122
+ missing = referencedGlyphNames - glyphNames
123
+ if missing:
124
+ raise VarLibValidationError(
125
+ "Missing glyphs are referenced in conditional substitution rules:"
126
+ f" {', '.join(missing)}"
127
+ )
128
+
129
+
130
+ def overlayFeatureVariations(conditionalSubstitutions):
131
+ """Compute overlaps between all conditional substitutions.
132
+
133
+ The `conditionalSubstitutions` argument is a list of (Region, Substitutions)
134
+ tuples.
135
+
136
+ A Region is a list of Boxes. A Box is a dict mapping axisTags to
137
+ (minValue, maxValue) tuples. Irrelevant axes may be omitted and they are
138
+ interpretted as extending to end of axis in each direction. A Box represents
139
+ an orthogonal 'rectangular' subset of an N-dimensional design space.
140
+ A Region represents a more complex subset of an N-dimensional design space,
141
+ ie. the union of all the Boxes in the Region.
142
+ For efficiency, Boxes within a Region should ideally not overlap, but
143
+ functionality is not compromised if they do.
144
+
145
+ The minimum and maximum values are expressed in normalized coordinates.
146
+
147
+ A Substitution is a dict mapping source glyph names to substitute glyph names.
148
+
149
+ Returns data is in similar but different format. Overlaps of distinct
150
+ substitution Boxes (*not* Regions) are explicitly listed as distinct rules,
151
+ and rules with the same Box merged. The more specific rules appear earlier
152
+ in the resulting list. Moreover, instead of just a dictionary of substitutions,
153
+ a list of dictionaries is returned for substitutions corresponding to each
154
+ unique space, with each dictionary being identical to one of the input
155
+ substitution dictionaries. These dictionaries are not merged to allow data
156
+ sharing when they are converted into font tables.
157
+
158
+ Example::
159
+
160
+ >>> condSubst = [
161
+ ... # A list of (Region, Substitution) tuples.
162
+ ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}),
163
+ ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}),
164
+ ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}),
165
+ ... ([{"wght": (0.5, 1.0), "wdth": (-1, 1.0)}], {"dollar": "dollar.rvrn"}),
166
+ ... ]
167
+ >>> from pprint import pprint
168
+ >>> pprint(overlayFeatureVariations(condSubst))
169
+ [({'wdth': (0.5, 1.0), 'wght': (0.5, 1.0)},
170
+ [{'dollar': 'dollar.rvrn'}, {'cent': 'cent.rvrn'}]),
171
+ ({'wdth': (0.5, 1.0)}, [{'cent': 'cent.rvrn'}]),
172
+ ({'wght': (0.5, 1.0)}, [{'dollar': 'dollar.rvrn'}])]
173
+
174
+ """
175
+
176
+ # Merge same-substitutions rules, as this creates fewer number oflookups.
177
+ merged = OrderedDict()
178
+ for value, key in conditionalSubstitutions:
179
+ key = hashdict(key)
180
+ if key in merged:
181
+ merged[key].extend(value)
182
+ else:
183
+ merged[key] = value
184
+ conditionalSubstitutions = [(v, dict(k)) for k, v in merged.items()]
185
+ del merged
186
+
187
+ # Merge same-region rules, as this is cheaper.
188
+ # Also convert boxes to hashdict()
189
+ #
190
+ # Reversing is such that earlier entries win in case of conflicting substitution
191
+ # rules for the same region.
192
+ merged = OrderedDict()
193
+ for key, value in reversed(conditionalSubstitutions):
194
+ key = tuple(
195
+ sorted(
196
+ (hashdict(cleanupBox(k)) for k in key),
197
+ key=lambda d: tuple(sorted(d.items())),
198
+ )
199
+ )
200
+ if key in merged:
201
+ merged[key].update(value)
202
+ else:
203
+ merged[key] = dict(value)
204
+ conditionalSubstitutions = list(reversed(merged.items()))
205
+ del merged
206
+
207
+ # Overlay
208
+ #
209
+ # Rank is the bit-set of the index of all contributing layers.
210
+ initMapInit = ((hashdict(), 0),) # Initializer representing the entire space
211
+ boxMap = OrderedDict(initMapInit) # Map from Box to Rank
212
+ for i, (currRegion, _) in enumerate(conditionalSubstitutions):
213
+ newMap = OrderedDict(initMapInit)
214
+ currRank = 1 << i
215
+ for box, rank in boxMap.items():
216
+ for currBox in currRegion:
217
+ intersection, remainder = overlayBox(currBox, box)
218
+ if intersection is not None:
219
+ intersection = hashdict(intersection)
220
+ newMap[intersection] = newMap.get(intersection, 0) | rank | currRank
221
+ if remainder is not None:
222
+ remainder = hashdict(remainder)
223
+ newMap[remainder] = newMap.get(remainder, 0) | rank
224
+ boxMap = newMap
225
+
226
+ # Generate output
227
+ items = []
228
+ for box, rank in sorted(
229
+ boxMap.items(), key=(lambda BoxAndRank: -bit_count(BoxAndRank[1]))
230
+ ):
231
+ # Skip any box that doesn't have any substitution.
232
+ if rank == 0:
233
+ continue
234
+ substsList = []
235
+ i = 0
236
+ while rank:
237
+ if rank & 1:
238
+ substsList.append(conditionalSubstitutions[i][1])
239
+ rank >>= 1
240
+ i += 1
241
+ items.append((dict(box), substsList))
242
+ return items
243
+
244
+
245
+ #
246
+ # Terminology:
247
+ #
248
+ # A 'Box' is a dict representing an orthogonal "rectangular" bit of N-dimensional space.
249
+ # The keys in the dict are axis tags, the values are (minValue, maxValue) tuples.
250
+ # Missing dimensions (keys) are substituted by the default min and max values
251
+ # from the corresponding axes.
252
+ #
253
+
254
+
255
+ def overlayBox(top, bot):
256
+ """Overlays ``top`` box on top of ``bot`` box.
257
+
258
+ Returns two items:
259
+
260
+ * Box for intersection of ``top`` and ``bot``, or None if they don't intersect.
261
+ * Box for remainder of ``bot``. Remainder box might not be exact (since the
262
+ remainder might not be a simple box), but is inclusive of the exact
263
+ remainder.
264
+ """
265
+
266
+ # Intersection
267
+ intersection = {}
268
+ intersection.update(top)
269
+ intersection.update(bot)
270
+ for axisTag in set(top) & set(bot):
271
+ min1, max1 = top[axisTag]
272
+ min2, max2 = bot[axisTag]
273
+ minimum = max(min1, min2)
274
+ maximum = min(max1, max2)
275
+ if not minimum < maximum:
276
+ return None, bot # Do not intersect
277
+ intersection[axisTag] = minimum, maximum
278
+
279
+ # Remainder
280
+ #
281
+ # Remainder is empty if bot's each axis range lies within that of intersection.
282
+ #
283
+ # Remainder is shrank if bot's each, except for exactly one, axis range lies
284
+ # within that of intersection, and that one axis, it extrudes out of the
285
+ # intersection only on one side.
286
+ #
287
+ # Bot is returned in full as remainder otherwise, as true remainder is not
288
+ # representable as a single box.
289
+
290
+ remainder = dict(bot)
291
+ extruding = False
292
+ fullyInside = True
293
+ for axisTag in top:
294
+ if axisTag in bot:
295
+ continue
296
+ extruding = True
297
+ fullyInside = False
298
+ break
299
+ for axisTag in bot:
300
+ if axisTag not in top:
301
+ continue # Axis range lies fully within
302
+ min1, max1 = intersection[axisTag]
303
+ min2, max2 = bot[axisTag]
304
+ if min1 <= min2 and max2 <= max1:
305
+ continue # Axis range lies fully within
306
+
307
+ # Bot's range doesn't fully lie within that of top's for this axis.
308
+ # We know they intersect, so it cannot lie fully without either; so they
309
+ # overlap.
310
+
311
+ # If we have had an overlapping axis before, remainder is not
312
+ # representable as a box, so return full bottom and go home.
313
+ if extruding:
314
+ return intersection, bot
315
+ extruding = True
316
+ fullyInside = False
317
+
318
+ # Otherwise, cut remainder on this axis and continue.
319
+ if min1 <= min2:
320
+ # Right side survives.
321
+ minimum = max(max1, min2)
322
+ maximum = max2
323
+ elif max2 <= max1:
324
+ # Left side survives.
325
+ minimum = min2
326
+ maximum = min(min1, max2)
327
+ else:
328
+ # Remainder leaks out from both sides. Can't cut either.
329
+ return intersection, bot
330
+
331
+ remainder[axisTag] = minimum, maximum
332
+
333
+ if fullyInside:
334
+ # bot is fully within intersection. Remainder is empty.
335
+ return intersection, None
336
+
337
+ return intersection, remainder
338
+
339
+
340
+ def cleanupBox(box):
341
+ """Return a sparse copy of `box`, without redundant (default) values.
342
+
343
+ >>> cleanupBox({})
344
+ {}
345
+ >>> cleanupBox({'wdth': (0.0, 1.0)})
346
+ {'wdth': (0.0, 1.0)}
347
+ >>> cleanupBox({'wdth': (-1.0, 1.0)})
348
+ {}
349
+
350
+ """
351
+ return {tag: limit for tag, limit in box.items() if limit != (-1.0, 1.0)}
352
+
353
+
354
+ #
355
+ # Low level implementation
356
+ #
357
+
358
+
359
+ def addFeatureVariationsRaw(font, table, conditionalSubstitutions, featureTag="rvrn"):
360
+ """Low level implementation of addFeatureVariations that directly
361
+ models the possibilities of the FeatureVariations table."""
362
+
363
+ featureTags = [featureTag] if isinstance(featureTag, str) else sorted(featureTag)
364
+ processLast = "rvrn" not in featureTags or len(featureTags) > 1
365
+
366
+ #
367
+ # if a <featureTag> feature is not present:
368
+ # make empty <featureTag> feature
369
+ # sort features, get <featureTag> feature index
370
+ # add <featureTag> feature to all scripts
371
+ # if a <featureTag> feature is present:
372
+ # reuse <featureTag> feature index
373
+ # make lookups
374
+ # add feature variations
375
+ #
376
+ if table.Version < 0x00010001:
377
+ table.Version = 0x00010001 # allow table.FeatureVariations
378
+
379
+ varFeatureIndices = set()
380
+
381
+ existingTags = {
382
+ feature.FeatureTag
383
+ for feature in table.FeatureList.FeatureRecord
384
+ if feature.FeatureTag in featureTags
385
+ }
386
+
387
+ newTags = set(featureTags) - existingTags
388
+ if newTags:
389
+ varFeatures = []
390
+ for featureTag in sorted(newTags):
391
+ varFeature = buildFeatureRecord(featureTag, [])
392
+ table.FeatureList.FeatureRecord.append(varFeature)
393
+ varFeatures.append(varFeature)
394
+ table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord)
395
+
396
+ sortFeatureList(table)
397
+
398
+ for varFeature in varFeatures:
399
+ varFeatureIndex = table.FeatureList.FeatureRecord.index(varFeature)
400
+
401
+ for scriptRecord in table.ScriptList.ScriptRecord:
402
+ if scriptRecord.Script.DefaultLangSys is None:
403
+ # We need to have a default LangSys to attach variations to.
404
+ langSys = ot.LangSys()
405
+ langSys.LookupOrder = None
406
+ langSys.ReqFeatureIndex = 0xFFFF
407
+ langSys.FeatureIndex = []
408
+ langSys.FeatureCount = 0
409
+ scriptRecord.Script.DefaultLangSys = langSys
410
+ langSystems = [lsr.LangSys for lsr in scriptRecord.Script.LangSysRecord]
411
+ for langSys in [scriptRecord.Script.DefaultLangSys] + langSystems:
412
+ langSys.FeatureIndex.append(varFeatureIndex)
413
+ langSys.FeatureCount = len(langSys.FeatureIndex)
414
+ varFeatureIndices.add(varFeatureIndex)
415
+
416
+ if existingTags:
417
+ # indices may have changed if we inserted new features and sorted feature list
418
+ # so we must do this after the above
419
+ varFeatureIndices.update(
420
+ index
421
+ for index, feature in enumerate(table.FeatureList.FeatureRecord)
422
+ if feature.FeatureTag in existingTags
423
+ )
424
+
425
+ axisIndices = {
426
+ axis.axisTag: axisIndex for axisIndex, axis in enumerate(font["fvar"].axes)
427
+ }
428
+
429
+ hasFeatureVariations = (
430
+ hasattr(table, "FeatureVariations") and table.FeatureVariations is not None
431
+ )
432
+
433
+ featureVariationRecords = []
434
+ for conditionSet, lookupIndices in conditionalSubstitutions:
435
+ conditionTable = []
436
+ for axisTag, (minValue, maxValue) in sorted(conditionSet.items()):
437
+ if minValue > maxValue:
438
+ raise VarLibValidationError(
439
+ "A condition set has a minimum value above the maximum value."
440
+ )
441
+ ct = buildConditionTable(axisIndices[axisTag], minValue, maxValue)
442
+ conditionTable.append(ct)
443
+ records = []
444
+ for varFeatureIndex in sorted(varFeatureIndices):
445
+ existingLookupIndices = table.FeatureList.FeatureRecord[
446
+ varFeatureIndex
447
+ ].Feature.LookupListIndex
448
+ combinedLookupIndices = (
449
+ existingLookupIndices + lookupIndices
450
+ if processLast
451
+ else lookupIndices + existingLookupIndices
452
+ )
453
+
454
+ records.append(
455
+ buildFeatureTableSubstitutionRecord(
456
+ varFeatureIndex, combinedLookupIndices
457
+ )
458
+ )
459
+ if hasFeatureVariations and (
460
+ fvr := findFeatureVariationRecord(table.FeatureVariations, conditionTable)
461
+ ):
462
+ fvr.FeatureTableSubstitution.SubstitutionRecord.extend(records)
463
+ fvr.FeatureTableSubstitution.SubstitutionCount = len(
464
+ fvr.FeatureTableSubstitution.SubstitutionRecord
465
+ )
466
+ else:
467
+ featureVariationRecords.append(
468
+ buildFeatureVariationRecord(conditionTable, records)
469
+ )
470
+
471
+ if hasFeatureVariations:
472
+ if table.FeatureVariations.Version != 0x00010000:
473
+ raise VarLibError(
474
+ "Unsupported FeatureVariations table version: "
475
+ f"0x{table.FeatureVariations.Version:08x} (expected 0x00010000)."
476
+ )
477
+ table.FeatureVariations.FeatureVariationRecord.extend(featureVariationRecords)
478
+ table.FeatureVariations.FeatureVariationCount = len(
479
+ table.FeatureVariations.FeatureVariationRecord
480
+ )
481
+ else:
482
+ table.FeatureVariations = buildFeatureVariations(featureVariationRecords)
483
+
484
+
485
+ #
486
+ # Building GSUB/FeatureVariations internals
487
+ #
488
+
489
+
490
+ def buildGSUB():
491
+ """Build a GSUB table from scratch."""
492
+ fontTable = newTable("GSUB")
493
+ gsub = fontTable.table = ot.GSUB()
494
+ gsub.Version = 0x00010001 # allow gsub.FeatureVariations
495
+
496
+ gsub.ScriptList = ot.ScriptList()
497
+ gsub.ScriptList.ScriptRecord = []
498
+ gsub.FeatureList = ot.FeatureList()
499
+ gsub.FeatureList.FeatureRecord = []
500
+ gsub.LookupList = ot.LookupList()
501
+ gsub.LookupList.Lookup = []
502
+
503
+ srec = ot.ScriptRecord()
504
+ srec.ScriptTag = "DFLT"
505
+ srec.Script = ot.Script()
506
+ srec.Script.DefaultLangSys = None
507
+ srec.Script.LangSysRecord = []
508
+ srec.Script.LangSysCount = 0
509
+
510
+ langrec = ot.LangSysRecord()
511
+ langrec.LangSys = ot.LangSys()
512
+ langrec.LangSys.ReqFeatureIndex = 0xFFFF
513
+ langrec.LangSys.FeatureIndex = []
514
+ srec.Script.DefaultLangSys = langrec.LangSys
515
+
516
+ gsub.ScriptList.ScriptRecord.append(srec)
517
+ gsub.ScriptList.ScriptCount = 1
518
+ gsub.FeatureVariations = None
519
+
520
+ return fontTable
521
+
522
+
523
+ def makeSubstitutionsHashable(conditionalSubstitutions):
524
+ """Turn all the substitution dictionaries in sorted tuples of tuples so
525
+ they are hashable, to detect duplicates so we don't write out redundant
526
+ data."""
527
+ allSubstitutions = set()
528
+ condSubst = []
529
+ for conditionSet, substitutionMaps in conditionalSubstitutions:
530
+ substitutions = []
531
+ for substitutionMap in substitutionMaps:
532
+ subst = tuple(sorted(substitutionMap.items()))
533
+ substitutions.append(subst)
534
+ allSubstitutions.add(subst)
535
+ condSubst.append((conditionSet, substitutions))
536
+ return condSubst, sorted(allSubstitutions)
537
+
538
+
539
+ class ShifterVisitor(TTVisitor):
540
+ def __init__(self, shift):
541
+ self.shift = shift
542
+
543
+
544
+ @ShifterVisitor.register_attr(ot.Feature, "LookupListIndex") # GSUB/GPOS
545
+ def visit(visitor, obj, attr, value):
546
+ shift = visitor.shift
547
+ value = [l + shift for l in value]
548
+ setattr(obj, attr, value)
549
+
550
+
551
+ @ShifterVisitor.register_attr(
552
+ (ot.SubstLookupRecord, ot.PosLookupRecord), "LookupListIndex"
553
+ )
554
+ def visit(visitor, obj, attr, value):
555
+ setattr(obj, attr, visitor.shift + value)
556
+
557
+
558
+ def buildSubstitutionLookups(gsub, allSubstitutions, processLast=False):
559
+ """Build the lookups for the glyph substitutions, return a dict mapping
560
+ the substitution to lookup indices."""
561
+
562
+ # Insert lookups at the beginning of the lookup vector
563
+ # https://github.com/googlefonts/fontmake/issues/950
564
+
565
+ firstIndex = len(gsub.LookupList.Lookup) if processLast else 0
566
+ lookupMap = {}
567
+ for i, substitutionMap in enumerate(allSubstitutions):
568
+ lookupMap[substitutionMap] = firstIndex + i
569
+
570
+ if not processLast:
571
+ # Shift all lookup indices in gsub by len(allSubstitutions)
572
+ shift = len(allSubstitutions)
573
+ visitor = ShifterVisitor(shift)
574
+ visitor.visit(gsub.FeatureList.FeatureRecord)
575
+ visitor.visit(gsub.LookupList.Lookup)
576
+
577
+ for i, subst in enumerate(allSubstitutions):
578
+ substMap = dict(subst)
579
+ lookup = buildLookup([buildSingleSubstSubtable(substMap)])
580
+ if processLast:
581
+ gsub.LookupList.Lookup.append(lookup)
582
+ else:
583
+ gsub.LookupList.Lookup.insert(i, lookup)
584
+ assert gsub.LookupList.Lookup[lookupMap[subst]] is lookup
585
+ gsub.LookupList.LookupCount = len(gsub.LookupList.Lookup)
586
+ return lookupMap
587
+
588
+
589
+ def buildFeatureVariations(featureVariationRecords):
590
+ """Build the FeatureVariations subtable."""
591
+ fv = ot.FeatureVariations()
592
+ fv.Version = 0x00010000
593
+ fv.FeatureVariationRecord = featureVariationRecords
594
+ fv.FeatureVariationCount = len(featureVariationRecords)
595
+ return fv
596
+
597
+
598
+ def buildFeatureRecord(featureTag, lookupListIndices):
599
+ """Build a FeatureRecord."""
600
+ fr = ot.FeatureRecord()
601
+ fr.FeatureTag = featureTag
602
+ fr.Feature = ot.Feature()
603
+ fr.Feature.LookupListIndex = lookupListIndices
604
+ fr.Feature.populateDefaults()
605
+ return fr
606
+
607
+
608
+ def buildFeatureVariationRecord(conditionTable, substitutionRecords):
609
+ """Build a FeatureVariationRecord."""
610
+ fvr = ot.FeatureVariationRecord()
611
+ if len(conditionTable) != 0:
612
+ fvr.ConditionSet = ot.ConditionSet()
613
+ fvr.ConditionSet.ConditionTable = conditionTable
614
+ fvr.ConditionSet.ConditionCount = len(conditionTable)
615
+ else:
616
+ fvr.ConditionSet = None
617
+ fvr.FeatureTableSubstitution = ot.FeatureTableSubstitution()
618
+ fvr.FeatureTableSubstitution.Version = 0x00010000
619
+ fvr.FeatureTableSubstitution.SubstitutionRecord = substitutionRecords
620
+ fvr.FeatureTableSubstitution.SubstitutionCount = len(substitutionRecords)
621
+ return fvr
622
+
623
+
624
+ def buildFeatureTableSubstitutionRecord(featureIndex, lookupListIndices):
625
+ """Build a FeatureTableSubstitutionRecord."""
626
+ ftsr = ot.FeatureTableSubstitutionRecord()
627
+ ftsr.FeatureIndex = featureIndex
628
+ ftsr.Feature = ot.Feature()
629
+ ftsr.Feature.LookupListIndex = lookupListIndices
630
+ ftsr.Feature.LookupCount = len(lookupListIndices)
631
+ return ftsr
632
+
633
+
634
+ def buildConditionTable(axisIndex, filterRangeMinValue, filterRangeMaxValue):
635
+ """Build a ConditionTable."""
636
+ ct = ot.ConditionTable()
637
+ ct.Format = 1
638
+ ct.AxisIndex = axisIndex
639
+ ct.FilterRangeMinValue = filterRangeMinValue
640
+ ct.FilterRangeMaxValue = filterRangeMaxValue
641
+ return ct
642
+
643
+
644
+ def findFeatureVariationRecord(featureVariations, conditionTable):
645
+ """Find a FeatureVariationRecord that has the same conditionTable."""
646
+ if featureVariations.Version != 0x00010000:
647
+ raise VarLibError(
648
+ "Unsupported FeatureVariations table version: "
649
+ f"0x{featureVariations.Version:08x} (expected 0x00010000)."
650
+ )
651
+
652
+ for fvr in featureVariations.FeatureVariationRecord:
653
+ if conditionTable == fvr.ConditionSet.ConditionTable:
654
+ return fvr
655
+
656
+ return None
657
+
658
+
659
+ def sortFeatureList(table):
660
+ """Sort the feature list by feature tag, and remap the feature indices
661
+ elsewhere. This is needed after the feature list has been modified.
662
+ """
663
+ # decorate, sort, undecorate, because we need to make an index remapping table
664
+ tagIndexFea = [
665
+ (fea.FeatureTag, index, fea)
666
+ for index, fea in enumerate(table.FeatureList.FeatureRecord)
667
+ ]
668
+ tagIndexFea.sort()
669
+ table.FeatureList.FeatureRecord = [fea for tag, index, fea in tagIndexFea]
670
+ featureRemap = dict(
671
+ zip([index for tag, index, fea in tagIndexFea], range(len(tagIndexFea)))
672
+ )
673
+
674
+ # Remap the feature indices
675
+ remapFeatures(table, featureRemap)
676
+
677
+
678
+ def remapFeatures(table, featureRemap):
679
+ """Go through the scripts list, and remap feature indices."""
680
+ for scriptIndex, script in enumerate(table.ScriptList.ScriptRecord):
681
+ defaultLangSys = script.Script.DefaultLangSys
682
+ if defaultLangSys is not None:
683
+ _remapLangSys(defaultLangSys, featureRemap)
684
+ for langSysRecordIndex, langSysRec in enumerate(script.Script.LangSysRecord):
685
+ langSys = langSysRec.LangSys
686
+ _remapLangSys(langSys, featureRemap)
687
+
688
+ if hasattr(table, "FeatureVariations") and table.FeatureVariations is not None:
689
+ for fvr in table.FeatureVariations.FeatureVariationRecord:
690
+ for ftsr in fvr.FeatureTableSubstitution.SubstitutionRecord:
691
+ ftsr.FeatureIndex = featureRemap[ftsr.FeatureIndex]
692
+
693
+
694
+ def _remapLangSys(langSys, featureRemap):
695
+ if langSys.ReqFeatureIndex != 0xFFFF:
696
+ langSys.ReqFeatureIndex = featureRemap[langSys.ReqFeatureIndex]
697
+ langSys.FeatureIndex = [featureRemap[index] for index in langSys.FeatureIndex]
698
+
699
+
700
+ if __name__ == "__main__":
701
+ import doctest, sys
702
+
703
+ sys.exit(doctest.testmod().failed)