Skip to content

Instantly share code, notes, and snippets.

@simoncozens
Last active April 26, 2024 17:48
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save simoncozens/2fffb60e99f45a71c5192fddb18a65ec to your computer and use it in GitHub Desktop.
Save simoncozens/2fffb60e99f45a71c5192fddb18a65ec to your computer and use it in GitHub Desktop.
sparsify.py - turn masters into sparse masters
import uuid
from glyphsLib import load, GSPath, GSNode, GSLayer
from fontTools.varLib.models import VariationModel, normalizeValue
import numpy as np
from tqdm import tqdm
import argparse
def interpolate_paths_without(glyph, intermediate_layer, intermediate_location):
tags = [axis.axisTag for axis in glyph.parent.axes]
locations = []
list_of_paths = []
widths = []
for layer in g.layers:
if layer == intermediate_layer:
continue
if layer.attributes and "coordinates" in layer.attributes:
locations.append(layer.attributes["coordinates"])
elif layer.associatedMasterId and layer.layerId != layer.associatedMasterId:
continue
else:
locations.append(glyph.parent.masters[layer.associatedMasterId].axes)
list_of_paths.append(layer.paths)
widths.append(layer.width)
limits = {tag: (min(x), max(x)) for tag, x in zip(tags, (zip(*locations)))}
master_locations = []
for loc in locations:
this_loc = {}
for ix, axisTag in enumerate(tags):
axismin, axismax = limits[axisTag]
this_loc[axisTag] = normalizeValue(loc[ix], (axismin, axismin, axismax))
master_locations.append(this_loc)
normalized_intermediate_location = {
axisTag: normalizeValue(
intermediate_location[ix],
(limits[axisTag][0], limits[axisTag][0], limits[axisTag][1]),
)
for ix, axisTag in enumerate(tags)
}
try:
model = VariationModel(master_locations, axisOrder=tags)
except Exception:
import IPython;IPython.embed()
paths_per_master = list(zip(*list_of_paths))
newlayer = GSLayer()
for paths in paths_per_master:
newlayer.paths.append(interpolate_path(paths, model, normalized_intermediate_location))
newlayer.width = model.interpolateFromMasters(normalized_intermediate_location, widths)
return newlayer
# Turn a GS Layer into a flat array of coords for easy comparison
def flatten(layer):
coords = []
if not layer.bounds:
return np.array(coords)
# We just want the center of the *paths*, ignoring components.
# So let's create a new layer, only with paths
newlayer = GSLayer()
newlayer.shapes = layer.paths
if not newlayer.shapes:
return np.array(coords)
center = newlayer.bounds.origin.x + newlayer.bounds.size.width / 2
for path in layer.paths:
for node in path.nodes:
# We subtract the center so that glyphs which are shifted (different
# LSBs) don't get penalized
coords.extend([node.position.x - center, node.position.y])
return np.array(coords)
def lerp(a, b, t):
return a + (b - a) * t
def error(a, b):
return np.sum((a - b) ** 2)
def interpolate_path(paths, model, location):
path = GSPath()
for master_nodes in zip(*[p.nodes for p in paths]):
node = GSNode()
node.type = master_nodes[0].type
node.smooth = master_nodes[0].smooth
xs = [n.position.x for n in master_nodes]
ys = [n.position.y for n in master_nodes]
node.position.x = model.interpolateFromMasters(location, xs)
node.position.y = model.interpolateFromMasters(location, ys)
path.nodes.append(node)
return path
def interpolate_to_background(glyph_1, intermediate_layer, glyph_2, interpolation):
intermediate_layer.background.shapes = []
for regular_path, bold_path in zip(glyph_1.paths, glyph_2.paths):
new_path = GSPath()
intermediate_layer.background.shapes.append(new_path)
for regular_node, bold_node in zip(regular_path.nodes, bold_path.nodes):
new_node = GSNode()
new_node.type = regular_node.type
new_node.position.x = lerp(
regular_node.position.x, bold_node.position.x, interpolation
)
new_node.position.y = lerp(
regular_node.position.y, bold_node.position.y, interpolation
)
new_path.nodes.append(new_node)
def sparsify_layer(font, parent, layer, location):
layer.name = font.masters[layer.associatedMasterId].name + " (intermediate)"
layer.associatedMasterId = parent.id
layer.layerId = uuid.uuid4()
layer.attributes = {"coordinates": location}
layer.parent.color = 10 # For review
deviations = {}
parser = argparse.ArgumentParser()
parser.add_argument(
"--cutoff",
type=float,
default=50,
help="Cut-off for reporting glyphs with deviations",
)
parser.add_argument(
"--width-cutoff",
type=float,
default=8,
help="Cut-off for reporting glyphs with width deviations",
)
parser.add_argument(
"--output",
type=str,
help="Output file name (writes interpolated master as background layer)",
metavar="GLYPHS",
)
parser.add_argument(
"--sparsify",
type=str,
help="Convert the intermediate master into a sparse master under the given master, with intermediate layers for glyphs which deviate more than the cutoff value",
)
parser.add_argument("input", type=str, help="Output file name", metavar="GLYPHS")
parser.add_argument(
"intermediate_master",
type=str,
help="Name or index of intermediate master (candidate for removal, e.g. SemiBold)",
metavar="INTERMEDIATE_MASTER",
)
args = parser.parse_args()
def find_master_index(font, master_name):
for ix, m in enumerate(font.masters):
if m.name == master_name or str(ix) == master_name:
return ix
names = [f'"{m.name}"' for m in font.masters]
raise Exception(
"Master not found: %s (try one of %s)" % (master_name, ", ".join(names))
)
print("Loading font")
font = load(args.input)
intermediate_index = find_master_index(font, args.intermediate_master)
if args.sparsify:
if font.format_version != 3:
raise Exception("--sparsify only works with Glyphs v3 files")
parent_master = font.masters[find_master_index(font, args.sparsify)]
intermediate_location = font.masters[intermediate_index].axes
intermediate_id = font.masters[intermediate_index].id
to_sparsify = []
to_delete = []
for g in font.glyphs:
intermediate_layer_candidates = [l for l in g.layers if l.associatedMasterId == intermediate_id]
if not intermediate_layer_candidates:
print("No intermediate layer for %s" % g.name)
continue
intermediate_layer = intermediate_layer_candidates[0]
expected = interpolate_paths_without(g, intermediate_layer, intermediate_location)
expected_flat = flatten(expected)
real_flat = flatten(intermediate_layer)
width_deviation = abs(expected.width - intermediate_layer.width)
if not intermediate_layer.paths:
if args.sparsify:
if width_deviation < args.width_cutoff:
to_delete.append(g)
else:
to_sparsify.append(intermediate_layer)
intermediate_layer.parent.color = 9
continue
deviation = error(expected_flat, real_flat) / len(expected_flat)
deviations[g.name] = deviation
if args.output:
if args.sparsify:
if deviation > args.cutoff or width_deviation > args.width_cutoff:
to_sparsify.append(intermediate_layer)
if deviation <= args.cutoff: # Just a width support layer
intermediate_layer.parent.color = 9
else:
to_delete.append(g)
intermediate_layer.background.shapes = []
intermediate_layer.background.shapes.extend(expected.shapes)
print("Glyphs with most deviation from interpolated master:")
for g in sorted(deviations, key=deviations.get, reverse=True):
if deviations[g] < args.cutoff:
continue
print("%40s %.2f" % (g, deviations[g]))
if args.output:
if args.sparsify:
for glyph in to_delete:
del glyph.layers[intermediate_index]
for layer in to_sparsify:
sparsify_layer(
font, parent_master, layer, intermediate_location
)
# font._masters.remove(font._masters[intermediate_index])
intermediate_id = font.masters[intermediate_index].id
# Check we didn't mess up
for glyph in font.glyphs:
for layer in glyph.layers:
if layer.associatedMasterId == intermediate_id:
raise Exception(
"Found layer with associatedMasterId %s" % intermediate_id
)
del font.masters[intermediate_index]
print("Saving interpolated master to background in %s" % args.output)
font.save(args.output)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment