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.glyphSet = glyphSet
|
||||
|
||||
def draw(self, pen):
|
||||
def draw(self, pen, outputImpliedClosingLine=False):
|
||||
"""
|
||||
Draw this glyph onto a *FontTools* Pen.
|
||||
"""
|
||||
pointPen = PointToSegmentPen(pen)
|
||||
pointPen = PointToSegmentPen(pen, outputImpliedClosingLine=outputImpliedClosingLine)
|
||||
self.drawPoints(pointPen)
|
||||
|
||||
def drawPoints(self, pointPen):
|
||||
|
@ -7,6 +7,7 @@ $ fonttools varLib.interpolatable font1 font2 ...
|
||||
"""
|
||||
|
||||
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
|
||||
@ -14,6 +15,14 @@ from collections import OrderedDict
|
||||
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):
|
||||
@ -55,6 +64,21 @@ class PerContourOrComponentPen(PerContourPen):
|
||||
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):
|
||||
return tuple(b - a for a, b in zip(v0, v1))
|
||||
|
||||
@ -65,6 +89,12 @@ def _vlen(vec):
|
||||
v += x * x
|
||||
return v
|
||||
|
||||
def _complex_vlen(vec):
|
||||
v = 0
|
||||
for x in vec:
|
||||
v += abs(x) * abs(x)
|
||||
return v
|
||||
|
||||
|
||||
def _matching_cost(G, matching):
|
||||
return sum(G[i][j] for i, j in enumerate(matching))
|
||||
@ -125,6 +155,7 @@ def test(glyphsets, glyphs=None, names=None):
|
||||
try:
|
||||
allVectors = []
|
||||
allNodeTypes = []
|
||||
allContourIsomorphisms = []
|
||||
for glyphset, name in zip(glyphsets, names):
|
||||
# print('.', end='')
|
||||
if glyph_name not in glyphset:
|
||||
@ -135,18 +166,24 @@ def test(glyphsets, glyphs=None, names=None):
|
||||
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):
|
||||
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)
|
||||
try:
|
||||
contour.replay(stats)
|
||||
@ -168,6 +205,38 @@ def test(glyphsets, glyphs=None, names=None):
|
||||
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))
|
||||
|
||||
# Check each master against the next one in the list.
|
||||
for i, (m0, m1) in enumerate(zip(allNodeTypes[:-1], allNodeTypes[1:])):
|
||||
if len(m0) != len(m1):
|
||||
@ -223,7 +292,9 @@ def test(glyphsets, glyphs=None, names=None):
|
||||
continue
|
||||
costs = [[_vlen(_vdiff(v0, v1)) for v1 in m1] for v0 in m0]
|
||||
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(
|
||||
glyph_name,
|
||||
{
|
||||
@ -235,21 +306,25 @@ def test(glyphsets, glyphs=None, names=None):
|
||||
},
|
||||
)
|
||||
break
|
||||
upem = 2048
|
||||
item_cost = round(
|
||||
(matching_cost / len(m0) / len(m0[0])) ** 0.5 / upem * 100
|
||||
)
|
||||
hist.append(item_cost)
|
||||
threshold = 7
|
||||
if item_cost >= threshold:
|
||||
|
||||
for i, (m0, m1) in enumerate(zip(allContourIsomorphisms[:-1], allContourIsomorphisms[1:])):
|
||||
if len(m0) != len(m1):
|
||||
# We already reported this
|
||||
continue
|
||||
if not m0:
|
||||
continue
|
||||
for contour0,contour1 in zip(m0,m1):
|
||||
c0 = contour0[0]
|
||||
costs = [v for v in (_complex_vlen(_vdiff(c0, c1)) for c1 in contour1)]
|
||||
min_cost = min(costs)
|
||||
first_cost = costs[0]
|
||||
if min_cost < first_cost * .95:
|
||||
add_problem(
|
||||
glyph_name,
|
||||
{
|
||||
"type": "high_cost",
|
||||
"type": "wrong_start_point",
|
||||
"master_1": names[i],
|
||||
"master_2": names[i + 1],
|
||||
"value_1": item_cost,
|
||||
"value_2": threshold,
|
||||
},
|
||||
)
|
||||
|
||||
@ -351,14 +426,12 @@ def main(args=None):
|
||||
p["master_2"],
|
||||
)
|
||||
)
|
||||
if p["type"] == "high_cost":
|
||||
if p["type"] == "wrong_start_point":
|
||||
print(
|
||||
" Interpolation has high cost: cost of %s to %s = %i, threshold %i"
|
||||
" Contour start point differs: %s, %s"
|
||||
% (
|
||||
p["master_1"],
|
||||
p["master_2"],
|
||||
p["value_1"],
|
||||
p["value_2"],
|
||||
)
|
||||
)
|
||||
if problems:
|
||||
|
Loading…
x
Reference in New Issue
Block a user