Skip to content

Instantly share code, notes, and snippets.

@anthrotype
Last active April 9, 2024 12:46
Show Gist options
  • Save anthrotype/54ab6414bb4404f0ec0daa0c0bf9a934 to your computer and use it in GitHub Desktop.
Save anthrotype/54ab6414bb4404f0ec0daa0c0bf9a934 to your computer and use it in GitHub Desktop.
import string
import sys
from pprint import pprint
try:
Glyphs
except NameError:
IN_GLYPHS_APP = False
else:
IN_GLYPHS_APP = True
if IN_GLYPHS_APP:
from Cocoa import NSPoint
else:
from glyphsLib.types import Point as NSPoint
from glyphsLib.classes import GSFont, GSAnchor
from fontTools.misc.transform import Transform
def anchors_traversing_components(layer):
seen_components = set()
number_of_base_glyphs = [0]
anchors = _anchors_traversing_components(layer, seen_components, number_of_base_glyphs)
return sorted(anchors.values(), key=lambda a: a.name)
def _copy_anchor(a, *, rename=None, origin_anchor=None):
if origin_anchor is not None:
origin = origin_anchor.position
position = NSPoint(a.position.x - origin.x, a.position.y - origin.y)
else:
position = NSPoint(*a.position)
return GSAnchor(rename or a.name, position)
def _component_layer(component):
if IN_GLYPHS_APP:
# for brace layers in composite glyphs, this 'componentLayer' property will
# interpolate on-the-fly if the base glyphs do not have matching brace layers.
return component.componentLayer
else:
# glyphsLib does not have a componentLayer property, so for now we only
# get the corresponding 'master' layer of a component's base glyph;
# this won't work for special (intermediate or alternate) layers!
base_glyph = component.component
parent_layer = component.parent
return base_glyph.layers[parent_layer.associatedMasterId]
def _anchors_traversing_components(layer, seen_components, number_of_base_glyphs):
glyph = layer.parent
assert glyph.name
if len(layer.components) == 0 and len(layer.anchors) == 0:
return {}
if layer.anchors and glyph.category == "Mark":
new_anchors = {a.name: _copy_anchor(a) for a in layer.anchors}
if "*origin" in new_anchors:
origin = new_anchors.pop("*origin").position
for anchor in new_anchors.values():
anchor.position = NSPoint(anchor.position.x - origin.x, anchor.position.y - origin.y)
return new_anchors
seen_components.add(glyph.name)
current_anchors = layer.anchors
all_anchors = {}
# simulating a C *int using a mutable list containing one int...
number_of_base_glyphs[0] = 0
is_ligature = glyph.subCategory == "Ligature"
has_underscore = False
for anchor in current_anchors:
if anchor.name.startswith("_"):
has_underscore = True
break
for component_idx, component in enumerate(layer.components):
if component.name in seen_components:
# shouldn't a cycle be an error?
return {}
component_layer = _component_layer(component)
if component_layer is None:
print(f"WARNING: could not get {component!r} component layer! skipped")
continue
component_number_of_base_glyphs = [0]
anchors = _anchors_traversing_components(
component_layer, seen_components, component_number_of_base_glyphs
)
if component_idx > 0 and component.anchor:
anchor_name = component.anchor
if "_" in anchor_name:
sub_name = anchor_name[: anchor_name.index("_")]
if sub_name in anchors and "_" + sub_name in anchors:
anchor = _copy_anchor(anchors[sub_name], rename=anchor_name)
del anchors[sub_name]
anchors[anchor_name] = anchor
# why is this pop() necessary? Can the _anchors_traversing_components _not_ return
# an *origin anchor instead of the caller needing to remove it after?
anchors.pop("*origin", None)
anchor_names = anchors.keys()
angle = component.rotation
scale = NSPoint(*component.scale)
if abs(angle - 180.0) < 0.001:
scale.x = -scale.x
scale.y = -scale.y
transform = Transform(*(component.transform))
comb_has_underscore = False
comb_has_exit = False
for anchor_name in anchor_names:
if len(anchor_name) < 2:
continue
if anchor_name.startswith("_"):
comb_has_underscore = True
if anchor_name.endswith("exit"):
comb_has_exit = True
if not (comb_has_underscore or comb_has_exit):
for key in list(all_anchors.keys()):
if key.endswith("exit"):
del all_anchors[key]
for anchor_name in anchor_names:
new_has_underscore = anchor_name.startswith("_")
if (component_idx > 0 or has_underscore) and new_has_underscore:
continue
if component_idx > 0 and anchor_name.endswith("entry"):
continue
new_anchor_name = _rename_anchor_for_scale(anchor_name, scale)
if (
is_ligature
and component_number_of_base_glyphs[0] > 0
and not new_anchor_name.startswith("_")
# the original code here uses 'containsString' (equivalent of Python `in`)
# but that would be inconsistent with the previous code where it uses 'hasSuffix'
# (equivalent of Python `endswith`), so I just go with the latter...
and not new_anchor_name.endswith(("entry", "exit"))
):
anchor_index = 0
if "_" in new_anchor_name:
anchor_index = int(
new_anchor_name[new_anchor_name.index("_") + 1 :]
)
if anchor_index > 0:
anchor_index -= (
1 # "only interested in the offset, adding 1 back below"
)
new_anchor_name = new_anchor_name[: new_anchor_name.index("_")]
new_anchor_name = (
new_anchor_name
+ "_"
+ str(number_of_base_glyphs[0] + anchor_index + 1)
)
anchor = anchors[anchor_name]
if anchor.parent:
anchor = _copy_anchor(anchor)
pos = tuple(anchor.position)
new_pos = transform.transformPoint(pos)
if abs(new_pos[0] - pos[0]) > 0.01 or abs(new_pos[1] - pos[1] > 0.01):
anchor.position = NSPoint(*new_pos)
if anchor.name != new_anchor_name:
anchor.name = new_anchor_name
all_anchors[new_anchor_name] = anchor
if new_has_underscore:
has_underscore = True
if component_number_of_base_glyphs[0] > 0:
number_of_base_glyphs[0] += component_number_of_base_glyphs[0]
seen_components.remove(glyph.name)
origin_anchor = next((a for a in current_anchors if a.name == "*origin"), None)
has_underscore_anchor = False
has_mark_anchor = False
for anchor in current_anchors:
# shouldn't we skip if anchor.name == '*origin'? Elsewhere this would get popped
new_anchor = _copy_anchor(anchor, origin_anchor=origin_anchor)
all_anchors[new_anchor.name] = new_anchor
component_count_from_anchors = 0
for name, anchor in all_anchors.items():
first_char = name[0]
if first_char == "_":
has_underscore_anchor = True
if first_char in string.ascii_letters:
has_mark_anchor = True
if (
not is_ligature
and number_of_base_glyphs[0] == 0
and not name.startswith("_")
and not name.endswith(("entry", "exit"))
):
anchor_index = 0
if "_" in name:
anchor_index = int(name[name.index("_") + 1 :])
if name.startswith("caret_"):
anchor_index += 1
if anchor_index > component_count_from_anchors:
component_count_from_anchors = anchor_index
if not has_underscore_anchor and number_of_base_glyphs[0] == 0 and has_mark_anchor:
number_of_base_glyphs[0] += 1
if number_of_base_glyphs[0] < component_count_from_anchors:
number_of_base_glyphs[0] = component_count_from_anchors
anchor_names = {a.name for a in current_anchors}
if "_bottom" in anchor_names:
if "top" in all_anchors:
del all_anchors["top"]
if "_top" in all_anchors:
del all_anchors["_top"]
if "_top" in anchor_names:
if "bottom" in all_anchors:
del all_anchors["bottom"]
if "_bottom" in all_anchors:
del all_anchors["_bottom"]
return all_anchors
def _rename_anchor_for_scale(anchor_name, scale):
if scale.y < 0:
# component is flipped vertically
if "bottom" in anchor_name:
anchor_name = anchor_name.replace("bottom", "top")
elif "top" in anchor_name:
anchor_name = anchor_name.replace("top", "bottom")
if scale.x < 0:
# component is flipped horizontally
if "left" in anchor_name:
anchor_name = anchor_name.replace("left", "right")
elif "right" in anchor_name:
anchor_name = anchor_name.replace("right", "left")
if "exit" in anchor_name:
anchor_name = anchor_name.replace("exit", "entry")
elif "entry" in anchor_name:
anchor_name = anchor_name.replace("entry", "exit")
return anchor_name
def propagate_anchors(layer):
# this is the undocumented built-in method (implemented in Obj-C).
# Un-comment to test this instead of the python port immediately below
# all_anchors = layer.anchorsTraversingComponents()
all_anchors = anchors_traversing_components(layer)
# I test for None because _sometimes_ anchorsTraversingComponents returns None
if all_anchors is not None:
all_anchors = sorted(all_anchors, key=lambda a: a.name)
if all_anchors:
# if I simply set `layers.anchors = all_anchors` it doesn't work...
# I have to append one by one :/
layer.anchors = []
for a in all_anchors:
layer.anchors.append(a)
def propagate_all_anchors(font):
for glyph in font.glyphs:
for i, layer in enumerate(glyph.layers):
if not layer.components:
continue
existing_anchors = {a.name for a in layer.anchors}
propagate_anchors(layer)
new_anchors = {a.name for a in layer.anchors}
diff_anchors = set(new_anchors) - set(existing_anchors)
if len(diff_anchors) > 0:
print(f"{glyph.name}: layer {i}")
pprint(
[
(a.name, tuple(a.position))
for a in (a for a in layer.anchors if a.name in diff_anchors)
]
)
# uncomment this to run only on the current (or selected) layer(s)
# for layer in Glyphs.font.selectedLayers:
#. propagate_anchors(layer)
if IN_GLYPHS_APP:
propagate_all_anchors(Glyphs.font)
else:
font = GSFont(sys.argv[1])
propagate_all_anchors(font)
if len(sys.argv) > 2:
font.save(sys.argv[2])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment