aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python
diff options
context:
space:
mode:
authorrobot-contrib <robot-contrib@yandex-team.com>2023-12-08 21:12:48 +0300
committerrobot-contrib <robot-contrib@yandex-team.com>2023-12-08 23:47:25 +0300
commit460528e80f26d04487dc242b7333d45bbeb43a4d (patch)
treefbc5feb120951c22582e83a53d107849abea94a4 /contrib/python
parentae5dc4d4d872ed4c4e9d11191ddd01bcbe171a55 (diff)
downloadydb-460528e80f26d04487dc242b7333d45bbeb43a4d.tar.gz
Update contrib/python/fonttools to 4.45.1
Diffstat (limited to 'contrib/python')
-rw-r--r--contrib/python/fonttools/.dist-info/METADATA11
-rw-r--r--contrib/python/fonttools/fontTools/__init__.py2
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/ttFont.py6
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py21
-rw-r--r--contrib/python/fonttools/fontTools/varLib/interpolatable.py600
-rw-r--r--contrib/python/fonttools/fontTools/varLib/interpolatablePlot.py67
-rw-r--r--contrib/python/fonttools/ya.make2
7 files changed, 511 insertions, 198 deletions
diff --git a/contrib/python/fonttools/.dist-info/METADATA b/contrib/python/fonttools/.dist-info/METADATA
index 86f1760961..9c1a26c22c 100644
--- a/contrib/python/fonttools/.dist-info/METADATA
+++ b/contrib/python/fonttools/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: fonttools
-Version: 4.45.0
+Version: 4.45.1
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -366,10 +366,17 @@ Have fun!
Changelog
~~~~~~~~~
+4.45.1 (released 2023-11-23)
+----------------------------
+
+- [varLib.interpolatable] Various bugfixes and improvements, better reporting, reduced
+ false positives.
+- [ttGlyphSet] Added option to not recalculate glyf bounds (#3348).
+
4.45.0 (released 2023-11-20)
----------------------------
-- [varLib.interpolator] Vastly improved algorithms. Also available now is ``--pdf``
+- [varLib.interpolatable] Vastly improved algorithms. Also available now is ``--pdf``
and ``--html`` options to generate a PDF or HTML report of the interpolation issues.
The PDF/HTML report showcases the problematic masters, the interpolated broken
glyph, as well as the proposed fixed version.
diff --git a/contrib/python/fonttools/fontTools/__init__.py b/contrib/python/fonttools/fontTools/__init__.py
index 90b2c2ece6..6e396b5084 100644
--- a/contrib/python/fonttools/fontTools/__init__.py
+++ b/contrib/python/fonttools/fontTools/__init__.py
@@ -3,6 +3,6 @@ from fontTools.misc.loggingTools import configLogger
log = logging.getLogger(__name__)
-version = __version__ = "4.45.0"
+version = __version__ = "4.45.1"
__all__ = ["version", "log", "configLogger"]
diff --git a/contrib/python/fonttools/fontTools/ttLib/ttFont.py b/contrib/python/fonttools/fontTools/ttLib/ttFont.py
index 6a9ca09820..c8c74fecfe 100644
--- a/contrib/python/fonttools/fontTools/ttLib/ttFont.py
+++ b/contrib/python/fonttools/fontTools/ttLib/ttFont.py
@@ -735,7 +735,9 @@ class TTFont(object):
else:
raise KeyError(tag)
- def getGlyphSet(self, preferCFF=True, location=None, normalized=False):
+ def getGlyphSet(
+ self, preferCFF=True, location=None, normalized=False, recalcBounds=True
+ ):
"""Return a generic GlyphSet, which is a dict-like object
mapping glyph names to glyph objects. The returned glyph objects
have a ``.draw()`` method that supports the Pen protocol, and will
@@ -766,7 +768,7 @@ class TTFont(object):
if ("CFF " in self or "CFF2" in self) and (preferCFF or "glyf" not in self):
return _TTGlyphSetCFF(self, location)
elif "glyf" in self:
- return _TTGlyphSetGlyf(self, location)
+ return _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds)
else:
raise TTLibError("Font contains no outlines")
diff --git a/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py b/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py
index d4384c89f6..349cc2c73f 100644
--- a/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py
+++ b/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py
@@ -17,7 +17,8 @@ class _TTGlyphSet(Mapping):
glyph shape from TrueType or CFF.
"""
- def __init__(self, font, location, glyphsMapping):
+ def __init__(self, font, location, glyphsMapping, *, recalcBounds=True):
+ self.recalcBounds = recalcBounds
self.font = font
self.defaultLocationNormalized = (
{axis.axisTag: 0 for axis in self.font["fvar"].axes}
@@ -89,13 +90,13 @@ class _TTGlyphSet(Mapping):
class _TTGlyphSetGlyf(_TTGlyphSet):
- def __init__(self, font, location):
+ def __init__(self, font, location, recalcBounds=True):
self.glyfTable = font["glyf"]
- super().__init__(font, location, self.glyfTable)
+ super().__init__(font, location, self.glyfTable, recalcBounds=recalcBounds)
self.gvarTable = font.get("gvar")
def __getitem__(self, glyphName):
- return _TTGlyphGlyf(self, glyphName)
+ return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds)
class _TTGlyphSetCFF(_TTGlyphSet):
@@ -129,9 +130,10 @@ class _TTGlyph(ABC):
attributes.
"""
- def __init__(self, glyphSet, glyphName):
+ def __init__(self, glyphSet, glyphName, *, recalcBounds=True):
self.glyphSet = glyphSet
self.name = glyphName
+ self.recalcBounds = recalcBounds
self.width, self.lsb = glyphSet.hMetrics[glyphName]
if glyphSet.vMetrics is not None:
self.height, self.tsb = glyphSet.vMetrics[glyphName]
@@ -258,7 +260,9 @@ class _TTGlyphGlyf(_TTGlyph):
coordinates += GlyphCoordinates(delta) * scalar
glyph = copy(glyfTable[self.name]) # Shallow copy
- width, lsb, height, tsb = _setCoordinates(glyph, coordinates, glyfTable)
+ width, lsb, height, tsb = _setCoordinates(
+ glyph, coordinates, glyfTable, recalcBounds=self.recalcBounds
+ )
self.lsb = lsb
self.tsb = tsb
if glyphSet.hvarTable is None:
@@ -276,7 +280,7 @@ class _TTGlyphCFF(_TTGlyph):
self.glyphSet.charStrings[self.name].draw(pen, self.glyphSet.blender)
-def _setCoordinates(glyph, coord, glyfTable):
+def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True):
# Handle phantom points for (left, right, top, bottom) positions.
assert len(coord) >= 4
leftSideX = coord[-4][0]
@@ -304,7 +308,8 @@ def _setCoordinates(glyph, coord, glyfTable):
assert len(coord) == len(glyph.coordinates)
glyph.coordinates = coord
- glyph.recalcBounds(glyfTable)
+ if recalcBounds:
+ glyph.recalcBounds(glyfTable)
horizontalAdvanceWidth = otRound(rightSideX - leftSideX)
verticalAdvanceWidth = otRound(topSideY - bottomSideY)
diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatable.py b/contrib/python/fonttools/fontTools/varLib/interpolatable.py
index 74dd15b968..9b72b4f502 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
diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatablePlot.py b/contrib/python/fonttools/fontTools/varLib/interpolatablePlot.py
index 224f60ae64..a4e86b3dba 100644
--- a/contrib/python/fonttools/fontTools/varLib/interpolatablePlot.py
+++ b/contrib/python/fonttools/fontTools/varLib/interpolatablePlot.py
@@ -98,16 +98,16 @@ class InterpolatablePlot:
,@@,.@@@. @.@@@,.
,@@. @@@. @@. @@,.
,@@@.@,.@. @. @@@@,.@.@@,.
- ,@@.@. @@.@@. @,. .@’ @’ @@,
- ,@@. @. .@@.@@@. @@’ @,
+ ,@@.@. @@.@@. @,. .@' @' @@,
+ ,@@. @. .@@.@@@. @@' @,
,@. @@. @,
@. @,@@,. , .@@,
@,. .@,@@,. .@@,. , .@@, @, @,
@. .@. @ @@,. , @
- @,.@@. @,. @@,. @. @,. @’
- @@||@,. @’@,. @@,. @@ @,. @’@@, @’
- \\@@@@’ @,. @’@@@@’ @@,. @@@’ //@@@’
- |||||||| @@,. @@’ ||||||| |@@@|@|| ||
+ @,.@@. @,. @@,. @. @,. @'
+ @@||@,. @'@,. @@,. @@ @,. @'@@, @'
+ \\@@@@' @,. @'@@@@' @@,. @@@' //@@@'
+ |||||||| @@,. @@' ||||||| |@@@|@|| ||
\\\\\\\ ||@@@|| ||||||| ||||||| //
||||||| |||||| |||||| |||||| ||
\\\\\\ |||||| |||||| |||||| //
@@ -148,7 +148,9 @@ class InterpolatablePlot:
current_glyph_problems = []
for p in glyph_problems:
masters = (
- p["master"] if "master" in p else (p["master_1"], p["master_2"])
+ p["master_idx"]
+ if "master_idx" in p
+ else (p["master_1_idx"], p["master_2_idx"])
)
if masters == last_masters:
current_glyph_problems.append(p)
@@ -176,10 +178,11 @@ class InterpolatablePlot:
log.info("Drawing %s: %s", glyphname, problem_type)
master_keys = (
- ("master",) if "master" in problems[0] else ("master_1", "master_2")
+ ("master_idx",)
+ if "master_idx" in problems[0]
+ else ("master_1_idx", "master_2_idx")
)
- master_names = [problems[0][k] for k in master_keys]
- master_indices = [self.names.index(n) for n in master_names]
+ master_indices = [problems[0][k] for k in master_keys]
if problem_type == "missing":
sample_glyph = next(
@@ -225,7 +228,10 @@ class InterpolatablePlot:
self.draw_shrug(x=x, y=y)
y += self.height + self.pad
- if any(pt in ("wrong_start_point", "contour_order") for pt in problem_types):
+ if any(
+ pt in ("nothing", "wrong_start_point", "contour_order", "wrong_structure")
+ for pt in problem_types
+ ):
x = self.pad + self.width + self.pad
y = self.pad
y += self.line_height + self.pad
@@ -251,6 +257,10 @@ class InterpolatablePlot:
self.draw_label("proposed fix", x=x, y=y, color=self.head_color, align=0.5)
y += self.line_height + self.pad
+ if problem_type == "wrong_structure":
+ self.draw_shrug(x=x, y=y)
+ return
+
overriding1 = OverridingDict(glyphset1)
overriding2 = OverridingDict(glyphset2)
perContourPen1 = PerContourOrComponentPen(
@@ -382,7 +392,16 @@ class InterpolatablePlot:
glyph_width = boundsPen.bounds[2] - boundsPen.bounds[0]
glyph_height = boundsPen.bounds[3] - boundsPen.bounds[1]
- scale = min(self.width / glyph_width, self.height / glyph_height)
+ scale = None
+ if glyph_width:
+ scale = self.width / glyph_width
+ if glyph_height:
+ if scale is None:
+ scale = self.height / glyph_height
+ else:
+ scale = min(scale, self.height / glyph_height)
+ if scale is None:
+ scale = 1
cr = cairo.Context(self.surface)
cr.translate(x, y)
@@ -415,7 +434,12 @@ class InterpolatablePlot:
cr.set_line_width(self.stroke_width / scale)
cr.stroke()
- if problem_type in ("node_count", "node_incompatibility"):
+ if problem_type in (
+ "nothing",
+ "node_count",
+ "node_incompatibility",
+ "wrong_structure",
+ ):
cr.set_line_cap(cairo.LINE_CAP_ROUND)
# Oncurve nodes
@@ -463,6 +487,7 @@ class InterpolatablePlot:
cr.set_line_width(self.handle_width / scale)
cr.stroke()
+ matching = None
for problem in problems:
if problem["type"] == "contour_order":
matching = problem["value_2"]
@@ -480,18 +505,20 @@ class InterpolatablePlot:
cr.fill()
for problem in problems:
- if problem["type"] == "wrong_start_point":
- idx = problem["contour"]
+ if problem["type"] in ("nothing", "wrong_start_point", "wrong_structure"):
+ idx = problem.get("contour")
# Draw suggested point
- if which == 1:
+ if idx is not None and which == 1 and "value_2" in problem:
perContourPen = PerContourOrComponentPen(
RecordingPen, glyphset=glyphset
)
recording.replay(perContourPen)
points = SimpleRecordingPointPen()
converter = SegmentToPointPen(points, False)
- perContourPen.value[idx].replay(converter)
+ perContourPen.value[
+ idx if matching is None else matching[idx]
+ ].replay(converter)
targetPoint = points.value[problem["value_2"]][0]
cr.move_to(*targetPoint)
cr.line_to(*targetPoint)
@@ -505,12 +532,12 @@ class InterpolatablePlot:
i = 0
for segment, args in recording.value:
if segment == "moveTo":
- if i == idx:
+ if idx is None or i == idx:
cr.move_to(*args[0])
cr.line_to(*args[0])
i += 1
- if which == 0 or not problem["reversed"]:
+ if which == 0 or not problem.get("reversed"):
cr.set_source_rgb(*self.start_point_color)
else:
cr.set_source_rgb(*self.reversed_start_point_color)
@@ -529,7 +556,7 @@ class InterpolatablePlot:
continue
second_pt = args[0]
- if i == idx:
+ if idx is None or i == idx:
first_pt = complex(*first_pt)
second_pt = complex(*second_pt)
length = abs(second_pt - first_pt)
diff --git a/contrib/python/fonttools/ya.make b/contrib/python/fonttools/ya.make
index fbda7f8c97..2e2c1c626e 100644
--- a/contrib/python/fonttools/ya.make
+++ b/contrib/python/fonttools/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(4.45.0)
+VERSION(4.45.1)
LICENSE(MIT)