fonttools 4.60.2__cp311-cp311-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. fontTools/__init__.py +8 -0
  2. fontTools/__main__.py +35 -0
  3. fontTools/afmLib.py +439 -0
  4. fontTools/agl.py +5233 -0
  5. fontTools/annotations.py +30 -0
  6. fontTools/cffLib/CFF2ToCFF.py +258 -0
  7. fontTools/cffLib/CFFToCFF2.py +305 -0
  8. fontTools/cffLib/__init__.py +3694 -0
  9. fontTools/cffLib/specializer.py +927 -0
  10. fontTools/cffLib/transforms.py +495 -0
  11. fontTools/cffLib/width.py +210 -0
  12. fontTools/colorLib/__init__.py +0 -0
  13. fontTools/colorLib/builder.py +664 -0
  14. fontTools/colorLib/errors.py +2 -0
  15. fontTools/colorLib/geometry.py +143 -0
  16. fontTools/colorLib/table_builder.py +223 -0
  17. fontTools/colorLib/unbuilder.py +81 -0
  18. fontTools/config/__init__.py +90 -0
  19. fontTools/cu2qu/__init__.py +15 -0
  20. fontTools/cu2qu/__main__.py +6 -0
  21. fontTools/cu2qu/benchmark.py +54 -0
  22. fontTools/cu2qu/cli.py +198 -0
  23. fontTools/cu2qu/cu2qu.c +15817 -0
  24. fontTools/cu2qu/cu2qu.cp311-win32.pyd +0 -0
  25. fontTools/cu2qu/cu2qu.py +563 -0
  26. fontTools/cu2qu/errors.py +77 -0
  27. fontTools/cu2qu/ufo.py +363 -0
  28. fontTools/designspaceLib/__init__.py +3343 -0
  29. fontTools/designspaceLib/__main__.py +6 -0
  30. fontTools/designspaceLib/split.py +475 -0
  31. fontTools/designspaceLib/statNames.py +260 -0
  32. fontTools/designspaceLib/types.py +147 -0
  33. fontTools/encodings/MacRoman.py +258 -0
  34. fontTools/encodings/StandardEncoding.py +258 -0
  35. fontTools/encodings/__init__.py +1 -0
  36. fontTools/encodings/codecs.py +135 -0
  37. fontTools/feaLib/__init__.py +4 -0
  38. fontTools/feaLib/__main__.py +78 -0
  39. fontTools/feaLib/ast.py +2143 -0
  40. fontTools/feaLib/builder.py +1814 -0
  41. fontTools/feaLib/error.py +22 -0
  42. fontTools/feaLib/lexer.c +17029 -0
  43. fontTools/feaLib/lexer.cp311-win32.pyd +0 -0
  44. fontTools/feaLib/lexer.py +287 -0
  45. fontTools/feaLib/location.py +12 -0
  46. fontTools/feaLib/lookupDebugInfo.py +12 -0
  47. fontTools/feaLib/parser.py +2394 -0
  48. fontTools/feaLib/variableScalar.py +118 -0
  49. fontTools/fontBuilder.py +1014 -0
  50. fontTools/help.py +36 -0
  51. fontTools/merge/__init__.py +248 -0
  52. fontTools/merge/__main__.py +6 -0
  53. fontTools/merge/base.py +81 -0
  54. fontTools/merge/cmap.py +173 -0
  55. fontTools/merge/layout.py +526 -0
  56. fontTools/merge/options.py +85 -0
  57. fontTools/merge/tables.py +352 -0
  58. fontTools/merge/unicode.py +78 -0
  59. fontTools/merge/util.py +143 -0
  60. fontTools/misc/__init__.py +1 -0
  61. fontTools/misc/arrayTools.py +424 -0
  62. fontTools/misc/bezierTools.c +39731 -0
  63. fontTools/misc/bezierTools.cp311-win32.pyd +0 -0
  64. fontTools/misc/bezierTools.py +1500 -0
  65. fontTools/misc/classifyTools.py +170 -0
  66. fontTools/misc/cliTools.py +53 -0
  67. fontTools/misc/configTools.py +349 -0
  68. fontTools/misc/cython.py +27 -0
  69. fontTools/misc/dictTools.py +83 -0
  70. fontTools/misc/eexec.py +119 -0
  71. fontTools/misc/encodingTools.py +72 -0
  72. fontTools/misc/enumTools.py +23 -0
  73. fontTools/misc/etree.py +456 -0
  74. fontTools/misc/filenames.py +245 -0
  75. fontTools/misc/filesystem/__init__.py +68 -0
  76. fontTools/misc/filesystem/_base.py +134 -0
  77. fontTools/misc/filesystem/_copy.py +45 -0
  78. fontTools/misc/filesystem/_errors.py +54 -0
  79. fontTools/misc/filesystem/_info.py +75 -0
  80. fontTools/misc/filesystem/_osfs.py +164 -0
  81. fontTools/misc/filesystem/_path.py +67 -0
  82. fontTools/misc/filesystem/_subfs.py +92 -0
  83. fontTools/misc/filesystem/_tempfs.py +34 -0
  84. fontTools/misc/filesystem/_tools.py +34 -0
  85. fontTools/misc/filesystem/_walk.py +55 -0
  86. fontTools/misc/filesystem/_zipfs.py +204 -0
  87. fontTools/misc/fixedTools.py +253 -0
  88. fontTools/misc/intTools.py +25 -0
  89. fontTools/misc/iterTools.py +12 -0
  90. fontTools/misc/lazyTools.py +42 -0
  91. fontTools/misc/loggingTools.py +543 -0
  92. fontTools/misc/macCreatorType.py +56 -0
  93. fontTools/misc/macRes.py +261 -0
  94. fontTools/misc/plistlib/__init__.py +681 -0
  95. fontTools/misc/plistlib/py.typed +0 -0
  96. fontTools/misc/psCharStrings.py +1511 -0
  97. fontTools/misc/psLib.py +398 -0
  98. fontTools/misc/psOperators.py +572 -0
  99. fontTools/misc/py23.py +96 -0
  100. fontTools/misc/roundTools.py +110 -0
  101. fontTools/misc/sstruct.py +227 -0
  102. fontTools/misc/symfont.py +242 -0
  103. fontTools/misc/testTools.py +233 -0
  104. fontTools/misc/textTools.py +156 -0
  105. fontTools/misc/timeTools.py +88 -0
  106. fontTools/misc/transform.py +516 -0
  107. fontTools/misc/treeTools.py +45 -0
  108. fontTools/misc/vector.py +147 -0
  109. fontTools/misc/visitor.py +158 -0
  110. fontTools/misc/xmlReader.py +188 -0
  111. fontTools/misc/xmlWriter.py +231 -0
  112. fontTools/mtiLib/__init__.py +1400 -0
  113. fontTools/mtiLib/__main__.py +5 -0
  114. fontTools/otlLib/__init__.py +1 -0
  115. fontTools/otlLib/builder.py +3465 -0
  116. fontTools/otlLib/error.py +11 -0
  117. fontTools/otlLib/maxContextCalc.py +96 -0
  118. fontTools/otlLib/optimize/__init__.py +53 -0
  119. fontTools/otlLib/optimize/__main__.py +6 -0
  120. fontTools/otlLib/optimize/gpos.py +439 -0
  121. fontTools/pens/__init__.py +1 -0
  122. fontTools/pens/areaPen.py +52 -0
  123. fontTools/pens/basePen.py +475 -0
  124. fontTools/pens/boundsPen.py +98 -0
  125. fontTools/pens/cairoPen.py +26 -0
  126. fontTools/pens/cocoaPen.py +26 -0
  127. fontTools/pens/cu2quPen.py +325 -0
  128. fontTools/pens/explicitClosingLinePen.py +101 -0
  129. fontTools/pens/filterPen.py +433 -0
  130. fontTools/pens/freetypePen.py +462 -0
  131. fontTools/pens/hashPointPen.py +89 -0
  132. fontTools/pens/momentsPen.c +13378 -0
  133. fontTools/pens/momentsPen.cp311-win32.pyd +0 -0
  134. fontTools/pens/momentsPen.py +879 -0
  135. fontTools/pens/perimeterPen.py +69 -0
  136. fontTools/pens/pointInsidePen.py +192 -0
  137. fontTools/pens/pointPen.py +643 -0
  138. fontTools/pens/qtPen.py +29 -0
  139. fontTools/pens/qu2cuPen.py +105 -0
  140. fontTools/pens/quartzPen.py +43 -0
  141. fontTools/pens/recordingPen.py +335 -0
  142. fontTools/pens/reportLabPen.py +79 -0
  143. fontTools/pens/reverseContourPen.py +96 -0
  144. fontTools/pens/roundingPen.py +130 -0
  145. fontTools/pens/statisticsPen.py +312 -0
  146. fontTools/pens/svgPathPen.py +310 -0
  147. fontTools/pens/t2CharStringPen.py +88 -0
  148. fontTools/pens/teePen.py +55 -0
  149. fontTools/pens/transformPen.py +115 -0
  150. fontTools/pens/ttGlyphPen.py +335 -0
  151. fontTools/pens/wxPen.py +29 -0
  152. fontTools/qu2cu/__init__.py +15 -0
  153. fontTools/qu2cu/__main__.py +7 -0
  154. fontTools/qu2cu/benchmark.py +56 -0
  155. fontTools/qu2cu/cli.py +125 -0
  156. fontTools/qu2cu/qu2cu.c +16682 -0
  157. fontTools/qu2cu/qu2cu.cp311-win32.pyd +0 -0
  158. fontTools/qu2cu/qu2cu.py +405 -0
  159. fontTools/subset/__init__.py +4096 -0
  160. fontTools/subset/__main__.py +6 -0
  161. fontTools/subset/cff.py +184 -0
  162. fontTools/subset/svg.py +253 -0
  163. fontTools/subset/util.py +25 -0
  164. fontTools/svgLib/__init__.py +3 -0
  165. fontTools/svgLib/path/__init__.py +65 -0
  166. fontTools/svgLib/path/arc.py +154 -0
  167. fontTools/svgLib/path/parser.py +322 -0
  168. fontTools/svgLib/path/shapes.py +183 -0
  169. fontTools/t1Lib/__init__.py +648 -0
  170. fontTools/tfmLib.py +460 -0
  171. fontTools/ttLib/__init__.py +30 -0
  172. fontTools/ttLib/__main__.py +148 -0
  173. fontTools/ttLib/macUtils.py +54 -0
  174. fontTools/ttLib/removeOverlaps.py +395 -0
  175. fontTools/ttLib/reorderGlyphs.py +285 -0
  176. fontTools/ttLib/scaleUpem.py +436 -0
  177. fontTools/ttLib/sfnt.py +661 -0
  178. fontTools/ttLib/standardGlyphOrder.py +271 -0
  179. fontTools/ttLib/tables/B_A_S_E_.py +14 -0
  180. fontTools/ttLib/tables/BitmapGlyphMetrics.py +64 -0
  181. fontTools/ttLib/tables/C_B_D_T_.py +113 -0
  182. fontTools/ttLib/tables/C_B_L_C_.py +19 -0
  183. fontTools/ttLib/tables/C_F_F_.py +61 -0
  184. fontTools/ttLib/tables/C_F_F__2.py +26 -0
  185. fontTools/ttLib/tables/C_O_L_R_.py +165 -0
  186. fontTools/ttLib/tables/C_P_A_L_.py +305 -0
  187. fontTools/ttLib/tables/D_S_I_G_.py +158 -0
  188. fontTools/ttLib/tables/D__e_b_g.py +35 -0
  189. fontTools/ttLib/tables/DefaultTable.py +49 -0
  190. fontTools/ttLib/tables/E_B_D_T_.py +835 -0
  191. fontTools/ttLib/tables/E_B_L_C_.py +718 -0
  192. fontTools/ttLib/tables/F_F_T_M_.py +52 -0
  193. fontTools/ttLib/tables/F__e_a_t.py +149 -0
  194. fontTools/ttLib/tables/G_D_E_F_.py +13 -0
  195. fontTools/ttLib/tables/G_M_A_P_.py +148 -0
  196. fontTools/ttLib/tables/G_P_K_G_.py +133 -0
  197. fontTools/ttLib/tables/G_P_O_S_.py +14 -0
  198. fontTools/ttLib/tables/G_S_U_B_.py +13 -0
  199. fontTools/ttLib/tables/G_V_A_R_.py +5 -0
  200. fontTools/ttLib/tables/G__l_a_t.py +235 -0
  201. fontTools/ttLib/tables/G__l_o_c.py +85 -0
  202. fontTools/ttLib/tables/H_V_A_R_.py +13 -0
  203. fontTools/ttLib/tables/J_S_T_F_.py +13 -0
  204. fontTools/ttLib/tables/L_T_S_H_.py +58 -0
  205. fontTools/ttLib/tables/M_A_T_H_.py +13 -0
  206. fontTools/ttLib/tables/M_E_T_A_.py +352 -0
  207. fontTools/ttLib/tables/M_V_A_R_.py +13 -0
  208. fontTools/ttLib/tables/O_S_2f_2.py +752 -0
  209. fontTools/ttLib/tables/S_I_N_G_.py +99 -0
  210. fontTools/ttLib/tables/S_T_A_T_.py +15 -0
  211. fontTools/ttLib/tables/S_V_G_.py +223 -0
  212. fontTools/ttLib/tables/S__i_l_f.py +1040 -0
  213. fontTools/ttLib/tables/S__i_l_l.py +92 -0
  214. fontTools/ttLib/tables/T_S_I_B_.py +13 -0
  215. fontTools/ttLib/tables/T_S_I_C_.py +14 -0
  216. fontTools/ttLib/tables/T_S_I_D_.py +13 -0
  217. fontTools/ttLib/tables/T_S_I_J_.py +13 -0
  218. fontTools/ttLib/tables/T_S_I_P_.py +13 -0
  219. fontTools/ttLib/tables/T_S_I_S_.py +13 -0
  220. fontTools/ttLib/tables/T_S_I_V_.py +26 -0
  221. fontTools/ttLib/tables/T_S_I__0.py +70 -0
  222. fontTools/ttLib/tables/T_S_I__1.py +163 -0
  223. fontTools/ttLib/tables/T_S_I__2.py +17 -0
  224. fontTools/ttLib/tables/T_S_I__3.py +22 -0
  225. fontTools/ttLib/tables/T_S_I__5.py +60 -0
  226. fontTools/ttLib/tables/T_T_F_A_.py +14 -0
  227. fontTools/ttLib/tables/TupleVariation.py +884 -0
  228. fontTools/ttLib/tables/V_A_R_C_.py +12 -0
  229. fontTools/ttLib/tables/V_D_M_X_.py +249 -0
  230. fontTools/ttLib/tables/V_O_R_G_.py +165 -0
  231. fontTools/ttLib/tables/V_V_A_R_.py +13 -0
  232. fontTools/ttLib/tables/__init__.py +98 -0
  233. fontTools/ttLib/tables/_a_n_k_r.py +15 -0
  234. fontTools/ttLib/tables/_a_v_a_r.py +193 -0
  235. fontTools/ttLib/tables/_b_s_l_n.py +15 -0
  236. fontTools/ttLib/tables/_c_i_d_g.py +24 -0
  237. fontTools/ttLib/tables/_c_m_a_p.py +1591 -0
  238. fontTools/ttLib/tables/_c_v_a_r.py +94 -0
  239. fontTools/ttLib/tables/_c_v_t.py +56 -0
  240. fontTools/ttLib/tables/_f_e_a_t.py +15 -0
  241. fontTools/ttLib/tables/_f_p_g_m.py +62 -0
  242. fontTools/ttLib/tables/_f_v_a_r.py +261 -0
  243. fontTools/ttLib/tables/_g_a_s_p.py +63 -0
  244. fontTools/ttLib/tables/_g_c_i_d.py +13 -0
  245. fontTools/ttLib/tables/_g_l_y_f.py +2311 -0
  246. fontTools/ttLib/tables/_g_v_a_r.py +340 -0
  247. fontTools/ttLib/tables/_h_d_m_x.py +127 -0
  248. fontTools/ttLib/tables/_h_e_a_d.py +130 -0
  249. fontTools/ttLib/tables/_h_h_e_a.py +147 -0
  250. fontTools/ttLib/tables/_h_m_t_x.py +164 -0
  251. fontTools/ttLib/tables/_k_e_r_n.py +289 -0
  252. fontTools/ttLib/tables/_l_c_a_r.py +13 -0
  253. fontTools/ttLib/tables/_l_o_c_a.py +70 -0
  254. fontTools/ttLib/tables/_l_t_a_g.py +72 -0
  255. fontTools/ttLib/tables/_m_a_x_p.py +147 -0
  256. fontTools/ttLib/tables/_m_e_t_a.py +112 -0
  257. fontTools/ttLib/tables/_m_o_r_t.py +14 -0
  258. fontTools/ttLib/tables/_m_o_r_x.py +15 -0
  259. fontTools/ttLib/tables/_n_a_m_e.py +1242 -0
  260. fontTools/ttLib/tables/_o_p_b_d.py +14 -0
  261. fontTools/ttLib/tables/_p_o_s_t.py +319 -0
  262. fontTools/ttLib/tables/_p_r_e_p.py +16 -0
  263. fontTools/ttLib/tables/_p_r_o_p.py +12 -0
  264. fontTools/ttLib/tables/_s_b_i_x.py +129 -0
  265. fontTools/ttLib/tables/_t_r_a_k.py +332 -0
  266. fontTools/ttLib/tables/_v_h_e_a.py +139 -0
  267. fontTools/ttLib/tables/_v_m_t_x.py +19 -0
  268. fontTools/ttLib/tables/asciiTable.py +20 -0
  269. fontTools/ttLib/tables/grUtils.py +92 -0
  270. fontTools/ttLib/tables/otBase.py +1458 -0
  271. fontTools/ttLib/tables/otConverters.py +2068 -0
  272. fontTools/ttLib/tables/otData.py +6400 -0
  273. fontTools/ttLib/tables/otTables.py +2703 -0
  274. fontTools/ttLib/tables/otTraverse.py +163 -0
  275. fontTools/ttLib/tables/sbixGlyph.py +149 -0
  276. fontTools/ttLib/tables/sbixStrike.py +177 -0
  277. fontTools/ttLib/tables/table_API_readme.txt +91 -0
  278. fontTools/ttLib/tables/ttProgram.py +594 -0
  279. fontTools/ttLib/ttCollection.py +125 -0
  280. fontTools/ttLib/ttFont.py +1148 -0
  281. fontTools/ttLib/ttGlyphSet.py +490 -0
  282. fontTools/ttLib/ttVisitor.py +32 -0
  283. fontTools/ttLib/woff2.py +1680 -0
  284. fontTools/ttx.py +479 -0
  285. fontTools/ufoLib/__init__.py +2575 -0
  286. fontTools/ufoLib/converters.py +407 -0
  287. fontTools/ufoLib/errors.py +30 -0
  288. fontTools/ufoLib/etree.py +6 -0
  289. fontTools/ufoLib/filenames.py +356 -0
  290. fontTools/ufoLib/glifLib.py +2120 -0
  291. fontTools/ufoLib/kerning.py +141 -0
  292. fontTools/ufoLib/plistlib.py +47 -0
  293. fontTools/ufoLib/pointPen.py +6 -0
  294. fontTools/ufoLib/utils.py +107 -0
  295. fontTools/ufoLib/validators.py +1208 -0
  296. fontTools/unicode.py +50 -0
  297. fontTools/unicodedata/Blocks.py +817 -0
  298. fontTools/unicodedata/Mirrored.py +446 -0
  299. fontTools/unicodedata/OTTags.py +50 -0
  300. fontTools/unicodedata/ScriptExtensions.py +832 -0
  301. fontTools/unicodedata/Scripts.py +3639 -0
  302. fontTools/unicodedata/__init__.py +306 -0
  303. fontTools/varLib/__init__.py +1600 -0
  304. fontTools/varLib/__main__.py +6 -0
  305. fontTools/varLib/avar/__init__.py +0 -0
  306. fontTools/varLib/avar/__main__.py +72 -0
  307. fontTools/varLib/avar/build.py +79 -0
  308. fontTools/varLib/avar/map.py +108 -0
  309. fontTools/varLib/avar/plan.py +1004 -0
  310. fontTools/varLib/avar/unbuild.py +271 -0
  311. fontTools/varLib/avarPlanner.py +8 -0
  312. fontTools/varLib/builder.py +215 -0
  313. fontTools/varLib/cff.py +631 -0
  314. fontTools/varLib/errors.py +219 -0
  315. fontTools/varLib/featureVars.py +703 -0
  316. fontTools/varLib/hvar.py +113 -0
  317. fontTools/varLib/instancer/__init__.py +2052 -0
  318. fontTools/varLib/instancer/__main__.py +5 -0
  319. fontTools/varLib/instancer/featureVars.py +190 -0
  320. fontTools/varLib/instancer/names.py +388 -0
  321. fontTools/varLib/instancer/solver.py +309 -0
  322. fontTools/varLib/interpolatable.py +1209 -0
  323. fontTools/varLib/interpolatableHelpers.py +399 -0
  324. fontTools/varLib/interpolatablePlot.py +1269 -0
  325. fontTools/varLib/interpolatableTestContourOrder.py +82 -0
  326. fontTools/varLib/interpolatableTestStartingPoint.py +107 -0
  327. fontTools/varLib/interpolate_layout.py +124 -0
  328. fontTools/varLib/iup.c +19815 -0
  329. fontTools/varLib/iup.cp311-win32.pyd +0 -0
  330. fontTools/varLib/iup.py +490 -0
  331. fontTools/varLib/merger.py +1717 -0
  332. fontTools/varLib/models.py +642 -0
  333. fontTools/varLib/multiVarStore.py +253 -0
  334. fontTools/varLib/mutator.py +529 -0
  335. fontTools/varLib/mvar.py +40 -0
  336. fontTools/varLib/plot.py +238 -0
  337. fontTools/varLib/stat.py +149 -0
  338. fontTools/varLib/varStore.py +739 -0
  339. fontTools/voltLib/__init__.py +5 -0
  340. fontTools/voltLib/__main__.py +206 -0
  341. fontTools/voltLib/ast.py +452 -0
  342. fontTools/voltLib/error.py +12 -0
  343. fontTools/voltLib/lexer.py +99 -0
  344. fontTools/voltLib/parser.py +664 -0
  345. fontTools/voltLib/voltToFea.py +911 -0
  346. fonttools-4.60.2.data/data/share/man/man1/ttx.1 +225 -0
  347. fonttools-4.60.2.dist-info/METADATA +2250 -0
  348. fonttools-4.60.2.dist-info/RECORD +353 -0
  349. fonttools-4.60.2.dist-info/WHEEL +5 -0
  350. fonttools-4.60.2.dist-info/entry_points.txt +5 -0
  351. fonttools-4.60.2.dist-info/licenses/LICENSE +21 -0
  352. fonttools-4.60.2.dist-info/licenses/LICENSE.external +388 -0
  353. fonttools-4.60.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1717 @@
