1"""
2Tool to find wrong contour order between different masters, and
3other interpolatability (or lack thereof) issues.
4
5Call as:
6$ fonttools varLib.interpolatable font1 font2 ...
7"""
8
9from __future__ import print_function, division, absolute_import
10from fontTools.misc.py23 import *
11
12from fontTools.pens.basePen import AbstractPen, BasePen
13from fontTools.pens.recordingPen import RecordingPen
14from fontTools.pens.statisticsPen import StatisticsPen
15import itertools
16
17
18class 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
45class PerContourOrComponentPen(PerContourPen):
46
47	def addComponent(self, glyphName, transformation):
48		self._newItem()
49		self.value[-1].addComponent(glyphName, transformation)
50
51
52def _vdiff(v0, v1):
53	return tuple(b-a for a,b in zip(v0,v1))
54def _vlen(vec):
55	v = 0
56	for x in vec:
57		v += x*x
58	return v
59
60def _matching_cost(G, matching):
61	return sum(G[i][j] for i,j in enumerate(matching))
62
63def 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
96def 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
163def 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
179if __name__ == '__main__':
180	import sys
181	main(sys.argv[1:])
182