Skip to content

Commit cace698

Browse files
authored
Merge pull request #2571 from fonttools/interpolatable-contour-starting-point
[varLib.interpolatable] Check for wrong contour starting point
2 parents 4bb0e77 + f021441 commit cace698

File tree

2 files changed

+101
-28
lines changed

2 files changed

+101
-28
lines changed

Lib/fontTools/ufoLib/glifLib.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,11 @@ def __init__(self, glyphName, glyphSet):
9595
self.glyphName = glyphName
9696
self.glyphSet = glyphSet
9797

98-
def draw(self, pen):
98+
def draw(self, pen, outputImpliedClosingLine=False):
9999
"""
100100
Draw this glyph onto a *FontTools* Pen.
101101
"""
102-
pointPen = PointToSegmentPen(pen)
102+
pointPen = PointToSegmentPen(pen, outputImpliedClosingLine=outputImpliedClosingLine)
103103
self.drawPoints(pointPen)
104104

105105
def drawPoints(self, pointPen):

Lib/fontTools/varLib/interpolatable.py

Lines changed: 99 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,22 @@
77
"""
88

99
from fontTools.pens.basePen import AbstractPen, BasePen
10+
from fontTools.pens.pointPen import SegmentToPointPen
1011
from fontTools.pens.recordingPen import RecordingPen
1112
from fontTools.pens.statisticsPen import StatisticsPen
1213
from fontTools.pens.momentsPen import OpenContourError
1314
from collections import OrderedDict
1415
import itertools
1516
import sys
1617

18+
def _rot_list(l, k):
19+
"""Rotate list by k items forward. Ie. item at position 0 will be
20+
at position k in returned list. Negative k is allowed."""
21+
n = len(l)
22+
k %= n
23+
if not k: return l
24+
return l[n-k:] + l[:n-k]
25+
1726

1827
class PerContourPen(BasePen):
1928
def __init__(self, Pen, glyphset=None):
@@ -55,6 +64,21 @@ def addComponent(self, glyphName, transformation):
5564
self.value[-1].addComponent(glyphName, transformation)
5665

5766

67+
class RecordingPointPen(BasePen):
68+
69+
def __init__(self):
70+
self.value = []
71+
72+
def beginPath(self, identifier = None, **kwargs):
73+
pass
74+
75+
def endPath(self) -> None:
76+
pass
77+
78+
def addPoint(self, pt, segmentType=None):
79+
self.value.append((pt, False if segmentType is None else True))
80+
81+
5882
def _vdiff(v0, v1):
5983
return tuple(b - a for a, b in zip(v0, v1))
6084

@@ -65,6 +89,12 @@ def _vlen(vec):
6589
v += x * x
6690
return v
6791

92+
def _complex_vlen(vec):
93+
v = 0
94+
for x in vec:
95+
v += abs(x) * abs(x)
96+
return v
97+
6898

6999
def _matching_cost(G, matching):
70100
return sum(G[i][j] for i, j in enumerate(matching))
@@ -125,6 +155,7 @@ def add_problem(glyphname, problem):
125155
try:
126156
allVectors = []
127157
allNodeTypes = []
158+
allContourIsomorphisms = []
128159
for glyphset, name in zip(glyphsets, names):
129160
# print('.', end='')
130161
if glyph_name not in glyphset:
@@ -135,18 +166,24 @@ def add_problem(glyphname, problem):
135166
perContourPen = PerContourOrComponentPen(
136167
RecordingPen, glyphset=glyphset
137168
)
138-
glyph.draw(perContourPen)
169+
try:
170+
glyph.draw(perContourPen, outputImpliedClosingLine=True)
171+
except TypeError:
172+
glyph.draw(perContourPen)
139173
contourPens = perContourPen.value
140174
del perContourPen
141175

142176
contourVectors = []
177+
contourIsomorphisms = []
143178
nodeTypes = []
144179
allNodeTypes.append(nodeTypes)
145180
allVectors.append(contourVectors)
181+
allContourIsomorphisms.append(contourIsomorphisms)
146182
for ix, contour in enumerate(contourPens):
147-
nodeTypes.append(
148-
tuple(instruction[0] for instruction in contour.value)
149-
)
183+
184+
nodeVecs = tuple(instruction[0] for instruction in contour.value)
185+
nodeTypes.append(nodeVecs)
186+
150187
stats = StatisticsPen(glyphset=glyphset)
151188
try:
152189
contour.replay(stats)
@@ -168,6 +205,38 @@ def add_problem(glyphname, problem):
168205
contourVectors.append(vector)
169206
# print(vector)
170207

208+
# Check starting point
209+
if nodeVecs[0] == 'addComponent':
210+
continue
211+
assert nodeVecs[0] == 'moveTo'
212+
assert nodeVecs[-1] in ('closePath', 'endPath')
213+
points = RecordingPointPen()
214+
converter = SegmentToPointPen(points, False)
215+
contour.replay(converter)
216+
# points.value is a list of pt,bool where bool is true if on-curve and false if off-curve;
217+
# now check all rotations and mirror-rotations of the contour and build list of isomorphic
218+
# possible starting points.
219+
bits = 0
220+
for pt,b in points.value:
221+
bits = (bits << 1) | b
222+
n = len(points.value)
223+
mask = (1 << n ) - 1
224+
isomorphisms = []
225+
contourIsomorphisms.append(isomorphisms)
226+
for i in range(n):
227+
b = ((bits << i) & mask) | ((bits >> (n - i)))
228+
if b == bits:
229+
isomorphisms.append(_rot_list ([complex(*pt) for pt,bl in points.value], i))
230+
# Add mirrored rotations
231+
mirrored = list(reversed(points.value))
232+
reversed_bits = 0
233+
for pt,b in mirrored:
234+
reversed_bits = (reversed_bits << 1) | b
235+
for i in range(n):
236+
b = ((reversed_bits << i) & mask) | ((reversed_bits >> (n - i)))
237+
if b == bits:
238+
isomorphisms.append(_rot_list ([complex(*pt) for pt,bl in mirrored], i))
239+
171240
# Check each master against the next one in the list.
172241
for i, (m0, m1) in enumerate(zip(allNodeTypes[:-1], allNodeTypes[1:])):
173242
if len(m0) != len(m1):
@@ -223,7 +292,9 @@ def add_problem(glyphname, problem):
223292
continue
224293
costs = [[_vlen(_vdiff(v0, v1)) for v1 in m1] for v0 in m0]
225294
matching, matching_cost = min_cost_perfect_bipartite_matching(costs)
226-
if matching != list(range(len(m0))):
295+
identity_matching = list(range(len(m0)))
296+
identity_cost = sum(costs[i][i] for i in range(len(m0)))
297+
if matching != identity_matching and matching_cost < identity_cost * .95:
227298
add_problem(
228299
glyph_name,
229300
{
@@ -235,23 +306,27 @@ def add_problem(glyphname, problem):
235306
},
236307
)
237308
break
238-
upem = 2048
239-
item_cost = round(
240-
(matching_cost / len(m0) / len(m0[0])) ** 0.5 / upem * 100
241-
)
242-
hist.append(item_cost)
243-
threshold = 7
244-
if item_cost >= threshold:
245-
add_problem(
246-
glyph_name,
247-
{
248-
"type": "high_cost",
249-
"master_1": names[i],
250-
"master_2": names[i + 1],
251-
"value_1": item_cost,
252-
"value_2": threshold,
253-
},
254-
)
309+
310+
for i, (m0, m1) in enumerate(zip(allContourIsomorphisms[:-1], allContourIsomorphisms[1:])):
311+
if len(m0) != len(m1):
312+
# We already reported this
313+
continue
314+
if not m0:
315+
continue
316+
for contour0,contour1 in zip(m0,m1):
317+
c0 = contour0[0]
318+
costs = [v for v in (_complex_vlen(_vdiff(c0, c1)) for c1 in contour1)]
319+
min_cost = min(costs)
320+
first_cost = costs[0]
321+
if min_cost < first_cost * .95:
322+
add_problem(
323+
glyph_name,
324+
{
325+
"type": "wrong_start_point",
326+
"master_1": names[i],
327+
"master_2": names[i + 1],
328+
},
329+
)
255330

256331
except ValueError as e:
257332
add_problem(
@@ -351,14 +426,12 @@ def main(args=None):
351426
p["master_2"],
352427
)
353428
)
354-
if p["type"] == "high_cost":
429+
if p["type"] == "wrong_start_point":
355430
print(
356-
" Interpolation has high cost: cost of %s to %s = %i, threshold %i"
431+
" Contour start point differs: %s, %s"
357432
% (
358433
p["master_1"],
359434
p["master_2"],
360-
p["value_1"],
361-
p["value_2"],
362435
)
363436
)
364437
if problems:

0 commit comments

Comments
 (0)