summaryrefslogtreecommitdiffstats
path: root/contrib/python/fonttools/fontTools/varLib/interpolatable.py
diff options
context:
space:
mode:
authorrobot-contrib <[email protected]>2023-12-08 21:12:48 +0300
committerrobot-contrib <[email protected]>2023-12-08 23:47:25 +0300
commit460528e80f26d04487dc242b7333d45bbeb43a4d (patch)
treefbc5feb120951c22582e83a53d107849abea94a4 /contrib/python/fonttools/fontTools/varLib/interpolatable.py
parentae5dc4d4d872ed4c4e9d11191ddd01bcbe171a55 (diff)
Update contrib/python/fonttools to 4.45.1
Diffstat (limited to 'contrib/python/fonttools/fontTools/varLib/interpolatable.py')
-rw-r--r--contrib/python/fonttools/fontTools/varLib/interpolatable.py600
1 files changed, 436 insertions, 164 deletions
diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatable.py b/contrib/python/fonttools/fontTools/varLib/interpolatable.py
index 74dd15b9689..9b72b4f5029 100644
--- a/contrib/python/fonttools/fontTools/varLib/interpolatable.py
+++ b/contrib/python/fonttools/fontTools/varLib/interpolatable.py
@@ -13,10 +13,11 @@ 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, deque
from functools import wraps
from pprint import pformat
-from math import sqrt, copysign
+from math import sqrt, copysign, atan2, pi
import itertools
import logging
@@ -96,9 +97,15 @@ def _vdiff_hypot2_complex(v0, v1):
for x0, x1 in zip(v0, v1):
d = x1 - x0
s += d.real * d.real + d.imag * d.imag
+ # This does the same but seems to be slower:
+ # s += (d * d.conjugate()).real
return s
+def _hypot2_complex(d):
+ return d.real * d.real + d.imag * d.imag
+
+
def _matching_cost(G, matching):
return sum(G[i][j] for i, j in enumerate(matching))
@@ -153,6 +160,9 @@ except ImportError:
def _contour_vector_from_stats(stats):
+ # Don't change the order of items here.
+ # It's okay to add to the end, but otherwise, other
+ # code depends on it. Search for "covariance".
size = sqrt(abs(stats.area))
return (
copysign((size), stats.area),
@@ -171,32 +181,41 @@ def _points_characteristic_bits(points):
return bits
+_NUM_ITEMS_PER_POINTS_COMPLEX_VECTOR = 4
+
+
def _points_complex_vector(points):
vector = []
+ if not points:
+ return vector
points = [complex(*pt) for pt, _ in points]
n = len(points)
- points.extend(points[:2])
+ assert _NUM_ITEMS_PER_POINTS_COMPLEX_VECTOR == 4
+ points.extend(points[: _NUM_ITEMS_PER_POINTS_COMPLEX_VECTOR - 1])
+ while len(points) < _NUM_ITEMS_PER_POINTS_COMPLEX_VECTOR:
+ points.extend(points[: _NUM_ITEMS_PER_POINTS_COMPLEX_VECTOR - 1])
for i in range(n):
- p0 = points[i]
+ # The weights are magic numbers.
# The point itself
+ p0 = points[i]
vector.append(p0)
- # The distance to the next point;
- # Emphasized by 2 empirically
+ # The vector to the next point
p1 = points[i + 1]
d0 = p1 - p0
- vector.append(d0 * 2)
+ vector.append(d0 * 3)
- """
- # The angle to the next point, as a cross product;
- # Square root of, to match dimentionality of distance.
+ # The turn vector
p2 = points[i + 2]
d1 = p2 - p1
+ vector.append(d1 - d0)
+
+ # The angle to the next point, as a cross product;
+ # Square root of, to match dimentionality of distance.
cross = d0.real * d1.imag - d0.imag * d1.real
cross = copysign(sqrt(abs(cross)), cross)
- vector.append(cross)
- """
+ vector.append(cross * 4)
return vector
@@ -291,6 +310,7 @@ def test_gen(
*,
locations=None,
tolerance=0.95,
+ show_all=False,
):
if names is None:
names = glyphsets
@@ -318,17 +338,24 @@ def test_gen(
allControlVectors = []
allNodeTypes = []
allContourIsomorphisms = []
+ allContourPoints = []
allGlyphs = [glyphset[glyph_name] for glyphset in glyphsets]
if len([1 for glyph in allGlyphs if glyph is not None]) <= 1:
continue
- for glyph, glyphset, name in zip(allGlyphs, glyphsets, names):
+ for master_idx, (glyph, glyphset, name) in enumerate(
+ zip(allGlyphs, glyphsets, names)
+ ):
if glyph is None:
if not ignore_missing:
- yield (glyph_name, {"type": "missing", "master": name})
+ yield (
+ glyph_name,
+ {"type": "missing", "master": name, "master_idx": master_idx},
+ )
allNodeTypes.append(None)
allControlVectors.append(None)
allGreenVectors.append(None)
allContourIsomorphisms.append(None)
+ allContourPoints.append(None)
continue
perContourPen = PerContourOrComponentPen(RecordingPen, glyphset=glyphset)
@@ -342,11 +369,13 @@ def test_gen(
contourControlVectors = []
contourGreenVectors = []
contourIsomorphisms = []
+ contourPoints = []
nodeTypes = []
allNodeTypes.append(nodeTypes)
allControlVectors.append(contourControlVectors)
allGreenVectors.append(contourGreenVectors)
allContourIsomorphisms.append(contourIsomorphisms)
+ allContourPoints.append(contourPoints)
for ix, contour in enumerate(contourPens):
contourOps = tuple(op for op, arg in contour.value)
nodeTypes.append(contourOps)
@@ -359,7 +388,12 @@ def test_gen(
except OpenContourError as e:
yield (
glyph_name,
- {"master": name, "contour": ix, "type": "open_path"},
+ {
+ "master": name,
+ "master_idx": master_idx,
+ "contour": ix,
+ "type": "open_path",
+ },
)
continue
contourGreenVectors.append(_contour_vector_from_stats(greenStats))
@@ -385,6 +419,8 @@ def test_gen(
# Add mirrored rotations
_add_isomorphisms(points.value, isomorphisms, True)
+ contourPoints.append(points.value)
+
matchings = [None] * len(allControlVectors)
for m1idx in order:
@@ -396,15 +432,20 @@ def test_gen(
if allNodeTypes[m0idx] is None:
continue
+ showed = False
+
m1 = allNodeTypes[m1idx]
m0 = allNodeTypes[m0idx]
if len(m0) != len(m1):
+ showed = True
yield (
glyph_name,
{
"type": "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),
},
@@ -416,6 +457,7 @@ def test_gen(
if nodes1 == nodes2:
continue
if len(nodes1) != len(nodes2):
+ showed = True
yield (
glyph_name,
{
@@ -423,6 +465,8 @@ def test_gen(
"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),
},
@@ -430,6 +474,7 @@ def test_gen(
continue
for nodeIx, (n1, n2) in enumerate(zip(nodes1, nodes2)):
if n1 != n2:
+ showed = True
yield (
glyph_name,
{
@@ -438,6 +483,8 @@ def test_gen(
"node": nodeIx,
"master_1": names[m0idx],
"master_2": names[m1idx],
+ "master_1_idx": m0idx,
+ "master_2_idx": m1idx,
"value_1": n1,
"value_2": n2,
},
@@ -504,12 +551,15 @@ def test_gen(
if matching_cost < identity_cost * tolerance:
# print(matching_cost_control / identity_cost_control, matching_cost_green / identity_cost_green)
+ showed = True
yield (
glyph_name,
{
"type": "contour_order",
"master_1": names[m0idx],
"master_2": names[m1idx],
+ "master_1_idx": m0idx,
+ "master_2_idx": m1idx,
"value_1": list(range(len(m0Control))),
"value_2": matching,
},
@@ -519,38 +569,194 @@ def test_gen(
m1 = allContourIsomorphisms[m1idx]
m0 = allContourIsomorphisms[m0idx]
+ # 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]]
+
for ix, (contour0, contour1) in enumerate(zip(m0, m1)):
if len(contour0) == 0 or len(contour0) != len(contour1):
- # We already reported this; or nothing to do
+ # We already reported this; or nothing to do; or not compatible
+ # after reordering above.
continue
c0 = contour0[0]
+ # Next few lines duplicated below.
costs = [_vdiff_hypot2_complex(c0[0], c1[0]) for c1 in contour1]
min_cost_idx, min_cost = min(enumerate(costs), key=lambda x: x[1])
first_cost = costs[0]
+
if min_cost < first_cost * tolerance:
+ # c0 is the first isomorphism of the m0 master
+ # contour1 is list of all isomorphisms of the m1 master
+ #
+ # If the two shapes are both circle-ish and slightly
+ # rotated, we detect wrong start point. This is for
+ # example the case hundreds of times in
+ # RobotoSerif-Italic[GRAD,opsz,wdth,wght].ttf
+ #
+ # If the proposed point is only one off from the first
+ # point (and not reversed), try harder:
+ #
+ # Find the major eigenvector of the covariance matrix,
+ # and rotate the contours by that angle. Then find the
+ # closest point again. If it matches this time, let it
+ # pass.
+
+ proposed_point = contour1[min_cost_idx][1]
reverse = contour1[min_cost_idx][2]
-
- # If contour-order is wrong, don't report a reversing
- if (
- reverse
- and matchings[m1idx] is not None
- and matchings[m1idx][ix] != ix
+ num_points = len(allContourPoints[m1idx][ix])
+ leeway = 3
+ okay = False
+ if not reverse and (
+ proposed_point <= leeway
+ or proposed_point >= num_points - leeway
):
- continue
+ # Try harder
+
+ m0Vectors = allGreenVectors[m1idx][ix]
+ m1Vectors = allGreenVectors[m1idx][ix]
+
+ # Recover the covariance matrix from the GreenVectors.
+ # This is a 2x2 matrix.
+ transforms = []
+ for vector in (m0Vectors, m1Vectors):
+ meanX = vector[1]
+ meanY = vector[2]
+ stddevX = vector[3] / 2
+ stddevY = vector[4] / 2
+ correlation = vector[5] / abs(vector[0])
+
+ # https://cookierobotics.com/007/
+ a = stddevX * stddevX # VarianceX
+ c = stddevY * stddevY # VarianceY
+ b = correlation * stddevX * stddevY # Covariance
+
+ delta = (((a - c) * 0.5) ** 2 + b * b) ** 0.5
+ lambda1 = (a + c) * 0.5 + delta # Major eigenvalue
+ lambda2 = (a + c) * 0.5 - delta # Minor eigenvalue
+ theta = (
+ atan2(lambda1 - a, b)
+ if b != 0
+ else (pi * 0.5 if a < c else 0)
+ )
+ trans = Transform()
+ trans = trans.translate(meanX, meanY)
+ trans = trans.rotate(theta)
+ trans = trans.scale(sqrt(lambda1), sqrt(lambda2))
+ transforms.append(trans)
+
+ trans = transforms[0]
+ new_c0 = (
+ [
+ complex(*trans.transformPoint((pt.real, pt.imag)))
+ for pt in c0[0]
+ ],
+ ) + c0[1:]
+ trans = transforms[1]
+ new_contour1 = []
+ for c1 in contour1:
+ new_c1 = (
+ [
+ complex(*trans.transformPoint((pt.real, pt.imag)))
+ for pt in c1[0]
+ ],
+ ) + c1[1:]
+ new_contour1.append(new_c1)
+
+ # Next few lines duplicate from above.
+ costs = [
+ _vdiff_hypot2_complex(new_c0[0], new_c1[0])
+ for new_c1 in new_contour1
+ ]
+ min_cost_idx, min_cost = min(
+ enumerate(costs), key=lambda x: x[1]
+ )
+ first_cost = costs[0]
+ # Only accept a perfect match
+ if min_cost_idx == 0:
+ okay = True
- yield (
- glyph_name,
- {
- "type": "wrong_start_point",
- "contour": ix,
- "master_1": names[m0idx],
- "master_2": names[m1idx],
- "value_1": 0,
- "value_2": contour1[min_cost_idx][1],
- "reversed": reverse,
- },
- )
+ if not okay:
+ showed = True
+ yield (
+ glyph_name,
+ {
+ "type": "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,
+ },
+ )
+ else:
+ # If first_cost is Too Largeā„¢, do further inspection.
+ # This can happen specially in the case of TrueType
+ # fonts, where the original contour had wrong start point,
+ # but because of the cubic->quadratic conversion, we don't
+ # have many isomorphisms to work with.
+
+ # The threshold here is all black magic. It's just here to
+ # speed things up so we don't end up doing a full matching
+ # on every contour that is correct.
+ threshold = (
+ len(c0[0]) * (allControlVectors[m0idx][ix][0] * 0.5) ** 2 / 4
+ ) # Magic only
+ c1 = contour1[min_cost_idx]
+
+ # If point counts are different it's because of the contour
+ # reordering above. We can in theory still try, but our
+ # bipartite-matching implementations currently assume
+ # equal number of vertices on both sides. I'm lazy to update
+ # all three different implementations!
+
+ if len(c0[0]) == len(c1[0]) and first_cost > threshold:
+ # Do a quick(!) matching between the points. If it's way off,
+ # flag it. This can happen specially in the case of TrueType
+ # fonts, where the original contour had wrong start point, but
+ # because of the cubic->quadratic conversion, we don't have many
+ # isomorphisms.
+ points0 = c0[0][::_NUM_ITEMS_PER_POINTS_COMPLEX_VECTOR]
+ points1 = c1[0][::_NUM_ITEMS_PER_POINTS_COMPLEX_VECTOR]
+
+ graph = [
+ [_hypot2_complex(p0 - p1) for p1 in points1]
+ for p0 in points0
+ ]
+ matching, matching_cost = min_cost_perfect_bipartite_matching(
+ graph
+ )
+ identity_cost = sum(graph[i][i] for i in range(len(graph)))
+
+ if matching_cost < identity_cost / 8: # Heuristic
+ # print(matching_cost, identity_cost, matching)
+ showed = True
+ yield (
+ glyph_name,
+ {
+ "type": "wrong_structure",
+ "contour": ix,
+ "master_1": names[m0idx],
+ "master_2": names[m1idx],
+ "master_1_idx": m0idx,
+ "master_2_idx": m1idx,
+ },
+ )
+
+ if show_all and not showed:
+ yield (
+ glyph_name,
+ {
+ "type": "nothing",
+ "master_1": names[m0idx],
+ "master_2": names[m1idx],
+ "master_1_idx": m0idx,
+ "master_2_idx": m1idx,
+ },
+ )
@wraps(test_gen)
@@ -585,6 +791,11 @@ def main(args=None):
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,
@@ -627,6 +838,13 @@ def main(args=None):
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.")
args = parser.parse_args(args)
@@ -643,6 +861,8 @@ def main(args=None):
names = []
locations = []
+ original_args_inputs = tuple(args.inputs)
+
if len(args.inputs) == 1:
designspace = None
if args.inputs[0].endswith(".designspace"):
@@ -721,7 +941,7 @@ def main(args=None):
locTuple = tuple(loc)
if locTuple not in ttGlyphSets:
ttGlyphSets[locTuple] = font.getGlyphSet(
- location=locDict, normalized=True
+ location=locDict, normalized=True, recalcBounds=False
)
recursivelyAddGlyph(
@@ -776,8 +996,19 @@ def main(args=None):
glyphset = font
glyphsets.append({k: glyphset[k] for k in glyphset.keys()})
- if len(glyphsets) == 1:
- return None
+ 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()]))
@@ -793,140 +1024,181 @@ def main(args=None):
# Normalize locations
locations = [normalizeLocation(loc, axis_triples) for loc in locations]
- 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,
- ignore_missing=args.ignore_missing,
- tolerance=args.tolerance or 0.95,
- )
- problems = defaultdict(list)
+ 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,
+ ignore_missing=args.ignore_missing,
+ tolerance=args.tolerance or 0.95,
+ show_all=args.show_all,
+ )
+ problems = defaultdict(list)
- f = sys.stdout if args.output is None else open(args.output, "w")
+ f = sys.stdout if args.output is None else open(args.output, "w")
- if not args.quiet:
- if args.json:
- import json
+ if not args.quiet:
+ if args.json:
+ import json
- for glyphname, problem in problems_gen:
- problems[glyphname].append(problem)
+ 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)
+ 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_masters = None
+ if glyphname != last_glyphname:
+ print(f"Glyph {glyphname} was not compatible:", file=f)
+ last_glyphname = glyphname
+ last_master_idxs = None
- masters = (
- (p["master"]) if "master" in p else (p["master_1"], p["master_2"])
- )
- if masters != last_masters:
- print(f" Masters: %s:" % ", ".join(masters), file=f)
- last_masters = masters
-
- if p["type"] == "missing":
- print(" Glyph was missing in master %s" % p["master"], file=f)
- if p["type"] == "open_path":
- print(
- " Glyph has an open path in master %s" % p["master"], file=f
- )
- if p["type"] == "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,
- )
- if p["type"] == "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,
- )
- if p["type"] == "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,
- )
- if p["type"] == "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,
+ master_idxs = (
+ (p["master_idx"])
+ if "master_idx" in p
+ else (p["master_1_idx"], p["master_2_idx"])
)
- if p["type"] == "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,
- )
- else:
- for glyphname, problem in problems_gen:
- problems[glyphname].append(problem)
-
- if args.pdf:
- log.info("Writing PDF to %s", args.pdf)
- from .interpolatablePlot import InterpolatablePDF
-
- with InterpolatablePDF(args.pdf, glyphsets=glyphsets, names=names) as pdf:
- pdf.add_problems(problems)
- if not problems and not args.quiet:
- pdf.draw_cupcake()
-
- if args.html:
- log.info("Writing HTML to %s", args.html)
- from .interpolatablePlot import InterpolatableSVG
-
- svgs = []
- with InterpolatableSVG(svgs, glyphsets=glyphsets, names=names) as svg:
- svg.add_problems(problems)
- if not problems and not args.quiet:
- svg.draw_cupcake()
-
- import base64
-
- with open(args.html, "wb") as f:
- f.write(b"<!DOCTYPE html>\n")
- f.write(b"<html><body align=center>\n")
- for svg in svgs:
- 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")
+ 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"] == "missing":
+ print(
+ " Glyph was missing in master %s" % p["master"], file=f
+ )
+ elif p["type"] == "open_path":
+ print(
+ " Glyph has an open path in master %s" % p["master"],
+ file=f,
+ )
+ elif p["type"] == "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"] == "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"] == "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"] == "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"] == "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"] == "wrong_structure":
+ print(
+ " Contour %d structures differ: %s, %s"
+ % (
+ p["contour"],
+ p["master_1"],
+ p["master_2"],
+ ),
+ file=f,
+ )
+ elif p["type"] == "nothing":
+ print(
+ " Nothing wrong between %s and %s"
+ % (
+ p["master_1"],
+ p["master_2"],
+ ),
+ file=f,
+ )
+ else:
+ for glyphname, problem in problems_gen:
+ problems[glyphname].append(problem)
+
+ if args.pdf:
+ log.info("Writing PDF to %s", args.pdf)
+ from .interpolatablePlot import InterpolatablePDF
+
+ with InterpolatablePDF(args.pdf, glyphsets=glyphsets, names=names) as pdf:
+ pdf.add_problems(problems)
+ if not problems and not args.quiet:
+ pdf.draw_cupcake()
+
+ if args.html:
+ log.info("Writing HTML to %s", args.html)
+ from .interpolatablePlot import InterpolatableSVG
+
+ svgs = []
+ with InterpolatableSVG(svgs, glyphsets=glyphsets, names=names) as svg:
+ svg.add_problems(problems)
+ if not problems and not args.quiet:
+ svg.draw_cupcake()
+
+ import base64
+
+ with open(args.html, "wb") as f:
+ f.write(b"<!DOCTYPE html>\n")
+ f.write(b"<html><body align=center>\n")
+ for svg in svgs:
+ 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