Skip to content

Commit

Permalink
Merge pull request #971 from googlefonts/interpolate-components-with-…
Browse files Browse the repository at this point in the history
…intermediate

Build intermediate layers with non-intermediate components
  • Loading branch information
anthrotype authored Jan 30, 2024
2 parents ce6d451 + 8b758e6 commit d345113
Show file tree
Hide file tree
Showing 7 changed files with 540 additions and 10 deletions.
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

0 comments on commit d345113

Please sign in to comment.