| #! /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:]) |