Home | History | Annotate | Download | only in varLib
      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