Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build intermediate layers with non-intermediate components #971

Merged
merged 12 commits into from
Jan 30, 2024
33 changes: 30 additions & 3 deletions Lib/glyphsLib/builder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import copy
import logging

from glyphsLib import classes

from .builders import UFOBuilder, GlyphsBuilder
from .transformations import TRANSFORMATIONS

logger = logging.getLogger(__name__)

Expand All @@ -34,11 +36,13 @@ def to_ufos(
expand_includes=False,
minimal=False,
glyph_data=None,
preserve_original=False,
):
"""Take a GSFont object and convert it into one UFO per master.

Takes in data as Glyphs.app-compatible classes, as documented at
https://docu.glyphsapp.com/
https://docu.glyphsapp.com/. The input ``GSFont`` object is modified
unless ``preserve_original`` is true.

If include_instances is True, also returns the parsed instance data.

Expand All @@ -54,9 +58,14 @@ def to_ufos(
If minimal is True, it is assumed that the UFOs will only be used in
font production, and unnecessary steps (e.g. converting background layers)
will be skipped.

If preserve_original is True, this works on a copy of the font object
to avoid modifying the original object.
"""
if preserve_original:
font = copy.deepcopy(font)
builder = UFOBuilder(
font,
preflight_glyphs(font),
ufo_module=ufo_module,
family_name=family_name,
propagate_anchors=propagate_anchors,
Expand Down Expand Up @@ -89,13 +98,16 @@ def to_designspace(
expand_includes=False,
minimal=False,
glyph_data=None,
preserve_original=False,
):
"""Take a GSFont object and convert it into a Designspace Document + UFOS.
The UFOs are available as the attribute `font` of each SourceDescriptor of
the DesignspaceDocument:

ufos = [source.font for source in designspace.sources]

The input object is modified unless ``preserve_original`` is true.

The designspace and the UFOs are not written anywhere by default, they
are all in-memory. If you want to write them to the disk, consider using
the `filename` attribute of the DesignspaceDocument and of its
Expand All @@ -111,9 +123,15 @@ def to_designspace(

If generate_GDEF is True, write a `table GDEF {...}` statement in the
UFO's features.fea, containing GlyphClassDef and LigatureCaretByPos.

If preserve_original is True, this works on a copy of the font object
to avoid modifying the original object.
"""
if preserve_original:
font = copy.deepcopy(font)

builder = UFOBuilder(
font,
preflight_glyphs(font),
ufo_module=ufo_module,
family_name=family_name,
instance_dir=instance_dir,
Expand All @@ -130,6 +148,15 @@ def to_designspace(
return builder.designspace


def preflight_glyphs(font):
"""Run a set of transformations over a GSFont object to make
it easier to convert to UFO; resolve all the "smart stuff"."""

for transform in TRANSFORMATIONS:
transform(font)
return font


def to_glyphs(
ufos_or_designspace,
glyphs_module=classes,
Expand Down
4 changes: 3 additions & 1 deletion Lib/glyphsLib/builder/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ def __init__(
"""Create a builder that goes from Glyphs to UFO + designspace.

Keyword arguments:
font -- The GSFont object to transform into UFOs
font -- The GSFont object to transform into UFOs. We expect this GSFont
object to have been pre-processed with
``glyphsLib.builder.preflight_glyphs``.
ufo_module -- A Python module to use to build UFO objects (you can pass
a custom module that has the same classes as ufoLib2 or
defcon to get instances of your own classes). Default: ufoLib2
Expand Down
4 changes: 4 additions & 0 deletions Lib/glyphsLib/builder/transformations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .intermediate_layers import resolve_intermediate_components


TRANSFORMATIONS = [resolve_intermediate_components]
129 changes: 129 additions & 0 deletions Lib/glyphsLib/builder/transformations/intermediate_layers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import logging
import uuid

from fontTools.varLib.models import VariationModel, normalizeValue

from glyphsLib.classes import GSLayer, GSNode, GSPath
from glyphsLib.builder.axes import get_regular_master


logger = logging.getLogger(__name__)


def resolve_intermediate_components(font):
for glyph in font.glyphs:
for layer in glyph.layers:
if layer._is_brace_layer():
# First, let's find glyphs with intermediate layers
# which have components which don't have intermediate layers
for shape in layer.components:
ensure_component_has_sparse_layer(font, shape, layer)


def variation_model(font, locations):
tags = [axis.axisTag for axis in font.axes]
limits = {tag: (min(x), max(x)) for tag, x in zip(tags, (zip(*locations)))}
master_locations = []
default_location = get_regular_master(font).axes
for loc in locations:
this_loc = {}
for ix, axisTag in enumerate(tags):
axismin, axismax = limits[axisTag]
this_loc[axisTag] = normalizeValue(
loc[ix], (axismin, default_location[ix], axismax)
)
master_locations.append(this_loc)
return VariationModel(master_locations, axisOrder=tags), limits


def ensure_component_has_sparse_layer(font, component, parent_layer):
tags = [axis.axisTag for axis in font.axes]
master_locations = [x.axes for x in font.masters]
_, limits = variation_model(font, master_locations)
location = tuple(parent_layer._brace_coordinates())
default_location = get_regular_master(font).axes
normalized_location = {
axisTag: normalizeValue(
location[ix], (limits[axisTag][0], default_location[ix], limits[axisTag][1])
)
for ix, axisTag in enumerate(tags)
}
componentglyph = component.component
for layer in componentglyph.layers:
if layer.layerId == parent_layer.layerId:
return
if "coordinates" in layer.attributes and layer._brace_coordinates() == location:
return

# We'll add the appropriate intermediate layer to the component, that'll fix it
logger.info(
"Adding intermediate layer to %s to support %s %s",
componentglyph.name,
parent_layer.parent.name,
parent_layer.name,
)
layer = GSLayer()
layer.attributes["coordinates"] = location
layer.layerId = str(uuid.uuid4())
layer.associatedMasterId = parent_layer.associatedMasterId
layer.name = parent_layer.name
# Create a glyph-level variation model for the component glyph,
# including any intermediate layers
interpolatable_layers = []
locations = []
for l in componentglyph.layers:
if l._is_brace_layer():
locations.append(l.attributes["coordinates"])
interpolatable_layers.append(l)
if l._is_master_layer:
locations.append(font.masters[l.associatedMasterId].axes)
interpolatable_layers.append(l)
glyph_level_model, _ = variation_model(font, locations)

# Interpolate new layer width
all_widths = [l.width for l in interpolatable_layers]
layer.width = glyph_level_model.interpolateFromMasters(
normalized_location, all_widths
)

# Interpolate layer shapes
for ix, shape in enumerate(componentglyph.layers[0].shapes):
all_shapes = [l.shapes[ix] for l in interpolatable_layers]
if isinstance(shape, GSPath):
# We are making big assumptions about compatibility here
layer.shapes.append(
interpolate_path(all_shapes, glyph_level_model, normalized_location)
)
else:
ensure_component_has_sparse_layer(font, shape, parent_layer)
layer.shapes.append(
interpolate_component(
all_shapes, glyph_level_model, normalized_location
)
)
componentglyph.layers.append(layer)


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_component(components, model, location):
component = components[0].clone()
if all(c.transform == component.transform for c in components):
return component
transforms = [c.transform for c in components]
for element in range(6):
values = [t[element] for t in transforms]
component.transform[element] = model.interpolateFromMasters(location, values)
return component
12 changes: 6 additions & 6 deletions Lib/glyphsLib/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3699,7 +3699,7 @@ def __repr__(self):
return f'<{self.__class__.__name__} "{name}" ({parent})>'

def __lt__(self, other):
if self.master and other.master and self.associatedMasterId == self.layerId:
if self.master and other.master and self._is_master_layer:
return (
self.master.weightValue < other.master.weightValue
or self.master.widthValue < other.master.widthValue
Expand Down Expand Up @@ -3734,13 +3734,13 @@ def master(self):
master = self.parent.parent.masterForId(self.associatedMasterId)
return master

@property
def _is_master_layer(self):
return self.associatedMasterId == self.layerId

@property
def name(self):
if (
self.associatedMasterId
and self.associatedMasterId == self.layerId
and self.parent
):
if self.associatedMasterId and self._is_master_layer and self.parent:
master = self.parent.parent.masterForId(self.associatedMasterId)
if master:
return master.name
Expand Down
9 changes: 9 additions & 0 deletions tests/builder/preflight_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import glyphsLib
from glyphsLib.builder.transformations import resolve_intermediate_components


def test_intermediates_with_components_without_intermediates(datadir):
font = glyphsLib.GSFont(str(datadir.join("ComponentsWithIntermediates.glyphs")))
assert len(font.glyphs["A"].layers) != len(font.glyphs["Astroke"].layers)
resolve_intermediate_components(font)
assert len(font.glyphs["A"].layers) == len(font.glyphs["Astroke"].layers)
Loading
Loading