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,2703 @@
1
+ # coding: utf-8
2
+ """fontTools.ttLib.tables.otTables -- A collection of classes representing the various
3
+ OpenType subtables.
4
+
5
+ Most are constructed upon import from data in otData.py, all are populated with
6
+ converter objects from otConverters.py.
7
+ """
8
+ import copy
9
+ from enum import IntEnum
10
+ from functools import reduce
11
+ from math import radians
12
+ import itertools
13
+ from collections import defaultdict, namedtuple
14
+ from fontTools.ttLib import OPTIMIZE_FONT_SPEED
15
+ from fontTools.ttLib.tables.TupleVariation import TupleVariation
16
+ from fontTools.ttLib.tables.otTraverse import dfs_base_table
17
+ from fontTools.misc.arrayTools import quantizeRect
18
+ from fontTools.misc.roundTools import otRound
19
+ from fontTools.misc.transform import Transform, Identity, DecomposedTransform
20
+ from fontTools.misc.textTools import bytesjoin, pad, safeEval
21
+ from fontTools.misc.vector import Vector
22
+ from fontTools.pens.boundsPen import ControlBoundsPen
23
+ from fontTools.pens.transformPen import TransformPen
24
+ from .otBase import (
25
+ BaseTable,
26
+ FormatSwitchingBaseTable,
27
+ ValueRecord,
28
+ CountReference,
29
+ getFormatSwitchingBaseTableClass,
30
+ )
31
+ from fontTools.misc.fixedTools import (
32
+ fixedToFloat as fi2fl,
33
+ floatToFixed as fl2fi,
34
+ floatToFixedToStr as fl2str,
35
+ strToFixedToFloat as str2fl,
36
+ )
37
+ from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY
38
+ import logging
39
+ import struct
40
+ import array
41
+ import sys
42
+ from enum import IntFlag
43
+ from typing import TYPE_CHECKING, Iterator, List, Optional, Set
44
+
45
+ if TYPE_CHECKING:
46
+ from fontTools.ttLib.ttGlyphSet import _TTGlyphSet
47
+
48
+
49
+ log = logging.getLogger(__name__)
50
+
51
+
52
+ class VarComponentFlags(IntFlag):
53
+ RESET_UNSPECIFIED_AXES = 1 << 0
54
+
55
+ HAVE_AXES = 1 << 1
56
+
57
+ AXIS_VALUES_HAVE_VARIATION = 1 << 2
58
+ TRANSFORM_HAS_VARIATION = 1 << 3
59
+
60
+ HAVE_TRANSLATE_X = 1 << 4
61
+ HAVE_TRANSLATE_Y = 1 << 5
62
+ HAVE_ROTATION = 1 << 6
63
+
64
+ HAVE_CONDITION = 1 << 7
65
+
66
+ HAVE_SCALE_X = 1 << 8
67
+ HAVE_SCALE_Y = 1 << 9
68
+ HAVE_TCENTER_X = 1 << 10
69
+ HAVE_TCENTER_Y = 1 << 11
70
+
71
+ GID_IS_24BIT = 1 << 12
72
+
73
+ HAVE_SKEW_X = 1 << 13
74
+ HAVE_SKEW_Y = 1 << 14
75
+
76
+ RESERVED_MASK = (1 << 32) - (1 << 15)
77
+
78
+
79
+ VarTransformMappingValues = namedtuple(
80
+ "VarTransformMappingValues",
81
+ ["flag", "fractionalBits", "scale", "defaultValue"],
82
+ )
83
+
84
+ VAR_TRANSFORM_MAPPING = {
85
+ "translateX": VarTransformMappingValues(
86
+ VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0
87
+ ),
88
+ "translateY": VarTransformMappingValues(
89
+ VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0
90
+ ),
91
+ "rotation": VarTransformMappingValues(VarComponentFlags.HAVE_ROTATION, 12, 180, 0),
92
+ "scaleX": VarTransformMappingValues(VarComponentFlags.HAVE_SCALE_X, 10, 1, 1),
93
+ "scaleY": VarTransformMappingValues(VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1),
94
+ "skewX": VarTransformMappingValues(VarComponentFlags.HAVE_SKEW_X, 12, -180, 0),
95
+ "skewY": VarTransformMappingValues(VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0),
96
+ "tCenterX": VarTransformMappingValues(VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0),
97
+ "tCenterY": VarTransformMappingValues(VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0),
98
+ }
99
+
100
+ # Probably should be somewhere in fontTools.misc
101
+ _packer = {
102
+ 1: lambda v: struct.pack(">B", v),
103
+ 2: lambda v: struct.pack(">H", v),
104
+ 3: lambda v: struct.pack(">L", v)[1:],
105
+ 4: lambda v: struct.pack(">L", v),
106
+ }
107
+ _unpacker = {
108
+ 1: lambda v: struct.unpack(">B", v)[0],
109
+ 2: lambda v: struct.unpack(">H", v)[0],
110
+ 3: lambda v: struct.unpack(">L", b"\0" + v)[0],
111
+ 4: lambda v: struct.unpack(">L", v)[0],
112
+ }
113
+
114
+
115
+ def _read_uint32var(data, i):
116
+ """Read a variable-length number from data starting at index i.
117
+
118
+ Return the number and the next index.
119
+ """
120
+
121
+ b0 = data[i]
122
+ if b0 < 0x80:
123
+ return b0, i + 1
124
+ elif b0 < 0xC0:
125
+ return (b0 - 0x80) << 8 | data[i + 1], i + 2
126
+ elif b0 < 0xE0:
127
+ return (b0 - 0xC0) << 16 | data[i + 1] << 8 | data[i + 2], i + 3
128
+ elif b0 < 0xF0:
129
+ return (b0 - 0xE0) << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[
130
+ i + 3
131
+ ], i + 4
132
+ else:
133
+ return (b0 - 0xF0) << 32 | data[i + 1] << 24 | data[i + 2] << 16 | data[
134
+ i + 3
135
+ ] << 8 | data[i + 4], i + 5
136
+
137
+
138
+ def _write_uint32var(v):
139
+ """Write a variable-length number.
140
+
141
+ Return the data.
142
+ """
143
+ if v < 0x80:
144
+ return struct.pack(">B", v)
145
+ elif v < 0x4000:
146
+ return struct.pack(">H", (v | 0x8000))
147
+ elif v < 0x200000:
148
+ return struct.pack(">L", (v | 0xC00000))[1:]
149
+ elif v < 0x10000000:
150
+ return struct.pack(">L", (v | 0xE0000000))
151
+ else:
152
+ return struct.pack(">B", 0xF0) + struct.pack(">L", v)
153
+
154
+
155
+ class VarComponent:
156
+ def __init__(self):
157
+ self.populateDefaults()
158
+
159
+ def populateDefaults(self, propagator=None):
160
+ self.flags = 0
161
+ self.glyphName = None
162
+ self.conditionIndex = None
163
+ self.axisIndicesIndex = None
164
+ self.axisValues = ()
165
+ self.axisValuesVarIndex = NO_VARIATION_INDEX
166
+ self.transformVarIndex = NO_VARIATION_INDEX
167
+ self.transform = DecomposedTransform()
168
+
169
+ def decompile(self, data, font, localState):
170
+ i = 0
171
+ self.flags, i = _read_uint32var(data, i)
172
+ flags = self.flags
173
+
174
+ gidSize = 3 if flags & VarComponentFlags.GID_IS_24BIT else 2
175
+ glyphID = _unpacker[gidSize](data[i : i + gidSize])
176
+ i += gidSize
177
+ self.glyphName = font.glyphOrder[glyphID]
178
+
179
+ if flags & VarComponentFlags.HAVE_CONDITION:
180
+ self.conditionIndex, i = _read_uint32var(data, i)
181
+
182
+ if flags & VarComponentFlags.HAVE_AXES:
183
+ self.axisIndicesIndex, i = _read_uint32var(data, i)
184
+ else:
185
+ self.axisIndicesIndex = None
186
+
187
+ if self.axisIndicesIndex is None:
188
+ numAxes = 0
189
+ else:
190
+ axisIndices = localState["AxisIndicesList"].Item[self.axisIndicesIndex]
191
+ numAxes = len(axisIndices)
192
+
193
+ if flags & VarComponentFlags.HAVE_AXES:
194
+ axisValues, i = TupleVariation.decompileDeltas_(numAxes, data, i)
195
+ self.axisValues = tuple(fi2fl(v, 14) for v in axisValues)
196
+ else:
197
+ self.axisValues = ()
198
+ assert len(self.axisValues) == numAxes
199
+
200
+ if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION:
201
+ self.axisValuesVarIndex, i = _read_uint32var(data, i)
202
+ else:
203
+ self.axisValuesVarIndex = NO_VARIATION_INDEX
204
+ if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION:
205
+ self.transformVarIndex, i = _read_uint32var(data, i)
206
+ else:
207
+ self.transformVarIndex = NO_VARIATION_INDEX
208
+
209
+ self.transform = DecomposedTransform()
210
+
211
+ def read_transform_component(values):
212
+ nonlocal i
213
+ if flags & values.flag:
214
+ v = (
215
+ fi2fl(
216
+ struct.unpack(">h", data[i : i + 2])[0], values.fractionalBits
217
+ )
218
+ * values.scale
219
+ )
220
+ i += 2
221
+ return v
222
+ else:
223
+ return values.defaultValue
224
+
225
+ for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items():
226
+ value = read_transform_component(mapping_values)
227
+ setattr(self.transform, attr_name, value)
228
+
229
+ if not (flags & VarComponentFlags.HAVE_SCALE_Y):
230
+ self.transform.scaleY = self.transform.scaleX
231
+
232
+ n = flags & VarComponentFlags.RESERVED_MASK
233
+ while n:
234
+ _, i = _read_uint32var(data, i)
235
+ n &= n - 1
236
+
237
+ return data[i:]
238
+
239
+ def compile(self, font):
240
+ optimizeSpeed = font.cfg[OPTIMIZE_FONT_SPEED]
241
+
242
+ data = []
243
+
244
+ flags = self.flags
245
+
246
+ glyphID = font.getGlyphID(self.glyphName)
247
+ if glyphID > 65535:
248
+ flags |= VarComponentFlags.GID_IS_24BIT
249
+ data.append(_packer[3](glyphID))
250
+ else:
251
+ flags &= ~VarComponentFlags.GID_IS_24BIT
252
+ data.append(_packer[2](glyphID))
253
+
254
+ if self.conditionIndex is not None:
255
+ flags |= VarComponentFlags.HAVE_CONDITION
256
+ data.append(_write_uint32var(self.conditionIndex))
257
+
258
+ numAxes = len(self.axisValues)
259
+
260
+ if numAxes:
261
+ flags |= VarComponentFlags.HAVE_AXES
262
+ data.append(_write_uint32var(self.axisIndicesIndex))
263
+ data.append(
264
+ TupleVariation.compileDeltaValues_(
265
+ [fl2fi(v, 14) for v in self.axisValues],
266
+ optimizeSize=not optimizeSpeed,
267
+ )
268
+ )
269
+ else:
270
+ flags &= ~VarComponentFlags.HAVE_AXES
271
+
272
+ if self.axisValuesVarIndex != NO_VARIATION_INDEX:
273
+ flags |= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION
274
+ data.append(_write_uint32var(self.axisValuesVarIndex))
275
+ else:
276
+ flags &= ~VarComponentFlags.AXIS_VALUES_HAVE_VARIATION
277
+ if self.transformVarIndex != NO_VARIATION_INDEX:
278
+ flags |= VarComponentFlags.TRANSFORM_HAS_VARIATION
279
+ data.append(_write_uint32var(self.transformVarIndex))
280
+ else:
281
+ flags &= ~VarComponentFlags.TRANSFORM_HAS_VARIATION
282
+
283
+ def write_transform_component(value, values):
284
+ if flags & values.flag:
285
+ return struct.pack(
286
+ ">h", fl2fi(value / values.scale, values.fractionalBits)
287
+ )
288
+ else:
289
+ return b""
290
+
291
+ for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items():
292
+ value = getattr(self.transform, attr_name)
293
+ data.append(write_transform_component(value, mapping_values))
294
+
295
+ return _write_uint32var(flags) + bytesjoin(data)
296
+
297
+ def toXML(self, writer, ttFont, attrs):
298
+ writer.begintag("VarComponent", attrs)
299
+ writer.newline()
300
+
301
+ def write(name, value, attrs=()):
302
+ if value is not None:
303
+ writer.simpletag(name, (("value", value),) + attrs)
304
+ writer.newline()
305
+
306
+ write("glyphName", self.glyphName)
307
+
308
+ if self.conditionIndex is not None:
309
+ write("conditionIndex", self.conditionIndex)
310
+ if self.axisIndicesIndex is not None:
311
+ write("axisIndicesIndex", self.axisIndicesIndex)
312
+ if (
313
+ self.axisIndicesIndex is not None
314
+ or self.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES
315
+ ):
316
+ if self.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES:
317
+ attrs = (("resetUnspecifiedAxes", 1),)
318
+ else:
319
+ attrs = ()
320
+ write("axisValues", [float(fl2str(v, 14)) for v in self.axisValues], attrs)
321
+
322
+ if self.axisValuesVarIndex != NO_VARIATION_INDEX:
323
+ write("axisValuesVarIndex", self.axisValuesVarIndex)
324
+ if self.transformVarIndex != NO_VARIATION_INDEX:
325
+ write("transformVarIndex", self.transformVarIndex)
326
+
327
+ # Only write transform components that are specified in the
328
+ # flags, even if they are the default value.
329
+ for attr_name, mapping in VAR_TRANSFORM_MAPPING.items():
330
+ if not (self.flags & mapping.flag):
331
+ continue
332
+ v = getattr(self.transform, attr_name)
333
+ write(attr_name, fl2str(v, mapping.fractionalBits))
334
+
335
+ writer.endtag("VarComponent")
336
+ writer.newline()
337
+
338
+ def fromXML(self, name, attrs, content, ttFont):
339
+ content = [c for c in content if isinstance(c, tuple)]
340
+
341
+ self.populateDefaults()
342
+
343
+ for name, attrs, content in content:
344
+ assert not content
345
+ v = attrs["value"]
346
+
347
+ if name == "glyphName":
348
+ self.glyphName = v
349
+ elif name == "conditionIndex":
350
+ self.conditionIndex = safeEval(v)
351
+ elif name == "axisIndicesIndex":
352
+ self.axisIndicesIndex = safeEval(v)
353
+ elif name == "axisValues":
354
+ self.axisValues = tuple(str2fl(v, 14) for v in safeEval(v))
355
+ if safeEval(attrs.get("resetUnspecifiedAxes", "0")):
356
+ self.flags |= VarComponentFlags.RESET_UNSPECIFIED_AXES
357
+ elif name == "axisValuesVarIndex":
358
+ self.axisValuesVarIndex = safeEval(v)
359
+ elif name == "transformVarIndex":
360
+ self.transformVarIndex = safeEval(v)
361
+ elif name in VAR_TRANSFORM_MAPPING:
362
+ setattr(
363
+ self.transform,
364
+ name,
365
+ safeEval(v),
366
+ )
367
+ self.flags |= VAR_TRANSFORM_MAPPING[name].flag
368
+ else:
369
+ assert False, name
370
+
371
+ def applyTransformDeltas(self, deltas):
372
+ i = 0
373
+
374
+ def read_transform_component_delta(values):
375
+ nonlocal i
376
+ if self.flags & values.flag:
377
+ v = fi2fl(deltas[i], values.fractionalBits) * values.scale
378
+ i += 1
379
+ return v
380
+ else:
381
+ return 0
382
+
383
+ for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items():
384
+ value = read_transform_component_delta(mapping_values)
385
+ setattr(
386
+ self.transform, attr_name, getattr(self.transform, attr_name) + value
387
+ )
388
+
389
+ if not (self.flags & VarComponentFlags.HAVE_SCALE_Y):
390
+ self.transform.scaleY = self.transform.scaleX
391
+
392
+ assert i == len(deltas), (i, len(deltas))
393
+
394
+ def __eq__(self, other):
395
+ if type(self) != type(other):
396
+ return NotImplemented
397
+ return self.__dict__ == other.__dict__
398
+
399
+ def __ne__(self, other):
400
+ result = self.__eq__(other)
401
+ return result if result is NotImplemented else not result
402
+
403
+
404
+ class VarCompositeGlyph:
405
+ def __init__(self, components=None):
406
+ self.components = components if components is not None else []
407
+
408
+ def decompile(self, data, font, localState):
409
+ self.components = []
410
+ while data:
411
+ component = VarComponent()
412
+ data = component.decompile(data, font, localState)
413
+ self.components.append(component)
414
+
415
+ def compile(self, font):
416
+ data = []
417
+ for component in self.components:
418
+ data.append(component.compile(font))
419
+ return bytesjoin(data)
420
+
421
+ def toXML(self, xmlWriter, font, attrs, name):
422
+ xmlWriter.begintag("VarCompositeGlyph", attrs)
423
+ xmlWriter.newline()
424
+ for i, component in enumerate(self.components):
425
+ component.toXML(xmlWriter, font, [("index", i)])
426
+ xmlWriter.endtag("VarCompositeGlyph")
427
+ xmlWriter.newline()
428
+
429
+ def fromXML(self, name, attrs, content, font):
430
+ content = [c for c in content if isinstance(c, tuple)]
431
+ for name, attrs, content in content:
432
+ assert name == "VarComponent"
433
+ component = VarComponent()
434
+ component.fromXML(name, attrs, content, font)
435
+ self.components.append(component)
436
+
437
+
438
+ class AATStateTable(object):
439
+ def __init__(self):
440
+ self.GlyphClasses = {} # GlyphID --> GlyphClass
441
+ self.States = [] # List of AATState, indexed by state number
442
+ self.PerGlyphLookups = [] # [{GlyphID:GlyphID}, ...]
443
+
444
+
445
+ class AATState(object):
446
+ def __init__(self):
447
+ self.Transitions = {} # GlyphClass --> AATAction
448
+
449
+
450
+ class AATAction(object):
451
+ _FLAGS = None
452
+
453
+ @staticmethod
454
+ def compileActions(font, states):
455
+ return (None, None)
456
+
457
+ def _writeFlagsToXML(self, xmlWriter):
458
+ flags = [f for f in self._FLAGS if self.__dict__[f]]
459
+ if flags:
460
+ xmlWriter.simpletag("Flags", value=",".join(flags))
461
+ xmlWriter.newline()
462
+ if self.ReservedFlags != 0:
463
+ xmlWriter.simpletag("ReservedFlags", value="0x%04X" % self.ReservedFlags)
464
+ xmlWriter.newline()
465
+
466
+ def _setFlag(self, flag):
467
+ assert flag in self._FLAGS, "unsupported flag %s" % flag
468
+ self.__dict__[flag] = True
469
+
470
+
471
+ class RearrangementMorphAction(AATAction):
472
+ staticSize = 4
473
+ actionHeaderSize = 0
474
+ _FLAGS = ["MarkFirst", "DontAdvance", "MarkLast"]
475
+
476
+ _VERBS = {
477
+ 0: "no change",
478
+ 1: "Ax ⇒ xA",
479
+ 2: "xD ⇒ Dx",
480
+ 3: "AxD ⇒ DxA",
481
+ 4: "ABx ⇒ xAB",
482
+ 5: "ABx ⇒ xBA",
483
+ 6: "xCD ⇒ CDx",
484
+ 7: "xCD ⇒ DCx",
485
+ 8: "AxCD ⇒ CDxA",
486
+ 9: "AxCD ⇒ DCxA",
487
+ 10: "ABxD ⇒ DxAB",
488
+ 11: "ABxD ⇒ DxBA",
489
+ 12: "ABxCD ⇒ CDxAB",
490
+ 13: "ABxCD ⇒ CDxBA",
491
+ 14: "ABxCD ⇒ DCxAB",
492
+ 15: "ABxCD ⇒ DCxBA",
493
+ }
494
+
495
+ def __init__(self):
496
+ self.NewState = 0
497
+ self.Verb = 0
498
+ self.MarkFirst = False
499
+ self.DontAdvance = False
500
+ self.MarkLast = False
501
+ self.ReservedFlags = 0
502
+
503
+ def compile(self, writer, font, actionIndex):
504
+ assert actionIndex is None
505
+ writer.writeUShort(self.NewState)
506
+ assert self.Verb >= 0 and self.Verb <= 15, self.Verb
507
+ flags = self.Verb | self.ReservedFlags
508
+ if self.MarkFirst:
509
+ flags |= 0x8000
510
+ if self.DontAdvance:
511
+ flags |= 0x4000
512
+ if self.MarkLast:
513
+ flags |= 0x2000
514
+ writer.writeUShort(flags)
515
+
516
+ def decompile(self, reader, font, actionReader):
517
+ assert actionReader is None
518
+ self.NewState = reader.readUShort()
519
+ flags = reader.readUShort()
520
+ self.Verb = flags & 0xF
521
+ self.MarkFirst = bool(flags & 0x8000)
522
+ self.DontAdvance = bool(flags & 0x4000)
523
+ self.MarkLast = bool(flags & 0x2000)
524
+ self.ReservedFlags = flags & 0x1FF0
525
+
526
+ def toXML(self, xmlWriter, font, attrs, name):
527
+ xmlWriter.begintag(name, **attrs)
528
+ xmlWriter.newline()
529
+ xmlWriter.simpletag("NewState", value=self.NewState)
530
+ xmlWriter.newline()
531
+ self._writeFlagsToXML(xmlWriter)
532
+ xmlWriter.simpletag("Verb", value=self.Verb)
533
+ verbComment = self._VERBS.get(self.Verb)
534
+ if verbComment is not None:
535
+ xmlWriter.comment(verbComment)
536
+ xmlWriter.newline()
537
+ xmlWriter.endtag(name)
538
+ xmlWriter.newline()
539
+
540
+ def fromXML(self, name, attrs, content, font):
541
+ self.NewState = self.Verb = self.ReservedFlags = 0
542
+ self.MarkFirst = self.DontAdvance = self.MarkLast = False
543
+ content = [t for t in content if isinstance(t, tuple)]
544
+ for eltName, eltAttrs, eltContent in content:
545
+ if eltName == "NewState":
546
+ self.NewState = safeEval(eltAttrs["value"])
547
+ elif eltName == "Verb":
548
+ self.Verb = safeEval(eltAttrs["value"])
549
+ elif eltName == "ReservedFlags":
550
+ self.ReservedFlags = safeEval(eltAttrs["value"])
551
+ elif eltName == "Flags":
552
+ for flag in eltAttrs["value"].split(","):
553
+ self._setFlag(flag.strip())
554
+
555
+
556
+ class ContextualMorphAction(AATAction):
557
+ staticSize = 8
558
+ actionHeaderSize = 0
559
+ _FLAGS = ["SetMark", "DontAdvance"]
560
+
561
+ def __init__(self):
562
+ self.NewState = 0
563
+ self.SetMark, self.DontAdvance = False, False
564
+ self.ReservedFlags = 0
565
+ self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF
566
+
567
+ def compile(self, writer, font, actionIndex):
568
+ assert actionIndex is None
569
+ writer.writeUShort(self.NewState)
570
+ flags = self.ReservedFlags
571
+ if self.SetMark:
572
+ flags |= 0x8000
573
+ if self.DontAdvance:
574
+ flags |= 0x4000
575
+ writer.writeUShort(flags)
576
+ writer.writeUShort(self.MarkIndex)
577
+ writer.writeUShort(self.CurrentIndex)
578
+
579
+ def decompile(self, reader, font, actionReader):
580
+ assert actionReader is None
581
+ self.NewState = reader.readUShort()
582
+ flags = reader.readUShort()
583
+ self.SetMark = bool(flags & 0x8000)
584
+ self.DontAdvance = bool(flags & 0x4000)
585
+ self.ReservedFlags = flags & 0x3FFF
586
+ self.MarkIndex = reader.readUShort()
587
+ self.CurrentIndex = reader.readUShort()
588
+
589
+ def toXML(self, xmlWriter, font, attrs, name):
590
+ xmlWriter.begintag(name, **attrs)
591
+ xmlWriter.newline()
592
+ xmlWriter.simpletag("NewState", value=self.NewState)
593
+ xmlWriter.newline()
594
+ self._writeFlagsToXML(xmlWriter)
595
+ xmlWriter.simpletag("MarkIndex", value=self.MarkIndex)
596
+ xmlWriter.newline()
597
+ xmlWriter.simpletag("CurrentIndex", value=self.CurrentIndex)
598
+ xmlWriter.newline()
599
+ xmlWriter.endtag(name)
600
+ xmlWriter.newline()
601
+
602
+ def fromXML(self, name, attrs, content, font):
603
+ self.NewState = self.ReservedFlags = 0
604
+ self.SetMark = self.DontAdvance = False
605
+ self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF
606
+ content = [t for t in content if isinstance(t, tuple)]
607
+ for eltName, eltAttrs, eltContent in content:
608
+ if eltName == "NewState":
609
+ self.NewState = safeEval(eltAttrs["value"])
610
+ elif eltName == "Flags":
611
+ for flag in eltAttrs["value"].split(","):
612
+ self._setFlag(flag.strip())
613
+ elif eltName == "ReservedFlags":
614
+ self.ReservedFlags = safeEval(eltAttrs["value"])
615
+ elif eltName == "MarkIndex":
616
+ self.MarkIndex = safeEval(eltAttrs["value"])
617
+ elif eltName == "CurrentIndex":
618
+ self.CurrentIndex = safeEval(eltAttrs["value"])
619
+
620
+
621
+ class LigAction(object):
622
+ def __init__(self):
623
+ self.Store = False
624
+ # GlyphIndexDelta is a (possibly negative) delta that gets
625
+ # added to the glyph ID at the top of the AAT runtime
626
+ # execution stack. It is *not* a byte offset into the
627
+ # morx table. The result of the addition, which is performed
628
+ # at run time by the shaping engine, is an index into
629
+ # the ligature components table. See 'morx' specification.
630
+ # In the AAT specification, this field is called Offset;
631
+ # but its meaning is quite different from other offsets
632
+ # in either AAT or OpenType, so we use a different name.
633
+ self.GlyphIndexDelta = 0
634
+
635
+
636
+ class LigatureMorphAction(AATAction):
637
+ staticSize = 6
638
+
639
+ # 4 bytes for each of {action,ligComponents,ligatures}Offset
640
+ actionHeaderSize = 12
641
+
642
+ _FLAGS = ["SetComponent", "DontAdvance"]
643
+
644
+ def __init__(self):
645
+ self.NewState = 0
646
+ self.SetComponent, self.DontAdvance = False, False
647
+ self.ReservedFlags = 0
648
+ self.Actions = []
649
+
650
+ def compile(self, writer, font, actionIndex):
651
+ assert actionIndex is not None
652
+ writer.writeUShort(self.NewState)
653
+ flags = self.ReservedFlags
654
+ if self.SetComponent:
655
+ flags |= 0x8000
656
+ if self.DontAdvance:
657
+ flags |= 0x4000
658
+ if len(self.Actions) > 0:
659
+ flags |= 0x2000
660
+ writer.writeUShort(flags)
661
+ if len(self.Actions) > 0:
662
+ actions = self.compileLigActions()
663
+ writer.writeUShort(actionIndex[actions])
664
+ else:
665
+ writer.writeUShort(0)
666
+
667
+ def decompile(self, reader, font, actionReader):
668
+ assert actionReader is not None
669
+ self.NewState = reader.readUShort()
670
+ flags = reader.readUShort()
671
+ self.SetComponent = bool(flags & 0x8000)
672
+ self.DontAdvance = bool(flags & 0x4000)
673
+ performAction = bool(flags & 0x2000)
674
+ # As of 2017-09-12, the 'morx' specification says that
675
+ # the reserved bitmask in ligature subtables is 0x3FFF.
676
+ # However, the specification also defines a flag 0x2000,
677
+ # so the reserved value should actually be 0x1FFF.
678
+ # TODO: Report this specification bug to Apple.
679
+ self.ReservedFlags = flags & 0x1FFF
680
+ actionIndex = reader.readUShort()
681
+ if performAction:
682
+ self.Actions = self._decompileLigActions(actionReader, actionIndex)
683
+ else:
684
+ self.Actions = []
685
+
686
+ @staticmethod
687
+ def compileActions(font, states):
688
+ result, actions, actionIndex = b"", set(), {}
689
+ for state in states:
690
+ for _glyphClass, trans in state.Transitions.items():
691
+ actions.add(trans.compileLigActions())
692
+ # Sort the compiled actions in decreasing order of
693
+ # length, so that the longer sequence come before the
694
+ # shorter ones. For each compiled action ABCD, its
695
+ # suffixes BCD, CD, and D do not be encoded separately
696
+ # (in case they occur); instead, we can just store an
697
+ # index that points into the middle of the longer
698
+ # sequence. Every compiled AAT ligature sequence is
699
+ # terminated with an end-of-sequence flag, which can
700
+ # only be set on the last element of the sequence.
701
+ # Therefore, it is sufficient to consider just the
702
+ # suffixes.
703
+ for a in sorted(actions, key=lambda x: (-len(x), x)):
704
+ if a not in actionIndex:
705
+ for i in range(0, len(a), 4):
706
+ suffix = a[i:]
707
+ suffixIndex = (len(result) + i) // 4
708
+ actionIndex.setdefault(suffix, suffixIndex)
709
+ result += a
710
+ result = pad(result, 4)
711
+ return (result, actionIndex)
712
+
713
+ def compileLigActions(self):
714
+ result = []
715
+ for i, action in enumerate(self.Actions):
716
+ last = i == len(self.Actions) - 1
717
+ value = action.GlyphIndexDelta & 0x3FFFFFFF
718
+ value |= 0x80000000 if last else 0
719
+ value |= 0x40000000 if action.Store else 0
720
+ result.append(struct.pack(">L", value))
721
+ return bytesjoin(result)
722
+
723
+ def _decompileLigActions(self, actionReader, actionIndex):
724
+ actions = []
725
+ last = False
726
+ reader = actionReader.getSubReader(actionReader.pos + actionIndex * 4)
727
+ while not last:
728
+ value = reader.readULong()
729
+ last = bool(value & 0x80000000)
730
+ action = LigAction()
731
+ actions.append(action)
732
+ action.Store = bool(value & 0x40000000)
733
+ delta = value & 0x3FFFFFFF
734
+ if delta >= 0x20000000: # sign-extend 30-bit value
735
+ delta = -0x40000000 + delta
736
+ action.GlyphIndexDelta = delta
737
+ return actions
738
+
739
+ def fromXML(self, name, attrs, content, font):
740
+ self.NewState = self.ReservedFlags = 0
741
+ self.SetComponent = self.DontAdvance = False
742
+ self.ReservedFlags = 0
743
+ self.Actions = []
744
+ content = [t for t in content if isinstance(t, tuple)]
745
+ for eltName, eltAttrs, eltContent in content:
746
+ if eltName == "NewState":
747
+ self.NewState = safeEval(eltAttrs["value"])
748
+ elif eltName == "Flags":
749
+ for flag in eltAttrs["value"].split(","):
750
+ self._setFlag(flag.strip())
751
+ elif eltName == "ReservedFlags":
752
+ self.ReservedFlags = safeEval(eltAttrs["value"])
753
+ elif eltName == "Action":
754
+ action = LigAction()
755
+ flags = eltAttrs.get("Flags", "").split(",")
756
+ flags = [f.strip() for f in flags]
757
+ action.Store = "Store" in flags
758
+ action.GlyphIndexDelta = safeEval(eltAttrs["GlyphIndexDelta"])
759
+ self.Actions.append(action)
760
+
761
+ def toXML(self, xmlWriter, font, attrs, name):
762
+ xmlWriter.begintag(name, **attrs)
763
+ xmlWriter.newline()
764
+ xmlWriter.simpletag("NewState", value=self.NewState)
765
+ xmlWriter.newline()
766
+ self._writeFlagsToXML(xmlWriter)
767
+ for action in self.Actions:
768
+ attribs = [("GlyphIndexDelta", action.GlyphIndexDelta)]
769
+ if action.Store:
770
+ attribs.append(("Flags", "Store"))
771
+ xmlWriter.simpletag("Action", attribs)
772
+ xmlWriter.newline()
773
+ xmlWriter.endtag(name)
774
+ xmlWriter.newline()
775
+
776
+
777
+ class InsertionMorphAction(AATAction):
778
+ staticSize = 8
779
+ actionHeaderSize = 4 # 4 bytes for actionOffset
780
+ _FLAGS = [
781
+ "SetMark",
782
+ "DontAdvance",
783
+ "CurrentIsKashidaLike",
784
+ "MarkedIsKashidaLike",
785
+ "CurrentInsertBefore",
786
+ "MarkedInsertBefore",
787
+ ]
788
+
789
+ def __init__(self):
790
+ self.NewState = 0
791
+ for flag in self._FLAGS:
792
+ setattr(self, flag, False)
793
+ self.ReservedFlags = 0
794
+ self.CurrentInsertionAction, self.MarkedInsertionAction = [], []
795
+
796
+ def compile(self, writer, font, actionIndex):
797
+ assert actionIndex is not None
798
+ writer.writeUShort(self.NewState)
799
+ flags = self.ReservedFlags
800
+ if self.SetMark:
801
+ flags |= 0x8000
802
+ if self.DontAdvance:
803
+ flags |= 0x4000
804
+ if self.CurrentIsKashidaLike:
805
+ flags |= 0x2000
806
+ if self.MarkedIsKashidaLike:
807
+ flags |= 0x1000
808
+ if self.CurrentInsertBefore:
809
+ flags |= 0x0800
810
+ if self.MarkedInsertBefore:
811
+ flags |= 0x0400
812
+ flags |= len(self.CurrentInsertionAction) << 5
813
+ flags |= len(self.MarkedInsertionAction)
814
+ writer.writeUShort(flags)
815
+ if len(self.CurrentInsertionAction) > 0:
816
+ currentIndex = actionIndex[tuple(self.CurrentInsertionAction)]
817
+ else:
818
+ currentIndex = 0xFFFF
819
+ writer.writeUShort(currentIndex)
820
+ if len(self.MarkedInsertionAction) > 0:
821
+ markedIndex = actionIndex[tuple(self.MarkedInsertionAction)]
822
+ else:
823
+ markedIndex = 0xFFFF
824
+ writer.writeUShort(markedIndex)
825
+
826
+ def decompile(self, reader, font, actionReader):
827
+ assert actionReader is not None
828
+ self.NewState = reader.readUShort()
829
+ flags = reader.readUShort()
830
+ self.SetMark = bool(flags & 0x8000)
831
+ self.DontAdvance = bool(flags & 0x4000)
832
+ self.CurrentIsKashidaLike = bool(flags & 0x2000)
833
+ self.MarkedIsKashidaLike = bool(flags & 0x1000)
834
+ self.CurrentInsertBefore = bool(flags & 0x0800)
835
+ self.MarkedInsertBefore = bool(flags & 0x0400)
836
+ self.CurrentInsertionAction = self._decompileInsertionAction(
837
+ actionReader, font, index=reader.readUShort(), count=((flags & 0x03E0) >> 5)
838
+ )
839
+ self.MarkedInsertionAction = self._decompileInsertionAction(
840
+ actionReader, font, index=reader.readUShort(), count=(flags & 0x001F)
841
+ )
842
+
843
+ def _decompileInsertionAction(self, actionReader, font, index, count):
844
+ if index == 0xFFFF or count == 0:
845
+ return []
846
+ reader = actionReader.getSubReader(actionReader.pos + index * 2)
847
+ return font.getGlyphNameMany(reader.readUShortArray(count))
848
+
849
+ def toXML(self, xmlWriter, font, attrs, name):
850
+ xmlWriter.begintag(name, **attrs)
851
+ xmlWriter.newline()
852
+ xmlWriter.simpletag("NewState", value=self.NewState)
853
+ xmlWriter.newline()
854
+ self._writeFlagsToXML(xmlWriter)
855
+ for g in self.CurrentInsertionAction:
856
+ xmlWriter.simpletag("CurrentInsertionAction", glyph=g)
857
+ xmlWriter.newline()
858
+ for g in self.MarkedInsertionAction:
859
+ xmlWriter.simpletag("MarkedInsertionAction", glyph=g)
860
+ xmlWriter.newline()
861
+ xmlWriter.endtag(name)
862
+ xmlWriter.newline()
863
+
864
+ def fromXML(self, name, attrs, content, font):
865
+ self.__init__()
866
+ content = [t for t in content if isinstance(t, tuple)]
867
+ for eltName, eltAttrs, eltContent in content:
868
+ if eltName == "NewState":
869
+ self.NewState = safeEval(eltAttrs["value"])
870
+ elif eltName == "Flags":
871
+ for flag in eltAttrs["value"].split(","):
872
+ self._setFlag(flag.strip())
873
+ elif eltName == "CurrentInsertionAction":
874
+ self.CurrentInsertionAction.append(eltAttrs["glyph"])
875
+ elif eltName == "MarkedInsertionAction":
876
+ self.MarkedInsertionAction.append(eltAttrs["glyph"])
877
+ else:
878
+ assert False, eltName
879
+
880
+ @staticmethod
881
+ def compileActions(font, states):
882
+ actions, actionIndex, result = set(), {}, b""
883
+ for state in states:
884
+ for _glyphClass, trans in state.Transitions.items():
885
+ if trans.CurrentInsertionAction is not None:
886
+ actions.add(tuple(trans.CurrentInsertionAction))
887
+ if trans.MarkedInsertionAction is not None:
888
+ actions.add(tuple(trans.MarkedInsertionAction))
889
+ # Sort the compiled actions in decreasing order of
890
+ # length, so that the longer sequence come before the
891
+ # shorter ones.
892
+ for action in sorted(actions, key=lambda x: (-len(x), x)):
893
+ # We insert all sub-sequences of the action glyph sequence
894
+ # into actionIndex. For example, if one action triggers on
895
+ # glyph sequence [A, B, C, D, E] and another action triggers
896
+ # on [C, D], we return result=[A, B, C, D, E] (as list of
897
+ # encoded glyph IDs), and actionIndex={('A','B','C','D','E'): 0,
898
+ # ('C','D'): 2}.
899
+ if action in actionIndex:
900
+ continue
901
+ for start in range(0, len(action)):
902
+ startIndex = (len(result) // 2) + start
903
+ for limit in range(start, len(action)):
904
+ glyphs = action[start : limit + 1]
905
+ actionIndex.setdefault(glyphs, startIndex)
906
+ for glyph in action:
907
+ glyphID = font.getGlyphID(glyph)
908
+ result += struct.pack(">H", glyphID)
909
+ return result, actionIndex
910
+
911
+
912
+ class FeatureParams(BaseTable):
913
+ def compile(self, writer, font):
914
+ assert (
915
+ featureParamTypes.get(writer["FeatureTag"]) == self.__class__
916
+ ), "Wrong FeatureParams type for feature '%s': %s" % (
917
+ writer["FeatureTag"],
918
+ self.__class__.__name__,
919
+ )
920
+ BaseTable.compile(self, writer, font)
921
+
922
+ def toXML(self, xmlWriter, font, attrs=None, name=None):
923
+ BaseTable.toXML(self, xmlWriter, font, attrs, name=self.__class__.__name__)
924
+
925
+
926
+ class FeatureParamsSize(FeatureParams):
927
+ pass
928
+
929
+
930
+ class FeatureParamsStylisticSet(FeatureParams):
931
+ pass
932
+
933
+
934
+ class FeatureParamsCharacterVariants(FeatureParams):
935
+ pass
936
+
937
+
938
+ class Coverage(FormatSwitchingBaseTable):
939
+ # manual implementation to get rid of glyphID dependencies
940
+
941
+ def populateDefaults(self, propagator=None):
942
+ if not hasattr(self, "glyphs"):
943
+ self.glyphs = []
944
+
945
+ def postRead(self, rawTable, font):
946
+ if self.Format == 1:
947
+ self.glyphs = rawTable["GlyphArray"]
948
+ elif self.Format == 2:
949
+ glyphs = self.glyphs = []
950
+ ranges = rawTable["RangeRecord"]
951
+ # Some SIL fonts have coverage entries that don't have sorted
952
+ # StartCoverageIndex. If it is so, fixup and warn. We undo
953
+ # this when writing font out.
954
+ sorted_ranges = sorted(ranges, key=lambda a: a.StartCoverageIndex)
955
+ if ranges != sorted_ranges:
956
+ log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.")
957
+ ranges = sorted_ranges
958
+ del sorted_ranges
959
+ for r in ranges:
960
+ start = r.Start
961
+ end = r.End
962
+ startID = font.getGlyphID(start)
963
+ endID = font.getGlyphID(end) + 1
964
+ glyphs.extend(font.getGlyphNameMany(range(startID, endID)))
965
+ else:
966
+ self.glyphs = []
967
+ log.warning("Unknown Coverage format: %s", self.Format)
968
+ del self.Format # Don't need this anymore
969
+
970
+ def preWrite(self, font):
971
+ glyphs = getattr(self, "glyphs", None)
972
+ if glyphs is None:
973
+ glyphs = self.glyphs = []
974
+ format = 1
975
+ rawTable = {"GlyphArray": glyphs}
976
+ if glyphs:
977
+ # find out whether Format 2 is more compact or not
978
+ glyphIDs = font.getGlyphIDMany(glyphs)
979
+ brokenOrder = sorted(glyphIDs) != glyphIDs
980
+
981
+ last = glyphIDs[0]
982
+ ranges = [[last]]
983
+ for glyphID in glyphIDs[1:]:
984
+ if glyphID != last + 1:
985
+ ranges[-1].append(last)
986
+ ranges.append([glyphID])
987
+ last = glyphID
988
+ ranges[-1].append(last)
989
+
990
+ if brokenOrder or len(ranges) * 3 < len(glyphs): # 3 words vs. 1 word
991
+ # Format 2 is more compact
992
+ index = 0
993
+ for i, (start, end) in enumerate(ranges):
994
+ r = RangeRecord()
995
+ r.StartID = start
996
+ r.Start = font.getGlyphName(start)
997
+ r.End = font.getGlyphName(end)
998
+ r.StartCoverageIndex = index
999
+ ranges[i] = r
1000
+ index = index + end - start + 1
1001
+ if brokenOrder:
1002
+ log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.")
1003
+ ranges.sort(key=lambda a: a.StartID)
1004
+ for r in ranges:
1005
+ del r.StartID
1006
+ format = 2
1007
+ rawTable = {"RangeRecord": ranges}
1008
+ # else:
1009
+ # fallthrough; Format 1 is more compact
1010
+ self.Format = format
1011
+ return rawTable
1012
+
1013
+ def toXML2(self, xmlWriter, font):
1014
+ for glyphName in getattr(self, "glyphs", []):
1015
+ xmlWriter.simpletag("Glyph", value=glyphName)
1016
+ xmlWriter.newline()
1017
+
1018
+ def fromXML(self, name, attrs, content, font):
1019
+ glyphs = getattr(self, "glyphs", None)
1020
+ if glyphs is None:
1021
+ glyphs = []
1022
+ self.glyphs = glyphs
1023
+ glyphs.append(attrs["value"])
1024
+
1025
+
1026
+ # The special 0xFFFFFFFF delta-set index is used to indicate that there
1027
+ # is no variation data in the ItemVariationStore for a given variable field
1028
+ NO_VARIATION_INDEX = 0xFFFFFFFF
1029
+
1030
+
1031
+ class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
1032
+ def populateDefaults(self, propagator=None):
1033
+ if not hasattr(self, "mapping"):
1034
+ self.mapping = []
1035
+
1036
+ def postRead(self, rawTable, font):
1037
+ assert (rawTable["EntryFormat"] & 0xFFC0) == 0
1038
+ self.mapping = rawTable["mapping"]
1039
+
1040
+ @staticmethod
1041
+ def getEntryFormat(mapping):
1042
+ ored = 0
1043
+ for idx in mapping:
1044
+ ored |= idx
1045
+
1046
+ inner = ored & 0xFFFF
1047
+ innerBits = 0
1048
+ while inner:
1049
+ innerBits += 1
1050
+ inner >>= 1
1051
+ innerBits = max(innerBits, 1)
1052
+ assert innerBits <= 16
1053
+
1054
+ ored = (ored >> (16 - innerBits)) | (ored & ((1 << innerBits) - 1))
1055
+ if ored <= 0x000000FF:
1056
+ entrySize = 1
1057
+ elif ored <= 0x0000FFFF:
1058
+ entrySize = 2
1059
+ elif ored <= 0x00FFFFFF:
1060
+ entrySize = 3
1061
+ else:
1062
+ entrySize = 4
1063
+
1064
+ return ((entrySize - 1) << 4) | (innerBits - 1)
1065
+
1066
+ def preWrite(self, font):
1067
+ mapping = getattr(self, "mapping", None)
1068
+ if mapping is None:
1069
+ mapping = self.mapping = []
1070
+ self.Format = 1 if len(mapping) > 0xFFFF else 0
1071
+ rawTable = self.__dict__.copy()
1072
+ rawTable["MappingCount"] = len(mapping)
1073
+ rawTable["EntryFormat"] = self.getEntryFormat(mapping)
1074
+ return rawTable
1075
+
1076
+ def toXML2(self, xmlWriter, font):
1077
+ # Make xml dump less verbose, by omitting no-op entries like:
1078
+ # <Map index="..." outer="65535" inner="65535"/>
1079
+ xmlWriter.comment("Omitted values default to 0xFFFF/0xFFFF (no variations)")
1080
+ xmlWriter.newline()
1081
+ for i, value in enumerate(getattr(self, "mapping", [])):
1082
+ attrs = [("index", i)]
1083
+ if value != NO_VARIATION_INDEX:
1084
+ attrs.extend(
1085
+ [
1086
+ ("outer", value >> 16),
1087
+ ("inner", value & 0xFFFF),
1088
+ ]
1089
+ )
1090
+ xmlWriter.simpletag("Map", attrs)
1091
+ xmlWriter.newline()
1092
+
1093
+ def fromXML(self, name, attrs, content, font):
1094
+ mapping = getattr(self, "mapping", None)
1095
+ if mapping is None:
1096
+ self.mapping = mapping = []
1097
+ index = safeEval(attrs["index"])
1098
+ outer = safeEval(attrs.get("outer", "0xFFFF"))
1099
+ inner = safeEval(attrs.get("inner", "0xFFFF"))
1100
+ assert inner <= 0xFFFF
1101
+ mapping.insert(index, (outer << 16) | inner)
1102
+
1103
+ def __getitem__(self, i):
1104
+ return self.mapping[i] if i < len(self.mapping) else NO_VARIATION_INDEX
1105
+
1106
+
1107
+ class VarIdxMap(BaseTable):
1108
+ def populateDefaults(self, propagator=None):
1109
+ if not hasattr(self, "mapping"):
1110
+ self.mapping = {}
1111
+
1112
+ def postRead(self, rawTable, font):
1113
+ assert (rawTable["EntryFormat"] & 0xFFC0) == 0
1114
+ glyphOrder = font.getGlyphOrder()
1115
+ mapList = rawTable["mapping"]
1116
+ mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList)))
1117
+ self.mapping = dict(zip(glyphOrder, mapList))
1118
+
1119
+ def preWrite(self, font):
1120
+ mapping = getattr(self, "mapping", None)
1121
+ if mapping is None:
1122
+ mapping = self.mapping = {}
1123
+
1124
+ glyphOrder = font.getGlyphOrder()
1125
+ mapping = [mapping[g] for g in glyphOrder]
1126
+ while len(mapping) > 1 and mapping[-2] == mapping[-1]:
1127
+ del mapping[-1]
1128
+
1129
+ rawTable = {"mapping": mapping}
1130
+ rawTable["MappingCount"] = len(mapping)
1131
+ rawTable["EntryFormat"] = DeltaSetIndexMap.getEntryFormat(mapping)
1132
+ return rawTable
1133
+
1134
+ def toXML2(self, xmlWriter, font):
1135
+ for glyph, value in sorted(getattr(self, "mapping", {}).items()):
1136
+ attrs = (
1137
+ ("glyph", glyph),
1138
+ ("outer", value >> 16),
1139
+ ("inner", value & 0xFFFF),
1140
+ )
1141
+ xmlWriter.simpletag("Map", attrs)
1142
+ xmlWriter.newline()
1143
+
1144
+ def fromXML(self, name, attrs, content, font):
1145
+ mapping = getattr(self, "mapping", None)
1146
+ if mapping is None:
1147
+ mapping = {}
1148
+ self.mapping = mapping
1149
+ try:
1150
+ glyph = attrs["glyph"]
1151
+ except: # https://github.com/fonttools/fonttools/commit/21cbab8ce9ded3356fef3745122da64dcaf314e9#commitcomment-27649836
1152
+ glyph = font.getGlyphOrder()[attrs["index"]]
1153
+ outer = safeEval(attrs["outer"])
1154
+ inner = safeEval(attrs["inner"])
1155
+ assert inner <= 0xFFFF
1156
+ mapping[glyph] = (outer << 16) | inner
1157
+
1158
+ def __getitem__(self, glyphName):
1159
+ return self.mapping.get(glyphName, NO_VARIATION_INDEX)
1160
+
1161
+
1162
+ class VarRegionList(BaseTable):
1163
+ def preWrite(self, font):
1164
+ # The OT spec says VarStore.VarRegionList.RegionAxisCount should always
1165
+ # be equal to the fvar.axisCount, and OTS < v8.0.0 enforces this rule
1166
+ # even when the VarRegionList is empty. We can't treat RegionAxisCount
1167
+ # like a normal propagated count (== len(Region[i].VarRegionAxis)),
1168
+ # otherwise it would default to 0 if VarRegionList is empty.
1169
+ # Thus, we force it to always be equal to fvar.axisCount.
1170
+ # https://github.com/khaledhosny/ots/pull/192
1171
+ fvarTable = font.get("fvar")
1172
+ if fvarTable:
1173
+ self.RegionAxisCount = len(fvarTable.axes)
1174
+ return {
1175
+ **self.__dict__,
1176
+ "RegionAxisCount": CountReference(self.__dict__, "RegionAxisCount"),
1177
+ }
1178
+
1179
+
1180
+ class SingleSubst(FormatSwitchingBaseTable):
1181
+ def populateDefaults(self, propagator=None):
1182
+ if not hasattr(self, "mapping"):
1183
+ self.mapping = {}
1184
+
1185
+ def postRead(self, rawTable, font):
1186
+ mapping = {}
1187
+ input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
1188
+ if self.Format == 1:
1189
+ delta = rawTable["DeltaGlyphID"]
1190
+ inputGIDS = font.getGlyphIDMany(input)
1191
+ outGIDS = [(glyphID + delta) % 65536 for glyphID in inputGIDS]
1192
+ outNames = font.getGlyphNameMany(outGIDS)
1193
+ for inp, out in zip(input, outNames):
1194
+ mapping[inp] = out
1195
+ elif self.Format == 2:
1196
+ assert (
1197
+ len(input) == rawTable["GlyphCount"]
1198
+ ), "invalid SingleSubstFormat2 table"
1199
+ subst = rawTable["Substitute"]
1200
+ for inp, sub in zip(input, subst):
1201
+ mapping[inp] = sub
1202
+ else:
1203
+ assert 0, "unknown format: %s" % self.Format
1204
+ self.mapping = mapping
1205
+ del self.Format # Don't need this anymore
1206
+
1207
+ def preWrite(self, font):
1208
+ mapping = getattr(self, "mapping", None)
1209
+ if mapping is None:
1210
+ mapping = self.mapping = {}
1211
+ items = list(mapping.items())
1212
+ getGlyphID = font.getGlyphID
1213
+ gidItems = [(getGlyphID(a), getGlyphID(b)) for a, b in items]
1214
+ sortableItems = sorted(zip(gidItems, items))
1215
+
1216
+ # figure out format
1217
+ format = 2
1218
+ delta = None
1219
+ for inID, outID in gidItems:
1220
+ if delta is None:
1221
+ delta = (outID - inID) % 65536
1222
+
1223
+ if (inID + delta) % 65536 != outID:
1224
+ break
1225
+ else:
1226
+ if delta is None:
1227
+ # the mapping is empty, better use format 2
1228
+ format = 2
1229
+ else:
1230
+ format = 1
1231
+
1232
+ rawTable = {}
1233
+ self.Format = format
1234
+ cov = Coverage()
1235
+ input = [item[1][0] for item in sortableItems]
1236
+ subst = [item[1][1] for item in sortableItems]
1237
+ cov.glyphs = input
1238
+ rawTable["Coverage"] = cov
1239
+ if format == 1:
1240
+ assert delta is not None
1241
+ rawTable["DeltaGlyphID"] = delta
1242
+ else:
1243
+ rawTable["Substitute"] = subst
1244
+ return rawTable
1245
+
1246
+ def toXML2(self, xmlWriter, font):
1247
+ items = sorted(self.mapping.items())
1248
+ for inGlyph, outGlyph in items:
1249
+ xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", outGlyph)])
1250
+ xmlWriter.newline()
1251
+
1252
+ def fromXML(self, name, attrs, content, font):
1253
+ mapping = getattr(self, "mapping", None)
1254
+ if mapping is None:
1255
+ mapping = {}
1256
+ self.mapping = mapping
1257
+ mapping[attrs["in"]] = attrs["out"]
1258
+
1259
+
1260
+ class MultipleSubst(FormatSwitchingBaseTable):
1261
+ def populateDefaults(self, propagator=None):
1262
+ if not hasattr(self, "mapping"):
1263
+ self.mapping = {}
1264
+
1265
+ def postRead(self, rawTable, font):
1266
+ mapping = {}
1267
+ if self.Format == 1:
1268
+ glyphs = _getGlyphsFromCoverageTable(rawTable["Coverage"])
1269
+ subst = [s.Substitute for s in rawTable["Sequence"]]
1270
+ mapping = dict(zip(glyphs, subst))
1271
+ else:
1272
+ assert 0, "unknown format: %s" % self.Format
1273
+ self.mapping = mapping
1274
+ del self.Format # Don't need this anymore
1275
+
1276
+ def preWrite(self, font):
1277
+ mapping = getattr(self, "mapping", None)
1278
+ if mapping is None:
1279
+ mapping = self.mapping = {}
1280
+ cov = Coverage()
1281
+ cov.glyphs = sorted(list(mapping.keys()), key=font.getGlyphID)
1282
+ self.Format = 1
1283
+ rawTable = {
1284
+ "Coverage": cov,
1285
+ "Sequence": [self.makeSequence_(mapping[glyph]) for glyph in cov.glyphs],
1286
+ }
1287
+ return rawTable
1288
+
1289
+ def toXML2(self, xmlWriter, font):
1290
+ items = sorted(self.mapping.items())
1291
+ for inGlyph, outGlyphs in items:
1292
+ out = ",".join(outGlyphs)
1293
+ xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", out)])
1294
+ xmlWriter.newline()
1295
+
1296
+ def fromXML(self, name, attrs, content, font):
1297
+ mapping = getattr(self, "mapping", None)
1298
+ if mapping is None:
1299
+ mapping = {}
1300
+ self.mapping = mapping
1301
+
1302
+ # TTX v3.0 and earlier.
1303
+ if name == "Coverage":
1304
+ self.old_coverage_ = []
1305
+ for element in content:
1306
+ if not isinstance(element, tuple):
1307
+ continue
1308
+ element_name, element_attrs, _ = element
1309
+ if element_name == "Glyph":
1310
+ self.old_coverage_.append(element_attrs["value"])
1311
+ return
1312
+ if name == "Sequence":
1313
+ index = int(attrs.get("index", len(mapping)))
1314
+ glyph = self.old_coverage_[index]
1315
+ glyph_mapping = mapping[glyph] = []
1316
+ for element in content:
1317
+ if not isinstance(element, tuple):
1318
+ continue
1319
+ element_name, element_attrs, _ = element
1320
+ if element_name == "Substitute":
1321
+ glyph_mapping.append(element_attrs["value"])
1322
+ return
1323
+
1324
+ # TTX v3.1 and later.
1325
+ outGlyphs = attrs["out"].split(",") if attrs["out"] else []
1326
+ mapping[attrs["in"]] = [g.strip() for g in outGlyphs]
1327
+
1328
+ @staticmethod
1329
+ def makeSequence_(g):
1330
+ seq = Sequence()
1331
+ seq.Substitute = g
1332
+ return seq
1333
+
1334
+
1335
+ class ClassDef(FormatSwitchingBaseTable):
1336
+ def populateDefaults(self, propagator=None):
1337
+ if not hasattr(self, "classDefs"):
1338
+ self.classDefs = {}
1339
+
1340
+ def postRead(self, rawTable, font):
1341
+ classDefs = {}
1342
+
1343
+ if self.Format == 1:
1344
+ start = rawTable["StartGlyph"]
1345
+ classList = rawTable["ClassValueArray"]
1346
+ startID = font.getGlyphID(start)
1347
+ endID = startID + len(classList)
1348
+ glyphNames = font.getGlyphNameMany(range(startID, endID))
1349
+ for glyphName, cls in zip(glyphNames, classList):
1350
+ if cls:
1351
+ classDefs[glyphName] = cls
1352
+
1353
+ elif self.Format == 2:
1354
+ records = rawTable["ClassRangeRecord"]
1355
+ for rec in records:
1356
+ cls = rec.Class
1357
+ if not cls:
1358
+ continue
1359
+ start = rec.Start
1360
+ end = rec.End
1361
+ startID = font.getGlyphID(start)
1362
+ endID = font.getGlyphID(end) + 1
1363
+ glyphNames = font.getGlyphNameMany(range(startID, endID))
1364
+ for glyphName in glyphNames:
1365
+ classDefs[glyphName] = cls
1366
+ else:
1367
+ log.warning("Unknown ClassDef format: %s", self.Format)
1368
+ self.classDefs = classDefs
1369
+ del self.Format # Don't need this anymore
1370
+
1371
+ def _getClassRanges(self, font):
1372
+ classDefs = getattr(self, "classDefs", None)
1373
+ if classDefs is None:
1374
+ self.classDefs = {}
1375
+ return
1376
+ getGlyphID = font.getGlyphID
1377
+ items = []
1378
+ for glyphName, cls in classDefs.items():
1379
+ if not cls:
1380
+ continue
1381
+ items.append((getGlyphID(glyphName), glyphName, cls))
1382
+ if items:
1383
+ items.sort()
1384
+ last, lastName, lastCls = items[0]
1385
+ ranges = [[lastCls, last, lastName]]
1386
+ for glyphID, glyphName, cls in items[1:]:
1387
+ if glyphID != last + 1 or cls != lastCls:
1388
+ ranges[-1].extend([last, lastName])
1389
+ ranges.append([cls, glyphID, glyphName])
1390
+ last = glyphID
1391
+ lastName = glyphName
1392
+ lastCls = cls
1393
+ ranges[-1].extend([last, lastName])
1394
+ return ranges
1395
+
1396
+ def preWrite(self, font):
1397
+ format = 2
1398
+ rawTable = {"ClassRangeRecord": []}
1399
+ ranges = self._getClassRanges(font)
1400
+ if ranges:
1401
+ startGlyph = ranges[0][1]
1402
+ endGlyph = ranges[-1][3]
1403
+ glyphCount = endGlyph - startGlyph + 1
1404
+ if len(ranges) * 3 < glyphCount + 1:
1405
+ # Format 2 is more compact
1406
+ for i, (cls, start, startName, end, endName) in enumerate(ranges):
1407
+ rec = ClassRangeRecord()
1408
+ rec.Start = startName
1409
+ rec.End = endName
1410
+ rec.Class = cls
1411
+ ranges[i] = rec
1412
+ format = 2
1413
+ rawTable = {"ClassRangeRecord": ranges}
1414
+ else:
1415
+ # Format 1 is more compact
1416
+ startGlyphName = ranges[0][2]
1417
+ classes = [0] * glyphCount
1418
+ for cls, start, startName, end, endName in ranges:
1419
+ for g in range(start - startGlyph, end - startGlyph + 1):
1420
+ classes[g] = cls
1421
+ format = 1
1422
+ rawTable = {"StartGlyph": startGlyphName, "ClassValueArray": classes}
1423
+ self.Format = format
1424
+ return rawTable
1425
+
1426
+ def toXML2(self, xmlWriter, font):
1427
+ items = sorted(self.classDefs.items())
1428
+ for glyphName, cls in items:
1429
+ xmlWriter.simpletag("ClassDef", [("glyph", glyphName), ("class", cls)])
1430
+ xmlWriter.newline()
1431
+
1432
+ def fromXML(self, name, attrs, content, font):
1433
+ classDefs = getattr(self, "classDefs", None)
1434
+ if classDefs is None:
1435
+ classDefs = {}
1436
+ self.classDefs = classDefs
1437
+ classDefs[attrs["glyph"]] = int(attrs["class"])
1438
+
1439
+
1440
+ class AlternateSubst(FormatSwitchingBaseTable):
1441
+ def populateDefaults(self, propagator=None):
1442
+ if not hasattr(self, "alternates"):
1443
+ self.alternates = {}
1444
+
1445
+ def postRead(self, rawTable, font):
1446
+ alternates = {}
1447
+ if self.Format == 1:
1448
+ input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
1449
+ alts = rawTable["AlternateSet"]
1450
+ assert len(input) == len(alts)
1451
+ for inp, alt in zip(input, alts):
1452
+ alternates[inp] = alt.Alternate
1453
+ else:
1454
+ assert 0, "unknown format: %s" % self.Format
1455
+ self.alternates = alternates
1456
+ del self.Format # Don't need this anymore
1457
+
1458
+ def preWrite(self, font):
1459
+ self.Format = 1
1460
+ alternates = getattr(self, "alternates", None)
1461
+ if alternates is None:
1462
+ alternates = self.alternates = {}
1463
+ items = list(alternates.items())
1464
+ for i, (glyphName, set) in enumerate(items):
1465
+ items[i] = font.getGlyphID(glyphName), glyphName, set
1466
+ items.sort()
1467
+ cov = Coverage()
1468
+ cov.glyphs = [item[1] for item in items]
1469
+ alternates = []
1470
+ setList = [item[-1] for item in items]
1471
+ for set in setList:
1472
+ alts = AlternateSet()
1473
+ alts.Alternate = set
1474
+ alternates.append(alts)
1475
+ # a special case to deal with the fact that several hundred Adobe Japan1-5
1476
+ # CJK fonts will overflow an offset if the coverage table isn't pushed to the end.
1477
+ # Also useful in that when splitting a sub-table because of an offset overflow
1478
+ # I don't need to calculate the change in the subtable offset due to the change in the coverage table size.
1479
+ # Allows packing more rules in subtable.
1480
+ self.sortCoverageLast = 1
1481
+ return {"Coverage": cov, "AlternateSet": alternates}
1482
+
1483
+ def toXML2(self, xmlWriter, font):
1484
+ items = sorted(self.alternates.items())
1485
+ for glyphName, alternates in items:
1486
+ xmlWriter.begintag("AlternateSet", glyph=glyphName)
1487
+ xmlWriter.newline()
1488
+ for alt in alternates:
1489
+ xmlWriter.simpletag("Alternate", glyph=alt)
1490
+ xmlWriter.newline()
1491
+ xmlWriter.endtag("AlternateSet")
1492
+ xmlWriter.newline()
1493
+
1494
+ def fromXML(self, name, attrs, content, font):
1495
+ alternates = getattr(self, "alternates", None)
1496
+ if alternates is None:
1497
+ alternates = {}
1498
+ self.alternates = alternates
1499
+ glyphName = attrs["glyph"]
1500
+ set = []
1501
+ alternates[glyphName] = set
1502
+ for element in content:
1503
+ if not isinstance(element, tuple):
1504
+ continue
1505
+ name, attrs, content = element
1506
+ set.append(attrs["glyph"])
1507
+
1508
+
1509
+ class LigatureSubst(FormatSwitchingBaseTable):
1510
+ def populateDefaults(self, propagator=None):
1511
+ if not hasattr(self, "ligatures"):
1512
+ self.ligatures = {}
1513
+
1514
+ def postRead(self, rawTable, font):
1515
+ ligatures = {}
1516
+ if self.Format == 1:
1517
+ input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
1518
+ ligSets = rawTable["LigatureSet"]
1519
+ assert len(input) == len(ligSets)
1520
+ for i, inp in enumerate(input):
1521
+ ligatures[inp] = ligSets[i].Ligature
1522
+ else:
1523
+ assert 0, "unknown format: %s" % self.Format
1524
+ self.ligatures = ligatures
1525
+ del self.Format # Don't need this anymore
1526
+
1527
+ @staticmethod
1528
+ def _getLigatureSortKey(components):
1529
+ # Computes a key for ordering ligatures in a GSUB Type-4 lookup.
1530
+
1531
+ # When building the OpenType lookup, we need to make sure that
1532
+ # the longest sequence of components is listed first, so we
1533
+ # use the negative length as the key for sorting.
1534
+ # Note, we no longer need to worry about deterministic order because the
1535
+ # ligature mapping `dict` remembers the insertion order, and this in
1536
+ # turn depends on the order in which the ligatures are written in the FEA.
1537
+ # Since python sort algorithm is stable, the ligatures of equal length
1538
+ # will keep the relative order in which they appear in the feature file.
1539
+ # For example, given the following ligatures (all starting with 'f' and
1540
+ # thus belonging to the same LigatureSet):
1541
+ #
1542
+ # feature liga {
1543
+ # sub f i by f_i;
1544
+ # sub f f f by f_f_f;
1545
+ # sub f f by f_f;
1546
+ # sub f f i by f_f_i;
1547
+ # } liga;
1548
+ #
1549
+ # this should sort to: f_f_f, f_f_i, f_i, f_f
1550
+ # This is also what fea-rs does, see:
1551
+ # https://github.com/adobe-type-tools/afdko/issues/1727
1552
+ # https://github.com/fonttools/fonttools/issues/3428
1553
+ # https://github.com/googlefonts/fontc/pull/680
1554
+ return -len(components)
1555
+
1556
+ def preWrite(self, font):
1557
+ self.Format = 1
1558
+ ligatures = getattr(self, "ligatures", None)
1559
+ if ligatures is None:
1560
+ ligatures = self.ligatures = {}
1561
+
1562
+ if ligatures and isinstance(next(iter(ligatures)), tuple):
1563
+ # New high-level API in v3.1 and later. Note that we just support compiling this
1564
+ # for now. We don't load to this API, and don't do XML with it.
1565
+
1566
+ # ligatures is map from components-sequence to lig-glyph
1567
+ newLigatures = dict()
1568
+ for comps in sorted(ligatures.keys(), key=self._getLigatureSortKey):
1569
+ ligature = Ligature()
1570
+ ligature.Component = comps[1:]
1571
+ ligature.CompCount = len(comps)
1572
+ ligature.LigGlyph = ligatures[comps]
1573
+ newLigatures.setdefault(comps[0], []).append(ligature)
1574
+ ligatures = newLigatures
1575
+
1576
+ items = list(ligatures.items())
1577
+ for i, (glyphName, set) in enumerate(items):
1578
+ items[i] = font.getGlyphID(glyphName), glyphName, set
1579
+ items.sort()
1580
+ cov = Coverage()
1581
+ cov.glyphs = [item[1] for item in items]
1582
+
1583
+ ligSets = []
1584
+ setList = [item[-1] for item in items]
1585
+ for set in setList:
1586
+ ligSet = LigatureSet()
1587
+ ligs = ligSet.Ligature = []
1588
+ for lig in set:
1589
+ ligs.append(lig)
1590
+ ligSets.append(ligSet)
1591
+ # Useful in that when splitting a sub-table because of an offset overflow
1592
+ # I don't need to calculate the change in subtabl offset due to the coverage table size.
1593
+ # Allows packing more rules in subtable.
1594
+ self.sortCoverageLast = 1
1595
+ return {"Coverage": cov, "LigatureSet": ligSets}
1596
+
1597
+ def toXML2(self, xmlWriter, font):
1598
+ items = sorted(self.ligatures.items())
1599
+ for glyphName, ligSets in items:
1600
+ xmlWriter.begintag("LigatureSet", glyph=glyphName)
1601
+ xmlWriter.newline()
1602
+ for lig in ligSets:
1603
+ xmlWriter.simpletag(
1604
+ "Ligature", glyph=lig.LigGlyph, components=",".join(lig.Component)
1605
+ )
1606
+ xmlWriter.newline()
1607
+ xmlWriter.endtag("LigatureSet")
1608
+ xmlWriter.newline()
1609
+
1610
+ def fromXML(self, name, attrs, content, font):
1611
+ ligatures = getattr(self, "ligatures", None)
1612
+ if ligatures is None:
1613
+ ligatures = {}
1614
+ self.ligatures = ligatures
1615
+ glyphName = attrs["glyph"]
1616
+ ligs = []
1617
+ ligatures[glyphName] = ligs
1618
+ for element in content:
1619
+ if not isinstance(element, tuple):
1620
+ continue
1621
+ name, attrs, content = element
1622
+ lig = Ligature()
1623
+ lig.LigGlyph = attrs["glyph"]
1624
+ components = attrs["components"]
1625
+ lig.Component = components.split(",") if components else []
1626
+ lig.CompCount = len(lig.Component)
1627
+ ligs.append(lig)
1628
+
1629
+
1630
+ class COLR(BaseTable):
1631
+ def decompile(self, reader, font):
1632
+ # COLRv0 is exceptional in that LayerRecordCount appears *after* the
1633
+ # LayerRecordArray it counts, but the parser logic expects Count fields
1634
+ # to always precede the arrays. Here we work around this by parsing the
1635
+ # LayerRecordCount before the rest of the table, and storing it in
1636
+ # the reader's local state.
1637
+ subReader = reader.getSubReader(offset=0)
1638
+ for conv in self.getConverters():
1639
+ if conv.name != "LayerRecordCount":
1640
+ subReader.advance(conv.staticSize)
1641
+ continue
1642
+ reader[conv.name] = conv.read(subReader, font, tableDict={})
1643
+ break
1644
+ else:
1645
+ raise AssertionError("LayerRecordCount converter not found")
1646
+ return BaseTable.decompile(self, reader, font)
1647
+
1648
+ def preWrite(self, font):
1649
+ # The writer similarly assumes Count values precede the things counted,
1650
+ # thus here we pre-initialize a CountReference; the actual count value
1651
+ # will be set to the lenght of the array by the time this is assembled.
1652
+ self.LayerRecordCount = None
1653
+ return {
1654
+ **self.__dict__,
1655
+ "LayerRecordCount": CountReference(self.__dict__, "LayerRecordCount"),
1656
+ }
1657
+
1658
+ def computeClipBoxes(self, glyphSet: "_TTGlyphSet", quantization: int = 1):
1659
+ if self.Version == 0:
1660
+ return
1661
+
1662
+ clips = {}
1663
+ for rec in self.BaseGlyphList.BaseGlyphPaintRecord:
1664
+ try:
1665
+ clipBox = rec.Paint.computeClipBox(self, glyphSet, quantization)
1666
+ except Exception as e:
1667
+ from fontTools.ttLib import TTLibError
1668
+
1669
+ raise TTLibError(
1670
+ f"Failed to compute COLR ClipBox for {rec.BaseGlyph!r}"
1671
+ ) from e
1672
+
1673
+ if clipBox is not None:
1674
+ clips[rec.BaseGlyph] = clipBox
1675
+
1676
+ hasClipList = hasattr(self, "ClipList") and self.ClipList is not None
1677
+ if not clips:
1678
+ if hasClipList:
1679
+ self.ClipList = None
1680
+ else:
1681
+ if not hasClipList:
1682
+ self.ClipList = ClipList()
1683
+ self.ClipList.Format = 1
1684
+ self.ClipList.clips = clips
1685
+
1686
+
1687
+ class LookupList(BaseTable):
1688
+ @property
1689
+ def table(self):
1690
+ for l in self.Lookup:
1691
+ for st in l.SubTable:
1692
+ if type(st).__name__.endswith("Subst"):
1693
+ return "GSUB"
1694
+ if type(st).__name__.endswith("Pos"):
1695
+ return "GPOS"
1696
+ raise ValueError
1697
+
1698
+ def toXML2(self, xmlWriter, font):
1699
+ if (
1700
+ not font
1701
+ or "Debg" not in font
1702
+ or LOOKUP_DEBUG_INFO_KEY not in font["Debg"].data
1703
+ ):
1704
+ return super().toXML2(xmlWriter, font)
1705
+ debugData = font["Debg"].data[LOOKUP_DEBUG_INFO_KEY][self.table]
1706
+ for conv in self.getConverters():
1707
+ if conv.repeat:
1708
+ value = getattr(self, conv.name, [])
1709
+ for lookupIndex, item in enumerate(value):
1710
+ if str(lookupIndex) in debugData:
1711
+ info = LookupDebugInfo(*debugData[str(lookupIndex)])
1712
+ tag = info.location
1713
+ if info.name:
1714
+ tag = f"{info.name}: {tag}"
1715
+ if info.feature:
1716
+ script, language, feature = info.feature
1717
+ tag = f"{tag} in {feature} ({script}/{language})"
1718
+ xmlWriter.comment(tag)
1719
+ xmlWriter.newline()
1720
+
1721
+ conv.xmlWrite(
1722
+ xmlWriter, font, item, conv.name, [("index", lookupIndex)]
1723
+ )
1724
+ else:
1725
+ if conv.aux and not eval(conv.aux, None, vars(self)):
1726
+ continue
1727
+ value = getattr(
1728
+ self, conv.name, None
1729
+ ) # TODO Handle defaults instead of defaulting to None!
1730
+ conv.xmlWrite(xmlWriter, font, value, conv.name, [])
1731
+
1732
+
1733
+ class BaseGlyphRecordArray(BaseTable):
1734
+ def preWrite(self, font):
1735
+ self.BaseGlyphRecord = sorted(
1736
+ self.BaseGlyphRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph)
1737
+ )
1738
+ return self.__dict__.copy()
1739
+
1740
+
1741
+ class BaseGlyphList(BaseTable):
1742
+ def preWrite(self, font):
1743
+ self.BaseGlyphPaintRecord = sorted(
1744
+ self.BaseGlyphPaintRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph)
1745
+ )
1746
+ return self.__dict__.copy()
1747
+
1748
+
1749
+ class ClipBoxFormat(IntEnum):
1750
+ Static = 1
1751
+ Variable = 2
1752
+
1753
+ def is_variable(self):
1754
+ return self is self.Variable
1755
+
1756
+ def as_variable(self):
1757
+ return self.Variable
1758
+
1759
+
1760
+ class ClipBox(getFormatSwitchingBaseTableClass("uint8")):
1761
+ formatEnum = ClipBoxFormat
1762
+
1763
+ def as_tuple(self):
1764
+ return tuple(getattr(self, conv.name) for conv in self.getConverters())
1765
+
1766
+ def __repr__(self):
1767
+ return f"{self.__class__.__name__}{self.as_tuple()}"
1768
+
1769
+
1770
+ class ClipList(getFormatSwitchingBaseTableClass("uint8")):
1771
+ def populateDefaults(self, propagator=None):
1772
+ if not hasattr(self, "clips"):
1773
+ self.clips = {}
1774
+
1775
+ def postRead(self, rawTable, font):
1776
+ clips = {}
1777
+ glyphOrder = font.getGlyphOrder()
1778
+ for i, rec in enumerate(rawTable["ClipRecord"]):
1779
+ if rec.StartGlyphID > rec.EndGlyphID:
1780
+ log.warning(
1781
+ "invalid ClipRecord[%i].StartGlyphID (%i) > "
1782
+ "EndGlyphID (%i); skipped",
1783
+ i,
1784
+ rec.StartGlyphID,
1785
+ rec.EndGlyphID,
1786
+ )
1787
+ continue
1788
+ redefinedGlyphs = []
1789
+ missingGlyphs = []
1790
+ for glyphID in range(rec.StartGlyphID, rec.EndGlyphID + 1):
1791
+ try:
1792
+ glyph = glyphOrder[glyphID]
1793
+ except IndexError:
1794
+ missingGlyphs.append(glyphID)
1795
+ continue
1796
+ if glyph not in clips:
1797
+ clips[glyph] = copy.copy(rec.ClipBox)
1798
+ else:
1799
+ redefinedGlyphs.append(glyphID)
1800
+ if redefinedGlyphs:
1801
+ log.warning(
1802
+ "ClipRecord[%i] overlaps previous records; "
1803
+ "ignoring redefined clip boxes for the "
1804
+ "following glyph ID range: [%i-%i]",
1805
+ i,
1806
+ min(redefinedGlyphs),
1807
+ max(redefinedGlyphs),
1808
+ )
1809
+ if missingGlyphs:
1810
+ log.warning(
1811
+ "ClipRecord[%i] range references missing " "glyph IDs: [%i-%i]",
1812
+ i,
1813
+ min(missingGlyphs),
1814
+ max(missingGlyphs),
1815
+ )
1816
+ self.clips = clips
1817
+
1818
+ def groups(self):
1819
+ glyphsByClip = defaultdict(list)
1820
+ uniqueClips = {}
1821
+ for glyphName, clipBox in self.clips.items():
1822
+ key = clipBox.as_tuple()
1823
+ glyphsByClip[key].append(glyphName)
1824
+ if key not in uniqueClips:
1825
+ uniqueClips[key] = clipBox
1826
+ return {
1827
+ frozenset(glyphs): uniqueClips[key] for key, glyphs in glyphsByClip.items()
1828
+ }
1829
+
1830
+ def preWrite(self, font):
1831
+ if not hasattr(self, "clips"):
1832
+ self.clips = {}
1833
+ clipBoxRanges = {}
1834
+ glyphMap = font.getReverseGlyphMap()
1835
+ for glyphs, clipBox in self.groups().items():
1836
+ glyphIDs = sorted(
1837
+ glyphMap[glyphName] for glyphName in glyphs if glyphName in glyphMap
1838
+ )
1839
+ if not glyphIDs:
1840
+ continue
1841
+ last = glyphIDs[0]
1842
+ ranges = [[last]]
1843
+ for glyphID in glyphIDs[1:]:
1844
+ if glyphID != last + 1:
1845
+ ranges[-1].append(last)
1846
+ ranges.append([glyphID])
1847
+ last = glyphID
1848
+ ranges[-1].append(last)
1849
+ for start, end in ranges:
1850
+ assert (start, end) not in clipBoxRanges
1851
+ clipBoxRanges[(start, end)] = clipBox
1852
+
1853
+ clipRecords = []
1854
+ for (start, end), clipBox in sorted(clipBoxRanges.items()):
1855
+ record = ClipRecord()
1856
+ record.StartGlyphID = start
1857
+ record.EndGlyphID = end
1858
+ record.ClipBox = clipBox
1859
+ clipRecords.append(record)
1860
+ rawTable = {
1861
+ "ClipCount": len(clipRecords),
1862
+ "ClipRecord": clipRecords,
1863
+ }
1864
+ return rawTable
1865
+
1866
+ def toXML(self, xmlWriter, font, attrs=None, name=None):
1867
+ tableName = name if name else self.__class__.__name__
1868
+ if attrs is None:
1869
+ attrs = []
1870
+ if hasattr(self, "Format"):
1871
+ attrs.append(("Format", self.Format))
1872
+ xmlWriter.begintag(tableName, attrs)
1873
+ xmlWriter.newline()
1874
+ # sort clips alphabetically to ensure deterministic XML dump
1875
+ for glyphs, clipBox in sorted(
1876
+ self.groups().items(), key=lambda item: min(item[0])
1877
+ ):
1878
+ xmlWriter.begintag("Clip")
1879
+ xmlWriter.newline()
1880
+ for glyphName in sorted(glyphs):
1881
+ xmlWriter.simpletag("Glyph", value=glyphName)
1882
+ xmlWriter.newline()
1883
+ xmlWriter.begintag("ClipBox", [("Format", clipBox.Format)])
1884
+ xmlWriter.newline()
1885
+ clipBox.toXML2(xmlWriter, font)
1886
+ xmlWriter.endtag("ClipBox")
1887
+ xmlWriter.newline()
1888
+ xmlWriter.endtag("Clip")
1889
+ xmlWriter.newline()
1890
+ xmlWriter.endtag(tableName)
1891
+ xmlWriter.newline()
1892
+
1893
+ def fromXML(self, name, attrs, content, font):
1894
+ clips = getattr(self, "clips", None)
1895
+ if clips is None:
1896
+ self.clips = clips = {}
1897
+ assert name == "Clip"
1898
+ glyphs = []
1899
+ clipBox = None
1900
+ for elem in content:
1901
+ if not isinstance(elem, tuple):
1902
+ continue
1903
+ name, attrs, content = elem
1904
+ if name == "Glyph":
1905
+ glyphs.append(attrs["value"])
1906
+ elif name == "ClipBox":
1907
+ clipBox = ClipBox()
1908
+ clipBox.Format = safeEval(attrs["Format"])
1909
+ for elem in content:
1910
+ if not isinstance(elem, tuple):
1911
+ continue
1912
+ name, attrs, content = elem
1913
+ clipBox.fromXML(name, attrs, content, font)
1914
+ if clipBox:
1915
+ for glyphName in glyphs:
1916
+ clips[glyphName] = clipBox
1917
+
1918
+
1919
+ class ExtendMode(IntEnum):
1920
+ PAD = 0
1921
+ REPEAT = 1
1922
+ REFLECT = 2
1923
+
1924
+
1925
+ # Porter-Duff modes for COLRv1 PaintComposite:
1926
+ # https://github.com/googlefonts/colr-gradients-spec/tree/off_sub_1#compositemode-enumeration
1927
+ class CompositeMode(IntEnum):
1928
+ CLEAR = 0
1929
+ SRC = 1
1930
+ DEST = 2
1931
+ SRC_OVER = 3
1932
+ DEST_OVER = 4
1933
+ SRC_IN = 5
1934
+ DEST_IN = 6
1935
+ SRC_OUT = 7
1936
+ DEST_OUT = 8
1937
+ SRC_ATOP = 9
1938
+ DEST_ATOP = 10
1939
+ XOR = 11
1940
+ PLUS = 12
1941
+ SCREEN = 13
1942
+ OVERLAY = 14
1943
+ DARKEN = 15
1944
+ LIGHTEN = 16
1945
+ COLOR_DODGE = 17
1946
+ COLOR_BURN = 18
1947
+ HARD_LIGHT = 19
1948
+ SOFT_LIGHT = 20
1949
+ DIFFERENCE = 21
1950
+ EXCLUSION = 22
1951
+ MULTIPLY = 23
1952
+ HSL_HUE = 24
1953
+ HSL_SATURATION = 25
1954
+ HSL_COLOR = 26
1955
+ HSL_LUMINOSITY = 27
1956
+
1957
+
1958
+ class PaintFormat(IntEnum):
1959
+ PaintColrLayers = 1
1960
+ PaintSolid = 2
1961
+ PaintVarSolid = 3
1962
+ PaintLinearGradient = 4
1963
+ PaintVarLinearGradient = 5
1964
+ PaintRadialGradient = 6
1965
+ PaintVarRadialGradient = 7
1966
+ PaintSweepGradient = 8
1967
+ PaintVarSweepGradient = 9
1968
+ PaintGlyph = 10
1969
+ PaintColrGlyph = 11
1970
+ PaintTransform = 12
1971
+ PaintVarTransform = 13
1972
+ PaintTranslate = 14
1973
+ PaintVarTranslate = 15
1974
+ PaintScale = 16
1975
+ PaintVarScale = 17
1976
+ PaintScaleAroundCenter = 18
1977
+ PaintVarScaleAroundCenter = 19
1978
+ PaintScaleUniform = 20
1979
+ PaintVarScaleUniform = 21
1980
+ PaintScaleUniformAroundCenter = 22
1981
+ PaintVarScaleUniformAroundCenter = 23
1982
+ PaintRotate = 24
1983
+ PaintVarRotate = 25
1984
+ PaintRotateAroundCenter = 26
1985
+ PaintVarRotateAroundCenter = 27
1986
+ PaintSkew = 28
1987
+ PaintVarSkew = 29
1988
+ PaintSkewAroundCenter = 30
1989
+ PaintVarSkewAroundCenter = 31
1990
+ PaintComposite = 32
1991
+
1992
+ def is_variable(self):
1993
+ return self.name.startswith("PaintVar")
1994
+
1995
+ def as_variable(self):
1996
+ if self.is_variable():
1997
+ return self
1998
+ try:
1999
+ return PaintFormat.__members__[f"PaintVar{self.name[5:]}"]
2000
+ except KeyError:
2001
+ return None
2002
+
2003
+
2004
+ class Paint(getFormatSwitchingBaseTableClass("uint8")):
2005
+ formatEnum = PaintFormat
2006
+
2007
+ def getFormatName(self):
2008
+ try:
2009
+ return self.formatEnum(self.Format).name
2010
+ except ValueError:
2011
+ raise NotImplementedError(f"Unknown Paint format: {self.Format}")
2012
+
2013
+ def toXML(self, xmlWriter, font, attrs=None, name=None):
2014
+ tableName = name if name else self.__class__.__name__
2015
+ if attrs is None:
2016
+ attrs = []
2017
+ attrs.append(("Format", self.Format))
2018
+ xmlWriter.begintag(tableName, attrs)
2019
+ xmlWriter.comment(self.getFormatName())
2020
+ xmlWriter.newline()
2021
+ self.toXML2(xmlWriter, font)
2022
+ xmlWriter.endtag(tableName)
2023
+ xmlWriter.newline()
2024
+
2025
+ def iterPaintSubTables(self, colr: COLR) -> Iterator[BaseTable.SubTableEntry]:
2026
+ if self.Format == PaintFormat.PaintColrLayers:
2027
+ # https://github.com/fonttools/fonttools/issues/2438: don't die when no LayerList exists
2028
+ layers = []
2029
+ if colr.LayerList is not None:
2030
+ layers = colr.LayerList.Paint
2031
+ yield from (
2032
+ BaseTable.SubTableEntry(name="Layers", value=v, index=i)
2033
+ for i, v in enumerate(
2034
+ layers[self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers]
2035
+ )
2036
+ )
2037
+ return
2038
+
2039
+ if self.Format == PaintFormat.PaintColrGlyph:
2040
+ for record in colr.BaseGlyphList.BaseGlyphPaintRecord:
2041
+ if record.BaseGlyph == self.Glyph:
2042
+ yield BaseTable.SubTableEntry(name="BaseGlyph", value=record.Paint)
2043
+ return
2044
+ else:
2045
+ raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphList")
2046
+
2047
+ for conv in self.getConverters():
2048
+ if conv.tableClass is not None and issubclass(conv.tableClass, type(self)):
2049
+ value = getattr(self, conv.name)
2050
+ yield BaseTable.SubTableEntry(name=conv.name, value=value)
2051
+
2052
+ def getChildren(self, colr) -> List["Paint"]:
2053
+ # this is kept for backward compatibility (e.g. it's used by the subsetter)
2054
+ return [p.value for p in self.iterPaintSubTables(colr)]
2055
+
2056
+ def traverse(self, colr: COLR, callback):
2057
+ """Depth-first traversal of graph rooted at self, callback on each node."""
2058
+ if not callable(callback):
2059
+ raise TypeError("callback must be callable")
2060
+
2061
+ for path in dfs_base_table(
2062
+ self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr)
2063
+ ):
2064
+ paint = path[-1].value
2065
+ callback(paint)
2066
+
2067
+ def getTransform(self) -> Transform:
2068
+ if self.Format == PaintFormat.PaintTransform:
2069
+ t = self.Transform
2070
+ return Transform(t.xx, t.yx, t.xy, t.yy, t.dx, t.dy)
2071
+ elif self.Format == PaintFormat.PaintTranslate:
2072
+ return Identity.translate(self.dx, self.dy)
2073
+ elif self.Format == PaintFormat.PaintScale:
2074
+ return Identity.scale(self.scaleX, self.scaleY)
2075
+ elif self.Format == PaintFormat.PaintScaleAroundCenter:
2076
+ return (
2077
+ Identity.translate(self.centerX, self.centerY)
2078
+ .scale(self.scaleX, self.scaleY)
2079
+ .translate(-self.centerX, -self.centerY)
2080
+ )
2081
+ elif self.Format == PaintFormat.PaintScaleUniform:
2082
+ return Identity.scale(self.scale)
2083
+ elif self.Format == PaintFormat.PaintScaleUniformAroundCenter:
2084
+ return (
2085
+ Identity.translate(self.centerX, self.centerY)
2086
+ .scale(self.scale)
2087
+ .translate(-self.centerX, -self.centerY)
2088
+ )
2089
+ elif self.Format == PaintFormat.PaintRotate:
2090
+ return Identity.rotate(radians(self.angle))
2091
+ elif self.Format == PaintFormat.PaintRotateAroundCenter:
2092
+ return (
2093
+ Identity.translate(self.centerX, self.centerY)
2094
+ .rotate(radians(self.angle))
2095
+ .translate(-self.centerX, -self.centerY)
2096
+ )
2097
+ elif self.Format == PaintFormat.PaintSkew:
2098
+ return Identity.skew(radians(-self.xSkewAngle), radians(self.ySkewAngle))
2099
+ elif self.Format == PaintFormat.PaintSkewAroundCenter:
2100
+ return (
2101
+ Identity.translate(self.centerX, self.centerY)
2102
+ .skew(radians(-self.xSkewAngle), radians(self.ySkewAngle))
2103
+ .translate(-self.centerX, -self.centerY)
2104
+ )
2105
+ if PaintFormat(self.Format).is_variable():
2106
+ raise NotImplementedError(f"Variable Paints not supported: {self.Format}")
2107
+
2108
+ return Identity
2109
+
2110
+ def computeClipBox(
2111
+ self, colr: COLR, glyphSet: "_TTGlyphSet", quantization: int = 1
2112
+ ) -> Optional[ClipBox]:
2113
+ pen = ControlBoundsPen(glyphSet)
2114
+ for path in dfs_base_table(
2115
+ self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr)
2116
+ ):
2117
+ paint = path[-1].value
2118
+ if paint.Format == PaintFormat.PaintGlyph:
2119
+ transformation = reduce(
2120
+ Transform.transform,
2121
+ (st.value.getTransform() for st in path),
2122
+ Identity,
2123
+ )
2124
+ glyphSet[paint.Glyph].draw(TransformPen(pen, transformation))
2125
+
2126
+ if pen.bounds is None:
2127
+ return None
2128
+
2129
+ cb = ClipBox()
2130
+ cb.Format = int(ClipBoxFormat.Static)
2131
+ cb.xMin, cb.yMin, cb.xMax, cb.yMax = quantizeRect(pen.bounds, quantization)
2132
+ return cb
2133
+
2134
+
2135
+ # For each subtable format there is a class. However, we don't really distinguish
2136
+ # between "field name" and "format name": often these are the same. Yet there's
2137
+ # a whole bunch of fields with different names. The following dict is a mapping
2138
+ # from "format name" to "field name". _buildClasses() uses this to create a
2139
+ # subclass for each alternate field name.
2140
+ #
2141
+ _equivalents = {
2142
+ "MarkArray": ("Mark1Array",),
2143
+ "LangSys": ("DefaultLangSys",),
2144
+ "Coverage": (
2145
+ "MarkCoverage",
2146
+ "BaseCoverage",
2147
+ "LigatureCoverage",
2148
+ "Mark1Coverage",
2149
+ "Mark2Coverage",
2150
+ "BacktrackCoverage",
2151
+ "InputCoverage",
2152
+ "LookAheadCoverage",
2153
+ "VertGlyphCoverage",
2154
+ "HorizGlyphCoverage",
2155
+ "TopAccentCoverage",
2156
+ "ExtendedShapeCoverage",
2157
+ "MathKernCoverage",
2158
+ ),
2159
+ "ClassDef": (
2160
+ "ClassDef1",
2161
+ "ClassDef2",
2162
+ "BacktrackClassDef",
2163
+ "InputClassDef",
2164
+ "LookAheadClassDef",
2165
+ "GlyphClassDef",
2166
+ "MarkAttachClassDef",
2167
+ ),
2168
+ "Anchor": (
2169
+ "EntryAnchor",
2170
+ "ExitAnchor",
2171
+ "BaseAnchor",
2172
+ "LigatureAnchor",
2173
+ "Mark2Anchor",
2174
+ "MarkAnchor",
2175
+ ),
2176
+ "Device": (
2177
+ "XPlaDevice",
2178
+ "YPlaDevice",
2179
+ "XAdvDevice",
2180
+ "YAdvDevice",
2181
+ "XDeviceTable",
2182
+ "YDeviceTable",
2183
+ "DeviceTable",
2184
+ ),
2185
+ "Axis": (
2186
+ "HorizAxis",
2187
+ "VertAxis",
2188
+ ),
2189
+ "MinMax": ("DefaultMinMax",),
2190
+ "BaseCoord": (
2191
+ "MinCoord",
2192
+ "MaxCoord",
2193
+ ),
2194
+ "JstfLangSys": ("DefJstfLangSys",),
2195
+ "JstfGSUBModList": (
2196
+ "ShrinkageEnableGSUB",
2197
+ "ShrinkageDisableGSUB",
2198
+ "ExtensionEnableGSUB",
2199
+ "ExtensionDisableGSUB",
2200
+ ),
2201
+ "JstfGPOSModList": (
2202
+ "ShrinkageEnableGPOS",
2203
+ "ShrinkageDisableGPOS",
2204
+ "ExtensionEnableGPOS",
2205
+ "ExtensionDisableGPOS",
2206
+ ),
2207
+ "JstfMax": (
2208
+ "ShrinkageJstfMax",
2209
+ "ExtensionJstfMax",
2210
+ ),
2211
+ "MathKern": (
2212
+ "TopRightMathKern",
2213
+ "TopLeftMathKern",
2214
+ "BottomRightMathKern",
2215
+ "BottomLeftMathKern",
2216
+ ),
2217
+ "MathGlyphConstruction": ("VertGlyphConstruction", "HorizGlyphConstruction"),
2218
+ }
2219
+
2220
+ #
2221
+ # OverFlow logic, to automatically create ExtensionLookups
2222
+ # XXX This should probably move to otBase.py
2223
+ #
2224
+
2225
+
2226
+ def fixLookupOverFlows(ttf, overflowRecord):
2227
+ """Either the offset from the LookupList to a lookup overflowed, or
2228
+ an offset from a lookup to a subtable overflowed.
2229
+
2230
+ The table layout is::
2231
+
2232
+ GPSO/GUSB
2233
+ Script List
2234
+ Feature List
2235
+ LookUpList
2236
+ Lookup[0] and contents
2237
+ SubTable offset list
2238
+ SubTable[0] and contents
2239
+ ...
2240
+ SubTable[n] and contents
2241
+ ...
2242
+ Lookup[n] and contents
2243
+ SubTable offset list
2244
+ SubTable[0] and contents
2245
+ ...
2246
+ SubTable[n] and contents
2247
+
2248
+ If the offset to a lookup overflowed (SubTableIndex is None)
2249
+ we must promote the *previous* lookup to an Extension type.
2250
+
2251
+ If the offset from a lookup to subtable overflowed, then we must promote it
2252
+ to an Extension Lookup type.
2253
+ """
2254
+ ok = 0
2255
+ lookupIndex = overflowRecord.LookupListIndex
2256
+ if overflowRecord.SubTableIndex is None:
2257
+ lookupIndex = lookupIndex - 1
2258
+ if lookupIndex < 0:
2259
+ return ok
2260
+ if overflowRecord.tableType == "GSUB":
2261
+ extType = 7
2262
+ elif overflowRecord.tableType == "GPOS":
2263
+ extType = 9
2264
+
2265
+ lookups = ttf[overflowRecord.tableType].table.LookupList.Lookup
2266
+ lookup = lookups[lookupIndex]
2267
+ # If the previous lookup is an extType, look further back. Very unlikely, but possible.
2268
+ while lookup.SubTable[0].__class__.LookupType == extType:
2269
+ lookupIndex = lookupIndex - 1
2270
+ if lookupIndex < 0:
2271
+ return ok
2272
+ lookup = lookups[lookupIndex]
2273
+
2274
+ for lookupIndex in range(lookupIndex, len(lookups)):
2275
+ lookup = lookups[lookupIndex]
2276
+ if lookup.LookupType != extType:
2277
+ lookup.LookupType = extType
2278
+ for si, subTable in enumerate(lookup.SubTable):
2279
+ extSubTableClass = lookupTypes[overflowRecord.tableType][extType]
2280
+ extSubTable = extSubTableClass()
2281
+ extSubTable.Format = 1
2282
+ extSubTable.ExtSubTable = subTable
2283
+ lookup.SubTable[si] = extSubTable
2284
+ ok = 1
2285
+ return ok
2286
+
2287
+
2288
+ def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord):
2289
+ ok = 1
2290
+ oldMapping = sorted(oldSubTable.mapping.items())
2291
+ oldLen = len(oldMapping)
2292
+
2293
+ if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
2294
+ # Coverage table is written last. Overflow is to or within the
2295
+ # the coverage table. We will just cut the subtable in half.
2296
+ newLen = oldLen // 2
2297
+
2298
+ elif overflowRecord.itemName == "Sequence":
2299
+ # We just need to back up by two items from the overflowed
2300
+ # Sequence index to make sure the offset to the Coverage table
2301
+ # doesn't overflow.
2302
+ newLen = overflowRecord.itemIndex - 1
2303
+
2304
+ newSubTable.mapping = {}
2305
+ for i in range(newLen, oldLen):
2306
+ item = oldMapping[i]
2307
+ key = item[0]
2308
+ newSubTable.mapping[key] = item[1]
2309
+ del oldSubTable.mapping[key]
2310
+
2311
+ return ok
2312
+
2313
+
2314
+ def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord):
2315
+ ok = 1
2316
+ if hasattr(oldSubTable, "sortCoverageLast"):
2317
+ newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast
2318
+
2319
+ oldAlts = sorted(oldSubTable.alternates.items())
2320
+ oldLen = len(oldAlts)
2321
+
2322
+ if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
2323
+ # Coverage table is written last. overflow is to or within the
2324
+ # the coverage table. We will just cut the subtable in half.
2325
+ newLen = oldLen // 2
2326
+
2327
+ elif overflowRecord.itemName == "AlternateSet":
2328
+ # We just need to back up by two items
2329
+ # from the overflowed AlternateSet index to make sure the offset
2330
+ # to the Coverage table doesn't overflow.
2331
+ newLen = overflowRecord.itemIndex - 1
2332
+
2333
+ newSubTable.alternates = {}
2334
+ for i in range(newLen, oldLen):
2335
+ item = oldAlts[i]
2336
+ key = item[0]
2337
+ newSubTable.alternates[key] = item[1]
2338
+ del oldSubTable.alternates[key]
2339
+
2340
+ return ok
2341
+
2342
+
2343
+ def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord):
2344
+ ok = 1
2345
+ oldLigs = sorted(oldSubTable.ligatures.items())
2346
+ oldLen = len(oldLigs)
2347
+
2348
+ if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
2349
+ # Coverage table is written last. overflow is to or within the
2350
+ # the coverage table. We will just cut the subtable in half.
2351
+ newLen = oldLen // 2
2352
+
2353
+ elif overflowRecord.itemName == "LigatureSet":
2354
+ # We just need to back up by two items
2355
+ # from the overflowed AlternateSet index to make sure the offset
2356
+ # to the Coverage table doesn't overflow.
2357
+ newLen = overflowRecord.itemIndex - 1
2358
+
2359
+ newSubTable.ligatures = {}
2360
+ for i in range(newLen, oldLen):
2361
+ item = oldLigs[i]
2362
+ key = item[0]
2363
+ newSubTable.ligatures[key] = item[1]
2364
+ del oldSubTable.ligatures[key]
2365
+
2366
+ return ok
2367
+
2368
+
2369
+ def splitPairPos(oldSubTable, newSubTable, overflowRecord):
2370
+ st = oldSubTable
2371
+ ok = False
2372
+ newSubTable.Format = oldSubTable.Format
2373
+ if oldSubTable.Format == 1 and len(oldSubTable.PairSet) > 1:
2374
+ for name in "ValueFormat1", "ValueFormat2":
2375
+ setattr(newSubTable, name, getattr(oldSubTable, name))
2376
+
2377
+ # Move top half of coverage to new subtable
2378
+
2379
+ newSubTable.Coverage = oldSubTable.Coverage.__class__()
2380
+
2381
+ coverage = oldSubTable.Coverage.glyphs
2382
+ records = oldSubTable.PairSet
2383
+
2384
+ oldCount = len(oldSubTable.PairSet) // 2
2385
+
2386
+ oldSubTable.Coverage.glyphs = coverage[:oldCount]
2387
+ oldSubTable.PairSet = records[:oldCount]
2388
+
2389
+ newSubTable.Coverage.glyphs = coverage[oldCount:]
2390
+ newSubTable.PairSet = records[oldCount:]
2391
+
2392
+ oldSubTable.PairSetCount = len(oldSubTable.PairSet)
2393
+ newSubTable.PairSetCount = len(newSubTable.PairSet)
2394
+
2395
+ ok = True
2396
+
2397
+ elif oldSubTable.Format == 2 and len(oldSubTable.Class1Record) > 1:
2398
+ if not hasattr(oldSubTable, "Class2Count"):
2399
+ oldSubTable.Class2Count = len(oldSubTable.Class1Record[0].Class2Record)
2400
+ for name in "Class2Count", "ClassDef2", "ValueFormat1", "ValueFormat2":
2401
+ setattr(newSubTable, name, getattr(oldSubTable, name))
2402
+
2403
+ # The two subtables will still have the same ClassDef2 and the table
2404
+ # sharing will still cause the sharing to overflow. As such, disable
2405
+ # sharing on the one that is serialized second (that's oldSubTable).
2406
+ oldSubTable.DontShare = True
2407
+
2408
+ # Move top half of class numbers to new subtable
2409
+
2410
+ newSubTable.Coverage = oldSubTable.Coverage.__class__()
2411
+ newSubTable.ClassDef1 = oldSubTable.ClassDef1.__class__()
2412
+
2413
+ coverage = oldSubTable.Coverage.glyphs
2414
+ classDefs = oldSubTable.ClassDef1.classDefs
2415
+ records = oldSubTable.Class1Record
2416
+
2417
+ oldCount = len(oldSubTable.Class1Record) // 2
2418
+ newGlyphs = set(k for k, v in classDefs.items() if v >= oldCount)
2419
+
2420
+ oldSubTable.Coverage.glyphs = [g for g in coverage if g not in newGlyphs]
2421
+ oldSubTable.ClassDef1.classDefs = {
2422
+ k: v for k, v in classDefs.items() if v < oldCount
2423
+ }
2424
+ oldSubTable.Class1Record = records[:oldCount]
2425
+
2426
+ newSubTable.Coverage.glyphs = [g for g in coverage if g in newGlyphs]
2427
+ newSubTable.ClassDef1.classDefs = {
2428
+ k: (v - oldCount) for k, v in classDefs.items() if v > oldCount
2429
+ }
2430
+ newSubTable.Class1Record = records[oldCount:]
2431
+
2432
+ oldSubTable.Class1Count = len(oldSubTable.Class1Record)
2433
+ newSubTable.Class1Count = len(newSubTable.Class1Record)
2434
+
2435
+ ok = True
2436
+
2437
+ return ok
2438
+
2439
+
2440
+ def splitMarkBasePos(oldSubTable, newSubTable, overflowRecord):
2441
+ # split half of the mark classes to the new subtable
2442
+ classCount = oldSubTable.ClassCount
2443
+ if classCount < 2:
2444
+ # oh well, not much left to split...
2445
+ return False
2446
+
2447
+ oldClassCount = classCount // 2
2448
+ newClassCount = classCount - oldClassCount
2449
+
2450
+ oldMarkCoverage, oldMarkRecords = [], []
2451
+ newMarkCoverage, newMarkRecords = [], []
2452
+ for glyphName, markRecord in zip(
2453
+ oldSubTable.MarkCoverage.glyphs, oldSubTable.MarkArray.MarkRecord
2454
+ ):
2455
+ if markRecord.Class < oldClassCount:
2456
+ oldMarkCoverage.append(glyphName)
2457
+ oldMarkRecords.append(markRecord)
2458
+ else:
2459
+ markRecord.Class -= oldClassCount
2460
+ newMarkCoverage.append(glyphName)
2461
+ newMarkRecords.append(markRecord)
2462
+
2463
+ oldBaseRecords, newBaseRecords = [], []
2464
+ for rec in oldSubTable.BaseArray.BaseRecord:
2465
+ oldBaseRecord, newBaseRecord = rec.__class__(), rec.__class__()
2466
+ oldBaseRecord.BaseAnchor = rec.BaseAnchor[:oldClassCount]
2467
+ newBaseRecord.BaseAnchor = rec.BaseAnchor[oldClassCount:]
2468
+ oldBaseRecords.append(oldBaseRecord)
2469
+ newBaseRecords.append(newBaseRecord)
2470
+
2471
+ newSubTable.Format = oldSubTable.Format
2472
+
2473
+ oldSubTable.MarkCoverage.glyphs = oldMarkCoverage
2474
+ newSubTable.MarkCoverage = oldSubTable.MarkCoverage.__class__()
2475
+ newSubTable.MarkCoverage.glyphs = newMarkCoverage
2476
+
2477
+ # share the same BaseCoverage in both halves
2478
+ newSubTable.BaseCoverage = oldSubTable.BaseCoverage
2479
+
2480
+ oldSubTable.ClassCount = oldClassCount
2481
+ newSubTable.ClassCount = newClassCount
2482
+
2483
+ oldSubTable.MarkArray.MarkRecord = oldMarkRecords
2484
+ newSubTable.MarkArray = oldSubTable.MarkArray.__class__()
2485
+ newSubTable.MarkArray.MarkRecord = newMarkRecords
2486
+
2487
+ oldSubTable.MarkArray.MarkCount = len(oldMarkRecords)
2488
+ newSubTable.MarkArray.MarkCount = len(newMarkRecords)
2489
+
2490
+ oldSubTable.BaseArray.BaseRecord = oldBaseRecords
2491
+ newSubTable.BaseArray = oldSubTable.BaseArray.__class__()
2492
+ newSubTable.BaseArray.BaseRecord = newBaseRecords
2493
+
2494
+ oldSubTable.BaseArray.BaseCount = len(oldBaseRecords)
2495
+ newSubTable.BaseArray.BaseCount = len(newBaseRecords)
2496
+
2497
+ return True
2498
+
2499
+
2500
+ splitTable = {
2501
+ "GSUB": {
2502
+ # 1: splitSingleSubst,
2503
+ 2: splitMultipleSubst,
2504
+ 3: splitAlternateSubst,
2505
+ 4: splitLigatureSubst,
2506
+ # 5: splitContextSubst,
2507
+ # 6: splitChainContextSubst,
2508
+ # 7: splitExtensionSubst,
2509
+ # 8: splitReverseChainSingleSubst,
2510
+ },
2511
+ "GPOS": {
2512
+ # 1: splitSinglePos,
2513
+ 2: splitPairPos,
2514
+ # 3: splitCursivePos,
2515
+ 4: splitMarkBasePos,
2516
+ # 5: splitMarkLigPos,
2517
+ # 6: splitMarkMarkPos,
2518
+ # 7: splitContextPos,
2519
+ # 8: splitChainContextPos,
2520
+ # 9: splitExtensionPos,
2521
+ },
2522
+ }
2523
+
2524
+
2525
+ def fixSubTableOverFlows(ttf, overflowRecord):
2526
+ """
2527
+ An offset has overflowed within a sub-table. We need to divide this subtable into smaller parts.
2528
+ """
2529
+ table = ttf[overflowRecord.tableType].table
2530
+ lookup = table.LookupList.Lookup[overflowRecord.LookupListIndex]
2531
+ subIndex = overflowRecord.SubTableIndex
2532
+ subtable = lookup.SubTable[subIndex]
2533
+
2534
+ # First, try not sharing anything for this subtable...
2535
+ if not hasattr(subtable, "DontShare"):
2536
+ subtable.DontShare = True
2537
+ return True
2538
+
2539
+ if hasattr(subtable, "ExtSubTable"):
2540
+ # We split the subtable of the Extension table, and add a new Extension table
2541
+ # to contain the new subtable.
2542
+
2543
+ subTableType = subtable.ExtSubTable.__class__.LookupType
2544
+ extSubTable = subtable
2545
+ subtable = extSubTable.ExtSubTable
2546
+ newExtSubTableClass = lookupTypes[overflowRecord.tableType][
2547
+ extSubTable.__class__.LookupType
2548
+ ]
2549
+ newExtSubTable = newExtSubTableClass()
2550
+ newExtSubTable.Format = extSubTable.Format
2551
+ toInsert = newExtSubTable
2552
+
2553
+ newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
2554
+ newSubTable = newSubTableClass()
2555
+ newExtSubTable.ExtSubTable = newSubTable
2556
+ else:
2557
+ subTableType = subtable.__class__.LookupType
2558
+ newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
2559
+ newSubTable = newSubTableClass()
2560
+ toInsert = newSubTable
2561
+
2562
+ if hasattr(lookup, "SubTableCount"): # may not be defined yet.
2563
+ lookup.SubTableCount = lookup.SubTableCount + 1
2564
+
2565
+ try:
2566
+ splitFunc = splitTable[overflowRecord.tableType][subTableType]
2567
+ except KeyError:
2568
+ log.error(
2569
+ "Don't know how to split %s lookup type %s",
2570
+ overflowRecord.tableType,
2571
+ subTableType,
2572
+ )
2573
+ return False
2574
+
2575
+ ok = splitFunc(subtable, newSubTable, overflowRecord)
2576
+ if ok:
2577
+ lookup.SubTable.insert(subIndex + 1, toInsert)
2578
+ return ok
2579
+
2580
+
2581
+ # End of OverFlow logic
2582
+
2583
+
2584
+ def _buildClasses():
2585
+ import re
2586
+ from .otData import otData
2587
+
2588
+ formatPat = re.compile(r"([A-Za-z0-9]+)Format(\d+)$")
2589
+ namespace = globals()
2590
+
2591
+ # populate module with classes
2592
+ for name, table in otData:
2593
+ baseClass = BaseTable
2594
+ m = formatPat.match(name)
2595
+ if m:
2596
+ # XxxFormatN subtable, we only add the "base" table
2597
+ name = m.group(1)
2598
+ # the first row of a format-switching otData table describes the Format;
2599
+ # the first column defines the type of the Format field.
2600
+ # Currently this can be either 'uint16' or 'uint8'.
2601
+ formatType = table[0][0]
2602
+ baseClass = getFormatSwitchingBaseTableClass(formatType)
2603
+ if name not in namespace:
2604
+ # the class doesn't exist yet, so the base implementation is used.
2605
+ cls = type(name, (baseClass,), {})
2606
+ if name in ("GSUB", "GPOS"):
2607
+ cls.DontShare = True
2608
+ namespace[name] = cls
2609
+
2610
+ # link Var{Table} <-> {Table} (e.g. ColorStop <-> VarColorStop, etc.)
2611
+ for name, _ in otData:
2612
+ if name.startswith("Var") and len(name) > 3 and name[3:] in namespace:
2613
+ varType = namespace[name]
2614
+ noVarType = namespace[name[3:]]
2615
+ varType.NoVarType = noVarType
2616
+ noVarType.VarType = varType
2617
+
2618
+ for base, alts in _equivalents.items():
2619
+ base = namespace[base]
2620
+ for alt in alts:
2621
+ namespace[alt] = base
2622
+
2623
+ global lookupTypes
2624
+ lookupTypes = {
2625
+ "GSUB": {
2626
+ 1: SingleSubst,
2627
+ 2: MultipleSubst,
2628
+ 3: AlternateSubst,
2629
+ 4: LigatureSubst,
2630
+ 5: ContextSubst,
2631
+ 6: ChainContextSubst,
2632
+ 7: ExtensionSubst,
2633
+ 8: ReverseChainSingleSubst,
2634
+ },
2635
+ "GPOS": {
2636
+ 1: SinglePos,
2637
+ 2: PairPos,
2638
+ 3: CursivePos,
2639
+ 4: MarkBasePos,
2640
+ 5: MarkLigPos,
2641
+ 6: MarkMarkPos,
2642
+ 7: ContextPos,
2643
+ 8: ChainContextPos,
2644
+ 9: ExtensionPos,
2645
+ },
2646
+ "mort": {
2647
+ 4: NoncontextualMorph,
2648
+ },
2649
+ "morx": {
2650
+ 0: RearrangementMorph,
2651
+ 1: ContextualMorph,
2652
+ 2: LigatureMorph,
2653
+ # 3: Reserved,
2654
+ 4: NoncontextualMorph,
2655
+ 5: InsertionMorph,
2656
+ },
2657
+ }
2658
+ lookupTypes["JSTF"] = lookupTypes["GPOS"] # JSTF contains GPOS
2659
+ for lookupEnum in lookupTypes.values():
2660
+ for enum, cls in lookupEnum.items():
2661
+ cls.LookupType = enum
2662
+
2663
+ global featureParamTypes
2664
+ featureParamTypes = {
2665
+ "size": FeatureParamsSize,
2666
+ }
2667
+ for i in range(1, 20 + 1):
2668
+ featureParamTypes["ss%02d" % i] = FeatureParamsStylisticSet
2669
+ for i in range(1, 99 + 1):
2670
+ featureParamTypes["cv%02d" % i] = FeatureParamsCharacterVariants
2671
+
2672
+ # add converters to classes
2673
+ from .otConverters import buildConverters
2674
+
2675
+ for name, table in otData:
2676
+ m = formatPat.match(name)
2677
+ if m:
2678
+ # XxxFormatN subtable, add converter to "base" table
2679
+ name, format = m.groups()
2680
+ format = int(format)
2681
+ cls = namespace[name]
2682
+ if not hasattr(cls, "converters"):
2683
+ cls.converters = {}
2684
+ cls.convertersByName = {}
2685
+ converters, convertersByName = buildConverters(table[1:], namespace)
2686
+ cls.converters[format] = converters
2687
+ cls.convertersByName[format] = convertersByName
2688
+ # XXX Add staticSize?
2689
+ else:
2690
+ cls = namespace[name]
2691
+ cls.converters, cls.convertersByName = buildConverters(table, namespace)
2692
+ # XXX Add staticSize?
2693
+
2694
+
2695
+ _buildClasses()
2696
+
2697
+
2698
+ def _getGlyphsFromCoverageTable(coverage):
2699
+ if coverage is None:
2700
+ # empty coverage table
2701
+ return []
2702
+ else:
2703
+ return coverage.glyphs