Merge pull request #3300 from fonttools/faster-interpolatable
[varLib.interpolatable] Speed up working on variable fonts
This commit is contained in:
commit
fadbb7d8bc
@ -11,7 +11,7 @@ from fontTools.pens.pointPen import SegmentToPointPen
|
|||||||
from fontTools.pens.recordingPen import RecordingPen
|
from fontTools.pens.recordingPen import RecordingPen
|
||||||
from fontTools.pens.statisticsPen import StatisticsPen
|
from fontTools.pens.statisticsPen import StatisticsPen
|
||||||
from fontTools.pens.momentsPen import OpenContourError
|
from fontTools.pens.momentsPen import OpenContourError
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict, defaultdict
|
||||||
import math
|
import math
|
||||||
import itertools
|
import itertools
|
||||||
import sys
|
import sys
|
||||||
@ -81,48 +81,43 @@ class RecordingPointPen(BasePen):
|
|||||||
self.value.append((pt, False if segmentType is None else True))
|
self.value.append((pt, False if segmentType is None else True))
|
||||||
|
|
||||||
|
|
||||||
def _vdiff(v0, v1):
|
def _vdiff_hypot2(v0, v1):
|
||||||
return tuple(b - a for a, b in zip(v0, v1))
|
s = 0
|
||||||
|
for x0, x1 in zip(v0, v1):
|
||||||
|
d = x1 - x0
|
||||||
|
s += d * d
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
def _vlen(vec):
|
def _vdiff_hypot2_complex(v0, v1):
|
||||||
v = 0
|
s = 0
|
||||||
for x in vec:
|
for x0, x1 in zip(v0, v1):
|
||||||
v += x * x
|
d = x1 - x0
|
||||||
return v
|
s += d.real * d.real + d.imag * d.imag
|
||||||
|
return s
|
||||||
|
|
||||||
def _complex_vlen(vec):
|
|
||||||
v = 0
|
|
||||||
for x in vec:
|
|
||||||
v += abs(x) * abs(x)
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
def _matching_cost(G, matching):
|
def _matching_cost(G, matching):
|
||||||
return sum(G[i][j] for i, j in enumerate(matching))
|
return sum(G[i][j] for i, j in enumerate(matching))
|
||||||
|
|
||||||
|
|
||||||
def min_cost_perfect_bipartite_matching(G):
|
def min_cost_perfect_bipartite_matching_scipy(G):
|
||||||
n = len(G)
|
n = len(G)
|
||||||
try:
|
rows, cols = linear_sum_assignment(G)
|
||||||
from scipy.optimize import linear_sum_assignment
|
assert (rows == list(range(n))).all()
|
||||||
|
return list(cols), _matching_cost(G, cols)
|
||||||
|
|
||||||
rows, cols = linear_sum_assignment(G)
|
|
||||||
assert (rows == list(range(n))).all()
|
|
||||||
return list(cols), _matching_cost(G, cols)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
def min_cost_perfect_bipartite_matching_munkres(G):
|
||||||
from munkres import Munkres
|
n = len(G)
|
||||||
|
cols = [None] * n
|
||||||
|
for row, col in Munkres().compute(G):
|
||||||
|
cols[row] = col
|
||||||
|
return cols, _matching_cost(G, cols)
|
||||||
|
|
||||||
cols = [None] * n
|
|
||||||
for row, col in Munkres().compute(G):
|
def min_cost_perfect_bipartite_matching_bruteforce(G):
|
||||||
cols[row] = col
|
n = len(G)
|
||||||
return cols, _matching_cost(G, cols)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if n > 6:
|
if n > 6:
|
||||||
raise Exception("Install Python module 'munkres' or 'scipy >= 0.17.0'")
|
raise Exception("Install Python module 'munkres' or 'scipy >= 0.17.0'")
|
||||||
@ -138,6 +133,23 @@ def min_cost_perfect_bipartite_matching(G):
|
|||||||
return best, best_cost
|
return best, best_cost
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from scipy.optimize import linear_sum_assignment
|
||||||
|
|
||||||
|
min_cost_perfect_bipartite_matching = min_cost_perfect_bipartite_matching_scipy
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from munkres import Munkres
|
||||||
|
|
||||||
|
min_cost_perfect_bipartite_matching = (
|
||||||
|
min_cost_perfect_bipartite_matching_munkres
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
min_cost_perfect_bipartite_matching = (
|
||||||
|
min_cost_perfect_bipartite_matching_bruteforce
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test(glyphsets, glyphs=None, names=None, ignore_missing=False):
|
def test(glyphsets, glyphs=None, names=None, ignore_missing=False):
|
||||||
if names is None:
|
if names is None:
|
||||||
names = glyphsets
|
names = glyphsets
|
||||||
@ -158,9 +170,10 @@ def test(glyphsets, glyphs=None, names=None, ignore_missing=False):
|
|||||||
allVectors = []
|
allVectors = []
|
||||||
allNodeTypes = []
|
allNodeTypes = []
|
||||||
allContourIsomorphisms = []
|
allContourIsomorphisms = []
|
||||||
for glyphset, name in zip(glyphsets, names):
|
allGlyphs = [glyphset[glyph_name] for glyphset in glyphsets]
|
||||||
glyph = glyphset[glyph_name]
|
if len([1 for glyph in allGlyphs if glyph is not None]) <= 1:
|
||||||
|
continue
|
||||||
|
for glyph, glyphset, name in zip(allGlyphs, glyphsets, names):
|
||||||
if glyph is None:
|
if glyph is None:
|
||||||
if not ignore_missing:
|
if not ignore_missing:
|
||||||
add_problem(glyph_name, {"type": "missing", "master": name})
|
add_problem(glyph_name, {"type": "missing", "master": name})
|
||||||
@ -247,11 +260,12 @@ def test(glyphsets, glyphs=None, names=None, ignore_missing=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# m0idx should be the index of the first non-None item in allNodeTypes,
|
# m0idx should be the index of the first non-None item in allNodeTypes,
|
||||||
# else give it the first index of None, which is likely 0
|
# else give it the last item.
|
||||||
m0idx = allNodeTypes.index(
|
m0idx = next(
|
||||||
next((x for x in allNodeTypes if x is not None), None)
|
(i for i, x in enumerate(allNodeTypes) if x is not None),
|
||||||
|
len(allNodeTypes) - 1,
|
||||||
)
|
)
|
||||||
# m0 is the first non-None item in allNodeTypes, or the first item if all are None
|
# m0 is the first non-None item in allNodeTypes, or last one if all None
|
||||||
m0 = allNodeTypes[m0idx]
|
m0 = allNodeTypes[m0idx]
|
||||||
for i, m1 in enumerate(allNodeTypes[m0idx + 1 :]):
|
for i, m1 in enumerate(allNodeTypes[m0idx + 1 :]):
|
||||||
if m1 is None:
|
if m1 is None:
|
||||||
@ -302,39 +316,39 @@ def test(glyphsets, glyphs=None, names=None, ignore_missing=False):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# m0idx should be the index of the first non-None item in allVectors,
|
# m0idx should be the index of the first non-None item in allVectors,
|
||||||
# else give it the first index of None, which is likely 0
|
# else give it the last item.
|
||||||
m0idx = allVectors.index(
|
m0idx = next(
|
||||||
next((x for x in allVectors if x is not None), None)
|
(i for i, x in enumerate(allVectors) if x is not None),
|
||||||
|
len(allVectors) - 1,
|
||||||
)
|
)
|
||||||
# m0 is the first non-None item in allVectors, or the first item if all are None
|
# m0 is the first non-None item in allVectors, or last one if all None
|
||||||
m0 = allVectors[m0idx]
|
m0 = allVectors[m0idx]
|
||||||
for i, m1 in enumerate(allVectors[m0idx + 1 :]):
|
if m0 is not None and len(m0) > 1:
|
||||||
if m1 is None:
|
for i, m1 in enumerate(allVectors[m0idx + 1 :]):
|
||||||
continue
|
if m1 is None:
|
||||||
if len(m0) != len(m1):
|
continue
|
||||||
# We already reported this
|
if len(m0) != len(m1):
|
||||||
continue
|
# We already reported this
|
||||||
if not m0:
|
continue
|
||||||
continue
|
costs = [[_vdiff_hypot2(v0, v1) for v1 in m1] for v0 in m0]
|
||||||
costs = [[_vlen(_vdiff(v0, v1)) for v1 in m1] for v0 in m0]
|
matching, matching_cost = min_cost_perfect_bipartite_matching(costs)
|
||||||
matching, matching_cost = min_cost_perfect_bipartite_matching(costs)
|
identity_matching = list(range(len(m0)))
|
||||||
identity_matching = list(range(len(m0)))
|
identity_cost = sum(costs[i][i] for i in range(len(m0)))
|
||||||
identity_cost = sum(costs[i][i] for i in range(len(m0)))
|
if (
|
||||||
if (
|
matching != identity_matching
|
||||||
matching != identity_matching
|
and matching_cost < identity_cost * 0.95
|
||||||
and matching_cost < identity_cost * 0.95
|
):
|
||||||
):
|
add_problem(
|
||||||
add_problem(
|
glyph_name,
|
||||||
glyph_name,
|
{
|
||||||
{
|
"type": "contour_order",
|
||||||
"type": "contour_order",
|
"master_1": names[m0idx],
|
||||||
"master_1": names[m0idx],
|
"master_2": names[m0idx + i + 1],
|
||||||
"master_2": names[m0idx + i + 1],
|
"value_1": list(range(len(m0))),
|
||||||
"value_1": list(range(len(m0))),
|
"value_2": matching,
|
||||||
"value_2": matching,
|
},
|
||||||
},
|
)
|
||||||
)
|
break
|
||||||
break
|
|
||||||
|
|
||||||
# m0idx should be the index of the first non-None item in allContourIsomorphisms,
|
# m0idx should be the index of the first non-None item in allContourIsomorphisms,
|
||||||
# else give it the first index of None, which is likely 0
|
# else give it the first index of None, which is likely 0
|
||||||
@ -343,31 +357,31 @@ def test(glyphsets, glyphs=None, names=None, ignore_missing=False):
|
|||||||
)
|
)
|
||||||
# m0 is the first non-None item in allContourIsomorphisms, or the first item if all are None
|
# m0 is the first non-None item in allContourIsomorphisms, or the first item if all are None
|
||||||
m0 = allContourIsomorphisms[m0idx]
|
m0 = allContourIsomorphisms[m0idx]
|
||||||
for i, m1 in enumerate(allContourIsomorphisms[m0idx + 1 :]):
|
if m0:
|
||||||
if m1 is None:
|
for i, m1 in enumerate(allContourIsomorphisms[m0idx + 1 :]):
|
||||||
continue
|
if m1 is None:
|
||||||
if len(m0) != len(m1):
|
continue
|
||||||
# We already reported this
|
if len(m0) != len(m1):
|
||||||
continue
|
# We already reported this
|
||||||
if not m0:
|
continue
|
||||||
continue
|
for ix, (contour0, contour1) in enumerate(zip(m0, m1)):
|
||||||
for ix, (contour0, contour1) in enumerate(zip(m0, m1)):
|
c0 = contour0[0]
|
||||||
c0 = contour0[0]
|
costs = [
|
||||||
costs = [
|
v
|
||||||
v for v in (_complex_vlen(_vdiff(c0, c1)) for c1 in contour1)
|
for v in (_vdiff_hypot2_complex(c0, c1) for c1 in contour1)
|
||||||
]
|
]
|
||||||
min_cost = min(costs)
|
min_cost = min(costs)
|
||||||
first_cost = costs[0]
|
first_cost = costs[0]
|
||||||
if min_cost < first_cost * 0.95:
|
if min_cost < first_cost * 0.95:
|
||||||
add_problem(
|
add_problem(
|
||||||
glyph_name,
|
glyph_name,
|
||||||
{
|
{
|
||||||
"type": "wrong_start_point",
|
"type": "wrong_start_point",
|
||||||
"contour": ix,
|
"contour": ix,
|
||||||
"master_1": names[m0idx],
|
"master_1": names[m0idx],
|
||||||
"master_2": names[m0idx + i + 1],
|
"master_2": names[m0idx + i + 1],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
add_problem(
|
add_problem(
|
||||||
@ -377,6 +391,15 @@ def test(glyphsets, glyphs=None, names=None, ignore_missing=False):
|
|||||||
return problems
|
return problems
|
||||||
|
|
||||||
|
|
||||||
|
def recursivelyAddGlyph(glyphname, glyphset, ttGlyphSet, glyf):
|
||||||
|
if glyphname in glyphset:
|
||||||
|
return
|
||||||
|
glyphset[glyphname] = ttGlyphSet[glyphname]
|
||||||
|
|
||||||
|
for component in getattr(glyf[glyphname], "components", []):
|
||||||
|
recursivelyAddGlyph(component.glyphName, glyphset, ttGlyphSet, glyf)
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
def main(args=None):
|
||||||
"""Test for interpolatability issues between fonts"""
|
"""Test for interpolatability issues between fonts"""
|
||||||
import argparse
|
import argparse
|
||||||
@ -415,7 +438,7 @@ def main(args=None):
|
|||||||
|
|
||||||
args = parser.parse_args(args)
|
args = parser.parse_args(args)
|
||||||
|
|
||||||
glyphs = set(args.glyphs.split()) if args.glyphs else None
|
glyphs = args.glyphs.split() if args.glyphs else None
|
||||||
|
|
||||||
from os.path import basename
|
from os.path import basename
|
||||||
|
|
||||||
@ -444,30 +467,37 @@ def main(args=None):
|
|||||||
if "gvar" in font:
|
if "gvar" in font:
|
||||||
# Is variable font
|
# Is variable font
|
||||||
gvar = font["gvar"]
|
gvar = font["gvar"]
|
||||||
# Gather all "master" locations
|
glyf = font["glyf"]
|
||||||
locs = set()
|
# Gather all glyphs at their "master" locations
|
||||||
for variations in gvar.variations.values():
|
ttGlyphSets = {}
|
||||||
for var in variations:
|
glyphsets = defaultdict(dict)
|
||||||
|
|
||||||
|
if glyphs is None:
|
||||||
|
glyphs = sorted(gvar.variations.keys())
|
||||||
|
for glyphname in glyphs:
|
||||||
|
for var in gvar.variations[glyphname]:
|
||||||
|
locDict = {}
|
||||||
loc = []
|
loc = []
|
||||||
for tag, val in sorted(var.axes.items()):
|
for tag, val in sorted(var.axes.items()):
|
||||||
|
locDict[tag] = val[1]
|
||||||
loc.append((tag, val[1]))
|
loc.append((tag, val[1]))
|
||||||
locs.add(tuple(loc))
|
|
||||||
# Rebuild locs as dictionaries
|
|
||||||
new_locs = [{}]
|
|
||||||
names.append("()")
|
|
||||||
for loc in sorted(locs, key=lambda v: (len(v), v)):
|
|
||||||
names.append(str(loc))
|
|
||||||
l = {}
|
|
||||||
for tag, val in loc:
|
|
||||||
l[tag] = val
|
|
||||||
new_locs.append(l)
|
|
||||||
locs = new_locs
|
|
||||||
del new_locs
|
|
||||||
# locs is all master locations now
|
|
||||||
|
|
||||||
for loc in locs:
|
locTuple = tuple(loc)
|
||||||
fonts.append(font.getGlyphSet(location=loc, normalized=True))
|
if locTuple not in ttGlyphSets:
|
||||||
|
ttGlyphSets[locTuple] = font.getGlyphSet(
|
||||||
|
location=locDict, normalized=True
|
||||||
|
)
|
||||||
|
|
||||||
|
recursivelyAddGlyph(
|
||||||
|
glyphname, glyphsets[locTuple], ttGlyphSets[locTuple], glyf
|
||||||
|
)
|
||||||
|
|
||||||
|
names = ["()"]
|
||||||
|
fonts = [font.getGlyphSet()]
|
||||||
|
for locTuple in sorted(glyphsets.keys(), key=lambda v: (len(v), v)):
|
||||||
|
names.append(str(locTuple))
|
||||||
|
fonts.append(glyphsets[locTuple])
|
||||||
|
args.ignore_missing = True
|
||||||
args.inputs = []
|
args.inputs = []
|
||||||
|
|
||||||
for filename in args.inputs:
|
for filename in args.inputs:
|
||||||
@ -491,11 +521,12 @@ def main(args=None):
|
|||||||
glyphsets.append({k: glyphset[k] for k in glyphset.keys()})
|
glyphsets.append({k: glyphset[k] for k in glyphset.keys()})
|
||||||
|
|
||||||
if not glyphs:
|
if not glyphs:
|
||||||
glyphs = set([gn for glyphset in glyphsets for gn in glyphset.keys()])
|
glyphs = sorted(set([gn for glyphset in glyphsets for gn in glyphset.keys()]))
|
||||||
|
|
||||||
|
glyphsSet = set(glyphs)
|
||||||
for glyphset in glyphsets:
|
for glyphset in glyphsets:
|
||||||
glyphSetGlyphNames = set(glyphset.keys())
|
glyphSetGlyphNames = set(glyphset.keys())
|
||||||
diff = glyphs - glyphSetGlyphNames
|
diff = glyphsSet - glyphSetGlyphNames
|
||||||
if diff:
|
if diff:
|
||||||
for gn in diff:
|
for gn in diff:
|
||||||
glyphset[gn] = None
|
glyphset[gn] = None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user