1 """ 2 Tool to find wrong contour order between different masters, and 3 other interpolatability (or lack thereof) issues. 4 5 Call as: 6 $ fonttools varLib.interpolatable font1 font2 ... 7 """ 8 9 from __future__ import print_function, division, absolute_import 10 from fontTools.misc.py23 import * 11 12 from fontTools.pens.basePen import AbstractPen, BasePen 13 from fontTools.pens.recordingPen import RecordingPen 14 from fontTools.pens.statisticsPen import StatisticsPen 15 import itertools 16 17 18 class PerContourPen(BasePen): 19 def __init__(self, Pen, glyphset=None): 20 BasePen.__init__(self, glyphset) 21 self._glyphset = glyphset 22 self._Pen = Pen 23 self._pen = None 24 self.value = [] 25 def _moveTo(self, p0): 26 self._newItem() 27 self._pen.moveTo(p0) 28 def _lineTo(self, p1): 29 self._pen.lineTo(p1) 30 def _qCurveToOne(self, p1, p2): 31 self._pen.qCurveTo(p1, p2) 32 def _curveToOne(self, p1, p2, p3): 33 self._pen.curveTo(p1, p2, p3) 34 def _closePath(self): 35 self._pen.closePath() 36 self._pen = None 37 def _endPath(self): 38 self._pen.endPath() 39 self._pen = None 40 41 def _newItem(self): 42 self._pen = pen = self._Pen() 43 self.value.append(pen) 44 45 class PerContourOrComponentPen(PerContourPen): 46 47 def addComponent(self, glyphName, transformation): 48 self._newItem() 49 self.value[-1].addComponent(glyphName, transformation) 50 51 52 def _vdiff(v0, v1): 53 return tuple(b-a for a,b in zip(v0,v1)) 54 def _vlen(vec): 55 v = 0 56 for x in vec: 57 v += x*x 58 return v 59 60 def _matching_cost(G, matching): 61 return sum(G[i][j] for i,j in enumerate(matching)) 62 63 def min_cost_perfect_bipartite_matching(G): 64 n = len(G) 65 try: 66 from scipy.optimize import linear_sum_assignment 67 rows, cols = linear_sum_assignment(G) 68 assert (rows == list(range(n))).all() 69 return list(cols), _matching_cost(G, cols) 70 except ImportError: 71 pass 72 73 try: 74 from munkres import Munkres 75 cols = [None] * n 76 for row,col in Munkres().compute(G): 77 cols[row] = col 78 return cols, _matching_cost(G, cols) 79 except ImportError: 80 pass 81 82 if n > 6: 83 raise Exception("Install Python module 'munkres' or 'scipy >= 0.17.0'") 84 85 # Otherwise just brute-force 86 permutations = itertools.permutations(range(n)) 87 best = list(next(permutations)) 88 best_cost = _matching_cost(G, best) 89 for p in permutations: 90 cost = _matching_cost(G, p) 91 if cost < best_cost: 92 best, best_cost = list(p), cost 93 return best, best_cost 94 95 96 def test(glyphsets, glyphs=None, names=None): 97 98 if names is None: 99 names = glyphsets 100 if glyphs is None: 101 glyphs = glyphsets[0].keys() 102 103 hist = [] 104 for glyph_name in glyphs: 105 #print() 106 #print(glyph_name) 107 108 try: 109 allVectors = [] 110 for glyphset,name in zip(glyphsets, names): 111 #print('.', end='') 112 glyph = glyphset[glyph_name] 113 114 perContourPen = PerContourOrComponentPen(RecordingPen, glyphset=glyphset) 115 glyph.draw(perContourPen) 116 contourPens = perContourPen.value 117 del perContourPen 118 119 contourVectors = [] 120 allVectors.append(contourVectors) 121 for contour in contourPens: 122 stats = StatisticsPen(glyphset=glyphset) 123 contour.replay(stats) 124 size = abs(stats.area) ** .5 * .5 125 vector = ( 126 int(size), 127 int(stats.meanX), 128 int(stats.meanY), 129 int(stats.stddevX * 2), 130 int(stats.stddevY * 2), 131 int(stats.correlation * size), 132 ) 133 contourVectors.append(vector) 134 #print(vector) 135 136 # Check each master against the next one in the list. 137 for i,(m0,m1) in enumerate(zip(allVectors[:-1],allVectors[1:])): 138 if len(m0) != len(m1): 139 print('%s: %s+%s: Glyphs not compatible!!!!!' % (glyph_name, names[i], names[i+1])) 140 continue 141 if not m0: 142 continue 143 costs = [[_vlen(_vdiff(v0,v1)) for v1 in m1] for v0 in m0] 144 matching, matching_cost = min_cost_perfect_bipartite_matching(costs) 145 if matching != list(range(len(m0))): 146 print('%s: %s+%s: Glyph has wrong contour/component order: %s' % (glyph_name, names[i], names[i+1], matching)) #, m0, m1) 147 break 148 upem = 2048 149 item_cost = round((matching_cost / len(m0) / len(m0[0])) ** .5 / upem * 100) 150 hist.append(item_cost) 151 threshold = 7 152 if item_cost >= threshold: 153 print('%s: %s+%s: Glyph has very high cost: %d%%' % (glyph_name, names[i], names[i+1], item_cost)) 154 155 156 except ValueError as e: 157 print('%s: %s: math error %s; skipping glyph.' % (glyph_name, name, e)) 158 print(contour.value) 159 #raise 160 #for x in hist: 161 # print(x) 162 163 def main(args): 164 filenames = args 165 glyphs = None 166 #glyphs = ['uni08DB', 'uniFD76'] 167 #glyphs = ['uni08DE', 'uni0034'] 168 #glyphs = ['uni08DE', 'uni0034', 'uni0751', 'uni0753', 'uni0754', 'uni08A4', 'uni08A4.fina', 'uni08A5.fina'] 169 170 from os.path import basename 171 names = [basename(filename).rsplit('.', 1)[0] for filename in filenames] 172 173 from fontTools.ttLib import TTFont 174 fonts = [TTFont(filename) for filename in filenames] 175 176 glyphsets = [font.getGlyphSet() for font in fonts] 177 test(glyphsets, glyphs=glyphs, names=names) 178 179 if __name__ == '__main__': 180 import sys 181 main(sys.argv[1:]) 182