To be moved to proper place soon. Using this in interpolatable.py makes the core of the computation over an order of magnitude faster.
197 lines
5.5 KiB
Python
Executable File
197 lines
5.5 KiB
Python
Executable File
#! /usr/bin/env python
|
|
|
|
"""
|
|
Tool to find wrong contour order between different masters, and
|
|
other interpolatability (or lack thereof) issues.
|
|
"""
|
|
|
|
from __future__ import print_function, division, absolute_import
|
|
from fontTools.misc.py23 import *
|
|
|
|
from fontTools.pens.basePen import BasePen
|
|
from symfont import GlyphStatistics
|
|
import itertools
|
|
|
|
class PerContourOrComponentPen(BasePen):
|
|
def __init__(self, Pen, glyphset=None):
|
|
BasePen.__init__(self, glyphset)
|
|
self._glyphset = glyphset
|
|
self._Pen = Pen
|
|
self.value = []
|
|
def _moveTo(self, p0):
|
|
self._newItem()
|
|
self.value[-1].moveTo(p0)
|
|
def _lineTo(self, p1):
|
|
self.value[-1].lineTo(p1)
|
|
def _qCurveToOne(self, p1, p2):
|
|
self.value[-1].qCurveTo(p1, p2)
|
|
def _curveToOne(self, p1, p2, p3):
|
|
self.value[-1].curveTo(p1, p2, p3)
|
|
def _closePath(self):
|
|
self.value[-1].closePath()
|
|
def _endPath(self):
|
|
self.value[-1].endPath()
|
|
def addComponent(self, glyphName, transformation):
|
|
self._newItem()
|
|
self.value[-1].addComponent(glyphName, transformation)
|
|
|
|
def _newItem(self):
|
|
self.value.append(self._Pen(glyphset=self._glyphset))
|
|
|
|
class RecordingPen(BasePen):
|
|
def __init__(self, glyphset):
|
|
BasePen.__init__(self, glyphset)
|
|
self._glyphset = glyphset
|
|
self.value = []
|
|
def _moveTo(self, p0):
|
|
self.value.append(('moveTo', (p0,)))
|
|
def _lineTo(self, p1):
|
|
self.value.append(('lineTo', (p1,)))
|
|
def _qCurveToOne(self, p1, p2):
|
|
self.value.append(('qCurveTo', (p1,p2)))
|
|
def _curveToOne(self, p1, p2, p3):
|
|
self.value.append(('curveTo', (p1,p2,p3)))
|
|
def _closePath(self):
|
|
self.value.append(('closePath', ()))
|
|
def _endPath(self):
|
|
self.value.append(('endPath', ()))
|
|
# Humm, adding the following method slows things down some 20%.
|
|
# We don't have as much control as we like currently.
|
|
#def addComponent(self, glyphName, transformation):
|
|
# self.value.append(('addComponent', (glyphName, transformation)))
|
|
|
|
def draw(self, pen):
|
|
for operator,operands in self.value:
|
|
getattr(pen, operator)(*operands)
|
|
|
|
def _vdiff(v0, v1):
|
|
return tuple(b-a for a,b in zip(v0,v1))
|
|
def _vlen(vec):
|
|
v = 0
|
|
for x in vec:
|
|
v += x*x
|
|
return v
|
|
|
|
def _matching_cost(G, matching):
|
|
return sum(G[i][j] for i,j in enumerate(matching))
|
|
|
|
def min_cost_perfect_bipartite_matching(G):
|
|
n = len(G)
|
|
try:
|
|
from scipy.optimize import linear_sum_assignment
|
|
rows, cols = linear_sum_assignment(G)
|
|
assert (rows == list(range(n))).all()
|
|
return list(cols), _matching_cost(G, cols)
|
|
except ImportError:
|
|
pass
|
|
|
|
try:
|
|
from munkres import Munkres
|
|
cols = [None] * n
|
|
for row,col in Munkres().compute(G):
|
|
cols[row] = col
|
|
return cols, _matching_cost(G, cols)
|
|
except ImportError:
|
|
pass
|
|
|
|
if n > 6:
|
|
raise Exception("Install Python module 'munkres' or 'scipy >= 0.17.0'")
|
|
|
|
# Otherwise just brute-force
|
|
permutations = itertools.permutations(range(n))
|
|
best = list(next(permutations))
|
|
best_cost = _matching_cost(G, best)
|
|
for p in permutations:
|
|
cost = _matching_cost(G, p)
|
|
if cost < best_cost:
|
|
best, best_cost = list(p), cost
|
|
return best, best_cost
|
|
|
|
|
|
def test(glyphsets, glyphs=None, names=None):
|
|
|
|
if names is None:
|
|
names = glyphsets
|
|
if glyphs is None:
|
|
glyphs = glyphsets[0].keys()
|
|
|
|
hist = []
|
|
for glyph_name in glyphs:
|
|
#print()
|
|
#print(glyph_name)
|
|
|
|
try:
|
|
allVectors = []
|
|
for glyphset,name in zip(glyphsets, names):
|
|
#print('.', end='')
|
|
glyph = glyphset[glyph_name]
|
|
|
|
perContourPen = PerContourOrComponentPen(RecordingPen, glyphset=glyphset)
|
|
glyph.draw(perContourPen)
|
|
contourPens = perContourPen.value
|
|
del perContourPen
|
|
|
|
contourVectors = []
|
|
allVectors.append(contourVectors)
|
|
for contour in contourPens:
|
|
stats = GlyphStatistics(contour, glyphset=glyphset)
|
|
size = abs(stats.Area) ** .5 * .5
|
|
vector = (
|
|
int(stats.Perimeter * .125),
|
|
int(size),
|
|
int(stats.MeanX),
|
|
int(stats.MeanY),
|
|
int(stats.StdDevX * 2),
|
|
int(stats.StdDevY * 2),
|
|
int(stats.Correlation * size),
|
|
)
|
|
contourVectors.append(vector)
|
|
#print(vector)
|
|
|
|
# Check each master against the next one in the list.
|
|
for i,(m0,m1) in enumerate(zip(allVectors[:-1],allVectors[1:])):
|
|
if len(m0) != len(m1):
|
|
print('%s: %s+%s: Glyphs not compatible!!!!!' % (glyph_name, names[i], names[i+1]))
|
|
continue
|
|
if not m0:
|
|
continue
|
|
costs = [[_vlen(_vdiff(v0,v1)) for v1 in m1] for v0 in m0]
|
|
matching, matching_cost = min_cost_perfect_bipartite_matching(costs)
|
|
if matching != list(range(len(m0))):
|
|
print('%s: %s+%s: Glyph has wrong contour/component order: %s' % (glyph_name, names[i], names[i+1], matching)) #, m0, m1)
|
|
break
|
|
upem = 2048
|
|
item_cost = int(round((matching_cost / len(m0) / len(m0[0])) ** .5 / upem * 100))
|
|
hist.append(item_cost)
|
|
threshold = 7
|
|
if item_cost >= threshold:
|
|
print('%s: %s+%s: Glyph has very high cost: %d%%' % (glyph_name, names[i], names[i+1], item_cost))
|
|
|
|
|
|
except ValueError as e:
|
|
print('%s: %s: math error %s; skipping glyph.' % (glyph_name, name, e))
|
|
print(contour.value)
|
|
#raise
|
|
#for x in hist:
|
|
# print(x)
|
|
|
|
def main(args):
|
|
filenames = args
|
|
glyphs = None
|
|
#glyphs = ['uni08DB', 'uniFD76']
|
|
#glyphs = ['uni08DE', 'uni0034']
|
|
#glyphs = ['uni08DE', 'uni0034', 'uni0751', 'uni0753', 'uni0754', 'uni08A4', 'uni08A4.fina', 'uni08A5.fina']
|
|
|
|
from os.path import basename
|
|
names = [basename(filename).rsplit('.', 1)[0] for filename in filenames]
|
|
|
|
from fontTools.ttLib import TTFont
|
|
fonts = [TTFont(filename) for filename in filenames]
|
|
|
|
glyphsets = [font.getGlyphSet() for font in fonts]
|
|
test(glyphsets, glyphs=glyphs, names=names)
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
main(sys.argv[1:])
|