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,2311 @@
1
+ """_g_l_y_f.py -- Converter classes for the 'glyf' table."""
2
+
3
+ from collections import namedtuple
4
+ from fontTools.misc import sstruct
5
+ from fontTools import ttLib
6
+ from fontTools import version
7
+ from fontTools.misc.transform import DecomposedTransform
8
+ from fontTools.misc.textTools import tostr, safeEval, pad
9
+ from fontTools.misc.arrayTools import updateBounds, pointInRect
10
+ from fontTools.misc.bezierTools import calcQuadraticBounds
11
+ from fontTools.misc.fixedTools import (
12
+ fixedToFloat as fi2fl,
13
+ floatToFixed as fl2fi,
14
+ floatToFixedToStr as fl2str,
15
+ strToFixedToFloat as str2fl,
16
+ )
17
+ from fontTools.misc.roundTools import noRound, otRound
18
+ from fontTools.misc.vector import Vector
19
+ from numbers import Number
20
+ from . import DefaultTable
21
+ from . import ttProgram
22
+ import sys
23
+ import struct
24
+ import array
25
+ import logging
26
+ import math
27
+ import os
28
+ from fontTools.misc import xmlWriter
29
+ from fontTools.misc.filenames import userNameToFileName
30
+ from fontTools.misc.loggingTools import deprecateFunction
31
+ from enum import IntFlag
32
+ from functools import partial
33
+ from types import SimpleNamespace
34
+ from typing import Set
35
+
36
+ log = logging.getLogger(__name__)
37
+
38
+ # We compute the version the same as is computed in ttlib/__init__
39
+ # so that we can write 'ttLibVersion' attribute of the glyf TTX files
40
+ # when glyf is written to separate files.
41
+ version = ".".join(version.split(".")[:2])
42
+
43
+ #
44
+ # The Apple and MS rasterizers behave differently for
45
+ # scaled composite components: one does scale first and then translate
46
+ # and the other does it vice versa. MS defined some flags to indicate
47
+ # the difference, but it seems nobody actually _sets_ those flags.
48
+ #
49
+ # Funny thing: Apple seems to _only_ do their thing in the
50
+ # WE_HAVE_A_SCALE (eg. Chicago) case, and not when it's WE_HAVE_AN_X_AND_Y_SCALE
51
+ # (eg. Charcoal)...
52
+ #
53
+ SCALE_COMPONENT_OFFSET_DEFAULT = 0 # 0 == MS, 1 == Apple
54
+
55
+
56
+ class table__g_l_y_f(DefaultTable.DefaultTable):
57
+ """Glyph Data table
58
+
59
+ This class represents the `glyf <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf>`_
60
+ table, which contains outlines for glyphs in TrueType format. In many cases,
61
+ it is easier to access and manipulate glyph outlines through the ``GlyphSet``
62
+ object returned from :py:meth:`fontTools.ttLib.ttFont.getGlyphSet`::
63
+
64
+ >> from fontTools.pens.boundsPen import BoundsPen
65
+ >> glyphset = font.getGlyphSet()
66
+ >> bp = BoundsPen(glyphset)
67
+ >> glyphset["A"].draw(bp)
68
+ >> bp.bounds
69
+ (19, 0, 633, 716)
70
+
71
+ However, this class can be used for low-level access to the ``glyf`` table data.
72
+ Objects of this class support dictionary-like access, mapping glyph names to
73
+ :py:class:`Glyph` objects::
74
+
75
+ >> glyf = font["glyf"]
76
+ >> len(glyf["Aacute"].components)
77
+ 2
78
+
79
+ Note that when adding glyphs to the font via low-level access to the ``glyf``
80
+ table, the new glyphs must also be added to the ``hmtx``/``vmtx`` table::
81
+
82
+ >> font["glyf"]["divisionslash"] = Glyph()
83
+ >> font["hmtx"]["divisionslash"] = (640, 0)
84
+
85
+ """
86
+
87
+ dependencies = ["fvar"]
88
+
89
+ # this attribute controls the amount of padding applied to glyph data upon compile.
90
+ # Glyph lenghts are aligned to multiples of the specified value.
91
+ # Allowed values are (0, 1, 2, 4). '0' means no padding; '1' (default) also means
92
+ # no padding, except for when padding would allow to use short loca offsets.
93
+ padding = 1
94
+
95
+ def decompile(self, data, ttFont):
96
+ self.axisTags = (
97
+ [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else []
98
+ )
99
+ loca = ttFont["loca"]
100
+ pos = int(loca[0])
101
+ nextPos = 0
102
+ noname = 0
103
+ self.glyphs = {}
104
+ self.glyphOrder = glyphOrder = ttFont.getGlyphOrder()
105
+ self._reverseGlyphOrder = {}
106
+ for i in range(0, len(loca) - 1):
107
+ try:
108
+ glyphName = glyphOrder[i]
109
+ except IndexError:
110
+ noname = noname + 1
111
+ glyphName = "ttxautoglyph%s" % i
112
+ nextPos = int(loca[i + 1])
113
+ glyphdata = data[pos:nextPos]
114
+ if len(glyphdata) != (nextPos - pos):
115
+ raise ttLib.TTLibError("not enough 'glyf' table data")
116
+ glyph = Glyph(glyphdata)
117
+ self.glyphs[glyphName] = glyph
118
+ pos = nextPos
119
+ if len(data) - nextPos >= 4:
120
+ log.warning(
121
+ "too much 'glyf' table data: expected %d, received %d bytes",
122
+ nextPos,
123
+ len(data),
124
+ )
125
+ if noname:
126
+ log.warning("%s glyphs have no name", noname)
127
+ if ttFont.lazy is False: # Be lazy for None and True
128
+ self.ensureDecompiled()
129
+
130
+ def ensureDecompiled(self, recurse=False):
131
+ # The recurse argument is unused, but part of the signature of
132
+ # ensureDecompiled across the library.
133
+ for glyph in self.glyphs.values():
134
+ glyph.expand(self)
135
+
136
+ def compile(self, ttFont):
137
+ optimizeSpeed = ttFont.cfg[ttLib.OPTIMIZE_FONT_SPEED]
138
+
139
+ self.axisTags = (
140
+ [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else []
141
+ )
142
+ if not hasattr(self, "glyphOrder"):
143
+ self.glyphOrder = ttFont.getGlyphOrder()
144
+ padding = self.padding
145
+ assert padding in (0, 1, 2, 4)
146
+ locations = []
147
+ currentLocation = 0
148
+ dataList = []
149
+ recalcBBoxes = ttFont.recalcBBoxes
150
+ boundsDone = set()
151
+ for glyphName in self.glyphOrder:
152
+ glyph = self.glyphs[glyphName]
153
+ glyphData = glyph.compile(
154
+ self,
155
+ recalcBBoxes,
156
+ boundsDone=boundsDone,
157
+ optimizeSize=not optimizeSpeed,
158
+ )
159
+ if padding > 1:
160
+ glyphData = pad(glyphData, size=padding)
161
+ locations.append(currentLocation)
162
+ currentLocation = currentLocation + len(glyphData)
163
+ dataList.append(glyphData)
164
+ locations.append(currentLocation)
165
+
166
+ if padding == 1 and currentLocation < 0x20000:
167
+ # See if we can pad any odd-lengthed glyphs to allow loca
168
+ # table to use the short offsets.
169
+ indices = [
170
+ i for i, glyphData in enumerate(dataList) if len(glyphData) % 2 == 1
171
+ ]
172
+ if indices and currentLocation + len(indices) < 0x20000:
173
+ # It fits. Do it.
174
+ for i in indices:
175
+ dataList[i] += b"\0"
176
+ currentLocation = 0
177
+ for i, glyphData in enumerate(dataList):
178
+ locations[i] = currentLocation
179
+ currentLocation += len(glyphData)
180
+ locations[len(dataList)] = currentLocation
181
+
182
+ data = b"".join(dataList)
183
+ if "loca" in ttFont:
184
+ ttFont["loca"].set(locations)
185
+ if "maxp" in ttFont:
186
+ ttFont["maxp"].numGlyphs = len(self.glyphs)
187
+ if not data:
188
+ # As a special case when all glyph in the font are empty, add a zero byte
189
+ # to the table, so that OTS doesn’t reject it, and to make the table work
190
+ # on Windows as well.
191
+ # See https://github.com/khaledhosny/ots/issues/52
192
+ data = b"\0"
193
+ return data
194
+
195
+ def toXML(self, writer, ttFont, splitGlyphs=False):
196
+ notice = (
197
+ "The xMin, yMin, xMax and yMax values\n"
198
+ "will be recalculated by the compiler."
199
+ )
200
+ glyphNames = ttFont.getGlyphNames()
201
+ if not splitGlyphs:
202
+ writer.newline()
203
+ writer.comment(notice)
204
+ writer.newline()
205
+ writer.newline()
206
+ numGlyphs = len(glyphNames)
207
+ if splitGlyphs:
208
+ path, ext = os.path.splitext(writer.file.name)
209
+ existingGlyphFiles = set()
210
+ for glyphName in glyphNames:
211
+ glyph = self.get(glyphName)
212
+ if glyph is None:
213
+ log.warning("glyph '%s' does not exist in glyf table", glyphName)
214
+ continue
215
+ if glyph.numberOfContours:
216
+ if splitGlyphs:
217
+ glyphPath = userNameToFileName(
218
+ tostr(glyphName, "utf-8"),
219
+ existingGlyphFiles,
220
+ prefix=path + ".",
221
+ suffix=ext,
222
+ )
223
+ existingGlyphFiles.add(glyphPath.lower())
224
+ glyphWriter = xmlWriter.XMLWriter(
225
+ glyphPath,
226
+ idlefunc=writer.idlefunc,
227
+ newlinestr=writer.newlinestr,
228
+ )
229
+ glyphWriter.begintag("ttFont", ttLibVersion=version)
230
+ glyphWriter.newline()
231
+ glyphWriter.begintag("glyf")
232
+ glyphWriter.newline()
233
+ glyphWriter.comment(notice)
234
+ glyphWriter.newline()
235
+ writer.simpletag("TTGlyph", src=os.path.basename(glyphPath))
236
+ else:
237
+ glyphWriter = writer
238
+ glyphWriter.begintag(
239
+ "TTGlyph",
240
+ [
241
+ ("name", glyphName),
242
+ ("xMin", glyph.xMin),
243
+ ("yMin", glyph.yMin),
244
+ ("xMax", glyph.xMax),
245
+ ("yMax", glyph.yMax),
246
+ ],
247
+ )
248
+ glyphWriter.newline()
249
+ glyph.toXML(glyphWriter, ttFont)
250
+ glyphWriter.endtag("TTGlyph")
251
+ glyphWriter.newline()
252
+ if splitGlyphs:
253
+ glyphWriter.endtag("glyf")
254
+ glyphWriter.newline()
255
+ glyphWriter.endtag("ttFont")
256
+ glyphWriter.newline()
257
+ glyphWriter.close()
258
+ else:
259
+ writer.simpletag("TTGlyph", name=glyphName)
260
+ writer.comment("contains no outline data")
261
+ if not splitGlyphs:
262
+ writer.newline()
263
+ writer.newline()
264
+
265
+ def fromXML(self, name, attrs, content, ttFont):
266
+ if name != "TTGlyph":
267
+ return
268
+ if not hasattr(self, "glyphs"):
269
+ self.glyphs = {}
270
+ if not hasattr(self, "glyphOrder"):
271
+ self.glyphOrder = ttFont.getGlyphOrder()
272
+ glyphName = attrs["name"]
273
+ log.debug("unpacking glyph '%s'", glyphName)
274
+ glyph = Glyph()
275
+ for attr in ["xMin", "yMin", "xMax", "yMax"]:
276
+ setattr(glyph, attr, safeEval(attrs.get(attr, "0")))
277
+ self.glyphs[glyphName] = glyph
278
+ for element in content:
279
+ if not isinstance(element, tuple):
280
+ continue
281
+ name, attrs, content = element
282
+ glyph.fromXML(name, attrs, content, ttFont)
283
+ if not ttFont.recalcBBoxes:
284
+ glyph.compact(self, 0)
285
+
286
+ def setGlyphOrder(self, glyphOrder):
287
+ """Sets the glyph order
288
+
289
+ Args:
290
+ glyphOrder ([str]): List of glyph names in order.
291
+ """
292
+ self.glyphOrder = glyphOrder
293
+ self._reverseGlyphOrder = {}
294
+
295
+ def getGlyphName(self, glyphID):
296
+ """Returns the name for the glyph with the given ID.
297
+
298
+ Raises a ``KeyError`` if the glyph name is not found in the font.
299
+ """
300
+ return self.glyphOrder[glyphID]
301
+
302
+ def _buildReverseGlyphOrderDict(self):
303
+ self._reverseGlyphOrder = d = {}
304
+ for glyphID, glyphName in enumerate(self.glyphOrder):
305
+ d[glyphName] = glyphID
306
+
307
+ def getGlyphID(self, glyphName):
308
+ """Returns the ID of the glyph with the given name.
309
+
310
+ Raises a ``ValueError`` if the glyph is not found in the font.
311
+ """
312
+ glyphOrder = self.glyphOrder
313
+ id = getattr(self, "_reverseGlyphOrder", {}).get(glyphName)
314
+ if id is None or id >= len(glyphOrder) or glyphOrder[id] != glyphName:
315
+ self._buildReverseGlyphOrderDict()
316
+ id = self._reverseGlyphOrder.get(glyphName)
317
+ if id is None:
318
+ raise ValueError(glyphName)
319
+ return id
320
+
321
+ def removeHinting(self):
322
+ """Removes TrueType hints from all glyphs in the glyphset.
323
+
324
+ See :py:meth:`Glyph.removeHinting`.
325
+ """
326
+ for glyph in self.glyphs.values():
327
+ glyph.removeHinting()
328
+
329
+ def keys(self):
330
+ return self.glyphs.keys()
331
+
332
+ def has_key(self, glyphName):
333
+ return glyphName in self.glyphs
334
+
335
+ __contains__ = has_key
336
+
337
+ def get(self, glyphName, default=None):
338
+ glyph = self.glyphs.get(glyphName, default)
339
+ if glyph is not None:
340
+ glyph.expand(self)
341
+ return glyph
342
+
343
+ def __getitem__(self, glyphName):
344
+ glyph = self.glyphs[glyphName]
345
+ glyph.expand(self)
346
+ return glyph
347
+
348
+ def __setitem__(self, glyphName, glyph):
349
+ self.glyphs[glyphName] = glyph
350
+ if glyphName not in self.glyphOrder:
351
+ self.glyphOrder.append(glyphName)
352
+
353
+ def __delitem__(self, glyphName):
354
+ del self.glyphs[glyphName]
355
+ self.glyphOrder.remove(glyphName)
356
+
357
+ def __len__(self):
358
+ assert len(self.glyphOrder) == len(self.glyphs)
359
+ return len(self.glyphs)
360
+
361
+ def _getPhantomPoints(self, glyphName, hMetrics, vMetrics=None):
362
+ """Compute the four "phantom points" for the given glyph from its bounding box
363
+ and the horizontal and vertical advance widths and sidebearings stored in the
364
+ ttFont's "hmtx" and "vmtx" tables.
365
+
366
+ 'hMetrics' should be ttFont['hmtx'].metrics.
367
+
368
+ 'vMetrics' should be ttFont['vmtx'].metrics if there is "vmtx" or None otherwise.
369
+ If there is no vMetrics passed in, vertical phantom points are set to the zero coordinate.
370
+
371
+ https://docs.microsoft.com/en-us/typography/opentype/spec/tt_instructing_glyphs#phantoms
372
+ """
373
+ glyph = self[glyphName]
374
+ if not hasattr(glyph, "xMin"):
375
+ glyph.recalcBounds(self)
376
+
377
+ horizontalAdvanceWidth, leftSideBearing = hMetrics[glyphName]
378
+ leftSideX = glyph.xMin - leftSideBearing
379
+ rightSideX = leftSideX + horizontalAdvanceWidth
380
+
381
+ if vMetrics:
382
+ verticalAdvanceWidth, topSideBearing = vMetrics[glyphName]
383
+ topSideY = topSideBearing + glyph.yMax
384
+ bottomSideY = topSideY - verticalAdvanceWidth
385
+ else:
386
+ bottomSideY = topSideY = 0
387
+
388
+ return [
389
+ (leftSideX, 0),
390
+ (rightSideX, 0),
391
+ (0, topSideY),
392
+ (0, bottomSideY),
393
+ ]
394
+
395
+ def _getCoordinatesAndControls(
396
+ self, glyphName, hMetrics, vMetrics=None, *, round=otRound
397
+ ):
398
+ """Return glyph coordinates and controls as expected by "gvar" table.
399
+
400
+ The coordinates includes four "phantom points" for the glyph metrics,
401
+ as mandated by the "gvar" spec.
402
+
403
+ The glyph controls is a namedtuple with the following attributes:
404
+ - numberOfContours: -1 for composite glyphs.
405
+ - endPts: list of indices of end points for each contour in simple
406
+ glyphs, or component indices in composite glyphs (used for IUP
407
+ optimization).
408
+ - flags: array of contour point flags for simple glyphs (None for
409
+ composite glyphs).
410
+ - components: list of base glyph names (str) for each component in
411
+ composite glyphs (None for simple glyphs).
412
+
413
+ The "hMetrics" and vMetrics are used to compute the "phantom points" (see
414
+ the "_getPhantomPoints" method).
415
+
416
+ Return None if the requested glyphName is not present.
417
+ """
418
+ glyph = self.get(glyphName)
419
+ if glyph is None:
420
+ return None
421
+ if glyph.isComposite():
422
+ coords = GlyphCoordinates(
423
+ [(getattr(c, "x", 0), getattr(c, "y", 0)) for c in glyph.components]
424
+ )
425
+ controls = _GlyphControls(
426
+ numberOfContours=glyph.numberOfContours,
427
+ endPts=list(range(len(glyph.components))),
428
+ flags=None,
429
+ components=[
430
+ (c.glyphName, getattr(c, "transform", None))
431
+ for c in glyph.components
432
+ ],
433
+ )
434
+ else:
435
+ coords, endPts, flags = glyph.getCoordinates(self)
436
+ coords = coords.copy()
437
+ controls = _GlyphControls(
438
+ numberOfContours=glyph.numberOfContours,
439
+ endPts=endPts,
440
+ flags=flags,
441
+ components=None,
442
+ )
443
+ # Add phantom points for (left, right, top, bottom) positions.
444
+ phantomPoints = self._getPhantomPoints(glyphName, hMetrics, vMetrics)
445
+ coords.extend(phantomPoints)
446
+ coords.toInt(round=round)
447
+ return coords, controls
448
+
449
+ def _setCoordinates(self, glyphName, coord, hMetrics, vMetrics=None):
450
+ """Set coordinates and metrics for the given glyph.
451
+
452
+ "coord" is an array of GlyphCoordinates which must include the "phantom
453
+ points" as the last four coordinates.
454
+
455
+ Both the horizontal/vertical advances and left/top sidebearings in "hmtx"
456
+ and "vmtx" tables (if any) are updated from four phantom points and
457
+ the glyph's bounding boxes.
458
+
459
+ The "hMetrics" and vMetrics are used to propagate "phantom points"
460
+ into "hmtx" and "vmtx" tables if desired. (see the "_getPhantomPoints"
461
+ method).
462
+ """
463
+ glyph = self[glyphName]
464
+
465
+ # Handle phantom points for (left, right, top, bottom) positions.
466
+ assert len(coord) >= 4
467
+ leftSideX = coord[-4][0]
468
+ rightSideX = coord[-3][0]
469
+ topSideY = coord[-2][1]
470
+ bottomSideY = coord[-1][1]
471
+
472
+ coord = coord[:-4]
473
+
474
+ if glyph.isComposite():
475
+ assert len(coord) == len(glyph.components)
476
+ for p, comp in zip(coord, glyph.components):
477
+ if hasattr(comp, "x"):
478
+ comp.x, comp.y = p
479
+ elif glyph.numberOfContours == 0:
480
+ assert len(coord) == 0
481
+ else:
482
+ assert len(coord) == len(glyph.coordinates)
483
+ glyph.coordinates = GlyphCoordinates(coord)
484
+
485
+ glyph.recalcBounds(self, boundsDone=set())
486
+
487
+ horizontalAdvanceWidth = otRound(rightSideX - leftSideX)
488
+ if horizontalAdvanceWidth < 0:
489
+ # unlikely, but it can happen, see:
490
+ # https://github.com/fonttools/fonttools/pull/1198
491
+ horizontalAdvanceWidth = 0
492
+ leftSideBearing = otRound(glyph.xMin - leftSideX)
493
+ hMetrics[glyphName] = horizontalAdvanceWidth, leftSideBearing
494
+
495
+ if vMetrics is not None:
496
+ verticalAdvanceWidth = otRound(topSideY - bottomSideY)
497
+ if verticalAdvanceWidth < 0: # unlikely but do the same as horizontal
498
+ verticalAdvanceWidth = 0
499
+ topSideBearing = otRound(topSideY - glyph.yMax)
500
+ vMetrics[glyphName] = verticalAdvanceWidth, topSideBearing
501
+
502
+ # Deprecated
503
+
504
+ def _synthesizeVMetrics(self, glyphName, ttFont, defaultVerticalOrigin):
505
+ """This method is wrong and deprecated.
506
+ For rationale see:
507
+ https://github.com/fonttools/fonttools/pull/2266/files#r613569473
508
+ """
509
+ vMetrics = getattr(ttFont.get("vmtx"), "metrics", None)
510
+ if vMetrics is None:
511
+ verticalAdvanceWidth = ttFont["head"].unitsPerEm
512
+ topSideY = getattr(ttFont.get("hhea"), "ascent", None)
513
+ if topSideY is None:
514
+ if defaultVerticalOrigin is not None:
515
+ topSideY = defaultVerticalOrigin
516
+ else:
517
+ topSideY = verticalAdvanceWidth
518
+ glyph = self[glyphName]
519
+ glyph.recalcBounds(self)
520
+ topSideBearing = otRound(topSideY - glyph.yMax)
521
+ vMetrics = {glyphName: (verticalAdvanceWidth, topSideBearing)}
522
+ return vMetrics
523
+
524
+ @deprecateFunction("use '_getPhantomPoints' instead", category=DeprecationWarning)
525
+ def getPhantomPoints(self, glyphName, ttFont, defaultVerticalOrigin=None):
526
+ """Old public name for self._getPhantomPoints().
527
+ See: https://github.com/fonttools/fonttools/pull/2266"""
528
+ hMetrics = ttFont["hmtx"].metrics
529
+ vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin)
530
+ return self._getPhantomPoints(glyphName, hMetrics, vMetrics)
531
+
532
+ @deprecateFunction(
533
+ "use '_getCoordinatesAndControls' instead", category=DeprecationWarning
534
+ )
535
+ def getCoordinatesAndControls(self, glyphName, ttFont, defaultVerticalOrigin=None):
536
+ """Old public name for self._getCoordinatesAndControls().
537
+ See: https://github.com/fonttools/fonttools/pull/2266"""
538
+ hMetrics = ttFont["hmtx"].metrics
539
+ vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin)
540
+ return self._getCoordinatesAndControls(glyphName, hMetrics, vMetrics)
541
+
542
+ @deprecateFunction("use '_setCoordinates' instead", category=DeprecationWarning)
543
+ def setCoordinates(self, glyphName, ttFont):
544
+ """Old public name for self._setCoordinates().
545
+ See: https://github.com/fonttools/fonttools/pull/2266"""
546
+ hMetrics = ttFont["hmtx"].metrics
547
+ vMetrics = getattr(ttFont.get("vmtx"), "metrics", None)
548
+ self._setCoordinates(glyphName, hMetrics, vMetrics)
549
+
550
+
551
+ _GlyphControls = namedtuple(
552
+ "_GlyphControls", "numberOfContours endPts flags components"
553
+ )
554
+
555
+
556
+ glyphHeaderFormat = """
557
+ > # big endian
558
+ numberOfContours: h
559
+ xMin: h
560
+ yMin: h
561
+ xMax: h
562
+ yMax: h
563
+ """
564
+
565
+ # flags
566
+ flagOnCurve = 0x01
567
+ flagXShort = 0x02
568
+ flagYShort = 0x04
569
+ flagRepeat = 0x08
570
+ flagXsame = 0x10
571
+ flagYsame = 0x20
572
+ flagOverlapSimple = 0x40
573
+ flagCubic = 0x80
574
+
575
+ # These flags are kept for XML output after decompiling the coordinates
576
+ keepFlags = flagOnCurve + flagOverlapSimple + flagCubic
577
+
578
+ _flagSignBytes = {
579
+ 0: 2,
580
+ flagXsame: 0,
581
+ flagXShort | flagXsame: +1,
582
+ flagXShort: -1,
583
+ flagYsame: 0,
584
+ flagYShort | flagYsame: +1,
585
+ flagYShort: -1,
586
+ }
587
+
588
+
589
+ def flagBest(x, y, onCurve):
590
+ """For a given x,y delta pair, returns the flag that packs this pair
591
+ most efficiently, as well as the number of byte cost of such flag."""
592
+
593
+ flag = flagOnCurve if onCurve else 0
594
+ cost = 0
595
+ # do x
596
+ if x == 0:
597
+ flag = flag | flagXsame
598
+ elif -255 <= x <= 255:
599
+ flag = flag | flagXShort
600
+ if x > 0:
601
+ flag = flag | flagXsame
602
+ cost += 1
603
+ else:
604
+ cost += 2
605
+ # do y
606
+ if y == 0:
607
+ flag = flag | flagYsame
608
+ elif -255 <= y <= 255:
609
+ flag = flag | flagYShort
610
+ if y > 0:
611
+ flag = flag | flagYsame
612
+ cost += 1
613
+ else:
614
+ cost += 2
615
+ return flag, cost
616
+
617
+
618
+ def flagFits(newFlag, oldFlag, mask):
619
+ newBytes = _flagSignBytes[newFlag & mask]
620
+ oldBytes = _flagSignBytes[oldFlag & mask]
621
+ return newBytes == oldBytes or abs(newBytes) > abs(oldBytes)
622
+
623
+
624
+ def flagSupports(newFlag, oldFlag):
625
+ return (
626
+ (oldFlag & flagOnCurve) == (newFlag & flagOnCurve)
627
+ and flagFits(newFlag, oldFlag, flagXsame | flagXShort)
628
+ and flagFits(newFlag, oldFlag, flagYsame | flagYShort)
629
+ )
630
+
631
+
632
+ def flagEncodeCoord(flag, mask, coord, coordBytes):
633
+ byteCount = _flagSignBytes[flag & mask]
634
+ if byteCount == 1:
635
+ coordBytes.append(coord)
636
+ elif byteCount == -1:
637
+ coordBytes.append(-coord)
638
+ elif byteCount == 2:
639
+ coordBytes.extend(struct.pack(">h", coord))
640
+
641
+
642
+ def flagEncodeCoords(flag, x, y, xBytes, yBytes):
643
+ flagEncodeCoord(flag, flagXsame | flagXShort, x, xBytes)
644
+ flagEncodeCoord(flag, flagYsame | flagYShort, y, yBytes)
645
+
646
+
647
+ ARG_1_AND_2_ARE_WORDS = 0x0001 # if set args are words otherwise they are bytes
648
+ ARGS_ARE_XY_VALUES = 0x0002 # if set args are xy values, otherwise they are points
649
+ ROUND_XY_TO_GRID = 0x0004 # for the xy values if above is true
650
+ WE_HAVE_A_SCALE = 0x0008 # Sx = Sy, otherwise scale == 1.0
651
+ NON_OVERLAPPING = 0x0010 # set to same value for all components (obsolete!)
652
+ MORE_COMPONENTS = 0x0020 # indicates at least one more glyph after this one
653
+ WE_HAVE_AN_X_AND_Y_SCALE = 0x0040 # Sx, Sy
654
+ WE_HAVE_A_TWO_BY_TWO = 0x0080 # t00, t01, t10, t11
655
+ WE_HAVE_INSTRUCTIONS = 0x0100 # instructions follow
656
+ USE_MY_METRICS = 0x0200 # apply these metrics to parent glyph
657
+ OVERLAP_COMPOUND = 0x0400 # used by Apple in GX fonts
658
+ SCALED_COMPONENT_OFFSET = 0x0800 # composite designed to have the component offset scaled (designed for Apple)
659
+ UNSCALED_COMPONENT_OFFSET = 0x1000 # composite designed not to have the component offset scaled (designed for MS)
660
+
661
+
662
+ CompositeMaxpValues = namedtuple(
663
+ "CompositeMaxpValues", ["nPoints", "nContours", "maxComponentDepth"]
664
+ )
665
+
666
+
667
+ class Glyph(object):
668
+ """This class represents an individual TrueType glyph.
669
+
670
+ TrueType glyph objects come in two flavours: simple and composite. Simple
671
+ glyph objects contain contours, represented via the ``.coordinates``,
672
+ ``.flags``, ``.numberOfContours``, and ``.endPtsOfContours`` attributes;
673
+ composite glyphs contain components, available through the ``.components``
674
+ attributes.
675
+
676
+ Because the ``.coordinates`` attribute (and other simple glyph attributes mentioned
677
+ above) is only set on simple glyphs and the ``.components`` attribute is only
678
+ set on composite glyphs, it is necessary to use the :py:meth:`isComposite`
679
+ method to test whether a glyph is simple or composite before attempting to
680
+ access its data.
681
+
682
+ For a composite glyph, the components can also be accessed via array-like access::
683
+
684
+ >> assert(font["glyf"]["Aacute"].isComposite())
685
+ >> font["glyf"]["Aacute"][0]
686
+ <fontTools.ttLib.tables._g_l_y_f.GlyphComponent at 0x1027b2ee0>
687
+
688
+ """
689
+
690
+ def __init__(self, data=b""):
691
+ if not data:
692
+ # empty char
693
+ self.numberOfContours = 0
694
+ return
695
+ self.data = data
696
+
697
+ def compact(self, glyfTable, recalcBBoxes=True):
698
+ data = self.compile(glyfTable, recalcBBoxes)
699
+ self.__dict__.clear()
700
+ self.data = data
701
+
702
+ def expand(self, glyfTable):
703
+ if not hasattr(self, "data"):
704
+ # already unpacked
705
+ return
706
+ if not self.data:
707
+ # empty char
708
+ del self.data
709
+ self.numberOfContours = 0
710
+ return
711
+ dummy, data = sstruct.unpack2(glyphHeaderFormat, self.data, self)
712
+ del self.data
713
+ # Some fonts (eg. Neirizi.ttf) have a 0 for numberOfContours in
714
+ # some glyphs; decompileCoordinates assumes that there's at least
715
+ # one, so short-circuit here.
716
+ if self.numberOfContours == 0:
717
+ return
718
+ if self.isComposite():
719
+ self.decompileComponents(data, glyfTable)
720
+ else:
721
+ self.decompileCoordinates(data)
722
+
723
+ def compile(
724
+ self, glyfTable, recalcBBoxes=True, *, boundsDone=None, optimizeSize=True
725
+ ):
726
+ if hasattr(self, "data"):
727
+ if recalcBBoxes:
728
+ # must unpack glyph in order to recalculate bounding box
729
+ self.expand(glyfTable)
730
+ else:
731
+ return self.data
732
+ if self.numberOfContours == 0:
733
+ return b""
734
+
735
+ if recalcBBoxes:
736
+ self.recalcBounds(glyfTable, boundsDone=boundsDone)
737
+
738
+ data = sstruct.pack(glyphHeaderFormat, self)
739
+ if self.isComposite():
740
+ data = data + self.compileComponents(glyfTable)
741
+ else:
742
+ data = data + self.compileCoordinates(optimizeSize=optimizeSize)
743
+ return data
744
+
745
+ def toXML(self, writer, ttFont):
746
+ if self.isComposite():
747
+ for compo in self.components:
748
+ compo.toXML(writer, ttFont)
749
+ haveInstructions = hasattr(self, "program")
750
+ else:
751
+ last = 0
752
+ for i in range(self.numberOfContours):
753
+ writer.begintag("contour")
754
+ writer.newline()
755
+ for j in range(last, self.endPtsOfContours[i] + 1):
756
+ attrs = [
757
+ ("x", self.coordinates[j][0]),
758
+ ("y", self.coordinates[j][1]),
759
+ ("on", self.flags[j] & flagOnCurve),
760
+ ]
761
+ if self.flags[j] & flagOverlapSimple:
762
+ # Apple's rasterizer uses flagOverlapSimple in the first contour/first pt to flag glyphs that contain overlapping contours
763
+ attrs.append(("overlap", 1))
764
+ if self.flags[j] & flagCubic:
765
+ attrs.append(("cubic", 1))
766
+ writer.simpletag("pt", attrs)
767
+ writer.newline()
768
+ last = self.endPtsOfContours[i] + 1
769
+ writer.endtag("contour")
770
+ writer.newline()
771
+ haveInstructions = self.numberOfContours > 0
772
+ if haveInstructions:
773
+ if self.program:
774
+ writer.begintag("instructions")
775
+ writer.newline()
776
+ self.program.toXML(writer, ttFont)
777
+ writer.endtag("instructions")
778
+ else:
779
+ writer.simpletag("instructions")
780
+ writer.newline()
781
+
782
+ def fromXML(self, name, attrs, content, ttFont):
783
+ if name == "contour":
784
+ if self.numberOfContours < 0:
785
+ raise ttLib.TTLibError("can't mix composites and contours in glyph")
786
+ self.numberOfContours = self.numberOfContours + 1
787
+ coordinates = GlyphCoordinates()
788
+ flags = bytearray()
789
+ for element in content:
790
+ if not isinstance(element, tuple):
791
+ continue
792
+ name, attrs, content = element
793
+ if name != "pt":
794
+ continue # ignore anything but "pt"
795
+ coordinates.append((safeEval(attrs["x"]), safeEval(attrs["y"])))
796
+ flag = bool(safeEval(attrs["on"]))
797
+ if "overlap" in attrs and bool(safeEval(attrs["overlap"])):
798
+ flag |= flagOverlapSimple
799
+ if "cubic" in attrs and bool(safeEval(attrs["cubic"])):
800
+ flag |= flagCubic
801
+ flags.append(flag)
802
+ if not hasattr(self, "coordinates"):
803
+ self.coordinates = coordinates
804
+ self.flags = flags
805
+ self.endPtsOfContours = [len(coordinates) - 1]
806
+ else:
807
+ self.coordinates.extend(coordinates)
808
+ self.flags.extend(flags)
809
+ self.endPtsOfContours.append(len(self.coordinates) - 1)
810
+ elif name == "component":
811
+ if self.numberOfContours > 0:
812
+ raise ttLib.TTLibError("can't mix composites and contours in glyph")
813
+ self.numberOfContours = -1
814
+ if not hasattr(self, "components"):
815
+ self.components = []
816
+ component = GlyphComponent()
817
+ self.components.append(component)
818
+ component.fromXML(name, attrs, content, ttFont)
819
+ elif name == "instructions":
820
+ self.program = ttProgram.Program()
821
+ for element in content:
822
+ if not isinstance(element, tuple):
823
+ continue
824
+ name, attrs, content = element
825
+ self.program.fromXML(name, attrs, content, ttFont)
826
+
827
+ def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1):
828
+ assert self.isComposite()
829
+ nContours = 0
830
+ nPoints = 0
831
+ initialMaxComponentDepth = maxComponentDepth
832
+ for compo in self.components:
833
+ baseGlyph = glyfTable[compo.glyphName]
834
+ if baseGlyph.numberOfContours == 0:
835
+ continue
836
+ elif baseGlyph.numberOfContours > 0:
837
+ nP, nC = baseGlyph.getMaxpValues()
838
+ else:
839
+ nP, nC, componentDepth = baseGlyph.getCompositeMaxpValues(
840
+ glyfTable, initialMaxComponentDepth + 1
841
+ )
842
+ maxComponentDepth = max(maxComponentDepth, componentDepth)
843
+ nPoints = nPoints + nP
844
+ nContours = nContours + nC
845
+ return CompositeMaxpValues(nPoints, nContours, maxComponentDepth)
846
+
847
+ def getMaxpValues(self):
848
+ assert self.numberOfContours > 0
849
+ return len(self.coordinates), len(self.endPtsOfContours)
850
+
851
+ def decompileComponents(self, data, glyfTable):
852
+ self.components = []
853
+ more = 1
854
+ haveInstructions = 0
855
+ while more:
856
+ component = GlyphComponent()
857
+ more, haveInstr, data = component.decompile(data, glyfTable)
858
+ haveInstructions = haveInstructions | haveInstr
859
+ self.components.append(component)
860
+ if haveInstructions:
861
+ (numInstructions,) = struct.unpack(">h", data[:2])
862
+ data = data[2:]
863
+ self.program = ttProgram.Program()
864
+ self.program.fromBytecode(data[:numInstructions])
865
+ data = data[numInstructions:]
866
+ if len(data) >= 4:
867
+ log.warning(
868
+ "too much glyph data at the end of composite glyph: %d excess bytes",
869
+ len(data),
870
+ )
871
+
872
+ def decompileCoordinates(self, data):
873
+ endPtsOfContours = array.array("H")
874
+ endPtsOfContours.frombytes(data[: 2 * self.numberOfContours])
875
+ if sys.byteorder != "big":
876
+ endPtsOfContours.byteswap()
877
+ self.endPtsOfContours = endPtsOfContours.tolist()
878
+
879
+ pos = 2 * self.numberOfContours
880
+ (instructionLength,) = struct.unpack(">h", data[pos : pos + 2])
881
+ self.program = ttProgram.Program()
882
+ self.program.fromBytecode(data[pos + 2 : pos + 2 + instructionLength])
883
+ pos += 2 + instructionLength
884
+ nCoordinates = self.endPtsOfContours[-1] + 1
885
+ flags, xCoordinates, yCoordinates = self.decompileCoordinatesRaw(
886
+ nCoordinates, data, pos
887
+ )
888
+
889
+ # fill in repetitions and apply signs
890
+ self.coordinates = coordinates = GlyphCoordinates.zeros(nCoordinates)
891
+ xIndex = 0
892
+ yIndex = 0
893
+ for i in range(nCoordinates):
894
+ flag = flags[i]
895
+ # x coordinate
896
+ if flag & flagXShort:
897
+ if flag & flagXsame:
898
+ x = xCoordinates[xIndex]
899
+ else:
900
+ x = -xCoordinates[xIndex]
901
+ xIndex = xIndex + 1
902
+ elif flag & flagXsame:
903
+ x = 0
904
+ else:
905
+ x = xCoordinates[xIndex]
906
+ xIndex = xIndex + 1
907
+ # y coordinate
908
+ if flag & flagYShort:
909
+ if flag & flagYsame:
910
+ y = yCoordinates[yIndex]
911
+ else:
912
+ y = -yCoordinates[yIndex]
913
+ yIndex = yIndex + 1
914
+ elif flag & flagYsame:
915
+ y = 0
916
+ else:
917
+ y = yCoordinates[yIndex]
918
+ yIndex = yIndex + 1
919
+ coordinates[i] = (x, y)
920
+ assert xIndex == len(xCoordinates)
921
+ assert yIndex == len(yCoordinates)
922
+ coordinates.relativeToAbsolute()
923
+ # discard all flags except "keepFlags"
924
+ for i in range(len(flags)):
925
+ flags[i] &= keepFlags
926
+ self.flags = flags
927
+
928
+ def decompileCoordinatesRaw(self, nCoordinates, data, pos=0):
929
+ # unpack flags and prepare unpacking of coordinates
930
+ flags = bytearray(nCoordinates)
931
+ # Warning: deep Python trickery going on. We use the struct module to unpack
932
+ # the coordinates. We build a format string based on the flags, so we can
933
+ # unpack the coordinates in one struct.unpack() call.
934
+ xFormat = ">" # big endian
935
+ yFormat = ">" # big endian
936
+ j = 0
937
+ while True:
938
+ flag = data[pos]
939
+ pos += 1
940
+ repeat = 1
941
+ if flag & flagRepeat:
942
+ repeat = data[pos] + 1
943
+ pos += 1
944
+ for k in range(repeat):
945
+ if flag & flagXShort:
946
+ xFormat = xFormat + "B"
947
+ elif not (flag & flagXsame):
948
+ xFormat = xFormat + "h"
949
+ if flag & flagYShort:
950
+ yFormat = yFormat + "B"
951
+ elif not (flag & flagYsame):
952
+ yFormat = yFormat + "h"
953
+ flags[j] = flag
954
+ j = j + 1
955
+ if j >= nCoordinates:
956
+ break
957
+ assert j == nCoordinates, "bad glyph flags"
958
+ # unpack raw coordinates, krrrrrr-tching!
959
+ xDataLen = struct.calcsize(xFormat)
960
+ yDataLen = struct.calcsize(yFormat)
961
+ if len(data) - pos - (xDataLen + yDataLen) >= 4:
962
+ log.warning(
963
+ "too much glyph data: %d excess bytes",
964
+ len(data) - pos - (xDataLen + yDataLen),
965
+ )
966
+ xCoordinates = struct.unpack(xFormat, data[pos : pos + xDataLen])
967
+ yCoordinates = struct.unpack(
968
+ yFormat, data[pos + xDataLen : pos + xDataLen + yDataLen]
969
+ )
970
+ return flags, xCoordinates, yCoordinates
971
+
972
+ def compileComponents(self, glyfTable):
973
+ data = b""
974
+ lastcomponent = len(self.components) - 1
975
+ more = 1
976
+ haveInstructions = 0
977
+ for i, compo in enumerate(self.components):
978
+ if i == lastcomponent:
979
+ haveInstructions = hasattr(self, "program")
980
+ more = 0
981
+ data = data + compo.compile(more, haveInstructions, glyfTable)
982
+ if haveInstructions:
983
+ instructions = self.program.getBytecode()
984
+ data = data + struct.pack(">h", len(instructions)) + instructions
985
+ return data
986
+
987
+ def compileCoordinates(self, *, optimizeSize=True):
988
+ assert len(self.coordinates) == len(self.flags)
989
+ data = []
990
+ endPtsOfContours = array.array("H", self.endPtsOfContours)
991
+ if sys.byteorder != "big":
992
+ endPtsOfContours.byteswap()
993
+ data.append(endPtsOfContours.tobytes())
994
+ instructions = self.program.getBytecode()
995
+ data.append(struct.pack(">h", len(instructions)))
996
+ data.append(instructions)
997
+
998
+ deltas = self.coordinates.copy()
999
+ deltas.toInt()
1000
+ deltas.absoluteToRelative()
1001
+
1002
+ if optimizeSize:
1003
+ # TODO(behdad): Add a configuration option for this?
1004
+ deltas = self.compileDeltasGreedy(self.flags, deltas)
1005
+ # deltas = self.compileDeltasOptimal(self.flags, deltas)
1006
+ else:
1007
+ deltas = self.compileDeltasForSpeed(self.flags, deltas)
1008
+
1009
+ data.extend(deltas)
1010
+ return b"".join(data)
1011
+
1012
+ def compileDeltasGreedy(self, flags, deltas):
1013
+ # Implements greedy algorithm for packing coordinate deltas:
1014
+ # uses shortest representation one coordinate at a time.
1015
+ compressedFlags = bytearray()
1016
+ compressedXs = bytearray()
1017
+ compressedYs = bytearray()
1018
+ lastflag = None
1019
+ repeat = 0
1020
+ for flag, (x, y) in zip(flags, deltas):
1021
+ # Oh, the horrors of TrueType
1022
+ # do x
1023
+ if x == 0:
1024
+ flag = flag | flagXsame
1025
+ elif -255 <= x <= 255:
1026
+ flag = flag | flagXShort
1027
+ if x > 0:
1028
+ flag = flag | flagXsame
1029
+ else:
1030
+ x = -x
1031
+ compressedXs.append(x)
1032
+ else:
1033
+ compressedXs.extend(struct.pack(">h", x))
1034
+ # do y
1035
+ if y == 0:
1036
+ flag = flag | flagYsame
1037
+ elif -255 <= y <= 255:
1038
+ flag = flag | flagYShort
1039
+ if y > 0:
1040
+ flag = flag | flagYsame
1041
+ else:
1042
+ y = -y
1043
+ compressedYs.append(y)
1044
+ else:
1045
+ compressedYs.extend(struct.pack(">h", y))
1046
+ # handle repeating flags
1047
+ if flag == lastflag and repeat != 255:
1048
+ repeat = repeat + 1
1049
+ if repeat == 1:
1050
+ compressedFlags.append(flag)
1051
+ else:
1052
+ compressedFlags[-2] = flag | flagRepeat
1053
+ compressedFlags[-1] = repeat
1054
+ else:
1055
+ repeat = 0
1056
+ compressedFlags.append(flag)
1057
+ lastflag = flag
1058
+ return (compressedFlags, compressedXs, compressedYs)
1059
+
1060
+ def compileDeltasOptimal(self, flags, deltas):
1061
+ # Implements optimal, dynaic-programming, algorithm for packing coordinate
1062
+ # deltas. The savings are negligible :(.
1063
+ candidates = []
1064
+ bestTuple = None
1065
+ bestCost = 0
1066
+ repeat = 0
1067
+ for flag, (x, y) in zip(flags, deltas):
1068
+ # Oh, the horrors of TrueType
1069
+ flag, coordBytes = flagBest(x, y, flag)
1070
+ bestCost += 1 + coordBytes
1071
+ newCandidates = [
1072
+ (bestCost, bestTuple, flag, coordBytes),
1073
+ (bestCost + 1, bestTuple, (flag | flagRepeat), coordBytes),
1074
+ ]
1075
+ for lastCost, lastTuple, lastFlag, coordBytes in candidates:
1076
+ if (
1077
+ lastCost + coordBytes <= bestCost + 1
1078
+ and (lastFlag & flagRepeat)
1079
+ and (lastFlag < 0xFF00)
1080
+ and flagSupports(lastFlag, flag)
1081
+ ):
1082
+ if (lastFlag & 0xFF) == (
1083
+ flag | flagRepeat
1084
+ ) and lastCost == bestCost + 1:
1085
+ continue
1086
+ newCandidates.append(
1087
+ (lastCost + coordBytes, lastTuple, lastFlag + 256, coordBytes)
1088
+ )
1089
+ candidates = newCandidates
1090
+ bestTuple = min(candidates, key=lambda t: t[0])
1091
+ bestCost = bestTuple[0]
1092
+
1093
+ flags = []
1094
+ while bestTuple:
1095
+ cost, bestTuple, flag, coordBytes = bestTuple
1096
+ flags.append(flag)
1097
+ flags.reverse()
1098
+
1099
+ compressedFlags = bytearray()
1100
+ compressedXs = bytearray()
1101
+ compressedYs = bytearray()
1102
+ coords = iter(deltas)
1103
+ ff = []
1104
+ for flag in flags:
1105
+ repeatCount, flag = flag >> 8, flag & 0xFF
1106
+ compressedFlags.append(flag)
1107
+ if flag & flagRepeat:
1108
+ assert repeatCount > 0
1109
+ compressedFlags.append(repeatCount)
1110
+ else:
1111
+ assert repeatCount == 0
1112
+ for i in range(1 + repeatCount):
1113
+ x, y = next(coords)
1114
+ flagEncodeCoords(flag, x, y, compressedXs, compressedYs)
1115
+ ff.append(flag)
1116
+ try:
1117
+ next(coords)
1118
+ raise Exception("internal error")
1119
+ except StopIteration:
1120
+ pass
1121
+
1122
+ return (compressedFlags, compressedXs, compressedYs)
1123
+
1124
+ def compileDeltasForSpeed(self, flags, deltas):
1125
+ # uses widest representation needed, for all deltas.
1126
+ compressedFlags = bytearray()
1127
+ compressedXs = bytearray()
1128
+ compressedYs = bytearray()
1129
+
1130
+ # Compute the necessary width for each axis
1131
+ xs = [d[0] for d in deltas]
1132
+ ys = [d[1] for d in deltas]
1133
+ minX, minY, maxX, maxY = min(xs), min(ys), max(xs), max(ys)
1134
+ xZero = minX == 0 and maxX == 0
1135
+ yZero = minY == 0 and maxY == 0
1136
+ xShort = -255 <= minX <= maxX <= 255
1137
+ yShort = -255 <= minY <= maxY <= 255
1138
+
1139
+ lastflag = None
1140
+ repeat = 0
1141
+ for flag, (x, y) in zip(flags, deltas):
1142
+ # Oh, the horrors of TrueType
1143
+ # do x
1144
+ if xZero:
1145
+ flag = flag | flagXsame
1146
+ elif xShort:
1147
+ flag = flag | flagXShort
1148
+ if x > 0:
1149
+ flag = flag | flagXsame
1150
+ else:
1151
+ x = -x
1152
+ compressedXs.append(x)
1153
+ else:
1154
+ compressedXs.extend(struct.pack(">h", x))
1155
+ # do y
1156
+ if yZero:
1157
+ flag = flag | flagYsame
1158
+ elif yShort:
1159
+ flag = flag | flagYShort
1160
+ if y > 0:
1161
+ flag = flag | flagYsame
1162
+ else:
1163
+ y = -y
1164
+ compressedYs.append(y)
1165
+ else:
1166
+ compressedYs.extend(struct.pack(">h", y))
1167
+ # handle repeating flags
1168
+ if flag == lastflag and repeat != 255:
1169
+ repeat = repeat + 1
1170
+ if repeat == 1:
1171
+ compressedFlags.append(flag)
1172
+ else:
1173
+ compressedFlags[-2] = flag | flagRepeat
1174
+ compressedFlags[-1] = repeat
1175
+ else:
1176
+ repeat = 0
1177
+ compressedFlags.append(flag)
1178
+ lastflag = flag
1179
+ return (compressedFlags, compressedXs, compressedYs)
1180
+
1181
+ def recalcBounds(self, glyfTable, *, boundsDone=None):
1182
+ """Recalculates the bounds of the glyph.
1183
+
1184
+ Each glyph object stores its bounding box in the
1185
+ ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be
1186
+ recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds
1187
+ must be provided to resolve component bounds.
1188
+ """
1189
+ if self.isComposite() and self.tryRecalcBoundsComposite(
1190
+ glyfTable, boundsDone=boundsDone
1191
+ ):
1192
+ return
1193
+ try:
1194
+ coords, endPts, flags = self.getCoordinates(glyfTable, round=otRound)
1195
+ self.xMin, self.yMin, self.xMax, self.yMax = coords.calcIntBounds()
1196
+ except NotImplementedError:
1197
+ pass
1198
+
1199
+ def tryRecalcBoundsComposite(self, glyfTable, *, boundsDone=None):
1200
+ """Try recalculating the bounds of a composite glyph that has
1201
+ certain constrained properties. Namely, none of the components
1202
+ have a transform other than an integer translate, and none
1203
+ uses the anchor points.
1204
+
1205
+ Each glyph object stores its bounding box in the
1206
+ ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be
1207
+ recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds
1208
+ must be provided to resolve component bounds.
1209
+
1210
+ Return True if bounds were calculated, False otherwise.
1211
+ """
1212
+ for compo in self.components:
1213
+ if not compo._hasOnlyIntegerTranslate():
1214
+ return False
1215
+
1216
+ # All components are untransformed and have an integer x/y translate
1217
+ bounds = None
1218
+ for compo in self.components:
1219
+ glyphName = compo.glyphName
1220
+ g = glyfTable[glyphName]
1221
+
1222
+ if boundsDone is None or glyphName not in boundsDone:
1223
+ g.recalcBounds(glyfTable, boundsDone=boundsDone)
1224
+ if boundsDone is not None:
1225
+ boundsDone.add(glyphName)
1226
+ # empty components shouldn't update the bounds of the parent glyph
1227
+ if g.yMin == g.yMax and g.xMin == g.xMax:
1228
+ continue
1229
+
1230
+ x, y = compo.x, compo.y
1231
+ bounds = updateBounds(bounds, (g.xMin + x, g.yMin + y))
1232
+ bounds = updateBounds(bounds, (g.xMax + x, g.yMax + y))
1233
+
1234
+ if bounds is None:
1235
+ bounds = (0, 0, 0, 0)
1236
+ self.xMin, self.yMin, self.xMax, self.yMax = bounds
1237
+ return True
1238
+
1239
+ def isComposite(self):
1240
+ """Test whether a glyph has components"""
1241
+ if hasattr(self, "data"):
1242
+ return struct.unpack(">h", self.data[:2])[0] == -1 if self.data else False
1243
+ else:
1244
+ return self.numberOfContours == -1
1245
+
1246
+ def getCoordinates(self, glyfTable, *, round=noRound):
1247
+ """Return the coordinates, end points and flags
1248
+
1249
+ This method returns three values: A :py:class:`GlyphCoordinates` object,
1250
+ a list of the indexes of the final points of each contour (allowing you
1251
+ to split up the coordinates list into contours) and a list of flags.
1252
+
1253
+ On simple glyphs, this method returns information from the glyph's own
1254
+ contours; on composite glyphs, it "flattens" all components recursively
1255
+ to return a list of coordinates representing all the components involved
1256
+ in the glyph.
1257
+
1258
+ To interpret the flags for each point, see the "Simple Glyph Flags"
1259
+ section of the `glyf table specification <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf#simple-glyph-description>`.
1260
+ """
1261
+
1262
+ if self.numberOfContours > 0:
1263
+ return self.coordinates, self.endPtsOfContours, self.flags
1264
+ elif self.isComposite():
1265
+ # it's a composite
1266
+ allCoords = GlyphCoordinates()
1267
+ allFlags = bytearray()
1268
+ allEndPts = []
1269
+ for compo in self.components:
1270
+ g = glyfTable[compo.glyphName]
1271
+ try:
1272
+ coordinates, endPts, flags = g.getCoordinates(
1273
+ glyfTable, round=round
1274
+ )
1275
+ except RecursionError:
1276
+ raise ttLib.TTLibError(
1277
+ "glyph '%s' contains a recursive component reference"
1278
+ % compo.glyphName
1279
+ )
1280
+ coordinates = GlyphCoordinates(coordinates)
1281
+ # if asked to round e.g. while computing bboxes, it's important we
1282
+ # do it immediately before a component transform is applied to a
1283
+ # simple glyph's coordinates in case these might still contain floats;
1284
+ # however, if the referenced component glyph is another composite, we
1285
+ # must not round here but only at the end, after all the nested
1286
+ # transforms have been applied, or else rounding errors will compound.
1287
+ if round is not noRound and g.numberOfContours > 0:
1288
+ coordinates.toInt(round=round)
1289
+ if hasattr(compo, "firstPt"):
1290
+ # component uses two reference points: we apply the transform _before_
1291
+ # computing the offset between the points
1292
+ if hasattr(compo, "transform"):
1293
+ coordinates.transform(compo.transform)
1294
+ x1, y1 = allCoords[compo.firstPt]
1295
+ x2, y2 = coordinates[compo.secondPt]
1296
+ move = x1 - x2, y1 - y2
1297
+ coordinates.translate(move)
1298
+ else:
1299
+ # component uses XY offsets
1300
+ move = compo.x, compo.y
1301
+ if not hasattr(compo, "transform"):
1302
+ coordinates.translate(move)
1303
+ else:
1304
+ apple_way = compo.flags & SCALED_COMPONENT_OFFSET
1305
+ ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET
1306
+ assert not (apple_way and ms_way)
1307
+ if not (apple_way or ms_way):
1308
+ scale_component_offset = (
1309
+ SCALE_COMPONENT_OFFSET_DEFAULT # see top of this file
1310
+ )
1311
+ else:
1312
+ scale_component_offset = apple_way
1313
+ if scale_component_offset:
1314
+ # the Apple way: first move, then scale (ie. scale the component offset)
1315
+ coordinates.translate(move)
1316
+ coordinates.transform(compo.transform)
1317
+ else:
1318
+ # the MS way: first scale, then move
1319
+ coordinates.transform(compo.transform)
1320
+ coordinates.translate(move)
1321
+ offset = len(allCoords)
1322
+ allEndPts.extend(e + offset for e in endPts)
1323
+ allCoords.extend(coordinates)
1324
+ allFlags.extend(flags)
1325
+ return allCoords, allEndPts, allFlags
1326
+ else:
1327
+ return GlyphCoordinates(), [], bytearray()
1328
+
1329
+ def getComponentNames(self, glyfTable):
1330
+ """Returns a list of names of component glyphs used in this glyph
1331
+
1332
+ This method can be used on simple glyphs (in which case it returns an
1333
+ empty list) or composite glyphs.
1334
+ """
1335
+ if not hasattr(self, "data"):
1336
+ if self.isComposite():
1337
+ return [c.glyphName for c in self.components]
1338
+ else:
1339
+ return []
1340
+
1341
+ # Extract components without expanding glyph
1342
+
1343
+ if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0:
1344
+ return [] # Not composite
1345
+
1346
+ data = self.data
1347
+ i = 10
1348
+ components = []
1349
+ more = 1
1350
+ while more:
1351
+ flags, glyphID = struct.unpack(">HH", data[i : i + 4])
1352
+ i += 4
1353
+ flags = int(flags)
1354
+ components.append(glyfTable.getGlyphName(int(glyphID)))
1355
+
1356
+ if flags & ARG_1_AND_2_ARE_WORDS:
1357
+ i += 4
1358
+ else:
1359
+ i += 2
1360
+ if flags & WE_HAVE_A_SCALE:
1361
+ i += 2
1362
+ elif flags & WE_HAVE_AN_X_AND_Y_SCALE:
1363
+ i += 4
1364
+ elif flags & WE_HAVE_A_TWO_BY_TWO:
1365
+ i += 8
1366
+ more = flags & MORE_COMPONENTS
1367
+
1368
+ return components
1369
+
1370
+ def trim(self, remove_hinting=False):
1371
+ """Remove padding and, if requested, hinting, from a glyph.
1372
+ This works on both expanded and compacted glyphs, without
1373
+ expanding it."""
1374
+ if not hasattr(self, "data"):
1375
+ if remove_hinting:
1376
+ if self.isComposite():
1377
+ if hasattr(self, "program"):
1378
+ del self.program
1379
+ else:
1380
+ self.program = ttProgram.Program()
1381
+ self.program.fromBytecode([])
1382
+ # No padding to trim.
1383
+ return
1384
+ if not self.data:
1385
+ return
1386
+ numContours = struct.unpack(">h", self.data[:2])[0]
1387
+ data = bytearray(self.data)
1388
+ i = 10
1389
+ if numContours >= 0:
1390
+ i += 2 * numContours # endPtsOfContours
1391
+ nCoordinates = ((data[i - 2] << 8) | data[i - 1]) + 1
1392
+ instructionLen = (data[i] << 8) | data[i + 1]
1393
+ if remove_hinting:
1394
+ # Zero instruction length
1395
+ data[i] = data[i + 1] = 0
1396
+ i += 2
1397
+ if instructionLen:
1398
+ # Splice it out
1399
+ data = data[:i] + data[i + instructionLen :]
1400
+ instructionLen = 0
1401
+ else:
1402
+ i += 2 + instructionLen
1403
+
1404
+ coordBytes = 0
1405
+ j = 0
1406
+ while True:
1407
+ flag = data[i]
1408
+ i = i + 1
1409
+ repeat = 1
1410
+ if flag & flagRepeat:
1411
+ repeat = data[i] + 1
1412
+ i = i + 1
1413
+ xBytes = yBytes = 0
1414
+ if flag & flagXShort:
1415
+ xBytes = 1
1416
+ elif not (flag & flagXsame):
1417
+ xBytes = 2
1418
+ if flag & flagYShort:
1419
+ yBytes = 1
1420
+ elif not (flag & flagYsame):
1421
+ yBytes = 2
1422
+ coordBytes += (xBytes + yBytes) * repeat
1423
+ j += repeat
1424
+ if j >= nCoordinates:
1425
+ break
1426
+ assert j == nCoordinates, "bad glyph flags"
1427
+ i += coordBytes
1428
+ # Remove padding
1429
+ data = data[:i]
1430
+ elif self.isComposite():
1431
+ more = 1
1432
+ we_have_instructions = False
1433
+ while more:
1434
+ flags = (data[i] << 8) | data[i + 1]
1435
+ if remove_hinting:
1436
+ flags &= ~WE_HAVE_INSTRUCTIONS
1437
+ if flags & WE_HAVE_INSTRUCTIONS:
1438
+ we_have_instructions = True
1439
+ data[i + 0] = flags >> 8
1440
+ data[i + 1] = flags & 0xFF
1441
+ i += 4
1442
+ flags = int(flags)
1443
+
1444
+ if flags & ARG_1_AND_2_ARE_WORDS:
1445
+ i += 4
1446
+ else:
1447
+ i += 2
1448
+ if flags & WE_HAVE_A_SCALE:
1449
+ i += 2
1450
+ elif flags & WE_HAVE_AN_X_AND_Y_SCALE:
1451
+ i += 4
1452
+ elif flags & WE_HAVE_A_TWO_BY_TWO:
1453
+ i += 8
1454
+ more = flags & MORE_COMPONENTS
1455
+ if we_have_instructions:
1456
+ instructionLen = (data[i] << 8) | data[i + 1]
1457
+ i += 2 + instructionLen
1458
+ # Remove padding
1459
+ data = data[:i]
1460
+
1461
+ self.data = data
1462
+
1463
+ def removeHinting(self):
1464
+ """Removes TrueType hinting instructions from the glyph."""
1465
+ self.trim(remove_hinting=True)
1466
+
1467
+ def draw(self, pen, glyfTable, offset=0):
1468
+ """Draws the glyph using the supplied pen object.
1469
+
1470
+ Arguments:
1471
+ pen: An object conforming to the pen protocol.
1472
+ glyfTable: A :py:class:`table__g_l_y_f` object, to resolve components.
1473
+ offset (int): A horizontal offset. If provided, all coordinates are
1474
+ translated by this offset.
1475
+ """
1476
+
1477
+ if self.isComposite():
1478
+ for component in self.components:
1479
+ glyphName, transform = component.getComponentInfo()
1480
+ pen.addComponent(glyphName, transform)
1481
+ return
1482
+
1483
+ self.expand(glyfTable)
1484
+ coordinates, endPts, flags = self.getCoordinates(glyfTable)
1485
+ if offset:
1486
+ coordinates = coordinates.copy()
1487
+ coordinates.translate((offset, 0))
1488
+ start = 0
1489
+ maybeInt = lambda v: int(v) if v == int(v) else v
1490
+ for end in endPts:
1491
+ end = end + 1
1492
+ contour = coordinates[start:end]
1493
+ cFlags = [flagOnCurve & f for f in flags[start:end]]
1494
+ cuFlags = [flagCubic & f for f in flags[start:end]]
1495
+ start = end
1496
+ if 1 not in cFlags:
1497
+ assert all(cuFlags) or not any(cuFlags)
1498
+ cubic = all(cuFlags)
1499
+ if cubic:
1500
+ count = len(contour)
1501
+ assert count % 2 == 0, "Odd number of cubic off-curves undefined"
1502
+ l = contour[-1]
1503
+ f = contour[0]
1504
+ p0 = (maybeInt((l[0] + f[0]) * 0.5), maybeInt((l[1] + f[1]) * 0.5))
1505
+ pen.moveTo(p0)
1506
+ for i in range(0, count, 2):
1507
+ p1 = contour[i]
1508
+ p2 = contour[i + 1]
1509
+ p4 = contour[i + 2 if i + 2 < count else 0]
1510
+ p3 = (
1511
+ maybeInt((p2[0] + p4[0]) * 0.5),
1512
+ maybeInt((p2[1] + p4[1]) * 0.5),
1513
+ )
1514
+ pen.curveTo(p1, p2, p3)
1515
+ else:
1516
+ # There is not a single on-curve point on the curve,
1517
+ # use pen.qCurveTo's special case by specifying None
1518
+ # as the on-curve point.
1519
+ contour.append(None)
1520
+ pen.qCurveTo(*contour)
1521
+ else:
1522
+ # Shuffle the points so that the contour is guaranteed
1523
+ # to *end* in an on-curve point, which we'll use for
1524
+ # the moveTo.
1525
+ firstOnCurve = cFlags.index(1) + 1
1526
+ contour = contour[firstOnCurve:] + contour[:firstOnCurve]
1527
+ cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve]
1528
+ cuFlags = cuFlags[firstOnCurve:] + cuFlags[:firstOnCurve]
1529
+ pen.moveTo(contour[-1])
1530
+ while contour:
1531
+ nextOnCurve = cFlags.index(1) + 1
1532
+ if nextOnCurve == 1:
1533
+ # Skip a final lineTo(), as it is implied by
1534
+ # pen.closePath()
1535
+ if len(contour) > 1:
1536
+ pen.lineTo(contour[0])
1537
+ else:
1538
+ cubicFlags = [f for f in cuFlags[: nextOnCurve - 1]]
1539
+ assert all(cubicFlags) or not any(cubicFlags)
1540
+ cubic = any(cubicFlags)
1541
+ if cubic:
1542
+ assert all(
1543
+ cubicFlags
1544
+ ), "Mixed cubic and quadratic segment undefined"
1545
+
1546
+ count = nextOnCurve
1547
+ assert (
1548
+ count >= 3
1549
+ ), "At least two cubic off-curve points required"
1550
+ assert (
1551
+ count - 1
1552
+ ) % 2 == 0, "Odd number of cubic off-curves undefined"
1553
+ for i in range(0, count - 3, 2):
1554
+ p1 = contour[i]
1555
+ p2 = contour[i + 1]
1556
+ p4 = contour[i + 2]
1557
+ p3 = (
1558
+ maybeInt((p2[0] + p4[0]) * 0.5),
1559
+ maybeInt((p2[1] + p4[1]) * 0.5),
1560
+ )
1561
+ lastOnCurve = p3
1562
+ pen.curveTo(p1, p2, p3)
1563
+ pen.curveTo(*contour[count - 3 : count])
1564
+ else:
1565
+ pen.qCurveTo(*contour[:nextOnCurve])
1566
+ contour = contour[nextOnCurve:]
1567
+ cFlags = cFlags[nextOnCurve:]
1568
+ cuFlags = cuFlags[nextOnCurve:]
1569
+ pen.closePath()
1570
+
1571
+ def drawPoints(self, pen, glyfTable, offset=0):
1572
+ """Draw the glyph using the supplied pointPen. As opposed to Glyph.draw(),
1573
+ this will not change the point indices.
1574
+ """
1575
+
1576
+ if self.isComposite():
1577
+ for component in self.components:
1578
+ glyphName, transform = component.getComponentInfo()
1579
+ pen.addComponent(glyphName, transform)
1580
+ return
1581
+
1582
+ coordinates, endPts, flags = self.getCoordinates(glyfTable)
1583
+ if offset:
1584
+ coordinates = coordinates.copy()
1585
+ coordinates.translate((offset, 0))
1586
+ start = 0
1587
+ for end in endPts:
1588
+ end = end + 1
1589
+ contour = coordinates[start:end]
1590
+ cFlags = flags[start:end]
1591
+ start = end
1592
+ pen.beginPath()
1593
+ # Start with the appropriate segment type based on the final segment
1594
+
1595
+ if cFlags[-1] & flagOnCurve:
1596
+ segmentType = "line"
1597
+ elif cFlags[-1] & flagCubic:
1598
+ segmentType = "curve"
1599
+ else:
1600
+ segmentType = "qcurve"
1601
+ for i, pt in enumerate(contour):
1602
+ if cFlags[i] & flagOnCurve:
1603
+ pen.addPoint(pt, segmentType=segmentType)
1604
+ segmentType = "line"
1605
+ else:
1606
+ pen.addPoint(pt)
1607
+ segmentType = "curve" if cFlags[i] & flagCubic else "qcurve"
1608
+ pen.endPath()
1609
+
1610
+ def __eq__(self, other):
1611
+ if type(self) != type(other):
1612
+ return NotImplemented
1613
+ return self.__dict__ == other.__dict__
1614
+
1615
+ def __ne__(self, other):
1616
+ result = self.__eq__(other)
1617
+ return result if result is NotImplemented else not result
1618
+
1619
+
1620
+ # Vector.__round__ uses the built-in (Banker's) `round` but we want
1621
+ # to use otRound below
1622
+ _roundv = partial(Vector.__round__, round=otRound)
1623
+
1624
+
1625
+ def _is_mid_point(p0: tuple, p1: tuple, p2: tuple) -> bool:
1626
+ # True if p1 is in the middle of p0 and p2, either before or after rounding
1627
+ p0 = Vector(p0)
1628
+ p1 = Vector(p1)
1629
+ p2 = Vector(p2)
1630
+ return ((p0 + p2) * 0.5).isclose(p1) or _roundv(p0) + _roundv(p2) == _roundv(p1) * 2
1631
+
1632
+
1633
+ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]:
1634
+ """Drop impliable on-curve points from the (simple) glyph or glyphs.
1635
+
1636
+ In TrueType glyf outlines, on-curve points can be implied when they are located at
1637
+ the midpoint of the line connecting two consecutive off-curve points.
1638
+
1639
+ If more than one glyphs are passed, these are assumed to be interpolatable masters
1640
+ of the same glyph impliable, and thus only the on-curve points that are impliable
1641
+ for all of them will actually be implied.
1642
+ Composite glyphs or empty glyphs are skipped, only simple glyphs with 1 or more
1643
+ contours are considered.
1644
+ The input glyph(s) is/are modified in-place.
1645
+
1646
+ Args:
1647
+ interpolatable_glyphs: The glyph or glyphs to modify in-place.
1648
+
1649
+ Returns:
1650
+ The set of point indices that were dropped if any.
1651
+
1652
+ Raises:
1653
+ ValueError if simple glyphs are not in fact interpolatable because they have
1654
+ different point flags or number of contours.
1655
+
1656
+ Reference:
1657
+ https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html
1658
+ """
1659
+ staticAttributes = SimpleNamespace(
1660
+ numberOfContours=None, flags=None, endPtsOfContours=None
1661
+ )
1662
+ drop = None
1663
+ simple_glyphs = []
1664
+ for i, glyph in enumerate(interpolatable_glyphs):
1665
+ if glyph.numberOfContours < 1:
1666
+ # ignore composite or empty glyphs
1667
+ continue
1668
+
1669
+ for attr in staticAttributes.__dict__:
1670
+ expected = getattr(staticAttributes, attr)
1671
+ found = getattr(glyph, attr)
1672
+ if expected is None:
1673
+ setattr(staticAttributes, attr, found)
1674
+ elif expected != found:
1675
+ raise ValueError(
1676
+ f"Incompatible {attr} for glyph at master index {i}: "
1677
+ f"expected {expected}, found {found}"
1678
+ )
1679
+
1680
+ may_drop = set()
1681
+ start = 0
1682
+ coords = glyph.coordinates
1683
+ flags = staticAttributes.flags
1684
+ endPtsOfContours = staticAttributes.endPtsOfContours
1685
+ for last in endPtsOfContours:
1686
+ for i in range(start, last + 1):
1687
+ if not (flags[i] & flagOnCurve):
1688
+ continue
1689
+ prv = i - 1 if i > start else last
1690
+ nxt = i + 1 if i < last else start
1691
+ if (flags[prv] & flagOnCurve) or flags[prv] != flags[nxt]:
1692
+ continue
1693
+ # we may drop the ith on-curve if halfway between previous/next off-curves
1694
+ if not _is_mid_point(coords[prv], coords[i], coords[nxt]):
1695
+ continue
1696
+
1697
+ may_drop.add(i)
1698
+ start = last + 1
1699
+ # we only want to drop if ALL interpolatable glyphs have the same implied oncurves
1700
+ if drop is None:
1701
+ drop = may_drop
1702
+ else:
1703
+ drop.intersection_update(may_drop)
1704
+
1705
+ simple_glyphs.append(glyph)
1706
+
1707
+ if drop:
1708
+ # Do the actual dropping
1709
+ flags = staticAttributes.flags
1710
+ assert flags is not None
1711
+ newFlags = array.array(
1712
+ "B", (flags[i] for i in range(len(flags)) if i not in drop)
1713
+ )
1714
+
1715
+ endPts = staticAttributes.endPtsOfContours
1716
+ assert endPts is not None
1717
+ newEndPts = []
1718
+ i = 0
1719
+ delta = 0
1720
+ for d in sorted(drop):
1721
+ while d > endPts[i]:
1722
+ newEndPts.append(endPts[i] - delta)
1723
+ i += 1
1724
+ delta += 1
1725
+ while i < len(endPts):
1726
+ newEndPts.append(endPts[i] - delta)
1727
+ i += 1
1728
+
1729
+ for glyph in simple_glyphs:
1730
+ coords = glyph.coordinates
1731
+ glyph.coordinates = GlyphCoordinates(
1732
+ coords[i] for i in range(len(coords)) if i not in drop
1733
+ )
1734
+ glyph.flags = newFlags
1735
+ glyph.endPtsOfContours = newEndPts
1736
+
1737
+ return drop if drop is not None else set()
1738
+
1739
+
1740
+ class GlyphComponent(object):
1741
+ """Represents a component within a composite glyph.
1742
+
1743
+ The component is represented internally with four attributes: ``glyphName``,
1744
+ ``x``, ``y`` and ``transform``. If there is no "two-by-two" matrix (i.e
1745
+ no scaling, reflection, or rotation; only translation), the ``transform``
1746
+ attribute is not present.
1747
+ """
1748
+
1749
+ # The above documentation is not *completely* true, but is *true enough* because
1750
+ # the rare firstPt/lastPt attributes are not totally supported and nobody seems to
1751
+ # mind - see below.
1752
+
1753
+ def __init__(self):
1754
+ pass
1755
+
1756
+ def getComponentInfo(self):
1757
+ """Return information about the component
1758
+
1759
+ This method returns a tuple of two values: the glyph name of the component's
1760
+ base glyph, and a transformation matrix. As opposed to accessing the attributes
1761
+ directly, ``getComponentInfo`` always returns a six-element tuple of the
1762
+ component's transformation matrix, even when the two-by-two ``.transform``
1763
+ matrix is not present.
1764
+ """
1765
+ # XXX Ignoring self.firstPt & self.lastpt for now: I need to implement
1766
+ # something equivalent in fontTools.objects.glyph (I'd rather not
1767
+ # convert it to an absolute offset, since it is valuable information).
1768
+ # This method will now raise "AttributeError: x" on glyphs that use
1769
+ # this TT feature.
1770
+ if hasattr(self, "transform"):
1771
+ [[xx, xy], [yx, yy]] = self.transform
1772
+ trans = (xx, xy, yx, yy, self.x, self.y)
1773
+ else:
1774
+ trans = (1, 0, 0, 1, self.x, self.y)
1775
+ return self.glyphName, trans
1776
+
1777
+ def decompile(self, data, glyfTable):
1778
+ flags, glyphID = struct.unpack(">HH", data[:4])
1779
+ self.flags = int(flags)
1780
+ glyphID = int(glyphID)
1781
+ self.glyphName = glyfTable.getGlyphName(int(glyphID))
1782
+ data = data[4:]
1783
+
1784
+ if self.flags & ARG_1_AND_2_ARE_WORDS:
1785
+ if self.flags & ARGS_ARE_XY_VALUES:
1786
+ self.x, self.y = struct.unpack(">hh", data[:4])
1787
+ else:
1788
+ x, y = struct.unpack(">HH", data[:4])
1789
+ self.firstPt, self.secondPt = int(x), int(y)
1790
+ data = data[4:]
1791
+ else:
1792
+ if self.flags & ARGS_ARE_XY_VALUES:
1793
+ self.x, self.y = struct.unpack(">bb", data[:2])
1794
+ else:
1795
+ x, y = struct.unpack(">BB", data[:2])
1796
+ self.firstPt, self.secondPt = int(x), int(y)
1797
+ data = data[2:]
1798
+
1799
+ if self.flags & WE_HAVE_A_SCALE:
1800
+ (scale,) = struct.unpack(">h", data[:2])
1801
+ self.transform = [
1802
+ [fi2fl(scale, 14), 0],
1803
+ [0, fi2fl(scale, 14)],
1804
+ ] # fixed 2.14
1805
+ data = data[2:]
1806
+ elif self.flags & WE_HAVE_AN_X_AND_Y_SCALE:
1807
+ xscale, yscale = struct.unpack(">hh", data[:4])
1808
+ self.transform = [
1809
+ [fi2fl(xscale, 14), 0],
1810
+ [0, fi2fl(yscale, 14)],
1811
+ ] # fixed 2.14
1812
+ data = data[4:]
1813
+ elif self.flags & WE_HAVE_A_TWO_BY_TWO:
1814
+ (xscale, scale01, scale10, yscale) = struct.unpack(">hhhh", data[:8])
1815
+ self.transform = [
1816
+ [fi2fl(xscale, 14), fi2fl(scale01, 14)],
1817
+ [fi2fl(scale10, 14), fi2fl(yscale, 14)],
1818
+ ] # fixed 2.14
1819
+ data = data[8:]
1820
+ more = self.flags & MORE_COMPONENTS
1821
+ haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS
1822
+ self.flags = self.flags & (
1823
+ ROUND_XY_TO_GRID
1824
+ | USE_MY_METRICS
1825
+ | SCALED_COMPONENT_OFFSET
1826
+ | UNSCALED_COMPONENT_OFFSET
1827
+ | NON_OVERLAPPING
1828
+ | OVERLAP_COMPOUND
1829
+ )
1830
+ return more, haveInstructions, data
1831
+
1832
+ def compile(self, more, haveInstructions, glyfTable):
1833
+ data = b""
1834
+
1835
+ # reset all flags we will calculate ourselves
1836
+ flags = self.flags & (
1837
+ ROUND_XY_TO_GRID
1838
+ | USE_MY_METRICS
1839
+ | SCALED_COMPONENT_OFFSET
1840
+ | UNSCALED_COMPONENT_OFFSET
1841
+ | NON_OVERLAPPING
1842
+ | OVERLAP_COMPOUND
1843
+ )
1844
+ if more:
1845
+ flags = flags | MORE_COMPONENTS
1846
+ if haveInstructions:
1847
+ flags = flags | WE_HAVE_INSTRUCTIONS
1848
+
1849
+ if hasattr(self, "firstPt"):
1850
+ if (0 <= self.firstPt <= 255) and (0 <= self.secondPt <= 255):
1851
+ data = data + struct.pack(">BB", self.firstPt, self.secondPt)
1852
+ else:
1853
+ data = data + struct.pack(">HH", self.firstPt, self.secondPt)
1854
+ flags = flags | ARG_1_AND_2_ARE_WORDS
1855
+ else:
1856
+ x = otRound(self.x)
1857
+ y = otRound(self.y)
1858
+ flags = flags | ARGS_ARE_XY_VALUES
1859
+ if (-128 <= x <= 127) and (-128 <= y <= 127):
1860
+ data = data + struct.pack(">bb", x, y)
1861
+ else:
1862
+ data = data + struct.pack(">hh", x, y)
1863
+ flags = flags | ARG_1_AND_2_ARE_WORDS
1864
+
1865
+ if hasattr(self, "transform"):
1866
+ transform = [[fl2fi(x, 14) for x in row] for row in self.transform]
1867
+ if transform[0][1] or transform[1][0]:
1868
+ flags = flags | WE_HAVE_A_TWO_BY_TWO
1869
+ data = data + struct.pack(
1870
+ ">hhhh",
1871
+ transform[0][0],
1872
+ transform[0][1],
1873
+ transform[1][0],
1874
+ transform[1][1],
1875
+ )
1876
+ elif transform[0][0] != transform[1][1]:
1877
+ flags = flags | WE_HAVE_AN_X_AND_Y_SCALE
1878
+ data = data + struct.pack(">hh", transform[0][0], transform[1][1])
1879
+ else:
1880
+ flags = flags | WE_HAVE_A_SCALE
1881
+ data = data + struct.pack(">h", transform[0][0])
1882
+
1883
+ glyphID = glyfTable.getGlyphID(self.glyphName)
1884
+ return struct.pack(">HH", flags, glyphID) + data
1885
+
1886
+ def toXML(self, writer, ttFont):
1887
+ attrs = [("glyphName", self.glyphName)]
1888
+ if not hasattr(self, "firstPt"):
1889
+ attrs = attrs + [("x", self.x), ("y", self.y)]
1890
+ else:
1891
+ attrs = attrs + [("firstPt", self.firstPt), ("secondPt", self.secondPt)]
1892
+
1893
+ if hasattr(self, "transform"):
1894
+ transform = self.transform
1895
+ if transform[0][1] or transform[1][0]:
1896
+ attrs = attrs + [
1897
+ ("scalex", fl2str(transform[0][0], 14)),
1898
+ ("scale01", fl2str(transform[0][1], 14)),
1899
+ ("scale10", fl2str(transform[1][0], 14)),
1900
+ ("scaley", fl2str(transform[1][1], 14)),
1901
+ ]
1902
+ elif transform[0][0] != transform[1][1]:
1903
+ attrs = attrs + [
1904
+ ("scalex", fl2str(transform[0][0], 14)),
1905
+ ("scaley", fl2str(transform[1][1], 14)),
1906
+ ]
1907
+ else:
1908
+ attrs = attrs + [("scale", fl2str(transform[0][0], 14))]
1909
+ attrs = attrs + [("flags", hex(self.flags))]
1910
+ writer.simpletag("component", attrs)
1911
+ writer.newline()
1912
+
1913
+ def fromXML(self, name, attrs, content, ttFont):
1914
+ self.glyphName = attrs["glyphName"]
1915
+ if "firstPt" in attrs:
1916
+ self.firstPt = safeEval(attrs["firstPt"])
1917
+ self.secondPt = safeEval(attrs["secondPt"])
1918
+ else:
1919
+ self.x = safeEval(attrs["x"])
1920
+ self.y = safeEval(attrs["y"])
1921
+ if "scale01" in attrs:
1922
+ scalex = str2fl(attrs["scalex"], 14)
1923
+ scale01 = str2fl(attrs["scale01"], 14)
1924
+ scale10 = str2fl(attrs["scale10"], 14)
1925
+ scaley = str2fl(attrs["scaley"], 14)
1926
+ self.transform = [[scalex, scale01], [scale10, scaley]]
1927
+ elif "scalex" in attrs:
1928
+ scalex = str2fl(attrs["scalex"], 14)
1929
+ scaley = str2fl(attrs["scaley"], 14)
1930
+ self.transform = [[scalex, 0], [0, scaley]]
1931
+ elif "scale" in attrs:
1932
+ scale = str2fl(attrs["scale"], 14)
1933
+ self.transform = [[scale, 0], [0, scale]]
1934
+ self.flags = safeEval(attrs["flags"])
1935
+
1936
+ def __eq__(self, other):
1937
+ if type(self) != type(other):
1938
+ return NotImplemented
1939
+ return self.__dict__ == other.__dict__
1940
+
1941
+ def __ne__(self, other):
1942
+ result = self.__eq__(other)
1943
+ return result if result is NotImplemented else not result
1944
+
1945
+ def _hasOnlyIntegerTranslate(self):
1946
+ """Return True if it's a 'simple' component.
1947
+
1948
+ That is, it has no anchor points and no transform other than integer translate.
1949
+ """
1950
+ return (
1951
+ not hasattr(self, "firstPt")
1952
+ and not hasattr(self, "transform")
1953
+ and float(self.x).is_integer()
1954
+ and float(self.y).is_integer()
1955
+ )
1956
+
1957
+
1958
+ class GlyphCoordinates(object):
1959
+ """A list of glyph coordinates.
1960
+
1961
+ Unlike an ordinary list, this is a numpy-like matrix object which supports
1962
+ matrix addition, scalar multiplication and other operations described below.
1963
+ """
1964
+
1965
+ def __init__(self, iterable=[]):
1966
+ self._a = array.array("d")
1967
+ self.extend(iterable)
1968
+
1969
+ @property
1970
+ def array(self):
1971
+ """Returns the underlying array of coordinates"""
1972
+ return self._a
1973
+
1974
+ @staticmethod
1975
+ def zeros(count):
1976
+ """Creates a new ``GlyphCoordinates`` object with all coordinates set to (0,0)"""
1977
+ g = GlyphCoordinates()
1978
+ g._a.frombytes(bytes(count * 2 * g._a.itemsize))
1979
+ return g
1980
+
1981
+ def copy(self):
1982
+ """Creates a new ``GlyphCoordinates`` object which is a copy of the current one."""
1983
+ c = GlyphCoordinates()
1984
+ c._a.extend(self._a)
1985
+ return c
1986
+
1987
+ def __len__(self):
1988
+ """Returns the number of coordinates in the array."""
1989
+ return len(self._a) // 2
1990
+
1991
+ def __getitem__(self, k):
1992
+ """Returns a two element tuple (x,y)"""
1993
+ a = self._a
1994
+ if isinstance(k, slice):
1995
+ indices = range(*k.indices(len(self)))
1996
+ # Instead of calling ourselves recursively, duplicate code; faster
1997
+ ret = []
1998
+ for k in indices:
1999
+ x = a[2 * k]
2000
+ y = a[2 * k + 1]
2001
+ ret.append(
2002
+ (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y)
2003
+ )
2004
+ return ret
2005
+ x = a[2 * k]
2006
+ y = a[2 * k + 1]
2007
+ return (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y)
2008
+
2009
+ def __setitem__(self, k, v):
2010
+ """Sets a point's coordinates to a two element tuple (x,y)"""
2011
+ if isinstance(k, slice):
2012
+ indices = range(*k.indices(len(self)))
2013
+ # XXX This only works if len(v) == len(indices)
2014
+ for j, i in enumerate(indices):
2015
+ self[i] = v[j]
2016
+ return
2017
+ self._a[2 * k], self._a[2 * k + 1] = v
2018
+
2019
+ def __delitem__(self, i):
2020
+ """Removes a point from the list"""
2021
+ i = (2 * i) % len(self._a)
2022
+ del self._a[i]
2023
+ del self._a[i]
2024
+
2025
+ def __repr__(self):
2026
+ return "GlyphCoordinates([" + ",".join(str(c) for c in self) + "])"
2027
+
2028
+ def append(self, p):
2029
+ self._a.extend(tuple(p))
2030
+
2031
+ def extend(self, iterable):
2032
+ for p in iterable:
2033
+ self._a.extend(p)
2034
+
2035
+ def toInt(self, *, round=otRound):
2036
+ if round is noRound:
2037
+ return
2038
+ a = self._a
2039
+ for i, value in enumerate(a):
2040
+ a[i] = round(value)
2041
+
2042
+ def calcBounds(self):
2043
+ a = self._a
2044
+ if not a:
2045
+ return 0, 0, 0, 0
2046
+ xs = a[0::2]
2047
+ ys = a[1::2]
2048
+ return min(xs), min(ys), max(xs), max(ys)
2049
+
2050
+ def calcIntBounds(self, round=otRound):
2051
+ return tuple(round(v) for v in self.calcBounds())
2052
+
2053
+ def relativeToAbsolute(self):
2054
+ a = self._a
2055
+ x, y = 0, 0
2056
+ for i in range(0, len(a), 2):
2057
+ a[i] = x = a[i] + x
2058
+ a[i + 1] = y = a[i + 1] + y
2059
+
2060
+ def absoluteToRelative(self):
2061
+ a = self._a
2062
+ x, y = 0, 0
2063
+ for i in range(0, len(a), 2):
2064
+ nx = a[i]
2065
+ ny = a[i + 1]
2066
+ a[i] = nx - x
2067
+ a[i + 1] = ny - y
2068
+ x = nx
2069
+ y = ny
2070
+
2071
+ def translate(self, p):
2072
+ """
2073
+ >>> GlyphCoordinates([(1,2)]).translate((.5,0))
2074
+ """
2075
+ x, y = p
2076
+ if x == 0 and y == 0:
2077
+ return
2078
+ a = self._a
2079
+ for i in range(0, len(a), 2):
2080
+ a[i] += x
2081
+ a[i + 1] += y
2082
+
2083
+ def scale(self, p):
2084
+ """
2085
+ >>> GlyphCoordinates([(1,2)]).scale((.5,0))
2086
+ """
2087
+ x, y = p
2088
+ if x == 1 and y == 1:
2089
+ return
2090
+ a = self._a
2091
+ for i in range(0, len(a), 2):
2092
+ a[i] *= x
2093
+ a[i + 1] *= y
2094
+
2095
+ def transform(self, t):
2096
+ """
2097
+ >>> GlyphCoordinates([(1,2)]).transform(((.5,0),(.2,.5)))
2098
+ """
2099
+ a = self._a
2100
+ for i in range(0, len(a), 2):
2101
+ x = a[i]
2102
+ y = a[i + 1]
2103
+ px = x * t[0][0] + y * t[1][0]
2104
+ py = x * t[0][1] + y * t[1][1]
2105
+ a[i] = px
2106
+ a[i + 1] = py
2107
+
2108
+ def __eq__(self, other):
2109
+ """
2110
+ >>> g = GlyphCoordinates([(1,2)])
2111
+ >>> g2 = GlyphCoordinates([(1.0,2)])
2112
+ >>> g3 = GlyphCoordinates([(1.5,2)])
2113
+ >>> g == g2
2114
+ True
2115
+ >>> g == g3
2116
+ False
2117
+ >>> g2 == g3
2118
+ False
2119
+ """
2120
+ if type(self) != type(other):
2121
+ return NotImplemented
2122
+ return self._a == other._a
2123
+
2124
+ def __ne__(self, other):
2125
+ """
2126
+ >>> g = GlyphCoordinates([(1,2)])
2127
+ >>> g2 = GlyphCoordinates([(1.0,2)])
2128
+ >>> g3 = GlyphCoordinates([(1.5,2)])
2129
+ >>> g != g2
2130
+ False
2131
+ >>> g != g3
2132
+ True
2133
+ >>> g2 != g3
2134
+ True
2135
+ """
2136
+ result = self.__eq__(other)
2137
+ return result if result is NotImplemented else not result
2138
+
2139
+ # Math operations
2140
+
2141
+ def __pos__(self):
2142
+ """
2143
+ >>> g = GlyphCoordinates([(1,2)])
2144
+ >>> g
2145
+ GlyphCoordinates([(1, 2)])
2146
+ >>> g2 = +g
2147
+ >>> g2
2148
+ GlyphCoordinates([(1, 2)])
2149
+ >>> g2.translate((1,0))
2150
+ >>> g2
2151
+ GlyphCoordinates([(2, 2)])
2152
+ >>> g
2153
+ GlyphCoordinates([(1, 2)])
2154
+ """
2155
+ return self.copy()
2156
+
2157
+ def __neg__(self):
2158
+ """
2159
+ >>> g = GlyphCoordinates([(1,2)])
2160
+ >>> g
2161
+ GlyphCoordinates([(1, 2)])
2162
+ >>> g2 = -g
2163
+ >>> g2
2164
+ GlyphCoordinates([(-1, -2)])
2165
+ >>> g
2166
+ GlyphCoordinates([(1, 2)])
2167
+ """
2168
+ r = self.copy()
2169
+ a = r._a
2170
+ for i, value in enumerate(a):
2171
+ a[i] = -value
2172
+ return r
2173
+
2174
+ def __round__(self, *, round=otRound):
2175
+ r = self.copy()
2176
+ r.toInt(round=round)
2177
+ return r
2178
+
2179
+ def __add__(self, other):
2180
+ return self.copy().__iadd__(other)
2181
+
2182
+ def __sub__(self, other):
2183
+ return self.copy().__isub__(other)
2184
+
2185
+ def __mul__(self, other):
2186
+ return self.copy().__imul__(other)
2187
+
2188
+ def __truediv__(self, other):
2189
+ return self.copy().__itruediv__(other)
2190
+
2191
+ __radd__ = __add__
2192
+ __rmul__ = __mul__
2193
+
2194
+ def __rsub__(self, other):
2195
+ return other + (-self)
2196
+
2197
+ def __iadd__(self, other):
2198
+ """
2199
+ >>> g = GlyphCoordinates([(1,2)])
2200
+ >>> g += (.5,0)
2201
+ >>> g
2202
+ GlyphCoordinates([(1.5, 2)])
2203
+ >>> g2 = GlyphCoordinates([(3,4)])
2204
+ >>> g += g2
2205
+ >>> g
2206
+ GlyphCoordinates([(4.5, 6)])
2207
+ """
2208
+ if isinstance(other, tuple):
2209
+ assert len(other) == 2
2210
+ self.translate(other)
2211
+ return self
2212
+ if isinstance(other, GlyphCoordinates):
2213
+ other = other._a
2214
+ a = self._a
2215
+ assert len(a) == len(other)
2216
+ for i, value in enumerate(other):
2217
+ a[i] += value
2218
+ return self
2219
+ return NotImplemented
2220
+
2221
+ def __isub__(self, other):
2222
+ """
2223
+ >>> g = GlyphCoordinates([(1,2)])
2224
+ >>> g -= (.5,0)
2225
+ >>> g
2226
+ GlyphCoordinates([(0.5, 2)])
2227
+ >>> g2 = GlyphCoordinates([(3,4)])
2228
+ >>> g -= g2
2229
+ >>> g
2230
+ GlyphCoordinates([(-2.5, -2)])
2231
+ """
2232
+ if isinstance(other, tuple):
2233
+ assert len(other) == 2
2234
+ self.translate((-other[0], -other[1]))
2235
+ return self
2236
+ if isinstance(other, GlyphCoordinates):
2237
+ other = other._a
2238
+ a = self._a
2239
+ assert len(a) == len(other)
2240
+ for i, value in enumerate(other):
2241
+ a[i] -= value
2242
+ return self
2243
+ return NotImplemented
2244
+
2245
+ def __imul__(self, other):
2246
+ """
2247
+ >>> g = GlyphCoordinates([(1,2)])
2248
+ >>> g *= (2,.5)
2249
+ >>> g *= 2
2250
+ >>> g
2251
+ GlyphCoordinates([(4, 2)])
2252
+ >>> g = GlyphCoordinates([(1,2)])
2253
+ >>> g *= 2
2254
+ >>> g
2255
+ GlyphCoordinates([(2, 4)])
2256
+ """
2257
+ if isinstance(other, tuple):
2258
+ assert len(other) == 2
2259
+ self.scale(other)
2260
+ return self
2261
+ if isinstance(other, Number):
2262
+ if other == 1:
2263
+ return self
2264
+ a = self._a
2265
+ for i in range(len(a)):
2266
+ a[i] *= other
2267
+ return self
2268
+ return NotImplemented
2269
+
2270
+ def __itruediv__(self, other):
2271
+ """
2272
+ >>> g = GlyphCoordinates([(1,3)])
2273
+ >>> g /= (.5,1.5)
2274
+ >>> g /= 2
2275
+ >>> g
2276
+ GlyphCoordinates([(1, 1)])
2277
+ """
2278
+ if isinstance(other, Number):
2279
+ other = (other, other)
2280
+ if isinstance(other, tuple):
2281
+ if other == (1, 1):
2282
+ return self
2283
+ assert len(other) == 2
2284
+ self.scale((1.0 / other[0], 1.0 / other[1]))
2285
+ return self
2286
+ return NotImplemented
2287
+
2288
+ def __bool__(self):
2289
+ """
2290
+ >>> g = GlyphCoordinates([])
2291
+ >>> bool(g)
2292
+ False
2293
+ >>> g = GlyphCoordinates([(0,0), (0.,0)])
2294
+ >>> bool(g)
2295
+ True
2296
+ >>> g = GlyphCoordinates([(0,0), (1,0)])
2297
+ >>> bool(g)
2298
+ True
2299
+ >>> g = GlyphCoordinates([(0,.5), (0,0)])
2300
+ >>> bool(g)
2301
+ True
2302
+ """
2303
+ return bool(self._a)
2304
+
2305
+ __nonzero__ = __bool__
2306
+
2307
+
2308
+ if __name__ == "__main__":
2309
+ import doctest, sys
2310
+
2311
+ sys.exit(doctest.testmod().failed)