xref: /aosp_15_r20/external/fonttools/Lib/fontTools/varLib/interpolatable.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""
2*e1fe3e4aSElliott HughesTool to find wrong contour order between different masters, and
3*e1fe3e4aSElliott Hughesother interpolatability (or lack thereof) issues.
4*e1fe3e4aSElliott Hughes
5*e1fe3e4aSElliott HughesCall as:
6*e1fe3e4aSElliott Hughes$ fonttools varLib.interpolatable font1 font2 ...
7*e1fe3e4aSElliott Hughes"""
8*e1fe3e4aSElliott Hughes
9*e1fe3e4aSElliott Hughesfrom .interpolatableHelpers import *
10*e1fe3e4aSElliott Hughesfrom .interpolatableTestContourOrder import test_contour_order
11*e1fe3e4aSElliott Hughesfrom .interpolatableTestStartingPoint import test_starting_point
12*e1fe3e4aSElliott Hughesfrom fontTools.pens.recordingPen import (
13*e1fe3e4aSElliott Hughes    RecordingPen,
14*e1fe3e4aSElliott Hughes    DecomposingRecordingPen,
15*e1fe3e4aSElliott Hughes    lerpRecordings,
16*e1fe3e4aSElliott Hughes)
17*e1fe3e4aSElliott Hughesfrom fontTools.pens.transformPen import TransformPen
18*e1fe3e4aSElliott Hughesfrom fontTools.pens.statisticsPen import StatisticsPen, StatisticsControlPen
19*e1fe3e4aSElliott Hughesfrom fontTools.pens.momentsPen import OpenContourError
20*e1fe3e4aSElliott Hughesfrom fontTools.varLib.models import piecewiseLinearMap, normalizeLocation
21*e1fe3e4aSElliott Hughesfrom fontTools.misc.fixedTools import floatToFixedToStr
22*e1fe3e4aSElliott Hughesfrom fontTools.misc.transform import Transform
23*e1fe3e4aSElliott Hughesfrom collections import defaultdict
24*e1fe3e4aSElliott Hughesfrom types import SimpleNamespace
25*e1fe3e4aSElliott Hughesfrom functools import wraps
26*e1fe3e4aSElliott Hughesfrom pprint import pformat
27*e1fe3e4aSElliott Hughesfrom math import sqrt, atan2, pi
28*e1fe3e4aSElliott Hughesimport logging
29*e1fe3e4aSElliott Hughesimport os
30*e1fe3e4aSElliott Hughes
31*e1fe3e4aSElliott Hugheslog = logging.getLogger("fontTools.varLib.interpolatable")
32*e1fe3e4aSElliott Hughes
33*e1fe3e4aSElliott HughesDEFAULT_TOLERANCE = 0.95
34*e1fe3e4aSElliott HughesDEFAULT_KINKINESS = 0.5
35*e1fe3e4aSElliott HughesDEFAULT_KINKINESS_LENGTH = 0.002  # ratio of UPEM
36*e1fe3e4aSElliott HughesDEFAULT_UPEM = 1000
37*e1fe3e4aSElliott Hughes
38*e1fe3e4aSElliott Hughes
39*e1fe3e4aSElliott Hughesclass Glyph:
40*e1fe3e4aSElliott Hughes    ITEMS = (
41*e1fe3e4aSElliott Hughes        "recordings",
42*e1fe3e4aSElliott Hughes        "greenStats",
43*e1fe3e4aSElliott Hughes        "controlStats",
44*e1fe3e4aSElliott Hughes        "greenVectors",
45*e1fe3e4aSElliott Hughes        "controlVectors",
46*e1fe3e4aSElliott Hughes        "nodeTypes",
47*e1fe3e4aSElliott Hughes        "isomorphisms",
48*e1fe3e4aSElliott Hughes        "points",
49*e1fe3e4aSElliott Hughes        "openContours",
50*e1fe3e4aSElliott Hughes    )
51*e1fe3e4aSElliott Hughes
52*e1fe3e4aSElliott Hughes    def __init__(self, glyphname, glyphset):
53*e1fe3e4aSElliott Hughes        self.name = glyphname
54*e1fe3e4aSElliott Hughes        for item in self.ITEMS:
55*e1fe3e4aSElliott Hughes            setattr(self, item, [])
56*e1fe3e4aSElliott Hughes        self._populate(glyphset)
57*e1fe3e4aSElliott Hughes
58*e1fe3e4aSElliott Hughes    def _fill_in(self, ix):
59*e1fe3e4aSElliott Hughes        for item in self.ITEMS:
60*e1fe3e4aSElliott Hughes            if len(getattr(self, item)) == ix:
61*e1fe3e4aSElliott Hughes                getattr(self, item).append(None)
62*e1fe3e4aSElliott Hughes
63*e1fe3e4aSElliott Hughes    def _populate(self, glyphset):
64*e1fe3e4aSElliott Hughes        glyph = glyphset[self.name]
65*e1fe3e4aSElliott Hughes        self.doesnt_exist = glyph is None
66*e1fe3e4aSElliott Hughes        if self.doesnt_exist:
67*e1fe3e4aSElliott Hughes            return
68*e1fe3e4aSElliott Hughes
69*e1fe3e4aSElliott Hughes        perContourPen = PerContourOrComponentPen(RecordingPen, glyphset=glyphset)
70*e1fe3e4aSElliott Hughes        try:
71*e1fe3e4aSElliott Hughes            glyph.draw(perContourPen, outputImpliedClosingLine=True)
72*e1fe3e4aSElliott Hughes        except TypeError:
73*e1fe3e4aSElliott Hughes            glyph.draw(perContourPen)
74*e1fe3e4aSElliott Hughes        self.recordings = perContourPen.value
75*e1fe3e4aSElliott Hughes        del perContourPen
76*e1fe3e4aSElliott Hughes
77*e1fe3e4aSElliott Hughes        for ix, contour in enumerate(self.recordings):
78*e1fe3e4aSElliott Hughes            nodeTypes = [op for op, arg in contour.value]
79*e1fe3e4aSElliott Hughes            self.nodeTypes.append(nodeTypes)
80*e1fe3e4aSElliott Hughes
81*e1fe3e4aSElliott Hughes            greenStats = StatisticsPen(glyphset=glyphset)
82*e1fe3e4aSElliott Hughes            controlStats = StatisticsControlPen(glyphset=glyphset)
83*e1fe3e4aSElliott Hughes            try:
84*e1fe3e4aSElliott Hughes                contour.replay(greenStats)
85*e1fe3e4aSElliott Hughes                contour.replay(controlStats)
86*e1fe3e4aSElliott Hughes                self.openContours.append(False)
87*e1fe3e4aSElliott Hughes            except OpenContourError as e:
88*e1fe3e4aSElliott Hughes                self.openContours.append(True)
89*e1fe3e4aSElliott Hughes                self._fill_in(ix)
90*e1fe3e4aSElliott Hughes                continue
91*e1fe3e4aSElliott Hughes            self.greenStats.append(greenStats)
92*e1fe3e4aSElliott Hughes            self.controlStats.append(controlStats)
93*e1fe3e4aSElliott Hughes            self.greenVectors.append(contour_vector_from_stats(greenStats))
94*e1fe3e4aSElliott Hughes            self.controlVectors.append(contour_vector_from_stats(controlStats))
95*e1fe3e4aSElliott Hughes
96*e1fe3e4aSElliott Hughes            # Check starting point
97*e1fe3e4aSElliott Hughes            if nodeTypes[0] == "addComponent":
98*e1fe3e4aSElliott Hughes                self._fill_in(ix)
99*e1fe3e4aSElliott Hughes                continue
100*e1fe3e4aSElliott Hughes
101*e1fe3e4aSElliott Hughes            assert nodeTypes[0] == "moveTo"
102*e1fe3e4aSElliott Hughes            assert nodeTypes[-1] in ("closePath", "endPath")
103*e1fe3e4aSElliott Hughes            points = SimpleRecordingPointPen()
104*e1fe3e4aSElliott Hughes            converter = SegmentToPointPen(points, False)
105*e1fe3e4aSElliott Hughes            contour.replay(converter)
106*e1fe3e4aSElliott Hughes            # points.value is a list of pt,bool where bool is true if on-curve and false if off-curve;
107*e1fe3e4aSElliott Hughes            # now check all rotations and mirror-rotations of the contour and build list of isomorphic
108*e1fe3e4aSElliott Hughes            # possible starting points.
109*e1fe3e4aSElliott Hughes            self.points.append(points.value)
110*e1fe3e4aSElliott Hughes
111*e1fe3e4aSElliott Hughes            isomorphisms = []
112*e1fe3e4aSElliott Hughes            self.isomorphisms.append(isomorphisms)
113*e1fe3e4aSElliott Hughes
114*e1fe3e4aSElliott Hughes            # Add rotations
115*e1fe3e4aSElliott Hughes            add_isomorphisms(points.value, isomorphisms, False)
116*e1fe3e4aSElliott Hughes            # Add mirrored rotations
117*e1fe3e4aSElliott Hughes            add_isomorphisms(points.value, isomorphisms, True)
118*e1fe3e4aSElliott Hughes
119*e1fe3e4aSElliott Hughes    def draw(self, pen, countor_idx=None):
120*e1fe3e4aSElliott Hughes        if countor_idx is None:
121*e1fe3e4aSElliott Hughes            for contour in self.recordings:
122*e1fe3e4aSElliott Hughes                contour.draw(pen)
123*e1fe3e4aSElliott Hughes        else:
124*e1fe3e4aSElliott Hughes            self.recordings[countor_idx].draw(pen)
125*e1fe3e4aSElliott Hughes
126*e1fe3e4aSElliott Hughes
127*e1fe3e4aSElliott Hughesdef test_gen(
128*e1fe3e4aSElliott Hughes    glyphsets,
129*e1fe3e4aSElliott Hughes    glyphs=None,
130*e1fe3e4aSElliott Hughes    names=None,
131*e1fe3e4aSElliott Hughes    ignore_missing=False,
132*e1fe3e4aSElliott Hughes    *,
133*e1fe3e4aSElliott Hughes    locations=None,
134*e1fe3e4aSElliott Hughes    tolerance=DEFAULT_TOLERANCE,
135*e1fe3e4aSElliott Hughes    kinkiness=DEFAULT_KINKINESS,
136*e1fe3e4aSElliott Hughes    upem=DEFAULT_UPEM,
137*e1fe3e4aSElliott Hughes    show_all=False,
138*e1fe3e4aSElliott Hughes):
139*e1fe3e4aSElliott Hughes    if tolerance >= 10:
140*e1fe3e4aSElliott Hughes        tolerance *= 0.01
141*e1fe3e4aSElliott Hughes    assert 0 <= tolerance <= 1
142*e1fe3e4aSElliott Hughes    if kinkiness >= 10:
143*e1fe3e4aSElliott Hughes        kinkiness *= 0.01
144*e1fe3e4aSElliott Hughes    assert 0 <= kinkiness
145*e1fe3e4aSElliott Hughes
146*e1fe3e4aSElliott Hughes    names = names or [repr(g) for g in glyphsets]
147*e1fe3e4aSElliott Hughes
148*e1fe3e4aSElliott Hughes    if glyphs is None:
149*e1fe3e4aSElliott Hughes        # `glyphs = glyphsets[0].keys()` is faster, certainly, but doesn't allow for sparse TTFs/OTFs given out of order
150*e1fe3e4aSElliott Hughes        # ... risks the sparse master being the first one, and only processing a subset of the glyphs
151*e1fe3e4aSElliott Hughes        glyphs = {g for glyphset in glyphsets for g in glyphset.keys()}
152*e1fe3e4aSElliott Hughes
153*e1fe3e4aSElliott Hughes    parents, order = find_parents_and_order(glyphsets, locations)
154*e1fe3e4aSElliott Hughes
155*e1fe3e4aSElliott Hughes    def grand_parent(i, glyphname):
156*e1fe3e4aSElliott Hughes        if i is None:
157*e1fe3e4aSElliott Hughes            return None
158*e1fe3e4aSElliott Hughes        i = parents[i]
159*e1fe3e4aSElliott Hughes        if i is None:
160*e1fe3e4aSElliott Hughes            return None
161*e1fe3e4aSElliott Hughes        while parents[i] is not None and glyphsets[i][glyphname] is None:
162*e1fe3e4aSElliott Hughes            i = parents[i]
163*e1fe3e4aSElliott Hughes        return i
164*e1fe3e4aSElliott Hughes
165*e1fe3e4aSElliott Hughes    for glyph_name in glyphs:
166*e1fe3e4aSElliott Hughes        log.info("Testing glyph %s", glyph_name)
167*e1fe3e4aSElliott Hughes        allGlyphs = [Glyph(glyph_name, glyphset) for glyphset in glyphsets]
168*e1fe3e4aSElliott Hughes        if len([1 for glyph in allGlyphs if glyph is not None]) <= 1:
169*e1fe3e4aSElliott Hughes            continue
170*e1fe3e4aSElliott Hughes        for master_idx, (glyph, glyphset, name) in enumerate(
171*e1fe3e4aSElliott Hughes            zip(allGlyphs, glyphsets, names)
172*e1fe3e4aSElliott Hughes        ):
173*e1fe3e4aSElliott Hughes            if glyph.doesnt_exist:
174*e1fe3e4aSElliott Hughes                if not ignore_missing:
175*e1fe3e4aSElliott Hughes                    yield (
176*e1fe3e4aSElliott Hughes                        glyph_name,
177*e1fe3e4aSElliott Hughes                        {
178*e1fe3e4aSElliott Hughes                            "type": InterpolatableProblem.MISSING,
179*e1fe3e4aSElliott Hughes                            "master": name,
180*e1fe3e4aSElliott Hughes                            "master_idx": master_idx,
181*e1fe3e4aSElliott Hughes                        },
182*e1fe3e4aSElliott Hughes                    )
183*e1fe3e4aSElliott Hughes                continue
184*e1fe3e4aSElliott Hughes
185*e1fe3e4aSElliott Hughes            has_open = False
186*e1fe3e4aSElliott Hughes            for ix, open in enumerate(glyph.openContours):
187*e1fe3e4aSElliott Hughes                if not open:
188*e1fe3e4aSElliott Hughes                    continue
189*e1fe3e4aSElliott Hughes                has_open = True
190*e1fe3e4aSElliott Hughes                yield (
191*e1fe3e4aSElliott Hughes                    glyph_name,
192*e1fe3e4aSElliott Hughes                    {
193*e1fe3e4aSElliott Hughes                        "type": InterpolatableProblem.OPEN_PATH,
194*e1fe3e4aSElliott Hughes                        "master": name,
195*e1fe3e4aSElliott Hughes                        "master_idx": master_idx,
196*e1fe3e4aSElliott Hughes                        "contour": ix,
197*e1fe3e4aSElliott Hughes                    },
198*e1fe3e4aSElliott Hughes                )
199*e1fe3e4aSElliott Hughes            if has_open:
200*e1fe3e4aSElliott Hughes                continue
201*e1fe3e4aSElliott Hughes
202*e1fe3e4aSElliott Hughes        matchings = [None] * len(glyphsets)
203*e1fe3e4aSElliott Hughes
204*e1fe3e4aSElliott Hughes        for m1idx in order:
205*e1fe3e4aSElliott Hughes            glyph1 = allGlyphs[m1idx]
206*e1fe3e4aSElliott Hughes            if glyph1 is None or not glyph1.nodeTypes:
207*e1fe3e4aSElliott Hughes                continue
208*e1fe3e4aSElliott Hughes            m0idx = grand_parent(m1idx, glyph_name)
209*e1fe3e4aSElliott Hughes            if m0idx is None:
210*e1fe3e4aSElliott Hughes                continue
211*e1fe3e4aSElliott Hughes            glyph0 = allGlyphs[m0idx]
212*e1fe3e4aSElliott Hughes            if glyph0 is None or not glyph0.nodeTypes:
213*e1fe3e4aSElliott Hughes                continue
214*e1fe3e4aSElliott Hughes
215*e1fe3e4aSElliott Hughes            #
216*e1fe3e4aSElliott Hughes            # Basic compatibility checks
217*e1fe3e4aSElliott Hughes            #
218*e1fe3e4aSElliott Hughes
219*e1fe3e4aSElliott Hughes            m1 = glyph0.nodeTypes
220*e1fe3e4aSElliott Hughes            m0 = glyph1.nodeTypes
221*e1fe3e4aSElliott Hughes            if len(m0) != len(m1):
222*e1fe3e4aSElliott Hughes                yield (
223*e1fe3e4aSElliott Hughes                    glyph_name,
224*e1fe3e4aSElliott Hughes                    {
225*e1fe3e4aSElliott Hughes                        "type": InterpolatableProblem.PATH_COUNT,
226*e1fe3e4aSElliott Hughes                        "master_1": names[m0idx],
227*e1fe3e4aSElliott Hughes                        "master_2": names[m1idx],
228*e1fe3e4aSElliott Hughes                        "master_1_idx": m0idx,
229*e1fe3e4aSElliott Hughes                        "master_2_idx": m1idx,
230*e1fe3e4aSElliott Hughes                        "value_1": len(m0),
231*e1fe3e4aSElliott Hughes                        "value_2": len(m1),
232*e1fe3e4aSElliott Hughes                    },
233*e1fe3e4aSElliott Hughes                )
234*e1fe3e4aSElliott Hughes                continue
235*e1fe3e4aSElliott Hughes
236*e1fe3e4aSElliott Hughes            if m0 != m1:
237*e1fe3e4aSElliott Hughes                for pathIx, (nodes1, nodes2) in enumerate(zip(m0, m1)):
238*e1fe3e4aSElliott Hughes                    if nodes1 == nodes2:
239*e1fe3e4aSElliott Hughes                        continue
240*e1fe3e4aSElliott Hughes                    if len(nodes1) != len(nodes2):
241*e1fe3e4aSElliott Hughes                        yield (
242*e1fe3e4aSElliott Hughes                            glyph_name,
243*e1fe3e4aSElliott Hughes                            {
244*e1fe3e4aSElliott Hughes                                "type": InterpolatableProblem.NODE_COUNT,
245*e1fe3e4aSElliott Hughes                                "path": pathIx,
246*e1fe3e4aSElliott Hughes                                "master_1": names[m0idx],
247*e1fe3e4aSElliott Hughes                                "master_2": names[m1idx],
248*e1fe3e4aSElliott Hughes                                "master_1_idx": m0idx,
249*e1fe3e4aSElliott Hughes                                "master_2_idx": m1idx,
250*e1fe3e4aSElliott Hughes                                "value_1": len(nodes1),
251*e1fe3e4aSElliott Hughes                                "value_2": len(nodes2),
252*e1fe3e4aSElliott Hughes                            },
253*e1fe3e4aSElliott Hughes                        )
254*e1fe3e4aSElliott Hughes                        continue
255*e1fe3e4aSElliott Hughes                    for nodeIx, (n1, n2) in enumerate(zip(nodes1, nodes2)):
256*e1fe3e4aSElliott Hughes                        if n1 != n2:
257*e1fe3e4aSElliott Hughes                            yield (
258*e1fe3e4aSElliott Hughes                                glyph_name,
259*e1fe3e4aSElliott Hughes                                {
260*e1fe3e4aSElliott Hughes                                    "type": InterpolatableProblem.NODE_INCOMPATIBILITY,
261*e1fe3e4aSElliott Hughes                                    "path": pathIx,
262*e1fe3e4aSElliott Hughes                                    "node": nodeIx,
263*e1fe3e4aSElliott Hughes                                    "master_1": names[m0idx],
264*e1fe3e4aSElliott Hughes                                    "master_2": names[m1idx],
265*e1fe3e4aSElliott Hughes                                    "master_1_idx": m0idx,
266*e1fe3e4aSElliott Hughes                                    "master_2_idx": m1idx,
267*e1fe3e4aSElliott Hughes                                    "value_1": n1,
268*e1fe3e4aSElliott Hughes                                    "value_2": n2,
269*e1fe3e4aSElliott Hughes                                },
270*e1fe3e4aSElliott Hughes                            )
271*e1fe3e4aSElliott Hughes                            continue
272*e1fe3e4aSElliott Hughes
273*e1fe3e4aSElliott Hughes            #
274*e1fe3e4aSElliott Hughes            # InterpolatableProblem.CONTOUR_ORDER check
275*e1fe3e4aSElliott Hughes            #
276*e1fe3e4aSElliott Hughes
277*e1fe3e4aSElliott Hughes            this_tolerance, matching = test_contour_order(glyph0, glyph1)
278*e1fe3e4aSElliott Hughes            if this_tolerance < tolerance:
279*e1fe3e4aSElliott Hughes                yield (
280*e1fe3e4aSElliott Hughes                    glyph_name,
281*e1fe3e4aSElliott Hughes                    {
282*e1fe3e4aSElliott Hughes                        "type": InterpolatableProblem.CONTOUR_ORDER,
283*e1fe3e4aSElliott Hughes                        "master_1": names[m0idx],
284*e1fe3e4aSElliott Hughes                        "master_2": names[m1idx],
285*e1fe3e4aSElliott Hughes                        "master_1_idx": m0idx,
286*e1fe3e4aSElliott Hughes                        "master_2_idx": m1idx,
287*e1fe3e4aSElliott Hughes                        "value_1": list(range(len(matching))),
288*e1fe3e4aSElliott Hughes                        "value_2": matching,
289*e1fe3e4aSElliott Hughes                        "tolerance": this_tolerance,
290*e1fe3e4aSElliott Hughes                    },
291*e1fe3e4aSElliott Hughes                )
292*e1fe3e4aSElliott Hughes                matchings[m1idx] = matching
293*e1fe3e4aSElliott Hughes
294*e1fe3e4aSElliott Hughes            #
295*e1fe3e4aSElliott Hughes            # wrong-start-point / weight check
296*e1fe3e4aSElliott Hughes            #
297*e1fe3e4aSElliott Hughes
298*e1fe3e4aSElliott Hughes            m0Isomorphisms = glyph0.isomorphisms
299*e1fe3e4aSElliott Hughes            m1Isomorphisms = glyph1.isomorphisms
300*e1fe3e4aSElliott Hughes            m0Vectors = glyph0.greenVectors
301*e1fe3e4aSElliott Hughes            m1Vectors = glyph1.greenVectors
302*e1fe3e4aSElliott Hughes            recording0 = glyph0.recordings
303*e1fe3e4aSElliott Hughes            recording1 = glyph1.recordings
304*e1fe3e4aSElliott Hughes
305*e1fe3e4aSElliott Hughes            # If contour-order is wrong, adjust it
306*e1fe3e4aSElliott Hughes            matching = matchings[m1idx]
307*e1fe3e4aSElliott Hughes            if (
308*e1fe3e4aSElliott Hughes                matching is not None and m1Isomorphisms
309*e1fe3e4aSElliott Hughes            ):  # m1 is empty for composite glyphs
310*e1fe3e4aSElliott Hughes                m1Isomorphisms = [m1Isomorphisms[i] for i in matching]
311*e1fe3e4aSElliott Hughes                m1Vectors = [m1Vectors[i] for i in matching]
312*e1fe3e4aSElliott Hughes                recording1 = [recording1[i] for i in matching]
313*e1fe3e4aSElliott Hughes
314*e1fe3e4aSElliott Hughes            midRecording = []
315*e1fe3e4aSElliott Hughes            for c0, c1 in zip(recording0, recording1):
316*e1fe3e4aSElliott Hughes                try:
317*e1fe3e4aSElliott Hughes                    r = RecordingPen()
318*e1fe3e4aSElliott Hughes                    r.value = list(lerpRecordings(c0.value, c1.value))
319*e1fe3e4aSElliott Hughes                    midRecording.append(r)
320*e1fe3e4aSElliott Hughes                except ValueError:
321*e1fe3e4aSElliott Hughes                    # Mismatch because of the reordering above
322*e1fe3e4aSElliott Hughes                    midRecording.append(None)
323*e1fe3e4aSElliott Hughes
324*e1fe3e4aSElliott Hughes            for ix, (contour0, contour1) in enumerate(
325*e1fe3e4aSElliott Hughes                zip(m0Isomorphisms, m1Isomorphisms)
326*e1fe3e4aSElliott Hughes            ):
327*e1fe3e4aSElliott Hughes                if (
328*e1fe3e4aSElliott Hughes                    contour0 is None
329*e1fe3e4aSElliott Hughes                    or contour1 is None
330*e1fe3e4aSElliott Hughes                    or len(contour0) == 0
331*e1fe3e4aSElliott Hughes                    or len(contour0) != len(contour1)
332*e1fe3e4aSElliott Hughes                ):
333*e1fe3e4aSElliott Hughes                    # We already reported this; or nothing to do; or not compatible
334*e1fe3e4aSElliott Hughes                    # after reordering above.
335*e1fe3e4aSElliott Hughes                    continue
336*e1fe3e4aSElliott Hughes
337*e1fe3e4aSElliott Hughes                this_tolerance, proposed_point, reverse = test_starting_point(
338*e1fe3e4aSElliott Hughes                    glyph0, glyph1, ix, tolerance, matching
339*e1fe3e4aSElliott Hughes                )
340*e1fe3e4aSElliott Hughes
341*e1fe3e4aSElliott Hughes                if this_tolerance < tolerance:
342*e1fe3e4aSElliott Hughes                    yield (
343*e1fe3e4aSElliott Hughes                        glyph_name,
344*e1fe3e4aSElliott Hughes                        {
345*e1fe3e4aSElliott Hughes                            "type": InterpolatableProblem.WRONG_START_POINT,
346*e1fe3e4aSElliott Hughes                            "contour": ix,
347*e1fe3e4aSElliott Hughes                            "master_1": names[m0idx],
348*e1fe3e4aSElliott Hughes                            "master_2": names[m1idx],
349*e1fe3e4aSElliott Hughes                            "master_1_idx": m0idx,
350*e1fe3e4aSElliott Hughes                            "master_2_idx": m1idx,
351*e1fe3e4aSElliott Hughes                            "value_1": 0,
352*e1fe3e4aSElliott Hughes                            "value_2": proposed_point,
353*e1fe3e4aSElliott Hughes                            "reversed": reverse,
354*e1fe3e4aSElliott Hughes                            "tolerance": this_tolerance,
355*e1fe3e4aSElliott Hughes                        },
356*e1fe3e4aSElliott Hughes                    )
357*e1fe3e4aSElliott Hughes
358*e1fe3e4aSElliott Hughes                # Weight check.
359*e1fe3e4aSElliott Hughes                #
360*e1fe3e4aSElliott Hughes                # If contour could be mid-interpolated, and the two
361*e1fe3e4aSElliott Hughes                # contours have the same area sign, proceeed.
362*e1fe3e4aSElliott Hughes                #
363*e1fe3e4aSElliott Hughes                # The sign difference can happen if it's a weirdo
364*e1fe3e4aSElliott Hughes                # self-intersecting contour; ignore it.
365*e1fe3e4aSElliott Hughes                contour = midRecording[ix]
366*e1fe3e4aSElliott Hughes
367*e1fe3e4aSElliott Hughes                if contour and (m0Vectors[ix][0] < 0) == (m1Vectors[ix][0] < 0):
368*e1fe3e4aSElliott Hughes                    midStats = StatisticsPen(glyphset=None)
369*e1fe3e4aSElliott Hughes                    contour.replay(midStats)
370*e1fe3e4aSElliott Hughes
371*e1fe3e4aSElliott Hughes                    midVector = contour_vector_from_stats(midStats)
372*e1fe3e4aSElliott Hughes
373*e1fe3e4aSElliott Hughes                    m0Vec = m0Vectors[ix]
374*e1fe3e4aSElliott Hughes                    m1Vec = m1Vectors[ix]
375*e1fe3e4aSElliott Hughes                    size0 = m0Vec[0] * m0Vec[0]
376*e1fe3e4aSElliott Hughes                    size1 = m1Vec[0] * m1Vec[0]
377*e1fe3e4aSElliott Hughes                    midSize = midVector[0] * midVector[0]
378*e1fe3e4aSElliott Hughes
379*e1fe3e4aSElliott Hughes                    for overweight, problem_type in enumerate(
380*e1fe3e4aSElliott Hughes                        (
381*e1fe3e4aSElliott Hughes                            InterpolatableProblem.UNDERWEIGHT,
382*e1fe3e4aSElliott Hughes                            InterpolatableProblem.OVERWEIGHT,
383*e1fe3e4aSElliott Hughes                        )
384*e1fe3e4aSElliott Hughes                    ):
385*e1fe3e4aSElliott Hughes                        if overweight:
386*e1fe3e4aSElliott Hughes                            expectedSize = max(size0, size1)
387*e1fe3e4aSElliott Hughes                            continue
388*e1fe3e4aSElliott Hughes                        else:
389*e1fe3e4aSElliott Hughes                            expectedSize = sqrt(size0 * size1)
390*e1fe3e4aSElliott Hughes
391*e1fe3e4aSElliott Hughes                        log.debug(
392*e1fe3e4aSElliott Hughes                            "%s: actual size %g; threshold size %g, master sizes: %g, %g",
393*e1fe3e4aSElliott Hughes                            problem_type,
394*e1fe3e4aSElliott Hughes                            midSize,
395*e1fe3e4aSElliott Hughes                            expectedSize,
396*e1fe3e4aSElliott Hughes                            size0,
397*e1fe3e4aSElliott Hughes                            size1,
398*e1fe3e4aSElliott Hughes                        )
399*e1fe3e4aSElliott Hughes
400*e1fe3e4aSElliott Hughes                        if (
401*e1fe3e4aSElliott Hughes                            not overweight and expectedSize * tolerance > midSize + 1e-5
402*e1fe3e4aSElliott Hughes                        ) or (overweight and 1e-5 + expectedSize / tolerance < midSize):
403*e1fe3e4aSElliott Hughes                            try:
404*e1fe3e4aSElliott Hughes                                if overweight:
405*e1fe3e4aSElliott Hughes                                    this_tolerance = expectedSize / midSize
406*e1fe3e4aSElliott Hughes                                else:
407*e1fe3e4aSElliott Hughes                                    this_tolerance = midSize / expectedSize
408*e1fe3e4aSElliott Hughes                            except ZeroDivisionError:
409*e1fe3e4aSElliott Hughes                                this_tolerance = 0
410*e1fe3e4aSElliott Hughes                            log.debug("tolerance %g", this_tolerance)
411*e1fe3e4aSElliott Hughes                            yield (
412*e1fe3e4aSElliott Hughes                                glyph_name,
413*e1fe3e4aSElliott Hughes                                {
414*e1fe3e4aSElliott Hughes                                    "type": problem_type,
415*e1fe3e4aSElliott Hughes                                    "contour": ix,
416*e1fe3e4aSElliott Hughes                                    "master_1": names[m0idx],
417*e1fe3e4aSElliott Hughes                                    "master_2": names[m1idx],
418*e1fe3e4aSElliott Hughes                                    "master_1_idx": m0idx,
419*e1fe3e4aSElliott Hughes                                    "master_2_idx": m1idx,
420*e1fe3e4aSElliott Hughes                                    "tolerance": this_tolerance,
421*e1fe3e4aSElliott Hughes                                },
422*e1fe3e4aSElliott Hughes                            )
423*e1fe3e4aSElliott Hughes
424*e1fe3e4aSElliott Hughes            #
425*e1fe3e4aSElliott Hughes            # "kink" detector
426*e1fe3e4aSElliott Hughes            #
427*e1fe3e4aSElliott Hughes            m0 = glyph0.points
428*e1fe3e4aSElliott Hughes            m1 = glyph1.points
429*e1fe3e4aSElliott Hughes
430*e1fe3e4aSElliott Hughes            # If contour-order is wrong, adjust it
431*e1fe3e4aSElliott Hughes            if matchings[m1idx] is not None and m1:  # m1 is empty for composite glyphs
432*e1fe3e4aSElliott Hughes                m1 = [m1[i] for i in matchings[m1idx]]
433*e1fe3e4aSElliott Hughes
434*e1fe3e4aSElliott Hughes            t = 0.1  # ~sin(radian(6)) for tolerance 0.95
435*e1fe3e4aSElliott Hughes            deviation_threshold = (
436*e1fe3e4aSElliott Hughes                upem * DEFAULT_KINKINESS_LENGTH * DEFAULT_KINKINESS / kinkiness
437*e1fe3e4aSElliott Hughes            )
438*e1fe3e4aSElliott Hughes
439*e1fe3e4aSElliott Hughes            for ix, (contour0, contour1) in enumerate(zip(m0, m1)):
440*e1fe3e4aSElliott Hughes                if (
441*e1fe3e4aSElliott Hughes                    contour0 is None
442*e1fe3e4aSElliott Hughes                    or contour1 is None
443*e1fe3e4aSElliott Hughes                    or len(contour0) == 0
444*e1fe3e4aSElliott Hughes                    or len(contour0) != len(contour1)
445*e1fe3e4aSElliott Hughes                ):
446*e1fe3e4aSElliott Hughes                    # We already reported this; or nothing to do; or not compatible
447*e1fe3e4aSElliott Hughes                    # after reordering above.
448*e1fe3e4aSElliott Hughes                    continue
449*e1fe3e4aSElliott Hughes
450*e1fe3e4aSElliott Hughes                # Walk the contour, keeping track of three consecutive points, with
451*e1fe3e4aSElliott Hughes                # middle one being an on-curve. If the three are co-linear then
452*e1fe3e4aSElliott Hughes                # check for kinky-ness.
453*e1fe3e4aSElliott Hughes                for i in range(len(contour0)):
454*e1fe3e4aSElliott Hughes                    pt0 = contour0[i]
455*e1fe3e4aSElliott Hughes                    pt1 = contour1[i]
456*e1fe3e4aSElliott Hughes                    if not pt0[1] or not pt1[1]:
457*e1fe3e4aSElliott Hughes                        # Skip off-curves
458*e1fe3e4aSElliott Hughes                        continue
459*e1fe3e4aSElliott Hughes                    pt0_prev = contour0[i - 1]
460*e1fe3e4aSElliott Hughes                    pt1_prev = contour1[i - 1]
461*e1fe3e4aSElliott Hughes                    pt0_next = contour0[(i + 1) % len(contour0)]
462*e1fe3e4aSElliott Hughes                    pt1_next = contour1[(i + 1) % len(contour1)]
463*e1fe3e4aSElliott Hughes
464*e1fe3e4aSElliott Hughes                    if pt0_prev[1] and pt1_prev[1]:
465*e1fe3e4aSElliott Hughes                        # At least one off-curve is required
466*e1fe3e4aSElliott Hughes                        continue
467*e1fe3e4aSElliott Hughes                    if pt0_prev[1] and pt1_prev[1]:
468*e1fe3e4aSElliott Hughes                        # At least one off-curve is required
469*e1fe3e4aSElliott Hughes                        continue
470*e1fe3e4aSElliott Hughes
471*e1fe3e4aSElliott Hughes                    pt0 = complex(*pt0[0])
472*e1fe3e4aSElliott Hughes                    pt1 = complex(*pt1[0])
473*e1fe3e4aSElliott Hughes                    pt0_prev = complex(*pt0_prev[0])
474*e1fe3e4aSElliott Hughes                    pt1_prev = complex(*pt1_prev[0])
475*e1fe3e4aSElliott Hughes                    pt0_next = complex(*pt0_next[0])
476*e1fe3e4aSElliott Hughes                    pt1_next = complex(*pt1_next[0])
477*e1fe3e4aSElliott Hughes
478*e1fe3e4aSElliott Hughes                    # We have three consecutive points. Check whether
479*e1fe3e4aSElliott Hughes                    # they are colinear.
480*e1fe3e4aSElliott Hughes                    d0_prev = pt0 - pt0_prev
481*e1fe3e4aSElliott Hughes                    d0_next = pt0_next - pt0
482*e1fe3e4aSElliott Hughes                    d1_prev = pt1 - pt1_prev
483*e1fe3e4aSElliott Hughes                    d1_next = pt1_next - pt1
484*e1fe3e4aSElliott Hughes
485*e1fe3e4aSElliott Hughes                    sin0 = d0_prev.real * d0_next.imag - d0_prev.imag * d0_next.real
486*e1fe3e4aSElliott Hughes                    sin1 = d1_prev.real * d1_next.imag - d1_prev.imag * d1_next.real
487*e1fe3e4aSElliott Hughes                    try:
488*e1fe3e4aSElliott Hughes                        sin0 /= abs(d0_prev) * abs(d0_next)
489*e1fe3e4aSElliott Hughes                        sin1 /= abs(d1_prev) * abs(d1_next)
490*e1fe3e4aSElliott Hughes                    except ZeroDivisionError:
491*e1fe3e4aSElliott Hughes                        continue
492*e1fe3e4aSElliott Hughes
493*e1fe3e4aSElliott Hughes                    if abs(sin0) > t or abs(sin1) > t:
494*e1fe3e4aSElliott Hughes                        # Not colinear / not smooth.
495*e1fe3e4aSElliott Hughes                        continue
496*e1fe3e4aSElliott Hughes
497*e1fe3e4aSElliott Hughes                    # Check the mid-point is actually, well, in the middle.
498*e1fe3e4aSElliott Hughes                    dot0 = d0_prev.real * d0_next.real + d0_prev.imag * d0_next.imag
499*e1fe3e4aSElliott Hughes                    dot1 = d1_prev.real * d1_next.real + d1_prev.imag * d1_next.imag
500*e1fe3e4aSElliott Hughes                    if dot0 < 0 or dot1 < 0:
501*e1fe3e4aSElliott Hughes                        # Sharp corner.
502*e1fe3e4aSElliott Hughes                        continue
503*e1fe3e4aSElliott Hughes
504*e1fe3e4aSElliott Hughes                    # Fine, if handle ratios are similar...
505*e1fe3e4aSElliott Hughes                    r0 = abs(d0_prev) / (abs(d0_prev) + abs(d0_next))
506*e1fe3e4aSElliott Hughes                    r1 = abs(d1_prev) / (abs(d1_prev) + abs(d1_next))
507*e1fe3e4aSElliott Hughes                    r_diff = abs(r0 - r1)
508*e1fe3e4aSElliott Hughes                    if abs(r_diff) < t:
509*e1fe3e4aSElliott Hughes                        # Smooth enough.
510*e1fe3e4aSElliott Hughes                        continue
511*e1fe3e4aSElliott Hughes
512*e1fe3e4aSElliott Hughes                    mid = (pt0 + pt1) / 2
513*e1fe3e4aSElliott Hughes                    mid_prev = (pt0_prev + pt1_prev) / 2
514*e1fe3e4aSElliott Hughes                    mid_next = (pt0_next + pt1_next) / 2
515*e1fe3e4aSElliott Hughes
516*e1fe3e4aSElliott Hughes                    mid_d0 = mid - mid_prev
517*e1fe3e4aSElliott Hughes                    mid_d1 = mid_next - mid
518*e1fe3e4aSElliott Hughes
519*e1fe3e4aSElliott Hughes                    sin_mid = mid_d0.real * mid_d1.imag - mid_d0.imag * mid_d1.real
520*e1fe3e4aSElliott Hughes                    try:
521*e1fe3e4aSElliott Hughes                        sin_mid /= abs(mid_d0) * abs(mid_d1)
522*e1fe3e4aSElliott Hughes                    except ZeroDivisionError:
523*e1fe3e4aSElliott Hughes                        continue
524*e1fe3e4aSElliott Hughes
525*e1fe3e4aSElliott Hughes                    # ...or if the angles are similar.
526*e1fe3e4aSElliott Hughes                    if abs(sin_mid) * (tolerance * kinkiness) <= t:
527*e1fe3e4aSElliott Hughes                        # Smooth enough.
528*e1fe3e4aSElliott Hughes                        continue
529*e1fe3e4aSElliott Hughes
530*e1fe3e4aSElliott Hughes                    # How visible is the kink?
531*e1fe3e4aSElliott Hughes
532*e1fe3e4aSElliott Hughes                    cross = sin_mid * abs(mid_d0) * abs(mid_d1)
533*e1fe3e4aSElliott Hughes                    arc_len = abs(mid_d0 + mid_d1)
534*e1fe3e4aSElliott Hughes                    deviation = abs(cross / arc_len)
535*e1fe3e4aSElliott Hughes                    if deviation < deviation_threshold:
536*e1fe3e4aSElliott Hughes                        continue
537*e1fe3e4aSElliott Hughes                    deviation_ratio = deviation / arc_len
538*e1fe3e4aSElliott Hughes                    if deviation_ratio > t:
539*e1fe3e4aSElliott Hughes                        continue
540*e1fe3e4aSElliott Hughes
541*e1fe3e4aSElliott Hughes                    this_tolerance = t / (abs(sin_mid) * kinkiness)
542*e1fe3e4aSElliott Hughes
543*e1fe3e4aSElliott Hughes                    log.debug(
544*e1fe3e4aSElliott Hughes                        "kink: deviation %g; deviation_ratio %g; sin_mid %g; r_diff %g",
545*e1fe3e4aSElliott Hughes                        deviation,
546*e1fe3e4aSElliott Hughes                        deviation_ratio,
547*e1fe3e4aSElliott Hughes                        sin_mid,
548*e1fe3e4aSElliott Hughes                        r_diff,
549*e1fe3e4aSElliott Hughes                    )
550*e1fe3e4aSElliott Hughes                    log.debug("tolerance %g", this_tolerance)
551*e1fe3e4aSElliott Hughes                    yield (
552*e1fe3e4aSElliott Hughes                        glyph_name,
553*e1fe3e4aSElliott Hughes                        {
554*e1fe3e4aSElliott Hughes                            "type": InterpolatableProblem.KINK,
555*e1fe3e4aSElliott Hughes                            "contour": ix,
556*e1fe3e4aSElliott Hughes                            "master_1": names[m0idx],
557*e1fe3e4aSElliott Hughes                            "master_2": names[m1idx],
558*e1fe3e4aSElliott Hughes                            "master_1_idx": m0idx,
559*e1fe3e4aSElliott Hughes                            "master_2_idx": m1idx,
560*e1fe3e4aSElliott Hughes                            "value": i,
561*e1fe3e4aSElliott Hughes                            "tolerance": this_tolerance,
562*e1fe3e4aSElliott Hughes                        },
563*e1fe3e4aSElliott Hughes                    )
564*e1fe3e4aSElliott Hughes
565*e1fe3e4aSElliott Hughes            #
566*e1fe3e4aSElliott Hughes            # --show-all
567*e1fe3e4aSElliott Hughes            #
568*e1fe3e4aSElliott Hughes
569*e1fe3e4aSElliott Hughes            if show_all:
570*e1fe3e4aSElliott Hughes                yield (
571*e1fe3e4aSElliott Hughes                    glyph_name,
572*e1fe3e4aSElliott Hughes                    {
573*e1fe3e4aSElliott Hughes                        "type": InterpolatableProblem.NOTHING,
574*e1fe3e4aSElliott Hughes                        "master_1": names[m0idx],
575*e1fe3e4aSElliott Hughes                        "master_2": names[m1idx],
576*e1fe3e4aSElliott Hughes                        "master_1_idx": m0idx,
577*e1fe3e4aSElliott Hughes                        "master_2_idx": m1idx,
578*e1fe3e4aSElliott Hughes                    },
579*e1fe3e4aSElliott Hughes                )
580*e1fe3e4aSElliott Hughes
581*e1fe3e4aSElliott Hughes
582*e1fe3e4aSElliott Hughes@wraps(test_gen)
583*e1fe3e4aSElliott Hughesdef test(*args, **kwargs):
584*e1fe3e4aSElliott Hughes    problems = defaultdict(list)
585*e1fe3e4aSElliott Hughes    for glyphname, problem in test_gen(*args, **kwargs):
586*e1fe3e4aSElliott Hughes        problems[glyphname].append(problem)
587*e1fe3e4aSElliott Hughes    return problems
588*e1fe3e4aSElliott Hughes
589*e1fe3e4aSElliott Hughes
590*e1fe3e4aSElliott Hughesdef recursivelyAddGlyph(glyphname, glyphset, ttGlyphSet, glyf):
591*e1fe3e4aSElliott Hughes    if glyphname in glyphset:
592*e1fe3e4aSElliott Hughes        return
593*e1fe3e4aSElliott Hughes    glyphset[glyphname] = ttGlyphSet[glyphname]
594*e1fe3e4aSElliott Hughes
595*e1fe3e4aSElliott Hughes    for component in getattr(glyf[glyphname], "components", []):
596*e1fe3e4aSElliott Hughes        recursivelyAddGlyph(component.glyphName, glyphset, ttGlyphSet, glyf)
597*e1fe3e4aSElliott Hughes
598*e1fe3e4aSElliott Hughes
599*e1fe3e4aSElliott Hughesdef ensure_parent_dir(path):
600*e1fe3e4aSElliott Hughes    dirname = os.path.dirname(path)
601*e1fe3e4aSElliott Hughes    if dirname:
602*e1fe3e4aSElliott Hughes        os.makedirs(dirname, exist_ok=True)
603*e1fe3e4aSElliott Hughes    return path
604*e1fe3e4aSElliott Hughes
605*e1fe3e4aSElliott Hughes
606*e1fe3e4aSElliott Hughesdef main(args=None):
607*e1fe3e4aSElliott Hughes    """Test for interpolatability issues between fonts"""
608*e1fe3e4aSElliott Hughes    import argparse
609*e1fe3e4aSElliott Hughes    import sys
610*e1fe3e4aSElliott Hughes
611*e1fe3e4aSElliott Hughes    parser = argparse.ArgumentParser(
612*e1fe3e4aSElliott Hughes        "fonttools varLib.interpolatable",
613*e1fe3e4aSElliott Hughes        description=main.__doc__,
614*e1fe3e4aSElliott Hughes    )
615*e1fe3e4aSElliott Hughes    parser.add_argument(
616*e1fe3e4aSElliott Hughes        "--glyphs",
617*e1fe3e4aSElliott Hughes        action="store",
618*e1fe3e4aSElliott Hughes        help="Space-separate name of glyphs to check",
619*e1fe3e4aSElliott Hughes    )
620*e1fe3e4aSElliott Hughes    parser.add_argument(
621*e1fe3e4aSElliott Hughes        "--show-all",
622*e1fe3e4aSElliott Hughes        action="store_true",
623*e1fe3e4aSElliott Hughes        help="Show all glyph pairs, even if no problems are found",
624*e1fe3e4aSElliott Hughes    )
625*e1fe3e4aSElliott Hughes    parser.add_argument(
626*e1fe3e4aSElliott Hughes        "--tolerance",
627*e1fe3e4aSElliott Hughes        action="store",
628*e1fe3e4aSElliott Hughes        type=float,
629*e1fe3e4aSElliott Hughes        help="Error tolerance. Between 0 and 1. Default %s" % DEFAULT_TOLERANCE,
630*e1fe3e4aSElliott Hughes    )
631*e1fe3e4aSElliott Hughes    parser.add_argument(
632*e1fe3e4aSElliott Hughes        "--kinkiness",
633*e1fe3e4aSElliott Hughes        action="store",
634*e1fe3e4aSElliott Hughes        type=float,
635*e1fe3e4aSElliott Hughes        help="How aggressively report kinks. Default %s" % DEFAULT_KINKINESS,
636*e1fe3e4aSElliott Hughes    )
637*e1fe3e4aSElliott Hughes    parser.add_argument(
638*e1fe3e4aSElliott Hughes        "--json",
639*e1fe3e4aSElliott Hughes        action="store_true",
640*e1fe3e4aSElliott Hughes        help="Output report in JSON format",
641*e1fe3e4aSElliott Hughes    )
642*e1fe3e4aSElliott Hughes    parser.add_argument(
643*e1fe3e4aSElliott Hughes        "--pdf",
644*e1fe3e4aSElliott Hughes        action="store",
645*e1fe3e4aSElliott Hughes        help="Output report in PDF format",
646*e1fe3e4aSElliott Hughes    )
647*e1fe3e4aSElliott Hughes    parser.add_argument(
648*e1fe3e4aSElliott Hughes        "--ps",
649*e1fe3e4aSElliott Hughes        action="store",
650*e1fe3e4aSElliott Hughes        help="Output report in PostScript format",
651*e1fe3e4aSElliott Hughes    )
652*e1fe3e4aSElliott Hughes    parser.add_argument(
653*e1fe3e4aSElliott Hughes        "--html",
654*e1fe3e4aSElliott Hughes        action="store",
655*e1fe3e4aSElliott Hughes        help="Output report in HTML format",
656*e1fe3e4aSElliott Hughes    )
657*e1fe3e4aSElliott Hughes    parser.add_argument(
658*e1fe3e4aSElliott Hughes        "--quiet",
659*e1fe3e4aSElliott Hughes        action="store_true",
660*e1fe3e4aSElliott Hughes        help="Only exit with code 1 or 0, no output",
661*e1fe3e4aSElliott Hughes    )
662*e1fe3e4aSElliott Hughes    parser.add_argument(
663*e1fe3e4aSElliott Hughes        "--output",
664*e1fe3e4aSElliott Hughes        action="store",
665*e1fe3e4aSElliott Hughes        help="Output file for the problem report; Default: stdout",
666*e1fe3e4aSElliott Hughes    )
667*e1fe3e4aSElliott Hughes    parser.add_argument(
668*e1fe3e4aSElliott Hughes        "--ignore-missing",
669*e1fe3e4aSElliott Hughes        action="store_true",
670*e1fe3e4aSElliott Hughes        help="Will not report glyphs missing from sparse masters as errors",
671*e1fe3e4aSElliott Hughes    )
672*e1fe3e4aSElliott Hughes    parser.add_argument(
673*e1fe3e4aSElliott Hughes        "inputs",
674*e1fe3e4aSElliott Hughes        metavar="FILE",
675*e1fe3e4aSElliott Hughes        type=str,
676*e1fe3e4aSElliott Hughes        nargs="+",
677*e1fe3e4aSElliott Hughes        help="Input a single variable font / DesignSpace / Glyphs file, or multiple TTF/UFO files",
678*e1fe3e4aSElliott Hughes    )
679*e1fe3e4aSElliott Hughes    parser.add_argument(
680*e1fe3e4aSElliott Hughes        "--name",
681*e1fe3e4aSElliott Hughes        metavar="NAME",
682*e1fe3e4aSElliott Hughes        type=str,
683*e1fe3e4aSElliott Hughes        action="append",
684*e1fe3e4aSElliott Hughes        help="Name of the master to use in the report. If not provided, all are used.",
685*e1fe3e4aSElliott Hughes    )
686*e1fe3e4aSElliott Hughes    parser.add_argument("-v", "--verbose", action="store_true", help="Run verbosely.")
687*e1fe3e4aSElliott Hughes    parser.add_argument("--debug", action="store_true", help="Run with debug output.")
688*e1fe3e4aSElliott Hughes
689*e1fe3e4aSElliott Hughes    args = parser.parse_args(args)
690*e1fe3e4aSElliott Hughes
691*e1fe3e4aSElliott Hughes    from fontTools import configLogger
692*e1fe3e4aSElliott Hughes
693*e1fe3e4aSElliott Hughes    configLogger(level=("INFO" if args.verbose else "ERROR"))
694*e1fe3e4aSElliott Hughes    if args.debug:
695*e1fe3e4aSElliott Hughes        configLogger(level="DEBUG")
696*e1fe3e4aSElliott Hughes
697*e1fe3e4aSElliott Hughes    glyphs = args.glyphs.split() if args.glyphs else None
698*e1fe3e4aSElliott Hughes
699*e1fe3e4aSElliott Hughes    from os.path import basename
700*e1fe3e4aSElliott Hughes
701*e1fe3e4aSElliott Hughes    fonts = []
702*e1fe3e4aSElliott Hughes    names = []
703*e1fe3e4aSElliott Hughes    locations = []
704*e1fe3e4aSElliott Hughes    upem = DEFAULT_UPEM
705*e1fe3e4aSElliott Hughes
706*e1fe3e4aSElliott Hughes    original_args_inputs = tuple(args.inputs)
707*e1fe3e4aSElliott Hughes
708*e1fe3e4aSElliott Hughes    if len(args.inputs) == 1:
709*e1fe3e4aSElliott Hughes        designspace = None
710*e1fe3e4aSElliott Hughes        if args.inputs[0].endswith(".designspace"):
711*e1fe3e4aSElliott Hughes            from fontTools.designspaceLib import DesignSpaceDocument
712*e1fe3e4aSElliott Hughes
713*e1fe3e4aSElliott Hughes            designspace = DesignSpaceDocument.fromfile(args.inputs[0])
714*e1fe3e4aSElliott Hughes            args.inputs = [master.path for master in designspace.sources]
715*e1fe3e4aSElliott Hughes            locations = [master.location for master in designspace.sources]
716*e1fe3e4aSElliott Hughes            axis_triples = {
717*e1fe3e4aSElliott Hughes                a.name: (a.minimum, a.default, a.maximum) for a in designspace.axes
718*e1fe3e4aSElliott Hughes            }
719*e1fe3e4aSElliott Hughes            axis_mappings = {a.name: a.map for a in designspace.axes}
720*e1fe3e4aSElliott Hughes            axis_triples = {
721*e1fe3e4aSElliott Hughes                k: tuple(piecewiseLinearMap(v, dict(axis_mappings[k])) for v in vv)
722*e1fe3e4aSElliott Hughes                for k, vv in axis_triples.items()
723*e1fe3e4aSElliott Hughes            }
724*e1fe3e4aSElliott Hughes
725*e1fe3e4aSElliott Hughes        elif args.inputs[0].endswith((".glyphs", ".glyphspackage")):
726*e1fe3e4aSElliott Hughes            from glyphsLib import GSFont, to_designspace
727*e1fe3e4aSElliott Hughes
728*e1fe3e4aSElliott Hughes            gsfont = GSFont(args.inputs[0])
729*e1fe3e4aSElliott Hughes            upem = gsfont.upm
730*e1fe3e4aSElliott Hughes            designspace = to_designspace(gsfont)
731*e1fe3e4aSElliott Hughes            fonts = [source.font for source in designspace.sources]
732*e1fe3e4aSElliott Hughes            names = ["%s-%s" % (f.info.familyName, f.info.styleName) for f in fonts]
733*e1fe3e4aSElliott Hughes            args.inputs = []
734*e1fe3e4aSElliott Hughes            locations = [master.location for master in designspace.sources]
735*e1fe3e4aSElliott Hughes            axis_triples = {
736*e1fe3e4aSElliott Hughes                a.name: (a.minimum, a.default, a.maximum) for a in designspace.axes
737*e1fe3e4aSElliott Hughes            }
738*e1fe3e4aSElliott Hughes            axis_mappings = {a.name: a.map for a in designspace.axes}
739*e1fe3e4aSElliott Hughes            axis_triples = {
740*e1fe3e4aSElliott Hughes                k: tuple(piecewiseLinearMap(v, dict(axis_mappings[k])) for v in vv)
741*e1fe3e4aSElliott Hughes                for k, vv in axis_triples.items()
742*e1fe3e4aSElliott Hughes            }
743*e1fe3e4aSElliott Hughes
744*e1fe3e4aSElliott Hughes        elif args.inputs[0].endswith(".ttf"):
745*e1fe3e4aSElliott Hughes            from fontTools.ttLib import TTFont
746*e1fe3e4aSElliott Hughes
747*e1fe3e4aSElliott Hughes            font = TTFont(args.inputs[0])
748*e1fe3e4aSElliott Hughes            upem = font["head"].unitsPerEm
749*e1fe3e4aSElliott Hughes            if "gvar" in font:
750*e1fe3e4aSElliott Hughes                # Is variable font
751*e1fe3e4aSElliott Hughes
752*e1fe3e4aSElliott Hughes                axisMapping = {}
753*e1fe3e4aSElliott Hughes                fvar = font["fvar"]
754*e1fe3e4aSElliott Hughes                for axis in fvar.axes:
755*e1fe3e4aSElliott Hughes                    axisMapping[axis.axisTag] = {
756*e1fe3e4aSElliott Hughes                        -1: axis.minValue,
757*e1fe3e4aSElliott Hughes                        0: axis.defaultValue,
758*e1fe3e4aSElliott Hughes                        1: axis.maxValue,
759*e1fe3e4aSElliott Hughes                    }
760*e1fe3e4aSElliott Hughes                if "avar" in font:
761*e1fe3e4aSElliott Hughes                    avar = font["avar"]
762*e1fe3e4aSElliott Hughes                    for axisTag, segments in avar.segments.items():
763*e1fe3e4aSElliott Hughes                        fvarMapping = axisMapping[axisTag].copy()
764*e1fe3e4aSElliott Hughes                        for location, value in segments.items():
765*e1fe3e4aSElliott Hughes                            axisMapping[axisTag][value] = piecewiseLinearMap(
766*e1fe3e4aSElliott Hughes                                location, fvarMapping
767*e1fe3e4aSElliott Hughes                            )
768*e1fe3e4aSElliott Hughes
769*e1fe3e4aSElliott Hughes                gvar = font["gvar"]
770*e1fe3e4aSElliott Hughes                glyf = font["glyf"]
771*e1fe3e4aSElliott Hughes                # Gather all glyphs at their "master" locations
772*e1fe3e4aSElliott Hughes                ttGlyphSets = {}
773*e1fe3e4aSElliott Hughes                glyphsets = defaultdict(dict)
774*e1fe3e4aSElliott Hughes
775*e1fe3e4aSElliott Hughes                if glyphs is None:
776*e1fe3e4aSElliott Hughes                    glyphs = sorted(gvar.variations.keys())
777*e1fe3e4aSElliott Hughes                for glyphname in glyphs:
778*e1fe3e4aSElliott Hughes                    for var in gvar.variations[glyphname]:
779*e1fe3e4aSElliott Hughes                        locDict = {}
780*e1fe3e4aSElliott Hughes                        loc = []
781*e1fe3e4aSElliott Hughes                        for tag, val in sorted(var.axes.items()):
782*e1fe3e4aSElliott Hughes                            locDict[tag] = val[1]
783*e1fe3e4aSElliott Hughes                            loc.append((tag, val[1]))
784*e1fe3e4aSElliott Hughes
785*e1fe3e4aSElliott Hughes                        locTuple = tuple(loc)
786*e1fe3e4aSElliott Hughes                        if locTuple not in ttGlyphSets:
787*e1fe3e4aSElliott Hughes                            ttGlyphSets[locTuple] = font.getGlyphSet(
788*e1fe3e4aSElliott Hughes                                location=locDict, normalized=True, recalcBounds=False
789*e1fe3e4aSElliott Hughes                            )
790*e1fe3e4aSElliott Hughes
791*e1fe3e4aSElliott Hughes                        recursivelyAddGlyph(
792*e1fe3e4aSElliott Hughes                            glyphname, glyphsets[locTuple], ttGlyphSets[locTuple], glyf
793*e1fe3e4aSElliott Hughes                        )
794*e1fe3e4aSElliott Hughes
795*e1fe3e4aSElliott Hughes                names = ["''"]
796*e1fe3e4aSElliott Hughes                fonts = [font.getGlyphSet()]
797*e1fe3e4aSElliott Hughes                locations = [{}]
798*e1fe3e4aSElliott Hughes                axis_triples = {a: (-1, 0, +1) for a in sorted(axisMapping.keys())}
799*e1fe3e4aSElliott Hughes                for locTuple in sorted(glyphsets.keys(), key=lambda v: (len(v), v)):
800*e1fe3e4aSElliott Hughes                    name = (
801*e1fe3e4aSElliott Hughes                        "'"
802*e1fe3e4aSElliott Hughes                        + " ".join(
803*e1fe3e4aSElliott Hughes                            "%s=%s"
804*e1fe3e4aSElliott Hughes                            % (
805*e1fe3e4aSElliott Hughes                                k,
806*e1fe3e4aSElliott Hughes                                floatToFixedToStr(
807*e1fe3e4aSElliott Hughes                                    piecewiseLinearMap(v, axisMapping[k]), 14
808*e1fe3e4aSElliott Hughes                                ),
809*e1fe3e4aSElliott Hughes                            )
810*e1fe3e4aSElliott Hughes                            for k, v in locTuple
811*e1fe3e4aSElliott Hughes                        )
812*e1fe3e4aSElliott Hughes                        + "'"
813*e1fe3e4aSElliott Hughes                    )
814*e1fe3e4aSElliott Hughes                    names.append(name)
815*e1fe3e4aSElliott Hughes                    fonts.append(glyphsets[locTuple])
816*e1fe3e4aSElliott Hughes                    locations.append(dict(locTuple))
817*e1fe3e4aSElliott Hughes                args.ignore_missing = True
818*e1fe3e4aSElliott Hughes                args.inputs = []
819*e1fe3e4aSElliott Hughes
820*e1fe3e4aSElliott Hughes    if not locations:
821*e1fe3e4aSElliott Hughes        locations = [{} for _ in fonts]
822*e1fe3e4aSElliott Hughes
823*e1fe3e4aSElliott Hughes    for filename in args.inputs:
824*e1fe3e4aSElliott Hughes        if filename.endswith(".ufo"):
825*e1fe3e4aSElliott Hughes            from fontTools.ufoLib import UFOReader
826*e1fe3e4aSElliott Hughes
827*e1fe3e4aSElliott Hughes            font = UFOReader(filename)
828*e1fe3e4aSElliott Hughes            info = SimpleNamespace()
829*e1fe3e4aSElliott Hughes            font.readInfo(info)
830*e1fe3e4aSElliott Hughes            upem = info.unitsPerEm
831*e1fe3e4aSElliott Hughes            fonts.append(font)
832*e1fe3e4aSElliott Hughes        else:
833*e1fe3e4aSElliott Hughes            from fontTools.ttLib import TTFont
834*e1fe3e4aSElliott Hughes
835*e1fe3e4aSElliott Hughes            font = TTFont(filename)
836*e1fe3e4aSElliott Hughes            upem = font["head"].unitsPerEm
837*e1fe3e4aSElliott Hughes            fonts.append(font)
838*e1fe3e4aSElliott Hughes
839*e1fe3e4aSElliott Hughes        names.append(basename(filename).rsplit(".", 1)[0])
840*e1fe3e4aSElliott Hughes
841*e1fe3e4aSElliott Hughes    glyphsets = []
842*e1fe3e4aSElliott Hughes    for font in fonts:
843*e1fe3e4aSElliott Hughes        if hasattr(font, "getGlyphSet"):
844*e1fe3e4aSElliott Hughes            glyphset = font.getGlyphSet()
845*e1fe3e4aSElliott Hughes        else:
846*e1fe3e4aSElliott Hughes            glyphset = font
847*e1fe3e4aSElliott Hughes        glyphsets.append({k: glyphset[k] for k in glyphset.keys()})
848*e1fe3e4aSElliott Hughes
849*e1fe3e4aSElliott Hughes    if args.name:
850*e1fe3e4aSElliott Hughes        accepted_names = set(args.name)
851*e1fe3e4aSElliott Hughes        glyphsets = [
852*e1fe3e4aSElliott Hughes            glyphset
853*e1fe3e4aSElliott Hughes            for name, glyphset in zip(names, glyphsets)
854*e1fe3e4aSElliott Hughes            if name in accepted_names
855*e1fe3e4aSElliott Hughes        ]
856*e1fe3e4aSElliott Hughes        locations = [
857*e1fe3e4aSElliott Hughes            location
858*e1fe3e4aSElliott Hughes            for name, location in zip(names, locations)
859*e1fe3e4aSElliott Hughes            if name in accepted_names
860*e1fe3e4aSElliott Hughes        ]
861*e1fe3e4aSElliott Hughes        names = [name for name in names if name in accepted_names]
862*e1fe3e4aSElliott Hughes
863*e1fe3e4aSElliott Hughes    if not glyphs:
864*e1fe3e4aSElliott Hughes        glyphs = sorted(set([gn for glyphset in glyphsets for gn in glyphset.keys()]))
865*e1fe3e4aSElliott Hughes
866*e1fe3e4aSElliott Hughes    glyphsSet = set(glyphs)
867*e1fe3e4aSElliott Hughes    for glyphset in glyphsets:
868*e1fe3e4aSElliott Hughes        glyphSetGlyphNames = set(glyphset.keys())
869*e1fe3e4aSElliott Hughes        diff = glyphsSet - glyphSetGlyphNames
870*e1fe3e4aSElliott Hughes        if diff:
871*e1fe3e4aSElliott Hughes            for gn in diff:
872*e1fe3e4aSElliott Hughes                glyphset[gn] = None
873*e1fe3e4aSElliott Hughes
874*e1fe3e4aSElliott Hughes    # Normalize locations
875*e1fe3e4aSElliott Hughes    locations = [normalizeLocation(loc, axis_triples) for loc in locations]
876*e1fe3e4aSElliott Hughes    tolerance = args.tolerance or DEFAULT_TOLERANCE
877*e1fe3e4aSElliott Hughes    kinkiness = args.kinkiness if args.kinkiness is not None else DEFAULT_KINKINESS
878*e1fe3e4aSElliott Hughes
879*e1fe3e4aSElliott Hughes    try:
880*e1fe3e4aSElliott Hughes        log.info("Running on %d glyphsets", len(glyphsets))
881*e1fe3e4aSElliott Hughes        log.info("Locations: %s", pformat(locations))
882*e1fe3e4aSElliott Hughes        problems_gen = test_gen(
883*e1fe3e4aSElliott Hughes            glyphsets,
884*e1fe3e4aSElliott Hughes            glyphs=glyphs,
885*e1fe3e4aSElliott Hughes            names=names,
886*e1fe3e4aSElliott Hughes            locations=locations,
887*e1fe3e4aSElliott Hughes            upem=upem,
888*e1fe3e4aSElliott Hughes            ignore_missing=args.ignore_missing,
889*e1fe3e4aSElliott Hughes            tolerance=tolerance,
890*e1fe3e4aSElliott Hughes            kinkiness=kinkiness,
891*e1fe3e4aSElliott Hughes            show_all=args.show_all,
892*e1fe3e4aSElliott Hughes        )
893*e1fe3e4aSElliott Hughes        problems = defaultdict(list)
894*e1fe3e4aSElliott Hughes
895*e1fe3e4aSElliott Hughes        f = (
896*e1fe3e4aSElliott Hughes            sys.stdout
897*e1fe3e4aSElliott Hughes            if args.output is None
898*e1fe3e4aSElliott Hughes            else open(ensure_parent_dir(args.output), "w")
899*e1fe3e4aSElliott Hughes        )
900*e1fe3e4aSElliott Hughes
901*e1fe3e4aSElliott Hughes        if not args.quiet:
902*e1fe3e4aSElliott Hughes            if args.json:
903*e1fe3e4aSElliott Hughes                import json
904*e1fe3e4aSElliott Hughes
905*e1fe3e4aSElliott Hughes                for glyphname, problem in problems_gen:
906*e1fe3e4aSElliott Hughes                    problems[glyphname].append(problem)
907*e1fe3e4aSElliott Hughes
908*e1fe3e4aSElliott Hughes                print(json.dumps(problems), file=f)
909*e1fe3e4aSElliott Hughes            else:
910*e1fe3e4aSElliott Hughes                last_glyphname = None
911*e1fe3e4aSElliott Hughes                for glyphname, p in problems_gen:
912*e1fe3e4aSElliott Hughes                    problems[glyphname].append(p)
913*e1fe3e4aSElliott Hughes
914*e1fe3e4aSElliott Hughes                    if glyphname != last_glyphname:
915*e1fe3e4aSElliott Hughes                        print(f"Glyph {glyphname} was not compatible:", file=f)
916*e1fe3e4aSElliott Hughes                        last_glyphname = glyphname
917*e1fe3e4aSElliott Hughes                        last_master_idxs = None
918*e1fe3e4aSElliott Hughes
919*e1fe3e4aSElliott Hughes                    master_idxs = (
920*e1fe3e4aSElliott Hughes                        (p["master_idx"])
921*e1fe3e4aSElliott Hughes                        if "master_idx" in p
922*e1fe3e4aSElliott Hughes                        else (p["master_1_idx"], p["master_2_idx"])
923*e1fe3e4aSElliott Hughes                    )
924*e1fe3e4aSElliott Hughes                    if master_idxs != last_master_idxs:
925*e1fe3e4aSElliott Hughes                        master_names = (
926*e1fe3e4aSElliott Hughes                            (p["master"])
927*e1fe3e4aSElliott Hughes                            if "master" in p
928*e1fe3e4aSElliott Hughes                            else (p["master_1"], p["master_2"])
929*e1fe3e4aSElliott Hughes                        )
930*e1fe3e4aSElliott Hughes                        print(f"  Masters: %s:" % ", ".join(master_names), file=f)
931*e1fe3e4aSElliott Hughes                        last_master_idxs = master_idxs
932*e1fe3e4aSElliott Hughes
933*e1fe3e4aSElliott Hughes                    if p["type"] == InterpolatableProblem.MISSING:
934*e1fe3e4aSElliott Hughes                        print(
935*e1fe3e4aSElliott Hughes                            "    Glyph was missing in master %s" % p["master"], file=f
936*e1fe3e4aSElliott Hughes                        )
937*e1fe3e4aSElliott Hughes                    elif p["type"] == InterpolatableProblem.OPEN_PATH:
938*e1fe3e4aSElliott Hughes                        print(
939*e1fe3e4aSElliott Hughes                            "    Glyph has an open path in master %s" % p["master"],
940*e1fe3e4aSElliott Hughes                            file=f,
941*e1fe3e4aSElliott Hughes                        )
942*e1fe3e4aSElliott Hughes                    elif p["type"] == InterpolatableProblem.PATH_COUNT:
943*e1fe3e4aSElliott Hughes                        print(
944*e1fe3e4aSElliott Hughes                            "    Path count differs: %i in %s, %i in %s"
945*e1fe3e4aSElliott Hughes                            % (
946*e1fe3e4aSElliott Hughes                                p["value_1"],
947*e1fe3e4aSElliott Hughes                                p["master_1"],
948*e1fe3e4aSElliott Hughes                                p["value_2"],
949*e1fe3e4aSElliott Hughes                                p["master_2"],
950*e1fe3e4aSElliott Hughes                            ),
951*e1fe3e4aSElliott Hughes                            file=f,
952*e1fe3e4aSElliott Hughes                        )
953*e1fe3e4aSElliott Hughes                    elif p["type"] == InterpolatableProblem.NODE_COUNT:
954*e1fe3e4aSElliott Hughes                        print(
955*e1fe3e4aSElliott Hughes                            "    Node count differs in path %i: %i in %s, %i in %s"
956*e1fe3e4aSElliott Hughes                            % (
957*e1fe3e4aSElliott Hughes                                p["path"],
958*e1fe3e4aSElliott Hughes                                p["value_1"],
959*e1fe3e4aSElliott Hughes                                p["master_1"],
960*e1fe3e4aSElliott Hughes                                p["value_2"],
961*e1fe3e4aSElliott Hughes                                p["master_2"],
962*e1fe3e4aSElliott Hughes                            ),
963*e1fe3e4aSElliott Hughes                            file=f,
964*e1fe3e4aSElliott Hughes                        )
965*e1fe3e4aSElliott Hughes                    elif p["type"] == InterpolatableProblem.NODE_INCOMPATIBILITY:
966*e1fe3e4aSElliott Hughes                        print(
967*e1fe3e4aSElliott Hughes                            "    Node %o incompatible in path %i: %s in %s, %s in %s"
968*e1fe3e4aSElliott Hughes                            % (
969*e1fe3e4aSElliott Hughes                                p["node"],
970*e1fe3e4aSElliott Hughes                                p["path"],
971*e1fe3e4aSElliott Hughes                                p["value_1"],
972*e1fe3e4aSElliott Hughes                                p["master_1"],
973*e1fe3e4aSElliott Hughes                                p["value_2"],
974*e1fe3e4aSElliott Hughes                                p["master_2"],
975*e1fe3e4aSElliott Hughes                            ),
976*e1fe3e4aSElliott Hughes                            file=f,
977*e1fe3e4aSElliott Hughes                        )
978*e1fe3e4aSElliott Hughes                    elif p["type"] == InterpolatableProblem.CONTOUR_ORDER:
979*e1fe3e4aSElliott Hughes                        print(
980*e1fe3e4aSElliott Hughes                            "    Contour order differs: %s in %s, %s in %s"
981*e1fe3e4aSElliott Hughes                            % (
982*e1fe3e4aSElliott Hughes                                p["value_1"],
983*e1fe3e4aSElliott Hughes                                p["master_1"],
984*e1fe3e4aSElliott Hughes                                p["value_2"],
985*e1fe3e4aSElliott Hughes                                p["master_2"],
986*e1fe3e4aSElliott Hughes                            ),
987*e1fe3e4aSElliott Hughes                            file=f,
988*e1fe3e4aSElliott Hughes                        )
989*e1fe3e4aSElliott Hughes                    elif p["type"] == InterpolatableProblem.WRONG_START_POINT:
990*e1fe3e4aSElliott Hughes                        print(
991*e1fe3e4aSElliott Hughes                            "    Contour %d start point differs: %s in %s, %s in %s; reversed: %s"
992*e1fe3e4aSElliott Hughes                            % (
993*e1fe3e4aSElliott Hughes                                p["contour"],
994*e1fe3e4aSElliott Hughes                                p["value_1"],
995*e1fe3e4aSElliott Hughes                                p["master_1"],
996*e1fe3e4aSElliott Hughes                                p["value_2"],
997*e1fe3e4aSElliott Hughes                                p["master_2"],
998*e1fe3e4aSElliott Hughes                                p["reversed"],
999*e1fe3e4aSElliott Hughes                            ),
1000*e1fe3e4aSElliott Hughes                            file=f,
1001*e1fe3e4aSElliott Hughes                        )
1002*e1fe3e4aSElliott Hughes                    elif p["type"] == InterpolatableProblem.UNDERWEIGHT:
1003*e1fe3e4aSElliott Hughes                        print(
1004*e1fe3e4aSElliott Hughes                            "    Contour %d interpolation is underweight: %s, %s"
1005*e1fe3e4aSElliott Hughes                            % (
1006*e1fe3e4aSElliott Hughes                                p["contour"],
1007*e1fe3e4aSElliott Hughes                                p["master_1"],
1008*e1fe3e4aSElliott Hughes                                p["master_2"],
1009*e1fe3e4aSElliott Hughes                            ),
1010*e1fe3e4aSElliott Hughes                            file=f,
1011*e1fe3e4aSElliott Hughes                        )
1012*e1fe3e4aSElliott Hughes                    elif p["type"] == InterpolatableProblem.OVERWEIGHT:
1013*e1fe3e4aSElliott Hughes                        print(
1014*e1fe3e4aSElliott Hughes                            "    Contour %d interpolation is overweight: %s, %s"
1015*e1fe3e4aSElliott Hughes                            % (
1016*e1fe3e4aSElliott Hughes                                p["contour"],
1017*e1fe3e4aSElliott Hughes                                p["master_1"],
1018*e1fe3e4aSElliott Hughes                                p["master_2"],
1019*e1fe3e4aSElliott Hughes                            ),
1020*e1fe3e4aSElliott Hughes                            file=f,
1021*e1fe3e4aSElliott Hughes                        )
1022*e1fe3e4aSElliott Hughes                    elif p["type"] == InterpolatableProblem.KINK:
1023*e1fe3e4aSElliott Hughes                        print(
1024*e1fe3e4aSElliott Hughes                            "    Contour %d has a kink at %s: %s, %s"
1025*e1fe3e4aSElliott Hughes                            % (
1026*e1fe3e4aSElliott Hughes                                p["contour"],
1027*e1fe3e4aSElliott Hughes                                p["value"],
1028*e1fe3e4aSElliott Hughes                                p["master_1"],
1029*e1fe3e4aSElliott Hughes                                p["master_2"],
1030*e1fe3e4aSElliott Hughes                            ),
1031*e1fe3e4aSElliott Hughes                            file=f,
1032*e1fe3e4aSElliott Hughes                        )
1033*e1fe3e4aSElliott Hughes                    elif p["type"] == InterpolatableProblem.NOTHING:
1034*e1fe3e4aSElliott Hughes                        print(
1035*e1fe3e4aSElliott Hughes                            "    Showing %s and %s"
1036*e1fe3e4aSElliott Hughes                            % (
1037*e1fe3e4aSElliott Hughes                                p["master_1"],
1038*e1fe3e4aSElliott Hughes                                p["master_2"],
1039*e1fe3e4aSElliott Hughes                            ),
1040*e1fe3e4aSElliott Hughes                            file=f,
1041*e1fe3e4aSElliott Hughes                        )
1042*e1fe3e4aSElliott Hughes        else:
1043*e1fe3e4aSElliott Hughes            for glyphname, problem in problems_gen:
1044*e1fe3e4aSElliott Hughes                problems[glyphname].append(problem)
1045*e1fe3e4aSElliott Hughes
1046*e1fe3e4aSElliott Hughes        problems = sort_problems(problems)
1047*e1fe3e4aSElliott Hughes
1048*e1fe3e4aSElliott Hughes        for p in "ps", "pdf":
1049*e1fe3e4aSElliott Hughes            arg = getattr(args, p)
1050*e1fe3e4aSElliott Hughes            if arg is None:
1051*e1fe3e4aSElliott Hughes                continue
1052*e1fe3e4aSElliott Hughes            log.info("Writing %s to %s", p.upper(), arg)
1053*e1fe3e4aSElliott Hughes            from .interpolatablePlot import InterpolatablePS, InterpolatablePDF
1054*e1fe3e4aSElliott Hughes
1055*e1fe3e4aSElliott Hughes            PlotterClass = InterpolatablePS if p == "ps" else InterpolatablePDF
1056*e1fe3e4aSElliott Hughes
1057*e1fe3e4aSElliott Hughes            with PlotterClass(
1058*e1fe3e4aSElliott Hughes                ensure_parent_dir(arg), glyphsets=glyphsets, names=names
1059*e1fe3e4aSElliott Hughes            ) as doc:
1060*e1fe3e4aSElliott Hughes                doc.add_title_page(
1061*e1fe3e4aSElliott Hughes                    original_args_inputs, tolerance=tolerance, kinkiness=kinkiness
1062*e1fe3e4aSElliott Hughes                )
1063*e1fe3e4aSElliott Hughes                if problems:
1064*e1fe3e4aSElliott Hughes                    doc.add_summary(problems)
1065*e1fe3e4aSElliott Hughes                doc.add_problems(problems)
1066*e1fe3e4aSElliott Hughes                if not problems and not args.quiet:
1067*e1fe3e4aSElliott Hughes                    doc.draw_cupcake()
1068*e1fe3e4aSElliott Hughes                if problems:
1069*e1fe3e4aSElliott Hughes                    doc.add_index()
1070*e1fe3e4aSElliott Hughes                    doc.add_table_of_contents()
1071*e1fe3e4aSElliott Hughes
1072*e1fe3e4aSElliott Hughes        if args.html:
1073*e1fe3e4aSElliott Hughes            log.info("Writing HTML to %s", args.html)
1074*e1fe3e4aSElliott Hughes            from .interpolatablePlot import InterpolatableSVG
1075*e1fe3e4aSElliott Hughes
1076*e1fe3e4aSElliott Hughes            svgs = []
1077*e1fe3e4aSElliott Hughes            glyph_starts = {}
1078*e1fe3e4aSElliott Hughes            with InterpolatableSVG(svgs, glyphsets=glyphsets, names=names) as svg:
1079*e1fe3e4aSElliott Hughes                svg.add_title_page(
1080*e1fe3e4aSElliott Hughes                    original_args_inputs,
1081*e1fe3e4aSElliott Hughes                    show_tolerance=False,
1082*e1fe3e4aSElliott Hughes                    tolerance=tolerance,
1083*e1fe3e4aSElliott Hughes                    kinkiness=kinkiness,
1084*e1fe3e4aSElliott Hughes                )
1085*e1fe3e4aSElliott Hughes                for glyph, glyph_problems in problems.items():
1086*e1fe3e4aSElliott Hughes                    glyph_starts[len(svgs)] = glyph
1087*e1fe3e4aSElliott Hughes                    svg.add_problems(
1088*e1fe3e4aSElliott Hughes                        {glyph: glyph_problems},
1089*e1fe3e4aSElliott Hughes                        show_tolerance=False,
1090*e1fe3e4aSElliott Hughes                        show_page_number=False,
1091*e1fe3e4aSElliott Hughes                    )
1092*e1fe3e4aSElliott Hughes                if not problems and not args.quiet:
1093*e1fe3e4aSElliott Hughes                    svg.draw_cupcake()
1094*e1fe3e4aSElliott Hughes
1095*e1fe3e4aSElliott Hughes            import base64
1096*e1fe3e4aSElliott Hughes
1097*e1fe3e4aSElliott Hughes            with open(ensure_parent_dir(args.html), "wb") as f:
1098*e1fe3e4aSElliott Hughes                f.write(b"<!DOCTYPE html>\n")
1099*e1fe3e4aSElliott Hughes                f.write(
1100*e1fe3e4aSElliott Hughes                    b'<html><body align="center" style="font-family: sans-serif; text-color: #222">\n'
1101*e1fe3e4aSElliott Hughes                )
1102*e1fe3e4aSElliott Hughes                f.write(b"<title>fonttools varLib.interpolatable report</title>\n")
1103*e1fe3e4aSElliott Hughes                for i, svg in enumerate(svgs):
1104*e1fe3e4aSElliott Hughes                    if i in glyph_starts:
1105*e1fe3e4aSElliott Hughes                        f.write(f"<h1>Glyph {glyph_starts[i]}</h1>\n".encode("utf-8"))
1106*e1fe3e4aSElliott Hughes                    f.write("<img src='data:image/svg+xml;base64,".encode("utf-8"))
1107*e1fe3e4aSElliott Hughes                    f.write(base64.b64encode(svg))
1108*e1fe3e4aSElliott Hughes                    f.write(b"' />\n")
1109*e1fe3e4aSElliott Hughes                    f.write(b"<hr>\n")
1110*e1fe3e4aSElliott Hughes                f.write(b"</body></html>\n")
1111*e1fe3e4aSElliott Hughes
1112*e1fe3e4aSElliott Hughes    except Exception as e:
1113*e1fe3e4aSElliott Hughes        e.args += original_args_inputs
1114*e1fe3e4aSElliott Hughes        log.error(e)
1115*e1fe3e4aSElliott Hughes        raise
1116*e1fe3e4aSElliott Hughes
1117*e1fe3e4aSElliott Hughes    if problems:
1118*e1fe3e4aSElliott Hughes        return problems
1119*e1fe3e4aSElliott Hughes
1120*e1fe3e4aSElliott Hughes
1121*e1fe3e4aSElliott Hughesif __name__ == "__main__":
1122*e1fe3e4aSElliott Hughes    import sys
1123*e1fe3e4aSElliott Hughes
1124*e1fe3e4aSElliott Hughes    problems = main()
1125*e1fe3e4aSElliott Hughes    sys.exit(int(bool(problems)))
1126