Revert "[varLib.interpolatable] Cythonize"
This reverts commit 0914c6c0eefb1317aba95779fb469e138b19f365.
This commit is contained in:
parent
0914c6c0ee
commit
2a855f8bd6
@ -1,419 +0,0 @@
|
|||||||
"""
|
|
||||||
Tool to find wrong contour order between different masters, and
|
|
||||||
other interpolatability (or lack thereof) issues.
|
|
||||||
|
|
||||||
Call as:
|
|
||||||
$ fonttools varLib.interpolatable font1 font2 ...
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
import cython
|
|
||||||
|
|
||||||
COMPILED = cython.compiled
|
|
||||||
except (AttributeError, ImportError):
|
|
||||||
# if cython not installed, use mock module with no-op decorators and types
|
|
||||||
from fontTools.misc import cython
|
|
||||||
|
|
||||||
COMPILED = False
|
|
||||||
|
|
||||||
|
|
||||||
from fontTools.pens.basePen import AbstractPen, BasePen
|
|
||||||
from fontTools.pens.pointPen import SegmentToPointPen
|
|
||||||
from fontTools.pens.recordingPen import RecordingPen
|
|
||||||
from fontTools.pens.statisticsPen import StatisticsPen
|
|
||||||
from fontTools.pens.momentsPen import OpenContourError
|
|
||||||
from collections import OrderedDict
|
|
||||||
import math
|
|
||||||
import itertools
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def _rot_list(l, k):
|
|
||||||
"""Rotate list by k items forward. Ie. item at position 0 will be
|
|
||||||
at position k in returned list. Negative k is allowed."""
|
|
||||||
n = len(l)
|
|
||||||
k %= n
|
|
||||||
if not k:
|
|
||||||
return l
|
|
||||||
return l[n - k :] + l[: n - k]
|
|
||||||
|
|
||||||
|
|
||||||
class PerContourPen(BasePen):
|
|
||||||
def __init__(self, Pen, glyphset=None):
|
|
||||||
BasePen.__init__(self, glyphset)
|
|
||||||
self._glyphset = glyphset
|
|
||||||
self._Pen = Pen
|
|
||||||
self._pen = None
|
|
||||||
self.value = []
|
|
||||||
|
|
||||||
def _moveTo(self, p0):
|
|
||||||
self._newItem()
|
|
||||||
self._pen.moveTo(p0)
|
|
||||||
|
|
||||||
def _lineTo(self, p1):
|
|
||||||
self._pen.lineTo(p1)
|
|
||||||
|
|
||||||
def _qCurveToOne(self, p1, p2):
|
|
||||||
self._pen.qCurveTo(p1, p2)
|
|
||||||
|
|
||||||
def _curveToOne(self, p1, p2, p3):
|
|
||||||
self._pen.curveTo(p1, p2, p3)
|
|
||||||
|
|
||||||
def _closePath(self):
|
|
||||||
self._pen.closePath()
|
|
||||||
self._pen = None
|
|
||||||
|
|
||||||
def _endPath(self):
|
|
||||||
self._pen.endPath()
|
|
||||||
self._pen = None
|
|
||||||
|
|
||||||
def _newItem(self):
|
|
||||||
self._pen = pen = self._Pen()
|
|
||||||
self.value.append(pen)
|
|
||||||
|
|
||||||
|
|
||||||
class PerContourOrComponentPen(PerContourPen):
|
|
||||||
def addComponent(self, glyphName, transformation):
|
|
||||||
self._newItem()
|
|
||||||
self.value[-1].addComponent(glyphName, transformation)
|
|
||||||
|
|
||||||
|
|
||||||
class RecordingPointPen(BasePen):
|
|
||||||
def __init__(self):
|
|
||||||
self.value = []
|
|
||||||
|
|
||||||
def beginPath(self, identifier=None, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def endPath(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def addPoint(self, pt, segmentType=None):
|
|
||||||
self.value.append((pt, False if segmentType is None else True))
|
|
||||||
|
|
||||||
|
|
||||||
@cython.cfunc
|
|
||||||
@cython.inline
|
|
||||||
@cython.locals(
|
|
||||||
s=cython.double,
|
|
||||||
d=cython.double,
|
|
||||||
x0=cython.double,
|
|
||||||
x1=cython.double,
|
|
||||||
)
|
|
||||||
def _vdiff_hypot2(v0, v1):
|
|
||||||
s = 0
|
|
||||||
for x0, x1 in zip(v0, v1):
|
|
||||||
d = x1 - x0
|
|
||||||
s += d * d
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
@cython.cfunc
|
|
||||||
@cython.inline
|
|
||||||
@cython.locals(
|
|
||||||
s=cython.double,
|
|
||||||
d=cython.complex,
|
|
||||||
x0=cython.complex,
|
|
||||||
x1=cython.complex,
|
|
||||||
)
|
|
||||||
def _vdiff_hypot2_complex(v0, v1):
|
|
||||||
s = 0
|
|
||||||
d = 0 # Make Cython happy
|
|
||||||
for x0, x1 in zip(v0, v1):
|
|
||||||
d = x1 - x0
|
|
||||||
s += d.real * d.real + d.imag * d.imag
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
def _matching_cost(G, matching):
|
|
||||||
return sum(G[i][j] for i, j in enumerate(matching))
|
|
||||||
|
|
||||||
|
|
||||||
def min_cost_perfect_bipartite_matching_scipy(G):
|
|
||||||
n = len(G)
|
|
||||||
rows, cols = linear_sum_assignment(G)
|
|
||||||
assert (rows == list(range(n))).all()
|
|
||||||
return list(cols), _matching_cost(G, cols)
|
|
||||||
|
|
||||||
|
|
||||||
def min_cost_perfect_bipartite_matching_munkres(G):
|
|
||||||
n = len(G)
|
|
||||||
cols = [None] * n
|
|
||||||
for row, col in Munkres().compute(G):
|
|
||||||
cols[row] = col
|
|
||||||
return cols, _matching_cost(G, cols)
|
|
||||||
|
|
||||||
|
|
||||||
def min_cost_perfect_bipartite_matching_bruteforce(G):
|
|
||||||
n = len(G)
|
|
||||||
|
|
||||||
if n > 6:
|
|
||||||
raise Exception("Install Python module 'munkres' or 'scipy >= 0.17.0'")
|
|
||||||
|
|
||||||
# Otherwise just brute-force
|
|
||||||
permutations = itertools.permutations(range(n))
|
|
||||||
best = list(next(permutations))
|
|
||||||
best_cost = _matching_cost(G, best)
|
|
||||||
for p in permutations:
|
|
||||||
cost = _matching_cost(G, p)
|
|
||||||
if cost < best_cost:
|
|
||||||
best, best_cost = list(p), cost
|
|
||||||
return best, best_cost
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from scipy.optimize import linear_sum_assignment
|
|
||||||
|
|
||||||
min_cost_perfect_bipartite_matching = min_cost_perfect_bipartite_matching_scipy
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
from munkres import Munkres
|
|
||||||
|
|
||||||
min_cost_perfect_bipartite_matching = (
|
|
||||||
min_cost_perfect_bipartite_matching_munkres
|
|
||||||
)
|
|
||||||
except ImportError:
|
|
||||||
min_cost_perfect_bipartite_matching = (
|
|
||||||
min_cost_perfect_bipartite_matching_bruteforce
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test(glyphsets, glyphs=None, names=None, ignore_missing=False):
|
|
||||||
if names is None:
|
|
||||||
names = 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()}
|
|
||||||
|
|
||||||
hist = []
|
|
||||||
problems = OrderedDict()
|
|
||||||
|
|
||||||
def add_problem(glyphname, problem):
|
|
||||||
problems.setdefault(glyphname, []).append(problem)
|
|
||||||
|
|
||||||
for glyph_name in glyphs:
|
|
||||||
try:
|
|
||||||
m0idx = 0
|
|
||||||
allVectors = []
|
|
||||||
allNodeTypes = []
|
|
||||||
allContourIsomorphisms = []
|
|
||||||
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):
|
|
||||||
if glyph is None:
|
|
||||||
if not ignore_missing:
|
|
||||||
add_problem(glyph_name, {"type": "missing", "master": name})
|
|
||||||
allNodeTypes.append(None)
|
|
||||||
allVectors.append(None)
|
|
||||||
allContourIsomorphisms.append(None)
|
|
||||||
continue
|
|
||||||
|
|
||||||
perContourPen = PerContourOrComponentPen(
|
|
||||||
RecordingPen, glyphset=glyphset
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
glyph.draw(perContourPen, outputImpliedClosingLine=True)
|
|
||||||
except TypeError:
|
|
||||||
glyph.draw(perContourPen)
|
|
||||||
contourPens = perContourPen.value
|
|
||||||
del perContourPen
|
|
||||||
|
|
||||||
contourVectors = []
|
|
||||||
contourIsomorphisms = []
|
|
||||||
nodeTypes = []
|
|
||||||
allNodeTypes.append(nodeTypes)
|
|
||||||
allVectors.append(contourVectors)
|
|
||||||
allContourIsomorphisms.append(contourIsomorphisms)
|
|
||||||
for ix, contour in enumerate(contourPens):
|
|
||||||
nodeVecs = tuple(instruction[0] for instruction in contour.value)
|
|
||||||
nodeTypes.append(nodeVecs)
|
|
||||||
|
|
||||||
stats = StatisticsPen(glyphset=glyphset)
|
|
||||||
try:
|
|
||||||
contour.replay(stats)
|
|
||||||
except OpenContourError as e:
|
|
||||||
add_problem(
|
|
||||||
glyph_name,
|
|
||||||
{"master": name, "contour": ix, "type": "open_path"},
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
size = math.sqrt(abs(stats.area)) * 0.5
|
|
||||||
vector = (
|
|
||||||
int(size),
|
|
||||||
int(stats.meanX),
|
|
||||||
int(stats.meanY),
|
|
||||||
int(stats.stddevX * 2),
|
|
||||||
int(stats.stddevY * 2),
|
|
||||||
int(stats.correlation * size),
|
|
||||||
)
|
|
||||||
contourVectors.append(vector)
|
|
||||||
# print(vector)
|
|
||||||
|
|
||||||
# Check starting point
|
|
||||||
if nodeVecs[0] == "addComponent":
|
|
||||||
continue
|
|
||||||
assert nodeVecs[0] == "moveTo"
|
|
||||||
assert nodeVecs[-1] in ("closePath", "endPath")
|
|
||||||
points = RecordingPointPen()
|
|
||||||
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.
|
|
||||||
bits = 0
|
|
||||||
for pt, b in points.value:
|
|
||||||
bits = (bits << 1) | b
|
|
||||||
n = len(points.value)
|
|
||||||
mask = (1 << n) - 1
|
|
||||||
isomorphisms = []
|
|
||||||
contourIsomorphisms.append(isomorphisms)
|
|
||||||
for i in range(n):
|
|
||||||
b = ((bits << i) & mask) | ((bits >> (n - i)))
|
|
||||||
if b == bits:
|
|
||||||
isomorphisms.append(
|
|
||||||
_rot_list([complex(*pt) for pt, bl in points.value], i)
|
|
||||||
)
|
|
||||||
# Add mirrored rotations
|
|
||||||
mirrored = list(reversed(points.value))
|
|
||||||
reversed_bits = 0
|
|
||||||
for pt, b in mirrored:
|
|
||||||
reversed_bits = (reversed_bits << 1) | b
|
|
||||||
for i in range(n):
|
|
||||||
b = ((reversed_bits << i) & mask) | ((reversed_bits >> (n - i)))
|
|
||||||
if b == bits:
|
|
||||||
isomorphisms.append(
|
|
||||||
_rot_list([complex(*pt) for pt, bl in mirrored], i)
|
|
||||||
)
|
|
||||||
|
|
||||||
# m0idx should be the index of the first non-None item in allNodeTypes,
|
|
||||||
# else give it the last item.
|
|
||||||
m0idx = next(
|
|
||||||
(i for i, x in enumerate(allNodeTypes) if x is not None),
|
|
||||||
len(allNodeTypes) - 1,
|
|
||||||
)
|
|
||||||
# m0 is the first non-None item in allNodeTypes, or last one if all None
|
|
||||||
m0 = allNodeTypes[m0idx]
|
|
||||||
for i, m1 in enumerate(allNodeTypes[m0idx + 1 :]):
|
|
||||||
if m1 is None:
|
|
||||||
continue
|
|
||||||
if len(m0) != len(m1):
|
|
||||||
add_problem(
|
|
||||||
glyph_name,
|
|
||||||
{
|
|
||||||
"type": "path_count",
|
|
||||||
"master_1": names[m0idx],
|
|
||||||
"master_2": names[m0idx + i + 1],
|
|
||||||
"value_1": len(m0),
|
|
||||||
"value_2": len(m1),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if m0 == m1:
|
|
||||||
continue
|
|
||||||
for pathIx, (nodes1, nodes2) in enumerate(zip(m0, m1)):
|
|
||||||
if nodes1 == nodes2:
|
|
||||||
continue
|
|
||||||
if len(nodes1) != len(nodes2):
|
|
||||||
add_problem(
|
|
||||||
glyph_name,
|
|
||||||
{
|
|
||||||
"type": "node_count",
|
|
||||||
"path": pathIx,
|
|
||||||
"master_1": names[m0idx],
|
|
||||||
"master_2": names[m0idx + i + 1],
|
|
||||||
"value_1": len(nodes1),
|
|
||||||
"value_2": len(nodes2),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
for nodeIx, (n1, n2) in enumerate(zip(nodes1, nodes2)):
|
|
||||||
if n1 != n2:
|
|
||||||
add_problem(
|
|
||||||
glyph_name,
|
|
||||||
{
|
|
||||||
"type": "node_incompatibility",
|
|
||||||
"path": pathIx,
|
|
||||||
"node": nodeIx,
|
|
||||||
"master_1": names[m0idx],
|
|
||||||
"master_2": names[m0idx + i + 1],
|
|
||||||
"value_1": n1,
|
|
||||||
"value_2": n2,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# m0idx should be the index of the first non-None item in allVectors,
|
|
||||||
# else give it the last item.
|
|
||||||
m0idx = next(
|
|
||||||
(i for i, x in enumerate(allVectors) if x is not None),
|
|
||||||
len(allVectors) - 1,
|
|
||||||
)
|
|
||||||
# m0 is the first non-None item in allVectors, or last one if all None
|
|
||||||
m0 = allVectors[m0idx]
|
|
||||||
if m0 is not None and len(m0) > 1:
|
|
||||||
for i, m1 in enumerate(allVectors[m0idx + 1 :]):
|
|
||||||
if m1 is None:
|
|
||||||
continue
|
|
||||||
if len(m0) != len(m1):
|
|
||||||
# We already reported this
|
|
||||||
continue
|
|
||||||
costs = [[_vdiff_hypot2(v0, v1) for v1 in m1] for v0 in m0]
|
|
||||||
matching, matching_cost = min_cost_perfect_bipartite_matching(costs)
|
|
||||||
identity_matching = list(range(len(m0)))
|
|
||||||
identity_cost = sum(costs[i][i] for i in range(len(m0)))
|
|
||||||
if (
|
|
||||||
matching != identity_matching
|
|
||||||
and matching_cost < identity_cost * 0.95
|
|
||||||
):
|
|
||||||
add_problem(
|
|
||||||
glyph_name,
|
|
||||||
{
|
|
||||||
"type": "contour_order",
|
|
||||||
"master_1": names[m0idx],
|
|
||||||
"master_2": names[m0idx + i + 1],
|
|
||||||
"value_1": list(range(len(m0))),
|
|
||||||
"value_2": matching,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
# m0idx should be the index of the first non-None item in allContourIsomorphisms,
|
|
||||||
# else give it the first index of None, which is likely 0
|
|
||||||
m0idx = allContourIsomorphisms.index(
|
|
||||||
next((x for x in allContourIsomorphisms if x is not None), None)
|
|
||||||
)
|
|
||||||
# m0 is the first non-None item in allContourIsomorphisms, or the first item if all are None
|
|
||||||
m0 = allContourIsomorphisms[m0idx]
|
|
||||||
if m0:
|
|
||||||
for i, m1 in enumerate(allContourIsomorphisms[m0idx + 1 :]):
|
|
||||||
if m1 is None:
|
|
||||||
continue
|
|
||||||
if len(m0) != len(m1):
|
|
||||||
# We already reported this
|
|
||||||
continue
|
|
||||||
for ix, (contour0, contour1) in enumerate(zip(m0, m1)):
|
|
||||||
c0 = contour0[0]
|
|
||||||
costs = [
|
|
||||||
v
|
|
||||||
for v in (_vdiff_hypot2_complex(c0, c1) for c1 in contour1)
|
|
||||||
]
|
|
||||||
min_cost = min(costs)
|
|
||||||
first_cost = costs[0]
|
|
||||||
if min_cost < first_cost * 0.95:
|
|
||||||
add_problem(
|
|
||||||
glyph_name,
|
|
||||||
{
|
|
||||||
"type": "wrong_start_point",
|
|
||||||
"contour": ix,
|
|
||||||
"master_1": names[m0idx],
|
|
||||||
"master_2": names[m0idx + i + 1],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
except ValueError as e:
|
|
||||||
add_problem(
|
|
||||||
glyph_name,
|
|
||||||
{"type": "math_error", "master": name, "error": e},
|
|
||||||
)
|
|
||||||
return problems
|
|
@ -6,8 +6,389 @@ Call as:
|
|||||||
$ fonttools varLib.interpolatable font1 font2 ...
|
$ fonttools varLib.interpolatable font1 font2 ...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from ._interpolatable import test
|
from fontTools.pens.basePen import AbstractPen, BasePen
|
||||||
from collections import defaultdict
|
from fontTools.pens.pointPen import SegmentToPointPen
|
||||||
|
from fontTools.pens.recordingPen import RecordingPen
|
||||||
|
from fontTools.pens.statisticsPen import StatisticsPen
|
||||||
|
from fontTools.pens.momentsPen import OpenContourError
|
||||||
|
from collections import OrderedDict, defaultdict
|
||||||
|
import math
|
||||||
|
import itertools
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def _rot_list(l, k):
|
||||||
|
"""Rotate list by k items forward. Ie. item at position 0 will be
|
||||||
|
at position k in returned list. Negative k is allowed."""
|
||||||
|
n = len(l)
|
||||||
|
k %= n
|
||||||
|
if not k:
|
||||||
|
return l
|
||||||
|
return l[n - k :] + l[: n - k]
|
||||||
|
|
||||||
|
|
||||||
|
class PerContourPen(BasePen):
|
||||||
|
def __init__(self, Pen, glyphset=None):
|
||||||
|
BasePen.__init__(self, glyphset)
|
||||||
|
self._glyphset = glyphset
|
||||||
|
self._Pen = Pen
|
||||||
|
self._pen = None
|
||||||
|
self.value = []
|
||||||
|
|
||||||
|
def _moveTo(self, p0):
|
||||||
|
self._newItem()
|
||||||
|
self._pen.moveTo(p0)
|
||||||
|
|
||||||
|
def _lineTo(self, p1):
|
||||||
|
self._pen.lineTo(p1)
|
||||||
|
|
||||||
|
def _qCurveToOne(self, p1, p2):
|
||||||
|
self._pen.qCurveTo(p1, p2)
|
||||||
|
|
||||||
|
def _curveToOne(self, p1, p2, p3):
|
||||||
|
self._pen.curveTo(p1, p2, p3)
|
||||||
|
|
||||||
|
def _closePath(self):
|
||||||
|
self._pen.closePath()
|
||||||
|
self._pen = None
|
||||||
|
|
||||||
|
def _endPath(self):
|
||||||
|
self._pen.endPath()
|
||||||
|
self._pen = None
|
||||||
|
|
||||||
|
def _newItem(self):
|
||||||
|
self._pen = pen = self._Pen()
|
||||||
|
self.value.append(pen)
|
||||||
|
|
||||||
|
|
||||||
|
class PerContourOrComponentPen(PerContourPen):
|
||||||
|
def addComponent(self, glyphName, transformation):
|
||||||
|
self._newItem()
|
||||||
|
self.value[-1].addComponent(glyphName, transformation)
|
||||||
|
|
||||||
|
|
||||||
|
class RecordingPointPen(BasePen):
|
||||||
|
def __init__(self):
|
||||||
|
self.value = []
|
||||||
|
|
||||||
|
def beginPath(self, identifier=None, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def endPath(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def addPoint(self, pt, segmentType=None):
|
||||||
|
self.value.append((pt, False if segmentType is None else True))
|
||||||
|
|
||||||
|
|
||||||
|
def _vdiff_hypot2(v0, v1):
|
||||||
|
s = 0
|
||||||
|
for x0, x1 in zip(v0, v1):
|
||||||
|
d = x1 - x0
|
||||||
|
s += d * d
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def _vdiff_hypot2_complex(v0, v1):
|
||||||
|
s = 0
|
||||||
|
for x0, x1 in zip(v0, v1):
|
||||||
|
d = x1 - x0
|
||||||
|
s += d.real * d.real + d.imag * d.imag
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def _matching_cost(G, matching):
|
||||||
|
return sum(G[i][j] for i, j in enumerate(matching))
|
||||||
|
|
||||||
|
|
||||||
|
def min_cost_perfect_bipartite_matching_scipy(G):
|
||||||
|
n = len(G)
|
||||||
|
rows, cols = linear_sum_assignment(G)
|
||||||
|
assert (rows == list(range(n))).all()
|
||||||
|
return list(cols), _matching_cost(G, cols)
|
||||||
|
|
||||||
|
|
||||||
|
def min_cost_perfect_bipartite_matching_munkres(G):
|
||||||
|
n = len(G)
|
||||||
|
cols = [None] * n
|
||||||
|
for row, col in Munkres().compute(G):
|
||||||
|
cols[row] = col
|
||||||
|
return cols, _matching_cost(G, cols)
|
||||||
|
|
||||||
|
|
||||||
|
def min_cost_perfect_bipartite_matching_bruteforce(G):
|
||||||
|
n = len(G)
|
||||||
|
|
||||||
|
if n > 6:
|
||||||
|
raise Exception("Install Python module 'munkres' or 'scipy >= 0.17.0'")
|
||||||
|
|
||||||
|
# Otherwise just brute-force
|
||||||
|
permutations = itertools.permutations(range(n))
|
||||||
|
best = list(next(permutations))
|
||||||
|
best_cost = _matching_cost(G, best)
|
||||||
|
for p in permutations:
|
||||||
|
cost = _matching_cost(G, p)
|
||||||
|
if cost < best_cost:
|
||||||
|
best, best_cost = list(p), cost
|
||||||
|
return best, best_cost
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from scipy.optimize import linear_sum_assignment
|
||||||
|
|
||||||
|
min_cost_perfect_bipartite_matching = min_cost_perfect_bipartite_matching_scipy
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from munkres import Munkres
|
||||||
|
|
||||||
|
min_cost_perfect_bipartite_matching = (
|
||||||
|
min_cost_perfect_bipartite_matching_munkres
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
min_cost_perfect_bipartite_matching = (
|
||||||
|
min_cost_perfect_bipartite_matching_bruteforce
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test(glyphsets, glyphs=None, names=None, ignore_missing=False):
|
||||||
|
if names is None:
|
||||||
|
names = 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()}
|
||||||
|
|
||||||
|
hist = []
|
||||||
|
problems = OrderedDict()
|
||||||
|
|
||||||
|
def add_problem(glyphname, problem):
|
||||||
|
problems.setdefault(glyphname, []).append(problem)
|
||||||
|
|
||||||
|
for glyph_name in glyphs:
|
||||||
|
try:
|
||||||
|
m0idx = 0
|
||||||
|
allVectors = []
|
||||||
|
allNodeTypes = []
|
||||||
|
allContourIsomorphisms = []
|
||||||
|
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):
|
||||||
|
if glyph is None:
|
||||||
|
if not ignore_missing:
|
||||||
|
add_problem(glyph_name, {"type": "missing", "master": name})
|
||||||
|
allNodeTypes.append(None)
|
||||||
|
allVectors.append(None)
|
||||||
|
allContourIsomorphisms.append(None)
|
||||||
|
continue
|
||||||
|
|
||||||
|
perContourPen = PerContourOrComponentPen(
|
||||||
|
RecordingPen, glyphset=glyphset
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
glyph.draw(perContourPen, outputImpliedClosingLine=True)
|
||||||
|
except TypeError:
|
||||||
|
glyph.draw(perContourPen)
|
||||||
|
contourPens = perContourPen.value
|
||||||
|
del perContourPen
|
||||||
|
|
||||||
|
contourVectors = []
|
||||||
|
contourIsomorphisms = []
|
||||||
|
nodeTypes = []
|
||||||
|
allNodeTypes.append(nodeTypes)
|
||||||
|
allVectors.append(contourVectors)
|
||||||
|
allContourIsomorphisms.append(contourIsomorphisms)
|
||||||
|
for ix, contour in enumerate(contourPens):
|
||||||
|
nodeVecs = tuple(instruction[0] for instruction in contour.value)
|
||||||
|
nodeTypes.append(nodeVecs)
|
||||||
|
|
||||||
|
stats = StatisticsPen(glyphset=glyphset)
|
||||||
|
try:
|
||||||
|
contour.replay(stats)
|
||||||
|
except OpenContourError as e:
|
||||||
|
add_problem(
|
||||||
|
glyph_name,
|
||||||
|
{"master": name, "contour": ix, "type": "open_path"},
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
size = math.sqrt(abs(stats.area)) * 0.5
|
||||||
|
vector = (
|
||||||
|
int(size),
|
||||||
|
int(stats.meanX),
|
||||||
|
int(stats.meanY),
|
||||||
|
int(stats.stddevX * 2),
|
||||||
|
int(stats.stddevY * 2),
|
||||||
|
int(stats.correlation * size),
|
||||||
|
)
|
||||||
|
contourVectors.append(vector)
|
||||||
|
# print(vector)
|
||||||
|
|
||||||
|
# Check starting point
|
||||||
|
if nodeVecs[0] == "addComponent":
|
||||||
|
continue
|
||||||
|
assert nodeVecs[0] == "moveTo"
|
||||||
|
assert nodeVecs[-1] in ("closePath", "endPath")
|
||||||
|
points = RecordingPointPen()
|
||||||
|
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.
|
||||||
|
bits = 0
|
||||||
|
for pt, b in points.value:
|
||||||
|
bits = (bits << 1) | b
|
||||||
|
n = len(points.value)
|
||||||
|
mask = (1 << n) - 1
|
||||||
|
isomorphisms = []
|
||||||
|
contourIsomorphisms.append(isomorphisms)
|
||||||
|
for i in range(n):
|
||||||
|
b = ((bits << i) & mask) | ((bits >> (n - i)))
|
||||||
|
if b == bits:
|
||||||
|
isomorphisms.append(
|
||||||
|
_rot_list([complex(*pt) for pt, bl in points.value], i)
|
||||||
|
)
|
||||||
|
# Add mirrored rotations
|
||||||
|
mirrored = list(reversed(points.value))
|
||||||
|
reversed_bits = 0
|
||||||
|
for pt, b in mirrored:
|
||||||
|
reversed_bits = (reversed_bits << 1) | b
|
||||||
|
for i in range(n):
|
||||||
|
b = ((reversed_bits << i) & mask) | ((reversed_bits >> (n - i)))
|
||||||
|
if b == bits:
|
||||||
|
isomorphisms.append(
|
||||||
|
_rot_list([complex(*pt) for pt, bl in mirrored], i)
|
||||||
|
)
|
||||||
|
|
||||||
|
# m0idx should be the index of the first non-None item in allNodeTypes,
|
||||||
|
# else give it the last item.
|
||||||
|
m0idx = next(
|
||||||
|
(i for i, x in enumerate(allNodeTypes) if x is not None),
|
||||||
|
len(allNodeTypes) - 1,
|
||||||
|
)
|
||||||
|
# m0 is the first non-None item in allNodeTypes, or last one if all None
|
||||||
|
m0 = allNodeTypes[m0idx]
|
||||||
|
for i, m1 in enumerate(allNodeTypes[m0idx + 1 :]):
|
||||||
|
if m1 is None:
|
||||||
|
continue
|
||||||
|
if len(m0) != len(m1):
|
||||||
|
add_problem(
|
||||||
|
glyph_name,
|
||||||
|
{
|
||||||
|
"type": "path_count",
|
||||||
|
"master_1": names[m0idx],
|
||||||
|
"master_2": names[m0idx + i + 1],
|
||||||
|
"value_1": len(m0),
|
||||||
|
"value_2": len(m1),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if m0 == m1:
|
||||||
|
continue
|
||||||
|
for pathIx, (nodes1, nodes2) in enumerate(zip(m0, m1)):
|
||||||
|
if nodes1 == nodes2:
|
||||||
|
continue
|
||||||
|
if len(nodes1) != len(nodes2):
|
||||||
|
add_problem(
|
||||||
|
glyph_name,
|
||||||
|
{
|
||||||
|
"type": "node_count",
|
||||||
|
"path": pathIx,
|
||||||
|
"master_1": names[m0idx],
|
||||||
|
"master_2": names[m0idx + i + 1],
|
||||||
|
"value_1": len(nodes1),
|
||||||
|
"value_2": len(nodes2),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
for nodeIx, (n1, n2) in enumerate(zip(nodes1, nodes2)):
|
||||||
|
if n1 != n2:
|
||||||
|
add_problem(
|
||||||
|
glyph_name,
|
||||||
|
{
|
||||||
|
"type": "node_incompatibility",
|
||||||
|
"path": pathIx,
|
||||||
|
"node": nodeIx,
|
||||||
|
"master_1": names[m0idx],
|
||||||
|
"master_2": names[m0idx + i + 1],
|
||||||
|
"value_1": n1,
|
||||||
|
"value_2": n2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# m0idx should be the index of the first non-None item in allVectors,
|
||||||
|
# else give it the last item.
|
||||||
|
m0idx = next(
|
||||||
|
(i for i, x in enumerate(allVectors) if x is not None),
|
||||||
|
len(allVectors) - 1,
|
||||||
|
)
|
||||||
|
# m0 is the first non-None item in allVectors, or last one if all None
|
||||||
|
m0 = allVectors[m0idx]
|
||||||
|
if m0 is not None and len(m0) > 1:
|
||||||
|
for i, m1 in enumerate(allVectors[m0idx + 1 :]):
|
||||||
|
if m1 is None:
|
||||||
|
continue
|
||||||
|
if len(m0) != len(m1):
|
||||||
|
# We already reported this
|
||||||
|
continue
|
||||||
|
costs = [[_vdiff_hypot2(v0, v1) for v1 in m1] for v0 in m0]
|
||||||
|
matching, matching_cost = min_cost_perfect_bipartite_matching(costs)
|
||||||
|
identity_matching = list(range(len(m0)))
|
||||||
|
identity_cost = sum(costs[i][i] for i in range(len(m0)))
|
||||||
|
if (
|
||||||
|
matching != identity_matching
|
||||||
|
and matching_cost < identity_cost * 0.95
|
||||||
|
):
|
||||||
|
add_problem(
|
||||||
|
glyph_name,
|
||||||
|
{
|
||||||
|
"type": "contour_order",
|
||||||
|
"master_1": names[m0idx],
|
||||||
|
"master_2": names[m0idx + i + 1],
|
||||||
|
"value_1": list(range(len(m0))),
|
||||||
|
"value_2": matching,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
# m0idx should be the index of the first non-None item in allContourIsomorphisms,
|
||||||
|
# else give it the first index of None, which is likely 0
|
||||||
|
m0idx = allContourIsomorphisms.index(
|
||||||
|
next((x for x in allContourIsomorphisms if x is not None), None)
|
||||||
|
)
|
||||||
|
# m0 is the first non-None item in allContourIsomorphisms, or the first item if all are None
|
||||||
|
m0 = allContourIsomorphisms[m0idx]
|
||||||
|
if m0:
|
||||||
|
for i, m1 in enumerate(allContourIsomorphisms[m0idx + 1 :]):
|
||||||
|
if m1 is None:
|
||||||
|
continue
|
||||||
|
if len(m0) != len(m1):
|
||||||
|
# We already reported this
|
||||||
|
continue
|
||||||
|
for ix, (contour0, contour1) in enumerate(zip(m0, m1)):
|
||||||
|
c0 = contour0[0]
|
||||||
|
costs = [
|
||||||
|
v
|
||||||
|
for v in (_vdiff_hypot2_complex(c0, c1) for c1 in contour1)
|
||||||
|
]
|
||||||
|
min_cost = min(costs)
|
||||||
|
first_cost = costs[0]
|
||||||
|
if min_cost < first_cost * 0.95:
|
||||||
|
add_problem(
|
||||||
|
glyph_name,
|
||||||
|
{
|
||||||
|
"type": "wrong_start_point",
|
||||||
|
"contour": ix,
|
||||||
|
"master_1": names[m0idx],
|
||||||
|
"master_2": names[m0idx + i + 1],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
add_problem(
|
||||||
|
glyph_name,
|
||||||
|
{"type": "math_error", "master": name, "error": e},
|
||||||
|
)
|
||||||
|
return problems
|
||||||
|
|
||||||
|
|
||||||
def recursivelyAddGlyph(glyphname, glyphset, ttGlyphSet, glyf):
|
def recursivelyAddGlyph(glyphname, glyphset, ttGlyphSet, glyf):
|
||||||
|
6
setup.py
6
setup.py
@ -82,12 +82,6 @@ if with_cython is True or (with_cython is None and has_cython):
|
|||||||
ext_modules.append(
|
ext_modules.append(
|
||||||
Extension("fontTools.pens.momentsPen", ["Lib/fontTools/pens/momentsPen.py"]),
|
Extension("fontTools.pens.momentsPen", ["Lib/fontTools/pens/momentsPen.py"]),
|
||||||
)
|
)
|
||||||
ext_modules.append(
|
|
||||||
Extension(
|
|
||||||
"fontTools.varLib._interpolatable",
|
|
||||||
["Lib/fontTools/varLib/_interpolatable.py"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
ext_modules.append(
|
ext_modules.append(
|
||||||
Extension("fontTools.varLib.iup", ["Lib/fontTools/varLib/iup.py"]),
|
Extension("fontTools.varLib.iup", ["Lib/fontTools/varLib/iup.py"]),
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user