77"""
88
99from fontTools .pens .basePen import AbstractPen , BasePen
10+ from fontTools .pens .pointPen import SegmentToPointPen
1011from fontTools .pens .recordingPen import RecordingPen
1112from fontTools .pens .statisticsPen import StatisticsPen
1213from fontTools .pens .momentsPen import OpenContourError
1314from collections import OrderedDict
1415import itertools
1516import 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
1827class 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+
5882def _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
6999def _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