# Source code for fontTools.varLib.interpolatable

    """
    Tool to find wrong contour order between different masters, and
    other interpolatability (or lack thereof) issues.

    Call as:
    $ fonttools varLib.interpolatable font1 font2 ...
    """

    from .interpolatableHelpers import *
    from .interpolatableTestContourOrder import test_contour_order
    from .interpolatableTestStartingPoint import test_starting_point
    from fontTools.pens.recordingPen import (
        RecordingPen,
        DecomposingRecordingPen,
        lerpRecordings,
    )
    from fontTools.pens.transformPen import TransformPen
    from fontTools.pens.statisticsPen import StatisticsPen, StatisticsControlPen
    from fontTools.pens.momentsPen import OpenContourError
    from fontTools.varLib.models import piecewiseLinearMap, normalizeLocation
    from fontTools.misc.fixedTools import floatToFixedToStr
    from fontTools.misc.transform import Transform
    from collections import defaultdict
    from types import SimpleNamespace
    from functools import wraps
    from pprint import pformat
    from math import sqrt, atan2, pi
    import logging
    import os

    log = logging.getLogger("fontTools.varLib.interpolatable")

    DEFAULT_TOLERANCE = 0.95
    DEFAULT_KINKINESS = 0.5
    DEFAULT_KINKINESS_LENGTH = 0.002  # ratio of UPEM
    DEFAULT_UPEM = 1000

    class Glyph:
        ITEMS = (
            "recordings",
            "greenStats",
            "controlStats",
            "greenVectors",
            "controlVectors",
            "nodeTypes",
            "isomorphisms",
            "points",
            "openContours",
        )

        def __init__(self, glyphname, glyphset):
            self.name = glyphname
            for item in self.ITEMS:
                setattr(self, item, [])
            self._populate(glyphset)

        def _fill_in(self, ix):
            for item in self.ITEMS:
                if len(getattr(self, item)) == ix:
                    getattr(self, item).append(None)

        def _populate(self, glyphset):
            glyph = glyphset[self.name]
            self.doesnt_exist = glyph is None
            if self.doesnt_exist:
                return

            perContourPen = PerContourOrComponentPen(RecordingPen, glyphset=glyphset)
            try:
                glyph.draw(perContourPen, outputImpliedClosingLine=True)
            except TypeError:
                glyph.draw(perContourPen)
            self.recordings = perContourPen.value
            del perContourPen

            for ix, contour in enumerate(self.recordings):
                nodeTypes = [op for op, arg in contour.value]
                self.nodeTypes.append(nodeTypes)

                greenStats = StatisticsPen(glyphset=glyphset)
                controlStats = StatisticsControlPen(glyphset=glyphset)
                try:
                    contour.replay(greenStats)
                    contour.replay(controlStats)
                    self.openContours.append(False)
                except OpenContourError as e:
                    self.openContours.append(True)
                    self._fill_in(ix)
                    continue
                self.greenStats.append(greenStats)
                self.controlStats.append(controlStats)
                self.greenVectors.append(contour_vector_from_stats(greenStats))
                self.controlVectors.append(contour_vector_from_stats(controlStats))

                # Check starting point
                if nodeTypes[0] == "addComponent":
                    self._fill_in(ix)
                    continue

                assert nodeTypes[0] == "moveTo"
                assert nodeTypes[-1] in ("closePath", "endPath")
                points = SimpleRecordingPointPen()
                converter = SegmentToPointPen(points, False)
                contour.replay(converter)
                # points.value is a list of pt,bool where bool is true if on-curve and false if off-curve;
                # now check all rotations and mirror-rotations of the contour and build list of isomorphic
                # possible starting points.
                self.points.append(points.value)

                isomorphisms = []
                self.isomorphisms.append(isomorphisms)

                # Add rotations
                add_isomorphisms(points.value, isomorphisms, False)
                # Add mirrored rotations
                add_isomorphisms(points.value, isomorphisms, True)

        def draw(self, pen, countor_idx=None):
            if countor_idx is None:
                for contour in self.recordings:
                    contour.draw(pen)
            else:
                self.recordings[countor_idx].draw(pen)

    def test_gen(
        glyphsets,
        glyphs=None,
        names=None,
        ignore_missing=False,
        *,
        locations=None,
        tolerance=DEFAULT_TOLERANCE,
        kinkiness=DEFAULT_KINKINESS,
        upem=DEFAULT_UPEM,
        show_all=False,
        discrete_axes=[],
    ):
        if tolerance >= 10:
            tolerance *= 0.01
        assert 0 <= tolerance <= 1
        if kinkiness >= 10:
            kinkiness *= 0.01
        assert 0 <= kinkiness

        names = names or [repr(g) for g in glyphsets]

        if glyphs is None:
            # `glyphs = glyphsets[0].keys()` is faster, certainly, but doesn't allow for sparse TTFs/OTFs given out of order
            # ... risks the sparse master being the first one, and only processing a subset of the glyphs
            glyphs = {g for glyphset in glyphsets for g in glyphset.keys()}

        parents, order = find_parents_and_order(
            glyphsets, locations, discrete_axes=discrete_axes
        )

        def grand_parent(i, glyphname):
            if i is None:
                return None
            i = parents[i]
            if i is None:
                return None
            while parents[i] is not None and glyphsets[i][glyphname] is None:
                i = parents[i]
            return i

        for glyph_name in glyphs:
            log.info("Testing glyph %s", glyph_name)
            allGlyphs = [Glyph(glyph_name, glyphset) for glyphset in glyphsets]
            if len([1 for glyph in allGlyphs if glyph is not None]) <= 1:
                continue
            for master_idx, (glyph, glyphset, name) in enumerate(
                zip(allGlyphs, glyphsets, names)
            ):
                if glyph.doesnt_exist:
                    if not ignore_missing:
                        yield (
                            glyph_name,
                            {
                                "type": InterpolatableProblem.MISSING,
                                "master": name,
                                "master_idx": master_idx,
                            },
                        )
                    continue

                has_open = False
                for ix, open in enumerate(glyph.openContours):
                    if not open:
                        continue
                    has_open = True
                    yield (
                        glyph_name,
                        {
                            "type": InterpolatableProblem.OPEN_PATH,
                            "master": name,
                            "master_idx": master_idx,
                            "contour": ix,
                        },
                    )
                if has_open:
                    continue

            matchings = [None] * len(glyphsets)

            for m1idx in order:
                glyph1 = allGlyphs[m1idx]
                if glyph1 is None or not glyph1.nodeTypes:
                    continue
                m0idx = grand_parent(m1idx, glyph_name)
                if m0idx is None:
                    continue
                glyph0 = allGlyphs[m0idx]
                if glyph0 is None or not glyph0.nodeTypes:
                    continue

                #
                # Basic compatibility checks
                #

                m1 = glyph0.nodeTypes
                m0 = glyph1.nodeTypes
                if len(m0) != len(m1):
                    yield (
                        glyph_name,
                        {
                            "type": InterpolatableProblem.PATH_COUNT,
                            "master_1": names[m0idx],
                            "master_2": names[m1idx],
                            "master_1_idx": m0idx,
                            "master_2_idx": m1idx,
                            "value_1": len(m0),
                            "value_2": len(m1),
                        },
                    )
                    continue

                if m0 != m1:
                    for pathIx, (nodes1, nodes2) in enumerate(zip(m0, m1)):
                        if nodes1 == nodes2:
                            continue
                        if len(nodes1) != len(nodes2):
                            yield (
                                glyph_name,
                                {
                                    "type": InterpolatableProblem.NODE_COUNT,
                                    "path": pathIx,
                                    "master_1": names[m0idx],
                                    "master_2": names[m1idx],
                                    "master_1_idx": m0idx,
                                    "master_2_idx": m1idx,
                                    "value_1": len(nodes1),
                                    "value_2": len(nodes2),
                                },
                            )
                            continue
                        for nodeIx, (n1, n2) in enumerate(zip(nodes1, nodes2)):
                            if n1 != n2:
                                yield (
                                    glyph_name,
                                    {
                                        "type": InterpolatableProblem.NODE_INCOMPATIBILITY,
                                        "path": pathIx,
                                        "node": nodeIx,
                                        "master_1": names[m0idx],
                                        "master_2": names[m1idx],
                                        "master_1_idx": m0idx,
                                        "master_2_idx": m1idx,
                                        "value_1": n1,
                                        "value_2": n2,
                                    },
                                )
                                continue

                #
                # InterpolatableProblem.CONTOUR_ORDER check
                #

                this_tolerance, matching = test_contour_order(glyph0, glyph1)
                if this_tolerance < tolerance:
                    yield (
                        glyph_name,
                        {
                            "type": InterpolatableProblem.CONTOUR_ORDER,
                            "master_1": names[m0idx],
                            "master_2": names[m1idx],
                            "master_1_idx": m0idx,
                            "master_2_idx": m1idx,
                            "value_1": list(range(len(matching))),
                            "value_2": matching,
                            "tolerance": this_tolerance,
                        },
                    )
                    matchings[m1idx] = matching

                #
                # wrong-start-point / weight check
                #

                m0Isomorphisms = glyph0.isomorphisms
                m1Isomorphisms = glyph1.isomorphisms
                m0Vectors = glyph0.greenVectors
                m1Vectors = glyph1.greenVectors
                recording0 = glyph0.recordings
                recording1 = glyph1.recordings

                # If contour-order is wrong, adjust it
                matching = matchings[m1idx]
                if (
                    matching is not None and m1Isomorphisms
                ):  # m1 is empty for composite glyphs
                    m1Isomorphisms = [m1Isomorphisms[i] for i in matching]
                    m1Vectors = [m1Vectors[i] for i in matching]
                    recording1 = [recording1[i] for i in matching]

                midRecording = []
                for c0, c1 in zip(recording0, recording1):
                    try:
                        r = RecordingPen()
                        r.value = list(lerpRecordings(c0.value, c1.value))
                        midRecording.append(r)
                    except ValueError:
                        # Mismatch because of the reordering above
                        midRecording.append(None)

                for ix, (contour0, contour1) in enumerate(
                    zip(m0Isomorphisms, m1Isomorphisms)
                ):
                    if (
                        contour0 is None
                        or contour1 is None
                        or len(contour0) == 0
                        or len(contour0) != len(contour1)
                    ):
                        # We already reported this; or nothing to do; or not compatible
                        # after reordering above.
                        continue

                    this_tolerance, proposed_point, reverse = test_starting_point(
                        glyph0, glyph1, ix, tolerance, matching
                    )

                    if this_tolerance < tolerance:
                        yield (
                            glyph_name,
                            {
                                "type": InterpolatableProblem.WRONG_START_POINT,
                                "contour": ix,
                                "master_1": names[m0idx],
                                "master_2": names[m1idx],
                                "master_1_idx": m0idx,
                                "master_2_idx": m1idx,
                                "value_1": 0,
                                "value_2": proposed_point,
                                "reversed": reverse,
                                "tolerance": this_tolerance,
                            },
                        )

                    # Weight check.
                    #
                    # If contour could be mid-interpolated, and the two
                    # contours have the same area sign, proceeed.
                    #
                    # The sign difference can happen if it's a weirdo
                    # self-intersecting contour; ignore it.
                    contour = midRecording[ix]

                    if contour and (m0Vectors[ix][0] < 0) == (m1Vectors[ix][0] < 0):
                        midStats = StatisticsPen(glyphset=None)
                        contour.replay(midStats)

                        midVector = contour_vector_from_stats(midStats)

                        m0Vec = m0Vectors[ix]
                        m1Vec = m1Vectors[ix]
                        size0 = m0Vec[0] * m0Vec[0]
                        size1 = m1Vec[0] * m1Vec[0]
                        midSize = midVector[0] * midVector[0]

                        for overweight, problem_type in enumerate(
                            (
                                InterpolatableProblem.UNDERWEIGHT,
                                InterpolatableProblem.OVERWEIGHT,
                            )
                        ):
                            if overweight:
                                expectedSize = max(size0, size1)
                                continue
                            else:
                                expectedSize = sqrt(size0 * size1)

                            log.debug(
                                "%s: actual size %g; threshold size %g, master sizes: %g, %g",
                                problem_type,
                                midSize,
                                expectedSize,
                                size0,
                                size1,
                            )

                            if (
                                not overweight and expectedSize * tolerance > midSize + 1e-5
                            ) or (overweight and 1e-5 + expectedSize / tolerance < midSize):
                                try:
                                    if overweight:
                                        this_tolerance = expectedSize / midSize
                                    else:
                                        this_tolerance = midSize / expectedSize
                                except ZeroDivisionError:
                                    this_tolerance = 0
                                log.debug("tolerance %g", this_tolerance)
                                yield (
                                    glyph_name,
                                    {
                                        "type": problem_type,
                                        "contour": ix,
                                        "master_1": names[m0idx],
                                        "master_2": names[m1idx],
                                        "master_1_idx": m0idx,
                                        "master_2_idx": m1idx,
                                        "tolerance": this_tolerance,
                                    },
                                )

                #
                # "kink" detector
                #
                m0 = glyph0.points
                m1 = glyph1.points

                # If contour-order is wrong, adjust it
                if matchings[m1idx] is not None and m1:  # m1 is empty for composite glyphs
                    m1 = [m1[i] for i in matchings[m1idx]]

                t = 0.1  # ~sin(radian(6)) for tolerance 0.95
                deviation_threshold = (
                    upem * DEFAULT_KINKINESS_LENGTH * DEFAULT_KINKINESS / kinkiness
                )

                for ix, (contour0, contour1) in enumerate(zip(m0, m1)):
                    if (
                        contour0 is None
                        or contour1 is None
                        or len(contour0) == 0
                        or len(contour0) != len(contour1)
                    ):
                        # We already reported this; or nothing to do; or not compatible
                        # after reordering above.
                        continue

                    # Walk the contour, keeping track of three consecutive points, with
                    # middle one being an on-curve. If the three are co-linear then
                    # check for kinky-ness.
                    for i in range(len(contour0)):
                        pt0 = contour0[i]
                        pt1 = contour1[i]
                        if not pt0[1] or not pt1[1]:
                            # Skip off-curves
                            continue
                        pt0_prev = contour0[i - 1]
                        pt1_prev = contour1[i - 1]
                        pt0_next = contour0[(i + 1) % len(contour0)]
                        pt1_next = contour1[(i + 1) % len(contour1)]

                        if pt0_prev[1] and pt1_prev[1]:
                            # At least one off-curve is required
                            continue
                        if pt0_prev[1] and pt1_prev[1]:
                            # At least one off-curve is required
                            continue

                        pt0 = complex(*pt0[0])
                        pt1 = complex(*pt1[0])
                        pt0_prev = complex(*pt0_prev[0])
                        pt1_prev = complex(*pt1_prev[0])
                        pt0_next = complex(*pt0_next[0])
                        pt1_next = complex(*pt1_next[0])

                        # We have three consecutive points. Check whether
                        # they are colinear.
                        d0_prev = pt0 - pt0_prev
                        d0_next = pt0_next - pt0
                        d1_prev = pt1 - pt1_prev
                        d1_next = pt1_next - pt1

                        sin0 = d0_prev.real * d0_next.imag - d0_prev.imag * d0_next.real
                        sin1 = d1_prev.real * d1_next.imag - d1_prev.imag * d1_next.real
                        try:
                            sin0 /= abs(d0_prev) * abs(d0_next)
                            sin1 /= abs(d1_prev) * abs(d1_next)
                        except ZeroDivisionError:
                            continue

                        if abs(sin0) > t or abs(sin1) > t:
                            # Not colinear / not smooth.
                            continue

                        # Check the mid-point is actually, well, in the middle.
                        dot0 = d0_prev.real * d0_next.real + d0_prev.imag * d0_next.imag
                        dot1 = d1_prev.real * d1_next.real + d1_prev.imag * d1_next.imag
                        if dot0 < 0 or dot1 < 0:
                            # Sharp corner.
                            continue

                        # Fine, if handle ratios are similar...
                        r0 = abs(d0_prev) / (abs(d0_prev) + abs(d0_next))
                        r1 = abs(d1_prev) / (abs(d1_prev) + abs(d1_next))
                        r_diff = abs(r0 - r1)
                        if abs(r_diff) < t:
                            # Smooth enough.
                            continue

                        mid = (pt0 + pt1) / 2
                        mid_prev = (pt0_prev + pt1_prev) / 2
                        mid_next = (pt0_next + pt1_next) / 2

                        mid_d0 = mid - mid_prev
                        mid_d1 = mid_next - mid

                        sin_mid = mid_d0.real * mid_d1.imag - mid_d0.imag * mid_d1.real
                        try:
                            sin_mid /= abs(mid_d0) * abs(mid_d1)
                        except ZeroDivisionError:
                            continue

                        # ...or if the angles are similar.
                        if abs(sin_mid) * (tolerance * kinkiness) <= t:
                            # Smooth enough.
                            continue

                        # How visible is the kink?

                        cross = sin_mid * abs(mid_d0) * abs(mid_d1)
                        arc_len = abs(mid_d0 + mid_d1)
                        deviation = abs(cross / arc_len)
                        if deviation < deviation_threshold:
                            continue
                        deviation_ratio = deviation / arc_len
                        if deviation_ratio > t:
                            continue

                        this_tolerance = t / (abs(sin_mid) * kinkiness)

                        log.debug(
                            "kink: deviation %g; deviation_ratio %g; sin_mid %g; r_diff %g",
                            deviation,
                            deviation_ratio,
                            sin_mid,
                            r_diff,
                        )
                        log.debug("tolerance %g", this_tolerance)
                        yield (
                            glyph_name,
                            {
                                "type": InterpolatableProblem.KINK,
                                "contour": ix,
                                "master_1": names[m0idx],
                                "master_2": names[m1idx],
                                "master_1_idx": m0idx,
                                "master_2_idx": m1idx,
                                "value": i,
                                "tolerance": this_tolerance,
                            },
                        )

                #
                # --show-all
                #

                if show_all:
                    yield (
                        glyph_name,
                        {
                            "type": InterpolatableProblem.NOTHING,
                            "master_1": names[m0idx],
                            "master_2": names[m1idx],
                            "master_1_idx": m0idx,
                            "master_2_idx": m1idx,
                        },
                    )

    @wraps(test_gen)
    def test(*args, **kwargs):
        problems = defaultdict(list)
        for glyphname, problem in test_gen(*args, **kwargs):
            problems[glyphname].append(problem)
        return problems

    def recursivelyAddGlyph(glyphname, glyphset, ttGlyphSet, glyf):
        if glyphname in glyphset:
            return
        glyphset[glyphname] = ttGlyphSet[glyphname]

        for component in getattr(glyf[glyphname], "components", []):
            recursivelyAddGlyph(component.glyphName, glyphset, ttGlyphSet, glyf)

    def ensure_parent_dir(path):
        dirname = os.path.dirname(path)
        if dirname:
            os.makedirs(dirname, exist_ok=True)
        return path

    def main(args=None):
        """Test for interpolatability issues between fonts"""
        import argparse
        import sys

        parser = argparse.ArgumentParser(
            "fonttools varLib.interpolatable",
            description=main.__doc__,
        )
        parser.add_argument(
            "--glyphs",
            action="store",
            help="Space-separate name of glyphs to check",
        )
        parser.add_argument(
            "--show-all",
            action="store_true",
            help="Show all glyph pairs, even if no problems are found",
        )
        parser.add_argument(
            "--tolerance",
            action="store",
            type=float,
            help="Error tolerance. Between 0 and 1. Default %s" % DEFAULT_TOLERANCE,
        )
        parser.add_argument(
            "--kinkiness",
            action="store",
            type=float,
            help="How aggressively report kinks. Default %s" % DEFAULT_KINKINESS,
        )
        parser.add_argument(
            "--json",
            action="store_true",
            help="Output report in JSON format",
        )
        parser.add_argument(
            "--pdf",
            action="store",
            help="Output report in PDF format",
        )
        parser.add_argument(
            "--ps",
            action="store",
            help="Output report in PostScript format",
        )
        parser.add_argument(
            "--html",
            action="store",
            help="Output report in HTML format",
        )
        parser.add_argument(
            "--quiet",
            action="store_true",
            help="Only exit with code 1 or 0, no output",
        )
        parser.add_argument(
            "--output",
            action="store",
            help="Output file for the problem report; Default: stdout",
        )
        parser.add_argument(
            "--ignore-missing",
            action="store_true",
            help="Will not report glyphs missing from sparse masters as errors",
        )
        parser.add_argument(
            "inputs",
            metavar="FILE",
            type=str,
            nargs="+",
            help="Input a single variable font / DesignSpace / Glyphs file, or multiple TTF/UFO files",
        )
        parser.add_argument(
            "--name",
            metavar="NAME",
            type=str,
            action="append",
            help="Name of the master to use in the report. If not provided, all are used.",
        )
        parser.add_argument("-v", "--verbose", action="store_true", help="Run verbosely.")
        parser.add_argument("--debug", action="store_true", help="Run with debug output.")

        args = parser.parse_args(args)

        from fontTools import configLogger

        configLogger(level=("INFO" if args.verbose else "WARNING"))
        if args.debug:
            configLogger(level="DEBUG")

        glyphs = args.glyphs.split() if args.glyphs else None

        from os.path import basename

        fonts = []
        names = []
        locations = []
        discrete_axes = set()
        upem = DEFAULT_UPEM

        original_args_inputs = tuple(args.inputs)

        if len(args.inputs) == 1:
            designspace = None
            if args.inputs[0].endswith(".designspace"):
                from fontTools.designspaceLib import DesignSpaceDocument

                designspace = DesignSpaceDocument.fromfile(args.inputs[0])
                args.inputs = [master.path for master in designspace.sources]
                locations = [master.location for master in designspace.sources]
                discrete_axes = {
                    a.name for a in designspace.axes if not hasattr(a, "minimum")
                }
                axis_triples = {
                    a.name: (a.minimum, a.default, a.maximum)
                    for a in designspace.axes
                    if a.name not in discrete_axes
                }
                axis_mappings = {a.name: a.map for a in designspace.axes}
                axis_triples = {
                    k: tuple(piecewiseLinearMap(v, dict(axis_mappings[k])) for v in vv)
                    for k, vv in axis_triples.items()
                }

            elif args.inputs[0].endswith((".glyphs", ".glyphspackage")):
                from glyphsLib import GSFont, to_designspace

                gsfont = GSFont(args.inputs[0])
                upem = gsfont.upm
                designspace = to_designspace(gsfont)
                fonts = [source.font for source in designspace.sources]
                names = ["%s-%s" % (f.info.familyName, f.info.styleName) for f in fonts]
                args.inputs = []
                locations = [master.location for master in designspace.sources]
                axis_triples = {
                    a.name: (a.minimum, a.default, a.maximum) for a in designspace.axes
                }
                axis_mappings = {a.name: a.map for a in designspace.axes}
                axis_triples = {
                    k: tuple(piecewiseLinearMap(v, dict(axis_mappings[k])) for v in vv)
                    for k, vv in axis_triples.items()
                }

            elif args.inputs[0].endswith(".ttf") or args.inputs[0].endswith(".otf"):
                from fontTools.ttLib import TTFont

                # Is variable font?

                font = TTFont(args.inputs[0])
                upem = font["head"].unitsPerEm

                fvar = font["fvar"]
                axisMapping = {}
                for axis in fvar.axes:
                    axisMapping[axis.axisTag] = {
                        -1: axis.minValue,
                        0: axis.defaultValue,
                        1: axis.maxValue,
                    }
                normalized = False
                if "avar" in font:
                    avar = font["avar"]
                    if getattr(avar.table, "VarStore", None):
                        axisMapping = {tag: {-1: -1, 0: 0, 1: 1} for tag in axisMapping}
                        normalized = True
                    else:
                        for axisTag, segments in avar.segments.items():
                            fvarMapping = axisMapping[axisTag].copy()
                            for location, value in segments.items():
                                axisMapping[axisTag][value] = piecewiseLinearMap(
                                    location, fvarMapping
                                )

                # Gather all glyphs at their "master" locations
                ttGlyphSets = {}
                glyphsets = defaultdict(dict)

                if "gvar" in font:
                    gvar = font["gvar"]
                    glyf = font["glyf"]

                    if glyphs is None:
                        glyphs = sorted(gvar.variations.keys())
                    for glyphname in glyphs:
                        for var in gvar.variations[glyphname]:
                            locDict = {}
                            loc = []
                            for tag, val in sorted(var.axes.items()):
                                locDict[tag] = val[1]
                                loc.append((tag, val[1]))

                            locTuple = tuple(loc)
                            if locTuple not in ttGlyphSets:
                                ttGlyphSets[locTuple] = font.getGlyphSet(
                                    location=locDict, normalized=True, recalcBounds=False
                                )

                            recursivelyAddGlyph(
                                glyphname, glyphsets[locTuple], ttGlyphSets[locTuple], glyf
                            )

                elif "CFF2" in font:
                    fvarAxes = font["fvar"].axes
                    cff2 = font["CFF2"].cff.topDictIndex[0]
                    charstrings = cff2.CharStrings

                    if glyphs is None:
                        glyphs = sorted(charstrings.keys())
                    for glyphname in glyphs:
                        cs = charstrings[glyphname]
                        private = cs.private

                        # Extract vsindex for the glyph
                        vsindices = {getattr(private, "vsindex", 0)}
                        vsindex = getattr(private, "vsindex", 0)
                        last_op = 0
                        # The spec says vsindex can only appear once and must be the first
                        # operator in the charstring, but we support multiple.
                        # https://github.com/harfbuzz/boring-expansion-spec/issues/158
                        for op in enumerate(cs.program):
                            if op == "blend":
                                vsindices.add(vsindex)
                            elif op == "vsindex":
                                assert isinstance(last_op, int)
                                vsindex = last_op
                            last_op = op

                        if not hasattr(private, "vstore"):
                            continue

                        varStore = private.vstore.otVarStore
                        for vsindex in vsindices:
                            varData = varStore.VarData[vsindex]
                            for regionIndex in varData.VarRegionIndex:
                                region = varStore.VarRegionList.Region[regionIndex]

                                locDict = {}
                                loc = []
                                for axisIndex, axis in enumerate(region.VarRegionAxis):
                                    tag = fvarAxes[axisIndex].axisTag
                                    val = axis.PeakCoord
                                    locDict[tag] = val
                                    loc.append((tag, val))

                                locTuple = tuple(loc)
                                if locTuple not in ttGlyphSets:
                                    ttGlyphSets[locTuple] = font.getGlyphSet(
                                        location=locDict,
                                        normalized=True,
                                        recalcBounds=False,
                                    )

                                glyphset = glyphsets[locTuple]
                                glyphset[glyphname] = ttGlyphSets[locTuple][glyphname]

                names = ["''"]
                fonts = [font.getGlyphSet()]
                locations = [{}]
                axis_triples = {a: (-1, 0, +1) for a in sorted(axisMapping.keys())}
                for locTuple in sorted(glyphsets.keys(), key=lambda v: (len(v), v)):
                    name = (
                        "'"
                        + " ".join(
                            "%s=%s"
                            % (
                                k,
                                floatToFixedToStr(
                                    piecewiseLinearMap(v, axisMapping[k]), 14
                                ),
                            )
                            for k, v in locTuple
                        )
                        + "'"
                    )
                    if normalized:
                        name += " (normalized)"
                    names.append(name)
                    fonts.append(glyphsets[locTuple])
                    locations.append(dict(locTuple))

                args.ignore_missing = True
                args.inputs = []

        if not locations:
            locations = [{} for _ in fonts]

        for filename in args.inputs:
            if filename.endswith(".ufo"):
                from fontTools.ufoLib import UFOReader

                font = UFOReader(filename)
                info = SimpleNamespace()
                font.readInfo(info)
                upem = info.unitsPerEm
                fonts.append(font)
            else:
                from fontTools.ttLib import TTFont

                font = TTFont(filename)
                upem = font["head"].unitsPerEm
                fonts.append(font)

            names.append(basename(filename).rsplit(".", 1)[0])

        if len(fonts) < 2:
            log.warning("Font file does not seem to be variable. Nothing to check.")
            return

        glyphsets = []
        for font in fonts:
            if hasattr(font, "getGlyphSet"):
                glyphset = font.getGlyphSet()
            else:
                glyphset = font
            glyphsets.append({k: glyphset[k] for k in glyphset.keys()})

        if args.name:
            accepted_names = set(args.name)
            glyphsets = [
                glyphset
                for name, glyphset in zip(names, glyphsets)
                if name in accepted_names
            ]
            locations = [
                location
                for name, location in zip(names, locations)
                if name in accepted_names
            ]
            names = [name for name in names if name in accepted_names]

        if not glyphs:
            glyphs = sorted(set([gn for glyphset in glyphsets for gn in glyphset.keys()]))

        glyphsSet = set(glyphs)
        for glyphset in glyphsets:
            glyphSetGlyphNames = set(glyphset.keys())
            diff = glyphsSet - glyphSetGlyphNames
            if diff:
                for gn in diff:
                    glyphset[gn] = None

        # Normalize locations
        locations = [
            {
                **normalizeLocation(loc, axis_triples),
                **{k: v for k, v in loc.items() if k in discrete_axes},
            }
            for loc in locations
        ]
        tolerance = args.tolerance or DEFAULT_TOLERANCE
        kinkiness = args.kinkiness if args.kinkiness is not None else DEFAULT_KINKINESS

        try:
            log.info("Running on %d glyphsets", len(glyphsets))
            log.info("Locations: %s", pformat(locations))
            problems_gen = test_gen(
                glyphsets,
                glyphs=glyphs,
                names=names,
                locations=locations,
                upem=upem,
                ignore_missing=args.ignore_missing,
                tolerance=tolerance,
                kinkiness=kinkiness,
                show_all=args.show_all,
                discrete_axes=discrete_axes,
            )
            problems = defaultdict(list)

            f = (
                sys.stdout
                if args.output is None
                else open(ensure_parent_dir(args.output), "w")
            )

            if not args.quiet:
                if args.json:
                    import json

                    for glyphname, problem in problems_gen:
                        problems[glyphname].append(problem)

                    print(json.dumps(problems), file=f)
                else:
                    last_glyphname = None
                    for glyphname, p in problems_gen:
                        problems[glyphname].append(p)

                        if glyphname != last_glyphname:
                            print(f"Glyph {glyphname} was not compatible:", file=f)
                            last_glyphname = glyphname
                            last_master_idxs = None

                        master_idxs = (
                            (p["master_idx"],)
                            if "master_idx" in p
                            else (p["master_1_idx"], p["master_2_idx"])
                        )
                        if master_idxs != last_master_idxs:
                            master_names = (
                                (p["master"],)
                                if "master" in p
                                else (p["master_1"], p["master_2"])
                            )
                            print(f"  Masters: %s:" % ", ".join(master_names), file=f)
                            last_master_idxs = master_idxs

                        if p["type"] == InterpolatableProblem.MISSING:
                            print(
                                "    Glyph was missing in master %s" % p["master"], file=f
                            )
                        elif p["type"] == InterpolatableProblem.OPEN_PATH:
                            print(
                                "    Glyph has an open path in master %s" % p["master"],
                                file=f,
                            )
                        elif p["type"] == InterpolatableProblem.PATH_COUNT:
                            print(
                                "    Path count differs: %i in %s, %i in %s"
                                % (
                                    p["value_1"],
                                    p["master_1"],
                                    p["value_2"],
                                    p["master_2"],
                                ),
                                file=f,
                            )
                        elif p["type"] == InterpolatableProblem.NODE_COUNT:
                            print(
                                "    Node count differs in path %i: %i in %s, %i in %s"
                                % (
                                    p["path"],
                                    p["value_1"],
                                    p["master_1"],
                                    p["value_2"],
                                    p["master_2"],
                                ),
                                file=f,
                            )
                        elif p["type"] == InterpolatableProblem.NODE_INCOMPATIBILITY:
                            print(
                                "    Node %o incompatible in path %i: %s in %s, %s in %s"
                                % (
                                    p["node"],
                                    p["path"],
                                    p["value_1"],
                                    p["master_1"],
                                    p["value_2"],
                                    p["master_2"],
                                ),
                                file=f,
                            )
                        elif p["type"] == InterpolatableProblem.CONTOUR_ORDER:
                            print(
                                "    Contour order differs: %s in %s, %s in %s"
                                % (
                                    p["value_1"],
                                    p["master_1"],
                                    p["value_2"],
                                    p["master_2"],
                                ),
                                file=f,
                            )
                        elif p["type"] == InterpolatableProblem.WRONG_START_POINT:
                            print(
                                "    Contour %d start point differs: %s in %s, %s in %s; reversed: %s"
                                % (
                                    p["contour"],
                                    p["value_1"],
                                    p["master_1"],
                                    p["value_2"],
                                    p["master_2"],
                                    p["reversed"],
                                ),
                                file=f,
                            )
                        elif p["type"] == InterpolatableProblem.UNDERWEIGHT:
                            print(
                                "    Contour %d interpolation is underweight: %s, %s"
                                % (
                                    p["contour"],
                                    p["master_1"],
                                    p["master_2"],
                                ),
                                file=f,
                            )
                        elif p["type"] == InterpolatableProblem.OVERWEIGHT:
                            print(
                                "    Contour %d interpolation is overweight: %s, %s"
                                % (
                                    p["contour"],
                                    p["master_1"],
                                    p["master_2"],
                                ),
                                file=f,
                            )
                        elif p["type"] == InterpolatableProblem.KINK:
                            print(
                                "    Contour %d has a kink at %s: %s, %s"
                                % (
                                    p["contour"],
                                    p["value"],
                                    p["master_1"],
                                    p["master_2"],
                                ),
                                file=f,
                            )
                        elif p["type"] == InterpolatableProblem.NOTHING:
                            print(
                                "    Showing %s and %s"
                                % (
                                    p["master_1"],
                                    p["master_2"],
                                ),
                                file=f,
                            )
            else:
                for glyphname, problem in problems_gen:
                    problems[glyphname].append(problem)

            problems = sort_problems(problems)

            for p in "ps", "pdf":
                arg = getattr(args, p)
                if arg is None:
                    continue
                log.info("Writing %s to %s", p.upper(), arg)
                from .interpolatablePlot import InterpolatablePS, InterpolatablePDF

                PlotterClass = InterpolatablePS if p == "ps" else InterpolatablePDF

                with PlotterClass(
                    ensure_parent_dir(arg), glyphsets=glyphsets, names=names
                ) as doc:
                    doc.add_title_page(
                        original_args_inputs, tolerance=tolerance, kinkiness=kinkiness
                    )
                    if problems:
                        doc.add_summary(problems)
                    doc.add_problems(problems)
                    if not problems and not args.quiet:
                        doc.draw_cupcake()
                    if problems:
                        doc.add_index()
                        doc.add_table_of_contents()

            if args.html:
                log.info("Writing HTML to %s", args.html)
                from .interpolatablePlot import InterpolatableSVG

                svgs = []
                glyph_starts = {}
                with InterpolatableSVG(svgs, glyphsets=glyphsets, names=names) as svg:
                    svg.add_title_page(
                        original_args_inputs,
                        show_tolerance=False,
                        tolerance=tolerance,
                        kinkiness=kinkiness,
                    )
                    for glyph, glyph_problems in problems.items():
                        glyph_starts[len(svgs)] = glyph
                        svg.add_problems(
                            {glyph: glyph_problems},
                            show_tolerance=False,
                            show_page_number=False,
                        )
                    if not problems and not args.quiet:
                        svg.draw_cupcake()

                import base64

                with open(ensure_parent_dir(args.html), "wb") as f:
                    f.write(b"<!DOCTYPE html>\n")
                    f.write(
                        b'<html><body align="center" style="font-family: sans-serif; text-color: #222">\n'
                    )
                    f.write(b"<title>fonttools varLib.interpolatable report</title>\n")
                    for i, svg in enumerate(svgs):
                        if i in glyph_starts:
                            f.write(f"<h1>Glyph {glyph_starts[i]}</h1>\n".encode("utf-8"))
                        f.write("<img src='data:image/svg+xml;base64,".encode("utf-8"))
                        f.write(base64.b64encode(svg))
                        f.write(b"' />\n")
                        f.write(b"<hr>\n")
                    f.write(b"</body></html>\n")

        except Exception as e:
            e.args += original_args_inputs
            log.error(e)
            raise

        if problems:
            return problems

    if __name__ == "__main__":
        import sys

        problems = main()
        sys.exit(int(bool(problems)))