code: 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 "ERROR"))
      if args.debug:
          configLogger(level="DEBUG")
  
      glyphs = args.glyphs.split() if args.glyphs else None
  
      from os.path import basename
  
      fonts = []
      names = []
      locations = []
      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
              ]
              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((".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"):
              from fontTools.ttLib import TTFont
  
              font = TTFont(args.inputs[0])
              upem = font["head"].unitsPerEm
              if "gvar" in font:
                  # Is variable font
  
                  axisMapping = {}
                  fvar = font["fvar"]
                  for axis in fvar.axes:
                      axisMapping[axis.axisTag] = {
                          -1: axis.minValue,
                          0: axis.defaultValue,
                          1: axis.maxValue,
                      }
                  if "avar" in font:
                      avar = font["avar"]
                      for axisTag, segments in avar.segments.items():
                          fvarMapping = axisMapping[axisTag].copy()
                          for location, value in segments.items():
                              axisMapping[axisTag][
                                  value
                              ] = piecewiseLinearMap(
                                  location, fvarMapping
                              )
  
                  gvar = font["gvar"]
                  glyf = font["glyf"]
                  # Gather all glyphs at their "master" locations
                  ttGlyphSets = {}
                  glyphsets = defaultdict(dict)
  
                  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,
                          )
  
                  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
                          )
                          + "'"
                      )
                      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])
  
      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) 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,
          )
          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
  
imports: 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
label_desc: Use image node_id 5 for calling a global function with example usage: main(args) and returns: problems