fonttools 4.55.6__cp311-cp311-musllinux_1_2_x86_64.whl

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

Potentially problematic release.


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

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