196 lines
5.5 KiB
Python
Executable File
196 lines
5.5 KiB
Python
Executable File
#! /usr/bin/env python
|
|
|
|
"""
|
|
Tool to find wront 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 in glyphsets:
|
|
#print('.', end='')
|
|
#print()
|
|
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)
|
|
vector = (
|
|
int(stats.Perimeter * .125),
|
|
int(abs(stats.Area) ** .5 * .5),
|
|
int(stats.MeanX),
|
|
int(stats.MeanY),
|
|
int(stats.StdDevX * 2),
|
|
int(stats.StdDevY * 2),
|
|
int(stats.Covariance/(stats.StdDevX*stats.StdDevY)**.5),
|
|
)
|
|
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: math error %s; skipping glyph' % (glyph_name, e))
|
|
#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:])
|