Skip to content

Commit ab5ec4c

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix-propagate-cursive-anchors
# Conflicts: # tests/builder/transformations/propagate_anchors_test.py
2 parents 46c9bf7 + 78b4775 commit ab5ec4c

File tree

5 files changed

+225
-11
lines changed

5 files changed

+225
-11
lines changed

Lib/glyphsLib/builder/builders.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,27 @@ def __init__(
190190
self.glyphdata = glyph_data
191191

192192
def _is_vertical(self):
193+
# Check for vhea custom parameters at font or master level.
194+
# To match ufo2ft, require all three vhea params (ascender, descender, lineGap):
195+
# https://github.com/googlefonts/ufo2ft/blob/28acf8870487/Lib/ufo2ft/outlineCompiler.py#L154-L163
196+
# https://github.com/googlefonts/glyphsLib/issues/1132
197+
# Each pair contains aliases for the same metric
198+
vhea_metrics = (
199+
{"vheaVertAscender", "vheaVertTypoAscender"},
200+
{"vheaVertDescender", "vheaVertTypoDescender"},
201+
{"vheaVertLineGap", "vheaVertTypoLineGap"},
202+
)
203+
204+
def has_all_vhea_params(custom_params):
205+
names = {p.name for p in custom_params}
206+
return all(names & metric for metric in vhea_metrics)
207+
208+
if has_all_vhea_params(self.font.customParameters) or any(
209+
has_all_vhea_params(master.customParameters) for master in self.font.masters
210+
):
211+
return True
212+
213+
# Check for glyph-level vertical attributes
193214
master_ids = {m.id for m in self.font.masters}
194215
for glyph in self.font.glyphs:
195216
for layer in glyph.layers:

Lib/glyphsLib/builder/smart_components.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ def decompose_smart_components_in_layer(self, layer):
129129
return new_layer
130130

131131

132-
def instantiate_smart_component(self, layer, component, pen):
133-
"""Instantiate a smart component by interpolating and drawing to a pointPen."""
132+
def get_smart_component_variation_model(layer, component):
133+
"""Get the variation model for a smart component."""
134134
# Find the GSGlyph that is being used as a component by this GSComponent
135135
root = component.component
136136

@@ -149,9 +149,7 @@ def instantiate_smart_component(self, layer, component, pen):
149149
)
150150

151151
if len(masters) == 1:
152-
# Treat this as a dumb component.
153-
pen.addComponent(component.name, component.transform)
154-
return
152+
return None, None, None
155153

156154
model = variation_model(root, masters, layer)
157155

@@ -169,6 +167,23 @@ def instantiate_smart_component(self, layer, component, pen):
169167
name: normalizeValue(value, axes_tuples[name], extrapolate=True)
170168
for name, value in component.smartComponentValues.items()
171169
}
170+
171+
return model, normalized_location, masters
172+
173+
174+
def instantiate_smart_component(self, layer, component, pen):
175+
"""Instantiate a smart component by interpolating and drawing to a pointPen."""
176+
# Find the GSGlyph that is being used as a component by this GSComponent
177+
root = component.component
178+
179+
model, normalized_location, masters = get_smart_component_variation_model(
180+
layer, component
181+
)
182+
if model is None:
183+
# Treat this as a dumb component.
184+
pen.addComponent(component.name, component.transform)
185+
return
186+
172187
# Decompose nested smart components before extracting coordinates
173188
decomposed_masters = [decompose_smart_components_in_layer(self, l) for l in masters]
174189
coordinates = [get_coordinates(l) for l in decomposed_masters]

Lib/glyphsLib/builder/transformations/propagate_anchors.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,20 @@ def maybe_log_new_anchors(
9999
)
100100

101101

102+
def _is_master_layer(layer: GSLayer) -> bool:
103+
# Treat smart component layers as master layers
104+
return layer._is_master_layer or (
105+
layer.parent.smartComponentAxes and layer.smartComponentPoleMapping
106+
)
107+
108+
102109
def _interesting_layers(glyph):
103110
# only master layers are currently supported for anchor propagation:
104111
# https://github.com/googlefonts/glyphsLib/issues/1017
105112
return (
106113
l
107114
for l in glyph.layers
108-
if l._is_master_layer or l._is_bracket_layer()
115+
if _is_master_layer(l) or l._is_bracket_layer()
109116
# or l._is_brace_layer
110117
# etc.
111118
)
@@ -139,6 +146,42 @@ def _get_subCategory(
139146
)
140147

141148

