Merge pull request #2571 from fonttools/interpolatable-contour-starting-point
[varLib.interpolatable] Check for wrong contour starting point
This commit is contained in:
commit
cace698bb0
@ -95,11 +95,11 @@ class Glyph:
|
|||||||
self.glyphName = glyphName
|
self.glyphName = glyphName
|
||||||
self.glyphSet = glyphSet
|
self.glyphSet = glyphSet
|
||||||
|
|
||||||
def draw(self, pen):
|
def draw(self, pen, outputImpliedClosingLine=False):
|
||||||
"""
|
"""
|
||||||
Draw this glyph onto a *FontTools* Pen.
|
Draw this glyph onto a *FontTools* Pen.
|
||||||
"""
|
"""
|
||||||
pointPen = PointToSegmentPen(pen)
|
pointPen = PointToSegmentPen(pen, outputImpliedClosingLine=outputImpliedClosingLine)
|
||||||
self.drawPoints(pointPen)
|
self.drawPoints(pointPen)
|
||||||
|
|
||||||
def drawPoints(self, pointPen):
|
def drawPoints(self, pointPen):
|
||||||
|
@ -7,6 +7,7 @@ $ fonttools varLib.interpolatable font1 font2 ...
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from fontTools.pens.basePen import AbstractPen, BasePen
|
from fontTools.pens.basePen import AbstractPen, BasePen
|
||||||
|
from fontTools.pens.pointPen import SegmentToPointPen
|
||||||
from fontTools.pens.recordingPen import RecordingPen
|
from fontTools.pens.recordingPen import RecordingPen
|
||||||
from fontTools.pens.statisticsPen import StatisticsPen
|
from fontTools.pens.statisticsPen import StatisticsPen
|
||||||
from fontTools.pens.momentsPen import OpenContourError
|
from fontTools.pens.momentsPen import OpenContourError
|
||||||
@ -14,6 +15,14 @@ from collections import OrderedDict
|
|||||||
import itertools
|
import itertools
|
||||||
import sys
|
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):
|
class PerContourPen(BasePen):
|
||||||
def __init__(self, Pen, glyphset=None):
|
def __init__(self, Pen, glyphset=None):
|
||||||
@ -55,6 +64,21 @@ class PerContourOrComponentPen(PerContourPen):
|
|||||||
self.value[-1].addComponent(glyphName, transformation)
|
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(v0, v1):
|
def _vdiff(v0, v1):
|
||||||
return tuple(b - a for a, b in zip(v0, v1))
|
return tuple(b - a for a, b in zip(v0, v1))
|
||||||
|
|
||||||
@ -65,6 +89,12 @@ def _vlen(vec):
|
|||||||
v += x * x
|
v += x * x
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
def _complex_vlen(vec):
|
||||||
|
v = 0
|
||||||
|
for x in vec:
|
||||||
|
v += abs(x) * abs(x)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
def _matching_cost(G, matching):
|
def _matching_cost(G, matching):
|
||||||
return sum(G[i][j] for i, j in enumerate(matching))
|
return sum(G[i][j] for i, j in enumerate(matching))
|
||||||
@ -125,6 +155,7 @@ def test(glyphsets, glyphs=None, names=None):
|
|||||||
try:
|
try:
|
||||||
allVectors = []
|
allVectors = []
|
||||||
allNodeTypes = []
|
allNodeTypes = []
|
||||||
|
allContourIsomorphisms = []
|
||||||
for glyphset, name in zip(glyphsets, names):
|
for glyphset, name in zip(glyphsets, names):
|
||||||
# print('.', end='')
|
# print('.', end='')
|
||||||
if glyph_name not in glyphset:
|
if glyph_name not in glyphset:
|
||||||
@ -135,18 +166,24 @@ def test(glyphsets, glyphs=None, names=None):
|
|||||||
perContourPen = PerContourOrComponentPen(
|
perContourPen = PerContourOrComponentPen(
|
||||||
RecordingPen, glyphset=glyphset
|
RecordingPen, glyphset=glyphset
|
||||||
)
|
)
|
||||||
glyph.draw(perContourPen)
|
try:
|
||||||
|
glyph.draw(perContourPen, outputImpliedClosingLine=True)
|
||||||
|
except TypeError:
|
||||||
|
glyph.draw(perContourPen)
|
||||||
contourPens = perContourPen.value
|
contourPens = perContourPen.value
|
||||||
del perContourPen
|
del perContourPen
|
||||||
|
|
||||||
contourVectors = []
|
contourVectors = []
|
||||||
|
contourIsomorphisms = []
|
||||||
nodeTypes = []
|
nodeTypes = []
|
||||||
allNodeTypes.append(nodeTypes)
|
allNodeTypes.append(nodeTypes)
|
||||||
allVectors.append(contourVectors)
|
allVectors.append(contourVectors)
|
||||||
|
allContourIsomorphisms.append(contourIsomorphisms)
|
||||||
for ix, contour in enumerate(contourPens):
|
for ix, contour in enumerate(contourPens):
|
||||||
nodeTypes.append(
|
|
||||||
tuple(instruction[0] for instruction in contour.value)
|
nodeVecs = tuple(instruction[0] for instruction in contour.value)
|
||||||
)
|
nodeTypes.append(nodeVecs)
|
||||||
|
|
||||||
stats = StatisticsPen(glyphset=glyphset)
|
stats = StatisticsPen(glyphset=glyphset)
|
||||||
try:
|
try:
|
||||||
contour.replay(stats)
|
contour.replay(stats)
|
||||||
@ -168,6 +205,38 @@ def test(glyphsets, glyphs=None, names=None):
|
|||||||
contourVectors.append(vector)
|
contourVectors.append(vector)
|
||||||
# print(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))
|
||||||
|
|
||||||
# Check each master against the next one in the list.
|
# Check each master against the next one in the list.
|
||||||
for i, (m0, m1) in enumerate(zip(allNodeTypes[:-1], allNodeTypes[1:])):
|
for i, (m0, m1) in enumerate(zip(allNodeTypes[:-1], allNodeTypes[1:])):
|
||||||
if len(m0) != len(m1):
|
if len(m0) != len(m1):
|
||||||
@ -223,7 +292,9 @@ def test(glyphsets, glyphs=None, names=None):
|
|||||||
continue
|
continue
|
||||||
costs = [[_vlen(_vdiff(v0, v1)) for v1 in m1] for v0 in m0]
|
costs = [[_vlen(_vdiff(v0, v1)) for v1 in m1] for v0 in m0]
|
||||||
matching, matching_cost = min_cost_perfect_bipartite_matching(costs)
|
matching, matching_cost = min_cost_perfect_bipartite_matching(costs)
|
||||||
if matching != list(range(len(m0))):
|
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 * .95:
|
||||||
add_problem(
|
add_problem(
|
||||||
glyph_name,
|
glyph_name,
|
||||||
{
|
{
|
||||||
@ -235,23 +306,27 @@ def test(glyphsets, glyphs=None, names=None):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
upem = 2048
|
|
||||||
item_cost = round(
|
for i, (m0, m1) in enumerate(zip(allContourIsomorphisms[:-1], allContourIsomorphisms[1:])):
|
||||||
(matching_cost / len(m0) / len(m0[0])) ** 0.5 / upem * 100
|
if len(m0) != len(m1):
|
||||||
)
|
# We already reported this
|
||||||
hist.append(item_cost)
|
continue
|
||||||
threshold = 7
|
if not m0:
|
||||||
if item_cost >= threshold:
|
continue
|
||||||
add_problem(
|
for contour0,contour1 in zip(m0,m1):
|
||||||
glyph_name,
|
c0 = contour0[0]
|
||||||
{
|
costs = [v for v in (_complex_vlen(_vdiff(c0, c1)) for c1 in contour1)]
|
||||||
"type": "high_cost",
|
min_cost = min(costs)
|
||||||
"master_1": names[i],
|
first_cost = costs[0]
|
||||||
"master_2": names[i + 1],
|
if min_cost < first_cost * .95:
|
||||||
"value_1": item_cost,
|
add_problem(
|
||||||
"value_2": threshold,
|
glyph_name,
|
||||||
},
|
{
|
||||||
)
|
"type": "wrong_start_point",
|
||||||
|
"master_1": names[i],
|
||||||
|
"master_2": names[i + 1],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
add_problem(
|
add_problem(
|
||||||
@ -351,14 +426,12 @@ def main(args=None):
|
|||||||
p["master_2"],
|
p["master_2"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if p["type"] == "high_cost":
|
if p["type"] == "wrong_start_point":
|
||||||
print(
|
print(
|
||||||
" Interpolation has high cost: cost of %s to %s = %i, threshold %i"
|
" Contour start point differs: %s, %s"
|
||||||
% (
|
% (
|
||||||
p["master_1"],
|
p["master_1"],
|
||||||
p["master_2"],
|
p["master_2"],
|
||||||
p["value_1"],
|
|
||||||
p["value_2"],
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if problems:
|
if problems:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user