Last active
April 26, 2024 17:48
-
-
Save simoncozens/2fffb60e99f45a71c5192fddb18a65ec to your computer and use it in GitHub Desktop.
sparsify.py - turn masters into sparse masters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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