234 lines
7.9 KiB
Python
234 lines
7.9 KiB
Python
"""
|
|
Tool to find wrong contour order between different masters, and
|
|
other interpolatability (or lack thereof) issues.
|
|
|
|
Call as:
|
|
$ fonttools varLib.interpolatable font1 font2 ...
|
|
"""
|
|
|
|
from ._interpolatable import test
|
|
from collections import defaultdict
|
|
|
|
|
|
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):
|
|
"""Test for interpolatability issues between fonts"""
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(
|
|
"fonttools varLib.interpolatable",
|
|
description=main.__doc__,
|
|
)
|
|
parser.add_argument(
|
|
"--glyphs",
|
|
action="store",
|
|
help="Space-separate name of glyphs to check",
|
|
)
|
|
parser.add_argument(
|
|
"--json",
|
|
action="store_true",
|
|
help="Output report in JSON format",
|
|
)
|
|
parser.add_argument(
|
|
"--quiet",
|
|
action="store_true",
|
|
help="Only exit with code 1 or 0, no output",
|
|
)
|
|
parser.add_argument(
|
|
"--ignore-missing",
|
|
action="store_true",
|
|
help="Will not report glyphs missing from sparse masters as errors",
|
|
)
|
|
parser.add_argument(
|
|
"inputs",
|
|
metavar="FILE",
|
|
type=str,
|
|
nargs="+",
|
|
help="Input a single DesignSpace/Glyphs file, or multiple TTF/UFO files",
|
|
)
|
|
|
|
args = parser.parse_args(args)
|
|
|
|
glyphs = args.glyphs.split() if args.glyphs else None
|
|
|
|
from os.path import basename
|
|
|
|
fonts = []
|
|
names = []
|
|
|
|
if len(args.inputs) == 1:
|
|
if args.inputs[0].endswith(".designspace"):
|
|
from fontTools.designspaceLib import DesignSpaceDocument
|
|
|
|
designspace = DesignSpaceDocument.fromfile(args.inputs[0])
|
|
args.inputs = [master.path for master in designspace.sources]
|
|
|
|
elif args.inputs[0].endswith(".glyphs"):
|
|
from glyphsLib import GSFont, to_ufos
|
|
|
|
gsfont = GSFont(args.inputs[0])
|
|
fonts.extend(to_ufos(gsfont))
|
|
names = ["%s-%s" % (f.info.familyName, f.info.styleName) for f in fonts]
|
|
args.inputs = []
|
|
|
|
elif args.inputs[0].endswith(".ttf"):
|
|
from fontTools.ttLib import TTFont
|
|
|
|
font = TTFont(args.inputs[0])
|
|
if "gvar" in font:
|
|
# Is variable font
|
|
gvar = font["gvar"]
|
|
glyf = font["glyf"]
|
|
# Gather all glyphs at their "master" locations
|
|
ttGlyphSets = {}
|
|
glyphsets = defaultdict(dict)
|
|
|
|
if glyphs is None:
|
|
glyphs = sorted(gvar.variations.keys())
|
|
for glyphname in glyphs:
|
|
for var in gvar.variations[glyphname]:
|
|
locDict = {}
|
|
loc = []
|
|
for tag, val in sorted(var.axes.items()):
|
|
locDict[tag] = val[1]
|
|
loc.append((tag, val[1]))
|
|
|
|
locTuple = tuple(loc)
|
|
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 = []
|
|
|
|
for filename in args.inputs:
|
|
if filename.endswith(".ufo"):
|
|
from fontTools.ufoLib import UFOReader
|
|
|
|
fonts.append(UFOReader(filename))
|
|
else:
|
|
from fontTools.ttLib import TTFont
|
|
|
|
fonts.append(TTFont(filename))
|
|
|
|
names.append(basename(filename).rsplit(".", 1)[0])
|
|
|
|
glyphsets = []
|
|
for font in fonts:
|
|
if hasattr(font, "getGlyphSet"):
|
|
glyphset = font.getGlyphSet()
|
|
else:
|
|
glyphset = font
|
|
glyphsets.append({k: glyphset[k] for k in glyphset.keys()})
|
|
|
|
if not glyphs:
|
|
glyphs = sorted(set([gn for glyphset in glyphsets for gn in glyphset.keys()]))
|
|
|
|
glyphsSet = set(glyphs)
|
|
for glyphset in glyphsets:
|
|
glyphSetGlyphNames = set(glyphset.keys())
|
|
diff = glyphsSet - glyphSetGlyphNames
|
|
if diff:
|
|
for gn in diff:
|
|
glyphset[gn] = None
|
|
|
|
problems = test(
|
|
glyphsets, glyphs=glyphs, names=names, ignore_missing=args.ignore_missing
|
|
)
|
|
|
|
if not args.quiet:
|
|
if args.json:
|
|
import json
|
|
|
|
print(json.dumps(problems))
|
|
else:
|
|
for glyph, glyph_problems in problems.items():
|
|
print(f"Glyph {glyph} was not compatible: ")
|
|
for p in glyph_problems:
|
|
if p["type"] == "missing":
|
|
print(" Glyph was missing in master %s" % p["master"])
|
|
if p["type"] == "open_path":
|
|
print(" Glyph has an open path in master %s" % p["master"])
|
|
if p["type"] == "path_count":
|
|
print(
|
|
" Path count differs: %i in %s, %i in %s"
|
|
% (p["value_1"], p["master_1"], p["value_2"], p["master_2"])
|
|
)
|
|
if p["type"] == "node_count":
|
|
print(
|
|
" Node count differs in path %i: %i in %s, %i in %s"
|
|
% (
|
|
p["path"],
|
|
p["value_1"],
|
|
p["master_1"],
|
|
p["value_2"],
|
|
p["master_2"],
|
|
)
|
|
)
|
|
if p["type"] == "node_incompatibility":
|
|
print(
|
|
" Node %o incompatible in path %i: %s in %s, %s in %s"
|
|
% (
|
|
p["node"],
|
|
p["path"],
|
|
p["value_1"],
|
|
p["master_1"],
|
|
p["value_2"],
|
|
p["master_2"],
|
|
)
|
|
)
|
|
if p["type"] == "contour_order":
|
|
print(
|
|
" Contour order differs: %s in %s, %s in %s"
|
|
% (
|
|
p["value_1"],
|
|
p["master_1"],
|
|
p["value_2"],
|
|
p["master_2"],
|
|
)
|
|
)
|
|
if p["type"] == "wrong_start_point":
|
|
print(
|
|
" Contour %d start point differs: %s, %s"
|
|
% (
|
|
p["contour"],
|
|
p["master_1"],
|
|
p["master_2"],
|
|
)
|
|
)
|
|
if p["type"] == "math_error":
|
|
print(
|
|
" Miscellaneous error in %s: %s"
|
|
% (
|
|
p["master"],
|
|
p["error"],
|
|
)
|
|
)
|
|
if problems:
|
|
return problems
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
problems = main()
|
|
sys.exit(int(bool(problems)))
|