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,1269 @@
1
+ from .interpolatableHelpers import *
2
+ from fontTools.ttLib import TTFont
3
+ from fontTools.ttLib.ttGlyphSet import LerpGlyphSet
4
+ from fontTools.pens.recordingPen import (
5
+ RecordingPen,
6
+ DecomposingRecordingPen,
7
+ RecordingPointPen,
8
+ )
9
+ from fontTools.pens.boundsPen import ControlBoundsPen
10
+ from fontTools.pens.cairoPen import CairoPen
11
+ from fontTools.pens.pointPen import (
12
+ SegmentToPointPen,
13
+ PointToSegmentPen,
14
+ ReverseContourPointPen,
15
+ )
16
+ from fontTools.varLib.interpolatableHelpers import (
17
+ PerContourOrComponentPen,
18
+ SimpleRecordingPointPen,
19
+ )
20
+ from itertools import cycle
21
+ from functools import wraps
22
+ from io import BytesIO
23
+ import cairo
24
+ import math
25
+ import os
26
+ import logging
27
+
28
+ log = logging.getLogger("fontTools.varLib.interpolatable")
29
+
30
+
31
+ class OverridingDict(dict):
32
+ def __init__(self, parent_dict):
33
+ self.parent_dict = parent_dict
34
+
35
+ def __missing__(self, key):
36
+ return self.parent_dict[key]
37
+
38
+
39
+ class InterpolatablePlot:
40
+ width = 8.5 * 72
41
+ height = 11 * 72
42
+ pad = 0.1 * 72
43
+ title_font_size = 24
44
+ font_size = 16
45
+ page_number = 1
46
+ head_color = (0.3, 0.3, 0.3)
47
+ label_color = (0.2, 0.2, 0.2)
48
+ border_color = (0.9, 0.9, 0.9)
49
+ border_width = 0.5
50
+ fill_color = (0.8, 0.8, 0.8)
51
+ stroke_color = (0.1, 0.1, 0.1)
52
+ stroke_width = 1
53
+ oncurve_node_color = (0, 0.8, 0, 0.7)
54
+ oncurve_node_diameter = 6
55
+ offcurve_node_color = (0, 0.5, 0, 0.7)
56
+ offcurve_node_diameter = 4
57
+ handle_color = (0, 0.5, 0, 0.7)
58
+ handle_width = 0.5
59
+ corrected_start_point_color = (0, 0.9, 0, 0.7)
60
+ corrected_start_point_size = 7
61
+ wrong_start_point_color = (1, 0, 0, 0.7)
62
+ start_point_color = (0, 0, 1, 0.7)
63
+ start_arrow_length = 9
64
+ kink_point_size = 7
65
+ kink_point_color = (1, 0, 1, 0.7)
66
+ kink_circle_size = 15
67
+ kink_circle_stroke_width = 1
68
+ kink_circle_color = (1, 0, 1, 0.7)
69
+ contour_colors = ((1, 0, 0), (0, 0, 1), (0, 1, 0), (1, 1, 0), (1, 0, 1), (0, 1, 1))
70
+ contour_alpha = 0.5
71
+ weight_issue_contour_color = (0, 0, 0, 0.4)
72
+ no_issues_label = "Your font's good! Have a cupcake..."
73
+ no_issues_label_color = (0, 0.5, 0)
74
+ cupcake_color = (0.3, 0, 0.3)
75
+ cupcake = r"""
76
+ ,@.
77
+ ,@.@@,.
78
+ ,@@,.@@@. @.@@@,.
79
+ ,@@. @@@. @@. @@,.
80
+ ,@@@.@,.@. @. @@@@,.@.@@,.
81
+ ,@@.@. @@.@@. @,. .@' @' @@,
82
+ ,@@. @. .@@.@@@. @@' @,
83
+ ,@. @@. @,
84
+ @. @,@@,. , .@@,
85
+ @,. .@,@@,. .@@,. , .@@, @, @,
86
+ @. .@. @ @@,. , @
87
+ @,.@@. @,. @@,. @. @,. @'
88
+ @@||@,. @'@,. @@,. @@ @,. @'@@, @'
89
+ \\@@@@' @,. @'@@@@' @@,. @@@' //@@@'
90
+ |||||||| @@,. @@' ||||||| |@@@|@|| ||
91
+ \\\\\\\ ||@@@|| ||||||| ||||||| //
92
+ ||||||| |||||| |||||| |||||| ||
93
+ \\\\\\ |||||| |||||| |||||| //
94
+ |||||| ||||| ||||| ||||| ||
95
+ \\\\\ ||||| ||||| ||||| //
96
+ ||||| |||| ||||| |||| ||
97
+ \\\\ |||| |||| |||| //
98
+ ||||||||||||||||||||||||
99
+ """
100
+ emoticon_color = (0, 0.3, 0.3)
101
+ shrug = r"""\_(")_/"""
102
+ underweight = r"""
103
+ o
104
+ /|\
105
+ / \
106
+ """
107
+ overweight = r"""
108
+ o
109
+ /O\
110
+ / \
111
+ """
112
+ yay = r""" \o/ """
113
+
114
+ def __init__(self, out, glyphsets, names=None, **kwargs):
115
+ self.out = out
116
+ self.glyphsets = glyphsets
117
+ self.names = names or [repr(g) for g in glyphsets]
118
+ self.toc = {}
119
+
120
+ for k, v in kwargs.items():
121
+ if not hasattr(self, k):
122
+ raise TypeError("Unknown keyword argument: %s" % k)
123
+ setattr(self, k, v)
124
+
125
+ self.panel_width = self.width / 2 - self.pad * 3
126
+ self.panel_height = (
127
+ self.height / 2 - self.pad * 6 - self.font_size * 2 - self.title_font_size
128
+ )
129
+
130
+ def __enter__(self):
131
+ return self
132
+
133
+ def __exit__(self, type, value, traceback):
134
+ pass
135
+
136
+ def show_page(self):
137
+ self.page_number += 1
138
+
139
+ def add_title_page(
140
+ self, files, *, show_tolerance=True, tolerance=None, kinkiness=None
141
+ ):
142
+ pad = self.pad
143
+ width = self.width - 3 * self.pad
144
+ height = self.height - 2 * self.pad
145
+ x = y = pad
146
+
147
+ self.draw_label(
148
+ "Problem report for:",
149
+ x=x,
150
+ y=y,
151
+ bold=True,
152
+ width=width,
153
+ font_size=self.title_font_size,
154
+ )
155
+ y += self.title_font_size
156
+
157
+ import hashlib
158
+
159
+ for file in files:
160
+ base_file = os.path.basename(file)
161
+ y += self.font_size + self.pad
162
+ self.draw_label(base_file, x=x, y=y, bold=True, width=width)
163
+ y += self.font_size + self.pad
164
+
165
+ try:
166
+ h = hashlib.sha1(open(file, "rb").read()).hexdigest()
167
+ self.draw_label("sha1: %s" % h, x=x + pad, y=y, width=width)
168
+ y += self.font_size
169
+ except IsADirectoryError:
170
+ pass
171
+
172
+ if file.endswith(".ttf"):
173
+ ttFont = TTFont(file)
174
+ name = ttFont["name"] if "name" in ttFont else None
175
+ if name:
176
+ for what, nameIDs in (
177
+ ("Family name", (21, 16, 1)),
178
+ ("Version", (5,)),
179
+ ):
180
+ n = name.getFirstDebugName(nameIDs)
181
+ if n is None:
182
+ continue
183
+ self.draw_label(
184
+ "%s: %s" % (what, n), x=x + pad, y=y, width=width
185
+ )
186
+ y += self.font_size + self.pad
187
+ elif file.endswith((".glyphs", ".glyphspackage")):
188
+ from glyphsLib import GSFont
189
+
190
+ f = GSFont(file)
191
+ for what, field in (
192
+ ("Family name", "familyName"),
193
+ ("VersionMajor", "versionMajor"),
194
+ ("VersionMinor", "_versionMinor"),
195
+ ):
196
+ self.draw_label(
197
+ "%s: %s" % (what, getattr(f, field)),
198
+ x=x + pad,
199
+ y=y,
200
+ width=width,
201
+ )
202
+ y += self.font_size + self.pad
203
+
204
+ self.draw_legend(
205
+ show_tolerance=show_tolerance, tolerance=tolerance, kinkiness=kinkiness
206
+ )
207
+ self.show_page()
208
+
209
+ def draw_legend(self, *, show_tolerance=True, tolerance=None, kinkiness=None):
210
+ cr = cairo.Context(self.surface)
211
+
212
+ x = self.pad
213
+ y = self.height - self.pad - self.font_size * 2
214
+ width = self.width - 2 * self.pad
215
+
216
+ xx = x + self.pad * 2
217
+ xxx = x + self.pad * 4
218
+
219
+ if show_tolerance:
220
+ self.draw_label(
221
+ "Tolerance: badness; closer to zero the worse", x=xxx, y=y, width=width
222
+ )
223
+ y -= self.pad + self.font_size
224
+
225
+ self.draw_label("Underweight contours", x=xxx, y=y, width=width)
226
+ cr.rectangle(xx - self.pad * 0.7, y, 1.5 * self.pad, self.font_size)
227
+ cr.set_source_rgb(*self.fill_color)
228
+ cr.fill_preserve()
229
+ if self.stroke_color:
230
+ cr.set_source_rgb(*self.stroke_color)
231
+ cr.set_line_width(self.stroke_width)
232
+ cr.stroke_preserve()
233
+ cr.set_source_rgba(*self.weight_issue_contour_color)
234
+ cr.fill()
235
+ y -= self.pad + self.font_size
236
+
237
+ self.draw_label(
238
+ "Colored contours: contours with the wrong order", x=xxx, y=y, width=width
239
+ )
240
+ cr.rectangle(xx - self.pad * 0.7, y, 1.5 * self.pad, self.font_size)
241
+ if self.fill_color:
242
+ cr.set_source_rgb(*self.fill_color)
243
+ cr.fill_preserve()
244
+ if self.stroke_color:
245
+ cr.set_source_rgb(*self.stroke_color)
246
+ cr.set_line_width(self.stroke_width)
247
+ cr.stroke_preserve()
248
+ cr.set_source_rgba(*self.contour_colors[0], self.contour_alpha)
249
+ cr.fill()
250
+ y -= self.pad + self.font_size
251
+
252
+ self.draw_label("Kink artifact", x=xxx, y=y, width=width)
253
+ self.draw_circle(
254
+ cr,
255
+ x=xx,
256
+ y=y + self.font_size * 0.5,
257
+ diameter=self.kink_circle_size,
258
+ stroke_width=self.kink_circle_stroke_width,
259
+ color=self.kink_circle_color,
260
+ )
261
+ y -= self.pad + self.font_size
262
+
263
+ self.draw_label("Point causing kink in the contour", x=xxx, y=y, width=width)
264
+ self.draw_dot(
265
+ cr,
266
+ x=xx,
267
+ y=y + self.font_size * 0.5,
268
+ diameter=self.kink_point_size,
269
+ color=self.kink_point_color,
270
+ )
271
+ y -= self.pad + self.font_size
272
+
273
+ self.draw_label("Suggested new contour start point", x=xxx, y=y, width=width)
274
+ self.draw_dot(
275
+ cr,
276
+ x=xx,
277
+ y=y + self.font_size * 0.5,
278
+ diameter=self.corrected_start_point_size,
279
+ color=self.corrected_start_point_color,
280
+ )
281
+ y -= self.pad + self.font_size
282
+
283
+ self.draw_label(
284
+ "Contour start point in contours with wrong direction",
285
+ x=xxx,
286
+ y=y,
287
+ width=width,
288
+ )
289
+ self.draw_arrow(
290
+ cr,
291
+ x=xx - self.start_arrow_length * 0.3,
292
+ y=y + self.font_size * 0.5,
293
+ color=self.wrong_start_point_color,
294
+ )
295
+ y -= self.pad + self.font_size
296
+
297
+ self.draw_label(
298
+ "Contour start point when the first two points overlap",
299
+ x=xxx,
300
+ y=y,
301
+ width=width,
302
+ )
303
+ self.draw_dot(
304
+ cr,
305
+ x=xx,
306
+ y=y + self.font_size * 0.5,
307
+ diameter=self.corrected_start_point_size,
308
+ color=self.start_point_color,
309
+ )
310
+ y -= self.pad + self.font_size
311
+
312
+ self.draw_label("Contour start point and direction", x=xxx, y=y, width=width)
313
+ self.draw_arrow(
314
+ cr,
315
+ x=xx - self.start_arrow_length * 0.3,
316
+ y=y + self.font_size * 0.5,
317
+ color=self.start_point_color,
318
+ )
319
+ y -= self.pad + self.font_size
320
+
321
+ self.draw_label("Legend:", x=x, y=y, width=width, bold=True)
322
+ y -= self.pad + self.font_size
323
+
324
+ if kinkiness is not None:
325
+ self.draw_label(
326
+ "Kink-reporting aggressiveness: %g" % kinkiness,
327
+ x=xxx,
328
+ y=y,
329
+ width=width,
330
+ )
331
+ y -= self.pad + self.font_size
332
+
333
+ if tolerance is not None:
334
+ self.draw_label(
335
+ "Error tolerance: %g" % tolerance,
336
+ x=xxx,
337
+ y=y,
338
+ width=width,
339
+ )
340
+ y -= self.pad + self.font_size
341
+
342
+ self.draw_label("Parameters:", x=x, y=y, width=width, bold=True)
343
+ y -= self.pad + self.font_size
344
+
345
+ def add_summary(self, problems):
346
+ pad = self.pad
347
+ width = self.width - 3 * self.pad
348
+ height = self.height - 2 * self.pad
349
+ x = y = pad
350
+
351
+ self.draw_label(
352
+ "Summary of problems",
353
+ x=x,
354
+ y=y,
355
+ bold=True,
356
+ width=width,
357
+ font_size=self.title_font_size,
358
+ )
359
+ y += self.title_font_size
360
+
361
+ glyphs_per_problem = defaultdict(set)
362
+ for glyphname, problems in sorted(problems.items()):
363
+ for problem in problems:
364
+ glyphs_per_problem[problem["type"]].add(glyphname)
365
+
366
+ if "nothing" in glyphs_per_problem:
367
+ del glyphs_per_problem["nothing"]
368
+
369
+ for problem_type in sorted(
370
+ glyphs_per_problem, key=lambda x: InterpolatableProblem.severity[x]
371
+ ):
372
+ y += self.font_size
373
+ self.draw_label(
374
+ "%s: %d" % (problem_type, len(glyphs_per_problem[problem_type])),
375
+ x=x,
376
+ y=y,
377
+ width=width,
378
+ bold=True,
379
+ )
380
+ y += self.font_size
381
+
382
+ for glyphname in sorted(glyphs_per_problem[problem_type]):
383
+ if y + self.font_size > height:
384
+ self.show_page()
385
+ y = self.font_size + pad
386
+ self.draw_label(glyphname, x=x + 2 * pad, y=y, width=width - 2 * pad)
387
+ y += self.font_size
388
+
389
+ self.show_page()
390
+
391
+ def _add_listing(self, title, items):
392
+ pad = self.pad
393
+ width = self.width - 2 * self.pad
394
+ height = self.height - 2 * self.pad
395
+ x = y = pad
396
+
397
+ self.draw_label(
398
+ title, x=x, y=y, bold=True, width=width, font_size=self.title_font_size
399
+ )
400
+ y += self.title_font_size + self.pad
401
+
402
+ last_glyphname = None
403
+ for page_no, (glyphname, problems) in items:
404
+ if glyphname == last_glyphname:
405
+ continue
406
+ last_glyphname = glyphname
407
+ if y + self.font_size > height:
408
+ self.show_page()
409
+ y = self.font_size + pad
410
+ self.draw_label(glyphname, x=x + 5 * pad, y=y, width=width - 2 * pad)
411
+ self.draw_label(str(page_no), x=x, y=y, width=4 * pad, align=1)
412
+ y += self.font_size
413
+
414
+ self.show_page()
415
+
416
+ def add_table_of_contents(self):
417
+ self._add_listing("Table of contents", sorted(self.toc.items()))
418
+
419
+ def add_index(self):
420
+ self._add_listing("Index", sorted(self.toc.items(), key=lambda x: x[1][0]))
421
+
422
+ def add_problems(self, problems, *, show_tolerance=True, show_page_number=True):
423
+ for glyph, glyph_problems in problems.items():
424
+ last_masters = None
425
+ current_glyph_problems = []
426
+ for p in glyph_problems:
427
+ masters = (
428
+ p["master_idx"]
429
+ if "master_idx" in p
430
+ else (p["master_1_idx"], p["master_2_idx"])
431
+ )
432
+ if masters == last_masters:
433
+ current_glyph_problems.append(p)
434
+ continue
435
+ # Flush
436
+ if current_glyph_problems:
437
+ self.add_problem(
438
+ glyph,
439
+ current_glyph_problems,
440
+ show_tolerance=show_tolerance,
441
+ show_page_number=show_page_number,
442
+ )
443
+ self.show_page()
444
+ current_glyph_problems = []
445
+ last_masters = masters
446
+ current_glyph_problems.append(p)
447
+ if current_glyph_problems:
448
+ self.add_problem(
449
+ glyph,
450
+ current_glyph_problems,
451
+ show_tolerance=show_tolerance,
452
+ show_page_number=show_page_number,
453
+ )
454
+ self.show_page()
455
+
456
+ def add_problem(
457
+ self, glyphname, problems, *, show_tolerance=True, show_page_number=True
458
+ ):
459
+ if type(problems) not in (list, tuple):
460
+ problems = [problems]
461
+
462
+ self.toc[self.page_number] = (glyphname, problems)
463
+
464
+ problem_type = problems[0]["type"]
465
+ problem_types = set(problem["type"] for problem in problems)
466
+ if not all(pt == problem_type for pt in problem_types):
467
+ problem_type = ", ".join(sorted({problem["type"] for problem in problems}))
468
+
469
+ log.info("Drawing %s: %s", glyphname, problem_type)
470
+
471
+ master_keys = (
472
+ ("master_idx",)
473
+ if "master_idx" in problems[0]
474
+ else ("master_1_idx", "master_2_idx")
475
+ )
476
+ master_indices = [problems[0][k] for k in master_keys]
477
+
478
+ if problem_type == InterpolatableProblem.MISSING:
479
+ sample_glyph = next(
480
+ i for i, m in enumerate(self.glyphsets) if m[glyphname] is not None
481
+ )
482
+ master_indices.insert(0, sample_glyph)
483
+
484
+ x = self.pad
485
+ y = self.pad
486
+
487
+ self.draw_label(
488
+ "Glyph name: " + glyphname,
489
+ x=x,
490
+ y=y,
491
+ color=self.head_color,
492
+ align=0,
493
+ bold=True,
494
+ font_size=self.title_font_size,
495
+ )
496
+ tolerance = min(p.get("tolerance", 1) for p in problems)
497
+ if tolerance < 1 and show_tolerance:
498
+ self.draw_label(
499
+ "tolerance: %.2f" % tolerance,
500
+ x=x,
501
+ y=y,
502
+ width=self.width - 2 * self.pad,
503
+ align=1,
504
+ bold=True,
505
+ )
506
+ y += self.title_font_size + self.pad
507
+ self.draw_label(
508
+ "Problems: " + problem_type,
509
+ x=x,
510
+ y=y,
511
+ width=self.width - 2 * self.pad,
512
+ color=self.head_color,
513
+ bold=True,
514
+ )
515
+ y += self.font_size + self.pad * 2
516
+
517
+ scales = []
518
+ for which, master_idx in enumerate(master_indices):
519
+ glyphset = self.glyphsets[master_idx]
520
+ name = self.names[master_idx]
521
+
522
+ self.draw_label(
523
+ name,
524
+ x=x,
525
+ y=y,
526
+ color=self.label_color,
527
+ width=self.panel_width,
528
+ align=0.5,
529
+ )
530
+ y += self.font_size + self.pad
531
+
532
+ if glyphset[glyphname] is not None:
533
+ scales.append(
534
+ self.draw_glyph(glyphset, glyphname, problems, which, x=x, y=y)
535
+ )
536
+ else:
537
+ self.draw_emoticon(self.shrug, x=x, y=y)
538
+ y += self.panel_height + self.font_size + self.pad
539
+
540
+ if any(
541
+ pt
542
+ in (
543
+ InterpolatableProblem.NOTHING,
544
+ InterpolatableProblem.WRONG_START_POINT,
545
+ InterpolatableProblem.CONTOUR_ORDER,
546
+ InterpolatableProblem.KINK,
547
+ InterpolatableProblem.UNDERWEIGHT,
548
+ InterpolatableProblem.OVERWEIGHT,
549
+ )
550
+ for pt in problem_types
551
+ ):
552
+ x = self.pad + self.panel_width + self.pad
553
+ y = self.pad
554
+ y += self.title_font_size + self.pad * 2
555
+ y += self.font_size + self.pad
556
+
557
+ glyphset1 = self.glyphsets[master_indices[0]]
558
+ glyphset2 = self.glyphsets[master_indices[1]]
559
+
560
+ # Draw the mid-way of the two masters
561
+
562
+ self.draw_label(
563
+ "midway interpolation",
564
+ x=x,
565
+ y=y,
566
+ color=self.head_color,
567
+ width=self.panel_width,
568
+ align=0.5,
569
+ )
570
+ y += self.font_size + self.pad
571
+
572
+ midway_glyphset = LerpGlyphSet(glyphset1, glyphset2)
573
+ self.draw_glyph(
574
+ midway_glyphset,
575
+ glyphname,
576
+ [{"type": "midway"}]
577
+ + [
578
+ p
579
+ for p in problems
580
+ if p["type"]
581
+ in (
582
+ InterpolatableProblem.KINK,
583
+ InterpolatableProblem.UNDERWEIGHT,
584
+ InterpolatableProblem.OVERWEIGHT,
585
+ )
586
+ ],
587
+ None,
588
+ x=x,
589
+ y=y,
590
+ scale=min(scales),
591
+ )
592
+
593
+ y += self.panel_height + self.font_size + self.pad
594
+
595
+ if any(
596
+ pt
597
+ in (
598
+ InterpolatableProblem.WRONG_START_POINT,
599
+ InterpolatableProblem.CONTOUR_ORDER,
600
+ InterpolatableProblem.KINK,
601
+ )
602
+ for pt in problem_types
603
+ ):
604
+ # Draw the proposed fix
605
+
606
+ self.draw_label(
607
+ "proposed fix",
608
+ x=x,
609
+ y=y,
610
+ color=self.head_color,
611
+ width=self.panel_width,
612
+ align=0.5,
613
+ )
614
+ y += self.font_size + self.pad
615
+
616
+ overriding1 = OverridingDict(glyphset1)
617
+ overriding2 = OverridingDict(glyphset2)
618
+ perContourPen1 = PerContourOrComponentPen(
619
+ RecordingPen, glyphset=overriding1
620
+ )
621
+ perContourPen2 = PerContourOrComponentPen(
622
+ RecordingPen, glyphset=overriding2
623
+ )
624
+ glyphset1[glyphname].draw(perContourPen1)
625
+ glyphset2[glyphname].draw(perContourPen2)
626
+
627
+ for problem in problems:
628
+ if problem["type"] == InterpolatableProblem.CONTOUR_ORDER:
629
+ fixed_contours = [
630
+ perContourPen2.value[i] for i in problems[0]["value_2"]
631
+ ]
632
+ perContourPen2.value = fixed_contours
633
+
634
+ for problem in problems:
635
+ if problem["type"] == InterpolatableProblem.WRONG_START_POINT:
636
+ # Save the wrong contours
637
+ wrongContour1 = perContourPen1.value[problem["contour"]]
638
+ wrongContour2 = perContourPen2.value[problem["contour"]]
639
+
640
+ # Convert the wrong contours to point pens
641
+ points1 = RecordingPointPen()
642
+ converter = SegmentToPointPen(points1, False)
643
+ wrongContour1.replay(converter)
644
+ points2 = RecordingPointPen()
645
+ converter = SegmentToPointPen(points2, False)
646
+ wrongContour2.replay(converter)
647
+
648
+ proposed_start = problem["value_2"]
649
+
650
+ # See if we need reversing; fragile but worth a try
651
+ if problem["reversed"]:
652
+ new_points2 = RecordingPointPen()
653
+ reversedPen = ReverseContourPointPen(new_points2)
654
+ points2.replay(reversedPen)
655
+ points2 = new_points2
656
+ proposed_start = len(points2.value) - 2 - proposed_start
657
+
658
+ # Rotate points2 so that the first point is the same as in points1
659
+ beginPath = points2.value[:1]
660
+ endPath = points2.value[-1:]
661
+ pts = points2.value[1:-1]
662
+ pts = pts[proposed_start:] + pts[:proposed_start]
663
+ points2.value = beginPath + pts + endPath
664
+
665
+ # Convert the point pens back to segment pens
666
+ segment1 = RecordingPen()
667
+ converter = PointToSegmentPen(segment1, True)
668
+ points1.replay(converter)
669
+ segment2 = RecordingPen()
670
+ converter = PointToSegmentPen(segment2, True)
671
+ points2.replay(converter)
672
+
673
+ # Replace the wrong contours
674
+ wrongContour1.value = segment1.value
675
+ wrongContour2.value = segment2.value
676
+ perContourPen1.value[problem["contour"]] = wrongContour1
677
+ perContourPen2.value[problem["contour"]] = wrongContour2
678
+
679
+ for problem in problems:
680
+ # If we have a kink, try to fix it.
681
+ if problem["type"] == InterpolatableProblem.KINK:
682
+ # Save the wrong contours
683
+ wrongContour1 = perContourPen1.value[problem["contour"]]
684
+ wrongContour2 = perContourPen2.value[problem["contour"]]
685
+
686
+ # Convert the wrong contours to point pens
687
+ points1 = RecordingPointPen()
688
+ converter = SegmentToPointPen(points1, False)
689
+ wrongContour1.replay(converter)
690
+ points2 = RecordingPointPen()
691
+ converter = SegmentToPointPen(points2, False)
692
+ wrongContour2.replay(converter)
693
+
694
+ i = problem["value"]
695
+
696
+ # Position points to be around the same ratio
697
+ # beginPath / endPath dance
698
+ j = i + 1
699
+ pt0 = points1.value[j][1][0]
700
+ pt1 = points2.value[j][1][0]
701
+ j_prev = (i - 1) % (len(points1.value) - 2) + 1
702
+ pt0_prev = points1.value[j_prev][1][0]
703
+ pt1_prev = points2.value[j_prev][1][0]
704
+ j_next = (i + 1) % (len(points1.value) - 2) + 1
705
+ pt0_next = points1.value[j_next][1][0]
706
+ pt1_next = points2.value[j_next][1][0]
707
+
708
+ pt0 = complex(*pt0)
709
+ pt1 = complex(*pt1)
710
+ pt0_prev = complex(*pt0_prev)
711
+ pt1_prev = complex(*pt1_prev)
712
+ pt0_next = complex(*pt0_next)
713
+ pt1_next = complex(*pt1_next)
714
+
715
+ # Find the ratio of the distance between the points
716
+ r0 = abs(pt0 - pt0_prev) / abs(pt0_next - pt0_prev)
717
+ r1 = abs(pt1 - pt1_prev) / abs(pt1_next - pt1_prev)
718
+ r_mid = (r0 + r1) / 2
719
+
720
+ pt0 = pt0_prev + r_mid * (pt0_next - pt0_prev)
721
+ pt1 = pt1_prev + r_mid * (pt1_next - pt1_prev)
722
+
723
+ points1.value[j] = (
724
+ points1.value[j][0],
725
+ (((pt0.real, pt0.imag),) + points1.value[j][1][1:]),
726
+ points1.value[j][2],
727
+ )
728
+ points2.value[j] = (
729
+ points2.value[j][0],
730
+ (((pt1.real, pt1.imag),) + points2.value[j][1][1:]),
731
+ points2.value[j][2],
732
+ )
733
+
734
+ # Convert the point pens back to segment pens
735
+ segment1 = RecordingPen()
736
+ converter = PointToSegmentPen(segment1, True)
737
+ points1.replay(converter)
738
+ segment2 = RecordingPen()
739
+ converter = PointToSegmentPen(segment2, True)
740
+ points2.replay(converter)
741
+
742
+ # Replace the wrong contours
743
+ wrongContour1.value = segment1.value
744
+ wrongContour2.value = segment2.value
745
+
746
+ # Assemble
747
+ fixed1 = RecordingPen()
748
+ fixed2 = RecordingPen()
749
+ for contour in perContourPen1.value:
750
+ fixed1.value.extend(contour.value)
751
+ for contour in perContourPen2.value:
752
+ fixed2.value.extend(contour.value)
753
+ fixed1.draw = fixed1.replay
754
+ fixed2.draw = fixed2.replay
755
+
756
+ overriding1[glyphname] = fixed1
757
+ overriding2[glyphname] = fixed2
758
+
759
+ try:
760
+ midway_glyphset = LerpGlyphSet(overriding1, overriding2)
761
+ self.draw_glyph(
762
+ midway_glyphset,
763
+ glyphname,
764
+ {"type": "fixed"},
765
+ None,
766
+ x=x,
767
+ y=y,
768
+ scale=min(scales),
769
+ )
770
+ except ValueError:
771
+ self.draw_emoticon(self.shrug, x=x, y=y)
772
+ y += self.panel_height + self.pad
773
+
774
+ else:
775
+ emoticon = self.shrug
776
+ if InterpolatableProblem.UNDERWEIGHT in problem_types:
777
+ emoticon = self.underweight
778
+ elif InterpolatableProblem.OVERWEIGHT in problem_types:
779
+ emoticon = self.overweight
780
+ elif InterpolatableProblem.NOTHING in problem_types:
781
+ emoticon = self.yay
782
+ self.draw_emoticon(emoticon, x=x, y=y)
783
+
784
+ if show_page_number:
785
+ self.draw_label(
786
+ str(self.page_number),
787
+ x=0,
788
+ y=self.height - self.font_size - self.pad,
789
+ width=self.width,
790
+ color=self.head_color,
791
+ align=0.5,
792
+ )
793
+
794
+ def draw_label(
795
+ self,
796
+ label,
797
+ *,
798
+ x=0,
799
+ y=0,
800
+ color=(0, 0, 0),
801
+ align=0,
802
+ bold=False,
803
+ width=None,
804
+ height=None,
805
+ font_size=None,
806
+ ):
807
+ if width is None:
808
+ width = self.width
809
+ if height is None:
810
+ height = self.height
811
+ if font_size is None:
812
+ font_size = self.font_size
813
+ cr = cairo.Context(self.surface)
814
+ cr.select_font_face(
815
+ "@cairo:",
816
+ cairo.FONT_SLANT_NORMAL,
817
+ cairo.FONT_WEIGHT_BOLD if bold else cairo.FONT_WEIGHT_NORMAL,
818
+ )
819
+ cr.set_font_size(font_size)
820
+ font_extents = cr.font_extents()
821
+ font_size = font_size * font_size / font_extents[2]
822
+ cr.set_font_size(font_size)
823
+ font_extents = cr.font_extents()
824
+
825
+ cr.set_source_rgb(*color)
826
+
827
+ extents = cr.text_extents(label)
828
+ if extents.width > width:
829
+ # Shrink
830
+ font_size *= width / extents.width
831
+ cr.set_font_size(font_size)
832
+ font_extents = cr.font_extents()
833
+ extents = cr.text_extents(label)
834
+
835
+ # Center
836
+ label_x = x + (width - extents.width) * align
837
+ label_y = y + font_extents[0]
838
+ cr.move_to(label_x, label_y)
839
+ cr.show_text(label)
840
+
841
+ def draw_glyph(self, glyphset, glyphname, problems, which, *, x=0, y=0, scale=None):
842
+ if type(problems) not in (list, tuple):
843
+ problems = [problems]
844
+
845
+ midway = any(problem["type"] == "midway" for problem in problems)
846
+ problem_type = problems[0]["type"]
847
+ problem_types = set(problem["type"] for problem in problems)
848
+ if not all(pt == problem_type for pt in problem_types):
849
+ problem_type = "mixed"
850
+ glyph = glyphset[glyphname]
851
+
852
+ recording = RecordingPen()
853
+ glyph.draw(recording)
854
+ decomposedRecording = DecomposingRecordingPen(glyphset)
855
+ glyph.draw(decomposedRecording)
856
+
857
+ boundsPen = ControlBoundsPen(glyphset)
858
+ decomposedRecording.replay(boundsPen)
859
+ bounds = boundsPen.bounds
860
+ if bounds is None:
861
+ bounds = (0, 0, 0, 0)
862
+
863
+ glyph_width = bounds[2] - bounds[0]
864
+ glyph_height = bounds[3] - bounds[1]
865
+
866
+ if glyph_width:
867
+ if scale is None:
868
+ scale = self.panel_width / glyph_width
869
+ else:
870
+ scale = min(scale, self.panel_height / glyph_height)
871
+ if glyph_height:
872
+ if scale is None:
873
+ scale = self.panel_height / glyph_height
874
+ else:
875
+ scale = min(scale, self.panel_height / glyph_height)
876
+ if scale is None:
877
+ scale = 1
878
+
879
+ cr = cairo.Context(self.surface)
880
+ cr.translate(x, y)
881
+ # Center
882
+ cr.translate(
883
+ (self.panel_width - glyph_width * scale) / 2,
884
+ (self.panel_height - glyph_height * scale) / 2,
885
+ )
886
+ cr.scale(scale, -scale)
887
+ cr.translate(-bounds[0], -bounds[3])
888
+
889
+ if self.border_color:
890
+ cr.set_source_rgb(*self.border_color)
891
+ cr.rectangle(bounds[0], bounds[1], glyph_width, glyph_height)
892
+ cr.set_line_width(self.border_width / scale)
893
+ cr.stroke()
894
+
895
+ if self.fill_color or self.stroke_color:
896
+ pen = CairoPen(glyphset, cr)
897
+ decomposedRecording.replay(pen)
898
+
899
+ if self.fill_color and problem_type != InterpolatableProblem.OPEN_PATH:
900
+ cr.set_source_rgb(*self.fill_color)
901
+ cr.fill_preserve()
902
+
903
+ if self.stroke_color:
904
+ cr.set_source_rgb(*self.stroke_color)
905
+ cr.set_line_width(self.stroke_width / scale)
906
+ cr.stroke_preserve()
907
+
908
+ cr.new_path()
909
+
910
+ if (
911
+ InterpolatableProblem.UNDERWEIGHT in problem_types
912
+ or InterpolatableProblem.OVERWEIGHT in problem_types
913
+ ):
914
+ perContourPen = PerContourOrComponentPen(RecordingPen, glyphset=glyphset)
915
+ recording.replay(perContourPen)
916
+ for problem in problems:
917
+ if problem["type"] in (
918
+ InterpolatableProblem.UNDERWEIGHT,
919
+ InterpolatableProblem.OVERWEIGHT,
920
+ ):
921
+ contour = perContourPen.value[problem["contour"]]
922
+ contour.replay(CairoPen(glyphset, cr))
923
+ cr.set_source_rgba(*self.weight_issue_contour_color)
924
+ cr.fill()
925
+
926
+ if any(
927
+ t in problem_types
928
+ for t in {
929
+ InterpolatableProblem.NOTHING,
930
+ InterpolatableProblem.NODE_COUNT,
931
+ InterpolatableProblem.NODE_INCOMPATIBILITY,
932
+ }
933
+ ):
934
+ cr.set_line_cap(cairo.LINE_CAP_ROUND)
935
+
936
+ # Oncurve nodes
937
+ for segment, args in decomposedRecording.value:
938
+ if not args:
939
+ continue
940
+ x, y = args[-1]
941
+ cr.move_to(x, y)
942
+ cr.line_to(x, y)
943
+ cr.set_source_rgba(*self.oncurve_node_color)
944
+ cr.set_line_width(self.oncurve_node_diameter / scale)
945
+ cr.stroke()
946
+
947
+ # Offcurve nodes
948
+ for segment, args in decomposedRecording.value:
949
+ if not args:
950
+ continue
951
+ for x, y in args[:-1]:
952
+ cr.move_to(x, y)
953
+ cr.line_to(x, y)
954
+ cr.set_source_rgba(*self.offcurve_node_color)
955
+ cr.set_line_width(self.offcurve_node_diameter / scale)
956
+ cr.stroke()
957
+
958
+ # Handles
959
+ for segment, args in decomposedRecording.value:
960
+ if not args:
961
+ pass
962
+ elif segment in ("moveTo", "lineTo"):
963
+ cr.move_to(*args[0])
964
+ elif segment == "qCurveTo":
965
+ for x, y in args:
966
+ cr.line_to(x, y)
967
+ cr.new_sub_path()
968
+ cr.move_to(*args[-1])
969
+ elif segment == "curveTo":
970
+ cr.line_to(*args[0])
971
+ cr.new_sub_path()
972
+ cr.move_to(*args[1])
973
+ cr.line_to(*args[2])
974
+ cr.new_sub_path()
975
+ cr.move_to(*args[-1])
976
+ else:
977
+ continue
978
+
979
+ cr.set_source_rgba(*self.handle_color)
980
+ cr.set_line_width(self.handle_width / scale)
981
+ cr.stroke()
982
+
983
+ matching = None
984
+ for problem in problems:
985
+ if problem["type"] == InterpolatableProblem.CONTOUR_ORDER:
986
+ matching = problem["value_2"]
987
+ colors = cycle(self.contour_colors)
988
+ perContourPen = PerContourOrComponentPen(
989
+ RecordingPen, glyphset=glyphset
990
+ )
991
+ recording.replay(perContourPen)
992
+ for i, contour in enumerate(perContourPen.value):
993
+ if matching[i] == i:
994
+ continue
995
+ color = next(colors)
996
+ contour.replay(CairoPen(glyphset, cr))
997
+ cr.set_source_rgba(*color, self.contour_alpha)
998
+ cr.fill()
999
+
1000
+ for problem in problems:
1001
+ if problem["type"] in (
1002
+ InterpolatableProblem.NOTHING,
1003
+ InterpolatableProblem.WRONG_START_POINT,
1004
+ ):
1005
+ idx = problem.get("contour")
1006
+
1007
+ # Draw suggested point
1008
+ if idx is not None and which == 1 and "value_2" in problem:
1009
+ perContourPen = PerContourOrComponentPen(
1010
+ RecordingPen, glyphset=glyphset
1011
+ )
1012
+ decomposedRecording.replay(perContourPen)
1013
+ points = SimpleRecordingPointPen()
1014
+ converter = SegmentToPointPen(points, False)
1015
+ perContourPen.value[
1016
+ idx if matching is None else matching[idx]
1017
+ ].replay(converter)
1018
+ targetPoint = points.value[problem["value_2"]][0]
1019
+ cr.save()
1020
+ cr.translate(*targetPoint)
1021
+ cr.scale(1 / scale, 1 / scale)
1022
+ self.draw_dot(
1023
+ cr,
1024
+ diameter=self.corrected_start_point_size,
1025
+ color=self.corrected_start_point_color,
1026
+ )
1027
+ cr.restore()
1028
+
1029
+ # Draw start-point arrow
1030
+ if which == 0 or not problem.get("reversed"):
1031
+ color = self.start_point_color
1032
+ else:
1033
+ color = self.wrong_start_point_color
1034
+ first_pt = None
1035
+ i = 0
1036
+ cr.save()
1037
+ for segment, args in decomposedRecording.value:
1038
+ if segment == "moveTo":
1039
+ first_pt = args[0]
1040
+ continue
1041
+ if first_pt is None:
1042
+ continue
1043
+ if segment == "closePath":
1044
+ second_pt = first_pt
1045
+ else:
1046
+ second_pt = args[0]
1047
+
1048
+ if idx is None or i == idx:
1049
+ cr.save()
1050
+ first_pt = complex(*first_pt)
1051
+ second_pt = complex(*second_pt)
1052
+ length = abs(second_pt - first_pt)
1053
+ cr.translate(first_pt.real, first_pt.imag)
1054
+ if length:
1055
+ # Draw arrowhead
1056
+ cr.rotate(
1057
+ math.atan2(
1058
+ second_pt.imag - first_pt.imag,
1059
+ second_pt.real - first_pt.real,
1060
+ )
1061
+ )
1062
+ cr.scale(1 / scale, 1 / scale)
1063
+ self.draw_arrow(cr, color=color)
1064
+ else:
1065
+ # Draw circle
1066
+ cr.scale(1 / scale, 1 / scale)
1067
+ self.draw_dot(
1068
+ cr,
1069
+ diameter=self.corrected_start_point_size,
1070
+ color=color,
1071
+ )
1072
+ cr.restore()
1073
+
1074
+ if idx is not None:
1075
+ break
1076
+
1077
+ first_pt = None
1078
+ i += 1
1079
+
1080
+ cr.restore()
1081
+
1082
+ if problem["type"] == InterpolatableProblem.KINK:
1083
+ idx = problem.get("contour")
1084
+ perContourPen = PerContourOrComponentPen(
1085
+ RecordingPen, glyphset=glyphset
1086
+ )
1087
+ decomposedRecording.replay(perContourPen)
1088
+ points = SimpleRecordingPointPen()
1089
+ converter = SegmentToPointPen(points, False)
1090
+ perContourPen.value[idx if matching is None else matching[idx]].replay(
1091
+ converter
1092
+ )
1093
+
1094
+ targetPoint = points.value[problem["value"]][0]
1095
+ cr.save()
1096
+ cr.translate(*targetPoint)
1097
+ cr.scale(1 / scale, 1 / scale)
1098
+ if midway:
1099
+ self.draw_circle(
1100
+ cr,
1101
+ diameter=self.kink_circle_size,
1102
+ stroke_width=self.kink_circle_stroke_width,
1103
+ color=self.kink_circle_color,
1104
+ )
1105
+ else:
1106
+ self.draw_dot(
1107
+ cr,
1108
+ diameter=self.kink_point_size,
1109
+ color=self.kink_point_color,
1110
+ )
1111
+ cr.restore()
1112
+
1113
+ return scale
1114
+
1115
+ def draw_dot(self, cr, *, x=0, y=0, color=(0, 0, 0), diameter=10):
1116
+ cr.save()
1117
+ cr.set_line_width(diameter)
1118
+ cr.set_line_cap(cairo.LINE_CAP_ROUND)
1119
+ cr.move_to(x, y)
1120
+ cr.line_to(x, y)
1121
+ if len(color) == 3:
1122
+ color = color + (1,)
1123
+ cr.set_source_rgba(*color)
1124
+ cr.stroke()
1125
+ cr.restore()
1126
+
1127
+ def draw_circle(
1128
+ self, cr, *, x=0, y=0, color=(0, 0, 0), diameter=10, stroke_width=1
1129
+ ):
1130
+ cr.save()
1131
+ cr.set_line_width(stroke_width)
1132
+ cr.set_line_cap(cairo.LINE_CAP_SQUARE)
1133
+ cr.arc(x, y, diameter / 2, 0, 2 * math.pi)
1134
+ if len(color) == 3:
1135
+ color = color + (1,)
1136
+ cr.set_source_rgba(*color)
1137
+ cr.stroke()
1138
+ cr.restore()
1139
+
1140
+ def draw_arrow(self, cr, *, x=0, y=0, color=(0, 0, 0)):
1141
+ cr.save()
1142
+ if len(color) == 3:
1143
+ color = color + (1,)
1144
+ cr.set_source_rgba(*color)
1145
+ cr.translate(self.start_arrow_length + x, y)
1146
+ cr.move_to(0, 0)
1147
+ cr.line_to(
1148
+ -self.start_arrow_length,
1149
+ -self.start_arrow_length * 0.4,
1150
+ )
1151
+ cr.line_to(
1152
+ -self.start_arrow_length,
1153
+ self.start_arrow_length * 0.4,
1154
+ )
1155
+ cr.close_path()
1156
+ cr.fill()
1157
+ cr.restore()
1158
+
1159
+ def draw_text(self, text, *, x=0, y=0, color=(0, 0, 0), width=None, height=None):
1160
+ if width is None:
1161
+ width = self.width
1162
+ if height is None:
1163
+ height = self.height
1164
+
1165
+ text = text.splitlines()
1166
+ cr = cairo.Context(self.surface)
1167
+ cr.set_source_rgb(*color)
1168
+ cr.set_font_size(self.font_size)
1169
+ cr.select_font_face(
1170
+ "@cairo:monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL
1171
+ )
1172
+ text_width = 0
1173
+ text_height = 0
1174
+ font_extents = cr.font_extents()
1175
+ font_font_size = font_extents[2]
1176
+ font_ascent = font_extents[0]
1177
+ for line in text:
1178
+ extents = cr.text_extents(line)
1179
+ text_width = max(text_width, extents.x_advance)
1180
+ text_height += font_font_size
1181
+ if not text_width:
1182
+ return
1183
+ cr.translate(x, y)
1184
+ scale = min(width / text_width, height / text_height)
1185
+ # center
1186
+ cr.translate(
1187
+ (width - text_width * scale) / 2, (height - text_height * scale) / 2
1188
+ )
1189
+ cr.scale(scale, scale)
1190
+
1191
+ cr.translate(0, font_ascent)
1192
+ for line in text:
1193
+ cr.move_to(0, 0)
1194
+ cr.show_text(line)
1195
+ cr.translate(0, font_font_size)
1196
+
1197
+ def draw_cupcake(self):
1198
+ self.draw_label(
1199
+ self.no_issues_label,
1200
+ x=self.pad,
1201
+ y=self.pad,
1202
+ color=self.no_issues_label_color,
1203
+ width=self.width - 2 * self.pad,
1204
+ align=0.5,
1205
+ bold=True,
1206
+ font_size=self.title_font_size,
1207
+ )
1208
+
1209
+ self.draw_text(
1210
+ self.cupcake,
1211
+ x=self.pad,
1212
+ y=self.pad + self.font_size,
1213
+ width=self.width - 2 * self.pad,
1214
+ height=self.height - 2 * self.pad - self.font_size,
1215
+ color=self.cupcake_color,
1216
+ )
1217
+
1218
+ def draw_emoticon(self, emoticon, x=0, y=0):
1219
+ self.draw_text(
1220
+ emoticon,
1221
+ x=x,
1222
+ y=y,
1223
+ color=self.emoticon_color,
1224
+ width=self.panel_width,
1225
+ height=self.panel_height,
1226
+ )
1227
+
1228
+
1229
+ class InterpolatablePostscriptLike(InterpolatablePlot):
1230
+ def __exit__(self, type, value, traceback):
1231
+ self.surface.finish()
1232
+
1233
+ def show_page(self):
1234
+ super().show_page()
1235
+ self.surface.show_page()
1236
+
1237
+
1238
+ class InterpolatablePS(InterpolatablePostscriptLike):
1239
+ def __enter__(self):
1240
+ self.surface = cairo.PSSurface(self.out, self.width, self.height)
1241
+ return self
1242
+
1243
+
1244
+ class InterpolatablePDF(InterpolatablePostscriptLike):
1245
+ def __enter__(self):
1246
+ self.surface = cairo.PDFSurface(self.out, self.width, self.height)
1247
+ self.surface.set_metadata(
1248
+ cairo.PDF_METADATA_CREATOR, "fonttools varLib.interpolatable"
1249
+ )
1250
+ self.surface.set_metadata(cairo.PDF_METADATA_CREATE_DATE, "")
1251
+ return self
1252
+
1253
+
1254
+ class InterpolatableSVG(InterpolatablePlot):
1255
+ def __enter__(self):
1256
+ self.sink = BytesIO()
1257
+ self.surface = cairo.SVGSurface(self.sink, self.width, self.height)
1258
+ return self
1259
+
1260
+ def __exit__(self, type, value, traceback):
1261
+ if self.surface is not None:
1262
+ self.show_page()
1263
+
1264
+ def show_page(self):
1265
+ super().show_page()
1266
+ self.surface.finish()
1267
+ self.out.append(self.sink.getvalue())
1268
+ self.sink = BytesIO()
1269
+ self.surface = cairo.SVGSurface(self.sink, self.width, self.height)