fonttools 4.55.6__cp311-cp311-musllinux_1_2_x86_64.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.

Potentially problematic release.


This version of fonttools might be problematic. Click here for more details.

Files changed (329) 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/cffLib/CFF2ToCFF.py +203 -0
  6. fontTools/cffLib/CFFToCFF2.py +305 -0
  7. fontTools/cffLib/__init__.py +3659 -0
  8. fontTools/cffLib/specializer.py +924 -0
  9. fontTools/cffLib/transforms.py +490 -0
  10. fontTools/cffLib/width.py +210 -0
  11. fontTools/colorLib/__init__.py +0 -0
  12. fontTools/colorLib/builder.py +664 -0
  13. fontTools/colorLib/errors.py +2 -0
  14. fontTools/colorLib/geometry.py +143 -0
  15. fontTools/colorLib/table_builder.py +223 -0
  16. fontTools/colorLib/unbuilder.py +81 -0
  17. fontTools/config/__init__.py +75 -0
  18. fontTools/cu2qu/__init__.py +15 -0
  19. fontTools/cu2qu/__main__.py +6 -0
  20. fontTools/cu2qu/benchmark.py +54 -0
  21. fontTools/cu2qu/cli.py +198 -0
  22. fontTools/cu2qu/cu2qu.c +14829 -0
  23. fontTools/cu2qu/cu2qu.cpython-311-x86_64-linux-musl.so +0 -0
  24. fontTools/cu2qu/cu2qu.py +531 -0
  25. fontTools/cu2qu/errors.py +77 -0
  26. fontTools/cu2qu/ufo.py +349 -0
  27. fontTools/designspaceLib/__init__.py +3338 -0
  28. fontTools/designspaceLib/__main__.py +6 -0
  29. fontTools/designspaceLib/split.py +475 -0
  30. fontTools/designspaceLib/statNames.py +253 -0
  31. fontTools/designspaceLib/types.py +147 -0
  32. fontTools/encodings/MacRoman.py +258 -0
  33. fontTools/encodings/StandardEncoding.py +258 -0
  34. fontTools/encodings/__init__.py +1 -0
  35. fontTools/encodings/codecs.py +135 -0
  36. fontTools/feaLib/__init__.py +4 -0
  37. fontTools/feaLib/__main__.py +78 -0
  38. fontTools/feaLib/ast.py +2134 -0
  39. fontTools/feaLib/builder.py +1747 -0
  40. fontTools/feaLib/error.py +22 -0
  41. fontTools/feaLib/lexer.c +17986 -0
  42. fontTools/feaLib/lexer.cpython-311-x86_64-linux-musl.so +0 -0
  43. fontTools/feaLib/lexer.py +287 -0
  44. fontTools/feaLib/location.py +12 -0
  45. fontTools/feaLib/lookupDebugInfo.py +12 -0
  46. fontTools/feaLib/parser.py +2359 -0
  47. fontTools/feaLib/variableScalar.py +113 -0
  48. fontTools/fontBuilder.py +1008 -0
  49. fontTools/help.py +36 -0
  50. fontTools/merge/__init__.py +248 -0
  51. fontTools/merge/__main__.py +6 -0
  52. fontTools/merge/base.py +81 -0
  53. fontTools/merge/cmap.py +141 -0
  54. fontTools/merge/layout.py +526 -0
  55. fontTools/merge/options.py +85 -0
  56. fontTools/merge/tables.py +341 -0
  57. fontTools/merge/unicode.py +78 -0
  58. fontTools/merge/util.py +143 -0
  59. fontTools/misc/__init__.py +1 -0
  60. fontTools/misc/arrayTools.py +424 -0
  61. fontTools/misc/bezierTools.c +41831 -0
  62. fontTools/misc/bezierTools.cpython-311-x86_64-linux-musl.so +0 -0
  63. fontTools/misc/bezierTools.py +1497 -0
  64. fontTools/misc/classifyTools.py +170 -0
  65. fontTools/misc/cliTools.py +53 -0
  66. fontTools/misc/configTools.py +349 -0
  67. fontTools/misc/cython.py +27 -0
  68. fontTools/misc/dictTools.py +83 -0
  69. fontTools/misc/eexec.py +119 -0
  70. fontTools/misc/encodingTools.py +72 -0
  71. fontTools/misc/etree.py +479 -0
  72. fontTools/misc/filenames.py +245 -0
  73. fontTools/misc/fixedTools.py +253 -0
  74. fontTools/misc/intTools.py +25 -0
  75. fontTools/misc/iterTools.py +12 -0
  76. fontTools/misc/lazyTools.py +42 -0
  77. fontTools/misc/loggingTools.py +543 -0
  78. fontTools/misc/macCreatorType.py +56 -0
  79. fontTools/misc/macRes.py +261 -0
  80. fontTools/misc/plistlib/__init__.py +681 -0
  81. fontTools/misc/plistlib/py.typed +0 -0
  82. fontTools/misc/psCharStrings.py +1496 -0
  83. fontTools/misc/psLib.py +398 -0
  84. fontTools/misc/psOperators.py +572 -0
  85. fontTools/misc/py23.py +96 -0
  86. fontTools/misc/roundTools.py +110 -0
  87. fontTools/misc/sstruct.py +231 -0
  88. fontTools/misc/symfont.py +244 -0
  89. fontTools/misc/testTools.py +229 -0
  90. fontTools/misc/textTools.py +154 -0
  91. fontTools/misc/timeTools.py +88 -0
  92. fontTools/misc/transform.py +516 -0
  93. fontTools/misc/treeTools.py +45 -0
  94. fontTools/misc/vector.py +147 -0
  95. fontTools/misc/visitor.py +142 -0
  96. fontTools/misc/xmlReader.py +188 -0
  97. fontTools/misc/xmlWriter.py +204 -0
  98. fontTools/mtiLib/__init__.py +1402 -0
  99. fontTools/mtiLib/__main__.py +5 -0
  100. fontTools/otlLib/__init__.py +1 -0
  101. fontTools/otlLib/builder.py +3221 -0
  102. fontTools/otlLib/error.py +11 -0
  103. fontTools/otlLib/maxContextCalc.py +96 -0
  104. fontTools/otlLib/optimize/__init__.py +53 -0
  105. fontTools/otlLib/optimize/__main__.py +6 -0
  106. fontTools/otlLib/optimize/gpos.py +453 -0
  107. fontTools/pens/__init__.py +1 -0
  108. fontTools/pens/areaPen.py +52 -0
  109. fontTools/pens/basePen.py +475 -0
  110. fontTools/pens/boundsPen.py +98 -0
  111. fontTools/pens/cairoPen.py +26 -0
  112. fontTools/pens/cocoaPen.py +26 -0
  113. fontTools/pens/cu2quPen.py +325 -0
  114. fontTools/pens/explicitClosingLinePen.py +101 -0
  115. fontTools/pens/filterPen.py +241 -0
  116. fontTools/pens/freetypePen.py +462 -0
  117. fontTools/pens/hashPointPen.py +89 -0
  118. fontTools/pens/momentsPen.c +13448 -0
  119. fontTools/pens/momentsPen.cpython-311-x86_64-linux-musl.so +0 -0
  120. fontTools/pens/momentsPen.py +879 -0
  121. fontTools/pens/perimeterPen.py +69 -0
  122. fontTools/pens/pointInsidePen.py +192 -0
  123. fontTools/pens/pointPen.py +600 -0
  124. fontTools/pens/qtPen.py +29 -0
  125. fontTools/pens/qu2cuPen.py +105 -0
  126. fontTools/pens/quartzPen.py +43 -0
  127. fontTools/pens/recordingPen.py +335 -0
  128. fontTools/pens/reportLabPen.py +79 -0
  129. fontTools/pens/reverseContourPen.py +96 -0
  130. fontTools/pens/roundingPen.py +130 -0
  131. fontTools/pens/statisticsPen.py +312 -0
  132. fontTools/pens/svgPathPen.py +310 -0
  133. fontTools/pens/t2CharStringPen.py +68 -0
  134. fontTools/pens/teePen.py +55 -0
  135. fontTools/pens/transformPen.py +115 -0
  136. fontTools/pens/ttGlyphPen.py +335 -0
  137. fontTools/pens/wxPen.py +29 -0
  138. fontTools/qu2cu/__init__.py +15 -0
  139. fontTools/qu2cu/__main__.py +7 -0
  140. fontTools/qu2cu/benchmark.py +56 -0
  141. fontTools/qu2cu/cli.py +125 -0
  142. fontTools/qu2cu/qu2cu.c +16269 -0
  143. fontTools/qu2cu/qu2cu.cpython-311-x86_64-linux-musl.so +0 -0
  144. fontTools/qu2cu/qu2cu.py +405 -0
  145. fontTools/subset/__init__.py +3838 -0
  146. fontTools/subset/__main__.py +6 -0
  147. fontTools/subset/cff.py +184 -0
  148. fontTools/subset/svg.py +253 -0
  149. fontTools/subset/util.py +25 -0
  150. fontTools/svgLib/__init__.py +3 -0
  151. fontTools/svgLib/path/__init__.py +65 -0
  152. fontTools/svgLib/path/arc.py +154 -0
  153. fontTools/svgLib/path/parser.py +322 -0
  154. fontTools/svgLib/path/shapes.py +183 -0
  155. fontTools/t1Lib/__init__.py +648 -0
  156. fontTools/tfmLib.py +460 -0
  157. fontTools/ttLib/__init__.py +26 -0
  158. fontTools/ttLib/__main__.py +109 -0
  159. fontTools/ttLib/macUtils.py +54 -0
  160. fontTools/ttLib/removeOverlaps.py +393 -0
  161. fontTools/ttLib/reorderGlyphs.py +284 -0
  162. fontTools/ttLib/scaleUpem.py +436 -0
  163. fontTools/ttLib/sfnt.py +662 -0
  164. fontTools/ttLib/standardGlyphOrder.py +271 -0
  165. fontTools/ttLib/tables/B_A_S_E_.py +14 -0
  166. fontTools/ttLib/tables/BitmapGlyphMetrics.py +64 -0
  167. fontTools/ttLib/tables/C_B_D_T_.py +113 -0
  168. fontTools/ttLib/tables/C_B_L_C_.py +19 -0
  169. fontTools/ttLib/tables/C_F_F_.py +61 -0
  170. fontTools/ttLib/tables/C_F_F__2.py +26 -0
  171. fontTools/ttLib/tables/C_O_L_R_.py +165 -0
  172. fontTools/ttLib/tables/C_P_A_L_.py +305 -0
  173. fontTools/ttLib/tables/D_S_I_G_.py +158 -0
  174. fontTools/ttLib/tables/D__e_b_g.py +17 -0
  175. fontTools/ttLib/tables/DefaultTable.py +49 -0
  176. fontTools/ttLib/tables/E_B_D_T_.py +835 -0
  177. fontTools/ttLib/tables/E_B_L_C_.py +718 -0
  178. fontTools/ttLib/tables/F_F_T_M_.py +52 -0
  179. fontTools/ttLib/tables/F__e_a_t.py +149 -0
  180. fontTools/ttLib/tables/G_D_E_F_.py +13 -0
  181. fontTools/ttLib/tables/G_M_A_P_.py +148 -0
  182. fontTools/ttLib/tables/G_P_K_G_.py +133 -0
  183. fontTools/ttLib/tables/G_P_O_S_.py +14 -0
  184. fontTools/ttLib/tables/G_S_U_B_.py +13 -0
  185. fontTools/ttLib/tables/G__l_a_t.py +235 -0
  186. fontTools/ttLib/tables/G__l_o_c.py +85 -0
  187. fontTools/ttLib/tables/H_V_A_R_.py +13 -0
  188. fontTools/ttLib/tables/J_S_T_F_.py +13 -0
  189. fontTools/ttLib/tables/L_T_S_H_.py +58 -0
  190. fontTools/ttLib/tables/M_A_T_H_.py +13 -0
  191. fontTools/ttLib/tables/M_E_T_A_.py +352 -0
  192. fontTools/ttLib/tables/M_V_A_R_.py +13 -0
  193. fontTools/ttLib/tables/O_S_2f_2.py +752 -0
  194. fontTools/ttLib/tables/S_I_N_G_.py +99 -0
  195. fontTools/ttLib/tables/S_T_A_T_.py +15 -0
  196. fontTools/ttLib/tables/S_V_G_.py +223 -0
  197. fontTools/ttLib/tables/S__i_l_f.py +1040 -0
  198. fontTools/ttLib/tables/S__i_l_l.py +92 -0
  199. fontTools/ttLib/tables/T_S_I_B_.py +13 -0
  200. fontTools/ttLib/tables/T_S_I_C_.py +14 -0
  201. fontTools/ttLib/tables/T_S_I_D_.py +13 -0
  202. fontTools/ttLib/tables/T_S_I_J_.py +13 -0
  203. fontTools/ttLib/tables/T_S_I_P_.py +13 -0
  204. fontTools/ttLib/tables/T_S_I_S_.py +13 -0
  205. fontTools/ttLib/tables/T_S_I_V_.py +26 -0
  206. fontTools/ttLib/tables/T_S_I__0.py +59 -0
  207. fontTools/ttLib/tables/T_S_I__1.py +166 -0
  208. fontTools/ttLib/tables/T_S_I__2.py +17 -0
  209. fontTools/ttLib/tables/T_S_I__3.py +22 -0
  210. fontTools/ttLib/tables/T_S_I__5.py +49 -0
  211. fontTools/ttLib/tables/T_T_F_A_.py +14 -0
  212. fontTools/ttLib/tables/TupleVariation.py +884 -0
  213. fontTools/ttLib/tables/V_A_R_C_.py +12 -0
  214. fontTools/ttLib/tables/V_D_M_X_.py +249 -0
  215. fontTools/ttLib/tables/V_O_R_G_.py +165 -0
  216. fontTools/ttLib/tables/V_V_A_R_.py +13 -0
  217. fontTools/ttLib/tables/__init__.py +97 -0
  218. fontTools/ttLib/tables/_a_n_k_r.py +15 -0
  219. fontTools/ttLib/tables/_a_v_a_r.py +191 -0
  220. fontTools/ttLib/tables/_b_s_l_n.py +15 -0
  221. fontTools/ttLib/tables/_c_i_d_g.py +24 -0
  222. fontTools/ttLib/tables/_c_m_a_p.py +1578 -0
  223. fontTools/ttLib/tables/_c_v_a_r.py +94 -0
  224. fontTools/ttLib/tables/_c_v_t.py +55 -0
  225. fontTools/ttLib/tables/_f_e_a_t.py +15 -0
  226. fontTools/ttLib/tables/_f_p_g_m.py +60 -0
  227. fontTools/ttLib/tables/_f_v_a_r.py +261 -0
  228. fontTools/ttLib/tables/_g_a_s_p.py +63 -0
  229. fontTools/ttLib/tables/_g_c_i_d.py +13 -0
  230. fontTools/ttLib/tables/_g_l_y_f.py +2311 -0
  231. fontTools/ttLib/tables/_g_v_a_r.py +292 -0
  232. fontTools/ttLib/tables/_h_d_m_x.py +127 -0
  233. fontTools/ttLib/tables/_h_e_a_d.py +130 -0
  234. fontTools/ttLib/tables/_h_h_e_a.py +147 -0
  235. fontTools/ttLib/tables/_h_m_t_x.py +160 -0
  236. fontTools/ttLib/tables/_k_e_r_n.py +289 -0
  237. fontTools/ttLib/tables/_l_c_a_r.py +13 -0
  238. fontTools/ttLib/tables/_l_o_c_a.py +70 -0
  239. fontTools/ttLib/tables/_l_t_a_g.py +72 -0
  240. fontTools/ttLib/tables/_m_a_x_p.py +147 -0
  241. fontTools/ttLib/tables/_m_e_t_a.py +112 -0
  242. fontTools/ttLib/tables/_m_o_r_t.py +14 -0
  243. fontTools/ttLib/tables/_m_o_r_x.py +15 -0
  244. fontTools/ttLib/tables/_n_a_m_e.py +1237 -0
  245. fontTools/ttLib/tables/_o_p_b_d.py +14 -0
  246. fontTools/ttLib/tables/_p_o_s_t.py +317 -0
  247. fontTools/ttLib/tables/_p_r_e_p.py +16 -0
  248. fontTools/ttLib/tables/_p_r_o_p.py +12 -0
  249. fontTools/ttLib/tables/_s_b_i_x.py +129 -0
  250. fontTools/ttLib/tables/_t_r_a_k.py +332 -0
  251. fontTools/ttLib/tables/_v_h_e_a.py +139 -0
  252. fontTools/ttLib/tables/_v_m_t_x.py +19 -0
  253. fontTools/ttLib/tables/asciiTable.py +20 -0
  254. fontTools/ttLib/tables/grUtils.py +92 -0
  255. fontTools/ttLib/tables/otBase.py +1465 -0
  256. fontTools/ttLib/tables/otConverters.py +2065 -0
  257. fontTools/ttLib/tables/otData.py +6400 -0
  258. fontTools/ttLib/tables/otTables.py +2700 -0
  259. fontTools/ttLib/tables/otTraverse.py +162 -0
  260. fontTools/ttLib/tables/sbixGlyph.py +149 -0
  261. fontTools/ttLib/tables/sbixStrike.py +177 -0
  262. fontTools/ttLib/tables/table_API_readme.txt +91 -0
  263. fontTools/ttLib/tables/ttProgram.py +594 -0
  264. fontTools/ttLib/ttCollection.py +125 -0
  265. fontTools/ttLib/ttFont.py +1155 -0
  266. fontTools/ttLib/ttGlyphSet.py +500 -0
  267. fontTools/ttLib/ttVisitor.py +32 -0
  268. fontTools/ttLib/woff2.py +1683 -0
  269. fontTools/ttx.py +467 -0
  270. fontTools/ufoLib/__init__.py +2477 -0
  271. fontTools/ufoLib/converters.py +334 -0
  272. fontTools/ufoLib/errors.py +22 -0
  273. fontTools/ufoLib/etree.py +6 -0
  274. fontTools/ufoLib/filenames.py +291 -0
  275. fontTools/ufoLib/glifLib.py +2022 -0
  276. fontTools/ufoLib/kerning.py +91 -0
  277. fontTools/ufoLib/plistlib.py +47 -0
  278. fontTools/ufoLib/pointPen.py +6 -0
  279. fontTools/ufoLib/utils.py +76 -0
  280. fontTools/ufoLib/validators.py +1186 -0
  281. fontTools/unicode.py +50 -0
  282. fontTools/unicodedata/Blocks.py +802 -0
  283. fontTools/unicodedata/OTTags.py +50 -0
  284. fontTools/unicodedata/ScriptExtensions.py +806 -0
  285. fontTools/unicodedata/Scripts.py +3618 -0
  286. fontTools/unicodedata/__init__.py +298 -0
  287. fontTools/varLib/__init__.py +1511 -0
  288. fontTools/varLib/__main__.py +6 -0
  289. fontTools/varLib/avar.py +260 -0
  290. fontTools/varLib/avarPlanner.py +1004 -0
  291. fontTools/varLib/builder.py +215 -0
  292. fontTools/varLib/cff.py +631 -0
  293. fontTools/varLib/errors.py +219 -0
  294. fontTools/varLib/featureVars.py +689 -0
  295. fontTools/varLib/instancer/__init__.py +1937 -0
  296. fontTools/varLib/instancer/__main__.py +5 -0
  297. fontTools/varLib/instancer/featureVars.py +190 -0
  298. fontTools/varLib/instancer/names.py +388 -0
  299. fontTools/varLib/instancer/solver.py +309 -0
  300. fontTools/varLib/interpolatable.py +1209 -0
  301. fontTools/varLib/interpolatableHelpers.py +396 -0
  302. fontTools/varLib/interpolatablePlot.py +1269 -0
  303. fontTools/varLib/interpolatableTestContourOrder.py +82 -0
  304. fontTools/varLib/interpolatableTestStartingPoint.py +107 -0
  305. fontTools/varLib/interpolate_layout.py +124 -0
  306. fontTools/varLib/iup.c +19154 -0
  307. fontTools/varLib/iup.cpython-311-x86_64-linux-musl.so +0 -0
  308. fontTools/varLib/iup.py +490 -0
  309. fontTools/varLib/merger.py +1717 -0
  310. fontTools/varLib/models.py +642 -0
  311. fontTools/varLib/multiVarStore.py +253 -0
  312. fontTools/varLib/mutator.py +518 -0
  313. fontTools/varLib/mvar.py +40 -0
  314. fontTools/varLib/plot.py +238 -0
  315. fontTools/varLib/stat.py +149 -0
  316. fontTools/varLib/varStore.py +767 -0
  317. fontTools/voltLib/__init__.py +5 -0
  318. fontTools/voltLib/ast.py +448 -0
  319. fontTools/voltLib/error.py +12 -0
  320. fontTools/voltLib/lexer.py +99 -0
  321. fontTools/voltLib/parser.py +656 -0
  322. fontTools/voltLib/voltToFea.py +730 -0
  323. fonttools-4.55.6.data/data/share/man/man1/ttx.1 +225 -0
  324. fonttools-4.55.6.dist-info/LICENSE +21 -0
  325. fonttools-4.55.6.dist-info/METADATA +3413 -0
  326. fonttools-4.55.6.dist-info/RECORD +329 -0
  327. fonttools-4.55.6.dist-info/WHEEL +5 -0
  328. fonttools-4.55.6.dist-info/entry_points.txt +5 -0
  329. fonttools-4.55.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1747 @@