1
+ """
2
+ Merge OpenType Layout tables (GDEF / GPOS / GSUB).
3
+ """
4
+
5
+ import os
6
+ import copy
7
+ import enum
8
+ from operator import ior
9
+ import logging
10
+ from fontTools.colorLib.builder import MAX_PAINT_COLR_LAYER_COUNT, LayerReuseCache
11
+ from fontTools.misc import classifyTools
12
+ from fontTools.misc.roundTools import otRound
13
+ from fontTools.misc.treeTools import build_n_ary_tree
14
+ from fontTools.ttLib.tables import otTables as ot
15
+ from fontTools.ttLib.tables import otBase as otBase
16
+ from fontTools.ttLib.tables.otConverters import BaseFixedValue
17
+ from fontTools.ttLib.tables.otTraverse import dfs_base_table
18
+ from fontTools.ttLib.tables.DefaultTable import DefaultTable
19
+ from fontTools.varLib import builder, models, varStore
20
+ from fontTools.varLib.models import nonNone, allNone, allEqual, allEqualTo, subList
21
+ from fontTools.varLib.varStore import VarStoreInstancer
22
+ from functools import reduce
23
+ from fontTools.otlLib.builder import buildSinglePos
24
+ from fontTools.otlLib.optimize.gpos import (
25
+ _compression_level_from_env,
26
+ compact_pair_pos,
27
+ )
28
+
29
+ log = logging.getLogger("fontTools.varLib.merger")
30
+
31
+ from .errors import (
32
+ ShouldBeConstant,
33
+ FoundANone,
34
+ MismatchedTypes,
35
+ NotANone,
36
+ LengthsDiffer,
37
+ KeysDiffer,
38
+ InconsistentGlyphOrder,
39
+ InconsistentExtensions,
40
+ InconsistentFormats,
41
+ UnsupportedFormat,
42
+ VarLibMergeError,
43
+ )
44
+
45
+
46
+ class Merger(object):
47
+ def __init__(self, font=None):
48
+ self.font = font
49
+ # mergeTables populates this from the parent's master ttfs
50
+ self.ttfs = None
51
+
52
+ @classmethod
53
+ def merger(celf, clazzes, attrs=(None,)):
54
+ assert celf != Merger, "Subclass Merger instead."
55
+ if "mergers" not in celf.__dict__:
56
+ celf.mergers = {}
57
+ if type(clazzes) in (type, enum.EnumMeta):
58
+ clazzes = (clazzes,)
59
+ if type(attrs) == str:
60
+ attrs = (attrs,)
61
+
62
+ def wrapper(method):
63
+ assert method.__name__ == "merge"
64
+ done = []
65
+ for clazz in clazzes:
66
+ if clazz in done:
67
+ continue # Support multiple names of a clazz
68
+ done.append(clazz)
69
+ mergers = celf.mergers.setdefault(clazz, {})
70
+ for attr in attrs:
71
+ assert attr not in mergers, (
72
+ "Oops, class '%s' has merge function for '%s' defined already."
73
+ % (clazz.__name__, attr)
74
+ )
75
+ mergers[attr] = method
76
+ return None
77
+
78
+ return wrapper
79
+
80
+ @classmethod
81
+ def mergersFor(celf, thing, _default={}):
82
+ typ = type(thing)
83
+
84
+ for celf in celf.mro():
85
+ mergers = getattr(celf, "mergers", None)
86
+ if mergers is None:
87
+ break
88
+
89
+ m = celf.mergers.get(typ, None)
90
+ if m is not None:
91
+ return m
92
+
93
+ return _default
94
+
95
+ def mergeObjects(self, out, lst, exclude=()):
96
+ if hasattr(out, "ensureDecompiled"):
97
+ out.ensureDecompiled(recurse=False)
98
+ for item in lst:
99
+ if hasattr(item, "ensureDecompiled"):
100
+ item.ensureDecompiled(recurse=False)
101
+ keys = sorted(vars(out).keys())
102
+ if not all(keys == sorted(vars(v).keys()) for v in lst):
103
+ raise KeysDiffer(
104
+ self, expected=keys, got=[sorted(vars(v).keys()) for v in lst]
105
+ )
106
+ mergers = self.mergersFor(out)
107
+ defaultMerger = mergers.get("*", self.__class__.mergeThings)
108
+ try:
109
+ for key in keys:
110
+ if key in exclude:
111
+ continue
112
+ value = getattr(out, key)
113
+ values = [getattr(table, key) for table in lst]
114
+ mergerFunc = mergers.get(key, defaultMerger)
115
+ mergerFunc(self, value, values)
116
+ except VarLibMergeError as e:
117
+ e.stack.append("." + key)
118
+ raise
119
+
120
+ def mergeLists(self, out, lst):
121
+ if not allEqualTo(out, lst, len):
122
+ raise LengthsDiffer(self, expected=len(out), got=[len(x) for x in lst])
123
+ for i, (value, values) in enumerate(zip(out, zip(*lst))):
124
+ try:
125
+ self.mergeThings(value, values)
126
+ except VarLibMergeError as e:
127
+ e.stack.append("[%d]" % i)
128
+ raise
129
+
130
+ def mergeThings(self, out, lst):
131
+ if not allEqualTo(out, lst, type):
132
+ raise MismatchedTypes(
133
+ self, expected=type(out).__name__, got=[type(x).__name__ for x in lst]
134
+ )
135
+ mergerFunc = self.mergersFor(out).get(None, None)
136
+ if mergerFunc is not None:
137
+ mergerFunc(self, out, lst)
138
+ elif isinstance(out, enum.Enum):
139
+ # need to special-case Enums as have __dict__ but are not regular 'objects',
140
+ # otherwise mergeObjects/mergeThings get trapped in a RecursionError
141
+ if not allEqualTo(out, lst):
142
+ raise ShouldBeConstant(self, expected=out, got=lst)
143
+ elif hasattr(out, "__dict__"):
144
+ self.mergeObjects(out, lst)
145
+ elif isinstance(out, list):
146
+ self.mergeLists(out, lst)
147
+ else:
148
+ if not allEqualTo(out, lst):
149
+ raise ShouldBeConstant(self, expected=out, got=lst)
150
+
151
+ def mergeTables(self, font, master_ttfs, tableTags):
152
+ for tag in tableTags:
153
+ if tag not in font:
154
+ continue
155
+ try:
156
+ self.ttfs = master_ttfs
157
+ self.mergeThings(font[tag], [m.get(tag) for m in master_ttfs])
158
+ except VarLibMergeError as e:
159
+ e.stack.append(tag)
160
+ raise
161
+
162
+
163
+ #
164
+ # Aligning merger
165
+ #
166
+ class AligningMerger(Merger):
167
+ pass
168
+
169
+
170
+ @AligningMerger.merger(ot.GDEF, "GlyphClassDef")
171
+ def merge(merger, self, lst):
172
+ if self is None:
173
+ if not allNone(lst):
174
+ raise NotANone(merger, expected=None, got=lst)
175
+ return
176
+
177
+ lst = [l.classDefs for l in lst]
178
+ self.classDefs = {}
179
+ # We only care about the .classDefs
180
+ self = self.classDefs
181
+
182
+ allKeys = set()
183
+ allKeys.update(*[l.keys() for l in lst])
184
+ for k in allKeys:
185
+ allValues = nonNone(l.get(k) for l in lst)
186
+ if not allEqual(allValues):
187
+ raise ShouldBeConstant(
188
+ merger, expected=allValues[0], got=lst, stack=["." + k]
189
+ )
190
+ if not allValues:
191
+ self[k] = None
192
+ else:
193
+ self[k] = allValues[0]
194
+
195
+
196
+ def _SinglePosUpgradeToFormat2(self):
197
+ if self.Format == 2:
198
+ return self
199
+
200
+ ret = ot.SinglePos()
201
+ ret.Format = 2
202
+ ret.Coverage = self.Coverage
203
+ ret.ValueFormat = self.ValueFormat
204
+ ret.Value = [self.Value for _ in ret.Coverage.glyphs]
205
+ ret.ValueCount = len(ret.Value)
206
+
207
+ return ret
208
+
209
+
210
+ def _merge_GlyphOrders(font, lst, values_lst=None, default=None):
211
+ """Takes font and list of glyph lists (must be sorted by glyph id), and returns
212
+ two things:
213
+ - Combined glyph list,
214
+ - If values_lst is None, return input glyph lists, but padded with None when a glyph
215
+ was missing in a list. Otherwise, return values_lst list-of-list, padded with None
216
+ to match combined glyph lists.
217
+ """
218
+ if values_lst is None:
219
+ dict_sets = [set(l) for l in lst]
220
+ else:
221
+ dict_sets = [{g: v for g, v in zip(l, vs)} for l, vs in zip(lst, values_lst)]
222
+ combined = set()
223
+ combined.update(*dict_sets)
224
+
225
+ sortKey = font.getReverseGlyphMap().__getitem__
226
+ order = sorted(combined, key=sortKey)
227
+ # Make sure all input glyphsets were in proper order
228
+ if not all(sorted(vs, key=sortKey) == vs for vs in lst):
229
+ raise InconsistentGlyphOrder()
230
+ del combined
231
+
232
+ paddedValues = None
233
+ if values_lst is None:
234
+ padded = [
235
+ [glyph if glyph in dict_set else default for glyph in order]
236
+ for dict_set in dict_sets
237
+ ]
238
+ else:
239
+ assert len(lst) == len(values_lst)
240
+ padded = [
241
+ [dict_set[glyph] if glyph in dict_set else default for glyph in order]
242
+ for dict_set in dict_sets
243
+ ]
244
+ return order, padded
245
+
246
+
247
+ @AligningMerger.merger(otBase.ValueRecord)
248
+ def merge(merger, self, lst):
249
+ # Code below sometimes calls us with self being
250
+ # a new object. Copy it from lst and recurse.
251
+ self.__dict__ = lst[0].__dict__.copy()
252
+ merger.mergeObjects(self, lst)
253
+
254
+
255
+ @AligningMerger.merger(ot.Anchor)
256
+ def merge(merger, self, lst):
257
+ # Code below sometimes calls us with self being
258
+ # a new object. Copy it from lst and recurse.
259
+ self.__dict__ = lst[0].__dict__.copy()
260
+ merger.mergeObjects(self, lst)
261
+
262
+
263
+ def _Lookup_SinglePos_get_effective_value(merger, subtables, glyph):
264
+ for self in subtables:
265
+ if (
266
+ self is None
267
+ or type(self) != ot.SinglePos
268
+ or self.Coverage is None
269
+ or glyph not in self.Coverage.glyphs
270
+ ):
271
+ continue
272
+ if self.Format == 1:
273
+ return self.Value
274
+ elif self.Format == 2:
275
+ return self.Value[self.Coverage.glyphs.index(glyph)]
276
+ else:
277
+ raise UnsupportedFormat(merger, subtable="single positioning lookup")
278
+ return None
279
+
280
+
281
+ def _Lookup_PairPos_get_effective_value_pair(
282
+ merger, subtables, firstGlyph, secondGlyph
283
+ ):
284
+ for self in subtables:
285
+ if (
286
+ self is None
287
+ or type(self) != ot.PairPos
288
+ or self.Coverage is None
289
+ or firstGlyph not in self.Coverage.glyphs
290
+ ):
291
+ continue
292
+ if self.Format == 1:
293
+ ps = self.PairSet[self.Coverage.glyphs.index(firstGlyph)]
294
+ pvr = ps.PairValueRecord
295
+ for rec in pvr: # TODO Speed up
296
+ if rec.SecondGlyph == secondGlyph:
297
+ return rec
298
+ continue
299
+ elif self.Format == 2:
300
+ klass1 = self.ClassDef1.classDefs.get(firstGlyph, 0)
301
+ klass2 = self.ClassDef2.classDefs.get(secondGlyph, 0)
302
+ return self.Class1Record[klass1].Class2Record[klass2]
303
+ else:
304
+ raise UnsupportedFormat(merger, subtable="pair positioning lookup")
305
+ return None
306
+
307
+
308
+ @AligningMerger.merger(ot.SinglePos)
309
+ def merge(merger, self, lst):
310
+ self.ValueFormat = valueFormat = reduce(int.__or__, [l.ValueFormat for l in lst], 0)
311
+ if not (len(lst) == 1 or (valueFormat & ~0xF == 0)):
312
+ raise UnsupportedFormat(merger, subtable="single positioning lookup")
313
+
314
+ # If all have same coverage table and all are format 1,
315
+ coverageGlyphs = self.Coverage.glyphs
316
+ if all(v.Format == 1 for v in lst) and all(
317
+ coverageGlyphs == v.Coverage.glyphs for v in lst
318
+ ):
319
+ self.Value = otBase.ValueRecord(valueFormat, self.Value)
320
+ if valueFormat != 0:
321
+ # If v.Value is None, it means a kerning of 0; we want
322
+ # it to participate in the model still.
323
+ # https://github.com/fonttools/fonttools/issues/3111
324
+ merger.mergeThings(
325
+ self.Value,
326
+ [v.Value if v.Value is not None else otBase.ValueRecord() for v in lst],
327
+ )
328
+ self.ValueFormat = self.Value.getFormat()
329
+ return
330
+
331
+ # Upgrade everything to Format=2
332
+ self.Format = 2
333
+ lst = [_SinglePosUpgradeToFormat2(v) for v in lst]
334
+
335
+ # Align them
336
+ glyphs, padded = _merge_GlyphOrders(
337
+ merger.font, [v.Coverage.glyphs for v in lst], [v.Value for v in lst]
338
+ )
339
+
340
+ self.Coverage.glyphs = glyphs
341
+ self.Value = [otBase.ValueRecord(valueFormat) for _ in glyphs]
342
+ self.ValueCount = len(self.Value)
343
+
344
+ for i, values in enumerate(padded):
345
+ for j, glyph in enumerate(glyphs):
346
+ if values[j] is not None:
347
+ continue
348
+ # Fill in value from other subtables
349
+ # Note!!! This *might* result in behavior change if ValueFormat2-zeroedness
350
+ # is different between used subtable and current subtable!
351
+ # TODO(behdad) Check and warn if that happens?
352
+ v = _Lookup_SinglePos_get_effective_value(
353
+ merger, merger.lookup_subtables[i], glyph
354
+ )
355
+ if v is None:
356
+ v = otBase.ValueRecord(valueFormat)
357
+ values[j] = v
358
+
359
+ merger.mergeLists(self.Value, padded)
360
+
361
+ # Merge everything else; though, there shouldn't be anything else. :)
362
+ merger.mergeObjects(
363
+ self, lst, exclude=("Format", "Coverage", "Value", "ValueCount", "ValueFormat")
364
+ )
365
+ self.ValueFormat = reduce(
366
+ int.__or__, [v.getEffectiveFormat() for v in self.Value], 0
367
+ )
368
+
369
+
370
+ @AligningMerger.merger(ot.PairSet)
371
+ def merge(merger, self, lst):
372
+ # Align them
373
+ glyphs, padded = _merge_GlyphOrders(
374
+ merger.font,
375
+ [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst],
376
+ [vs.PairValueRecord for vs in lst],
377
+ )
378
+
379
+ self.PairValueRecord = pvrs = []
380
+ for glyph in glyphs:
381
+ pvr = ot.PairValueRecord()
382
+ pvr.SecondGlyph = glyph
383
+ pvr.Value1 = (
384
+ otBase.ValueRecord(merger.valueFormat1) if merger.valueFormat1 else None
385
+ )
386
+ pvr.Value2 = (
387
+ otBase.ValueRecord(merger.valueFormat2) if merger.valueFormat2 else None
388
+ )
389
+ pvrs.append(pvr)
390
+ self.PairValueCount = len(self.PairValueRecord)
391
+
392
+ for i, values in enumerate(padded):
393
+ for j, glyph in enumerate(glyphs):
394
+ # Fill in value from other subtables
395
+ v = ot.PairValueRecord()
396
+ v.SecondGlyph = glyph
397
+ if values[j] is not None:
398
+ vpair = values[j]
399
+ else:
400
+ vpair = _Lookup_PairPos_get_effective_value_pair(
401
+ merger, merger.lookup_subtables[i], self._firstGlyph, glyph
402
+ )
403
+ if vpair is None:
404
+ v1, v2 = None, None
405
+ else:
406
+ v1 = getattr(vpair, "Value1", None)
407
+ v2 = getattr(vpair, "Value2", None)
408
+ v.Value1 = (
409
+ otBase.ValueRecord(merger.valueFormat1, src=v1)
410
+ if merger.valueFormat1
411
+ else None
412
+ )
413
+ v.Value2 = (
414
+ otBase.ValueRecord(merger.valueFormat2, src=v2)
415
+ if merger.valueFormat2
416
+ else None
417
+ )
418
+ values[j] = v
419
+ del self._firstGlyph
420
+
421
+ merger.mergeLists(self.PairValueRecord, padded)
422
+
423
+
424
+ def _PairPosFormat1_merge(self, lst, merger):
425
+ assert allEqual(
426
+ [l.ValueFormat2 == 0 for l in lst if l.PairSet]
427
+ ), "Report bug against fonttools."
428
+
429
+ # Merge everything else; makes sure Format is the same.
430
+ merger.mergeObjects(
431
+ self,
432
+ lst,
433
+ exclude=("Coverage", "PairSet", "PairSetCount", "ValueFormat1", "ValueFormat2"),
434
+ )
435
+
436
+ empty = ot.PairSet()
437
+ empty.PairValueRecord = []
438
+ empty.PairValueCount = 0
439
+
440
+ # Align them
441
+ glyphs, padded = _merge_GlyphOrders(
442
+ merger.font,
443
+ [v.Coverage.glyphs for v in lst],
444
+ [v.PairSet for v in lst],
445
+ default=empty,
446
+ )
447
+
448
+ self.Coverage.glyphs = glyphs
449
+ self.PairSet = [ot.PairSet() for _ in glyphs]
450
+ self.PairSetCount = len(self.PairSet)
451
+ for glyph, ps in zip(glyphs, self.PairSet):
452
+ ps._firstGlyph = glyph
453
+
454
+ merger.mergeLists(self.PairSet, padded)
455
+
456
+
457
+ def _ClassDef_invert(self, allGlyphs=None):
458
+ if isinstance(self, dict):
459
+ classDefs = self
460
+ else:
461
+ classDefs = self.classDefs if self and self.classDefs else {}
462
+ m = max(classDefs.values()) if classDefs else 0
463
+
464
+ ret = []
465
+ for _ in range(m + 1):
466
+ ret.append(set())
467
+
468
+ for k, v in classDefs.items():
469
+ ret[v].add(k)
470
+
471
+ # Class-0 is special. It's "everything else".
472
+ if allGlyphs is None:
473
+ ret[0] = None
474
+ else:
475
+ # Limit all classes to glyphs in allGlyphs.
476
+ # Collect anything without a non-zero class into class=zero.
477
+ ret[0] = class0 = set(allGlyphs)
478
+ for s in ret[1:]:
479
+ s.intersection_update(class0)
480
+ class0.difference_update(s)
481
+
482
+ return ret
483
+
484
+
485
+ def _ClassDef_merge_classify(lst, allGlyphses=None):
486
+ self = ot.ClassDef()
487
+ self.classDefs = classDefs = {}
488
+ allGlyphsesWasNone = allGlyphses is None
489
+ if allGlyphsesWasNone:
490
+ allGlyphses = [None] * len(lst)
491
+
492
+ classifier = classifyTools.Classifier()
493
+ for classDef, allGlyphs in zip(lst, allGlyphses):
494
+ sets = _ClassDef_invert(classDef, allGlyphs)
495
+ if allGlyphs is None:
496
+ sets = sets[1:]
497
+ classifier.update(sets)
498
+ classes = classifier.getClasses()
499
+
500
+ if allGlyphsesWasNone:
501
+ classes.insert(0, set())
502
+
503
+ for i, classSet in enumerate(classes):
504
+ if i == 0:
505
+ continue
506
+ for g in classSet:
507
+ classDefs[g] = i
508
+
509
+ return self, classes
510
+
511
+
512
+ def _PairPosFormat2_align_matrices(self, lst, font, transparent=False):
513
+ matrices = [l.Class1Record for l in lst]
514
+
515
+ # Align first classes
516
+ self.ClassDef1, classes = _ClassDef_merge_classify(
517
+ [l.ClassDef1 for l in lst], [l.Coverage.glyphs for l in lst]
518
+ )
519
+ self.Class1Count = len(classes)
520
+ new_matrices = []
521
+ for l, matrix in zip(lst, matrices):
522
+ nullRow = None
523
+ coverage = set(l.Coverage.glyphs)
524
+ classDef1 = l.ClassDef1.classDefs
525
+ class1Records = []
526
+ for classSet in classes:
527
+ exemplarGlyph = next(iter(classSet))
528
+ if exemplarGlyph not in coverage:
529
+ # Follow-up to e6125b353e1f54a0280ded5434b8e40d042de69f,
530
+ # Fixes https://github.com/googlei18n/fontmake/issues/470
531
+ # Again, revert 8d441779e5afc664960d848f62c7acdbfc71d7b9
532
+ # when merger becomes selfless.
533
+ nullRow = None
534
+ if nullRow is None:
535
+ nullRow = ot.Class1Record()
536
+ class2records = nullRow.Class2Record = []
537
+ # TODO: When merger becomes selfless, revert e6125b353e1f54a0280ded5434b8e40d042de69f
538
+ for _ in range(l.Class2Count):
539
+ if transparent:
540
+ rec2 = None
541
+ else:
542
+ rec2 = ot.Class2Record()
543
+ rec2.Value1 = (
544
+ otBase.ValueRecord(self.ValueFormat1)
545
+ if self.ValueFormat1
546
+ else None
547
+ )
548
+ rec2.Value2 = (
549
+ otBase.ValueRecord(self.ValueFormat2)
550
+ if self.ValueFormat2
551
+ else None
552
+ )
553
+ class2records.append(rec2)
554
+ rec1 = nullRow
555
+ else:
556
+ klass = classDef1.get(exemplarGlyph, 0)
557
+ rec1 = matrix[klass] # TODO handle out-of-range?
558
+ class1Records.append(rec1)
559
+ new_matrices.append(class1Records)
560
+ matrices = new_matrices
561
+ del new_matrices
562
+
563
+ # Align second classes
564
+ self.ClassDef2, classes = _ClassDef_merge_classify([l.ClassDef2 for l in lst])
565
+ self.Class2Count = len(classes)
566
+ new_matrices = []
567
+ for l, matrix in zip(lst, matrices):
568
+ classDef2 = l.ClassDef2.classDefs
569
+ class1Records = []
570
+ for rec1old in matrix:
571
+ oldClass2Records = rec1old.Class2Record
572
+ rec1new = ot.Class1Record()
573
+ class2Records = rec1new.Class2Record = []
574
+ for classSet in classes:
575
+ if not classSet: # class=0
576
+ rec2 = oldClass2Records[0]
577
+ else:
578
+ exemplarGlyph = next(iter(classSet))
579
+ klass = classDef2.get(exemplarGlyph, 0)
580
+ rec2 = oldClass2Records[klass]
581
+ class2Records.append(copy.deepcopy(rec2))
582
+ class1Records.append(rec1new)
583
+ new_matrices.append(class1Records)
584
+ matrices = new_matrices
585
+ del new_matrices
586
+
587
+ return matrices
588
+
589
+
590
+ def _PairPosFormat2_merge(self, lst, merger):
591
+ assert allEqual(
592
+ [l.ValueFormat2 == 0 for l in lst if l.Class1Record]
593
+ ), "Report bug against fonttools."
594
+
595
+ merger.mergeObjects(
596
+ self,
597
+ lst,
598
+ exclude=(
599
+ "Coverage",
600
+ "ClassDef1",
601
+ "Class1Count",
602
+ "ClassDef2",
603
+ "Class2Count",
604
+ "Class1Record",
605
+ "ValueFormat1",
606
+ "ValueFormat2",
607
+ ),
608
+ )
609
+
610
+ # Align coverages
611
+ glyphs, _ = _merge_GlyphOrders(merger.font, [v.Coverage.glyphs for v in lst])
612
+ self.Coverage.glyphs = glyphs
613
+
614
+ # Currently, if the coverage of PairPosFormat2 subtables are different,
615
+ # we do NOT bother walking down the subtable list when filling in new
616
+ # rows for alignment. As such, this is only correct if current subtable
617
+ # is the last subtable in the lookup. Ensure that.
618
+ #
619
+ # Note that our canonicalization process merges trailing PairPosFormat2's,
620
+ # so in reality this is rare.
621
+ for l, subtables in zip(lst, merger.lookup_subtables):
622
+ if l.Coverage.glyphs != glyphs:
623
+ assert l == subtables[-1]
624
+
625
+ matrices = _PairPosFormat2_align_matrices(self, lst, merger.font)
626
+
627
+ self.Class1Record = list(matrices[0]) # TODO move merger to be selfless
628
+ merger.mergeLists(self.Class1Record, matrices)
629
+
630
+
631
+ @AligningMerger.merger(ot.PairPos)
632
+ def merge(merger, self, lst):
633
+ merger.valueFormat1 = self.ValueFormat1 = reduce(
634
+ int.__or__, [l.ValueFormat1 for l in lst], 0
635
+ )
636
+ merger.valueFormat2 = self.ValueFormat2 = reduce(
637
+ int.__or__, [l.ValueFormat2 for l in lst], 0
638
+ )
639
+
640
+ if self.Format == 1:
641
+ _PairPosFormat1_merge(self, lst, merger)
642
+ elif self.Format == 2:
643
+ _PairPosFormat2_merge(self, lst, merger)
644
+ else:
645
+ raise UnsupportedFormat(merger, subtable="pair positioning lookup")
646
+
647
+ del merger.valueFormat1, merger.valueFormat2
648
+
649
+ # Now examine the list of value records, and update to the union of format values,
650
+ # as merge might have created new values.
651
+ vf1 = 0
652
+ vf2 = 0
653
+ if self.Format == 1:
654
+ for pairSet in self.PairSet:
655
+ for pairValueRecord in pairSet.PairValueRecord:
656
+ pv1 = getattr(pairValueRecord, "Value1", None)
657
+ if pv1 is not None:
658
+ vf1 |= pv1.getFormat()
659
+ pv2 = getattr(pairValueRecord, "Value2", None)
660
+ if pv2 is not None:
661
+ vf2 |= pv2.getFormat()
662
+ elif self.Format == 2:
663
+ for class1Record in self.Class1Record:
664
+ for class2Record in class1Record.Class2Record:
665
+ pv1 = getattr(class2Record, "Value1", None)
666
+ if pv1 is not None:
667
+ vf1 |= pv1.getFormat()
668
+ pv2 = getattr(class2Record, "Value2", None)
669
+ if pv2 is not None:
670
+ vf2 |= pv2.getFormat()
671
+ self.ValueFormat1 = vf1
672
+ self.ValueFormat2 = vf2
673
+
674
+
675
+ def _MarkBasePosFormat1_merge(self, lst, merger, Mark="Mark", Base="Base"):
676
+ self.ClassCount = max(l.ClassCount for l in lst)
677
+
678
+ MarkCoverageGlyphs, MarkRecords = _merge_GlyphOrders(
679
+ merger.font,
680
+ [getattr(l, Mark + "Coverage").glyphs for l in lst],
681
+ [getattr(l, Mark + "Array").MarkRecord for l in lst],
682
+ )
683
+ getattr(self, Mark + "Coverage").glyphs = MarkCoverageGlyphs
684
+
685
+ BaseCoverageGlyphs, BaseRecords = _merge_GlyphOrders(
686
+ merger.font,
687
+ [getattr(l, Base + "Coverage").glyphs for l in lst],
688
+ [getattr(getattr(l, Base + "Array"), Base + "Record") for l in lst],
689
+ )
690
+ getattr(self, Base + "Coverage").glyphs = BaseCoverageGlyphs
691
+
692
+ # MarkArray
693
+ records = []
694
+ for g, glyphRecords in zip(MarkCoverageGlyphs, zip(*MarkRecords)):
695
+ allClasses = [r.Class for r in glyphRecords if r is not None]
696
+
697
+ # TODO Right now we require that all marks have same class in
698
+ # all masters that cover them. This is not required.
699
+ #
700
+ # We can relax that by just requiring that all marks that have
701
+ # the same class in a master, have the same class in every other
702
+ # master. Indeed, if, say, a sparse master only covers one mark,
703
+ # that mark probably will get class 0, which would possibly be
704
+ # different from its class in other masters.
705
+ #
706
+ # We can even go further and reclassify marks to support any
707
+ # input. But, since, it's unlikely that two marks being both,
708
+ # say, "top" in one master, and one being "top" and other being
709
+ # "top-right" in another master, we shouldn't do that, as any
710
+ # failures in that case will probably signify mistakes in the
711
+ # input masters.
712
+
713
+ if not allEqual(allClasses):
714
+ raise ShouldBeConstant(merger, expected=allClasses[0], got=allClasses)
715
+ else:
716
+ rec = ot.MarkRecord()
717
+ rec.Class = allClasses[0]
718
+ allAnchors = [None if r is None else r.MarkAnchor for r in glyphRecords]
719
+ if allNone(allAnchors):
720
+ anchor = None
721
+ else:
722
+ anchor = ot.Anchor()
723
+ anchor.Format = 1
724
+ merger.mergeThings(anchor, allAnchors)
725
+ rec.MarkAnchor = anchor
726
+ records.append(rec)
727
+ array = ot.MarkArray()
728
+ array.MarkRecord = records
729
+ array.MarkCount = len(records)
730
+ setattr(self, Mark + "Array", array)
731
+
732
+ # BaseArray
733
+ records = []
734
+ for g, glyphRecords in zip(BaseCoverageGlyphs, zip(*BaseRecords)):
735
+ if allNone(glyphRecords):
736
+ rec = None
737
+ else:
738
+ rec = getattr(ot, Base + "Record")()
739
+ anchors = []
740
+ setattr(rec, Base + "Anchor", anchors)
741
+ glyphAnchors = [
742
+ [] if r is None else getattr(r, Base + "Anchor") for r in glyphRecords
743
+ ]
744
+ for l in glyphAnchors:
745
+ l.extend([None] * (self.ClassCount - len(l)))
746
+ for allAnchors in zip(*glyphAnchors):
747
+ if allNone(allAnchors):
748
+ anchor = None
749
+ else:
750
+ anchor = ot.Anchor()
751
+ anchor.Format = 1
752
+ merger.mergeThings(anchor, allAnchors)
753
+ anchors.append(anchor)
754
+ records.append(rec)
755
+ array = getattr(ot, Base + "Array")()
756
+ setattr(array, Base + "Record", records)
757
+ setattr(array, Base + "Count", len(records))
758
+ setattr(self, Base + "Array", array)
759
+
760
+
761
+ @AligningMerger.merger(ot.MarkBasePos)
762
+ def merge(merger, self, lst):
763
+ if not allEqualTo(self.Format, (l.Format for l in lst)):
764
+ raise InconsistentFormats(
765
+ merger,
766
+ subtable="mark-to-base positioning lookup",
767
+ expected=self.Format,
768
+ got=[l.Format for l in lst],
769
+ )
770
+ if self.Format == 1:
771
+ _MarkBasePosFormat1_merge(self, lst, merger)
772
+ else:
773
+ raise UnsupportedFormat(merger, subtable="mark-to-base positioning lookup")
774
+
775
+
776
+ @AligningMerger.merger(ot.MarkMarkPos)
777
+ def merge(merger, self, lst):
778
+ if not allEqualTo(self.Format, (l.Format for l in lst)):
779
+ raise InconsistentFormats(
780
+ merger,
781
+ subtable="mark-to-mark positioning lookup",
782
+ expected=self.Format,
783
+ got=[l.Format for l in lst],
784
+ )
785
+ if self.Format == 1:
786
+ _MarkBasePosFormat1_merge(self, lst, merger, "Mark1", "Mark2")
787
+ else:
788
+ raise UnsupportedFormat(merger, subtable="mark-to-mark positioning lookup")
789
+
790
+
791
+ def _PairSet_flatten(lst, font):
792
+ self = ot.PairSet()
793
+ self.Coverage = ot.Coverage()
794
+
795
+ # Align them
796
+ glyphs, padded = _merge_GlyphOrders(
797
+ font,
798
+ [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst],
799
+ [vs.PairValueRecord for vs in lst],
800
+ )
801
+
802
+ self.Coverage.glyphs = glyphs
803
+ self.PairValueRecord = pvrs = []
804
+ for values in zip(*padded):
805
+ for v in values:
806
+ if v is not None:
807
+ pvrs.append(v)
808
+ break
809
+ else:
810
+ assert False
811
+ self.PairValueCount = len(self.PairValueRecord)
812
+
813
+ return self
814
+
815
+
816
+ def _Lookup_PairPosFormat1_subtables_flatten(lst, font):
817
+ assert allEqual(
818
+ [l.ValueFormat2 == 0 for l in lst if l.PairSet]
819
+ ), "Report bug against fonttools."
820
+
821
+ self = ot.PairPos()
822
+ self.Format = 1
823
+ self.Coverage = ot.Coverage()
824
+ self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0)
825
+ self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0)
826
+
827
+ # Align them
828
+ glyphs, padded = _merge_GlyphOrders(
829
+ font, [v.Coverage.glyphs for v in lst], [v.PairSet for v in lst]
830
+ )
831
+
832
+ self.Coverage.glyphs = glyphs
833
+ self.PairSet = [
834
+ _PairSet_flatten([v for v in values if v is not None], font)
835
+ for values in zip(*padded)
836
+ ]
837
+ self.PairSetCount = len(self.PairSet)
838
+ return self
839
+
840
+
841
+ def _Lookup_PairPosFormat2_subtables_flatten(lst, font):
842
+ assert allEqual(
843
+ [l.ValueFormat2 == 0 for l in lst if l.Class1Record]
844
+ ), "Report bug against fonttools."
845
+
846
+ self = ot.PairPos()
847
+ self.Format = 2
848
+ self.Coverage = ot.Coverage()
849
+ self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0)
850
+ self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0)
851
+
852
+ # Align them
853
+ glyphs, _ = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst])
854
+ self.Coverage.glyphs = glyphs
855
+
856
+ matrices = _PairPosFormat2_align_matrices(self, lst, font, transparent=True)
857
+
858
+ matrix = self.Class1Record = []
859
+ for rows in zip(*matrices):
860
+ row = ot.Class1Record()
861
+ matrix.append(row)
862
+ row.Class2Record = []
863
+ row = row.Class2Record
864
+ for cols in zip(*list(r.Class2Record for r in rows)):
865
+ col = next(iter(c for c in cols if c is not None))
866
+ row.append(col)
867
+
868
+ return self
869
+
870
+
871
+ def _Lookup_PairPos_subtables_canonicalize(lst, font):
872
+ """Merge multiple Format1 subtables at the beginning of lst,
873
+ and merge multiple consecutive Format2 subtables that have the same
874
+ Class2 (ie. were split because of offset overflows). Returns new list."""
875
+ lst = list(lst)
876
+
877
+ l = len(lst)
878
+ i = 0
879
+ while i < l and lst[i].Format == 1:
880
+ i += 1
881
+ lst[:i] = [_Lookup_PairPosFormat1_subtables_flatten(lst[:i], font)]
882
+
883
+ l = len(lst)
884
+ i = l
885
+ while i > 0 and lst[i - 1].Format == 2:
886
+ i -= 1
887
+ lst[i:] = [_Lookup_PairPosFormat2_subtables_flatten(lst[i:], font)]
888
+
889
+ return lst
890
+
891
+
892
+ def _Lookup_SinglePos_subtables_flatten(lst, font, min_inclusive_rec_format):
893
+ glyphs, _ = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst], None)
894
+ num_glyphs = len(glyphs)
895
+ new = ot.SinglePos()
896
+ new.Format = 2
897
+ new.ValueFormat = min_inclusive_rec_format
898
+ new.Coverage = ot.Coverage()
899
+ new.Coverage.glyphs = glyphs
900
+ new.ValueCount = num_glyphs
901
+ new.Value = [None] * num_glyphs
902
+ for singlePos in lst:
903
+ if singlePos.Format == 1:
904
+ val_rec = singlePos.Value
905
+ for gname in singlePos.Coverage.glyphs:
906
+ i = glyphs.index(gname)
907
+ new.Value[i] = copy.deepcopy(val_rec)
908
+ elif singlePos.Format == 2:
909
+ for j, gname in enumerate(singlePos.Coverage.glyphs):
910
+ val_rec = singlePos.Value[j]
911
+ i = glyphs.index(gname)
912
+ new.Value[i] = copy.deepcopy(val_rec)
913
+ return [new]
914
+
915
+
916
+ @AligningMerger.merger(ot.CursivePos)
917
+ def merge(merger, self, lst):
918
+ # Align them
919
+ glyphs, padded = _merge_GlyphOrders(
920
+ merger.font,
921
+ [l.Coverage.glyphs for l in lst],
922
+ [l.EntryExitRecord for l in lst],
923
+ )
924
+
925
+ self.Format = 1
926
+ self.Coverage = ot.Coverage()
927
+ self.Coverage.glyphs = glyphs
928
+ self.EntryExitRecord = []
929
+ for _ in glyphs:
930
+ rec = ot.EntryExitRecord()
931
+ rec.EntryAnchor = ot.Anchor()
932
+ rec.EntryAnchor.Format = 1
933
+ rec.ExitAnchor = ot.Anchor()
934
+ rec.ExitAnchor.Format = 1
935
+ self.EntryExitRecord.append(rec)
936
+ merger.mergeLists(self.EntryExitRecord, padded)
937
+ self.EntryExitCount = len(self.EntryExitRecord)
938
+
939
+
940
+ @AligningMerger.merger(ot.EntryExitRecord)
941
+ def merge(merger, self, lst):
942
+ if all(master.EntryAnchor is None for master in lst):
943
+ self.EntryAnchor = None
944
+ if all(master.ExitAnchor is None for master in lst):
945
+ self.ExitAnchor = None
946
+ merger.mergeObjects(self, lst)
947
+
948
+
949
+ @AligningMerger.merger(ot.Lookup)
950
+ def merge(merger, self, lst):
951
+ subtables = merger.lookup_subtables = [l.SubTable for l in lst]
952
+
953
+ # Remove Extension subtables
954
+ for l, sts in list(zip(lst, subtables)) + [(self, self.SubTable)]:
955
+ if not sts:
956
+ continue
957
+ if sts[0].__class__.__name__.startswith("Extension"):
958
+ if not allEqual([st.__class__ for st in sts]):
959
+ raise InconsistentExtensions(
960
+ merger,
961
+ expected="Extension",
962
+ got=[st.__class__.__name__ for st in sts],
963
+ )
964
+ if not allEqual([st.ExtensionLookupType for st in sts]):
965
+ raise InconsistentExtensions(merger)
966
+ l.LookupType = sts[0].ExtensionLookupType
967
+ new_sts = [st.ExtSubTable for st in sts]
968
+ del sts[:]
969
+ sts.extend(new_sts)
970
+
971
+ isPairPos = self.SubTable and isinstance(self.SubTable[0], ot.PairPos)
972
+
973
+ if isPairPos:
974
+ # AFDKO and feaLib sometimes generate two Format1 subtables instead of one.
975
+ # Merge those before continuing.
976
+ # https://github.com/fonttools/fonttools/issues/719
977
+ self.SubTable = _Lookup_PairPos_subtables_canonicalize(
978
+ self.SubTable, merger.font
979
+ )
980
+ subtables = merger.lookup_subtables = [
981
+ _Lookup_PairPos_subtables_canonicalize(st, merger.font) for st in subtables
982
+ ]
983
+ else:
984
+ isSinglePos = self.SubTable and isinstance(self.SubTable[0], ot.SinglePos)
985
+ if isSinglePos:
986
+ numSubtables = [len(st) for st in subtables]
987
+ if not all([nums == numSubtables[0] for nums in numSubtables]):
988
+ # Flatten list of SinglePos subtables to single Format 2 subtable,
989
+ # with all value records set to the rec format type.
990
+ # We use buildSinglePos() to optimize the lookup after merging.
991
+ valueFormatList = [t.ValueFormat for st in subtables for t in st]
992
+ # Find the minimum value record that can accomodate all the singlePos subtables.
993
+ mirf = reduce(ior, valueFormatList)
994
+ self.SubTable = _Lookup_SinglePos_subtables_flatten(
995
+ self.SubTable, merger.font, mirf
996
+ )
997
+ subtables = merger.lookup_subtables = [
998
+ _Lookup_SinglePos_subtables_flatten(st, merger.font, mirf)
999
+ for st in subtables
1000
+ ]
1001
+ flattened = True
1002
+ else:
1003
+ flattened = False
1004
+
1005
+ merger.mergeLists(self.SubTable, subtables)
1006
+ self.SubTableCount = len(self.SubTable)
1007
+
1008
+ if isPairPos:
1009
+ # If format-1 subtable created during canonicalization is empty, remove it.
1010
+ assert len(self.SubTable) >= 1 and self.SubTable[0].Format == 1
1011
+ if not self.SubTable[0].Coverage.glyphs:
1012
+ self.SubTable.pop(0)
1013
+ self.SubTableCount -= 1
1014
+
1015
+ # If format-2 subtable created during canonicalization is empty, remove it.
1016
+ assert len(self.SubTable) >= 1 and self.SubTable[-1].Format == 2
1017
+ if not self.SubTable[-1].Coverage.glyphs:
1018
+ self.SubTable.pop(-1)
1019
+ self.SubTableCount -= 1
1020
+
1021
+ # Compact the merged subtables
1022
+ # This is a good moment to do it because the compaction should create
1023
+ # smaller subtables, which may prevent overflows from happening.
1024
+ # Keep reading the value from the ENV until ufo2ft switches to the config system
1025
+ level = merger.font.cfg.get(
1026
+ "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL",
1027
+ default=_compression_level_from_env(),
1028
+ )
1029
+ if level != 0:
1030
+ log.info("Compacting GPOS...")
1031
+ self.SubTable = compact_pair_pos(merger.font, level, self.SubTable)
1032
+ self.SubTableCount = len(self.SubTable)
1033
+
1034
+ elif isSinglePos and flattened:
1035
+ singlePosTable = self.SubTable[0]
1036
+ glyphs = singlePosTable.Coverage.glyphs
1037
+ # We know that singlePosTable is Format 2, as this is set
1038
+ # in _Lookup_SinglePos_subtables_flatten.
1039
+ singlePosMapping = {
1040
+ gname: valRecord for gname, valRecord in zip(glyphs, singlePosTable.Value)
1041
+ }
1042
+ self.SubTable = buildSinglePos(
1043
+ singlePosMapping, merger.font.getReverseGlyphMap()
1044
+ )
1045
+ merger.mergeObjects(self, lst, exclude=["SubTable", "SubTableCount"])
1046
+
1047
+ del merger.lookup_subtables
1048
+
1049
+
1050
+ #
1051
+ # InstancerMerger
1052
+ #
1053
+
1054
+
1055
+ class InstancerMerger(AligningMerger):
1056
+ """A merger that takes multiple master fonts, and instantiates
1057
+ an instance."""
1058
+
1059
+ def __init__(self, font, model, location):
1060
+ Merger.__init__(self, font)
1061
+ self.model = model
1062
+ self.location = location
1063
+ self.masterScalars = model.getMasterScalars(location)
1064
+
1065
+
1066
+ @InstancerMerger.merger(ot.CaretValue)
1067
+ def merge(merger, self, lst):
1068
+ assert self.Format == 1
1069
+ Coords = [a.Coordinate for a in lst]
1070
+ model = merger.model
1071
+ masterScalars = merger.masterScalars
1072
+ self.Coordinate = otRound(
1073
+ model.interpolateFromValuesAndScalars(Coords, masterScalars)
1074
+ )
1075
+
1076
+
1077
+ @InstancerMerger.merger(ot.Anchor)
1078
+ def merge(merger, self, lst):
1079
+ assert self.Format == 1
1080
+ XCoords = [a.XCoordinate for a in lst]
1081
+ YCoords = [a.YCoordinate for a in lst]
1082
+ model = merger.model
1083
+ masterScalars = merger.masterScalars
1084
+ self.XCoordinate = otRound(
1085
+ model.interpolateFromValuesAndScalars(XCoords, masterScalars)
1086
+ )
1087
+ self.YCoordinate = otRound(
1088
+ model.interpolateFromValuesAndScalars(YCoords, masterScalars)
1089
+ )
1090
+
1091
+
1092
+ @InstancerMerger.merger(otBase.ValueRecord)
1093
+ def merge(merger, self, lst):
1094
+ model = merger.model
1095
+ masterScalars = merger.masterScalars
1096
+ # TODO Handle differing valueformats
1097
+ for name, tableName in [
1098
+ ("XAdvance", "XAdvDevice"),
1099
+ ("YAdvance", "YAdvDevice"),
1100
+ ("XPlacement", "XPlaDevice"),
1101
+ ("YPlacement", "YPlaDevice"),
1102
+ ]:
1103
+ assert not hasattr(self, tableName)
1104
+
1105
+ if hasattr(self, name):
1106
+ values = [getattr(a, name, 0) for a in lst]
1107
+ value = otRound(
1108
+ model.interpolateFromValuesAndScalars(values, masterScalars)
1109
+ )
1110
+ setattr(self, name, value)
1111
+
1112
+
1113
+ #
1114
+ # MutatorMerger
1115
+ #
1116
+
1117
+
1118
+ class MutatorMerger(AligningMerger):
1119
+ """A merger that takes a variable font, and instantiates
1120
+ an instance. While there's no "merging" to be done per se,
1121
+ the operation can benefit from many operations that the
1122
+ aligning merger does."""
1123
+
1124
+ def __init__(self, font, instancer, deleteVariations=True):
1125
+ Merger.__init__(self, font)
1126
+ self.instancer = instancer
1127
+ self.deleteVariations = deleteVariations
1128
+
1129
+
1130
+ @MutatorMerger.merger(ot.CaretValue)
1131
+ def merge(merger, self, lst):
1132
+ # Hack till we become selfless.
1133
+ self.__dict__ = lst[0].__dict__.copy()
1134
+
1135
+ if self.Format != 3:
1136
+ return
1137
+
1138
+ instancer = merger.instancer
1139
+ dev = self.DeviceTable
1140
+ if merger.deleteVariations:
1141
+ del self.DeviceTable
1142
+ if dev:
1143
+ assert dev.DeltaFormat == 0x8000
1144
+ varidx = (dev.StartSize << 16) + dev.EndSize
1145
+ delta = otRound(instancer[varidx])
1146
+ self.Coordinate += delta
1147
+
1148
+ if merger.deleteVariations:
1149
+ self.Format = 1
1150
+
1151
+
1152
+ @MutatorMerger.merger(ot.Anchor)
1153
+ def merge(merger, self, lst):
1154
+ # Hack till we become selfless.
1155
+ self.__dict__ = lst[0].__dict__.copy()
1156
+
1157
+ if self.Format != 3:
1158
+ return
1159
+
1160
+ instancer = merger.instancer
1161
+ for v in "XY":
1162
+ tableName = v + "DeviceTable"
1163
+ if not hasattr(self, tableName):
1164
+ continue
1165
+ dev = getattr(self, tableName)
1166
+ if merger.deleteVariations:
1167
+ delattr(self, tableName)
1168
+ if dev is None:
1169
+ continue
1170
+
1171
+ assert dev.DeltaFormat == 0x8000
1172
+ varidx = (dev.StartSize << 16) + dev.EndSize
1173
+ delta = otRound(instancer[varidx])
1174
+
1175
+ attr = v + "Coordinate"
1176
+ setattr(self, attr, getattr(self, attr) + delta)
1177
+
1178
+ if merger.deleteVariations:
1179
+ self.Format = 1
1180
+
1181
+
1182
+ @MutatorMerger.merger(otBase.ValueRecord)
1183
+ def merge(merger, self, lst):
1184
+ # Hack till we become selfless.
1185
+ self.__dict__ = lst[0].__dict__.copy()
1186
+
1187
+ instancer = merger.instancer
1188
+ for name, tableName in [
1189
+ ("XAdvance", "XAdvDevice"),
1190
+ ("YAdvance", "YAdvDevice"),
1191
+ ("XPlacement", "XPlaDevice"),
1192
+ ("YPlacement", "YPlaDevice"),
1193
+ ]:
1194
+ if not hasattr(self, tableName):
1195
+ continue
1196
+ dev = getattr(self, tableName)
1197
+ if merger.deleteVariations:
1198
+ delattr(self, tableName)
1199
+ if dev is None:
1200
+ continue
1201
+
1202
+ assert dev.DeltaFormat == 0x8000
1203
+ varidx = (dev.StartSize << 16) + dev.EndSize
1204
+ delta = otRound(instancer[varidx])
1205
+
1206
+ setattr(self, name, getattr(self, name, 0) + delta)
1207
+
1208
+
1209
+ #
1210
+ # VariationMerger
1211
+ #
1212
+
1213
+
1214
+ class VariationMerger(AligningMerger):
1215
+ """A merger that takes multiple master fonts, and builds a
1216
+ variable font."""
1217
+
1218
+ def __init__(self, model, axisTags, font):
1219
+ Merger.__init__(self, font)
1220
+ self.store_builder = varStore.OnlineVarStoreBuilder(axisTags)
1221
+ self.setModel(model)
1222
+
1223
+ def setModel(self, model):
1224
+ self.model = model
1225
+ self.store_builder.setModel(model)
1226
+
1227
+ def mergeThings(self, out, lst):
1228
+ masterModel = None
1229
+ origTTFs = None
1230
+ if None in lst:
1231
+ if allNone(lst):
1232
+ if out is not None:
1233
+ raise FoundANone(self, got=lst)
1234
+ return
1235
+
1236
+ # temporarily subset the list of master ttfs to the ones for which
1237
+ # master values are not None
1238
+ origTTFs = self.ttfs
1239
+ if self.ttfs:
1240
+ self.ttfs = subList([v is not None for v in lst], self.ttfs)
1241
+
1242
+ masterModel = self.model
1243
+ model, lst = masterModel.getSubModel(lst)
1244
+ self.setModel(model)
1245
+
1246
+ super(VariationMerger, self).mergeThings(out, lst)
1247
+
1248
+ if masterModel:
1249
+ self.setModel(masterModel)
1250
+ if origTTFs:
1251
+ self.ttfs = origTTFs
1252
+
1253
+
1254
+ def buildVarDevTable(store_builder, master_values):
1255
+ if allEqual(master_values):
1256
+ return master_values[0], None
1257
+ base, varIdx = store_builder.storeMasters(master_values)
1258
+ return base, builder.buildVarDevTable(varIdx)
1259
+
1260
+
1261
+ @VariationMerger.merger(ot.BaseCoord)
1262
+ def merge(merger, self, lst):
1263
+ if self.Format != 1:
1264
+ raise UnsupportedFormat(merger, subtable="a baseline coordinate")
1265
+ self.Coordinate, DeviceTable = buildVarDevTable(
1266
+ merger.store_builder, [a.Coordinate for a in lst]
1267
+ )
1268
+ if DeviceTable:
1269
+ self.Format = 3
1270
+ self.DeviceTable = DeviceTable
1271
+
1272
+
1273
+ @VariationMerger.merger(ot.CaretValue)
1274
+ def merge(merger, self, lst):
1275
+ if self.Format != 1:
1276
+ raise UnsupportedFormat(merger, subtable="a caret")
1277
+ self.Coordinate, DeviceTable = buildVarDevTable(
1278
+ merger.store_builder, [a.Coordinate for a in lst]
1279
+ )
1280
+ if DeviceTable:
1281
+ self.Format = 3
1282
+ self.DeviceTable = DeviceTable
1283
+
1284
+
1285
+ @VariationMerger.merger(ot.Anchor)
1286
+ def merge(merger, self, lst):
1287
+ if self.Format != 1:
1288
+ raise UnsupportedFormat(merger, subtable="an anchor")
1289
+ self.XCoordinate, XDeviceTable = buildVarDevTable(
1290
+ merger.store_builder, [a.XCoordinate for a in lst]
1291
+ )
1292
+ self.YCoordinate, YDeviceTable = buildVarDevTable(
1293
+ merger.store_builder, [a.YCoordinate for a in lst]
1294
+ )
1295
+ if XDeviceTable or YDeviceTable:
1296
+ self.Format = 3
1297
+ self.XDeviceTable = XDeviceTable
1298
+ self.YDeviceTable = YDeviceTable
1299
+
1300
+
1301
+ @VariationMerger.merger(otBase.ValueRecord)
1302
+ def merge(merger, self, lst):
1303
+ for name, tableName in [
1304
+ ("XAdvance", "XAdvDevice"),
1305
+ ("YAdvance", "YAdvDevice"),
1306
+ ("XPlacement", "XPlaDevice"),
1307
+ ("YPlacement", "YPlaDevice"),
1308
+ ]:
1309
+ if hasattr(self, name):
1310
+ value, deviceTable = buildVarDevTable(
1311
+ merger.store_builder, [getattr(a, name, 0) for a in lst]
1312
+ )
1313
+ setattr(self, name, value)
1314
+ if deviceTable:
1315
+ setattr(self, tableName, deviceTable)
1316
+
1317
+
1318
+ class COLRVariationMerger(VariationMerger):
1319
+ """A specialized VariationMerger that takes multiple master fonts containing
1320
+ COLRv1 tables, and builds a variable COLR font.
1321
+
1322
+ COLR tables are special in that variable subtables can be associated with
1323
+ multiple delta-set indices (via VarIndexBase).
1324
+ They also contain tables that must change their type (not simply the Format)
1325
+ as they become variable (e.g. Affine2x3 -> VarAffine2x3) so this merger takes
1326
+ care of that too.
1327
+ """
1328
+
1329
+ def __init__(self, model, axisTags, font, allowLayerReuse=True):
1330
+ VariationMerger.__init__(self, model, axisTags, font)
1331
+ # maps {tuple(varIdxes): VarIndexBase} to facilitate reuse of VarIndexBase
1332
+ # between variable tables with same varIdxes.
1333
+ self.varIndexCache = {}
1334
+ # flat list of all the varIdxes generated while merging
1335
+ self.varIdxes = []
1336
+ # set of id()s of the subtables that contain variations after merging
1337
+ # and need to be upgraded to the associated VarType.
1338
+ self.varTableIds = set()
1339
+ # we keep these around for rebuilding a LayerList while merging PaintColrLayers
1340
+ self.layers = []
1341
+ self.layerReuseCache = None
1342
+ if allowLayerReuse:
1343
+ self.layerReuseCache = LayerReuseCache()
1344
+ # flag to ensure BaseGlyphList is fully merged before LayerList gets processed
1345
+ self._doneBaseGlyphs = False
1346
+
1347
+ def mergeTables(self, font, master_ttfs, tableTags=("COLR",)):
1348
+ if "COLR" in tableTags and "COLR" in font:
1349
+ # The merger modifies the destination COLR table in-place. If this contains
1350
+ # multiple PaintColrLayers referencing the same layers from LayerList, it's
1351
+ # a problem because we may risk modifying the same paint more than once, or
1352
+ # worse, fail while attempting to do that.
1353
+ # We don't know whether the master COLR table was built with layer reuse
1354
+ # disabled, thus to be safe we rebuild its LayerList so that it contains only
1355
+ # unique layers referenced from non-overlapping PaintColrLayers throughout
1356
+ # the base paint graphs.
1357
+ self.expandPaintColrLayers(font["COLR"].table)
1358
+ VariationMerger.mergeTables(self, font, master_ttfs, tableTags)
1359
+
1360
+ def checkFormatEnum(self, out, lst, validate=lambda _: True):
1361
+ fmt = out.Format
1362
+ formatEnum = out.formatEnum
1363
+ ok = False
1364
+ try:
1365
+ fmt = formatEnum(fmt)
1366
+ except ValueError:
1367
+ pass
1368
+ else:
1369
+ ok = validate(fmt)
1370
+ if not ok:
1371
+ raise UnsupportedFormat(self, subtable=type(out).__name__, value=fmt)
1372
+ expected = fmt
1373
+ got = []
1374
+ for v in lst:
1375
+ fmt = getattr(v, "Format", None)
1376
+ try:
1377
+ fmt = formatEnum(fmt)
1378
+ except ValueError:
1379
+ pass
1380
+ got.append(fmt)
1381
+ if not allEqualTo(expected, got):
1382
+ raise InconsistentFormats(
1383
+ self,
1384
+ subtable=type(out).__name__,
1385
+ expected=expected,
1386
+ got=got,
1387
+ )
1388
+ return expected
1389
+
1390
+ def mergeSparseDict(self, out, lst):
1391
+ for k in out.keys():
1392
+ try:
1393
+ self.mergeThings(out[k], [v.get(k) for v in lst])
1394
+ except VarLibMergeError as e:
1395
+ e.stack.append(f"[{k!r}]")
1396
+ raise
1397
+
1398
+ def mergeAttrs(self, out, lst, attrs):
1399
+ for attr in attrs:
1400
+ value = getattr(out, attr)
1401
+ values = [getattr(item, attr) for item in lst]
1402
+ try:
1403
+ self.mergeThings(value, values)
1404
+ except VarLibMergeError as e:
1405
+ e.stack.append(f".{attr}")
1406
+ raise
1407
+
1408
+ def storeMastersForAttr(self, out, lst, attr):
1409
+ master_values = [getattr(item, attr) for item in lst]
1410
+
1411
+ # VarStore treats deltas for fixed-size floats as integers, so we
1412
+ # must convert master values to int before storing them in the builder
1413
+ # then back to float.
1414
+ is_fixed_size_float = False
1415
+ conv = out.getConverterByName(attr)
1416
+ if isinstance(conv, BaseFixedValue):
1417
+ is_fixed_size_float = True
1418
+ master_values = [conv.toInt(v) for v in master_values]
1419
+
1420
+ baseValue = master_values[0]
1421
+ varIdx = ot.NO_VARIATION_INDEX
1422
+ if not allEqual(master_values):
1423
+ baseValue, varIdx = self.store_builder.storeMasters(master_values)
1424
+
1425
+ if is_fixed_size_float:
1426
+ baseValue = conv.fromInt(baseValue)
1427
+
1428
+ return baseValue, varIdx
1429
+
1430
+ def storeVariationIndices(self, varIdxes) -> int:
1431
+ # try to reuse an existing VarIndexBase for the same varIdxes, or else
1432
+ # create a new one
1433
+ key = tuple(varIdxes)
1434
+ varIndexBase = self.varIndexCache.get(key)
1435
+
1436
+ if varIndexBase is None:
1437
+ # scan for a full match anywhere in the self.varIdxes
1438
+ for i in range(len(self.varIdxes) - len(varIdxes) + 1):
1439
+ if self.varIdxes[i : i + len(varIdxes)] == varIdxes:
1440
+ self.varIndexCache[key] = varIndexBase = i
1441
+ break
1442
+
1443
+ if varIndexBase is None:
1444
+ # try find a partial match at the end of the self.varIdxes
1445
+ for n in range(len(varIdxes) - 1, 0, -1):
1446
+ if self.varIdxes[-n:] == varIdxes[:n]:
1447
+ varIndexBase = len(self.varIdxes) - n
1448
+ self.varIndexCache[key] = varIndexBase
1449
+ self.varIdxes.extend(varIdxes[n:])
1450
+ break
1451
+
1452
+ if varIndexBase is None:
1453
+ # no match found, append at the end
1454
+ self.varIndexCache[key] = varIndexBase = len(self.varIdxes)
1455
+ self.varIdxes.extend(varIdxes)
1456
+
1457
+ return varIndexBase
1458
+
1459
+ def mergeVariableAttrs(self, out, lst, attrs) -> int:
1460
+ varIndexBase = ot.NO_VARIATION_INDEX
1461
+ varIdxes = []
1462
+ for attr in attrs:
1463
+ baseValue, varIdx = self.storeMastersForAttr(out, lst, attr)
1464
+ setattr(out, attr, baseValue)
1465
+ varIdxes.append(varIdx)
1466
+
1467
+ if any(v != ot.NO_VARIATION_INDEX for v in varIdxes):
1468
+ varIndexBase = self.storeVariationIndices(varIdxes)
1469
+
1470
+ return varIndexBase
1471
+
1472
+ @classmethod
1473
+ def convertSubTablesToVarType(cls, table):
1474
+ for path in dfs_base_table(
1475
+ table,
1476
+ skip_root=True,
1477
+ predicate=lambda path: (
1478
+ getattr(type(path[-1].value), "VarType", None) is not None
1479
+ ),
1480
+ ):
1481
+ st = path[-1]
1482
+ subTable = st.value
1483
+ varType = type(subTable).VarType
1484
+ newSubTable = varType()
1485
+ newSubTable.__dict__.update(subTable.__dict__)
1486
+ newSubTable.populateDefaults()
1487
+ parent = path[-2].value
1488
+ if st.index is not None:
1489
+ getattr(parent, st.name)[st.index] = newSubTable
1490
+ else:
1491
+ setattr(parent, st.name, newSubTable)
1492
+
1493
+ @staticmethod
1494
+ def expandPaintColrLayers(colr):
1495
+ """Rebuild LayerList without PaintColrLayers reuse.
1496
+
1497
+ Each base paint graph is fully DFS-traversed (with exception of PaintColrGlyph
1498
+ which are irrelevant for this); any layers referenced via PaintColrLayers are
1499
+ collected into a new LayerList and duplicated when reuse is detected, to ensure
1500
+ that all paints are distinct objects at the end of the process.
1501
+ PaintColrLayers's FirstLayerIndex/NumLayers are updated so that no overlap
1502
+ is left. Also, any consecutively nested PaintColrLayers are flattened.
1503
+ The COLR table's LayerList is replaced with the new unique layers.
1504
+ A side effect is also that any layer from the old LayerList which is not
1505
+ referenced by any PaintColrLayers is dropped.
1506
+ """
1507
+ if not colr.LayerList:
1508
+ # if no LayerList, there's nothing to expand
1509
+ return
1510
+ uniqueLayerIDs = set()
1511
+ newLayerList = []
1512
+ for rec in colr.BaseGlyphList.BaseGlyphPaintRecord:
1513
+ frontier = [rec.Paint]
1514
+ while frontier:
1515
+ paint = frontier.pop()
1516
+ if paint.Format == ot.PaintFormat.PaintColrGlyph:
1517
+ # don't traverse these, we treat them as constant for merging
1518
+ continue
1519
+ elif paint.Format == ot.PaintFormat.PaintColrLayers:
1520
+ # de-treeify any nested PaintColrLayers, append unique copies to
1521
+ # the new layer list and update PaintColrLayers index/count
1522
+ children = list(_flatten_layers(paint, colr))
1523
+ first_layer_index = len(newLayerList)
1524
+ for layer in children:
1525
+ if id(layer) in uniqueLayerIDs:
1526
+ layer = copy.deepcopy(layer)
1527
+ assert id(layer) not in uniqueLayerIDs
1528
+ newLayerList.append(layer)
1529
+ uniqueLayerIDs.add(id(layer))
1530
+ paint.FirstLayerIndex = first_layer_index
1531
+ paint.NumLayers = len(children)
1532
+ else:
1533
+ children = paint.getChildren(colr)
1534
+ frontier.extend(reversed(children))
1535
+ # sanity check all the new layers are distinct objects
1536
+ assert len(newLayerList) == len(uniqueLayerIDs)
1537
+ colr.LayerList.Paint = newLayerList
1538
+ colr.LayerList.LayerCount = len(newLayerList)
1539
+
1540
+
1541
+ @COLRVariationMerger.merger(ot.BaseGlyphList)
1542
+ def merge(merger, self, lst):
1543
+ # ignore BaseGlyphCount, allow sparse glyph sets across masters
1544
+ out = {rec.BaseGlyph: rec for rec in self.BaseGlyphPaintRecord}
1545
+ masters = [{rec.BaseGlyph: rec for rec in m.BaseGlyphPaintRecord} for m in lst]
1546
+
1547
+ for i, g in enumerate(out.keys()):
1548
+ try:
1549
+ # missing base glyphs don't participate in the merge
1550
+ merger.mergeThings(out[g], [v.get(g) for v in masters])
1551
+ except VarLibMergeError as e:
1552
+ e.stack.append(f".BaseGlyphPaintRecord[{i}]")
1553
+ e.cause["location"] = f"base glyph {g!r}"
1554
+ raise
1555
+
1556
+ merger._doneBaseGlyphs = True
1557
+
1558
+
1559
+ @COLRVariationMerger.merger(ot.LayerList)
1560
+ def merge(merger, self, lst):
1561
+ # nothing to merge for LayerList, assuming we have already merged all PaintColrLayers
1562
+ # found while traversing the paint graphs rooted at BaseGlyphPaintRecords.
1563
+ assert merger._doneBaseGlyphs, "BaseGlyphList must be merged before LayerList"
1564
+ # Simply flush the final list of layers and go home.
1565
+ self.LayerCount = len(merger.layers)
1566
+ self.Paint = merger.layers
1567
+
1568
+
1569
+ def _flatten_layers(root, colr):
1570
+ assert root.Format == ot.PaintFormat.PaintColrLayers
1571
+ for paint in root.getChildren(colr):
1572
+ if paint.Format == ot.PaintFormat.PaintColrLayers:
1573
+ yield from _flatten_layers(paint, colr)
1574
+ else:
1575
+ yield paint
1576
+
1577
+
1578
+ def _merge_PaintColrLayers(self, out, lst):
1579
+ # we only enforce that the (flat) number of layers is the same across all masters
1580
+ # but we allow FirstLayerIndex to differ to acommodate for sparse glyph sets.
1581
+
1582
+ out_layers = list(_flatten_layers(out, self.font["COLR"].table))
1583
+
1584
+ # sanity check ttfs are subset to current values (see VariationMerger.mergeThings)
1585
+ # before matching each master PaintColrLayers to its respective COLR by position
1586
+ assert len(self.ttfs) == len(lst)
1587
+ master_layerses = [
1588
+ list(_flatten_layers(lst[i], self.ttfs[i]["COLR"].table))
1589
+ for i in range(len(lst))
1590
+ ]
1591
+
1592
+ try:
1593
+ self.mergeLists(out_layers, master_layerses)
1594
+ except VarLibMergeError as e:
1595
+ # NOTE: This attribute doesn't actually exist in PaintColrLayers but it's
1596
+ # handy to have it in the stack trace for debugging.
1597
+ e.stack.append(".Layers")
1598
+ raise
1599
+
1600
+ # following block is very similar to LayerListBuilder._beforeBuildPaintColrLayers
1601
+ # but I couldn't find a nice way to share the code between the two...
1602
+
1603
+ if self.layerReuseCache is not None:
1604
+ # successful reuse can make the list smaller
1605
+ out_layers = self.layerReuseCache.try_reuse(out_layers)
1606
+
1607
+ # if the list is still too big we need to tree-fy it
1608
+ is_tree = len(out_layers) > MAX_PAINT_COLR_LAYER_COUNT
1609
+ out_layers = build_n_ary_tree(out_layers, n=MAX_PAINT_COLR_LAYER_COUNT)
1610
+
1611
+ # We now have a tree of sequences with Paint leaves.
1612
+ # Convert the sequences into PaintColrLayers.
1613
+ def listToColrLayers(paint):
1614
+ if isinstance(paint, list):
1615
+ layers = [listToColrLayers(l) for l in paint]
1616
+ paint = ot.Paint()
1617
+ paint.Format = int(ot.PaintFormat.PaintColrLayers)
1618
+ paint.NumLayers = len(layers)
1619
+ paint.FirstLayerIndex = len(self.layers)
1620
+ self.layers.extend(layers)
1621
+ if self.layerReuseCache is not None:
1622
+ self.layerReuseCache.add(layers, paint.FirstLayerIndex)
1623
+ return paint
1624
+
1625
+ out_layers = [listToColrLayers(l) for l in out_layers]
1626
+
1627
+ if len(out_layers) == 1 and out_layers[0].Format == ot.PaintFormat.PaintColrLayers:
1628
+ # special case when the reuse cache finds a single perfect PaintColrLayers match
1629
+ # (it can only come from a successful reuse, _flatten_layers has gotten rid of
1630
+ # all nested PaintColrLayers already); we assign it directly and avoid creating
1631
+ # an extra table
1632
+ out.NumLayers = out_layers[0].NumLayers
1633
+ out.FirstLayerIndex = out_layers[0].FirstLayerIndex
1634
+ else:
1635
+ out.NumLayers = len(out_layers)
1636
+ out.FirstLayerIndex = len(self.layers)
1637
+
1638
+ self.layers.extend(out_layers)
1639
+
1640
+ # Register our parts for reuse provided we aren't a tree
1641
+ # If we are a tree the leaves registered for reuse and that will suffice
1642
+ if self.layerReuseCache is not None and not is_tree:
1643
+ self.layerReuseCache.add(out_layers, out.FirstLayerIndex)
1644
+
1645
+
1646
+ @COLRVariationMerger.merger((ot.Paint, ot.ClipBox))
1647
+ def merge(merger, self, lst):
1648
+ fmt = merger.checkFormatEnum(self, lst, lambda fmt: not fmt.is_variable())
1649
+
1650
+ if fmt is ot.PaintFormat.PaintColrLayers:
1651
+ _merge_PaintColrLayers(merger, self, lst)
1652
+ return
1653
+
1654
+ varFormat = fmt.as_variable()
1655
+
1656
+ varAttrs = ()
1657
+ if varFormat is not None:
1658
+ varAttrs = otBase.getVariableAttrs(type(self), varFormat)
1659
+ staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs)
1660
+
1661
+ merger.mergeAttrs(self, lst, staticAttrs)
1662
+
1663
+ varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs)
1664
+
1665
+ subTables = [st.value for st in self.iterSubTables()]
1666
+
1667
+ # Convert table to variable if itself has variations or any subtables have
1668
+ isVariable = varIndexBase != ot.NO_VARIATION_INDEX or any(
1669
+ id(table) in merger.varTableIds for table in subTables
1670
+ )
1671
+
1672
+ if isVariable:
1673
+ if varAttrs:
1674
+ # Some PaintVar* don't have any scalar attributes that can vary,
1675
+ # only indirect offsets to other variable subtables, thus have
1676
+ # no VarIndexBase of their own (e.g. PaintVarTransform)
1677
+ self.VarIndexBase = varIndexBase
1678
+
1679
+ if subTables:
1680
+ # Convert Affine2x3 -> VarAffine2x3, ColorLine -> VarColorLine, etc.
1681
+ merger.convertSubTablesToVarType(self)
1682
+
1683
+ assert varFormat is not None
1684
+ self.Format = int(varFormat)
1685
+
1686
+
1687
+ @COLRVariationMerger.merger((ot.Affine2x3, ot.ColorStop))
1688
+ def merge(merger, self, lst):
1689
+ varType = type(self).VarType
1690
+
1691
+ varAttrs = otBase.getVariableAttrs(varType)
1692
+ staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs)
1693
+
1694
+ merger.mergeAttrs(self, lst, staticAttrs)
1695
+
1696
+ varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs)
1697
+
1698
+ if varIndexBase != ot.NO_VARIATION_INDEX:
1699
+ self.VarIndexBase = varIndexBase
1700
+ # mark as having variations so the parent table will convert to Var{Type}
1701
+ merger.varTableIds.add(id(self))
1702
+
1703
+
1704
+ @COLRVariationMerger.merger(ot.ColorLine)
1705
+ def merge(merger, self, lst):
1706
+ merger.mergeAttrs(self, lst, (c.name for c in self.getConverters()))
1707
+
1708
+ if any(id(stop) in merger.varTableIds for stop in self.ColorStop):
1709
+ merger.convertSubTablesToVarType(self)
1710
+ merger.varTableIds.add(id(self))
1711
+
1712
+
1713
+ @COLRVariationMerger.merger(ot.ClipList, "clips")
1714
+ def merge(merger, self, lst):
1715
+ # 'sparse' in that we allow non-default masters to omit ClipBox entries
1716
+ # for some/all glyphs (i.e. they don't participate)
1717
+ merger.mergeSparseDict(self, lst)