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.
- fontTools/__init__.py +8 -0
- fontTools/__main__.py +35 -0
- fontTools/afmLib.py +439 -0
- fontTools/agl.py +5233 -0
- fontTools/annotations.py +30 -0
- fontTools/cffLib/CFF2ToCFF.py +258 -0
- fontTools/cffLib/CFFToCFF2.py +305 -0
- fontTools/cffLib/__init__.py +3694 -0
- fontTools/cffLib/specializer.py +927 -0
- fontTools/cffLib/transforms.py +495 -0
- fontTools/cffLib/width.py +210 -0
- fontTools/colorLib/__init__.py +0 -0
- fontTools/colorLib/builder.py +664 -0
- fontTools/colorLib/errors.py +2 -0
- fontTools/colorLib/geometry.py +143 -0
- fontTools/colorLib/table_builder.py +223 -0
- fontTools/colorLib/unbuilder.py +81 -0
- fontTools/config/__init__.py +90 -0
- fontTools/cu2qu/__init__.py +15 -0
- fontTools/cu2qu/__main__.py +6 -0
- fontTools/cu2qu/benchmark.py +54 -0
- fontTools/cu2qu/cli.py +198 -0
- fontTools/cu2qu/cu2qu.c +15817 -0
- fontTools/cu2qu/cu2qu.cp311-win32.pyd +0 -0
- fontTools/cu2qu/cu2qu.py +563 -0
- fontTools/cu2qu/errors.py +77 -0
- fontTools/cu2qu/ufo.py +363 -0
- fontTools/designspaceLib/__init__.py +3343 -0
- fontTools/designspaceLib/__main__.py +6 -0
- fontTools/designspaceLib/split.py +475 -0
- fontTools/designspaceLib/statNames.py +260 -0
- fontTools/designspaceLib/types.py +147 -0
- fontTools/encodings/MacRoman.py +258 -0
- fontTools/encodings/StandardEncoding.py +258 -0
- fontTools/encodings/__init__.py +1 -0
- fontTools/encodings/codecs.py +135 -0
- fontTools/feaLib/__init__.py +4 -0
- fontTools/feaLib/__main__.py +78 -0
- fontTools/feaLib/ast.py +2143 -0
- fontTools/feaLib/builder.py +1814 -0
- fontTools/feaLib/error.py +22 -0
- fontTools/feaLib/lexer.c +17029 -0
- fontTools/feaLib/lexer.cp311-win32.pyd +0 -0
- fontTools/feaLib/lexer.py +287 -0
- fontTools/feaLib/location.py +12 -0
- fontTools/feaLib/lookupDebugInfo.py +12 -0
- fontTools/feaLib/parser.py +2394 -0
- fontTools/feaLib/variableScalar.py +118 -0
- fontTools/fontBuilder.py +1014 -0
- fontTools/help.py +36 -0
- fontTools/merge/__init__.py +248 -0
- fontTools/merge/__main__.py +6 -0
- fontTools/merge/base.py +81 -0
- fontTools/merge/cmap.py +173 -0
- fontTools/merge/layout.py +526 -0
- fontTools/merge/options.py +85 -0
- fontTools/merge/tables.py +352 -0
- fontTools/merge/unicode.py +78 -0
- fontTools/merge/util.py +143 -0
- fontTools/misc/__init__.py +1 -0
- fontTools/misc/arrayTools.py +424 -0
- fontTools/misc/bezierTools.c +39731 -0
- fontTools/misc/bezierTools.cp311-win32.pyd +0 -0
- fontTools/misc/bezierTools.py +1500 -0
- fontTools/misc/classifyTools.py +170 -0
- fontTools/misc/cliTools.py +53 -0
- fontTools/misc/configTools.py +349 -0
- fontTools/misc/cython.py +27 -0
- fontTools/misc/dictTools.py +83 -0
- fontTools/misc/eexec.py +119 -0
- fontTools/misc/encodingTools.py +72 -0
- fontTools/misc/enumTools.py +23 -0
- fontTools/misc/etree.py +456 -0
- fontTools/misc/filenames.py +245 -0
- fontTools/misc/filesystem/__init__.py +68 -0
- fontTools/misc/filesystem/_base.py +134 -0
- fontTools/misc/filesystem/_copy.py +45 -0
- fontTools/misc/filesystem/_errors.py +54 -0
- fontTools/misc/filesystem/_info.py +75 -0
- fontTools/misc/filesystem/_osfs.py +164 -0
- fontTools/misc/filesystem/_path.py +67 -0
- fontTools/misc/filesystem/_subfs.py +92 -0
- fontTools/misc/filesystem/_tempfs.py +34 -0
- fontTools/misc/filesystem/_tools.py +34 -0
- fontTools/misc/filesystem/_walk.py +55 -0
- fontTools/misc/filesystem/_zipfs.py +204 -0
- fontTools/misc/fixedTools.py +253 -0
- fontTools/misc/intTools.py +25 -0
- fontTools/misc/iterTools.py +12 -0
- fontTools/misc/lazyTools.py +42 -0
- fontTools/misc/loggingTools.py +543 -0
- fontTools/misc/macCreatorType.py +56 -0
- fontTools/misc/macRes.py +261 -0
- fontTools/misc/plistlib/__init__.py +681 -0
- fontTools/misc/plistlib/py.typed +0 -0
- fontTools/misc/psCharStrings.py +1511 -0
- fontTools/misc/psLib.py +398 -0
- fontTools/misc/psOperators.py +572 -0
- fontTools/misc/py23.py +96 -0
- fontTools/misc/roundTools.py +110 -0
- fontTools/misc/sstruct.py +227 -0
- fontTools/misc/symfont.py +242 -0
- fontTools/misc/testTools.py +233 -0
- fontTools/misc/textTools.py +156 -0
- fontTools/misc/timeTools.py +88 -0
- fontTools/misc/transform.py +516 -0
- fontTools/misc/treeTools.py +45 -0
- fontTools/misc/vector.py +147 -0
- fontTools/misc/visitor.py +158 -0
- fontTools/misc/xmlReader.py +188 -0
- fontTools/misc/xmlWriter.py +231 -0
- fontTools/mtiLib/__init__.py +1400 -0
- fontTools/mtiLib/__main__.py +5 -0
- fontTools/otlLib/__init__.py +1 -0
- fontTools/otlLib/builder.py +3465 -0
- fontTools/otlLib/error.py +11 -0
- fontTools/otlLib/maxContextCalc.py +96 -0
- fontTools/otlLib/optimize/__init__.py +53 -0
- fontTools/otlLib/optimize/__main__.py +6 -0
- fontTools/otlLib/optimize/gpos.py +439 -0
- fontTools/pens/__init__.py +1 -0
- fontTools/pens/areaPen.py +52 -0
- fontTools/pens/basePen.py +475 -0
- fontTools/pens/boundsPen.py +98 -0
- fontTools/pens/cairoPen.py +26 -0
- fontTools/pens/cocoaPen.py +26 -0
- fontTools/pens/cu2quPen.py +325 -0
- fontTools/pens/explicitClosingLinePen.py +101 -0
- fontTools/pens/filterPen.py +433 -0
- fontTools/pens/freetypePen.py +462 -0
- fontTools/pens/hashPointPen.py +89 -0
- fontTools/pens/momentsPen.c +13378 -0
- fontTools/pens/momentsPen.cp311-win32.pyd +0 -0
- fontTools/pens/momentsPen.py +879 -0
- fontTools/pens/perimeterPen.py +69 -0
- fontTools/pens/pointInsidePen.py +192 -0
- fontTools/pens/pointPen.py +643 -0
- fontTools/pens/qtPen.py +29 -0
- fontTools/pens/qu2cuPen.py +105 -0
- fontTools/pens/quartzPen.py +43 -0
- fontTools/pens/recordingPen.py +335 -0
- fontTools/pens/reportLabPen.py +79 -0
- fontTools/pens/reverseContourPen.py +96 -0
- fontTools/pens/roundingPen.py +130 -0
- fontTools/pens/statisticsPen.py +312 -0
- fontTools/pens/svgPathPen.py +310 -0
- fontTools/pens/t2CharStringPen.py +88 -0
- fontTools/pens/teePen.py +55 -0
- fontTools/pens/transformPen.py +115 -0
- fontTools/pens/ttGlyphPen.py +335 -0
- fontTools/pens/wxPen.py +29 -0
- fontTools/qu2cu/__init__.py +15 -0
- fontTools/qu2cu/__main__.py +7 -0
- fontTools/qu2cu/benchmark.py +56 -0
- fontTools/qu2cu/cli.py +125 -0
- fontTools/qu2cu/qu2cu.c +16682 -0
- fontTools/qu2cu/qu2cu.cp311-win32.pyd +0 -0
- fontTools/qu2cu/qu2cu.py +405 -0
- fontTools/subset/__init__.py +4096 -0
- fontTools/subset/__main__.py +6 -0
- fontTools/subset/cff.py +184 -0
- fontTools/subset/svg.py +253 -0
- fontTools/subset/util.py +25 -0
- fontTools/svgLib/__init__.py +3 -0
- fontTools/svgLib/path/__init__.py +65 -0
- fontTools/svgLib/path/arc.py +154 -0
- fontTools/svgLib/path/parser.py +322 -0
- fontTools/svgLib/path/shapes.py +183 -0
- fontTools/t1Lib/__init__.py +648 -0
- fontTools/tfmLib.py +460 -0
- fontTools/ttLib/__init__.py +30 -0
- fontTools/ttLib/__main__.py +148 -0
- fontTools/ttLib/macUtils.py +54 -0
- fontTools/ttLib/removeOverlaps.py +395 -0
- fontTools/ttLib/reorderGlyphs.py +285 -0
- fontTools/ttLib/scaleUpem.py +436 -0
- fontTools/ttLib/sfnt.py +661 -0
- fontTools/ttLib/standardGlyphOrder.py +271 -0
- fontTools/ttLib/tables/B_A_S_E_.py +14 -0
- fontTools/ttLib/tables/BitmapGlyphMetrics.py +64 -0
- fontTools/ttLib/tables/C_B_D_T_.py +113 -0
- fontTools/ttLib/tables/C_B_L_C_.py +19 -0
- fontTools/ttLib/tables/C_F_F_.py +61 -0
- fontTools/ttLib/tables/C_F_F__2.py +26 -0
- fontTools/ttLib/tables/C_O_L_R_.py +165 -0
- fontTools/ttLib/tables/C_P_A_L_.py +305 -0
- fontTools/ttLib/tables/D_S_I_G_.py +158 -0
- fontTools/ttLib/tables/D__e_b_g.py +35 -0
- fontTools/ttLib/tables/DefaultTable.py +49 -0
- fontTools/ttLib/tables/E_B_D_T_.py +835 -0
- fontTools/ttLib/tables/E_B_L_C_.py +718 -0
- fontTools/ttLib/tables/F_F_T_M_.py +52 -0
- fontTools/ttLib/tables/F__e_a_t.py +149 -0
- fontTools/ttLib/tables/G_D_E_F_.py +13 -0
- fontTools/ttLib/tables/G_M_A_P_.py +148 -0
- fontTools/ttLib/tables/G_P_K_G_.py +133 -0
- fontTools/ttLib/tables/G_P_O_S_.py +14 -0
- fontTools/ttLib/tables/G_S_U_B_.py +13 -0
- fontTools/ttLib/tables/G_V_A_R_.py +5 -0
- fontTools/ttLib/tables/G__l_a_t.py +235 -0
- fontTools/ttLib/tables/G__l_o_c.py +85 -0
- fontTools/ttLib/tables/H_V_A_R_.py +13 -0
- fontTools/ttLib/tables/J_S_T_F_.py +13 -0
- fontTools/ttLib/tables/L_T_S_H_.py +58 -0
- fontTools/ttLib/tables/M_A_T_H_.py +13 -0
- fontTools/ttLib/tables/M_E_T_A_.py +352 -0
- fontTools/ttLib/tables/M_V_A_R_.py +13 -0
- fontTools/ttLib/tables/O_S_2f_2.py +752 -0
- fontTools/ttLib/tables/S_I_N_G_.py +99 -0
- fontTools/ttLib/tables/S_T_A_T_.py +15 -0
- fontTools/ttLib/tables/S_V_G_.py +223 -0
- fontTools/ttLib/tables/S__i_l_f.py +1040 -0
- fontTools/ttLib/tables/S__i_l_l.py +92 -0
- fontTools/ttLib/tables/T_S_I_B_.py +13 -0
- fontTools/ttLib/tables/T_S_I_C_.py +14 -0
- fontTools/ttLib/tables/T_S_I_D_.py +13 -0
- fontTools/ttLib/tables/T_S_I_J_.py +13 -0
- fontTools/ttLib/tables/T_S_I_P_.py +13 -0
- fontTools/ttLib/tables/T_S_I_S_.py +13 -0
- fontTools/ttLib/tables/T_S_I_V_.py +26 -0
- fontTools/ttLib/tables/T_S_I__0.py +70 -0
- fontTools/ttLib/tables/T_S_I__1.py +163 -0
- fontTools/ttLib/tables/T_S_I__2.py +17 -0
- fontTools/ttLib/tables/T_S_I__3.py +22 -0
- fontTools/ttLib/tables/T_S_I__5.py +60 -0
- fontTools/ttLib/tables/T_T_F_A_.py +14 -0
- fontTools/ttLib/tables/TupleVariation.py +884 -0
- fontTools/ttLib/tables/V_A_R_C_.py +12 -0
- fontTools/ttLib/tables/V_D_M_X_.py +249 -0
- fontTools/ttLib/tables/V_O_R_G_.py +165 -0
- fontTools/ttLib/tables/V_V_A_R_.py +13 -0
- fontTools/ttLib/tables/__init__.py +98 -0
- fontTools/ttLib/tables/_a_n_k_r.py +15 -0
- fontTools/ttLib/tables/_a_v_a_r.py +193 -0
- fontTools/ttLib/tables/_b_s_l_n.py +15 -0
- fontTools/ttLib/tables/_c_i_d_g.py +24 -0
- fontTools/ttLib/tables/_c_m_a_p.py +1591 -0
- fontTools/ttLib/tables/_c_v_a_r.py +94 -0
- fontTools/ttLib/tables/_c_v_t.py +56 -0
- fontTools/ttLib/tables/_f_e_a_t.py +15 -0
- fontTools/ttLib/tables/_f_p_g_m.py +62 -0
- fontTools/ttLib/tables/_f_v_a_r.py +261 -0
- fontTools/ttLib/tables/_g_a_s_p.py +63 -0
- fontTools/ttLib/tables/_g_c_i_d.py +13 -0
- fontTools/ttLib/tables/_g_l_y_f.py +2311 -0
- fontTools/ttLib/tables/_g_v_a_r.py +340 -0
- fontTools/ttLib/tables/_h_d_m_x.py +127 -0
- fontTools/ttLib/tables/_h_e_a_d.py +130 -0
- fontTools/ttLib/tables/_h_h_e_a.py +147 -0
- fontTools/ttLib/tables/_h_m_t_x.py +164 -0
- fontTools/ttLib/tables/_k_e_r_n.py +289 -0
- fontTools/ttLib/tables/_l_c_a_r.py +13 -0
- fontTools/ttLib/tables/_l_o_c_a.py +70 -0
- fontTools/ttLib/tables/_l_t_a_g.py +72 -0
- fontTools/ttLib/tables/_m_a_x_p.py +147 -0
- fontTools/ttLib/tables/_m_e_t_a.py +112 -0
- fontTools/ttLib/tables/_m_o_r_t.py +14 -0
- fontTools/ttLib/tables/_m_o_r_x.py +15 -0
- fontTools/ttLib/tables/_n_a_m_e.py +1242 -0
- fontTools/ttLib/tables/_o_p_b_d.py +14 -0
- fontTools/ttLib/tables/_p_o_s_t.py +319 -0
- fontTools/ttLib/tables/_p_r_e_p.py +16 -0
- fontTools/ttLib/tables/_p_r_o_p.py +12 -0
- fontTools/ttLib/tables/_s_b_i_x.py +129 -0
- fontTools/ttLib/tables/_t_r_a_k.py +332 -0
- fontTools/ttLib/tables/_v_h_e_a.py +139 -0
- fontTools/ttLib/tables/_v_m_t_x.py +19 -0
- fontTools/ttLib/tables/asciiTable.py +20 -0
- fontTools/ttLib/tables/grUtils.py +92 -0
- fontTools/ttLib/tables/otBase.py +1458 -0
- fontTools/ttLib/tables/otConverters.py +2068 -0
- fontTools/ttLib/tables/otData.py +6400 -0
- fontTools/ttLib/tables/otTables.py +2703 -0
- fontTools/ttLib/tables/otTraverse.py +163 -0
- fontTools/ttLib/tables/sbixGlyph.py +149 -0
- fontTools/ttLib/tables/sbixStrike.py +177 -0
- fontTools/ttLib/tables/table_API_readme.txt +91 -0
- fontTools/ttLib/tables/ttProgram.py +594 -0
- fontTools/ttLib/ttCollection.py +125 -0
- fontTools/ttLib/ttFont.py +1148 -0
- fontTools/ttLib/ttGlyphSet.py +490 -0
- fontTools/ttLib/ttVisitor.py +32 -0
- fontTools/ttLib/woff2.py +1680 -0
- fontTools/ttx.py +479 -0
- fontTools/ufoLib/__init__.py +2575 -0
- fontTools/ufoLib/converters.py +407 -0
- fontTools/ufoLib/errors.py +30 -0
- fontTools/ufoLib/etree.py +6 -0
- fontTools/ufoLib/filenames.py +356 -0
- fontTools/ufoLib/glifLib.py +2120 -0
- fontTools/ufoLib/kerning.py +141 -0
- fontTools/ufoLib/plistlib.py +47 -0
- fontTools/ufoLib/pointPen.py +6 -0
- fontTools/ufoLib/utils.py +107 -0
- fontTools/ufoLib/validators.py +1208 -0
- fontTools/unicode.py +50 -0
- fontTools/unicodedata/Blocks.py +817 -0
- fontTools/unicodedata/Mirrored.py +446 -0
- fontTools/unicodedata/OTTags.py +50 -0
- fontTools/unicodedata/ScriptExtensions.py +832 -0
- fontTools/unicodedata/Scripts.py +3639 -0
- fontTools/unicodedata/__init__.py +306 -0
- fontTools/varLib/__init__.py +1600 -0
- fontTools/varLib/__main__.py +6 -0
- fontTools/varLib/avar/__init__.py +0 -0
- fontTools/varLib/avar/__main__.py +72 -0
- fontTools/varLib/avar/build.py +79 -0
- fontTools/varLib/avar/map.py +108 -0
- fontTools/varLib/avar/plan.py +1004 -0
- fontTools/varLib/avar/unbuild.py +271 -0
- fontTools/varLib/avarPlanner.py +8 -0
- fontTools/varLib/builder.py +215 -0
- fontTools/varLib/cff.py +631 -0
- fontTools/varLib/errors.py +219 -0
- fontTools/varLib/featureVars.py +703 -0
- fontTools/varLib/hvar.py +113 -0
- fontTools/varLib/instancer/__init__.py +2052 -0
- fontTools/varLib/instancer/__main__.py +5 -0
- fontTools/varLib/instancer/featureVars.py +190 -0
- fontTools/varLib/instancer/names.py +388 -0
- fontTools/varLib/instancer/solver.py +309 -0
- fontTools/varLib/interpolatable.py +1209 -0
- fontTools/varLib/interpolatableHelpers.py +399 -0
- fontTools/varLib/interpolatablePlot.py +1269 -0
- fontTools/varLib/interpolatableTestContourOrder.py +82 -0
- fontTools/varLib/interpolatableTestStartingPoint.py +107 -0
- fontTools/varLib/interpolate_layout.py +124 -0
- fontTools/varLib/iup.c +19815 -0
- fontTools/varLib/iup.cp311-win32.pyd +0 -0
- fontTools/varLib/iup.py +490 -0
- fontTools/varLib/merger.py +1717 -0
- fontTools/varLib/models.py +642 -0
- fontTools/varLib/multiVarStore.py +253 -0
- fontTools/varLib/mutator.py +529 -0
- fontTools/varLib/mvar.py +40 -0
- fontTools/varLib/plot.py +238 -0
- fontTools/varLib/stat.py +149 -0
- fontTools/varLib/varStore.py +739 -0
- fontTools/voltLib/__init__.py +5 -0
- fontTools/voltLib/__main__.py +206 -0
- fontTools/voltLib/ast.py +452 -0
- fontTools/voltLib/error.py +12 -0
- fontTools/voltLib/lexer.py +99 -0
- fontTools/voltLib/parser.py +664 -0
- fontTools/voltLib/voltToFea.py +911 -0
- fonttools-4.60.2.data/data/share/man/man1/ttx.1 +225 -0
- fonttools-4.60.2.dist-info/METADATA +2250 -0
- fonttools-4.60.2.dist-info/RECORD +353 -0
- fonttools-4.60.2.dist-info/WHEEL +5 -0
- fonttools-4.60.2.dist-info/entry_points.txt +5 -0
- fonttools-4.60.2.dist-info/licenses/LICENSE +21 -0
- fonttools-4.60.2.dist-info/licenses/LICENSE.external +388 -0
- fonttools-4.60.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1209 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool to find wrong contour order between different masters, and
|
|
3
|
+
other interpolatability (or lack thereof) issues.
|
|
4
|
+
|
|
5
|
+
Call as:
|
|
6
|
+
$ fonttools varLib.interpolatable font1 font2 ...
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .interpolatableHelpers import *
|
|
10
|
+
from .interpolatableTestContourOrder import test_contour_order
|
|
11
|
+
from .interpolatableTestStartingPoint import test_starting_point
|
|
12
|
+
from fontTools.pens.recordingPen import (
|
|
13
|
+
RecordingPen,
|
|
14
|
+
DecomposingRecordingPen,
|
|
15
|
+
lerpRecordings,
|
|
16
|
+
)
|
|
17
|
+
from fontTools.pens.transformPen import TransformPen
|
|
18
|
+
from fontTools.pens.statisticsPen import StatisticsPen, StatisticsControlPen
|
|
19
|
+
from fontTools.pens.momentsPen import OpenContourError
|
|
20
|
+
from fontTools.varLib.models import piecewiseLinearMap, normalizeLocation
|
|
21
|
+
from fontTools.misc.fixedTools import floatToFixedToStr
|
|
22
|
+
from fontTools.misc.transform import Transform
|
|
23
|
+
from collections import defaultdict
|
|
24
|
+
from types import SimpleNamespace
|
|
25
|
+
from functools import wraps
|
|
26
|
+
from pprint import pformat
|
|
27
|
+
from math import sqrt, atan2, pi
|
|
28
|
+
import logging
|
|
29
|
+
import os
|
|
30
|
+
|
|
31
|
+
log = logging.getLogger("fontTools.varLib.interpolatable")
|
|
32
|
+
|
|
33
|
+
DEFAULT_TOLERANCE = 0.95
|
|
34
|
+
DEFAULT_KINKINESS = 0.5
|
|
35
|
+
DEFAULT_KINKINESS_LENGTH = 0.002 # ratio of UPEM
|
|
36
|
+
DEFAULT_UPEM = 1000
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Glyph:
|
|
40
|
+
ITEMS = (
|
|
41
|
+
"recordings",
|
|
42
|
+
"greenStats",
|
|
43
|
+
"controlStats",
|
|
44
|
+
"greenVectors",
|
|
45
|
+
"controlVectors",
|
|
46
|
+
"nodeTypes",
|
|
47
|
+
"isomorphisms",
|
|
48
|
+
"points",
|
|
49
|
+
"openContours",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def __init__(self, glyphname, glyphset):
|
|
53
|
+
self.name = glyphname
|
|
54
|
+
for item in self.ITEMS:
|
|
55
|
+
setattr(self, item, [])
|
|
56
|
+
self._populate(glyphset)
|
|
57
|
+
|
|
58
|
+
def _fill_in(self, ix):
|
|
59
|
+
for item in self.ITEMS:
|
|
60
|
+
if len(getattr(self, item)) == ix:
|
|
61
|
+
getattr(self, item).append(None)
|
|
62
|
+
|
|
63
|
+
def _populate(self, glyphset):
|
|
64
|
+
glyph = glyphset[self.name]
|
|
65
|
+
self.doesnt_exist = glyph is None
|
|
66
|
+
if self.doesnt_exist:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
perContourPen = PerContourOrComponentPen(RecordingPen, glyphset=glyphset)
|
|
70
|
+
try:
|
|
71
|
+
glyph.draw(perContourPen, outputImpliedClosingLine=True)
|
|
72
|
+
except TypeError:
|
|
73
|
+
glyph.draw(perContourPen)
|
|
74
|
+
self.recordings = perContourPen.value
|
|
75
|
+
del perContourPen
|
|
76
|
+
|
|
77
|
+
for ix, contour in enumerate(self.recordings):
|
|
78
|
+
nodeTypes = [op for op, arg in contour.value]
|
|
79
|
+
self.nodeTypes.append(nodeTypes)
|
|
80
|
+
|
|
81
|
+
greenStats = StatisticsPen(glyphset=glyphset)
|
|
82
|
+
controlStats = StatisticsControlPen(glyphset=glyphset)
|
|
83
|
+
try:
|
|
84
|
+
contour.replay(greenStats)
|
|
85
|
+
contour.replay(controlStats)
|
|
86
|
+
self.openContours.append(False)
|
|
87
|
+
except OpenContourError as e:
|
|
88
|
+
self.openContours.append(True)
|
|
89
|
+
self._fill_in(ix)
|
|
90
|
+
continue
|
|
91
|
+
self.greenStats.append(greenStats)
|
|
92
|
+
self.controlStats.append(controlStats)
|
|
93
|
+
self.greenVectors.append(contour_vector_from_stats(greenStats))
|
|
94
|
+
self.controlVectors.append(contour_vector_from_stats(controlStats))
|
|
95
|
+
|
|
96
|
+
# Check starting point
|
|
97
|
+
if nodeTypes[0] == "addComponent":
|
|
98
|
+
self._fill_in(ix)
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
assert nodeTypes[0] == "moveTo"
|
|
102
|
+
assert nodeTypes[-1] in ("closePath", "endPath")
|
|
103
|
+
points = SimpleRecordingPointPen()
|
|
104
|
+
converter = SegmentToPointPen(points, False)
|
|
105
|
+
contour.replay(converter)
|
|
106
|
+
# points.value is a list of pt,bool where bool is true if on-curve and false if off-curve;
|
|
107
|
+
# now check all rotations and mirror-rotations of the contour and build list of isomorphic
|
|
108
|
+
# possible starting points.
|
|
109
|
+
self.points.append(points.value)
|
|
110
|
+
|
|
111
|
+
isomorphisms = []
|
|
112
|
+
self.isomorphisms.append(isomorphisms)
|
|
113
|
+
|
|
114
|
+
# Add rotations
|
|
115
|
+
add_isomorphisms(points.value, isomorphisms, False)
|
|
116
|
+
# Add mirrored rotations
|
|
117
|
+
add_isomorphisms(points.value, isomorphisms, True)
|
|
118
|
+
|
|
119
|
+
def draw(self, pen, countor_idx=None):
|
|
120
|
+
if countor_idx is None:
|
|
121
|
+
for contour in self.recordings:
|
|
122
|
+
contour.draw(pen)
|
|
123
|
+
else:
|
|
124
|
+
self.recordings[countor_idx].draw(pen)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_gen(
|
|
128
|
+
glyphsets,
|
|
129
|
+
glyphs=None,
|
|
130
|
+
names=None,
|
|
131
|
+
ignore_missing=False,
|
|
132
|
+
*,
|
|
133
|
+
locations=None,
|
|
134
|
+
tolerance=DEFAULT_TOLERANCE,
|
|
135
|
+
kinkiness=DEFAULT_KINKINESS,
|
|
136
|
+
upem=DEFAULT_UPEM,
|
|
137
|
+
show_all=False,
|
|
138
|
+
discrete_axes=[],
|
|
139
|
+
):
|
|
140
|
+
if tolerance >= 10:
|
|
141
|
+
tolerance *= 0.01
|
|
142
|
+
assert 0 <= tolerance <= 1
|
|
143
|
+
if kinkiness >= 10:
|
|
144
|
+
kinkiness *= 0.01
|
|
145
|
+
assert 0 <= kinkiness
|
|
146
|
+
|
|
147
|
+
names = names or [repr(g) for g in glyphsets]
|
|
148
|
+
|
|
149
|
+
if glyphs is None:
|
|
150
|
+
# `glyphs = glyphsets[0].keys()` is faster, certainly, but doesn't allow for sparse TTFs/OTFs given out of order
|
|
151
|
+
# ... risks the sparse master being the first one, and only processing a subset of the glyphs
|
|
152
|
+
glyphs = {g for glyphset in glyphsets for g in glyphset.keys()}
|
|
153
|
+
|
|
154
|
+
parents, order = find_parents_and_order(
|
|
155
|
+
glyphsets, locations, discrete_axes=discrete_axes
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def grand_parent(i, glyphname):
|
|
159
|
+
if i is None:
|
|
160
|
+
return None
|
|
161
|
+
i = parents[i]
|
|
162
|
+
if i is None:
|
|
163
|
+
return None
|
|
164
|
+
while parents[i] is not None and glyphsets[i][glyphname] is None:
|
|
165
|
+
i = parents[i]
|
|
166
|
+
return i
|
|
167
|
+
|
|
168
|
+
for glyph_name in glyphs:
|
|
169
|
+
log.info("Testing glyph %s", glyph_name)
|
|
170
|
+
allGlyphs = [Glyph(glyph_name, glyphset) for glyphset in glyphsets]
|
|
171
|
+
if len([1 for glyph in allGlyphs if glyph is not None]) <= 1:
|
|
172
|
+
continue
|
|
173
|
+
for master_idx, (glyph, glyphset, name) in enumerate(
|
|
174
|
+
zip(allGlyphs, glyphsets, names)
|
|
175
|
+
):
|
|
176
|
+
if glyph.doesnt_exist:
|
|
177
|
+
if not ignore_missing:
|
|
178
|
+
yield (
|
|
179
|
+
glyph_name,
|
|
180
|
+
{
|
|
181
|
+
"type": InterpolatableProblem.MISSING,
|
|
182
|
+
"master": name,
|
|
183
|
+
"master_idx": master_idx,
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
has_open = False
|
|
189
|
+
for ix, open in enumerate(glyph.openContours):
|
|
190
|
+
if not open:
|
|
191
|
+
continue
|
|
192
|
+
has_open = True
|
|
193
|
+
yield (
|
|
194
|
+
glyph_name,
|
|
195
|
+
{
|
|
196
|
+
"type": InterpolatableProblem.OPEN_PATH,
|
|
197
|
+
"master": name,
|
|
198
|
+
"master_idx": master_idx,
|
|
199
|
+
"contour": ix,
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
if has_open:
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
matchings = [None] * len(glyphsets)
|
|
206
|
+
|
|
207
|
+
for m1idx in order:
|
|
208
|
+
glyph1 = allGlyphs[m1idx]
|
|
209
|
+
if glyph1 is None or not glyph1.nodeTypes:
|
|
210
|
+
continue
|
|
211
|
+
m0idx = grand_parent(m1idx, glyph_name)
|
|
212
|
+
if m0idx is None:
|
|
213
|
+
continue
|
|
214
|
+
glyph0 = allGlyphs[m0idx]
|
|
215
|
+
if glyph0 is None or not glyph0.nodeTypes:
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
#
|
|
219
|
+
# Basic compatibility checks
|
|
220
|
+
#
|
|
221
|
+
|
|
222
|
+
m1 = glyph0.nodeTypes
|
|
223
|
+
m0 = glyph1.nodeTypes
|
|
224
|
+
if len(m0) != len(m1):
|
|
225
|
+
yield (
|
|
226
|
+
glyph_name,
|
|
227
|
+
{
|
|
228
|
+
"type": InterpolatableProblem.PATH_COUNT,
|
|
229
|
+
"master_1": names[m0idx],
|
|
230
|
+
"master_2": names[m1idx],
|
|
231
|
+
"master_1_idx": m0idx,
|
|
232
|
+
"master_2_idx": m1idx,
|
|
233
|
+
"value_1": len(m0),
|
|
234
|
+
"value_2": len(m1),
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
if m0 != m1:
|
|
240
|
+
for pathIx, (nodes1, nodes2) in enumerate(zip(m0, m1)):
|
|
241
|
+
if nodes1 == nodes2:
|
|
242
|
+
continue
|
|
243
|
+
if len(nodes1) != len(nodes2):
|
|
244
|
+
yield (
|
|
245
|
+
glyph_name,
|
|
246
|
+
{
|
|
247
|
+
"type": InterpolatableProblem.NODE_COUNT,
|
|
248
|
+
"path": pathIx,
|
|
249
|
+
"master_1": names[m0idx],
|
|
250
|
+
"master_2": names[m1idx],
|
|
251
|
+
"master_1_idx": m0idx,
|
|
252
|
+
"master_2_idx": m1idx,
|
|
253
|
+
"value_1": len(nodes1),
|
|
254
|
+
"value_2": len(nodes2),
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
continue
|
|
258
|
+
for nodeIx, (n1, n2) in enumerate(zip(nodes1, nodes2)):
|
|
259
|
+
if n1 != n2:
|
|
260
|
+
yield (
|
|
261
|
+
glyph_name,
|
|
262
|
+
{
|
|
263
|
+
"type": InterpolatableProblem.NODE_INCOMPATIBILITY,
|
|
264
|
+
"path": pathIx,
|
|
265
|
+
"node": nodeIx,
|
|
266
|
+
"master_1": names[m0idx],
|
|
267
|
+
"master_2": names[m1idx],
|
|
268
|
+
"master_1_idx": m0idx,
|
|
269
|
+
"master_2_idx": m1idx,
|
|
270
|
+
"value_1": n1,
|
|
271
|
+
"value_2": n2,
|
|
272
|
+
},
|
|
273
|
+
)
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
#
|
|
277
|
+
# InterpolatableProblem.CONTOUR_ORDER check
|
|
278
|
+
#
|
|
279
|
+
|
|
280
|
+
this_tolerance, matching = test_contour_order(glyph0, glyph1)
|
|
281
|
+
if this_tolerance < tolerance:
|
|
282
|
+
yield (
|
|
283
|
+
glyph_name,
|
|
284
|
+
{
|
|
285
|
+
"type": InterpolatableProblem.CONTOUR_ORDER,
|
|
286
|
+
"master_1": names[m0idx],
|
|
287
|
+
"master_2": names[m1idx],
|
|
288
|
+
"master_1_idx": m0idx,
|
|
289
|
+
"master_2_idx": m1idx,
|
|
290
|
+
"value_1": list(range(len(matching))),
|
|
291
|
+
"value_2": matching,
|
|
292
|
+
"tolerance": this_tolerance,
|
|
293
|
+
},
|
|
294
|
+
)
|
|
295
|
+
matchings[m1idx] = matching
|
|
296
|
+
|
|
297
|
+
#
|
|
298
|
+
# wrong-start-point / weight check
|
|
299
|
+
#
|
|
300
|
+
|
|
301
|
+
m0Isomorphisms = glyph0.isomorphisms
|
|
302
|
+
m1Isomorphisms = glyph1.isomorphisms
|
|
303
|
+
m0Vectors = glyph0.greenVectors
|
|
304
|
+
m1Vectors = glyph1.greenVectors
|
|
305
|
+
recording0 = glyph0.recordings
|
|
306
|
+
recording1 = glyph1.recordings
|
|
307
|
+
|
|
308
|
+
# If contour-order is wrong, adjust it
|
|
309
|
+
matching = matchings[m1idx]
|
|
310
|
+
if (
|
|
311
|
+
matching is not None and m1Isomorphisms
|
|
312
|
+
): # m1 is empty for composite glyphs
|
|
313
|
+
m1Isomorphisms = [m1Isomorphisms[i] for i in matching]
|
|
314
|
+
m1Vectors = [m1Vectors[i] for i in matching]
|
|
315
|
+
recording1 = [recording1[i] for i in matching]
|
|
316
|
+
|
|
317
|
+
midRecording = []
|
|
318
|
+
for c0, c1 in zip(recording0, recording1):
|
|
319
|
+
try:
|
|
320
|
+
r = RecordingPen()
|
|
321
|
+
r.value = list(lerpRecordings(c0.value, c1.value))
|
|
322
|
+
midRecording.append(r)
|
|
323
|
+
except ValueError:
|
|
324
|
+
# Mismatch because of the reordering above
|
|
325
|
+
midRecording.append(None)
|
|
326
|
+
|
|
327
|
+
for ix, (contour0, contour1) in enumerate(
|
|
328
|
+
zip(m0Isomorphisms, m1Isomorphisms)
|
|
329
|
+
):
|
|
330
|
+
if (
|
|
331
|
+
contour0 is None
|
|
332
|
+
or contour1 is None
|
|
333
|
+
or len(contour0) == 0
|
|
334
|
+
or len(contour0) != len(contour1)
|
|
335
|
+
):
|
|
336
|
+
# We already reported this; or nothing to do; or not compatible
|
|
337
|
+
# after reordering above.
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
this_tolerance, proposed_point, reverse = test_starting_point(
|
|
341
|
+
glyph0, glyph1, ix, tolerance, matching
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
if this_tolerance < tolerance:
|
|
345
|
+
yield (
|
|
346
|
+
glyph_name,
|
|
347
|
+
{
|
|
348
|
+
"type": InterpolatableProblem.WRONG_START_POINT,
|
|
349
|
+
"contour": ix,
|
|
350
|
+
"master_1": names[m0idx],
|
|
351
|
+
"master_2": names[m1idx],
|
|
352
|
+
"master_1_idx": m0idx,
|
|
353
|
+
"master_2_idx": m1idx,
|
|
354
|
+
"value_1": 0,
|
|
355
|
+
"value_2": proposed_point,
|
|
356
|
+
"reversed": reverse,
|
|
357
|
+
"tolerance": this_tolerance,
|
|
358
|
+
},
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Weight check.
|
|
362
|
+
#
|
|
363
|
+
# If contour could be mid-interpolated, and the two
|
|
364
|
+
# contours have the same area sign, proceeed.
|
|
365
|
+
#
|
|
366
|
+
# The sign difference can happen if it's a weirdo
|
|
367
|
+
# self-intersecting contour; ignore it.
|
|
368
|
+
contour = midRecording[ix]
|
|
369
|
+
|
|
370
|
+
if contour and (m0Vectors[ix][0] < 0) == (m1Vectors[ix][0] < 0):
|
|
371
|
+
midStats = StatisticsPen(glyphset=None)
|
|
372
|
+
contour.replay(midStats)
|
|
373
|
+
|
|
374
|
+
midVector = contour_vector_from_stats(midStats)
|
|
375
|
+
|
|
376
|
+
m0Vec = m0Vectors[ix]
|
|
377
|
+
m1Vec = m1Vectors[ix]
|
|
378
|
+
size0 = m0Vec[0] * m0Vec[0]
|
|
379
|
+
size1 = m1Vec[0] * m1Vec[0]
|
|
380
|
+
midSize = midVector[0] * midVector[0]
|
|
381
|
+
|
|
382
|
+
for overweight, problem_type in enumerate(
|
|
383
|
+
(
|
|
384
|
+
InterpolatableProblem.UNDERWEIGHT,
|
|
385
|
+
InterpolatableProblem.OVERWEIGHT,
|
|
386
|
+
)
|
|
387
|
+
):
|
|
388
|
+
if overweight:
|
|
389
|
+
expectedSize = max(size0, size1)
|
|
390
|
+
continue
|
|
391
|
+
else:
|
|
392
|
+
expectedSize = sqrt(size0 * size1)
|
|
393
|
+
|
|
394
|
+
log.debug(
|
|
395
|
+
"%s: actual size %g; threshold size %g, master sizes: %g, %g",
|
|
396
|
+
problem_type,
|
|
397
|
+
midSize,
|
|
398
|
+
expectedSize,
|
|
399
|
+
size0,
|
|
400
|
+
size1,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
if (
|
|
404
|
+
not overweight and expectedSize * tolerance > midSize + 1e-5
|
|
405
|
+
) or (overweight and 1e-5 + expectedSize / tolerance < midSize):
|
|
406
|
+
try:
|
|
407
|
+
if overweight:
|
|
408
|
+
this_tolerance = expectedSize / midSize
|
|
409
|
+
else:
|
|
410
|
+
this_tolerance = midSize / expectedSize
|
|
411
|
+
except ZeroDivisionError:
|
|
412
|
+
this_tolerance = 0
|
|
413
|
+
log.debug("tolerance %g", this_tolerance)
|
|
414
|
+
yield (
|
|
415
|
+
glyph_name,
|
|
416
|
+
{
|
|
417
|
+
"type": problem_type,
|
|
418
|
+
"contour": ix,
|
|
419
|
+
"master_1": names[m0idx],
|
|
420
|
+
"master_2": names[m1idx],
|
|
421
|
+
"master_1_idx": m0idx,
|
|
422
|
+
"master_2_idx": m1idx,
|
|
423
|
+
"tolerance": this_tolerance,
|
|
424
|
+
},
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
#
|
|
428
|
+
# "kink" detector
|
|
429
|
+
#
|
|
430
|
+
m0 = glyph0.points
|
|
431
|
+
m1 = glyph1.points
|
|
432
|
+
|
|
433
|
+
# If contour-order is wrong, adjust it
|
|
434
|
+
if matchings[m1idx] is not None and m1: # m1 is empty for composite glyphs
|
|
435
|
+
m1 = [m1[i] for i in matchings[m1idx]]
|
|
436
|
+
|
|
437
|
+
t = 0.1 # ~sin(radian(6)) for tolerance 0.95
|
|
438
|
+
deviation_threshold = (
|
|
439
|
+
upem * DEFAULT_KINKINESS_LENGTH * DEFAULT_KINKINESS / kinkiness
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
for ix, (contour0, contour1) in enumerate(zip(m0, m1)):
|
|
443
|
+
if (
|
|
444
|
+
contour0 is None
|
|
445
|
+
or contour1 is None
|
|
446
|
+
or len(contour0) == 0
|
|
447
|
+
or len(contour0) != len(contour1)
|
|
448
|
+
):
|
|
449
|
+
# We already reported this; or nothing to do; or not compatible
|
|
450
|
+
# after reordering above.
|
|
451
|
+
continue
|
|
452
|
+
|
|
453
|
+
# Walk the contour, keeping track of three consecutive points, with
|
|
454
|
+
# middle one being an on-curve. If the three are co-linear then
|
|
455
|
+
# check for kinky-ness.
|
|
456
|
+
for i in range(len(contour0)):
|
|
457
|
+
pt0 = contour0[i]
|
|
458
|
+
pt1 = contour1[i]
|
|
459
|
+
if not pt0[1] or not pt1[1]:
|
|
460
|
+
# Skip off-curves
|
|
461
|
+
continue
|
|
462
|
+
pt0_prev = contour0[i - 1]
|
|
463
|
+
pt1_prev = contour1[i - 1]
|
|
464
|
+
pt0_next = contour0[(i + 1) % len(contour0)]
|
|
465
|
+
pt1_next = contour1[(i + 1) % len(contour1)]
|
|
466
|
+
|
|
467
|
+
if pt0_prev[1] and pt1_prev[1]:
|
|
468
|
+
# At least one off-curve is required
|
|
469
|
+
continue
|
|
470
|
+
if pt0_prev[1] and pt1_prev[1]:
|
|
471
|
+
# At least one off-curve is required
|
|
472
|
+
continue
|
|
473
|
+
|
|
474
|
+
pt0 = complex(*pt0[0])
|
|
475
|
+
pt1 = complex(*pt1[0])
|
|
476
|
+
pt0_prev = complex(*pt0_prev[0])
|
|
477
|
+
pt1_prev = complex(*pt1_prev[0])
|
|
478
|
+
pt0_next = complex(*pt0_next[0])
|
|
479
|
+
pt1_next = complex(*pt1_next[0])
|
|
480
|
+
|
|
481
|
+
# We have three consecutive points. Check whether
|
|
482
|
+
# they are colinear.
|
|
483
|
+
d0_prev = pt0 - pt0_prev
|
|
484
|
+
d0_next = pt0_next - pt0
|
|
485
|
+
d1_prev = pt1 - pt1_prev
|
|
486
|
+
d1_next = pt1_next - pt1
|
|
487
|
+
|
|
488
|
+
sin0 = d0_prev.real * d0_next.imag - d0_prev.imag * d0_next.real
|
|
489
|
+
sin1 = d1_prev.real * d1_next.imag - d1_prev.imag * d1_next.real
|
|
490
|
+
try:
|
|
491
|
+
sin0 /= abs(d0_prev) * abs(d0_next)
|
|
492
|
+
sin1 /= abs(d1_prev) * abs(d1_next)
|
|
493
|
+
except ZeroDivisionError:
|
|
494
|
+
continue
|
|
495
|
+
|
|
496
|
+
if abs(sin0) > t or abs(sin1) > t:
|
|
497
|
+
# Not colinear / not smooth.
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
# Check the mid-point is actually, well, in the middle.
|
|
501
|
+
dot0 = d0_prev.real * d0_next.real + d0_prev.imag * d0_next.imag
|
|
502
|
+
dot1 = d1_prev.real * d1_next.real + d1_prev.imag * d1_next.imag
|
|
503
|
+
if dot0 < 0 or dot1 < 0:
|
|
504
|
+
# Sharp corner.
|
|
505
|
+
continue
|
|
506
|
+
|
|
507
|
+
# Fine, if handle ratios are similar...
|
|
508
|
+
r0 = abs(d0_prev) / (abs(d0_prev) + abs(d0_next))
|
|
509
|
+
r1 = abs(d1_prev) / (abs(d1_prev) + abs(d1_next))
|
|
510
|
+
r_diff = abs(r0 - r1)
|
|
511
|
+
if abs(r_diff) < t:
|
|
512
|
+
# Smooth enough.
|
|
513
|
+
continue
|
|
514
|
+
|
|
515
|
+
mid = (pt0 + pt1) / 2
|
|
516
|
+
mid_prev = (pt0_prev + pt1_prev) / 2
|
|
517
|
+
mid_next = (pt0_next + pt1_next) / 2
|
|
518
|
+
|
|
519
|
+
mid_d0 = mid - mid_prev
|
|
520
|
+
mid_d1 = mid_next - mid
|
|
521
|
+
|
|
522
|
+
sin_mid = mid_d0.real * mid_d1.imag - mid_d0.imag * mid_d1.real
|
|
523
|
+
try:
|
|
524
|
+
sin_mid /= abs(mid_d0) * abs(mid_d1)
|
|
525
|
+
except ZeroDivisionError:
|
|
526
|
+
continue
|
|
527
|
+
|
|
528
|
+
# ...or if the angles are similar.
|
|
529
|
+
if abs(sin_mid) * (tolerance * kinkiness) <= t:
|
|
530
|
+
# Smooth enough.
|
|
531
|
+
continue
|
|
532
|
+
|
|
533
|
+
# How visible is the kink?
|
|
534
|
+
|
|
535
|
+
cross = sin_mid * abs(mid_d0) * abs(mid_d1)
|
|
536
|
+
arc_len = abs(mid_d0 + mid_d1)
|
|
537
|
+
deviation = abs(cross / arc_len)
|
|
538
|
+
if deviation < deviation_threshold:
|
|
539
|
+
continue
|
|
540
|
+
deviation_ratio = deviation / arc_len
|
|
541
|
+
if deviation_ratio > t:
|
|
542
|
+
continue
|
|
543
|
+
|
|
544
|
+
this_tolerance = t / (abs(sin_mid) * kinkiness)
|
|
545
|
+
|
|
546
|
+
log.debug(
|
|
547
|
+
"kink: deviation %g; deviation_ratio %g; sin_mid %g; r_diff %g",
|
|
548
|
+
deviation,
|
|
549
|
+
deviation_ratio,
|
|
550
|
+
sin_mid,
|
|
551
|
+
r_diff,
|
|
552
|
+
)
|
|
553
|
+
log.debug("tolerance %g", this_tolerance)
|
|
554
|
+
yield (
|
|
555
|
+
glyph_name,
|
|
556
|
+
{
|
|
557
|
+
"type": InterpolatableProblem.KINK,
|
|
558
|
+
"contour": ix,
|
|
559
|
+
"master_1": names[m0idx],
|
|
560
|
+
"master_2": names[m1idx],
|
|
561
|
+
"master_1_idx": m0idx,
|
|
562
|
+
"master_2_idx": m1idx,
|
|
563
|
+
"value": i,
|
|
564
|
+
"tolerance": this_tolerance,
|
|
565
|
+
},
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
#
|
|
569
|
+
# --show-all
|
|
570
|
+
#
|
|
571
|
+
|
|
572
|
+
if show_all:
|
|
573
|
+
yield (
|
|
574
|
+
glyph_name,
|
|
575
|
+
{
|
|
576
|
+
"type": InterpolatableProblem.NOTHING,
|
|
577
|
+
"master_1": names[m0idx],
|
|
578
|
+
"master_2": names[m1idx],
|
|
579
|
+
"master_1_idx": m0idx,
|
|
580
|
+
"master_2_idx": m1idx,
|
|
581
|
+
},
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
@wraps(test_gen)
|
|
586
|
+
def test(*args, **kwargs):
|
|
587
|
+
problems = defaultdict(list)
|
|
588
|
+
for glyphname, problem in test_gen(*args, **kwargs):
|
|
589
|
+
problems[glyphname].append(problem)
|
|
590
|
+
return problems
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def recursivelyAddGlyph(glyphname, glyphset, ttGlyphSet, glyf):
|
|
594
|
+
if glyphname in glyphset:
|
|
595
|
+
return
|
|
596
|
+
glyphset[glyphname] = ttGlyphSet[glyphname]
|
|
597
|
+
|
|
598
|
+
for component in getattr(glyf[glyphname], "components", []):
|
|
599
|
+
recursivelyAddGlyph(component.glyphName, glyphset, ttGlyphSet, glyf)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def ensure_parent_dir(path):
|
|
603
|
+
dirname = os.path.dirname(path)
|
|
604
|
+
if dirname:
|
|
605
|
+
os.makedirs(dirname, exist_ok=True)
|
|
606
|
+
return path
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def main(args=None):
|
|
610
|
+
"""Test for interpolatability issues between fonts"""
|
|
611
|
+
import argparse
|
|
612
|
+
import sys
|
|
613
|
+
|
|
614
|
+
parser = argparse.ArgumentParser(
|
|
615
|
+
"fonttools varLib.interpolatable",
|
|
616
|
+
description=main.__doc__,
|
|
617
|
+
)
|
|
618
|
+
parser.add_argument(
|
|
619
|
+
"--glyphs",
|
|
620
|
+
action="store",
|
|
621
|
+
help="Space-separate name of glyphs to check",
|
|
622
|
+
)
|
|
623
|
+
parser.add_argument(
|
|
624
|
+
"--show-all",
|
|
625
|
+
action="store_true",
|
|
626
|
+
help="Show all glyph pairs, even if no problems are found",
|
|
627
|
+
)
|
|
628
|
+
parser.add_argument(
|
|
629
|
+
"--tolerance",
|
|
630
|
+
action="store",
|
|
631
|
+
type=float,
|
|
632
|
+
help="Error tolerance. Between 0 and 1. Default %s" % DEFAULT_TOLERANCE,
|
|
633
|
+
)
|
|
634
|
+
parser.add_argument(
|
|
635
|
+
"--kinkiness",
|
|
636
|
+
action="store",
|
|
637
|
+
type=float,
|
|
638
|
+
help="How aggressively report kinks. Default %s" % DEFAULT_KINKINESS,
|
|
639
|
+
)
|
|
640
|
+
parser.add_argument(
|
|
641
|
+
"--json",
|
|
642
|
+
action="store_true",
|
|
643
|
+
help="Output report in JSON format",
|
|
644
|
+
)
|
|
645
|
+
parser.add_argument(
|
|
646
|
+
"--pdf",
|
|
647
|
+
action="store",
|
|
648
|
+
help="Output report in PDF format",
|
|
649
|
+
)
|
|
650
|
+
parser.add_argument(
|
|
651
|
+
"--ps",
|
|
652
|
+
action="store",
|
|
653
|
+
help="Output report in PostScript format",
|
|
654
|
+
)
|
|
655
|
+
parser.add_argument(
|
|
656
|
+
"--html",
|
|
657
|
+
action="store",
|
|
658
|
+
help="Output report in HTML format",
|
|
659
|
+
)
|
|
660
|
+
parser.add_argument(
|
|
661
|
+
"--quiet",
|
|
662
|
+
action="store_true",
|
|
663
|
+
help="Only exit with code 1 or 0, no output",
|
|
664
|
+
)
|
|
665
|
+
parser.add_argument(
|
|
666
|
+
"--output",
|
|
667
|
+
action="store",
|
|
668
|
+
help="Output file for the problem report; Default: stdout",
|
|
669
|
+
)
|
|
670
|
+
parser.add_argument(
|
|
671
|
+
"--ignore-missing",
|
|
672
|
+
action="store_true",
|
|
673
|
+
help="Will not report glyphs missing from sparse masters as errors",
|
|
674
|
+
)
|
|
675
|
+
parser.add_argument(
|
|
676
|
+
"inputs",
|
|
677
|
+
metavar="FILE",
|
|
678
|
+
type=str,
|
|
679
|
+
nargs="+",
|
|
680
|
+
help="Input a single variable font / DesignSpace / Glyphs file, or multiple TTF/UFO files",
|
|
681
|
+
)
|
|
682
|
+
parser.add_argument(
|
|
683
|
+
"--name",
|
|
684
|
+
metavar="NAME",
|
|
685
|
+
type=str,
|
|
686
|
+
action="append",
|
|
687
|
+
help="Name of the master to use in the report. If not provided, all are used.",
|
|
688
|
+
)
|
|
689
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Run verbosely.")
|
|
690
|
+
parser.add_argument("--debug", action="store_true", help="Run with debug output.")
|
|
691
|
+
|
|
692
|
+
args = parser.parse_args(args)
|
|
693
|
+
|
|
694
|
+
from fontTools import configLogger
|
|
695
|
+
|
|
696
|
+
configLogger(level=("INFO" if args.verbose else "WARNING"))
|
|
697
|
+
if args.debug:
|
|
698
|
+
configLogger(level="DEBUG")
|
|
699
|
+
|
|
700
|
+
glyphs = args.glyphs.split() if args.glyphs else None
|
|
701
|
+
|
|
702
|
+
from os.path import basename
|
|
703
|
+
|
|
704
|
+
fonts = []
|
|
705
|
+
names = []
|
|
706
|
+
locations = []
|
|
707
|
+
discrete_axes = set()
|
|
708
|
+
upem = DEFAULT_UPEM
|
|
709
|
+
|
|
710
|
+
original_args_inputs = tuple(args.inputs)
|
|
711
|
+
|
|
712
|
+
if len(args.inputs) == 1:
|
|
713
|
+
designspace = None
|
|
714
|
+
if args.inputs[0].endswith(".designspace"):
|
|
715
|
+
from fontTools.designspaceLib import DesignSpaceDocument
|
|
716
|
+
|
|
717
|
+
designspace = DesignSpaceDocument.fromfile(args.inputs[0])
|
|
718
|
+
args.inputs = [master.path for master in designspace.sources]
|
|
719
|
+
locations = [master.location for master in designspace.sources]
|
|
720
|
+
discrete_axes = {
|
|
721
|
+
a.name for a in designspace.axes if not hasattr(a, "minimum")
|
|
722
|
+
}
|
|
723
|
+
axis_triples = {
|
|
724
|
+
a.name: (a.minimum, a.default, a.maximum)
|
|
725
|
+
for a in designspace.axes
|
|
726
|
+
if a.name not in discrete_axes
|
|
727
|
+
}
|
|
728
|
+
axis_mappings = {a.name: a.map for a in designspace.axes}
|
|
729
|
+
axis_triples = {
|
|
730
|
+
k: tuple(piecewiseLinearMap(v, dict(axis_mappings[k])) for v in vv)
|
|
731
|
+
for k, vv in axis_triples.items()
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
elif args.inputs[0].endswith((".glyphs", ".glyphspackage")):
|
|
735
|
+
from glyphsLib import GSFont, to_designspace
|
|
736
|
+
|
|
737
|
+
gsfont = GSFont(args.inputs[0])
|
|
738
|
+
upem = gsfont.upm
|
|
739
|
+
designspace = to_designspace(gsfont)
|
|
740
|
+
fonts = [source.font for source in designspace.sources]
|
|
741
|
+
names = ["%s-%s" % (f.info.familyName, f.info.styleName) for f in fonts]
|
|
742
|
+
args.inputs = []
|
|
743
|
+
locations = [master.location for master in designspace.sources]
|
|
744
|
+
axis_triples = {
|
|
745
|
+
a.name: (a.minimum, a.default, a.maximum) for a in designspace.axes
|
|
746
|
+
}
|
|
747
|
+
axis_mappings = {a.name: a.map for a in designspace.axes}
|
|
748
|
+
axis_triples = {
|
|
749
|
+
k: tuple(piecewiseLinearMap(v, dict(axis_mappings[k])) for v in vv)
|
|
750
|
+
for k, vv in axis_triples.items()
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
elif args.inputs[0].endswith(".ttf") or args.inputs[0].endswith(".otf"):
|
|
754
|
+
from fontTools.ttLib import TTFont
|
|
755
|
+
|
|
756
|
+
# Is variable font?
|
|
757
|
+
|
|
758
|
+
font = TTFont(args.inputs[0])
|
|
759
|
+
upem = font["head"].unitsPerEm
|
|
760
|
+
|
|
761
|
+
fvar = font["fvar"]
|
|
762
|
+
axisMapping = {}
|
|
763
|
+
for axis in fvar.axes:
|
|
764
|
+
axisMapping[axis.axisTag] = {
|
|
765
|
+
-1: axis.minValue,
|
|
766
|
+
0: axis.defaultValue,
|
|
767
|
+
1: axis.maxValue,
|
|
768
|
+
}
|
|
769
|
+
normalized = False
|
|
770
|
+
if "avar" in font:
|
|
771
|
+
avar = font["avar"]
|
|
772
|
+
if getattr(avar.table, "VarStore", None):
|
|
773
|
+
axisMapping = {tag: {-1: -1, 0: 0, 1: 1} for tag in axisMapping}
|
|
774
|
+
normalized = True
|
|
775
|
+
else:
|
|
776
|
+
for axisTag, segments in avar.segments.items():
|
|
777
|
+
fvarMapping = axisMapping[axisTag].copy()
|
|
778
|
+
for location, value in segments.items():
|
|
779
|
+
axisMapping[axisTag][value] = piecewiseLinearMap(
|
|
780
|
+
location, fvarMapping
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
# Gather all glyphs at their "master" locations
|
|
784
|
+
ttGlyphSets = {}
|
|
785
|
+
glyphsets = defaultdict(dict)
|
|
786
|
+
|
|
787
|
+
if "gvar" in font:
|
|
788
|
+
gvar = font["gvar"]
|
|
789
|
+
glyf = font["glyf"]
|
|
790
|
+
|
|
791
|
+
if glyphs is None:
|
|
792
|
+
glyphs = sorted(gvar.variations.keys())
|
|
793
|
+
for glyphname in glyphs:
|
|
794
|
+
for var in gvar.variations[glyphname]:
|
|
795
|
+
locDict = {}
|
|
796
|
+
loc = []
|
|
797
|
+
for tag, val in sorted(var.axes.items()):
|
|
798
|
+
locDict[tag] = val[1]
|
|
799
|
+
loc.append((tag, val[1]))
|
|
800
|
+
|
|
801
|
+
locTuple = tuple(loc)
|
|
802
|
+
if locTuple not in ttGlyphSets:
|
|
803
|
+
ttGlyphSets[locTuple] = font.getGlyphSet(
|
|
804
|
+
location=locDict, normalized=True, recalcBounds=False
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
recursivelyAddGlyph(
|
|
808
|
+
glyphname, glyphsets[locTuple], ttGlyphSets[locTuple], glyf
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
elif "CFF2" in font:
|
|
812
|
+
fvarAxes = font["fvar"].axes
|
|
813
|
+
cff2 = font["CFF2"].cff.topDictIndex[0]
|
|
814
|
+
charstrings = cff2.CharStrings
|
|
815
|
+
|
|
816
|
+
if glyphs is None:
|
|
817
|
+
glyphs = sorted(charstrings.keys())
|
|
818
|
+
for glyphname in glyphs:
|
|
819
|
+
cs = charstrings[glyphname]
|
|
820
|
+
private = cs.private
|
|
821
|
+
|
|
822
|
+
# Extract vsindex for the glyph
|
|
823
|
+
vsindices = {getattr(private, "vsindex", 0)}
|
|
824
|
+
vsindex = getattr(private, "vsindex", 0)
|
|
825
|
+
last_op = 0
|
|
826
|
+
# The spec says vsindex can only appear once and must be the first
|
|
827
|
+
# operator in the charstring, but we support multiple.
|
|
828
|
+
# https://github.com/harfbuzz/boring-expansion-spec/issues/158
|
|
829
|
+
for op in enumerate(cs.program):
|
|
830
|
+
if op == "blend":
|
|
831
|
+
vsindices.add(vsindex)
|
|
832
|
+
elif op == "vsindex":
|
|
833
|
+
assert isinstance(last_op, int)
|
|
834
|
+
vsindex = last_op
|
|
835
|
+
last_op = op
|
|
836
|
+
|
|
837
|
+
if not hasattr(private, "vstore"):
|
|
838
|
+
continue
|
|
839
|
+
|
|
840
|
+
varStore = private.vstore.otVarStore
|
|
841
|
+
for vsindex in vsindices:
|
|
842
|
+
varData = varStore.VarData[vsindex]
|
|
843
|
+
for regionIndex in varData.VarRegionIndex:
|
|
844
|
+
region = varStore.VarRegionList.Region[regionIndex]
|
|
845
|
+
|
|
846
|
+
locDict = {}
|
|
847
|
+
loc = []
|
|
848
|
+
for axisIndex, axis in enumerate(region.VarRegionAxis):
|
|
849
|
+
tag = fvarAxes[axisIndex].axisTag
|
|
850
|
+
val = axis.PeakCoord
|
|
851
|
+
locDict[tag] = val
|
|
852
|
+
loc.append((tag, val))
|
|
853
|
+
|
|
854
|
+
locTuple = tuple(loc)
|
|
855
|
+
if locTuple not in ttGlyphSets:
|
|
856
|
+
ttGlyphSets[locTuple] = font.getGlyphSet(
|
|
857
|
+
location=locDict,
|
|
858
|
+
normalized=True,
|
|
859
|
+
recalcBounds=False,
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
glyphset = glyphsets[locTuple]
|
|
863
|
+
glyphset[glyphname] = ttGlyphSets[locTuple][glyphname]
|
|
864
|
+
|
|
865
|
+
names = ["''"]
|
|
866
|
+
fonts = [font.getGlyphSet()]
|
|
867
|
+
locations = [{}]
|
|
868
|
+
axis_triples = {a: (-1, 0, +1) for a in sorted(axisMapping.keys())}
|
|
869
|
+
for locTuple in sorted(glyphsets.keys(), key=lambda v: (len(v), v)):
|
|
870
|
+
name = (
|
|
871
|
+
"'"
|
|
872
|
+
+ " ".join(
|
|
873
|
+
"%s=%s"
|
|
874
|
+
% (
|
|
875
|
+
k,
|
|
876
|
+
floatToFixedToStr(
|
|
877
|
+
piecewiseLinearMap(v, axisMapping[k]), 14
|
|
878
|
+
),
|
|
879
|
+
)
|
|
880
|
+
for k, v in locTuple
|
|
881
|
+
)
|
|
882
|
+
+ "'"
|
|
883
|
+
)
|
|
884
|
+
if normalized:
|
|
885
|
+
name += " (normalized)"
|
|
886
|
+
names.append(name)
|
|
887
|
+
fonts.append(glyphsets[locTuple])
|
|
888
|
+
locations.append(dict(locTuple))
|
|
889
|
+
|
|
890
|
+
args.ignore_missing = True
|
|
891
|
+
args.inputs = []
|
|
892
|
+
|
|
893
|
+
if not locations:
|
|
894
|
+
locations = [{} for _ in fonts]
|
|
895
|
+
|
|
896
|
+
for filename in args.inputs:
|
|
897
|
+
if filename.endswith(".ufo"):
|
|
898
|
+
from fontTools.ufoLib import UFOReader
|
|
899
|
+
|
|
900
|
+
font = UFOReader(filename)
|
|
901
|
+
info = SimpleNamespace()
|
|
902
|
+
font.readInfo(info)
|
|
903
|
+
upem = info.unitsPerEm
|
|
904
|
+
fonts.append(font)
|
|
905
|
+
else:
|
|
906
|
+
from fontTools.ttLib import TTFont
|
|
907
|
+
|
|
908
|
+
font = TTFont(filename)
|
|
909
|
+
upem = font["head"].unitsPerEm
|
|
910
|
+
fonts.append(font)
|
|
911
|
+
|
|
912
|
+
names.append(basename(filename).rsplit(".", 1)[0])
|
|
913
|
+
|
|
914
|
+
if len(fonts) < 2:
|
|
915
|
+
log.warning("Font file does not seem to be variable. Nothing to check.")
|
|
916
|
+
return
|
|
917
|
+
|
|
918
|
+
glyphsets = []
|
|
919
|
+
for font in fonts:
|
|
920
|
+
if hasattr(font, "getGlyphSet"):
|
|
921
|
+
glyphset = font.getGlyphSet()
|
|
922
|
+
else:
|
|
923
|
+
glyphset = font
|
|
924
|
+
glyphsets.append({k: glyphset[k] for k in glyphset.keys()})
|
|
925
|
+
|
|
926
|
+
if args.name:
|
|
927
|
+
accepted_names = set(args.name)
|
|
928
|
+
glyphsets = [
|
|
929
|
+
glyphset
|
|
930
|
+
for name, glyphset in zip(names, glyphsets)
|
|
931
|
+
if name in accepted_names
|
|
932
|
+
]
|
|
933
|
+
locations = [
|
|
934
|
+
location
|
|
935
|
+
for name, location in zip(names, locations)
|
|
936
|
+
if name in accepted_names
|
|
937
|
+
]
|
|
938
|
+
names = [name for name in names if name in accepted_names]
|
|
939
|
+
|
|
940
|
+
if not glyphs:
|
|
941
|
+
glyphs = sorted(set([gn for glyphset in glyphsets for gn in glyphset.keys()]))
|
|
942
|
+
|
|
943
|
+
glyphsSet = set(glyphs)
|
|
944
|
+
for glyphset in glyphsets:
|
|
945
|
+
glyphSetGlyphNames = set(glyphset.keys())
|
|
946
|
+
diff = glyphsSet - glyphSetGlyphNames
|
|
947
|
+
if diff:
|
|
948
|
+
for gn in diff:
|
|
949
|
+
glyphset[gn] = None
|
|
950
|
+
|
|
951
|
+
# Normalize locations
|
|
952
|
+
locations = [
|
|
953
|
+
{
|
|
954
|
+
**normalizeLocation(loc, axis_triples),
|
|
955
|
+
**{k: v for k, v in loc.items() if k in discrete_axes},
|
|
956
|
+
}
|
|
957
|
+
for loc in locations
|
|
958
|
+
]
|
|
959
|
+
tolerance = args.tolerance or DEFAULT_TOLERANCE
|
|
960
|
+
kinkiness = args.kinkiness if args.kinkiness is not None else DEFAULT_KINKINESS
|
|
961
|
+
|
|
962
|
+
try:
|
|
963
|
+
log.info("Running on %d glyphsets", len(glyphsets))
|
|
964
|
+
log.info("Locations: %s", pformat(locations))
|
|
965
|
+
problems_gen = test_gen(
|
|
966
|
+
glyphsets,
|
|
967
|
+
glyphs=glyphs,
|
|
968
|
+
names=names,
|
|
969
|
+
locations=locations,
|
|
970
|
+
upem=upem,
|
|
971
|
+
ignore_missing=args.ignore_missing,
|
|
972
|
+
tolerance=tolerance,
|
|
973
|
+
kinkiness=kinkiness,
|
|
974
|
+
show_all=args.show_all,
|
|
975
|
+
discrete_axes=discrete_axes,
|
|
976
|
+
)
|
|
977
|
+
problems = defaultdict(list)
|
|
978
|
+
|
|
979
|
+
f = (
|
|
980
|
+
sys.stdout
|
|
981
|
+
if args.output is None
|
|
982
|
+
else open(ensure_parent_dir(args.output), "w")
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
if not args.quiet:
|
|
986
|
+
if args.json:
|
|
987
|
+
import json
|
|
988
|
+
|
|
989
|
+
for glyphname, problem in problems_gen:
|
|
990
|
+
problems[glyphname].append(problem)
|
|
991
|
+
|
|
992
|
+
print(json.dumps(problems), file=f)
|
|
993
|
+
else:
|
|
994
|
+
last_glyphname = None
|
|
995
|
+
for glyphname, p in problems_gen:
|
|
996
|
+
problems[glyphname].append(p)
|
|
997
|
+
|
|
998
|
+
if glyphname != last_glyphname:
|
|
999
|
+
print(f"Glyph {glyphname} was not compatible:", file=f)
|
|
1000
|
+
last_glyphname = glyphname
|
|
1001
|
+
last_master_idxs = None
|
|
1002
|
+
|
|
1003
|
+
master_idxs = (
|
|
1004
|
+
(p["master_idx"],)
|
|
1005
|
+
if "master_idx" in p
|
|
1006
|
+
else (p["master_1_idx"], p["master_2_idx"])
|
|
1007
|
+
)
|
|
1008
|
+
if master_idxs != last_master_idxs:
|
|
1009
|
+
master_names = (
|
|
1010
|
+
(p["master"],)
|
|
1011
|
+
if "master" in p
|
|
1012
|
+
else (p["master_1"], p["master_2"])
|
|
1013
|
+
)
|
|
1014
|
+
print(f" Masters: %s:" % ", ".join(master_names), file=f)
|
|
1015
|
+
last_master_idxs = master_idxs
|
|
1016
|
+
|
|
1017
|
+
if p["type"] == InterpolatableProblem.MISSING:
|
|
1018
|
+
print(
|
|
1019
|
+
" Glyph was missing in master %s" % p["master"], file=f
|
|
1020
|
+
)
|
|
1021
|
+
elif p["type"] == InterpolatableProblem.OPEN_PATH:
|
|
1022
|
+
print(
|
|
1023
|
+
" Glyph has an open path in master %s" % p["master"],
|
|
1024
|
+
file=f,
|
|
1025
|
+
)
|
|
1026
|
+
elif p["type"] == InterpolatableProblem.PATH_COUNT:
|
|
1027
|
+
print(
|
|
1028
|
+
" Path count differs: %i in %s, %i in %s"
|
|
1029
|
+
% (
|
|
1030
|
+
p["value_1"],
|
|
1031
|
+
p["master_1"],
|
|
1032
|
+
p["value_2"],
|
|
1033
|
+
p["master_2"],
|
|
1034
|
+
),
|
|
1035
|
+
file=f,
|
|
1036
|
+
)
|
|
1037
|
+
elif p["type"] == InterpolatableProblem.NODE_COUNT:
|
|
1038
|
+
print(
|
|
1039
|
+
" Node count differs in path %i: %i in %s, %i in %s"
|
|
1040
|
+
% (
|
|
1041
|
+
p["path"],
|
|
1042
|
+
p["value_1"],
|
|
1043
|
+
p["master_1"],
|
|
1044
|
+
p["value_2"],
|
|
1045
|
+
p["master_2"],
|
|
1046
|
+
),
|
|
1047
|
+
file=f,
|
|
1048
|
+
)
|
|
1049
|
+
elif p["type"] == InterpolatableProblem.NODE_INCOMPATIBILITY:
|
|
1050
|
+
print(
|
|
1051
|
+
" Node %o incompatible in path %i: %s in %s, %s in %s"
|
|
1052
|
+
% (
|
|
1053
|
+
p["node"],
|
|
1054
|
+
p["path"],
|
|
1055
|
+
p["value_1"],
|
|
1056
|
+
p["master_1"],
|
|
1057
|
+
p["value_2"],
|
|
1058
|
+
p["master_2"],
|
|
1059
|
+
),
|
|
1060
|
+
file=f,
|
|
1061
|
+
)
|
|
1062
|
+
elif p["type"] == InterpolatableProblem.CONTOUR_ORDER:
|
|
1063
|
+
print(
|
|
1064
|
+
" Contour order differs: %s in %s, %s in %s"
|
|
1065
|
+
% (
|
|
1066
|
+
p["value_1"],
|
|
1067
|
+
p["master_1"],
|
|
1068
|
+
p["value_2"],
|
|
1069
|
+
p["master_2"],
|
|
1070
|
+
),
|
|
1071
|
+
file=f,
|
|
1072
|
+
)
|
|
1073
|
+
elif p["type"] == InterpolatableProblem.WRONG_START_POINT:
|
|
1074
|
+
print(
|
|
1075
|
+
" Contour %d start point differs: %s in %s, %s in %s; reversed: %s"
|
|
1076
|
+
% (
|
|
1077
|
+
p["contour"],
|
|
1078
|
+
p["value_1"],
|
|
1079
|
+
p["master_1"],
|
|
1080
|
+
p["value_2"],
|
|
1081
|
+
p["master_2"],
|
|
1082
|
+
p["reversed"],
|
|
1083
|
+
),
|
|
1084
|
+
file=f,
|
|
1085
|
+
)
|
|
1086
|
+
elif p["type"] == InterpolatableProblem.UNDERWEIGHT:
|
|
1087
|
+
print(
|
|
1088
|
+
" Contour %d interpolation is underweight: %s, %s"
|
|
1089
|
+
% (
|
|
1090
|
+
p["contour"],
|
|
1091
|
+
p["master_1"],
|
|
1092
|
+
p["master_2"],
|
|
1093
|
+
),
|
|
1094
|
+
file=f,
|
|
1095
|
+
)
|
|
1096
|
+
elif p["type"] == InterpolatableProblem.OVERWEIGHT:
|
|
1097
|
+
print(
|
|
1098
|
+
" Contour %d interpolation is overweight: %s, %s"
|
|
1099
|
+
% (
|
|
1100
|
+
p["contour"],
|
|
1101
|
+
p["master_1"],
|
|
1102
|
+
p["master_2"],
|
|
1103
|
+
),
|
|
1104
|
+
file=f,
|
|
1105
|
+
)
|
|
1106
|
+
elif p["type"] == InterpolatableProblem.KINK:
|
|
1107
|
+
print(
|
|
1108
|
+
" Contour %d has a kink at %s: %s, %s"
|
|
1109
|
+
% (
|
|
1110
|
+
p["contour"],
|
|
1111
|
+
p["value"],
|
|
1112
|
+
p["master_1"],
|
|
1113
|
+
p["master_2"],
|
|
1114
|
+
),
|
|
1115
|
+
file=f,
|
|
1116
|
+
)
|
|
1117
|
+
elif p["type"] == InterpolatableProblem.NOTHING:
|
|
1118
|
+
print(
|
|
1119
|
+
" Showing %s and %s"
|
|
1120
|
+
% (
|
|
1121
|
+
p["master_1"],
|
|
1122
|
+
p["master_2"],
|
|
1123
|
+
),
|
|
1124
|
+
file=f,
|
|
1125
|
+
)
|
|
1126
|
+
else:
|
|
1127
|
+
for glyphname, problem in problems_gen:
|
|
1128
|
+
problems[glyphname].append(problem)
|
|
1129
|
+
|
|
1130
|
+
problems = sort_problems(problems)
|
|
1131
|
+
|
|
1132
|
+
for p in "ps", "pdf":
|
|
1133
|
+
arg = getattr(args, p)
|
|
1134
|
+
if arg is None:
|
|
1135
|
+
continue
|
|
1136
|
+
log.info("Writing %s to %s", p.upper(), arg)
|
|
1137
|
+
from .interpolatablePlot import InterpolatablePS, InterpolatablePDF
|
|
1138
|
+
|
|
1139
|
+
PlotterClass = InterpolatablePS if p == "ps" else InterpolatablePDF
|
|
1140
|
+
|
|
1141
|
+
with PlotterClass(
|
|
1142
|
+
ensure_parent_dir(arg), glyphsets=glyphsets, names=names
|
|
1143
|
+
) as doc:
|
|
1144
|
+
doc.add_title_page(
|
|
1145
|
+
original_args_inputs, tolerance=tolerance, kinkiness=kinkiness
|
|
1146
|
+
)
|
|
1147
|
+
if problems:
|
|
1148
|
+
doc.add_summary(problems)
|
|
1149
|
+
doc.add_problems(problems)
|
|
1150
|
+
if not problems and not args.quiet:
|
|
1151
|
+
doc.draw_cupcake()
|
|
1152
|
+
if problems:
|
|
1153
|
+
doc.add_index()
|
|
1154
|
+
doc.add_table_of_contents()
|
|
1155
|
+
|
|
1156
|
+
if args.html:
|
|
1157
|
+
log.info("Writing HTML to %s", args.html)
|
|
1158
|
+
from .interpolatablePlot import InterpolatableSVG
|
|
1159
|
+
|
|
1160
|
+
svgs = []
|
|
1161
|
+
glyph_starts = {}
|
|
1162
|
+
with InterpolatableSVG(svgs, glyphsets=glyphsets, names=names) as svg:
|
|
1163
|
+
svg.add_title_page(
|
|
1164
|
+
original_args_inputs,
|
|
1165
|
+
show_tolerance=False,
|
|
1166
|
+
tolerance=tolerance,
|
|
1167
|
+
kinkiness=kinkiness,
|
|
1168
|
+
)
|
|
1169
|
+
for glyph, glyph_problems in problems.items():
|
|
1170
|
+
glyph_starts[len(svgs)] = glyph
|
|
1171
|
+
svg.add_problems(
|
|
1172
|
+
{glyph: glyph_problems},
|
|
1173
|
+
show_tolerance=False,
|
|
1174
|
+
show_page_number=False,
|
|
1175
|
+
)
|
|
1176
|
+
if not problems and not args.quiet:
|
|
1177
|
+
svg.draw_cupcake()
|
|
1178
|
+
|
|
1179
|
+
import base64
|
|
1180
|
+
|
|
1181
|
+
with open(ensure_parent_dir(args.html), "wb") as f:
|
|
1182
|
+
f.write(b"<!DOCTYPE html>\n")
|
|
1183
|
+
f.write(
|
|
1184
|
+
b'<html><body align="center" style="font-family: sans-serif; text-color: #222">\n'
|
|
1185
|
+
)
|
|
1186
|
+
f.write(b"<title>fonttools varLib.interpolatable report</title>\n")
|
|
1187
|
+
for i, svg in enumerate(svgs):
|
|
1188
|
+
if i in glyph_starts:
|
|
1189
|
+
f.write(f"<h1>Glyph {glyph_starts[i]}</h1>\n".encode("utf-8"))
|
|
1190
|
+
f.write("<img src='data:image/svg+xml;base64,".encode("utf-8"))
|
|
1191
|
+
f.write(base64.b64encode(svg))
|
|
1192
|
+
f.write(b"' />\n")
|
|
1193
|
+
f.write(b"<hr>\n")
|
|
1194
|
+
f.write(b"</body></html>\n")
|
|
1195
|
+
|
|
1196
|
+
except Exception as e:
|
|
1197
|
+
e.args += original_args_inputs
|
|
1198
|
+
log.error(e)
|
|
1199
|
+
raise
|
|
1200
|
+
|
|
1201
|
+
if problems:
|
|
1202
|
+
return problems
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
if __name__ == "__main__":
|
|
1206
|
+
import sys
|
|
1207
|
+
|
|
1208
|
+
problems = main()
|
|
1209
|
+
sys.exit(int(bool(problems)))
|