fonttools 4.58.3__cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (334) hide show
  1. fontTools/__init__.py +8 -0
  2. fontTools/__main__.py +35 -0
  3. fontTools/afmLib.py +439 -0
  4. fontTools/agl.py +5233 -0
  5. fontTools/cffLib/CFF2ToCFF.py +203 -0
  6. fontTools/cffLib/CFFToCFF2.py +305 -0
  7. fontTools/cffLib/__init__.py +3694 -0
  8. fontTools/cffLib/specializer.py +927 -0
  9. fontTools/cffLib/transforms.py +490 -0
  10. fontTools/cffLib/width.py +210 -0
  11. fontTools/colorLib/__init__.py +0 -0
  12. fontTools/colorLib/builder.py +664 -0
  13. fontTools/colorLib/errors.py +2 -0
  14. fontTools/colorLib/geometry.py +143 -0
  15. fontTools/colorLib/table_builder.py +223 -0
  16. fontTools/colorLib/unbuilder.py +81 -0
  17. fontTools/config/__init__.py +90 -0
  18. fontTools/cu2qu/__init__.py +15 -0
  19. fontTools/cu2qu/__main__.py +6 -0
  20. fontTools/cu2qu/benchmark.py +54 -0
  21. fontTools/cu2qu/cli.py +198 -0
  22. fontTools/cu2qu/cu2qu.c +15545 -0
  23. fontTools/cu2qu/cu2qu.cpython-311-aarch64-linux-gnu.so +0 -0
  24. fontTools/cu2qu/cu2qu.py +531 -0
  25. fontTools/cu2qu/errors.py +77 -0
  26. fontTools/cu2qu/ufo.py +349 -0
  27. fontTools/designspaceLib/__init__.py +3338 -0
  28. fontTools/designspaceLib/__main__.py +6 -0
  29. fontTools/designspaceLib/split.py +475 -0
  30. fontTools/designspaceLib/statNames.py +260 -0
  31. fontTools/designspaceLib/types.py +147 -0
  32. fontTools/encodings/MacRoman.py +258 -0
  33. fontTools/encodings/StandardEncoding.py +258 -0
  34. fontTools/encodings/__init__.py +1 -0
  35. fontTools/encodings/codecs.py +135 -0
  36. fontTools/feaLib/__init__.py +4 -0
  37. fontTools/feaLib/__main__.py +78 -0
  38. fontTools/feaLib/ast.py +2142 -0
  39. fontTools/feaLib/builder.py +1796 -0
  40. fontTools/feaLib/error.py +22 -0
  41. fontTools/feaLib/lexer.c +17336 -0
  42. fontTools/feaLib/lexer.cpython-311-aarch64-linux-gnu.so +0 -0
  43. fontTools/feaLib/lexer.py +287 -0
  44. fontTools/feaLib/location.py +12 -0
  45. fontTools/feaLib/lookupDebugInfo.py +12 -0
  46. fontTools/feaLib/parser.py +2379 -0
  47. fontTools/feaLib/variableScalar.py +113 -0
  48. fontTools/fontBuilder.py +1014 -0
  49. fontTools/help.py +36 -0
  50. fontTools/merge/__init__.py +248 -0
  51. fontTools/merge/__main__.py +6 -0
  52. fontTools/merge/base.py +81 -0
  53. fontTools/merge/cmap.py +173 -0
  54. fontTools/merge/layout.py +526 -0
  55. fontTools/merge/options.py +85 -0
  56. fontTools/merge/tables.py +352 -0
  57. fontTools/merge/unicode.py +78 -0
  58. fontTools/merge/util.py +143 -0
  59. fontTools/misc/__init__.py +1 -0
  60. fontTools/misc/arrayTools.py +424 -0
  61. fontTools/misc/bezierTools.c +40136 -0
  62. fontTools/misc/bezierTools.cpython-311-aarch64-linux-gnu.so +0 -0
  63. fontTools/misc/bezierTools.py +1497 -0
  64. fontTools/misc/classifyTools.py +170 -0
  65. fontTools/misc/cliTools.py +53 -0
  66. fontTools/misc/configTools.py +349 -0
  67. fontTools/misc/cython.py +27 -0
  68. fontTools/misc/dictTools.py +83 -0
  69. fontTools/misc/eexec.py +119 -0
  70. fontTools/misc/encodingTools.py +72 -0
  71. fontTools/misc/etree.py +456 -0
  72. fontTools/misc/filenames.py +245 -0
  73. fontTools/misc/fixedTools.py +253 -0
  74. fontTools/misc/intTools.py +25 -0
  75. fontTools/misc/iterTools.py +12 -0
  76. fontTools/misc/lazyTools.py +42 -0
  77. fontTools/misc/loggingTools.py +543 -0
  78. fontTools/misc/macCreatorType.py +56 -0
  79. fontTools/misc/macRes.py +261 -0
  80. fontTools/misc/plistlib/__init__.py +681 -0
  81. fontTools/misc/plistlib/py.typed +0 -0
  82. fontTools/misc/psCharStrings.py +1496 -0
  83. fontTools/misc/psLib.py +398 -0
  84. fontTools/misc/psOperators.py +572 -0
  85. fontTools/misc/py23.py +96 -0
  86. fontTools/misc/roundTools.py +110 -0
  87. fontTools/misc/sstruct.py +231 -0
  88. fontTools/misc/symfont.py +242 -0
  89. fontTools/misc/testTools.py +233 -0
  90. fontTools/misc/textTools.py +154 -0
  91. fontTools/misc/timeTools.py +88 -0
  92. fontTools/misc/transform.py +516 -0
  93. fontTools/misc/treeTools.py +45 -0
  94. fontTools/misc/vector.py +147 -0
  95. fontTools/misc/visitor.py +142 -0
  96. fontTools/misc/xmlReader.py +188 -0
  97. fontTools/misc/xmlWriter.py +204 -0
  98. fontTools/mtiLib/__init__.py +1400 -0
  99. fontTools/mtiLib/__main__.py +5 -0
  100. fontTools/otlLib/__init__.py +1 -0
  101. fontTools/otlLib/builder.py +3435 -0
  102. fontTools/otlLib/error.py +11 -0
  103. fontTools/otlLib/maxContextCalc.py +96 -0
  104. fontTools/otlLib/optimize/__init__.py +53 -0
  105. fontTools/otlLib/optimize/__main__.py +6 -0
  106. fontTools/otlLib/optimize/gpos.py +439 -0
  107. fontTools/pens/__init__.py +1 -0
  108. fontTools/pens/areaPen.py +52 -0
  109. fontTools/pens/basePen.py +475 -0
  110. fontTools/pens/boundsPen.py +98 -0
  111. fontTools/pens/cairoPen.py +26 -0
  112. fontTools/pens/cocoaPen.py +26 -0
  113. fontTools/pens/cu2quPen.py +325 -0
  114. fontTools/pens/explicitClosingLinePen.py +101 -0
  115. fontTools/pens/filterPen.py +241 -0
  116. fontTools/pens/freetypePen.py +462 -0
  117. fontTools/pens/hashPointPen.py +89 -0
  118. fontTools/pens/momentsPen.c +13459 -0
  119. fontTools/pens/momentsPen.cpython-311-aarch64-linux-gnu.so +0 -0
  120. fontTools/pens/momentsPen.py +879 -0
  121. fontTools/pens/perimeterPen.py +69 -0
  122. fontTools/pens/pointInsidePen.py +192 -0
  123. fontTools/pens/pointPen.py +609 -0
  124. fontTools/pens/qtPen.py +29 -0
  125. fontTools/pens/qu2cuPen.py +105 -0
  126. fontTools/pens/quartzPen.py +43 -0
  127. fontTools/pens/recordingPen.py +335 -0
  128. fontTools/pens/reportLabPen.py +79 -0
  129. fontTools/pens/reverseContourPen.py +96 -0
  130. fontTools/pens/roundingPen.py +130 -0
  131. fontTools/pens/statisticsPen.py +312 -0
  132. fontTools/pens/svgPathPen.py +310 -0
  133. fontTools/pens/t2CharStringPen.py +88 -0
  134. fontTools/pens/teePen.py +55 -0
  135. fontTools/pens/transformPen.py +115 -0
  136. fontTools/pens/ttGlyphPen.py +335 -0
  137. fontTools/pens/wxPen.py +29 -0
  138. fontTools/qu2cu/__init__.py +15 -0
  139. fontTools/qu2cu/__main__.py +7 -0
  140. fontTools/qu2cu/benchmark.py +56 -0
  141. fontTools/qu2cu/cli.py +125 -0
  142. fontTools/qu2cu/qu2cu.c +16738 -0
  143. fontTools/qu2cu/qu2cu.cpython-311-aarch64-linux-gnu.so +0 -0
  144. fontTools/qu2cu/qu2cu.py +405 -0
  145. fontTools/subset/__init__.py +3929 -0
  146. fontTools/subset/__main__.py +6 -0
  147. fontTools/subset/cff.py +184 -0
  148. fontTools/subset/svg.py +253 -0
  149. fontTools/subset/util.py +25 -0
  150. fontTools/svgLib/__init__.py +3 -0
  151. fontTools/svgLib/path/__init__.py +65 -0
  152. fontTools/svgLib/path/arc.py +154 -0
  153. fontTools/svgLib/path/parser.py +322 -0
  154. fontTools/svgLib/path/shapes.py +183 -0
  155. fontTools/t1Lib/__init__.py +648 -0
  156. fontTools/tfmLib.py +460 -0
  157. fontTools/ttLib/__init__.py +30 -0
  158. fontTools/ttLib/__main__.py +148 -0
  159. fontTools/ttLib/macUtils.py +54 -0
  160. fontTools/ttLib/removeOverlaps.py +393 -0
  161. fontTools/ttLib/reorderGlyphs.py +285 -0
  162. fontTools/ttLib/scaleUpem.py +436 -0
  163. fontTools/ttLib/sfnt.py +662 -0
  164. fontTools/ttLib/standardGlyphOrder.py +271 -0
  165. fontTools/ttLib/tables/B_A_S_E_.py +14 -0
  166. fontTools/ttLib/tables/BitmapGlyphMetrics.py +64 -0
  167. fontTools/ttLib/tables/C_B_D_T_.py +113 -0
  168. fontTools/ttLib/tables/C_B_L_C_.py +19 -0
  169. fontTools/ttLib/tables/C_F_F_.py +61 -0
  170. fontTools/ttLib/tables/C_F_F__2.py +26 -0
  171. fontTools/ttLib/tables/C_O_L_R_.py +165 -0
  172. fontTools/ttLib/tables/C_P_A_L_.py +305 -0
  173. fontTools/ttLib/tables/D_S_I_G_.py +158 -0
  174. fontTools/ttLib/tables/D__e_b_g.py +35 -0
  175. fontTools/ttLib/tables/DefaultTable.py +49 -0
  176. fontTools/ttLib/tables/E_B_D_T_.py +835 -0
  177. fontTools/ttLib/tables/E_B_L_C_.py +718 -0
  178. fontTools/ttLib/tables/F_F_T_M_.py +52 -0
  179. fontTools/ttLib/tables/F__e_a_t.py +149 -0
  180. fontTools/ttLib/tables/G_D_E_F_.py +13 -0
  181. fontTools/ttLib/tables/G_M_A_P_.py +148 -0
  182. fontTools/ttLib/tables/G_P_K_G_.py +133 -0
  183. fontTools/ttLib/tables/G_P_O_S_.py +14 -0
  184. fontTools/ttLib/tables/G_S_U_B_.py +13 -0
  185. fontTools/ttLib/tables/G_V_A_R_.py +5 -0
  186. fontTools/ttLib/tables/G__l_a_t.py +235 -0
  187. fontTools/ttLib/tables/G__l_o_c.py +85 -0
  188. fontTools/ttLib/tables/H_V_A_R_.py +13 -0
  189. fontTools/ttLib/tables/J_S_T_F_.py +13 -0
  190. fontTools/ttLib/tables/L_T_S_H_.py +58 -0
  191. fontTools/ttLib/tables/M_A_T_H_.py +13 -0
  192. fontTools/ttLib/tables/M_E_T_A_.py +352 -0
  193. fontTools/ttLib/tables/M_V_A_R_.py +13 -0
  194. fontTools/ttLib/tables/O_S_2f_2.py +752 -0
  195. fontTools/ttLib/tables/S_I_N_G_.py +99 -0
  196. fontTools/ttLib/tables/S_T_A_T_.py +15 -0
  197. fontTools/ttLib/tables/S_V_G_.py +223 -0
  198. fontTools/ttLib/tables/S__i_l_f.py +1040 -0
  199. fontTools/ttLib/tables/S__i_l_l.py +92 -0
  200. fontTools/ttLib/tables/T_S_I_B_.py +13 -0
  201. fontTools/ttLib/tables/T_S_I_C_.py +14 -0
  202. fontTools/ttLib/tables/T_S_I_D_.py +13 -0
  203. fontTools/ttLib/tables/T_S_I_J_.py +13 -0
  204. fontTools/ttLib/tables/T_S_I_P_.py +13 -0
  205. fontTools/ttLib/tables/T_S_I_S_.py +13 -0
  206. fontTools/ttLib/tables/T_S_I_V_.py +26 -0
  207. fontTools/ttLib/tables/T_S_I__0.py +70 -0
  208. fontTools/ttLib/tables/T_S_I__1.py +166 -0
  209. fontTools/ttLib/tables/T_S_I__2.py +17 -0
  210. fontTools/ttLib/tables/T_S_I__3.py +22 -0
  211. fontTools/ttLib/tables/T_S_I__5.py +60 -0
  212. fontTools/ttLib/tables/T_T_F_A_.py +14 -0
  213. fontTools/ttLib/tables/TupleVariation.py +884 -0
  214. fontTools/ttLib/tables/V_A_R_C_.py +12 -0
  215. fontTools/ttLib/tables/V_D_M_X_.py +249 -0
  216. fontTools/ttLib/tables/V_O_R_G_.py +165 -0
  217. fontTools/ttLib/tables/V_V_A_R_.py +13 -0
  218. fontTools/ttLib/tables/__init__.py +98 -0
  219. fontTools/ttLib/tables/_a_n_k_r.py +15 -0
  220. fontTools/ttLib/tables/_a_v_a_r.py +191 -0
  221. fontTools/ttLib/tables/_b_s_l_n.py +15 -0
  222. fontTools/ttLib/tables/_c_i_d_g.py +24 -0
  223. fontTools/ttLib/tables/_c_m_a_p.py +1591 -0
  224. fontTools/ttLib/tables/_c_v_a_r.py +94 -0
  225. fontTools/ttLib/tables/_c_v_t.py +57 -0
  226. fontTools/ttLib/tables/_f_e_a_t.py +15 -0
  227. fontTools/ttLib/tables/_f_p_g_m.py +62 -0
  228. fontTools/ttLib/tables/_f_v_a_r.py +261 -0
  229. fontTools/ttLib/tables/_g_a_s_p.py +63 -0
  230. fontTools/ttLib/tables/_g_c_i_d.py +13 -0
  231. fontTools/ttLib/tables/_g_l_y_f.py +2312 -0
  232. fontTools/ttLib/tables/_g_v_a_r.py +337 -0
  233. fontTools/ttLib/tables/_h_d_m_x.py +127 -0
  234. fontTools/ttLib/tables/_h_e_a_d.py +130 -0
  235. fontTools/ttLib/tables/_h_h_e_a.py +147 -0
  236. fontTools/ttLib/tables/_h_m_t_x.py +160 -0
  237. fontTools/ttLib/tables/_k_e_r_n.py +289 -0
  238. fontTools/ttLib/tables/_l_c_a_r.py +13 -0
  239. fontTools/ttLib/tables/_l_o_c_a.py +70 -0
  240. fontTools/ttLib/tables/_l_t_a_g.py +72 -0
  241. fontTools/ttLib/tables/_m_a_x_p.py +147 -0
  242. fontTools/ttLib/tables/_m_e_t_a.py +112 -0
  243. fontTools/ttLib/tables/_m_o_r_t.py +14 -0
  244. fontTools/ttLib/tables/_m_o_r_x.py +15 -0
  245. fontTools/ttLib/tables/_n_a_m_e.py +1237 -0
  246. fontTools/ttLib/tables/_o_p_b_d.py +14 -0
  247. fontTools/ttLib/tables/_p_o_s_t.py +320 -0
  248. fontTools/ttLib/tables/_p_r_e_p.py +16 -0
  249. fontTools/ttLib/tables/_p_r_o_p.py +12 -0
  250. fontTools/ttLib/tables/_s_b_i_x.py +129 -0
  251. fontTools/ttLib/tables/_t_r_a_k.py +332 -0
  252. fontTools/ttLib/tables/_v_h_e_a.py +139 -0
  253. fontTools/ttLib/tables/_v_m_t_x.py +19 -0
  254. fontTools/ttLib/tables/asciiTable.py +20 -0
  255. fontTools/ttLib/tables/grUtils.py +92 -0
  256. fontTools/ttLib/tables/otBase.py +1466 -0
  257. fontTools/ttLib/tables/otConverters.py +2068 -0
  258. fontTools/ttLib/tables/otData.py +6400 -0
  259. fontTools/ttLib/tables/otTables.py +2708 -0
  260. fontTools/ttLib/tables/otTraverse.py +163 -0
  261. fontTools/ttLib/tables/sbixGlyph.py +149 -0
  262. fontTools/ttLib/tables/sbixStrike.py +177 -0
  263. fontTools/ttLib/tables/table_API_readme.txt +91 -0
  264. fontTools/ttLib/tables/ttProgram.py +594 -0
  265. fontTools/ttLib/ttCollection.py +125 -0
  266. fontTools/ttLib/ttFont.py +1157 -0
  267. fontTools/ttLib/ttGlyphSet.py +490 -0
  268. fontTools/ttLib/ttVisitor.py +32 -0
  269. fontTools/ttLib/woff2.py +1683 -0
  270. fontTools/ttx.py +479 -0
  271. fontTools/ufoLib/__init__.py +2477 -0
  272. fontTools/ufoLib/converters.py +398 -0
  273. fontTools/ufoLib/errors.py +30 -0
  274. fontTools/ufoLib/etree.py +6 -0
  275. fontTools/ufoLib/filenames.py +346 -0
  276. fontTools/ufoLib/glifLib.py +2029 -0
  277. fontTools/ufoLib/kerning.py +121 -0
  278. fontTools/ufoLib/plistlib.py +47 -0
  279. fontTools/ufoLib/pointPen.py +6 -0
  280. fontTools/ufoLib/utils.py +79 -0
  281. fontTools/ufoLib/validators.py +1186 -0
  282. fontTools/unicode.py +50 -0
  283. fontTools/unicodedata/Blocks.py +801 -0
  284. fontTools/unicodedata/Mirrored.py +446 -0
  285. fontTools/unicodedata/OTTags.py +50 -0
  286. fontTools/unicodedata/ScriptExtensions.py +826 -0
  287. fontTools/unicodedata/Scripts.py +3617 -0
  288. fontTools/unicodedata/__init__.py +302 -0
  289. fontTools/varLib/__init__.py +1517 -0
  290. fontTools/varLib/__main__.py +6 -0
  291. fontTools/varLib/avar.py +260 -0
  292. fontTools/varLib/avarPlanner.py +1004 -0
  293. fontTools/varLib/builder.py +215 -0
  294. fontTools/varLib/cff.py +631 -0
  295. fontTools/varLib/errors.py +219 -0
  296. fontTools/varLib/featureVars.py +695 -0
  297. fontTools/varLib/hvar.py +113 -0
  298. fontTools/varLib/instancer/__init__.py +1946 -0
  299. fontTools/varLib/instancer/__main__.py +5 -0
  300. fontTools/varLib/instancer/featureVars.py +190 -0
  301. fontTools/varLib/instancer/names.py +388 -0
  302. fontTools/varLib/instancer/solver.py +309 -0
  303. fontTools/varLib/interpolatable.py +1209 -0
  304. fontTools/varLib/interpolatableHelpers.py +396 -0
  305. fontTools/varLib/interpolatablePlot.py +1269 -0
  306. fontTools/varLib/interpolatableTestContourOrder.py +82 -0
  307. fontTools/varLib/interpolatableTestStartingPoint.py +107 -0
  308. fontTools/varLib/interpolate_layout.py +124 -0
  309. fontTools/varLib/iup.c +19830 -0
  310. fontTools/varLib/iup.cpython-311-aarch64-linux-gnu.so +0 -0
  311. fontTools/varLib/iup.py +490 -0
  312. fontTools/varLib/merger.py +1717 -0
  313. fontTools/varLib/models.py +642 -0
  314. fontTools/varLib/multiVarStore.py +253 -0
  315. fontTools/varLib/mutator.py +518 -0
  316. fontTools/varLib/mvar.py +40 -0
  317. fontTools/varLib/plot.py +238 -0
  318. fontTools/varLib/stat.py +149 -0
  319. fontTools/varLib/varStore.py +739 -0
  320. fontTools/voltLib/__init__.py +5 -0
  321. fontTools/voltLib/__main__.py +206 -0
  322. fontTools/voltLib/ast.py +452 -0
  323. fontTools/voltLib/error.py +12 -0
  324. fontTools/voltLib/lexer.py +99 -0
  325. fontTools/voltLib/parser.py +664 -0
  326. fontTools/voltLib/voltToFea.py +911 -0
  327. fonttools-4.58.3.data/data/share/man/man1/ttx.1 +225 -0
  328. fonttools-4.58.3.dist-info/METADATA +2133 -0
  329. fonttools-4.58.3.dist-info/RECORD +334 -0
  330. fonttools-4.58.3.dist-info/WHEEL +7 -0
  331. fonttools-4.58.3.dist-info/entry_points.txt +5 -0
  332. fonttools-4.58.3.dist-info/licenses/LICENSE +21 -0
  333. fonttools-4.58.3.dist-info/licenses/LICENSE.external +359 -0
  334. fonttools-4.58.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2708 @@
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 in range(len(ranges)):
994
+ start, end = ranges[i]
995
+ r = RangeRecord()
996
+ r.StartID = start
997
+ r.Start = font.getGlyphName(start)
998
+ r.End = font.getGlyphName(end)
999
+ r.StartCoverageIndex = index
1000
+ ranges[i] = r
1001
+ index = index + end - start + 1
1002
+ if brokenOrder:
1003
+ log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.")
1004
+ ranges.sort(key=lambda a: a.StartID)
1005
+ for r in ranges:
1006
+ del r.StartID
1007
+ format = 2
1008
+ rawTable = {"RangeRecord": ranges}
1009
+ # else:
1010
+ # fallthrough; Format 1 is more compact
1011
+ self.Format = format
1012
+ return rawTable
1013
+
1014
+ def toXML2(self, xmlWriter, font):
1015
+ for glyphName in getattr(self, "glyphs", []):
1016
+ xmlWriter.simpletag("Glyph", value=glyphName)
1017
+ xmlWriter.newline()
1018
+
1019
+ def fromXML(self, name, attrs, content, font):
1020
+ glyphs = getattr(self, "glyphs", None)
1021
+ if glyphs is None:
1022
+ glyphs = []
1023
+ self.glyphs = glyphs
1024
+ glyphs.append(attrs["value"])
1025
+
1026
+
1027
+ # The special 0xFFFFFFFF delta-set index is used to indicate that there
1028
+ # is no variation data in the ItemVariationStore for a given variable field
1029
+ NO_VARIATION_INDEX = 0xFFFFFFFF
1030
+
1031
+
1032
+ class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
1033
+ def populateDefaults(self, propagator=None):
1034
+ if not hasattr(self, "mapping"):
1035
+ self.mapping = []
1036
+
1037
+ def postRead(self, rawTable, font):
1038
+ assert (rawTable["EntryFormat"] & 0xFFC0) == 0
1039
+ self.mapping = rawTable["mapping"]
1040
+
1041
+ @staticmethod
1042
+ def getEntryFormat(mapping):
1043
+ ored = 0
1044
+ for idx in mapping:
1045
+ ored |= idx
1046
+
1047
+ inner = ored & 0xFFFF
1048
+ innerBits = 0
1049
+ while inner:
1050
+ innerBits += 1
1051
+ inner >>= 1
1052
+ innerBits = max(innerBits, 1)
1053
+ assert innerBits <= 16
1054
+
1055
+ ored = (ored >> (16 - innerBits)) | (ored & ((1 << innerBits) - 1))
1056
+ if ored <= 0x000000FF:
1057
+ entrySize = 1
1058
+ elif ored <= 0x0000FFFF:
1059
+ entrySize = 2
1060
+ elif ored <= 0x00FFFFFF:
1061
+ entrySize = 3
1062
+ else:
1063
+ entrySize = 4
1064
+
1065
+ return ((entrySize - 1) << 4) | (innerBits - 1)
1066
+
1067
+ def preWrite(self, font):
1068
+ mapping = getattr(self, "mapping", None)
1069
+ if mapping is None:
1070
+ mapping = self.mapping = []
1071
+ self.Format = 1 if len(mapping) > 0xFFFF else 0
1072
+ rawTable = self.__dict__.copy()
1073
+ rawTable["MappingCount"] = len(mapping)
1074
+ rawTable["EntryFormat"] = self.getEntryFormat(mapping)
1075
+ return rawTable
1076
+
1077
+ def toXML2(self, xmlWriter, font):
1078
+ # Make xml dump less verbose, by omitting no-op entries like:
1079
+ # <Map index="..." outer="65535" inner="65535"/>
1080
+ xmlWriter.comment("Omitted values default to 0xFFFF/0xFFFF (no variations)")
1081
+ xmlWriter.newline()
1082
+ for i, value in enumerate(getattr(self, "mapping", [])):
1083
+ attrs = [("index", i)]
1084
+ if value != NO_VARIATION_INDEX:
1085
+ attrs.extend(
1086
+ [
1087
+ ("outer", value >> 16),
1088
+ ("inner", value & 0xFFFF),
1089
+ ]
1090
+ )
1091
+ xmlWriter.simpletag("Map", attrs)
1092
+ xmlWriter.newline()
1093
+
1094
+ def fromXML(self, name, attrs, content, font):
1095
+ mapping = getattr(self, "mapping", None)
1096
+ if mapping is None:
1097
+ self.mapping = mapping = []
1098
+ index = safeEval(attrs["index"])
1099
+ outer = safeEval(attrs.get("outer", "0xFFFF"))
1100
+ inner = safeEval(attrs.get("inner", "0xFFFF"))
1101
+ assert inner <= 0xFFFF
1102
+ mapping.insert(index, (outer << 16) | inner)
1103
+
1104
+ def __getitem__(self, i):
1105
+ return self.mapping[i] if i < len(self.mapping) else NO_VARIATION_INDEX
1106
+
1107
+
1108
+ class VarIdxMap(BaseTable):
1109
+ def populateDefaults(self, propagator=None):
1110
+ if not hasattr(self, "mapping"):
1111
+ self.mapping = {}
1112
+
1113
+ def postRead(self, rawTable, font):
1114
+ assert (rawTable["EntryFormat"] & 0xFFC0) == 0
1115
+ glyphOrder = font.getGlyphOrder()
1116
+ mapList = rawTable["mapping"]
1117
+ mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList)))
1118
+ self.mapping = dict(zip(glyphOrder, mapList))
1119
+
1120
+ def preWrite(self, font):
1121
+ mapping = getattr(self, "mapping", None)
1122
+ if mapping is None:
1123
+ mapping = self.mapping = {}
1124
+
1125
+ glyphOrder = font.getGlyphOrder()
1126
+ mapping = [mapping[g] for g in glyphOrder]
1127
+ while len(mapping) > 1 and mapping[-2] == mapping[-1]:
1128
+ del mapping[-1]
1129
+
1130
+ rawTable = {"mapping": mapping}
1131
+ rawTable["MappingCount"] = len(mapping)
1132
+ rawTable["EntryFormat"] = DeltaSetIndexMap.getEntryFormat(mapping)
1133
+ return rawTable
1134
+
1135
+ def toXML2(self, xmlWriter, font):
1136
+ for glyph, value in sorted(getattr(self, "mapping", {}).items()):
1137
+ attrs = (
1138
+ ("glyph", glyph),
1139
+ ("outer", value >> 16),
1140
+ ("inner", value & 0xFFFF),
1141
+ )
1142
+ xmlWriter.simpletag("Map", attrs)
1143
+ xmlWriter.newline()
1144
+
1145
+ def fromXML(self, name, attrs, content, font):
1146
+ mapping = getattr(self, "mapping", None)
1147
+ if mapping is None:
1148
+ mapping = {}
1149
+ self.mapping = mapping
1150
+ try:
1151
+ glyph = attrs["glyph"]
1152
+ except: # https://github.com/fonttools/fonttools/commit/21cbab8ce9ded3356fef3745122da64dcaf314e9#commitcomment-27649836
1153
+ glyph = font.getGlyphOrder()[attrs["index"]]
1154
+ outer = safeEval(attrs["outer"])
1155
+ inner = safeEval(attrs["inner"])
1156
+ assert inner <= 0xFFFF
1157
+ mapping[glyph] = (outer << 16) | inner
1158
+
1159
+ def __getitem__(self, glyphName):
1160
+ return self.mapping.get(glyphName, NO_VARIATION_INDEX)
1161
+
1162
+
1163
+ class VarRegionList(BaseTable):
1164
+ def preWrite(self, font):
1165
+ # The OT spec says VarStore.VarRegionList.RegionAxisCount should always
1166
+ # be equal to the fvar.axisCount, and OTS < v8.0.0 enforces this rule
1167
+ # even when the VarRegionList is empty. We can't treat RegionAxisCount
1168
+ # like a normal propagated count (== len(Region[i].VarRegionAxis)),
1169
+ # otherwise it would default to 0 if VarRegionList is empty.
1170
+ # Thus, we force it to always be equal to fvar.axisCount.
1171
+ # https://github.com/khaledhosny/ots/pull/192
1172
+ fvarTable = font.get("fvar")
1173
+ if fvarTable:
1174
+ self.RegionAxisCount = len(fvarTable.axes)
1175
+ return {
1176
+ **self.__dict__,
1177
+ "RegionAxisCount": CountReference(self.__dict__, "RegionAxisCount"),
1178
+ }
1179
+
1180
+
1181
+ class SingleSubst(FormatSwitchingBaseTable):
1182
+ def populateDefaults(self, propagator=None):
1183
+ if not hasattr(self, "mapping"):
1184
+ self.mapping = {}
1185
+
1186
+ def postRead(self, rawTable, font):
1187
+ mapping = {}
1188
+ input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
1189
+ if self.Format == 1:
1190
+ delta = rawTable["DeltaGlyphID"]
1191
+ inputGIDS = font.getGlyphIDMany(input)
1192
+ outGIDS = [(glyphID + delta) % 65536 for glyphID in inputGIDS]
1193
+ outNames = font.getGlyphNameMany(outGIDS)
1194
+ for inp, out in zip(input, outNames):
1195
+ mapping[inp] = out
1196
+ elif self.Format == 2:
1197
+ assert (
1198
+ len(input) == rawTable["GlyphCount"]
1199
+ ), "invalid SingleSubstFormat2 table"
1200
+ subst = rawTable["Substitute"]
1201
+ for inp, sub in zip(input, subst):
1202
+ mapping[inp] = sub
1203
+ else:
1204
+ assert 0, "unknown format: %s" % self.Format
1205
+ self.mapping = mapping
1206
+ del self.Format # Don't need this anymore
1207
+
1208
+ def preWrite(self, font):
1209
+ mapping = getattr(self, "mapping", None)
1210
+ if mapping is None:
1211
+ mapping = self.mapping = {}
1212
+ items = list(mapping.items())
1213
+ getGlyphID = font.getGlyphID
1214
+ gidItems = [(getGlyphID(a), getGlyphID(b)) for a, b in items]
1215
+ sortableItems = sorted(zip(gidItems, items))
1216
+
1217
+ # figure out format
1218
+ format = 2
1219
+ delta = None
1220
+ for inID, outID in gidItems:
1221
+ if delta is None:
1222
+ delta = (outID - inID) % 65536
1223
+
1224
+ if (inID + delta) % 65536 != outID:
1225
+ break
1226
+ else:
1227
+ if delta is None:
1228
+ # the mapping is empty, better use format 2
1229
+ format = 2
1230
+ else:
1231
+ format = 1
1232
+
1233
+ rawTable = {}
1234
+ self.Format = format
1235
+ cov = Coverage()
1236
+ input = [item[1][0] for item in sortableItems]
1237
+ subst = [item[1][1] for item in sortableItems]
1238
+ cov.glyphs = input
1239
+ rawTable["Coverage"] = cov
1240
+ if format == 1:
1241
+ assert delta is not None
1242
+ rawTable["DeltaGlyphID"] = delta
1243
+ else:
1244
+ rawTable["Substitute"] = subst
1245
+ return rawTable
1246
+
1247
+ def toXML2(self, xmlWriter, font):
1248
+ items = sorted(self.mapping.items())
1249
+ for inGlyph, outGlyph in items:
1250
+ xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", outGlyph)])
1251
+ xmlWriter.newline()
1252
+
1253
+ def fromXML(self, name, attrs, content, font):
1254
+ mapping = getattr(self, "mapping", None)
1255
+ if mapping is None:
1256
+ mapping = {}
1257
+ self.mapping = mapping
1258
+ mapping[attrs["in"]] = attrs["out"]
1259
+
1260
+
1261
+ class MultipleSubst(FormatSwitchingBaseTable):
1262
+ def populateDefaults(self, propagator=None):
1263
+ if not hasattr(self, "mapping"):
1264
+ self.mapping = {}
1265
+
1266
+ def postRead(self, rawTable, font):
1267
+ mapping = {}
1268
+ if self.Format == 1:
1269
+ glyphs = _getGlyphsFromCoverageTable(rawTable["Coverage"])
1270
+ subst = [s.Substitute for s in rawTable["Sequence"]]
1271
+ mapping = dict(zip(glyphs, subst))
1272
+ else:
1273
+ assert 0, "unknown format: %s" % self.Format
1274
+ self.mapping = mapping
1275
+ del self.Format # Don't need this anymore
1276
+
1277
+ def preWrite(self, font):
1278
+ mapping = getattr(self, "mapping", None)
1279
+ if mapping is None:
1280
+ mapping = self.mapping = {}
1281
+ cov = Coverage()
1282
+ cov.glyphs = sorted(list(mapping.keys()), key=font.getGlyphID)
1283
+ self.Format = 1
1284
+ rawTable = {
1285
+ "Coverage": cov,
1286
+ "Sequence": [self.makeSequence_(mapping[glyph]) for glyph in cov.glyphs],
1287
+ }
1288
+ return rawTable
1289
+
1290
+ def toXML2(self, xmlWriter, font):
1291
+ items = sorted(self.mapping.items())
1292
+ for inGlyph, outGlyphs in items:
1293
+ out = ",".join(outGlyphs)
1294
+ xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", out)])
1295
+ xmlWriter.newline()
1296
+
1297
+ def fromXML(self, name, attrs, content, font):
1298
+ mapping = getattr(self, "mapping", None)
1299
+ if mapping is None:
1300
+ mapping = {}
1301
+ self.mapping = mapping
1302
+
1303
+ # TTX v3.0 and earlier.
1304
+ if name == "Coverage":
1305
+ self.old_coverage_ = []
1306
+ for element in content:
1307
+ if not isinstance(element, tuple):
1308
+ continue
1309
+ element_name, element_attrs, _ = element
1310
+ if element_name == "Glyph":
1311
+ self.old_coverage_.append(element_attrs["value"])
1312
+ return
1313
+ if name == "Sequence":
1314
+ index = int(attrs.get("index", len(mapping)))
1315
+ glyph = self.old_coverage_[index]
1316
+ glyph_mapping = mapping[glyph] = []
1317
+ for element in content:
1318
+ if not isinstance(element, tuple):
1319
+ continue
1320
+ element_name, element_attrs, _ = element
1321
+ if element_name == "Substitute":
1322
+ glyph_mapping.append(element_attrs["value"])
1323
+ return
1324
+
1325
+ # TTX v3.1 and later.
1326
+ outGlyphs = attrs["out"].split(",") if attrs["out"] else []
1327
+ mapping[attrs["in"]] = [g.strip() for g in outGlyphs]
1328
+
1329
+ @staticmethod
1330
+ def makeSequence_(g):
1331
+ seq = Sequence()
1332
+ seq.Substitute = g
1333
+ return seq
1334
+
1335
+
1336
+ class ClassDef(FormatSwitchingBaseTable):
1337
+ def populateDefaults(self, propagator=None):
1338
+ if not hasattr(self, "classDefs"):
1339
+ self.classDefs = {}
1340
+
1341
+ def postRead(self, rawTable, font):
1342
+ classDefs = {}
1343
+
1344
+ if self.Format == 1:
1345
+ start = rawTable["StartGlyph"]
1346
+ classList = rawTable["ClassValueArray"]
1347
+ startID = font.getGlyphID(start)
1348
+ endID = startID + len(classList)
1349
+ glyphNames = font.getGlyphNameMany(range(startID, endID))
1350
+ for glyphName, cls in zip(glyphNames, classList):
1351
+ if cls:
1352
+ classDefs[glyphName] = cls
1353
+
1354
+ elif self.Format == 2:
1355
+ records = rawTable["ClassRangeRecord"]
1356
+ for rec in records:
1357
+ cls = rec.Class
1358
+ if not cls:
1359
+ continue
1360
+ start = rec.Start
1361
+ end = rec.End
1362
+ startID = font.getGlyphID(start)
1363
+ endID = font.getGlyphID(end) + 1
1364
+ glyphNames = font.getGlyphNameMany(range(startID, endID))
1365
+ for glyphName in glyphNames:
1366
+ classDefs[glyphName] = cls
1367
+ else:
1368
+ log.warning("Unknown ClassDef format: %s", self.Format)
1369
+ self.classDefs = classDefs
1370
+ del self.Format # Don't need this anymore
1371
+
1372
+ def _getClassRanges(self, font):
1373
+ classDefs = getattr(self, "classDefs", None)
1374
+ if classDefs is None:
1375
+ self.classDefs = {}
1376
+ return
1377
+ getGlyphID = font.getGlyphID
1378
+ items = []
1379
+ for glyphName, cls in classDefs.items():
1380
+ if not cls:
1381
+ continue
1382
+ items.append((getGlyphID(glyphName), glyphName, cls))
1383
+ if items:
1384
+ items.sort()
1385
+ last, lastName, lastCls = items[0]
1386
+ ranges = [[lastCls, last, lastName]]
1387
+ for glyphID, glyphName, cls in items[1:]:
1388
+ if glyphID != last + 1 or cls != lastCls:
1389
+ ranges[-1].extend([last, lastName])
1390
+ ranges.append([cls, glyphID, glyphName])
1391
+ last = glyphID
1392
+ lastName = glyphName
1393
+ lastCls = cls
1394
+ ranges[-1].extend([last, lastName])
1395
+ return ranges
1396
+
1397
+ def preWrite(self, font):
1398
+ format = 2
1399
+ rawTable = {"ClassRangeRecord": []}
1400
+ ranges = self._getClassRanges(font)
1401
+ if ranges:
1402
+ startGlyph = ranges[0][1]
1403
+ endGlyph = ranges[-1][3]
1404
+ glyphCount = endGlyph - startGlyph + 1
1405
+ if len(ranges) * 3 < glyphCount + 1:
1406
+ # Format 2 is more compact
1407
+ for i in range(len(ranges)):
1408
+ cls, start, startName, end, endName = ranges[i]
1409
+ rec = ClassRangeRecord()
1410
+ rec.Start = startName
1411
+ rec.End = endName
1412
+ rec.Class = cls
1413
+ ranges[i] = rec
1414
+ format = 2
1415
+ rawTable = {"ClassRangeRecord": ranges}
1416
+ else:
1417
+ # Format 1 is more compact
1418
+ startGlyphName = ranges[0][2]
1419
+ classes = [0] * glyphCount
1420
+ for cls, start, startName, end, endName in ranges:
1421
+ for g in range(start - startGlyph, end - startGlyph + 1):
1422
+ classes[g] = cls
1423
+ format = 1
1424
+ rawTable = {"StartGlyph": startGlyphName, "ClassValueArray": classes}
1425
+ self.Format = format
1426
+ return rawTable
1427
+
1428
+ def toXML2(self, xmlWriter, font):
1429
+ items = sorted(self.classDefs.items())
1430
+ for glyphName, cls in items:
1431
+ xmlWriter.simpletag("ClassDef", [("glyph", glyphName), ("class", cls)])
1432
+ xmlWriter.newline()
1433
+
1434
+ def fromXML(self, name, attrs, content, font):
1435
+ classDefs = getattr(self, "classDefs", None)
1436
+ if classDefs is None:
1437
+ classDefs = {}
1438
+ self.classDefs = classDefs
1439
+ classDefs[attrs["glyph"]] = int(attrs["class"])
1440
+
1441
+
1442
+ class AlternateSubst(FormatSwitchingBaseTable):
1443
+ def populateDefaults(self, propagator=None):
1444
+ if not hasattr(self, "alternates"):
1445
+ self.alternates = {}
1446
+
1447
+ def postRead(self, rawTable, font):
1448
+ alternates = {}
1449
+ if self.Format == 1:
1450
+ input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
1451
+ alts = rawTable["AlternateSet"]
1452
+ assert len(input) == len(alts)
1453
+ for inp, alt in zip(input, alts):
1454
+ alternates[inp] = alt.Alternate
1455
+ else:
1456
+ assert 0, "unknown format: %s" % self.Format
1457
+ self.alternates = alternates
1458
+ del self.Format # Don't need this anymore
1459
+
1460
+ def preWrite(self, font):
1461
+ self.Format = 1
1462
+ alternates = getattr(self, "alternates", None)
1463
+ if alternates is None:
1464
+ alternates = self.alternates = {}
1465
+ items = list(alternates.items())
1466
+ for i in range(len(items)):
1467
+ glyphName, set = items[i]
1468
+ items[i] = font.getGlyphID(glyphName), glyphName, set
1469
+ items.sort()
1470
+ cov = Coverage()
1471
+ cov.glyphs = [item[1] for item in items]
1472
+ alternates = []
1473
+ setList = [item[-1] for item in items]
1474
+ for set in setList:
1475
+ alts = AlternateSet()
1476
+ alts.Alternate = set
1477
+ alternates.append(alts)
1478
+ # a special case to deal with the fact that several hundred Adobe Japan1-5
1479
+ # CJK fonts will overflow an offset if the coverage table isn't pushed to the end.
1480
+ # Also useful in that when splitting a sub-table because of an offset overflow
1481
+ # I don't need to calculate the change in the subtable offset due to the change in the coverage table size.
1482
+ # Allows packing more rules in subtable.
1483
+ self.sortCoverageLast = 1
1484
+ return {"Coverage": cov, "AlternateSet": alternates}
1485
+
1486
+ def toXML2(self, xmlWriter, font):
1487
+ items = sorted(self.alternates.items())
1488
+ for glyphName, alternates in items:
1489
+ xmlWriter.begintag("AlternateSet", glyph=glyphName)
1490
+ xmlWriter.newline()
1491
+ for alt in alternates:
1492
+ xmlWriter.simpletag("Alternate", glyph=alt)
1493
+ xmlWriter.newline()
1494
+ xmlWriter.endtag("AlternateSet")
1495
+ xmlWriter.newline()
1496
+
1497
+ def fromXML(self, name, attrs, content, font):
1498
+ alternates = getattr(self, "alternates", None)
1499
+ if alternates is None:
1500
+ alternates = {}
1501
+ self.alternates = alternates
1502
+ glyphName = attrs["glyph"]
1503
+ set = []
1504
+ alternates[glyphName] = set
1505
+ for element in content:
1506
+ if not isinstance(element, tuple):
1507
+ continue
1508
+ name, attrs, content = element
1509
+ set.append(attrs["glyph"])
1510
+
1511
+
1512
+ class LigatureSubst(FormatSwitchingBaseTable):
1513
+ def populateDefaults(self, propagator=None):
1514
+ if not hasattr(self, "ligatures"):
1515
+ self.ligatures = {}
1516
+
1517
+ def postRead(self, rawTable, font):
1518
+ ligatures = {}
1519
+ if self.Format == 1:
1520
+ input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
1521
+ ligSets = rawTable["LigatureSet"]
1522
+ assert len(input) == len(ligSets)
1523
+ for i in range(len(input)):
1524
+ ligatures[input[i]] = ligSets[i].Ligature
1525
+ else:
1526
+ assert 0, "unknown format: %s" % self.Format
1527
+ self.ligatures = ligatures
1528
+ del self.Format # Don't need this anymore
1529
+
1530
+ @staticmethod
1531
+ def _getLigatureSortKey(components):
1532
+ # Computes a key for ordering ligatures in a GSUB Type-4 lookup.
1533
+
1534
+ # When building the OpenType lookup, we need to make sure that
1535
+ # the longest sequence of components is listed first, so we
1536
+ # use the negative length as the key for sorting.
1537
+ # Note, we no longer need to worry about deterministic order because the
1538
+ # ligature mapping `dict` remembers the insertion order, and this in
1539
+ # turn depends on the order in which the ligatures are written in the FEA.
1540
+ # Since python sort algorithm is stable, the ligatures of equal length
1541
+ # will keep the relative order in which they appear in the feature file.
1542
+ # For example, given the following ligatures (all starting with 'f' and
1543
+ # thus belonging to the same LigatureSet):
1544
+ #
1545
+ # feature liga {
1546
+ # sub f i by f_i;
1547
+ # sub f f f by f_f_f;
1548
+ # sub f f by f_f;
1549
+ # sub f f i by f_f_i;
1550
+ # } liga;
1551
+ #
1552
+ # this should sort to: f_f_f, f_f_i, f_i, f_f
1553
+ # This is also what fea-rs does, see:
1554
+ # https://github.com/adobe-type-tools/afdko/issues/1727
1555
+ # https://github.com/fonttools/fonttools/issues/3428
1556
+ # https://github.com/googlefonts/fontc/pull/680
1557
+ return -len(components)
1558
+
1559
+ def preWrite(self, font):
1560
+ self.Format = 1
1561
+ ligatures = getattr(self, "ligatures", None)
1562
+ if ligatures is None:
1563
+ ligatures = self.ligatures = {}
1564
+
1565
+ if ligatures and isinstance(next(iter(ligatures)), tuple):
1566
+ # New high-level API in v3.1 and later. Note that we just support compiling this
1567
+ # for now. We don't load to this API, and don't do XML with it.
1568
+
1569
+ # ligatures is map from components-sequence to lig-glyph
1570
+ newLigatures = dict()
1571
+ for comps in sorted(ligatures.keys(), key=self._getLigatureSortKey):
1572
+ ligature = Ligature()
1573
+ ligature.Component = comps[1:]
1574
+ ligature.CompCount = len(comps)
1575
+ ligature.LigGlyph = ligatures[comps]
1576
+ newLigatures.setdefault(comps[0], []).append(ligature)
1577
+ ligatures = newLigatures
1578
+
1579
+ items = list(ligatures.items())
1580
+ for i in range(len(items)):
1581
+ glyphName, set = items[i]
1582
+ items[i] = font.getGlyphID(glyphName), glyphName, set
1583
+ items.sort()
1584
+ cov = Coverage()
1585
+ cov.glyphs = [item[1] for item in items]
1586
+
1587
+ ligSets = []
1588
+ setList = [item[-1] for item in items]
1589
+ for set in setList:
1590
+ ligSet = LigatureSet()
1591
+ ligs = ligSet.Ligature = []
1592
+ for lig in set:
1593
+ ligs.append(lig)
1594
+ ligSets.append(ligSet)
1595
+ # Useful in that when splitting a sub-table because of an offset overflow
1596
+ # I don't need to calculate the change in subtabl offset due to the coverage table size.
1597
+ # Allows packing more rules in subtable.
1598
+ self.sortCoverageLast = 1
1599
+ return {"Coverage": cov, "LigatureSet": ligSets}
1600
+
1601
+ def toXML2(self, xmlWriter, font):
1602
+ items = sorted(self.ligatures.items())
1603
+ for glyphName, ligSets in items:
1604
+ xmlWriter.begintag("LigatureSet", glyph=glyphName)
1605
+ xmlWriter.newline()
1606
+ for lig in ligSets:
1607
+ xmlWriter.simpletag(
1608
+ "Ligature", glyph=lig.LigGlyph, components=",".join(lig.Component)
1609
+ )
1610
+ xmlWriter.newline()
1611
+ xmlWriter.endtag("LigatureSet")
1612
+ xmlWriter.newline()
1613
+
1614
+ def fromXML(self, name, attrs, content, font):
1615
+ ligatures = getattr(self, "ligatures", None)
1616
+ if ligatures is None:
1617
+ ligatures = {}
1618
+ self.ligatures = ligatures
1619
+ glyphName = attrs["glyph"]
1620
+ ligs = []
1621
+ ligatures[glyphName] = ligs
1622
+ for element in content:
1623
+ if not isinstance(element, tuple):
1624
+ continue
1625
+ name, attrs, content = element
1626
+ lig = Ligature()
1627
+ lig.LigGlyph = attrs["glyph"]
1628
+ components = attrs["components"]
1629
+ lig.Component = components.split(",") if components else []
1630
+ lig.CompCount = len(lig.Component)
1631
+ ligs.append(lig)
1632
+
1633
+
1634
+ class COLR(BaseTable):
1635
+ def decompile(self, reader, font):
1636
+ # COLRv0 is exceptional in that LayerRecordCount appears *after* the
1637
+ # LayerRecordArray it counts, but the parser logic expects Count fields
1638
+ # to always precede the arrays. Here we work around this by parsing the
1639
+ # LayerRecordCount before the rest of the table, and storing it in
1640
+ # the reader's local state.
1641
+ subReader = reader.getSubReader(offset=0)
1642
+ for conv in self.getConverters():
1643
+ if conv.name != "LayerRecordCount":
1644
+ subReader.advance(conv.staticSize)
1645
+ continue
1646
+ reader[conv.name] = conv.read(subReader, font, tableDict={})
1647
+ break
1648
+ else:
1649
+ raise AssertionError("LayerRecordCount converter not found")
1650
+ return BaseTable.decompile(self, reader, font)
1651
+
1652
+ def preWrite(self, font):
1653
+ # The writer similarly assumes Count values precede the things counted,
1654
+ # thus here we pre-initialize a CountReference; the actual count value
1655
+ # will be set to the lenght of the array by the time this is assembled.
1656
+ self.LayerRecordCount = None
1657
+ return {
1658
+ **self.__dict__,
1659
+ "LayerRecordCount": CountReference(self.__dict__, "LayerRecordCount"),
1660
+ }
1661
+
1662
+ def computeClipBoxes(self, glyphSet: "_TTGlyphSet", quantization: int = 1):
1663
+ if self.Version == 0:
1664
+ return
1665
+
1666
+ clips = {}
1667
+ for rec in self.BaseGlyphList.BaseGlyphPaintRecord:
1668
+ try:
1669
+ clipBox = rec.Paint.computeClipBox(self, glyphSet, quantization)
1670
+ except Exception as e:
1671
+ from fontTools.ttLib import TTLibError
1672
+
1673
+ raise TTLibError(
1674
+ f"Failed to compute COLR ClipBox for {rec.BaseGlyph!r}"
1675
+ ) from e
1676
+
1677
+ if clipBox is not None:
1678
+ clips[rec.BaseGlyph] = clipBox
1679
+
1680
+ hasClipList = hasattr(self, "ClipList") and self.ClipList is not None
1681
+ if not clips:
1682
+ if hasClipList:
1683
+ self.ClipList = None
1684
+ else:
1685
+ if not hasClipList:
1686
+ self.ClipList = ClipList()
1687
+ self.ClipList.Format = 1
1688
+ self.ClipList.clips = clips
1689
+
1690
+
1691
+ class LookupList(BaseTable):
1692
+ @property
1693
+ def table(self):
1694
+ for l in self.Lookup:
1695
+ for st in l.SubTable:
1696
+ if type(st).__name__.endswith("Subst"):
1697
+ return "GSUB"
1698
+ if type(st).__name__.endswith("Pos"):
1699
+ return "GPOS"
1700
+ raise ValueError
1701
+
1702
+ def toXML2(self, xmlWriter, font):
1703
+ if (
1704
+ not font
1705
+ or "Debg" not in font
1706
+ or LOOKUP_DEBUG_INFO_KEY not in font["Debg"].data
1707
+ ):
1708
+ return super().toXML2(xmlWriter, font)
1709
+ debugData = font["Debg"].data[LOOKUP_DEBUG_INFO_KEY][self.table]
1710
+ for conv in self.getConverters():
1711
+ if conv.repeat:
1712
+ value = getattr(self, conv.name, [])
1713
+ for lookupIndex, item in enumerate(value):
1714
+ if str(lookupIndex) in debugData:
1715
+ info = LookupDebugInfo(*debugData[str(lookupIndex)])
1716
+ tag = info.location
1717
+ if info.name:
1718
+ tag = f"{info.name}: {tag}"
1719
+ if info.feature:
1720
+ script, language, feature = info.feature
1721
+ tag = f"{tag} in {feature} ({script}/{language})"
1722
+ xmlWriter.comment(tag)
1723
+ xmlWriter.newline()
1724
+
1725
+ conv.xmlWrite(
1726
+ xmlWriter, font, item, conv.name, [("index", lookupIndex)]
1727
+ )
1728
+ else:
1729
+ if conv.aux and not eval(conv.aux, None, vars(self)):
1730
+ continue
1731
+ value = getattr(
1732
+ self, conv.name, None
1733
+ ) # TODO Handle defaults instead of defaulting to None!
1734
+ conv.xmlWrite(xmlWriter, font, value, conv.name, [])
1735
+
1736
+
1737
+ class BaseGlyphRecordArray(BaseTable):
1738
+ def preWrite(self, font):
1739
+ self.BaseGlyphRecord = sorted(
1740
+ self.BaseGlyphRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph)
1741
+ )
1742
+ return self.__dict__.copy()
1743
+
1744
+
1745
+ class BaseGlyphList(BaseTable):
1746
+ def preWrite(self, font):
1747
+ self.BaseGlyphPaintRecord = sorted(
1748
+ self.BaseGlyphPaintRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph)
1749
+ )
1750
+ return self.__dict__.copy()
1751
+
1752
+
1753
+ class ClipBoxFormat(IntEnum):
1754
+ Static = 1
1755
+ Variable = 2
1756
+
1757
+ def is_variable(self):
1758
+ return self is self.Variable
1759
+
1760
+ def as_variable(self):
1761
+ return self.Variable
1762
+
1763
+
1764
+ class ClipBox(getFormatSwitchingBaseTableClass("uint8")):
1765
+ formatEnum = ClipBoxFormat
1766
+
1767
+ def as_tuple(self):
1768
+ return tuple(getattr(self, conv.name) for conv in self.getConverters())
1769
+
1770
+ def __repr__(self):
1771
+ return f"{self.__class__.__name__}{self.as_tuple()}"
1772
+
1773
+
1774
+ class ClipList(getFormatSwitchingBaseTableClass("uint8")):
1775
+ def populateDefaults(self, propagator=None):
1776
+ if not hasattr(self, "clips"):
1777
+ self.clips = {}
1778
+
1779
+ def postRead(self, rawTable, font):
1780
+ clips = {}
1781
+ glyphOrder = font.getGlyphOrder()
1782
+ for i, rec in enumerate(rawTable["ClipRecord"]):
1783
+ if rec.StartGlyphID > rec.EndGlyphID:
1784
+ log.warning(
1785
+ "invalid ClipRecord[%i].StartGlyphID (%i) > "
1786
+ "EndGlyphID (%i); skipped",
1787
+ i,
1788
+ rec.StartGlyphID,
1789
+ rec.EndGlyphID,
1790
+ )
1791
+ continue
1792
+ redefinedGlyphs = []
1793
+ missingGlyphs = []
1794
+ for glyphID in range(rec.StartGlyphID, rec.EndGlyphID + 1):
1795
+ try:
1796
+ glyph = glyphOrder[glyphID]
1797
+ except IndexError:
1798
+ missingGlyphs.append(glyphID)
1799
+ continue
1800
+ if glyph not in clips:
1801
+ clips[glyph] = copy.copy(rec.ClipBox)
1802
+ else:
1803
+ redefinedGlyphs.append(glyphID)
1804
+ if redefinedGlyphs:
1805
+ log.warning(
1806
+ "ClipRecord[%i] overlaps previous records; "
1807
+ "ignoring redefined clip boxes for the "
1808
+ "following glyph ID range: [%i-%i]",
1809
+ i,
1810
+ min(redefinedGlyphs),
1811
+ max(redefinedGlyphs),
1812
+ )
1813
+ if missingGlyphs:
1814
+ log.warning(
1815
+ "ClipRecord[%i] range references missing " "glyph IDs: [%i-%i]",
1816
+ i,
1817
+ min(missingGlyphs),
1818
+ max(missingGlyphs),
1819
+ )
1820
+ self.clips = clips
1821
+
1822
+ def groups(self):
1823
+ glyphsByClip = defaultdict(list)
1824
+ uniqueClips = {}
1825
+ for glyphName, clipBox in self.clips.items():
1826
+ key = clipBox.as_tuple()
1827
+ glyphsByClip[key].append(glyphName)
1828
+ if key not in uniqueClips:
1829
+ uniqueClips[key] = clipBox
1830
+ return {
1831
+ frozenset(glyphs): uniqueClips[key] for key, glyphs in glyphsByClip.items()
1832
+ }
1833
+
1834
+ def preWrite(self, font):
1835
+ if not hasattr(self, "clips"):
1836
+ self.clips = {}
1837
+ clipBoxRanges = {}
1838
+ glyphMap = font.getReverseGlyphMap()
1839
+ for glyphs, clipBox in self.groups().items():
1840
+ glyphIDs = sorted(
1841
+ glyphMap[glyphName] for glyphName in glyphs if glyphName in glyphMap
1842
+ )
1843
+ if not glyphIDs:
1844
+ continue
1845
+ last = glyphIDs[0]
1846
+ ranges = [[last]]
1847
+ for glyphID in glyphIDs[1:]:
1848
+ if glyphID != last + 1:
1849
+ ranges[-1].append(last)
1850
+ ranges.append([glyphID])
1851
+ last = glyphID
1852
+ ranges[-1].append(last)
1853
+ for start, end in ranges:
1854
+ assert (start, end) not in clipBoxRanges
1855
+ clipBoxRanges[(start, end)] = clipBox
1856
+
1857
+ clipRecords = []
1858
+ for (start, end), clipBox in sorted(clipBoxRanges.items()):
1859
+ record = ClipRecord()
1860
+ record.StartGlyphID = start
1861
+ record.EndGlyphID = end
1862
+ record.ClipBox = clipBox
1863
+ clipRecords.append(record)
1864
+ rawTable = {
1865
+ "ClipCount": len(clipRecords),
1866
+ "ClipRecord": clipRecords,
1867
+ }
1868
+ return rawTable
1869
+
1870
+ def toXML(self, xmlWriter, font, attrs=None, name=None):
1871
+ tableName = name if name else self.__class__.__name__
1872
+ if attrs is None:
1873
+ attrs = []
1874
+ if hasattr(self, "Format"):
1875
+ attrs.append(("Format", self.Format))
1876
+ xmlWriter.begintag(tableName, attrs)
1877
+ xmlWriter.newline()
1878
+ # sort clips alphabetically to ensure deterministic XML dump
1879
+ for glyphs, clipBox in sorted(
1880
+ self.groups().items(), key=lambda item: min(item[0])
1881
+ ):
1882
+ xmlWriter.begintag("Clip")
1883
+ xmlWriter.newline()
1884
+ for glyphName in sorted(glyphs):
1885
+ xmlWriter.simpletag("Glyph", value=glyphName)
1886
+ xmlWriter.newline()
1887
+ xmlWriter.begintag("ClipBox", [("Format", clipBox.Format)])
1888
+ xmlWriter.newline()
1889
+ clipBox.toXML2(xmlWriter, font)
1890
+ xmlWriter.endtag("ClipBox")
1891
+ xmlWriter.newline()
1892
+ xmlWriter.endtag("Clip")
1893
+ xmlWriter.newline()
1894
+ xmlWriter.endtag(tableName)
1895
+ xmlWriter.newline()
1896
+
1897
+ def fromXML(self, name, attrs, content, font):
1898
+ clips = getattr(self, "clips", None)
1899
+ if clips is None:
1900
+ self.clips = clips = {}
1901
+ assert name == "Clip"
1902
+ glyphs = []
1903
+ clipBox = None
1904
+ for elem in content:
1905
+ if not isinstance(elem, tuple):
1906
+ continue
1907
+ name, attrs, content = elem
1908
+ if name == "Glyph":
1909
+ glyphs.append(attrs["value"])
1910
+ elif name == "ClipBox":
1911
+ clipBox = ClipBox()
1912
+ clipBox.Format = safeEval(attrs["Format"])
1913
+ for elem in content:
1914
+ if not isinstance(elem, tuple):
1915
+ continue
1916
+ name, attrs, content = elem
1917
+ clipBox.fromXML(name, attrs, content, font)
1918
+ if clipBox:
1919
+ for glyphName in glyphs:
1920
+ clips[glyphName] = clipBox
1921
+
1922
+
1923
+ class ExtendMode(IntEnum):
1924
+ PAD = 0
1925
+ REPEAT = 1
1926
+ REFLECT = 2
1927
+
1928
+
1929
+ # Porter-Duff modes for COLRv1 PaintComposite:
1930
+ # https://github.com/googlefonts/colr-gradients-spec/tree/off_sub_1#compositemode-enumeration
1931
+ class CompositeMode(IntEnum):
1932
+ CLEAR = 0
1933
+ SRC = 1
1934
+ DEST = 2
1935
+ SRC_OVER = 3
1936
+ DEST_OVER = 4
1937
+ SRC_IN = 5
1938
+ DEST_IN = 6
1939
+ SRC_OUT = 7
1940
+ DEST_OUT = 8
1941
+ SRC_ATOP = 9
1942
+ DEST_ATOP = 10
1943
+ XOR = 11
1944
+ PLUS = 12
1945
+ SCREEN = 13
1946
+ OVERLAY = 14
1947
+ DARKEN = 15
1948
+ LIGHTEN = 16
1949
+ COLOR_DODGE = 17
1950
+ COLOR_BURN = 18
1951
+ HARD_LIGHT = 19
1952
+ SOFT_LIGHT = 20
1953
+ DIFFERENCE = 21
1954
+ EXCLUSION = 22
1955
+ MULTIPLY = 23
1956
+ HSL_HUE = 24
1957
+ HSL_SATURATION = 25
1958
+ HSL_COLOR = 26
1959
+ HSL_LUMINOSITY = 27
1960
+
1961
+
1962
+ class PaintFormat(IntEnum):
1963
+ PaintColrLayers = 1
1964
+ PaintSolid = 2
1965
+ PaintVarSolid = 3
1966
+ PaintLinearGradient = 4
1967
+ PaintVarLinearGradient = 5
1968
+ PaintRadialGradient = 6
1969
+ PaintVarRadialGradient = 7
1970
+ PaintSweepGradient = 8
1971
+ PaintVarSweepGradient = 9
1972
+ PaintGlyph = 10
1973
+ PaintColrGlyph = 11
1974
+ PaintTransform = 12
1975
+ PaintVarTransform = 13
1976
+ PaintTranslate = 14
1977
+ PaintVarTranslate = 15
1978
+ PaintScale = 16
1979
+ PaintVarScale = 17
1980
+ PaintScaleAroundCenter = 18
1981
+ PaintVarScaleAroundCenter = 19
1982
+ PaintScaleUniform = 20
1983
+ PaintVarScaleUniform = 21
1984
+ PaintScaleUniformAroundCenter = 22
1985
+ PaintVarScaleUniformAroundCenter = 23
1986
+ PaintRotate = 24
1987
+ PaintVarRotate = 25
1988
+ PaintRotateAroundCenter = 26
1989
+ PaintVarRotateAroundCenter = 27
1990
+ PaintSkew = 28
1991
+ PaintVarSkew = 29
1992
+ PaintSkewAroundCenter = 30
1993
+ PaintVarSkewAroundCenter = 31
1994
+ PaintComposite = 32
1995
+
1996
+ def is_variable(self):
1997
+ return self.name.startswith("PaintVar")
1998
+
1999
+ def as_variable(self):
2000
+ if self.is_variable():
2001
+ return self
2002
+ try:
2003
+ return PaintFormat.__members__[f"PaintVar{self.name[5:]}"]
2004
+ except KeyError:
2005
+ return None
2006
+
2007
+
2008
+ class Paint(getFormatSwitchingBaseTableClass("uint8")):
2009
+ formatEnum = PaintFormat
2010
+
2011
+ def getFormatName(self):
2012
+ try:
2013
+ return self.formatEnum(self.Format).name
2014
+ except ValueError:
2015
+ raise NotImplementedError(f"Unknown Paint format: {self.Format}")
2016
+
2017
+ def toXML(self, xmlWriter, font, attrs=None, name=None):
2018
+ tableName = name if name else self.__class__.__name__
2019
+ if attrs is None:
2020
+ attrs = []
2021
+ attrs.append(("Format", self.Format))
2022
+ xmlWriter.begintag(tableName, attrs)
2023
+ xmlWriter.comment(self.getFormatName())
2024
+ xmlWriter.newline()
2025
+ self.toXML2(xmlWriter, font)
2026
+ xmlWriter.endtag(tableName)
2027
+ xmlWriter.newline()
2028
+
2029
+ def iterPaintSubTables(self, colr: COLR) -> Iterator[BaseTable.SubTableEntry]:
2030
+ if self.Format == PaintFormat.PaintColrLayers:
2031
+ # https://github.com/fonttools/fonttools/issues/2438: don't die when no LayerList exists
2032
+ layers = []
2033
+ if colr.LayerList is not None:
2034
+ layers = colr.LayerList.Paint
2035
+ yield from (
2036
+ BaseTable.SubTableEntry(name="Layers", value=v, index=i)
2037
+ for i, v in enumerate(
2038
+ layers[self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers]
2039
+ )
2040
+ )
2041
+ return
2042
+
2043
+ if self.Format == PaintFormat.PaintColrGlyph:
2044
+ for record in colr.BaseGlyphList.BaseGlyphPaintRecord:
2045
+ if record.BaseGlyph == self.Glyph:
2046
+ yield BaseTable.SubTableEntry(name="BaseGlyph", value=record.Paint)
2047
+ return
2048
+ else:
2049
+ raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphList")
2050
+
2051
+ for conv in self.getConverters():
2052
+ if conv.tableClass is not None and issubclass(conv.tableClass, type(self)):
2053
+ value = getattr(self, conv.name)
2054
+ yield BaseTable.SubTableEntry(name=conv.name, value=value)
2055
+
2056
+ def getChildren(self, colr) -> List["Paint"]:
2057
+ # this is kept for backward compatibility (e.g. it's used by the subsetter)
2058
+ return [p.value for p in self.iterPaintSubTables(colr)]
2059
+
2060
+ def traverse(self, colr: COLR, callback):
2061
+ """Depth-first traversal of graph rooted at self, callback on each node."""
2062
+ if not callable(callback):
2063
+ raise TypeError("callback must be callable")
2064
+
2065
+ for path in dfs_base_table(
2066
+ self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr)
2067
+ ):
2068
+ paint = path[-1].value
2069
+ callback(paint)
2070
+
2071
+ def getTransform(self) -> Transform:
2072
+ if self.Format == PaintFormat.PaintTransform:
2073
+ t = self.Transform
2074
+ return Transform(t.xx, t.yx, t.xy, t.yy, t.dx, t.dy)
2075
+ elif self.Format == PaintFormat.PaintTranslate:
2076
+ return Identity.translate(self.dx, self.dy)
2077
+ elif self.Format == PaintFormat.PaintScale:
2078
+ return Identity.scale(self.scaleX, self.scaleY)
2079
+ elif self.Format == PaintFormat.PaintScaleAroundCenter:
2080
+ return (
2081
+ Identity.translate(self.centerX, self.centerY)
2082
+ .scale(self.scaleX, self.scaleY)
2083
+ .translate(-self.centerX, -self.centerY)
2084
+ )
2085
+ elif self.Format == PaintFormat.PaintScaleUniform:
2086
+ return Identity.scale(self.scale)
2087
+ elif self.Format == PaintFormat.PaintScaleUniformAroundCenter:
2088
+ return (
2089
+ Identity.translate(self.centerX, self.centerY)
2090
+ .scale(self.scale)
2091
+ .translate(-self.centerX, -self.centerY)
2092
+ )
2093
+ elif self.Format == PaintFormat.PaintRotate:
2094
+ return Identity.rotate(radians(self.angle))
2095
+ elif self.Format == PaintFormat.PaintRotateAroundCenter:
2096
+ return (
2097
+ Identity.translate(self.centerX, self.centerY)
2098
+ .rotate(radians(self.angle))
2099
+ .translate(-self.centerX, -self.centerY)
2100
+ )
2101
+ elif self.Format == PaintFormat.PaintSkew:
2102
+ return Identity.skew(radians(-self.xSkewAngle), radians(self.ySkewAngle))
2103
+ elif self.Format == PaintFormat.PaintSkewAroundCenter:
2104
+ return (
2105
+ Identity.translate(self.centerX, self.centerY)
2106
+ .skew(radians(-self.xSkewAngle), radians(self.ySkewAngle))
2107
+ .translate(-self.centerX, -self.centerY)
2108
+ )
2109
+ if PaintFormat(self.Format).is_variable():
2110
+ raise NotImplementedError(f"Variable Paints not supported: {self.Format}")
2111
+
2112
+ return Identity
2113
+
2114
+ def computeClipBox(
2115
+ self, colr: COLR, glyphSet: "_TTGlyphSet", quantization: int = 1
2116
+ ) -> Optional[ClipBox]:
2117
+ pen = ControlBoundsPen(glyphSet)
2118
+ for path in dfs_base_table(
2119
+ self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr)
2120
+ ):
2121
+ paint = path[-1].value
2122
+ if paint.Format == PaintFormat.PaintGlyph:
2123
+ transformation = reduce(
2124
+ Transform.transform,
2125
+ (st.value.getTransform() for st in path),
2126
+ Identity,
2127
+ )
2128
+ glyphSet[paint.Glyph].draw(TransformPen(pen, transformation))
2129
+
2130
+ if pen.bounds is None:
2131
+ return None
2132
+
2133
+ cb = ClipBox()
2134
+ cb.Format = int(ClipBoxFormat.Static)
2135
+ cb.xMin, cb.yMin, cb.xMax, cb.yMax = quantizeRect(pen.bounds, quantization)
2136
+ return cb
2137
+
2138
+
2139
+ # For each subtable format there is a class. However, we don't really distinguish
2140
+ # between "field name" and "format name": often these are the same. Yet there's
2141
+ # a whole bunch of fields with different names. The following dict is a mapping
2142
+ # from "format name" to "field name". _buildClasses() uses this to create a
2143
+ # subclass for each alternate field name.
2144
+ #
2145
+ _equivalents = {
2146
+ "MarkArray": ("Mark1Array",),
2147
+ "LangSys": ("DefaultLangSys",),
2148
+ "Coverage": (
2149
+ "MarkCoverage",
2150
+ "BaseCoverage",
2151
+ "LigatureCoverage",
2152
+ "Mark1Coverage",
2153
+ "Mark2Coverage",
2154
+ "BacktrackCoverage",
2155
+ "InputCoverage",
2156
+ "LookAheadCoverage",
2157
+ "VertGlyphCoverage",
2158
+ "HorizGlyphCoverage",
2159
+ "TopAccentCoverage",
2160
+ "ExtendedShapeCoverage",
2161
+ "MathKernCoverage",
2162
+ ),
2163
+ "ClassDef": (
2164
+ "ClassDef1",
2165
+ "ClassDef2",
2166
+ "BacktrackClassDef",
2167
+ "InputClassDef",
2168
+ "LookAheadClassDef",
2169
+ "GlyphClassDef",
2170
+ "MarkAttachClassDef",
2171
+ ),
2172
+ "Anchor": (
2173
+ "EntryAnchor",
2174
+ "ExitAnchor",
2175
+ "BaseAnchor",
2176
+ "LigatureAnchor",
2177
+ "Mark2Anchor",
2178
+ "MarkAnchor",
2179
+ ),
2180
+ "Device": (
2181
+ "XPlaDevice",
2182
+ "YPlaDevice",
2183
+ "XAdvDevice",
2184
+ "YAdvDevice",
2185
+ "XDeviceTable",
2186
+ "YDeviceTable",
2187
+ "DeviceTable",
2188
+ ),
2189
+ "Axis": (
2190
+ "HorizAxis",
2191
+ "VertAxis",
2192
+ ),
2193
+ "MinMax": ("DefaultMinMax",),
2194
+ "BaseCoord": (
2195
+ "MinCoord",
2196
+ "MaxCoord",
2197
+ ),
2198
+ "JstfLangSys": ("DefJstfLangSys",),
2199
+ "JstfGSUBModList": (
2200
+ "ShrinkageEnableGSUB",
2201
+ "ShrinkageDisableGSUB",
2202
+ "ExtensionEnableGSUB",
2203
+ "ExtensionDisableGSUB",
2204
+ ),
2205
+ "JstfGPOSModList": (
2206
+ "ShrinkageEnableGPOS",
2207
+ "ShrinkageDisableGPOS",
2208
+ "ExtensionEnableGPOS",
2209
+ "ExtensionDisableGPOS",
2210
+ ),
2211
+ "JstfMax": (
2212
+ "ShrinkageJstfMax",
2213
+ "ExtensionJstfMax",
2214
+ ),
2215
+ "MathKern": (
2216
+ "TopRightMathKern",
2217
+ "TopLeftMathKern",
2218
+ "BottomRightMathKern",
2219
+ "BottomLeftMathKern",
2220
+ ),
2221
+ "MathGlyphConstruction": ("VertGlyphConstruction", "HorizGlyphConstruction"),
2222
+ }
2223
+
2224
+ #
2225
+ # OverFlow logic, to automatically create ExtensionLookups
2226
+ # XXX This should probably move to otBase.py
2227
+ #
2228
+
2229
+
2230
+ def fixLookupOverFlows(ttf, overflowRecord):
2231
+ """Either the offset from the LookupList to a lookup overflowed, or
2232
+ an offset from a lookup to a subtable overflowed.
2233
+
2234
+ The table layout is::
2235
+
2236
+ GPSO/GUSB
2237
+ Script List
2238
+ Feature List
2239
+ LookUpList
2240
+ Lookup[0] and contents
2241
+ SubTable offset list
2242
+ SubTable[0] and contents
2243
+ ...
2244
+ SubTable[n] and contents
2245
+ ...
2246
+ Lookup[n] and contents
2247
+ SubTable offset list
2248
+ SubTable[0] and contents
2249
+ ...
2250
+ SubTable[n] and contents
2251
+
2252
+ If the offset to a lookup overflowed (SubTableIndex is None)
2253
+ we must promote the *previous* lookup to an Extension type.
2254
+
2255
+ If the offset from a lookup to subtable overflowed, then we must promote it
2256
+ to an Extension Lookup type.
2257
+ """
2258
+ ok = 0
2259
+ lookupIndex = overflowRecord.LookupListIndex
2260
+ if overflowRecord.SubTableIndex is None:
2261
+ lookupIndex = lookupIndex - 1
2262
+ if lookupIndex < 0:
2263
+ return ok
2264
+ if overflowRecord.tableType == "GSUB":
2265
+ extType = 7
2266
+ elif overflowRecord.tableType == "GPOS":
2267
+ extType = 9
2268
+
2269
+ lookups = ttf[overflowRecord.tableType].table.LookupList.Lookup
2270
+ lookup = lookups[lookupIndex]
2271
+ # If the previous lookup is an extType, look further back. Very unlikely, but possible.
2272
+ while lookup.SubTable[0].__class__.LookupType == extType:
2273
+ lookupIndex = lookupIndex - 1
2274
+ if lookupIndex < 0:
2275
+ return ok
2276
+ lookup = lookups[lookupIndex]
2277
+
2278
+ for lookupIndex in range(lookupIndex, len(lookups)):
2279
+ lookup = lookups[lookupIndex]
2280
+ if lookup.LookupType != extType:
2281
+ lookup.LookupType = extType
2282
+ for si in range(len(lookup.SubTable)):
2283
+ subTable = lookup.SubTable[si]
2284
+ extSubTableClass = lookupTypes[overflowRecord.tableType][extType]
2285
+ extSubTable = extSubTableClass()
2286
+ extSubTable.Format = 1
2287
+ extSubTable.ExtSubTable = subTable
2288
+ lookup.SubTable[si] = extSubTable
2289
+ ok = 1
2290
+ return ok
2291
+
2292
+
2293
+ def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord):
2294
+ ok = 1
2295
+ oldMapping = sorted(oldSubTable.mapping.items())
2296
+ oldLen = len(oldMapping)
2297
+
2298
+ if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
2299
+ # Coverage table is written last. Overflow is to or within the
2300
+ # the coverage table. We will just cut the subtable in half.
2301
+ newLen = oldLen // 2
2302
+
2303
+ elif overflowRecord.itemName == "Sequence":
2304
+ # We just need to back up by two items from the overflowed
2305
+ # Sequence index to make sure the offset to the Coverage table
2306
+ # doesn't overflow.
2307
+ newLen = overflowRecord.itemIndex - 1
2308
+
2309
+ newSubTable.mapping = {}
2310
+ for i in range(newLen, oldLen):
2311
+ item = oldMapping[i]
2312
+ key = item[0]
2313
+ newSubTable.mapping[key] = item[1]
2314
+ del oldSubTable.mapping[key]
2315
+
2316
+ return ok
2317
+
2318
+
2319
+ def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord):
2320
+ ok = 1
2321
+ if hasattr(oldSubTable, "sortCoverageLast"):
2322
+ newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast
2323
+
2324
+ oldAlts = sorted(oldSubTable.alternates.items())
2325
+ oldLen = len(oldAlts)
2326
+
2327
+ if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
2328
+ # Coverage table is written last. overflow is to or within the
2329
+ # the coverage table. We will just cut the subtable in half.
2330
+ newLen = oldLen // 2
2331
+
2332
+ elif overflowRecord.itemName == "AlternateSet":
2333
+ # We just need to back up by two items
2334
+ # from the overflowed AlternateSet index to make sure the offset
2335
+ # to the Coverage table doesn't overflow.
2336
+ newLen = overflowRecord.itemIndex - 1
2337
+
2338
+ newSubTable.alternates = {}
2339
+ for i in range(newLen, oldLen):
2340
+ item = oldAlts[i]
2341
+ key = item[0]
2342
+ newSubTable.alternates[key] = item[1]
2343
+ del oldSubTable.alternates[key]
2344
+
2345
+ return ok
2346
+
2347
+
2348
+ def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord):
2349
+ ok = 1
2350
+ oldLigs = sorted(oldSubTable.ligatures.items())
2351
+ oldLen = len(oldLigs)
2352
+
2353
+ if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
2354
+ # Coverage table is written last. overflow is to or within the
2355
+ # the coverage table. We will just cut the subtable in half.
2356
+ newLen = oldLen // 2
2357
+
2358
+ elif overflowRecord.itemName == "LigatureSet":
2359
+ # We just need to back up by two items
2360
+ # from the overflowed AlternateSet index to make sure the offset
2361
+ # to the Coverage table doesn't overflow.
2362
+ newLen = overflowRecord.itemIndex - 1
2363
+
2364
+ newSubTable.ligatures = {}
2365
+ for i in range(newLen, oldLen):
2366
+ item = oldLigs[i]
2367
+ key = item[0]
2368
+ newSubTable.ligatures[key] = item[1]
2369
+ del oldSubTable.ligatures[key]
2370
+
2371
+ return ok
2372
+
2373
+
2374
+ def splitPairPos(oldSubTable, newSubTable, overflowRecord):
2375
+ st = oldSubTable
2376
+ ok = False
2377
+ newSubTable.Format = oldSubTable.Format
2378
+ if oldSubTable.Format == 1 and len(oldSubTable.PairSet) > 1:
2379
+ for name in "ValueFormat1", "ValueFormat2":
2380
+ setattr(newSubTable, name, getattr(oldSubTable, name))
2381
+
2382
+ # Move top half of coverage to new subtable
2383
+
2384
+ newSubTable.Coverage = oldSubTable.Coverage.__class__()
2385
+
2386
+ coverage = oldSubTable.Coverage.glyphs
2387
+ records = oldSubTable.PairSet
2388
+
2389
+ oldCount = len(oldSubTable.PairSet) // 2
2390
+
2391
+ oldSubTable.Coverage.glyphs = coverage[:oldCount]
2392
+ oldSubTable.PairSet = records[:oldCount]
2393
+
2394
+ newSubTable.Coverage.glyphs = coverage[oldCount:]
2395
+ newSubTable.PairSet = records[oldCount:]
2396
+
2397
+ oldSubTable.PairSetCount = len(oldSubTable.PairSet)
2398
+ newSubTable.PairSetCount = len(newSubTable.PairSet)
2399
+
2400
+ ok = True
2401
+
2402
+ elif oldSubTable.Format == 2 and len(oldSubTable.Class1Record) > 1:
2403
+ if not hasattr(oldSubTable, "Class2Count"):
2404
+ oldSubTable.Class2Count = len(oldSubTable.Class1Record[0].Class2Record)
2405
+ for name in "Class2Count", "ClassDef2", "ValueFormat1", "ValueFormat2":
2406
+ setattr(newSubTable, name, getattr(oldSubTable, name))
2407
+
2408
+ # The two subtables will still have the same ClassDef2 and the table
2409
+ # sharing will still cause the sharing to overflow. As such, disable
2410
+ # sharing on the one that is serialized second (that's oldSubTable).
2411
+ oldSubTable.DontShare = True
2412
+
2413
+ # Move top half of class numbers to new subtable
2414
+
2415
+ newSubTable.Coverage = oldSubTable.Coverage.__class__()
2416
+ newSubTable.ClassDef1 = oldSubTable.ClassDef1.__class__()
2417
+
2418
+ coverage = oldSubTable.Coverage.glyphs
2419
+ classDefs = oldSubTable.ClassDef1.classDefs
2420
+ records = oldSubTable.Class1Record
2421
+
2422
+ oldCount = len(oldSubTable.Class1Record) // 2
2423
+ newGlyphs = set(k for k, v in classDefs.items() if v >= oldCount)
2424
+
2425
+ oldSubTable.Coverage.glyphs = [g for g in coverage if g not in newGlyphs]
2426
+ oldSubTable.ClassDef1.classDefs = {
2427
+ k: v for k, v in classDefs.items() if v < oldCount
2428
+ }
2429
+ oldSubTable.Class1Record = records[:oldCount]
2430
+
2431
+ newSubTable.Coverage.glyphs = [g for g in coverage if g in newGlyphs]
2432
+ newSubTable.ClassDef1.classDefs = {
2433
+ k: (v - oldCount) for k, v in classDefs.items() if v > oldCount
2434
+ }
2435
+ newSubTable.Class1Record = records[oldCount:]
2436
+
2437
+ oldSubTable.Class1Count = len(oldSubTable.Class1Record)
2438
+ newSubTable.Class1Count = len(newSubTable.Class1Record)
2439
+
2440
+ ok = True
2441
+
2442
+ return ok
2443
+
2444
+
2445
+ def splitMarkBasePos(oldSubTable, newSubTable, overflowRecord):
2446
+ # split half of the mark classes to the new subtable
2447
+ classCount = oldSubTable.ClassCount
2448
+ if classCount < 2:
2449
+ # oh well, not much left to split...
2450
+ return False
2451
+
2452
+ oldClassCount = classCount // 2
2453
+ newClassCount = classCount - oldClassCount
2454
+
2455
+ oldMarkCoverage, oldMarkRecords = [], []
2456
+ newMarkCoverage, newMarkRecords = [], []
2457
+ for glyphName, markRecord in zip(
2458
+ oldSubTable.MarkCoverage.glyphs, oldSubTable.MarkArray.MarkRecord
2459
+ ):
2460
+ if markRecord.Class < oldClassCount:
2461
+ oldMarkCoverage.append(glyphName)
2462
+ oldMarkRecords.append(markRecord)
2463
+ else:
2464
+ markRecord.Class -= oldClassCount
2465
+ newMarkCoverage.append(glyphName)
2466
+ newMarkRecords.append(markRecord)
2467
+
2468
+ oldBaseRecords, newBaseRecords = [], []
2469
+ for rec in oldSubTable.BaseArray.BaseRecord:
2470
+ oldBaseRecord, newBaseRecord = rec.__class__(), rec.__class__()
2471
+ oldBaseRecord.BaseAnchor = rec.BaseAnchor[:oldClassCount]
2472
+ newBaseRecord.BaseAnchor = rec.BaseAnchor[oldClassCount:]
2473
+ oldBaseRecords.append(oldBaseRecord)
2474
+ newBaseRecords.append(newBaseRecord)
2475
+
2476
+ newSubTable.Format = oldSubTable.Format
2477
+
2478
+ oldSubTable.MarkCoverage.glyphs = oldMarkCoverage
2479
+ newSubTable.MarkCoverage = oldSubTable.MarkCoverage.__class__()
2480
+ newSubTable.MarkCoverage.glyphs = newMarkCoverage
2481
+
2482
+ # share the same BaseCoverage in both halves
2483
+ newSubTable.BaseCoverage = oldSubTable.BaseCoverage
2484
+
2485
+ oldSubTable.ClassCount = oldClassCount
2486
+ newSubTable.ClassCount = newClassCount
2487
+
2488
+ oldSubTable.MarkArray.MarkRecord = oldMarkRecords
2489
+ newSubTable.MarkArray = oldSubTable.MarkArray.__class__()
2490
+ newSubTable.MarkArray.MarkRecord = newMarkRecords
2491
+
2492
+ oldSubTable.MarkArray.MarkCount = len(oldMarkRecords)
2493
+ newSubTable.MarkArray.MarkCount = len(newMarkRecords)
2494
+
2495
+ oldSubTable.BaseArray.BaseRecord = oldBaseRecords
2496
+ newSubTable.BaseArray = oldSubTable.BaseArray.__class__()
2497
+ newSubTable.BaseArray.BaseRecord = newBaseRecords
2498
+
2499
+ oldSubTable.BaseArray.BaseCount = len(oldBaseRecords)
2500
+ newSubTable.BaseArray.BaseCount = len(newBaseRecords)
2501
+
2502
+ return True
2503
+
2504
+
2505
+ splitTable = {
2506
+ "GSUB": {
2507
+ # 1: splitSingleSubst,
2508
+ 2: splitMultipleSubst,
2509
+ 3: splitAlternateSubst,
2510
+ 4: splitLigatureSubst,
2511
+ # 5: splitContextSubst,
2512
+ # 6: splitChainContextSubst,
2513
+ # 7: splitExtensionSubst,
2514
+ # 8: splitReverseChainSingleSubst,
2515
+ },
2516
+ "GPOS": {
2517
+ # 1: splitSinglePos,
2518
+ 2: splitPairPos,
2519
+ # 3: splitCursivePos,
2520
+ 4: splitMarkBasePos,
2521
+ # 5: splitMarkLigPos,
2522
+ # 6: splitMarkMarkPos,
2523
+ # 7: splitContextPos,
2524
+ # 8: splitChainContextPos,
2525
+ # 9: splitExtensionPos,
2526
+ },
2527
+ }
2528
+
2529
+
2530
+ def fixSubTableOverFlows(ttf, overflowRecord):
2531
+ """
2532
+ An offset has overflowed within a sub-table. We need to divide this subtable into smaller parts.
2533
+ """
2534
+ table = ttf[overflowRecord.tableType].table
2535
+ lookup = table.LookupList.Lookup[overflowRecord.LookupListIndex]
2536
+ subIndex = overflowRecord.SubTableIndex
2537
+ subtable = lookup.SubTable[subIndex]
2538
+
2539
+ # First, try not sharing anything for this subtable...
2540
+ if not hasattr(subtable, "DontShare"):
2541
+ subtable.DontShare = True
2542
+ return True
2543
+
2544
+ if hasattr(subtable, "ExtSubTable"):
2545
+ # We split the subtable of the Extension table, and add a new Extension table
2546
+ # to contain the new subtable.
2547
+
2548
+ subTableType = subtable.ExtSubTable.__class__.LookupType
2549
+ extSubTable = subtable
2550
+ subtable = extSubTable.ExtSubTable
2551
+ newExtSubTableClass = lookupTypes[overflowRecord.tableType][
2552
+ extSubTable.__class__.LookupType
2553
+ ]
2554
+ newExtSubTable = newExtSubTableClass()
2555
+ newExtSubTable.Format = extSubTable.Format
2556
+ toInsert = newExtSubTable
2557
+
2558
+ newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
2559
+ newSubTable = newSubTableClass()
2560
+ newExtSubTable.ExtSubTable = newSubTable
2561
+ else:
2562
+ subTableType = subtable.__class__.LookupType
2563
+ newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
2564
+ newSubTable = newSubTableClass()
2565
+ toInsert = newSubTable
2566
+
2567
+ if hasattr(lookup, "SubTableCount"): # may not be defined yet.
2568
+ lookup.SubTableCount = lookup.SubTableCount + 1
2569
+
2570
+ try:
2571
+ splitFunc = splitTable[overflowRecord.tableType][subTableType]
2572
+ except KeyError:
2573
+ log.error(
2574
+ "Don't know how to split %s lookup type %s",
2575
+ overflowRecord.tableType,
2576
+ subTableType,
2577
+ )
2578
+ return False
2579
+
2580
+ ok = splitFunc(subtable, newSubTable, overflowRecord)
2581
+ if ok:
2582
+ lookup.SubTable.insert(subIndex + 1, toInsert)
2583
+ return ok
2584
+
2585
+
2586
+ # End of OverFlow logic
2587
+
2588
+
2589
+ def _buildClasses():
2590
+ import re
2591
+ from .otData import otData
2592
+
2593
+ formatPat = re.compile(r"([A-Za-z0-9]+)Format(\d+)$")
2594
+ namespace = globals()
2595
+
2596
+ # populate module with classes
2597
+ for name, table in otData:
2598
+ baseClass = BaseTable
2599
+ m = formatPat.match(name)
2600
+ if m:
2601
+ # XxxFormatN subtable, we only add the "base" table
2602
+ name = m.group(1)
2603
+ # the first row of a format-switching otData table describes the Format;
2604
+ # the first column defines the type of the Format field.
2605
+ # Currently this can be either 'uint16' or 'uint8'.
2606
+ formatType = table[0][0]
2607
+ baseClass = getFormatSwitchingBaseTableClass(formatType)
2608
+ if name not in namespace:
2609
+ # the class doesn't exist yet, so the base implementation is used.
2610
+ cls = type(name, (baseClass,), {})
2611
+ if name in ("GSUB", "GPOS"):
2612
+ cls.DontShare = True
2613
+ namespace[name] = cls
2614
+
2615
+ # link Var{Table} <-> {Table} (e.g. ColorStop <-> VarColorStop, etc.)
2616
+ for name, _ in otData:
2617
+ if name.startswith("Var") and len(name) > 3 and name[3:] in namespace:
2618
+ varType = namespace[name]
2619
+ noVarType = namespace[name[3:]]
2620
+ varType.NoVarType = noVarType
2621
+ noVarType.VarType = varType
2622
+
2623
+ for base, alts in _equivalents.items():
2624
+ base = namespace[base]
2625
+ for alt in alts:
2626
+ namespace[alt] = base
2627
+
2628
+ global lookupTypes
2629
+ lookupTypes = {
2630
+ "GSUB": {
2631
+ 1: SingleSubst,
2632
+ 2: MultipleSubst,
2633
+ 3: AlternateSubst,
2634
+ 4: LigatureSubst,
2635
+ 5: ContextSubst,
2636
+ 6: ChainContextSubst,
2637
+ 7: ExtensionSubst,
2638
+ 8: ReverseChainSingleSubst,
2639
+ },
2640
+ "GPOS": {
2641
+ 1: SinglePos,
2642
+ 2: PairPos,
2643
+ 3: CursivePos,
2644
+ 4: MarkBasePos,
2645
+ 5: MarkLigPos,
2646
+ 6: MarkMarkPos,
2647
+ 7: ContextPos,
2648
+ 8: ChainContextPos,
2649
+ 9: ExtensionPos,
2650
+ },
2651
+ "mort": {
2652
+ 4: NoncontextualMorph,
2653
+ },
2654
+ "morx": {
2655
+ 0: RearrangementMorph,
2656
+ 1: ContextualMorph,
2657
+ 2: LigatureMorph,
2658
+ # 3: Reserved,
2659
+ 4: NoncontextualMorph,
2660
+ 5: InsertionMorph,
2661
+ },
2662
+ }
2663
+ lookupTypes["JSTF"] = lookupTypes["GPOS"] # JSTF contains GPOS
2664
+ for lookupEnum in lookupTypes.values():
2665
+ for enum, cls in lookupEnum.items():
2666
+ cls.LookupType = enum
2667
+
2668
+ global featureParamTypes
2669
+ featureParamTypes = {
2670
+ "size": FeatureParamsSize,
2671
+ }
2672
+ for i in range(1, 20 + 1):
2673
+ featureParamTypes["ss%02d" % i] = FeatureParamsStylisticSet
2674
+ for i in range(1, 99 + 1):
2675
+ featureParamTypes["cv%02d" % i] = FeatureParamsCharacterVariants
2676
+
2677
+ # add converters to classes
2678
+ from .otConverters import buildConverters
2679
+
2680
+ for name, table in otData:
2681
+ m = formatPat.match(name)
2682
+ if m:
2683
+ # XxxFormatN subtable, add converter to "base" table
2684
+ name, format = m.groups()
2685
+ format = int(format)
2686
+ cls = namespace[name]
2687
+ if not hasattr(cls, "converters"):
2688
+ cls.converters = {}
2689
+ cls.convertersByName = {}
2690
+ converters, convertersByName = buildConverters(table[1:], namespace)
2691
+ cls.converters[format] = converters
2692
+ cls.convertersByName[format] = convertersByName
2693
+ # XXX Add staticSize?
2694
+ else:
2695
+ cls = namespace[name]
2696
+ cls.converters, cls.convertersByName = buildConverters(table, namespace)
2697
+ # XXX Add staticSize?
2698
+
2699
+
2700
+ _buildClasses()
2701
+
2702
+
2703
+ def _getGlyphsFromCoverageTable(coverage):
2704
+ if coverage is None:
2705
+ # empty coverage table
2706
+ return []
2707
+ else:
2708
+ return coverage.glyphs