1
+ from fontTools.misc import sstruct
2
+ from fontTools.misc.textTools import Tag, tostr, binary2num, safeEval
3
+ from fontTools.feaLib.error import FeatureLibError
4
+ from fontTools.feaLib.lookupDebugInfo import (
5
+ LookupDebugInfo,
6
+ LOOKUP_DEBUG_INFO_KEY,
7
+ LOOKUP_DEBUG_ENV_VAR,
8
+ )
9
+ from fontTools.feaLib.parser import Parser
10
+ from fontTools.feaLib.ast import FeatureFile
11
+ from fontTools.feaLib.variableScalar import VariableScalar
12
+ from fontTools.otlLib import builder as otl
13
+ from fontTools.otlLib.maxContextCalc import maxCtxFont
14
+ from fontTools.ttLib import newTable, getTableModule
15
+ from fontTools.ttLib.tables import otBase, otTables
16
+ from fontTools.otlLib.builder import (
17
+ AlternateSubstBuilder,
18
+ ChainContextPosBuilder,
19
+ ChainContextSubstBuilder,
20
+ LigatureSubstBuilder,
21
+ MultipleSubstBuilder,
22
+ CursivePosBuilder,
23
+ MarkBasePosBuilder,
24
+ MarkLigPosBuilder,
25
+ MarkMarkPosBuilder,
26
+ ReverseChainSingleSubstBuilder,
27
+ SingleSubstBuilder,
28
+ ClassPairPosSubtableBuilder,
29
+ PairPosBuilder,
30
+ SinglePosBuilder,
31
+ ChainContextualRule,
32
+ )
33
+ from fontTools.otlLib.error import OpenTypeLibError
34
+ from fontTools.varLib.varStore import OnlineVarStoreBuilder
35
+ from fontTools.varLib.builder import buildVarDevTable
36
+ from fontTools.varLib.featureVars import addFeatureVariationsRaw
37
+ from fontTools.varLib.models import normalizeValue, piecewiseLinearMap
38
+ from collections import defaultdict
39
+ import copy
40
+ import itertools
41
+ from io import StringIO
42
+ import logging
43
+ import warnings
44
+ import os
45
+
46
+
47
+ log = logging.getLogger(__name__)
48
+
49
+
50
+ def addOpenTypeFeatures(font, featurefile, tables=None, debug=False):
51
+ """Add features from a file to a font. Note that this replaces any features
52
+ currently present.
53
+
54
+ Args:
55
+ font (feaLib.ttLib.TTFont): The font object.
56
+ featurefile: Either a path or file object (in which case we
57
+ parse it into an AST), or a pre-parsed AST instance.
58
+ tables: If passed, restrict the set of affected tables to those in the
59
+ list.
60
+ debug: Whether to add source debugging information to the font in the
61
+ ``Debg`` table
62
+
63
+ """
64
+ builder = Builder(font, featurefile)
65
+ builder.build(tables=tables, debug=debug)
66
+
67
+
68
+ def addOpenTypeFeaturesFromString(
69
+ font, features, filename=None, tables=None, debug=False
70
+ ):
71
+ """Add features from a string to a font. Note that this replaces any
72
+ features currently present.
73
+
74
+ Args:
75
+ font (feaLib.ttLib.TTFont): The font object.
76
+ features: A string containing feature code.
77
+ filename: The directory containing ``filename`` is used as the root of
78
+ relative ``include()`` paths; if ``None`` is provided, the current
79
+ directory is assumed.
80
+ tables: If passed, restrict the set of affected tables to those in the
81
+ list.
82
+ debug: Whether to add source debugging information to the font in the
83
+ ``Debg`` table
84
+
85
+ """
86
+
87
+ featurefile = StringIO(tostr(features))
88
+ if filename:
89
+ featurefile.name = filename
90
+ addOpenTypeFeatures(font, featurefile, tables=tables, debug=debug)
91
+
92
+
93
+ class Builder(object):
94
+ supportedTables = frozenset(
95
+ Tag(tag)
96
+ for tag in [
97
+ "BASE",
98
+ "GDEF",
99
+ "GPOS",
100
+ "GSUB",
101
+ "OS/2",
102
+ "head",
103
+ "hhea",
104
+ "name",
105
+ "vhea",
106
+ "STAT",
107
+ ]
108
+ )
109
+
110
+ def __init__(self, font, featurefile):
111
+ self.font = font
112
+ # 'featurefile' can be either a path or file object (in which case we
113
+ # parse it into an AST), or a pre-parsed AST instance
114
+ if isinstance(featurefile, FeatureFile):
115
+ self.parseTree, self.file = featurefile, None
116
+ else:
117
+ self.parseTree, self.file = None, featurefile
118
+ self.glyphMap = font.getReverseGlyphMap()
119
+ self.varstorebuilder = None
120
+ if "fvar" in font:
121
+ self.axes = font["fvar"].axes
122
+ self.varstorebuilder = OnlineVarStoreBuilder(
123
+ [ax.axisTag for ax in self.axes]
124
+ )
125
+ self.default_language_systems_ = set()
126
+ self.script_ = None
127
+ self.lookupflag_ = 0
128
+ self.lookupflag_markFilterSet_ = None
129
+ self.language_systems = set()
130
+ self.seen_non_DFLT_script_ = False
131
+ self.named_lookups_ = {}
132
+ self.cur_lookup_ = None
133
+ self.cur_lookup_name_ = None
134
+ self.cur_feature_name_ = None
135
+ self.lookups_ = []
136
+ self.lookup_locations = {"GSUB": {}, "GPOS": {}}
137
+ self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*]
138
+ self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp'
139
+ self.feature_variations_ = {}
140
+ # for feature 'aalt'
141
+ self.aalt_features_ = [] # [(location, featureName)*], for 'aalt'
142
+ self.aalt_location_ = None
143
+ self.aalt_alternates_ = {}
144
+ # for 'featureNames'
145
+ self.featureNames_ = set()
146
+ self.featureNames_ids_ = {}
147
+ # for 'cvParameters'
148
+ self.cv_parameters_ = set()
149
+ self.cv_parameters_ids_ = {}
150
+ self.cv_num_named_params_ = {}
151
+ self.cv_characters_ = defaultdict(list)
152
+ # for feature 'size'
153
+ self.size_parameters_ = None
154
+ # for table 'head'
155
+ self.fontRevision_ = None # 2.71
156
+ # for table 'name'
157
+ self.names_ = []
158
+ # for table 'BASE'
159
+ self.base_horiz_axis_ = None
160
+ self.base_vert_axis_ = None
161
+ # for table 'GDEF'
162
+ self.attachPoints_ = {} # "a" --> {3, 7}
163
+ self.ligCaretCoords_ = {} # "f_f_i" --> {300, 600}
164
+ self.ligCaretPoints_ = {} # "f_f_i" --> {3, 7}
165
+ self.glyphClassDefs_ = {} # "fi" --> (2, (file, line, column))
166
+ self.markAttach_ = {} # "acute" --> (4, (file, line, column))
167
+ self.markAttachClassID_ = {} # frozenset({"acute", "grave"}) --> 4
168
+ self.markFilterSets_ = {} # frozenset({"acute", "grave"}) --> 4
169
+ # for table 'OS/2'
170
+ self.os2_ = {}
171
+ # for table 'hhea'
172
+ self.hhea_ = {}
173
+ # for table 'vhea'
174
+ self.vhea_ = {}
175
+ # for table 'STAT'
176
+ self.stat_ = {}
177
+ # for conditionsets
178
+ self.conditionsets_ = {}
179
+ # We will often use exactly the same locations (i.e. the font's masters)
180
+ # for a large number of variable scalars. Instead of creating a model
181
+ # for each, let's share the models.
182
+ self.model_cache = {}
183
+
184
+ def build(self, tables=None, debug=False):
185
+ if self.parseTree is None:
186
+ self.parseTree = Parser(self.file, self.glyphMap).parse()
187
+ self.parseTree.build(self)
188
+ # by default, build all the supported tables
189
+ if tables is None:
190
+ tables = self.supportedTables
191
+ else:
192
+ tables = frozenset(tables)
193
+ unsupported = tables - self.supportedTables
194
+ if unsupported:
195
+ unsupported_string = ", ".join(sorted(unsupported))
196
+ raise NotImplementedError(
197
+ "The following tables were requested but are unsupported: "
198
+ f"{unsupported_string}."
199
+ )
200
+ if "GSUB" in tables:
201
+ self.build_feature_aalt_()
202
+ if "head" in tables:
203
+ self.build_head()
204
+ if "hhea" in tables:
205
+ self.build_hhea()
206
+ if "vhea" in tables:
207
+ self.build_vhea()
208
+ if "name" in tables:
209
+ self.build_name()
210
+ if "OS/2" in tables:
211
+ self.build_OS_2()
212
+ if "STAT" in tables:
213
+ self.build_STAT()
214
+ for tag in ("GPOS", "GSUB"):
215
+ if tag not in tables:
216
+ continue
217
+ table = self.makeTable(tag)
218
+ if self.feature_variations_:
219
+ self.makeFeatureVariations(table, tag)
220
+ if (
221
+ table.ScriptList.ScriptCount > 0
222
+ or table.FeatureList.FeatureCount > 0
223
+ or table.LookupList.LookupCount > 0
224
+ ):
225
+ fontTable = self.font[tag] = newTable(tag)
226
+ fontTable.table = table
227
+ elif tag in self.font:
228
+ del self.font[tag]
229
+ if any(tag in self.font for tag in ("GPOS", "GSUB")) and "OS/2" in self.font:
230
+ self.font["OS/2"].usMaxContext = maxCtxFont(self.font)
231
+ if "GDEF" in tables:
232
+ gdef = self.buildGDEF()
233
+ if gdef:
234
+ self.font["GDEF"] = gdef
235
+ elif "GDEF" in self.font:
236
+ del self.font["GDEF"]
237
+ if "BASE" in tables:
238
+ base = self.buildBASE()
239
+ if base:
240
+ self.font["BASE"] = base
241
+ elif "BASE" in self.font:
242
+ del self.font["BASE"]
243
+ if debug or os.environ.get(LOOKUP_DEBUG_ENV_VAR):
244
+ self.buildDebg()
245
+
246
+ def get_chained_lookup_(self, location, builder_class):
247
+ result = builder_class(self.font, location)
248
+ result.lookupflag = self.lookupflag_
249
+ result.markFilterSet = self.lookupflag_markFilterSet_
250
+ self.lookups_.append(result)
251
+ return result
252
+
253
+ def add_lookup_to_feature_(self, lookup, feature_name):
254
+ for script, lang in self.language_systems:
255
+ key = (script, lang, feature_name)
256
+ self.features_.setdefault(key, []).append(lookup)
257
+
258
+ def get_lookup_(self, location, builder_class):
259
+ if (
260
+ self.cur_lookup_
261
+ and type(self.cur_lookup_) == builder_class
262
+ and self.cur_lookup_.lookupflag == self.lookupflag_
263
+ and self.cur_lookup_.markFilterSet == self.lookupflag_markFilterSet_
264
+ ):
265
+ return self.cur_lookup_
266
+ if self.cur_lookup_name_ and self.cur_lookup_:
267
+ raise FeatureLibError(
268
+ "Within a named lookup block, all rules must be of "
269
+ "the same lookup type and flag",
270
+ location,
271
+ )
272
+ self.cur_lookup_ = builder_class(self.font, location)
273
+ self.cur_lookup_.lookupflag = self.lookupflag_
274
+ self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_
275
+ self.lookups_.append(self.cur_lookup_)
276
+ if self.cur_lookup_name_:
277
+ # We are starting a lookup rule inside a named lookup block.
278
+ self.named_lookups_[self.cur_lookup_name_] = self.cur_lookup_
279
+ if self.cur_feature_name_:
280
+ # We are starting a lookup rule inside a feature. This includes
281
+ # lookup rules inside named lookups inside features.
282
+ self.add_lookup_to_feature_(self.cur_lookup_, self.cur_feature_name_)
283
+ return self.cur_lookup_
284
+
285
+ def build_feature_aalt_(self):
286
+ if not self.aalt_features_ and not self.aalt_alternates_:
287
+ return
288
+ # > alternate glyphs will be sorted in the order that the source features
289
+ # > are named in the aalt definition, not the order of the feature definitions
290
+ # > in the file. Alternates defined explicitly ... will precede all others.
291
+ # https://github.com/fonttools/fonttools/issues/836
292
+ alternates = {g: list(a) for g, a in self.aalt_alternates_.items()}
293
+ for location, name in self.aalt_features_ + [(None, "aalt")]:
294
+ feature = [
295
+ (script, lang, feature, lookups)
296
+ for (script, lang, feature), lookups in self.features_.items()
297
+ if feature == name
298
+ ]
299
+ # "aalt" does not have to specify its own lookups, but it might.
300
+ if not feature and name != "aalt":
301
+ warnings.warn("%s: Feature %s has not been defined" % (location, name))
302
+ continue
303
+ for script, lang, feature, lookups in feature:
304
+ for lookuplist in lookups:
305
+ if not isinstance(lookuplist, list):
306
+ lookuplist = [lookuplist]
307
+ for lookup in lookuplist:
308
+ for glyph, alts in lookup.getAlternateGlyphs().items():
309
+ alts_for_glyph = alternates.setdefault(glyph, [])
310
+ alts_for_glyph.extend(
311
+ g for g in alts if g not in alts_for_glyph
312
+ )
313
+ single = {
314
+ glyph: repl[0] for glyph, repl in alternates.items() if len(repl) == 1
315
+ }
316
+ multi = {glyph: repl for glyph, repl in alternates.items() if len(repl) > 1}
317
+ if not single and not multi:
318
+ return
319
+ self.features_ = {
320
+ (script, lang, feature): lookups
321
+ for (script, lang, feature), lookups in self.features_.items()
322
+ if feature != "aalt"
323
+ }
324
+ old_lookups = self.lookups_
325
+ self.lookups_ = []
326
+ self.start_feature(self.aalt_location_, "aalt")
327
+ if single:
328
+ single_lookup = self.get_lookup_(location, SingleSubstBuilder)
329
+ single_lookup.mapping = single
330
+ if multi:
331
+ multi_lookup = self.get_lookup_(location, AlternateSubstBuilder)
332
+ multi_lookup.alternates = multi
333
+ self.end_feature()
334
+ self.lookups_.extend(old_lookups)
335
+
336
+ def build_head(self):
337
+ if not self.fontRevision_:
338
+ return
339
+ table = self.font.get("head")
340
+ if not table: # this only happens for unit tests
341
+ table = self.font["head"] = newTable("head")
342
+ table.decompile(b"\0" * 54, self.font)
343
+ table.tableVersion = 1.0
344
+ table.created = table.modified = 3406620153 # 2011-12-13 11:22:33
345
+ table.fontRevision = self.fontRevision_
346
+
347
+ def build_hhea(self):
348
+ if not self.hhea_:
349
+ return
350
+ table = self.font.get("hhea")
351
+ if not table: # this only happens for unit tests
352
+ table = self.font["hhea"] = newTable("hhea")
353
+ table.decompile(b"\0" * 36, self.font)
354
+ table.tableVersion = 0x00010000
355
+ if "caretoffset" in self.hhea_:
356
+ table.caretOffset = self.hhea_["caretoffset"]
357
+ if "ascender" in self.hhea_:
358
+ table.ascent = self.hhea_["ascender"]
359
+ if "descender" in self.hhea_:
360
+ table.descent = self.hhea_["descender"]
361
+ if "linegap" in self.hhea_:
362
+ table.lineGap = self.hhea_["linegap"]
363
+
364
+ def build_vhea(self):
365
+ if not self.vhea_:
366
+ return
367
+ table = self.font.get("vhea")
368
+ if not table: # this only happens for unit tests
369
+ table = self.font["vhea"] = newTable("vhea")
370
+ table.decompile(b"\0" * 36, self.font)
371
+ table.tableVersion = 0x00011000
372
+ if "verttypoascender" in self.vhea_:
373
+ table.ascent = self.vhea_["verttypoascender"]
374
+ if "verttypodescender" in self.vhea_:
375
+ table.descent = self.vhea_["verttypodescender"]
376
+ if "verttypolinegap" in self.vhea_:
377
+ table.lineGap = self.vhea_["verttypolinegap"]
378
+
379
+ def get_user_name_id(self, table):
380
+ # Try to find first unused font-specific name id
381
+ nameIDs = [name.nameID for name in table.names]
382
+ for user_name_id in range(256, 32767):
383
+ if user_name_id not in nameIDs:
384
+ return user_name_id
385
+
386
+ def buildFeatureParams(self, tag):
387
+ params = None
388
+ if tag == "size":
389
+ params = otTables.FeatureParamsSize()
390
+ (
391
+ params.DesignSize,
392
+ params.SubfamilyID,
393
+ params.RangeStart,
394
+ params.RangeEnd,
395
+ ) = self.size_parameters_
396
+ if tag in self.featureNames_ids_:
397
+ params.SubfamilyNameID = self.featureNames_ids_[tag]
398
+ else:
399
+ params.SubfamilyNameID = 0
400
+ elif tag in self.featureNames_:
401
+ if not self.featureNames_ids_:
402
+ # name table wasn't selected among the tables to build; skip
403
+ pass
404
+ else:
405
+ assert tag in self.featureNames_ids_
406
+ params = otTables.FeatureParamsStylisticSet()
407
+ params.Version = 0
408
+ params.UINameID = self.featureNames_ids_[tag]
409
+ elif tag in self.cv_parameters_:
410
+ params = otTables.FeatureParamsCharacterVariants()
411
+ params.Format = 0
412
+ params.FeatUILabelNameID = self.cv_parameters_ids_.get(
413
+ (tag, "FeatUILabelNameID"), 0
414
+ )
415
+ params.FeatUITooltipTextNameID = self.cv_parameters_ids_.get(
416
+ (tag, "FeatUITooltipTextNameID"), 0
417
+ )
418
+ params.SampleTextNameID = self.cv_parameters_ids_.get(
419
+ (tag, "SampleTextNameID"), 0
420
+ )
421
+ params.NumNamedParameters = self.cv_num_named_params_.get(tag, 0)
422
+ params.FirstParamUILabelNameID = self.cv_parameters_ids_.get(
423
+ (tag, "ParamUILabelNameID_0"), 0
424
+ )
425
+ params.CharCount = len(self.cv_characters_[tag])
426
+ params.Character = self.cv_characters_[tag]
427
+ return params
428
+
429
+ def build_name(self):
430
+ if not self.names_:
431
+ return
432
+ table = self.font.get("name")
433
+ if not table: # this only happens for unit tests
434
+ table = self.font["name"] = newTable("name")
435
+ table.names = []
436
+ for name in self.names_:
437
+ nameID, platformID, platEncID, langID, string = name
438
+ # For featureNames block, nameID is 'feature tag'
439
+ # For cvParameters blocks, nameID is ('feature tag', 'block name')
440
+ if not isinstance(nameID, int):
441
+ tag = nameID
442
+ if tag in self.featureNames_:
443
+ if tag not in self.featureNames_ids_:
444
+ self.featureNames_ids_[tag] = self.get_user_name_id(table)
445
+ assert self.featureNames_ids_[tag] is not None
446
+ nameID = self.featureNames_ids_[tag]
447
+ elif tag[0] in self.cv_parameters_:
448
+ if tag not in self.cv_parameters_ids_:
449
+ self.cv_parameters_ids_[tag] = self.get_user_name_id(table)
450
+ assert self.cv_parameters_ids_[tag] is not None
451
+ nameID = self.cv_parameters_ids_[tag]
452
+ table.setName(string, nameID, platformID, platEncID, langID)
453
+ table.names.sort()
454
+
455
+ def build_OS_2(self):
456
+ if not self.os2_:
457
+ return
458
+ table = self.font.get("OS/2")
459
+ if not table: # this only happens for unit tests
460
+ table = self.font["OS/2"] = newTable("OS/2")
461
+ data = b"\0" * sstruct.calcsize(getTableModule("OS/2").OS2_format_0)
462
+ table.decompile(data, self.font)
463
+ version = 0
464
+ if "fstype" in self.os2_:
465
+ table.fsType = self.os2_["fstype"]
466
+ if "panose" in self.os2_:
467
+ panose = getTableModule("OS/2").Panose()
468
+ (
469
+ panose.bFamilyType,
470
+ panose.bSerifStyle,
471
+ panose.bWeight,
472
+ panose.bProportion,
473
+ panose.bContrast,
474
+ panose.bStrokeVariation,
475
+ panose.bArmStyle,
476
+ panose.bLetterForm,
477
+ panose.bMidline,
478
+ panose.bXHeight,
479
+ ) = self.os2_["panose"]
480
+ table.panose = panose
481
+ if "typoascender" in self.os2_:
482
+ table.sTypoAscender = self.os2_["typoascender"]
483
+ if "typodescender" in self.os2_:
484
+ table.sTypoDescender = self.os2_["typodescender"]
485
+ if "typolinegap" in self.os2_:
486
+ table.sTypoLineGap = self.os2_["typolinegap"]
487
+ if "winascent" in self.os2_:
488
+ table.usWinAscent = self.os2_["winascent"]
489
+ if "windescent" in self.os2_:
490
+ table.usWinDescent = self.os2_["windescent"]
491
+ if "vendor" in self.os2_:
492
+ table.achVendID = safeEval("'''" + self.os2_["vendor"] + "'''")
493
+ if "weightclass" in self.os2_:
494
+ table.usWeightClass = self.os2_["weightclass"]
495
+ if "widthclass" in self.os2_:
496
+ table.usWidthClass = self.os2_["widthclass"]
497
+ if "unicoderange" in self.os2_:
498
+ table.setUnicodeRanges(self.os2_["unicoderange"])
499
+ if "codepagerange" in self.os2_:
500
+ pages = self.build_codepages_(self.os2_["codepagerange"])
501
+ table.ulCodePageRange1, table.ulCodePageRange2 = pages
502
+ version = 1
503
+ if "xheight" in self.os2_:
504
+ table.sxHeight = self.os2_["xheight"]
505
+ version = 2
506
+ if "capheight" in self.os2_:
507
+ table.sCapHeight = self.os2_["capheight"]
508
+ version = 2
509
+ if "loweropsize" in self.os2_:
510
+ table.usLowerOpticalPointSize = self.os2_["loweropsize"]
511
+ version = 5
512
+ if "upperopsize" in self.os2_:
513
+ table.usUpperOpticalPointSize = self.os2_["upperopsize"]
514
+ version = 5
515
+
516
+ def checkattr(table, attrs):
517
+ for attr in attrs:
518
+ if not hasattr(table, attr):
519
+ setattr(table, attr, 0)
520
+
521
+ table.version = max(version, table.version)
522
+ # this only happens for unit tests
523
+ if version >= 1:
524
+ checkattr(table, ("ulCodePageRange1", "ulCodePageRange2"))
525
+ if version >= 2:
526
+ checkattr(
527
+ table,
528
+ (
529
+ "sxHeight",
530
+ "sCapHeight",
531
+ "usDefaultChar",
532
+ "usBreakChar",
533
+ "usMaxContext",
534
+ ),
535
+ )
536
+ if version >= 5:
537
+ checkattr(table, ("usLowerOpticalPointSize", "usUpperOpticalPointSize"))
538
+
539
+ def setElidedFallbackName(self, value, location):
540
+ # ElidedFallbackName is a convenience method for setting
541
+ # ElidedFallbackNameID so only one can be allowed
542
+ for token in ("ElidedFallbackName", "ElidedFallbackNameID"):
543
+ if token in self.stat_:
544
+ raise FeatureLibError(
545
+ f"{token} is already set.",
546
+ location,
547
+ )
548
+ if isinstance(value, int):
549
+ self.stat_["ElidedFallbackNameID"] = value
550
+ elif isinstance(value, list):
551
+ self.stat_["ElidedFallbackName"] = value
552
+ else:
553
+ raise AssertionError(value)
554
+
555
+ def addDesignAxis(self, designAxis, location):
556
+ if "DesignAxes" not in self.stat_:
557
+ self.stat_["DesignAxes"] = []
558
+ if designAxis.tag in (r.tag for r in self.stat_["DesignAxes"]):
559
+ raise FeatureLibError(
560
+ f'DesignAxis already defined for tag "{designAxis.tag}".',
561
+ location,
562
+ )
563
+ if designAxis.axisOrder in (r.axisOrder for r in self.stat_["DesignAxes"]):
564
+ raise FeatureLibError(
565
+ f"DesignAxis already defined for axis number {designAxis.axisOrder}.",
566
+ location,
567
+ )
568
+ self.stat_["DesignAxes"].append(designAxis)
569
+
570
+ def addAxisValueRecord(self, axisValueRecord, location):
571
+ if "AxisValueRecords" not in self.stat_:
572
+ self.stat_["AxisValueRecords"] = []
573
+ # Check for duplicate AxisValueRecords
574
+ for record_ in self.stat_["AxisValueRecords"]:
575
+ if (
576
+ {n.asFea() for n in record_.names}
577
+ == {n.asFea() for n in axisValueRecord.names}
578
+ and {n.asFea() for n in record_.locations}
579
+ == {n.asFea() for n in axisValueRecord.locations}
580
+ and record_.flags == axisValueRecord.flags
581
+ ):
582
+ raise FeatureLibError(
583
+ "An AxisValueRecord with these values is already defined.",
584
+ location,
585
+ )
586
+ self.stat_["AxisValueRecords"].append(axisValueRecord)
587
+
588
+ def build_STAT(self):
589
+ if not self.stat_:
590
+ return
591
+
592
+ axes = self.stat_.get("DesignAxes")
593
+ if not axes:
594
+ raise FeatureLibError("DesignAxes not defined", None)
595
+ axisValueRecords = self.stat_.get("AxisValueRecords")
596
+ axisValues = {}
597
+ format4_locations = []
598
+ for tag in axes:
599
+ axisValues[tag.tag] = []
600
+ if axisValueRecords is not None:
601
+ for avr in axisValueRecords:
602
+ valuesDict = {}
603
+ if avr.flags > 0:
604
+ valuesDict["flags"] = avr.flags
605
+ if len(avr.locations) == 1:
606
+ location = avr.locations[0]
607
+ values = location.values
608
+ if len(values) == 1: # format1
609
+ valuesDict.update({"value": values[0], "name": avr.names})
610
+ if len(values) == 2: # format3
611
+ valuesDict.update(
612
+ {
613
+ "value": values[0],
614
+ "linkedValue": values[1],
615
+ "name": avr.names,
616
+ }
617
+ )
618
+ if len(values) == 3: # format2
619
+ nominal, minVal, maxVal = values
620
+ valuesDict.update(
621
+ {
622
+ "nominalValue": nominal,
623
+ "rangeMinValue": minVal,
624
+ "rangeMaxValue": maxVal,
625
+ "name": avr.names,
626
+ }
627
+ )
628
+ axisValues[location.tag].append(valuesDict)
629
+ else:
630
+ valuesDict.update(
631
+ {
632
+ "location": {i.tag: i.values[0] for i in avr.locations},
633
+ "name": avr.names,
634
+ }
635
+ )
636
+ format4_locations.append(valuesDict)
637
+
638
+ designAxes = [
639
+ {
640
+ "ordering": a.axisOrder,
641
+ "tag": a.tag,
642
+ "name": a.names,
643
+ "values": axisValues[a.tag],
644
+ }
645
+ for a in axes
646
+ ]
647
+
648
+ nameTable = self.font.get("name")
649
+ if not nameTable: # this only happens for unit tests
650
+ nameTable = self.font["name"] = newTable("name")
651
+ nameTable.names = []
652
+
653
+ if "ElidedFallbackNameID" in self.stat_:
654
+ nameID = self.stat_["ElidedFallbackNameID"]
655
+ name = nameTable.getDebugName(nameID)
656
+ if not name:
657
+ raise FeatureLibError(
658
+ f"ElidedFallbackNameID {nameID} points "
659
+ "to a nameID that does not exist in the "
660
+ '"name" table',
661
+ None,
662
+ )
663
+ elif "ElidedFallbackName" in self.stat_:
664
+ nameID = self.stat_["ElidedFallbackName"]
665
+
666
+ otl.buildStatTable(
667
+ self.font,
668
+ designAxes,
669
+ locations=format4_locations,
670
+ elidedFallbackName=nameID,
671
+ )
672
+
673
+ def build_codepages_(self, pages):
674
+ pages2bits = {
675
+ 1252: 0,
676
+ 1250: 1,
677
+ 1251: 2,
678
+ 1253: 3,
679
+ 1254: 4,
680
+ 1255: 5,
681
+ 1256: 6,
682
+ 1257: 7,
683
+ 1258: 8,
684
+ 874: 16,
685
+ 932: 17,
686
+ 936: 18,
687
+ 949: 19,
688
+ 950: 20,
689
+ 1361: 21,
690
+ 869: 48,
691
+ 866: 49,
692
+ 865: 50,
693
+ 864: 51,
694
+ 863: 52,
695
+ 862: 53,
696
+ 861: 54,
697
+ 860: 55,
698
+ 857: 56,
699
+ 855: 57,
700
+ 852: 58,
701
+ 775: 59,
702
+ 737: 60,
703
+ 708: 61,
704
+ 850: 62,
705
+ 437: 63,
706
+ }
707
+ bits = [pages2bits[p] for p in pages if p in pages2bits]
708
+ pages = []
709
+ for i in range(2):
710
+ pages.append("")
711
+ for j in range(i * 32, (i + 1) * 32):
712
+ if j in bits:
713
+ pages[i] += "1"
714
+ else:
715
+ pages[i] += "0"
716
+ return [binary2num(p[::-1]) for p in pages]
717
+
718
+ def buildBASE(self):
719
+ if not self.base_horiz_axis_ and not self.base_vert_axis_:
720
+ return None
721
+ base = otTables.BASE()
722
+ base.Version = 0x00010000
723
+ base.HorizAxis = self.buildBASEAxis(self.base_horiz_axis_)
724
+ base.VertAxis = self.buildBASEAxis(self.base_vert_axis_)
725
+
726
+ result = newTable("BASE")
727
+ result.table = base
728
+ return result
729
+
730
+ def buildBASEAxis(self, axis):
731
+ if not axis:
732
+ return
733
+ bases, scripts = axis
734
+ axis = otTables.Axis()
735
+ axis.BaseTagList = otTables.BaseTagList()
736
+ axis.BaseTagList.BaselineTag = bases
737
+ axis.BaseTagList.BaseTagCount = len(bases)
738
+ axis.BaseScriptList = otTables.BaseScriptList()
739
+ axis.BaseScriptList.BaseScriptRecord = []
740
+ axis.BaseScriptList.BaseScriptCount = len(scripts)
741
+ for script in sorted(scripts):
742
+ record = otTables.BaseScriptRecord()
743
+ record.BaseScriptTag = script[0]
744
+ record.BaseScript = otTables.BaseScript()
745
+ record.BaseScript.BaseLangSysCount = 0
746
+ record.BaseScript.BaseValues = otTables.BaseValues()
747
+ record.BaseScript.BaseValues.DefaultIndex = bases.index(script[1])
748
+ record.BaseScript.BaseValues.BaseCoord = []
749
+ record.BaseScript.BaseValues.BaseCoordCount = len(script[2])
750
+ for c in script[2]:
751
+ coord = otTables.BaseCoord()
752
+ coord.Format = 1
753
+ coord.Coordinate = c
754
+ record.BaseScript.BaseValues.BaseCoord.append(coord)
755
+ axis.BaseScriptList.BaseScriptRecord.append(record)
756
+ return axis
757
+
758
+ def buildGDEF(self):
759
+ gdef = otTables.GDEF()
760
+ gdef.GlyphClassDef = self.buildGDEFGlyphClassDef_()
761
+ gdef.AttachList = otl.buildAttachList(self.attachPoints_, self.glyphMap)
762
+ gdef.LigCaretList = otl.buildLigCaretList(
763
+ self.ligCaretCoords_, self.ligCaretPoints_, self.glyphMap
764
+ )
765
+ gdef.MarkAttachClassDef = self.buildGDEFMarkAttachClassDef_()
766
+ gdef.MarkGlyphSetsDef = self.buildGDEFMarkGlyphSetsDef_()
767
+ gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef else 0x00010000
768
+ if self.varstorebuilder:
769
+ store = self.varstorebuilder.finish()
770
+ if store:
771
+ gdef.Version = 0x00010003
772
+ gdef.VarStore = store
773
+ varidx_map = store.optimize()
774
+
775
+ gdef.remap_device_varidxes(varidx_map)
776
+ if "GPOS" in self.font:
777
+ self.font["GPOS"].table.remap_device_varidxes(varidx_map)
778
+ self.model_cache.clear()
779
+ if any(
780
+ (
781
+ gdef.GlyphClassDef,
782
+ gdef.AttachList,
783
+ gdef.LigCaretList,
784
+ gdef.MarkAttachClassDef,
785
+ gdef.MarkGlyphSetsDef,
786
+ )
787
+ ) or hasattr(gdef, "VarStore"):
788
+ result = newTable("GDEF")
789
+ result.table = gdef
790
+ return result
791
+ else:
792
+ return None
793
+
794
+ def buildGDEFGlyphClassDef_(self):
795
+ if self.glyphClassDefs_:
796
+ classes = {g: c for (g, (c, _)) in self.glyphClassDefs_.items()}
797
+ else:
798
+ classes = {}
799
+ for lookup in self.lookups_:
800
+ classes.update(lookup.inferGlyphClasses())
801
+ for markClass in self.parseTree.markClasses.values():
802
+ for markClassDef in markClass.definitions:
803
+ for glyph in markClassDef.glyphSet():
804
+ classes[glyph] = 3
805
+ if classes:
806
+ result = otTables.GlyphClassDef()
807
+ result.classDefs = classes
808
+ return result
809
+ else:
810
+ return None
811
+
812
+ def buildGDEFMarkAttachClassDef_(self):
813
+ classDefs = {g: c for g, (c, _) in self.markAttach_.items()}
814
+ if not classDefs:
815
+ return None
816
+ result = otTables.MarkAttachClassDef()
817
+ result.classDefs = classDefs
818
+ return result
819
+
820
+ def buildGDEFMarkGlyphSetsDef_(self):
821
+ sets = []
822
+ for glyphs, id_ in sorted(
823
+ self.markFilterSets_.items(), key=lambda item: item[1]
824
+ ):
825
+ sets.append(glyphs)
826
+ return otl.buildMarkGlyphSetsDef(sets, self.glyphMap)
827
+
828
+ def buildDebg(self):
829
+ if "Debg" not in self.font:
830
+ self.font["Debg"] = newTable("Debg")
831
+ self.font["Debg"].data = {}
832
+ self.font["Debg"].data[LOOKUP_DEBUG_INFO_KEY] = self.lookup_locations
833
+
834
+ def buildLookups_(self, tag):
835
+ assert tag in ("GPOS", "GSUB"), tag
836
+ for lookup in self.lookups_:
837
+ lookup.lookup_index = None
838
+ lookups = []
839
+ for lookup in self.lookups_:
840
+ if lookup.table != tag:
841
+ continue
842
+ lookup.lookup_index = len(lookups)
843
+ self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo(
844
+ location=str(lookup.location),
845
+ name=self.get_lookup_name_(lookup),
846
+ feature=None,
847
+ )
848
+ lookups.append(lookup)
849
+ otLookups = []
850
+ for l in lookups:
851
+ try:
852
+ otLookups.append(l.build())
853
+ except OpenTypeLibError as e:
854
+ raise FeatureLibError(str(e), e.location) from e
855
+ except Exception as e:
856
+ location = self.lookup_locations[tag][str(l.lookup_index)].location
857
+ raise FeatureLibError(str(e), location) from e
858
+ return otLookups
859
+
860
+ def makeTable(self, tag):
861
+ table = getattr(otTables, tag, None)()
862
+ table.Version = 0x00010000
863
+ table.ScriptList = otTables.ScriptList()
864
+ table.ScriptList.ScriptRecord = []
865
+ table.FeatureList = otTables.FeatureList()
866
+ table.FeatureList.FeatureRecord = []
867
+ table.LookupList = otTables.LookupList()
868
+ table.LookupList.Lookup = self.buildLookups_(tag)
869
+
870
+ # Build a table for mapping (tag, lookup_indices) to feature_index.
871
+ # For example, ('liga', (2,3,7)) --> 23.
872
+ feature_indices = {}
873
+ required_feature_indices = {} # ('latn', 'DEU') --> 23
874
+ scripts = {} # 'latn' --> {'DEU': [23, 24]} for feature #23,24
875
+ # Sort the feature table by feature tag:
876
+ # https://github.com/fonttools/fonttools/issues/568
877
+ sortFeatureTag = lambda f: (f[0][2], f[0][1], f[0][0], f[1])
878
+ for key, lookups in sorted(self.features_.items(), key=sortFeatureTag):
879
+ script, lang, feature_tag = key
880
+ # l.lookup_index will be None when a lookup is not needed
881
+ # for the table under construction. For example, substitution
882
+ # rules will have no lookup_index while building GPOS tables.
883
+ # We also deduplicate lookup indices, as they only get applied once
884
+ # within a given feature:
885
+ # https://github.com/fonttools/fonttools/issues/2946
886
+ lookup_indices = tuple(
887
+ dict.fromkeys(
888
+ l.lookup_index for l in lookups if l.lookup_index is not None
889
+ )
890
+ )
891
+
892
+ size_feature = tag == "GPOS" and feature_tag == "size"
893
+ force_feature = self.any_feature_variations(feature_tag, tag)
894
+ if len(lookup_indices) == 0 and not size_feature and not force_feature:
895
+ continue
896
+
897
+ for ix in lookup_indices:
898
+ try:
899
+ self.lookup_locations[tag][str(ix)] = self.lookup_locations[tag][
900
+ str(ix)
901
+ ]._replace(feature=key)
902
+ except KeyError:
903
+ warnings.warn(
904
+ "feaLib.Builder subclass needs upgrading to "
905
+ "stash debug information. See fonttools#2065."
906
+ )
907
+
908
+ feature_key = (feature_tag, lookup_indices)
909
+ feature_index = feature_indices.get(feature_key)
910
+ if feature_index is None:
911
+ feature_index = len(table.FeatureList.FeatureRecord)
912
+ frec = otTables.FeatureRecord()
913
+ frec.FeatureTag = feature_tag
914
+ frec.Feature = otTables.Feature()
915
+ frec.Feature.FeatureParams = self.buildFeatureParams(feature_tag)
916
+ frec.Feature.LookupListIndex = list(lookup_indices)
917
+ frec.Feature.LookupCount = len(lookup_indices)
918
+ table.FeatureList.FeatureRecord.append(frec)
919
+ feature_indices[feature_key] = feature_index
920
+ scripts.setdefault(script, {}).setdefault(lang, []).append(feature_index)
921
+ if self.required_features_.get((script, lang)) == feature_tag:
922
+ required_feature_indices[(script, lang)] = feature_index
923
+
924
+ # Build ScriptList.
925
+ for script, lang_features in sorted(scripts.items()):
926
+ srec = otTables.ScriptRecord()
927
+ srec.ScriptTag = script
928
+ srec.Script = otTables.Script()
929
+ srec.Script.DefaultLangSys = None
930
+ srec.Script.LangSysRecord = []
931
+ for lang, feature_indices in sorted(lang_features.items()):
932
+ langrec = otTables.LangSysRecord()
933
+ langrec.LangSys = otTables.LangSys()
934
+ langrec.LangSys.LookupOrder = None
935
+
936
+ req_feature_index = required_feature_indices.get((script, lang))
937
+ if req_feature_index is None:
938
+ langrec.LangSys.ReqFeatureIndex = 0xFFFF
939
+ else:
940
+ langrec.LangSys.ReqFeatureIndex = req_feature_index
941
+
942
+ langrec.LangSys.FeatureIndex = [
943
+ i for i in feature_indices if i != req_feature_index
944
+ ]
945
+ langrec.LangSys.FeatureCount = len(langrec.LangSys.FeatureIndex)
946
+
947
+ if lang == "dflt":
948
+ srec.Script.DefaultLangSys = langrec.LangSys
949
+ else:
950
+ langrec.LangSysTag = lang
951
+ srec.Script.LangSysRecord.append(langrec)
952
+ srec.Script.LangSysCount = len(srec.Script.LangSysRecord)
953
+ table.ScriptList.ScriptRecord.append(srec)
954
+
955
+ table.ScriptList.ScriptCount = len(table.ScriptList.ScriptRecord)
956
+ table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord)
957
+ table.LookupList.LookupCount = len(table.LookupList.Lookup)
958
+ return table
959
+
960
+ def makeFeatureVariations(self, table, table_tag):
961
+ feature_vars = {}
962
+ has_any_variations = False
963
+ # Sort out which lookups to build, gather their indices
964
+ for (_, _, feature_tag), variations in self.feature_variations_.items():
965
+ feature_vars[feature_tag] = []
966
+ for conditionset, builders in variations.items():
967
+ raw_conditionset = self.conditionsets_[conditionset]
968
+ indices = []
969
+ for b in builders:
970
+ if b.table != table_tag:
971
+ continue
972
+ assert b.lookup_index is not None
973
+ indices.append(b.lookup_index)
974
+ has_any_variations = True
975
+ feature_vars[feature_tag].append((raw_conditionset, indices))
976
+
977
+ if has_any_variations:
978
+ for feature_tag, conditions_and_lookups in feature_vars.items():
979
+ addFeatureVariationsRaw(
980
+ self.font, table, conditions_and_lookups, feature_tag
981
+ )
982
+
983
+ def any_feature_variations(self, feature_tag, table_tag):
984
+ for (_, _, feature), variations in self.feature_variations_.items():
985
+ if feature != feature_tag:
986
+ continue
987
+ for conditionset, builders in variations.items():
988
+ if any(b.table == table_tag for b in builders):
989
+ return True
990
+ return False
991
+
992
+ def get_lookup_name_(self, lookup):
993
+ rev = {v: k for k, v in self.named_lookups_.items()}
994
+ if lookup in rev:
995
+ return rev[lookup]
996
+ return None
997
+
998
+ def add_language_system(self, location, script, language):
999
+ # OpenType Feature File Specification, section 4.b.i
1000
+ if script == "DFLT" and language == "dflt" and self.default_language_systems_:
1001
+ raise FeatureLibError(
1002
+ 'If "languagesystem DFLT dflt" is present, it must be '
1003
+ "the first of the languagesystem statements",
1004
+ location,
1005
+ )
1006
+ if script == "DFLT":
1007
+ if self.seen_non_DFLT_script_:
1008
+ raise FeatureLibError(
1009
+ 'languagesystems using the "DFLT" script tag must '
1010
+ "precede all other languagesystems",
1011
+ location,
1012
+ )
1013
+ else:
1014
+ self.seen_non_DFLT_script_ = True
1015
+ if (script, language) in self.default_language_systems_:
1016
+ raise FeatureLibError(
1017
+ '"languagesystem %s %s" has already been specified'
1018
+ % (script.strip(), language.strip()),
1019
+ location,
1020
+ )
1021
+ self.default_language_systems_.add((script, language))
1022
+
1023
+ def get_default_language_systems_(self):
1024
+ # OpenType Feature File specification, 4.b.i. languagesystem:
1025
+ # If no "languagesystem" statement is present, then the
1026
+ # implementation must behave exactly as though the following
1027
+ # statement were present at the beginning of the feature file:
1028
+ # languagesystem DFLT dflt;
1029
+ if self.default_language_systems_:
1030
+ return frozenset(self.default_language_systems_)
1031
+ else:
1032
+ return frozenset({("DFLT", "dflt")})
1033
+
1034
+ def start_feature(self, location, name):
1035
+ self.language_systems = self.get_default_language_systems_()
1036
+ self.script_ = "DFLT"
1037
+ self.cur_lookup_ = None
1038
+ self.cur_feature_name_ = name
1039
+ self.lookupflag_ = 0
1040
+ self.lookupflag_markFilterSet_ = None
1041
+ if name == "aalt":
1042
+ self.aalt_location_ = location
1043
+
1044
+ def end_feature(self):
1045
+ assert self.cur_feature_name_ is not None
1046
+ self.cur_feature_name_ = None
1047
+ self.language_systems = None
1048
+ self.cur_lookup_ = None
1049
+ self.lookupflag_ = 0
1050
+ self.lookupflag_markFilterSet_ = None
1051
+
1052
+ def start_lookup_block(self, location, name):
1053
+ if name in self.named_lookups_:
1054
+ raise FeatureLibError(
1055
+ 'Lookup "%s" has already been defined' % name, location
1056
+ )
1057
+ if self.cur_feature_name_ == "aalt":
1058
+ raise FeatureLibError(
1059
+ "Lookup blocks cannot be placed inside 'aalt' features; "
1060
+ "move it out, and then refer to it with a lookup statement",
1061
+ location,
1062
+ )
1063
+ self.cur_lookup_name_ = name
1064
+ self.named_lookups_[name] = None
1065
+ self.cur_lookup_ = None
1066
+ if self.cur_feature_name_ is None:
1067
+ self.lookupflag_ = 0
1068
+ self.lookupflag_markFilterSet_ = None
1069
+
1070
+ def end_lookup_block(self):
1071
+ assert self.cur_lookup_name_ is not None
1072
+ self.cur_lookup_name_ = None
1073
+ self.cur_lookup_ = None
1074
+ if self.cur_feature_name_ is None:
1075
+ self.lookupflag_ = 0
1076
+ self.lookupflag_markFilterSet_ = None
1077
+
1078
+ def add_lookup_call(self, lookup_name):
1079
+ assert lookup_name in self.named_lookups_, lookup_name
1080
+ self.cur_lookup_ = None
1081
+ lookup = self.named_lookups_[lookup_name]
1082
+ if lookup is not None: # skip empty named lookup
1083
+ self.add_lookup_to_feature_(lookup, self.cur_feature_name_)
1084
+
1085
+ def set_font_revision(self, location, revision):
1086
+ self.fontRevision_ = revision
1087
+
1088
+ def set_language(self, location, language, include_default, required):
1089
+ assert len(language) == 4
1090
+ if self.cur_feature_name_ in ("aalt", "size"):
1091
+ raise FeatureLibError(
1092
+ "Language statements are not allowed "
1093
+ 'within "feature %s"' % self.cur_feature_name_,
1094
+ location,
1095
+ )
1096
+ if self.cur_feature_name_ is None:
1097
+ raise FeatureLibError(
1098
+ "Language statements are not allowed "
1099
+ "within standalone lookup blocks",
1100
+ location,
1101
+ )
1102
+ self.cur_lookup_ = None
1103
+
1104
+ key = (self.script_, language, self.cur_feature_name_)
1105
+ lookups = self.features_.get((key[0], "dflt", key[2]))
1106
+ if (language == "dflt" or include_default) and lookups:
1107
+ self.features_[key] = lookups[:]
1108
+ else:
1109
+ # if we aren't including default we need to manually remove the
1110
+ # default lookups, which were added to all declared langsystems
1111
+ # as they were encountered (we don't remove all lookups because
1112
+ # we want to allow duplicate script/lang statements;
1113
+ # see https://github.com/fonttools/fonttools/issues/3748
1114
+ cur_lookups = self.features_.get(key, [])
1115
+ self.features_[key] = [x for x in cur_lookups if x not in lookups]
1116
+ self.language_systems = frozenset([(self.script_, language)])
1117
+
1118
+ if required:
1119
+ key = (self.script_, language)
1120
+ if key in self.required_features_:
1121
+ raise FeatureLibError(
1122
+ "Language %s (script %s) has already "
1123
+ "specified feature %s as its required feature"
1124
+ % (
1125
+ language.strip(),
1126
+ self.script_.strip(),
1127
+ self.required_features_[key].strip(),
1128
+ ),
1129
+ location,
1130
+ )
1131
+ self.required_features_[key] = self.cur_feature_name_
1132
+
1133
+ def getMarkAttachClass_(self, location, glyphs):
1134
+ glyphs = frozenset(glyphs)
1135
+ id_ = self.markAttachClassID_.get(glyphs)
1136
+ if id_ is not None:
1137
+ return id_
1138
+ id_ = len(self.markAttachClassID_) + 1
1139
+ self.markAttachClassID_[glyphs] = id_
1140
+ for glyph in glyphs:
1141
+ if glyph in self.markAttach_:
1142
+ _, loc = self.markAttach_[glyph]
1143
+ raise FeatureLibError(
1144
+ "Glyph %s already has been assigned "
1145
+ "a MarkAttachmentType at %s" % (glyph, loc),
1146
+ location,
1147
+ )
1148
+ self.markAttach_[glyph] = (id_, location)
1149
+ return id_
1150
+
1151
+ def getMarkFilterSet_(self, location, glyphs):
1152
+ glyphs = frozenset(glyphs)
1153
+ id_ = self.markFilterSets_.get(glyphs)
1154
+ if id_ is not None:
1155
+ return id_
1156
+ id_ = len(self.markFilterSets_)
1157
+ self.markFilterSets_[glyphs] = id_
1158
+ return id_
1159
+
1160
+ def set_lookup_flag(self, location, value, markAttach, markFilter):
1161
+ value = value & 0xFF
1162
+ if markAttach:
1163
+ markAttachClass = self.getMarkAttachClass_(location, markAttach)
1164
+ value = value | (markAttachClass << 8)
1165
+ if markFilter:
1166
+ markFilterSet = self.getMarkFilterSet_(location, markFilter)
1167
+ value = value | 0x10
1168
+ self.lookupflag_markFilterSet_ = markFilterSet
1169
+ else:
1170
+ self.lookupflag_markFilterSet_ = None
1171
+ self.lookupflag_ = value
1172
+
1173
+ def set_script(self, location, script):
1174
+ if self.cur_feature_name_ in ("aalt", "size"):
1175
+ raise FeatureLibError(
1176
+ "Script statements are not allowed "
1177
+ 'within "feature %s"' % self.cur_feature_name_,
1178
+ location,
1179
+ )
1180
+ if self.cur_feature_name_ is None:
1181
+ raise FeatureLibError(
1182
+ "Script statements are not allowed " "within standalone lookup blocks",
1183
+ location,
1184
+ )
1185
+ if self.language_systems == {(script, "dflt")}:
1186
+ # Nothing to do.
1187
+ return
1188
+ self.cur_lookup_ = None
1189
+ self.script_ = script
1190
+ self.lookupflag_ = 0
1191
+ self.lookupflag_markFilterSet_ = None
1192
+ self.set_language(location, "dflt", include_default=True, required=False)
1193
+
1194
+ def find_lookup_builders_(self, lookups):
1195
+ """Helper for building chain contextual substitutions
1196
+
1197
+ Given a list of lookup names, finds the LookupBuilder for each name.
1198
+ If an input name is None, it gets mapped to a None LookupBuilder.
1199
+ """
1200
+ lookup_builders = []
1201
+ for lookuplist in lookups:
1202
+ if lookuplist is not None:
1203
+ lookup_builders.append(
1204
+ [self.named_lookups_.get(l.name) for l in lookuplist]
1205
+ )
1206
+ else:
1207
+ lookup_builders.append(None)
1208
+ return lookup_builders
1209
+
1210
+ def add_attach_points(self, location, glyphs, contourPoints):
1211
+ for glyph in glyphs:
1212
+ self.attachPoints_.setdefault(glyph, set()).update(contourPoints)
1213
+
1214
+ def add_feature_reference(self, location, featureName):
1215
+ if self.cur_feature_name_ != "aalt":
1216
+ raise FeatureLibError(
1217
+ 'Feature references are only allowed inside "feature aalt"', location
1218
+ )
1219
+ self.aalt_features_.append((location, featureName))
1220
+
1221
+ def add_featureName(self, tag):
1222
+ self.featureNames_.add(tag)
1223
+
1224
+ def add_cv_parameter(self, tag):
1225
+ self.cv_parameters_.add(tag)
1226
+
1227
+ def add_to_cv_num_named_params(self, tag):
1228
+ """Adds new items to ``self.cv_num_named_params_``
1229
+ or increments the count of existing items."""
1230
+ if tag in self.cv_num_named_params_:
1231
+ self.cv_num_named_params_[tag] += 1
1232
+ else:
1233
+ self.cv_num_named_params_[tag] = 1
1234
+
1235
+ def add_cv_character(self, character, tag):
1236
+ self.cv_characters_[tag].append(character)
1237
+
1238
+ def set_base_axis(self, bases, scripts, vertical):
1239
+ if vertical:
1240
+ self.base_vert_axis_ = (bases, scripts)
1241
+ else:
1242
+ self.base_horiz_axis_ = (bases, scripts)
1243
+
1244
+ def set_size_parameters(
1245
+ self, location, DesignSize, SubfamilyID, RangeStart, RangeEnd
1246
+ ):
1247
+ if self.cur_feature_name_ != "size":
1248
+ raise FeatureLibError(
1249
+ "Parameters statements are not allowed "
1250
+ 'within "feature %s"' % self.cur_feature_name_,
1251
+ location,
1252
+ )
1253
+ self.size_parameters_ = [DesignSize, SubfamilyID, RangeStart, RangeEnd]
1254
+ for script, lang in self.language_systems:
1255
+ key = (script, lang, self.cur_feature_name_)
1256
+ self.features_.setdefault(key, [])
1257
+
1258
+ # GSUB rules
1259
+
1260
+ # GSUB 1
1261
+ def add_single_subst(self, location, prefix, suffix, mapping, forceChain):
1262
+ if self.cur_feature_name_ == "aalt":
1263
+ for from_glyph, to_glyph in mapping.items():
1264
+ alts = self.aalt_alternates_.setdefault(from_glyph, [])
1265
+ if to_glyph not in alts:
1266
+ alts.append(to_glyph)
1267
+ return
1268
+ if prefix or suffix or forceChain:
1269
+ self.add_single_subst_chained_(location, prefix, suffix, mapping)
1270
+ return
1271
+ lookup = self.get_lookup_(location, SingleSubstBuilder)
1272
+ for from_glyph, to_glyph in mapping.items():
1273
+ if from_glyph in lookup.mapping:
1274
+ if to_glyph == lookup.mapping[from_glyph]:
1275
+ log.info(
1276
+ "Removing duplicate single substitution from glyph"
1277
+ ' "%s" to "%s" at %s',
1278
+ from_glyph,
1279
+ to_glyph,
1280
+ location,
1281
+ )
1282
+ else:
1283
+ raise FeatureLibError(
1284
+ 'Already defined rule for replacing glyph "%s" by "%s"'
1285
+ % (from_glyph, lookup.mapping[from_glyph]),
1286
+ location,
1287
+ )
1288
+ lookup.mapping[from_glyph] = to_glyph
1289
+
1290
+ # GSUB 2
1291
+ def add_multiple_subst(
1292
+ self, location, prefix, glyph, suffix, replacements, forceChain=False
1293
+ ):
1294
+ if prefix or suffix or forceChain:
1295
+ self.add_multi_subst_chained_(location, prefix, glyph, suffix, replacements)
1296
+ return
1297
+ lookup = self.get_lookup_(location, MultipleSubstBuilder)
1298
+ if glyph in lookup.mapping:
1299
+ if replacements == lookup.mapping[glyph]:
1300
+ log.info(
1301
+ "Removing duplicate multiple substitution from glyph"
1302
+ ' "%s" to %s%s',
1303
+ glyph,
1304
+ replacements,
1305
+ f" at {location}" if location else "",
1306
+ )
1307
+ else:
1308
+ raise FeatureLibError(
1309
+ 'Already defined substitution for glyph "%s"' % glyph, location
1310
+ )
1311
+ lookup.mapping[glyph] = replacements
1312
+
1313
+ # GSUB 3
1314
+ def add_alternate_subst(self, location, prefix, glyph, suffix, replacement):
1315
+ if self.cur_feature_name_ == "aalt":
1316
+ alts = self.aalt_alternates_.setdefault(glyph, [])
1317
+ alts.extend(g for g in replacement if g not in alts)
1318
+ return
1319
+ if prefix or suffix:
1320
+ chain = self.get_lookup_(location, ChainContextSubstBuilder)
1321
+ lookup = self.get_chained_lookup_(location, AlternateSubstBuilder)
1322
+ chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [lookup]))
1323
+ else:
1324
+ lookup = self.get_lookup_(location, AlternateSubstBuilder)
1325
+ if glyph in lookup.alternates:
1326
+ raise FeatureLibError(
1327
+ 'Already defined alternates for glyph "%s"' % glyph, location
1328
+ )
1329
+ # We allow empty replacement glyphs here.
1330
+ lookup.alternates[glyph] = replacement
1331
+
1332
+ # GSUB 4
1333
+ def add_ligature_subst(
1334
+ self, location, prefix, glyphs, suffix, replacement, forceChain
1335
+ ):
1336
+ if prefix or suffix or forceChain:
1337
+ self.add_ligature_subst_chained_(
1338
+ location, prefix, glyphs, suffix, replacement
1339
+ )
1340
+ return
1341
+ else:
1342
+ lookup = self.get_lookup_(location, LigatureSubstBuilder)
1343
+
1344
+ if not all(glyphs):
1345
+ raise FeatureLibError("Empty glyph class in substitution", location)
1346
+
1347
+ # OpenType feature file syntax, section 5.d, "Ligature substitution":
1348
+ # "Since the OpenType specification does not allow ligature
1349
+ # substitutions to be specified on target sequences that contain
1350
+ # glyph classes, the implementation software will enumerate
1351
+ # all specific glyph sequences if glyph classes are detected"
1352
+ for g in itertools.product(*glyphs):
1353
+ lookup.ligatures[g] = replacement
1354
+
1355
+ # GSUB 5/6
1356
+ def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups):
1357
+ if not all(glyphs) or not all(prefix) or not all(suffix):
1358
+ raise FeatureLibError(
1359
+ "Empty glyph class in contextual substitution", location
1360
+ )
1361
+ lookup = self.get_lookup_(location, ChainContextSubstBuilder)
1362
+ lookup.rules.append(
1363
+ ChainContextualRule(
1364
+ prefix, glyphs, suffix, self.find_lookup_builders_(lookups)
1365
+ )
1366
+ )
1367
+
1368
+ def add_single_subst_chained_(self, location, prefix, suffix, mapping):
1369
+ if not mapping or not all(prefix) or not all(suffix):
1370
+ raise FeatureLibError(
1371
+ "Empty glyph class in contextual substitution", location
1372
+ )
1373
+ # https://github.com/fonttools/fonttools/issues/512
1374
+ # https://github.com/fonttools/fonttools/issues/2150
1375
+ chain = self.get_lookup_(location, ChainContextSubstBuilder)
1376
+ sub = chain.find_chainable_subst(mapping, SingleSubstBuilder)
1377
+ if sub is None:
1378
+ sub = self.get_chained_lookup_(location, SingleSubstBuilder)
1379
+ sub.mapping.update(mapping)
1380
+ chain.rules.append(
1381
+ ChainContextualRule(prefix, [list(mapping.keys())], suffix, [sub])
1382
+ )
1383
+
1384
+ def add_multi_subst_chained_(self, location, prefix, glyph, suffix, replacements):
1385
+ if not all(prefix) or not all(suffix):
1386
+ raise FeatureLibError(
1387
+ "Empty glyph class in contextual substitution", location
1388
+ )
1389
+ # https://github.com/fonttools/fonttools/issues/3551
1390
+ chain = self.get_lookup_(location, ChainContextSubstBuilder)
1391
+ sub = chain.find_chainable_subst({glyph: replacements}, MultipleSubstBuilder)
1392
+ if sub is None:
1393
+ sub = self.get_chained_lookup_(location, MultipleSubstBuilder)
1394
+ sub.mapping[glyph] = replacements
1395
+ chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [sub]))
1396
+
1397
+ def add_ligature_subst_chained_(
1398
+ self, location, prefix, glyphs, suffix, replacement
1399
+ ):
1400
+ # https://github.com/fonttools/fonttools/issues/3701
1401
+ if not all(prefix) or not all(suffix):
1402
+ raise FeatureLibError(
1403
+ "Empty glyph class in contextual substitution", location
1404
+ )
1405
+ chain = self.get_lookup_(location, ChainContextSubstBuilder)
1406
+ sub = chain.find_chainable_ligature_subst(glyphs, replacement)
1407
+ if sub is None:
1408
+ sub = self.get_chained_lookup_(location, LigatureSubstBuilder)
1409
+
1410
+ for g in itertools.product(*glyphs):
1411
+ sub.ligatures[g] = replacement
1412
+
1413
+ chain.rules.append(ChainContextualRule(prefix, glyphs, suffix, [sub]))
1414
+
1415
+ # GSUB 8
1416
+ def add_reverse_chain_single_subst(self, location, old_prefix, old_suffix, mapping):
1417
+ if not mapping:
1418
+ raise FeatureLibError("Empty glyph class in substitution", location)
1419
+ lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder)
1420
+ lookup.rules.append((old_prefix, old_suffix, mapping))
1421
+
1422
+ # GPOS rules
1423
+
1424
+ # GPOS 1
1425
+ def add_single_pos(self, location, prefix, suffix, pos, forceChain):
1426
+ if prefix or suffix or forceChain:
1427
+ self.add_single_pos_chained_(location, prefix, suffix, pos)
1428
+ else:
1429
+ lookup = self.get_lookup_(location, SinglePosBuilder)
1430
+ for glyphs, value in pos:
1431
+ if not glyphs:
1432
+ raise FeatureLibError(
1433
+ "Empty glyph class in positioning rule", location
1434
+ )
1435
+ otValueRecord = self.makeOpenTypeValueRecord(
1436
+ location, value, pairPosContext=False
1437
+ )
1438
+ for glyph in glyphs:
1439
+ try:
1440
+ lookup.add_pos(location, glyph, otValueRecord)
1441
+ except OpenTypeLibError as e:
1442
+ raise FeatureLibError(str(e), e.location) from e
1443
+
1444
+ # GPOS 2
1445
+ def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2):
1446
+ if not glyphclass1 or not glyphclass2:
1447
+ raise FeatureLibError("Empty glyph class in positioning rule", location)
1448
+ lookup = self.get_lookup_(location, PairPosBuilder)
1449
+ v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
1450
+ v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
1451
+ lookup.addClassPair(location, glyphclass1, v1, glyphclass2, v2)
1452
+
1453
+ def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
1454
+ if not glyph1 or not glyph2:
1455
+ raise FeatureLibError("Empty glyph class in positioning rule", location)
1456
+ lookup = self.get_lookup_(location, PairPosBuilder)
1457
+ v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
1458
+ v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
1459
+ lookup.addGlyphPair(location, glyph1, v1, glyph2, v2)
1460
+
1461
+ # GPOS 3
1462
+ def add_cursive_pos(self, location, glyphclass, entryAnchor, exitAnchor):
1463
+ if not glyphclass:
1464
+ raise FeatureLibError("Empty glyph class in positioning rule", location)
1465
+ lookup = self.get_lookup_(location, CursivePosBuilder)
1466
+ lookup.add_attachment(
1467
+ location,
1468
+ glyphclass,
1469
+ self.makeOpenTypeAnchor(location, entryAnchor),
1470
+ self.makeOpenTypeAnchor(location, exitAnchor),
1471
+ )
1472
+
1473
+ # GPOS 4
1474
+ def add_mark_base_pos(self, location, bases, marks):
1475
+ builder = self.get_lookup_(location, MarkBasePosBuilder)
1476
+ self.add_marks_(location, builder, marks)
1477
+ if not bases:
1478
+ raise FeatureLibError("Empty glyph class in positioning rule", location)
1479
+ for baseAnchor, markClass in marks:
1480
+ otBaseAnchor = self.makeOpenTypeAnchor(location, baseAnchor)
1481
+ for base in bases:
1482
+ builder.bases.setdefault(base, {})[markClass.name] = otBaseAnchor
1483
+
1484
+ # GPOS 5
1485
+ def add_mark_lig_pos(self, location, ligatures, components):
1486
+ builder = self.get_lookup_(location, MarkLigPosBuilder)
1487
+ componentAnchors = []
1488
+ if not ligatures:
1489
+ raise FeatureLibError("Empty glyph class in positioning rule", location)
1490
+ for marks in components:
1491
+ anchors = {}
1492
+ self.add_marks_(location, builder, marks)
1493
+ for ligAnchor, markClass in marks:
1494
+ anchors[markClass.name] = self.makeOpenTypeAnchor(location, ligAnchor)
1495
+ componentAnchors.append(anchors)
1496
+ for glyph in ligatures:
1497
+ builder.ligatures[glyph] = componentAnchors
1498
+
1499
+ # GPOS 6
1500
+ def add_mark_mark_pos(self, location, baseMarks, marks):
1501
+ builder = self.get_lookup_(location, MarkMarkPosBuilder)
1502
+ self.add_marks_(location, builder, marks)
1503
+ if not baseMarks:
1504
+ raise FeatureLibError("Empty glyph class in positioning rule", location)
1505
+ for baseAnchor, markClass in marks:
1506
+ otBaseAnchor = self.makeOpenTypeAnchor(location, baseAnchor)
1507
+ for baseMark in baseMarks:
1508
+ builder.baseMarks.setdefault(baseMark, {})[
1509
+ markClass.name
1510
+ ] = otBaseAnchor
1511
+
1512
+ # GPOS 7/8
1513
+ def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups):
1514
+ if not all(glyphs) or not all(prefix) or not all(suffix):
1515
+ raise FeatureLibError(
1516
+ "Empty glyph class in contextual positioning rule", location
1517
+ )
1518
+ lookup = self.get_lookup_(location, ChainContextPosBuilder)
1519
+ lookup.rules.append(
1520
+ ChainContextualRule(
1521
+ prefix, glyphs, suffix, self.find_lookup_builders_(lookups)
1522
+ )
1523
+ )
1524
+
1525
+ def add_single_pos_chained_(self, location, prefix, suffix, pos):
1526
+ if not pos or not all(prefix) or not all(suffix):
1527
+ raise FeatureLibError(
1528
+ "Empty glyph class in contextual positioning rule", location
1529
+ )
1530
+ # https://github.com/fonttools/fonttools/issues/514
1531
+ chain = self.get_lookup_(location, ChainContextPosBuilder)
1532
+ targets = []
1533
+ for _, _, _, lookups in chain.rules:
1534
+ targets.extend(lookups)
1535
+ subs = []
1536
+ for glyphs, value in pos:
1537
+ if value is None:
1538
+ subs.append(None)
1539
+ continue
1540
+ otValue = self.makeOpenTypeValueRecord(
1541
+ location, value, pairPosContext=False
1542
+ )
1543
+ sub = chain.find_chainable_single_pos(targets, glyphs, otValue)
1544
+ if sub is None:
1545
+ sub = self.get_chained_lookup_(location, SinglePosBuilder)
1546
+ targets.append(sub)
1547
+ for glyph in glyphs:
1548
+ sub.add_pos(location, glyph, otValue)
1549
+ subs.append(sub)
1550
+ assert len(pos) == len(subs), (pos, subs)
1551
+ chain.rules.append(
1552
+ ChainContextualRule(prefix, [g for g, v in pos], suffix, subs)
1553
+ )
1554
+
1555
+ def add_marks_(self, location, lookupBuilder, marks):
1556
+ """Helper for add_mark_{base,liga,mark}_pos."""
1557
+ for _, markClass in marks:
1558
+ for markClassDef in markClass.definitions:
1559
+ for mark in markClassDef.glyphs.glyphSet():
1560
+ if mark not in lookupBuilder.marks:
1561
+ otMarkAnchor = self.makeOpenTypeAnchor(
1562
+ location, copy.deepcopy(markClassDef.anchor)
1563
+ )
1564
+ lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor)
1565
+ else:
1566
+ existingMarkClass = lookupBuilder.marks[mark][0]
1567
+ if markClass.name != existingMarkClass:
1568
+ raise FeatureLibError(
1569
+ "Glyph %s cannot be in both @%s and @%s"
1570
+ % (mark, existingMarkClass, markClass.name),
1571
+ location,
1572
+ )
1573
+
1574
+ def add_subtable_break(self, location):
1575
+ self.cur_lookup_.add_subtable_break(location)
1576
+
1577
+ def setGlyphClass_(self, location, glyph, glyphClass):
1578
+ oldClass, oldLocation = self.glyphClassDefs_.get(glyph, (None, None))
1579
+ if oldClass and oldClass != glyphClass:
1580
+ raise FeatureLibError(
1581
+ "Glyph %s was assigned to a different class at %s"
1582
+ % (glyph, oldLocation),
1583
+ location,
1584
+ )
1585
+ self.glyphClassDefs_[glyph] = (glyphClass, location)
1586
+
1587
+ def add_glyphClassDef(
1588
+ self, location, baseGlyphs, ligatureGlyphs, markGlyphs, componentGlyphs
1589
+ ):
1590
+ for glyph in baseGlyphs:
1591
+ self.setGlyphClass_(location, glyph, 1)
1592
+ for glyph in ligatureGlyphs:
1593
+ self.setGlyphClass_(location, glyph, 2)
1594
+ for glyph in markGlyphs:
1595
+ self.setGlyphClass_(location, glyph, 3)
1596
+ for glyph in componentGlyphs:
1597
+ self.setGlyphClass_(location, glyph, 4)
1598
+
1599
+ def add_ligatureCaretByIndex_(self, location, glyphs, carets):
1600
+ for glyph in glyphs:
1601
+ if glyph not in self.ligCaretPoints_:
1602
+ self.ligCaretPoints_[glyph] = carets
1603
+
1604
+ def makeLigCaret(self, location, caret):
1605
+ if not isinstance(caret, VariableScalar):
1606
+ return caret
1607
+ default, device = self.makeVariablePos(location, caret)
1608
+ if device is not None:
1609
+ return (default, device)
1610
+ return default
1611
+
1612
+ def add_ligatureCaretByPos_(self, location, glyphs, carets):
1613
+ carets = [self.makeLigCaret(location, caret) for caret in carets]
1614
+ for glyph in glyphs:
1615
+ if glyph not in self.ligCaretCoords_:
1616
+ self.ligCaretCoords_[glyph] = carets
1617
+
1618
+ def add_name_record(self, location, nameID, platformID, platEncID, langID, string):
1619
+ self.names_.append([nameID, platformID, platEncID, langID, string])
1620
+
1621
+ def add_os2_field(self, key, value):
1622
+ self.os2_[key] = value
1623
+
1624
+ def add_hhea_field(self, key, value):
1625
+ self.hhea_[key] = value
1626
+
1627
+ def add_vhea_field(self, key, value):
1628
+ self.vhea_[key] = value
1629
+
1630
+ def add_conditionset(self, location, key, value):
1631
+ if "fvar" not in self.font:
1632
+ raise FeatureLibError(
1633
+ "Cannot add feature variations to a font without an 'fvar' table",
1634
+ location,
1635
+ )
1636
+
1637
+ # Normalize
1638
+ axisMap = {
1639
+ axis.axisTag: (axis.minValue, axis.defaultValue, axis.maxValue)
1640
+ for axis in self.axes
1641
+ }
1642
+
1643
+ value = {
1644
+ tag: (
1645
+ normalizeValue(bottom, axisMap[tag]),
1646
+ normalizeValue(top, axisMap[tag]),
1647
+ )
1648
+ for tag, (bottom, top) in value.items()
1649
+ }
1650
+
1651
+ # NOTE: This might result in rounding errors (off-by-ones) compared to
1652
+ # rules in Designspace files, since we're working with what's in the
1653
+ # `avar` table rather than the original values.
1654
+ if "avar" in self.font:
1655
+ mapping = self.font["avar"].segments
1656
+ value = {
1657
+ axis: tuple(
1658
+ piecewiseLinearMap(v, mapping[axis]) if axis in mapping else v
1659
+ for v in condition_range
1660
+ )
1661
+ for axis, condition_range in value.items()
1662
+ }
1663
+
1664
+ self.conditionsets_[key] = value
1665
+
1666
+ def makeVariablePos(self, location, varscalar):
1667
+ if not self.varstorebuilder:
1668
+ raise FeatureLibError(
1669
+ "Can't define a variable scalar in a non-variable font", location
1670
+ )
1671
+
1672
+ varscalar.axes = self.axes
1673
+ if not varscalar.does_vary:
1674
+ return varscalar.default, None
1675
+
1676
+ default, index = varscalar.add_to_variation_store(
1677
+ self.varstorebuilder, self.model_cache, self.font.get("avar")
1678
+ )
1679
+
1680
+ device = None
1681
+ if index is not None and index != 0xFFFFFFFF:
1682
+ device = buildVarDevTable(index)
1683
+
1684
+ return default, device
1685
+
1686
+ def makeAnchorPos(self, varscalar, deviceTable, location):
1687
+ device = None
1688
+ if not isinstance(varscalar, VariableScalar):
1689
+ if deviceTable is not None:
1690
+ device = otl.buildDevice(dict(deviceTable))
1691
+ return varscalar, device
1692
+ default, device = self.makeVariablePos(location, varscalar)
1693
+ if device is not None and deviceTable is not None:
1694
+ raise FeatureLibError(
1695
+ "Can't define a device coordinate and variable scalar", location
1696
+ )
1697
+ return default, device
1698
+
1699
+ def makeOpenTypeAnchor(self, location, anchor):
1700
+ """ast.Anchor --> otTables.Anchor"""
1701
+ if anchor is None:
1702
+ return None
1703
+ deviceX, deviceY = None, None
1704
+ if anchor.xDeviceTable is not None:
1705
+ deviceX = otl.buildDevice(dict(anchor.xDeviceTable))
1706
+ if anchor.yDeviceTable is not None:
1707
+ deviceY = otl.buildDevice(dict(anchor.yDeviceTable))
1708
+ x, deviceX = self.makeAnchorPos(anchor.x, anchor.xDeviceTable, location)
1709
+ y, deviceY = self.makeAnchorPos(anchor.y, anchor.yDeviceTable, location)
1710
+ otlanchor = otl.buildAnchor(x, y, anchor.contourpoint, deviceX, deviceY)
1711
+ return otlanchor
1712
+
1713
+ _VALUEREC_ATTRS = {
1714
+ name[0].lower() + name[1:]: (name, isDevice)
1715
+ for _, name, isDevice, _ in otBase.valueRecordFormat
1716
+ if not name.startswith("Reserved")
1717
+ }
1718
+
1719
+ def makeOpenTypeValueRecord(self, location, v, pairPosContext):
1720
+ """ast.ValueRecord --> otBase.ValueRecord"""
1721
+ if not v:
1722
+ return None
1723
+
1724
+ vr = {}
1725
+ for astName, (otName, isDevice) in self._VALUEREC_ATTRS.items():
1726
+ val = getattr(v, astName, None)
1727
+ if not val:
1728
+ continue
1729
+ if isDevice:
1730
+ vr[otName] = otl.buildDevice(dict(val))
1731
+ elif isinstance(val, VariableScalar):
1732
+ otDeviceName = otName[0:4] + "Device"
1733
+ feaDeviceName = otDeviceName[0].lower() + otDeviceName[1:]
1734
+ if getattr(v, feaDeviceName):
1735
+ raise FeatureLibError(
1736
+ "Can't define a device coordinate and variable scalar", location
1737
+ )
1738
+ vr[otName], device = self.makeVariablePos(location, val)
1739
+ if device is not None:
1740
+ vr[otDeviceName] = device
1741
+ else:
1742
+ vr[otName] = val
1743
+
1744
+ if pairPosContext and not vr:
1745
+ vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0}
1746
+ valRec = otl.buildValue(vr)
1747
+ return valRec