Last active
April 9, 2024 12:46
-
-
Save anthrotype/54ab6414bb4404f0ec0daa0c0bf9a934 to your computer and use it in GitHub Desktop.
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 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