149+
def _interpolate_smart_component_anchors(
150+
layer: GSLayer,
151+
component: GSComponent,
152+
glyphs: dict[str, GSGlyph],
153+
done_anchors: dict[str, dict[str, list[GSAnchor]]],
154+
anchors: list[GSAnchor],
155+
) -> None:
156+
from ..smart_components import get_smart_component_variation_model
157+
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
158+
159+
model, location, masters = get_smart_component_variation_model(layer, component)
160+
if model is not None:
161+
coords = [
162+
GlyphCoordinates(
163+
[
164+
anchor.position
165+
for anchor in get_component_layer_anchors(
166+
component, master, glyphs, done_anchors
167+
)
168+
]
169+
)
170+
for master in masters
171+
]
172+
173+
try:
174+
new_coords = model.interpolateFromMasters(location, coords)
175+
except Exception as e:
176+
raise ValueError(
177+
"Could not interpolate smart component %s used in %s"
178+
% (component.name, layer)
179+
) from e
180+
181+
for anchor, new_coord in zip(anchors, new_coords):
182+
anchor.position = Point(new_coord[0], new_coord[1])
183+
184+
142185
def anchors_traversing_components(
143186
glyph: GSGlyph,
144187
layer: GSLayer,
@@ -187,6 +230,12 @@ def anchors_traversing_components(
187230
)
188231
continue
189232

233+
if component.smartComponentValues and component.component.smartComponentAxes:
234+
# If this is a smart component, we need to interpolate the anchors
235+
_interpolate_smart_component_anchors(
236+
layer, component, glyphs, done_anchors, anchors
237+
)
238+
190239
# if this component has an explicitly set attachment anchor, use it
191240
if component_idx > 0 and component.anchor:
192241
maybe_rename_component_anchor(component.anchor, anchors)
@@ -401,7 +450,7 @@ def get_component_layer_anchors(
401450
# whether the parent layer where the component is defined is a 'master' layer
402451
# and/or a 'bracket' or alternate layer (masters can have bracket layers too but
403452
# glyphsLib doesn't support that yet).
404-
parent_is_master = layer._is_master_layer
453+
parent_is_master = _is_master_layer(layer)
405454
parent_is_bracket = layer._is_bracket_layer()
406455
parent_axis_rules = (
407456
[] if not parent_is_bracket else list(layer._bracket_axis_rules())

tests/builder/builder_test.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2857,3 +2857,37 @@ def test_glyphs_to_ufo_with_partial_glyphOrder(self, ufo_module):
28572857
ufo = self.from_glyphs(ufo_module)
28582858
assert ["xxx1", "f", "xxx2", "c", "a"] == ufo.lib["public.glyphOrder"]
28592859
assert GLYPHS_PREFIX + "glyphOrder" not in ufo.lib
2860+
2861+
2862+
def test_vhea_custom_params_trigger_is_vertical(ufo_module):
2863+
"""Test that vhea custom parameters (without per-glyph vertical metrics)
2864+
trigger is_vertical=True and populate glyph heights in UFO.
2865+
2866+
https://github.com/googlefonts/glyphsLib/issues/1132
2867+
"""
2868+
font = generate_minimal_font()
2869+
font.masters[0].ascender = 800
2870+
font.masters[0].descender = -200
2871+
2872+
# Set vhea custom parameters on master (no per-glyph vertical metrics)
2873+
font.masters[0].customParameters["vheaVertAscender"] = 500
2874+
font.masters[0].customParameters["vheaVertDescender"] = -500
2875+
font.masters[0].customParameters["vheaVertLineGap"] = 0
2876+
2877+
# Add a glyph without explicit vertWidth/vertOrigin
2878+
add_glyph(font, "a")
2879+
2880+
builder = UFOBuilder(font, ufo_module=ufo_module)
2881+
assert builder.is_vertical is True
2882+
2883+
ufo = next(iter(builder.masters))
2884+
2885+
# Glyph should have height set to typoAscender - typoDescender
2886+
# (which defaults to master ascender - descender when not explicitly set)
2887+
expected_height = font.masters[0].ascender - font.masters[0].descender
2888+
assert ufo["a"].height == expected_height
2889+
2890+
# vhea UFO info should be populated
2891+
assert ufo.info.openTypeVheaVertTypoAscender == 500
2892+
assert ufo.info.openTypeVheaVertTypoDescender == -500
2893+
assert ufo.info.openTypeVheaVertTypoLineGap == 0

tests/builder/transformations/propagate_anchors_test.py

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@
1212

1313
from fontTools.misc.transform import Transform as Affine
1414

15-
from glyphsLib.classes import GSAnchor, GSFont, GSGlyph, GSLayer, GSComponent
15+
from glyphsLib.classes import (
16+
GSAnchor,
17+
GSFont,
18+
GSGlyph,
19+
GSLayer,
20+
GSComponent,
21+
GSSmartComponentAxis,
22+
)
1623
from glyphsLib.glyphdata import get_glyph
1724
from glyphsLib.types import Point, Transform
1825
from glyphsLib.writer import dumps
@@ -50,7 +57,7 @@ def build(self) -> dict[str, GSGlyph]:
5057
return {g.name: g for g in self.font.glyphs}
5158

5259
def add_glyph(self, name: str, build_fn: Callable[["GlyphBuilder"], None]) -> Self:
53-
glyph = GlyphBuilder(name)
60+
glyph = GlyphBuilder(name, self.font)
5461
build_fn(glyph)
5562
# this inserts the glyph in the font and sets the glyph.parent attribute to it;
5663
# GSLayer._is_bracket_layer()/_bracket_axis_rules() require this to check the
@@ -60,13 +67,14 @@ def add_glyph(self, name: str, build_fn: Callable[["GlyphBuilder"], None]) -> Se
6067

6168

6269
class GlyphBuilder:
63-
def __init__(self, name: str):
70+
def __init__(self, name: str, font: GSFont = None):
6471
info = get_glyph(name)
6572
self.glyph = glyph = GSGlyph()
6673
glyph.name = name
6774
glyph.unicode = info.unicode
6875
glyph.category = info.category
6976
glyph.subCategory = info.subCategory
77+
glyph.parent = font
7078
self.num_masters = 0
7179
self.num_alternates = 0
7280
self.add_master_layer()
@@ -117,9 +125,30 @@ def set_subCategory(self, subCategory: str) -> Self:
117125
self.glyph.subCategory = subCategory
118126
return self
119127

120-
def add_component(self, name: str, pos: tuple[float, float]) -> Self:
128+
def add_smartComponentAxis(
129+
self, name: str, top_value: float, bottom_value: float
130+
) -> Self:
131+
axis = GSSmartComponentAxis()
132+
axis.name = name
133+
axis.topValue = top_value
134+
axis.bottomValue = bottom_value
135+
self.glyph.smartComponentAxes.append(axis)
136+
return self
137+
138+
def set_smartComponentPoleMapping(self, name: str, pole: int) -> Self:
139+
self.current_layer.smartComponentPoleMapping[name] = pole
140+
return self
141+
142+
def add_component(
143+
self,
144+
name: str,
145+
pos: tuple[float, float],
146+
smart_component_values: dict[str, float] = None,
147+
) -> Self:
121148
component = GSComponent(name, offset=pos)
122149
self.current_layer.components.append(component)
150+
if smart_component_values:
151+
component.smartComponentValues = smart_component_values
123152
return self
124153

125154
def rotate_component(self, degrees: float) -> Self:
@@ -884,3 +913,69 @@ def test_cursive_anchors_ligature():
884913
("exit.1", (100, 0)),
885914
],
886915
)
916+
917+
918+
def test_smart_component_anchors():
919+
glyphs = (
920+
GlyphSetBuilder()
921+
.add_glyph(
922+
"base",
923+
lambda glyph: (
924+
glyph.add_smartComponentAxis("TEST", 100, -100)
925+
.set_smartComponentPoleMapping("TEST", 1)
926+
.add_anchor("top", (23, 103))
927+
.add_anchor("bottom", (36, -51))
928+
.add_backup_layer()
929+
.set_smartComponentPoleMapping("TEST", 2)
930+
.add_anchor("top", (33, 123))
931+
.add_anchor("bottom", (36, -51))
932+
),
933+
)
934+
.add_glyph(
935+
"smart1",
936+
lambda glyph: (glyph.add_component("base", (0, 0), {"TEST": -100})),
937+
)
938+
.add_glyph(
939+
"smart2",
940+
lambda glyph: (glyph.add_component("base", (0, 0), {"TEST": 100})),
941+
)
942+
.add_glyph(
943+
"smart3",
944+
lambda glyph: (glyph.add_component("base", (0, 0), {"TEST": 0})),
945+
)
946+
.add_glyph(
947+
"smart4",
948+
lambda glyph: (glyph.add_component("base", (20, 10), {"TEST": 0})),
949+
)
950+
.build()
951+
)
952+
propagate_all_anchors_impl(glyphs)
953+
954+
assert_anchors(
955+
glyphs["smart1"].layers[0].anchors,
956+
[
957+
("top", (23, 103)),
958+
("bottom", (36, -51)),
959+
],
960+
)
961+
assert_anchors(
962+
glyphs["smart2"].layers[0].anchors,
963+
[
964+
("top", (33, 123)),
965+
("bottom", (36, -51)),
966+
],
967+
)
968+
assert_anchors(
969+
glyphs["smart3"].layers[0].anchors,
970+
[
971+
("top", (28, 113)),
972+
("bottom", (36, -51)),
973+
],
974+
)
975+
assert_anchors(
976+
glyphs["smart4"].layers[0].anchors,
977+
[
978+
("top", (48, 123)),
979+
("bottom", (56, -41)),
980+
],
981+
)

0 commit comments

Comments
 (0)