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

Don't propagate anchors to "mark" composite glyphs with anchors #815

Merged
merged 2 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 3 additions & 43 deletions Lib/ufo2ft/featureWriters/baseFeatureWriter.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import logging
from collections import OrderedDict, namedtuple
from collections import OrderedDict
from types import SimpleNamespace

from fontTools.designspaceLib import DesignSpaceDocument
from fontTools.feaLib.variableScalar import VariableScalar
from fontTools.misc.fixedTools import otRound

from ufo2ft.constants import OPENTYPE_CATEGORIES_KEY
from ufo2ft.errors import InvalidFeaturesData
from ufo2ft.featureWriters import ast
from ufo2ft.util import (
OpenTypeCategories,
collapse_varscalar,
get_userspace_location,
quantize,
Expand Down Expand Up @@ -351,47 +351,7 @@ def extraSubstitutions(self):
def getOpenTypeCategories(self):
"""Return 'public.openTypeCategories' values as a tuple of sets of
unassigned, bases, ligatures, marks, components."""
font = self.context.font
unassigned, bases, ligatures, marks, components = (
set(),
set(),
set(),
set(),
set(),
)
openTypeCategories = font.lib.get(OPENTYPE_CATEGORIES_KEY, {})
# Handle case where we are a variable feature writer
if not openTypeCategories and isinstance(font, DesignSpaceDocument):
font = font.sources[0].font
openTypeCategories = font.lib.get(OPENTYPE_CATEGORIES_KEY, {})

for glyphName, category in openTypeCategories.items():
if category == "unassigned":
unassigned.add(glyphName)
elif category == "base":
bases.add(glyphName)
elif category == "ligature":
ligatures.add(glyphName)
elif category == "mark":
marks.add(glyphName)
elif category == "component":
components.add(glyphName)
else:
self.log.warning(
f"The '{OPENTYPE_CATEGORIES_KEY}' value of {glyphName} in "
f"{font.info.familyName} {font.info.styleName} is '{category}' "
"when it should be 'unassigned', 'base', 'ligature', 'mark' "
"or 'component'."
)
return namedtuple(
"OpenTypeCategories", "unassigned base ligature mark component"
)(
frozenset(unassigned),
frozenset(bases),
frozenset(ligatures),
frozenset(marks),
frozenset(components),
)
return OpenTypeCategories.load(self.context.font)

def getGDEFGlyphClasses(self):
"""Return a tuple of GDEF GlyphClassDef base, ligature, mark, component
Expand Down
23 changes: 15 additions & 8 deletions Lib/ufo2ft/filters/propagateAnchors.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from fontTools.misc.transform import Transform

from ufo2ft.filters import BaseFilter
from ufo2ft.util import OpenTypeCategories

logger = logging.getLogger(__name__)

Expand All @@ -26,14 +27,14 @@ class PropagateAnchorsFilter(BaseFilter):
def set_context(self, font, glyphSet):
ctx = super().set_context(font, glyphSet)
ctx.processed = set()
ctx.categories = OpenTypeCategories.load(font)
return ctx

def __call__(self, font, glyphSet=None):
if super().__call__(font, glyphSet):
modified = self.context.modified
if modified:
logger.info("Glyphs with propagated anchors: %i" % len(modified))
return modified
modified = super().__call__(font, glyphSet)
if modified:
logger.info("Glyphs with propagated anchors: %i" % len(modified))
return modified

def filter(self, glyph):
if not glyph.components:
Expand All @@ -44,11 +45,12 @@ def filter(self, glyph):
glyph,
self.context.processed,
self.context.modified,
self.context.categories,
)
return len(glyph.anchors) > before


def _propagate_glyph_anchors(glyphSet, composite, processed, modified):
def _propagate_glyph_anchors(glyphSet, composite, processed, modified, categories):
"""
Propagate anchors from base glyphs to a given composite
glyph, and to all composite glyphs used in between.
Expand All @@ -58,7 +60,12 @@ def _propagate_glyph_anchors(glyphSet, composite, processed, modified):
return
processed.add(composite.name)

if not composite.components:
if not composite.components or (
# "If it is a 'mark' and there are anchors, it will not look into components"
# Georg said: https://github.com/googlefonts/ufo2ft/issues/802#issuecomment-1904109457
composite.name in categories.mark
and composite.anchors
):
return

base_components = []
Expand All @@ -74,7 +81,7 @@ def _propagate_glyph_anchors(glyphSet, composite, processed, modified):
"in glyph {}".format(component.baseGlyph, composite.name)
)
else:
_propagate_glyph_anchors(glyphSet, glyph, processed, modified)
_propagate_glyph_anchors(glyphSet, glyph, processed, modified, categories)
if any(a.name.startswith("_") for a in glyph.anchors):
mark_components.append(component)
else:
Expand Down
55 changes: 53 additions & 2 deletions Lib/ufo2ft/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import re
from copy import deepcopy
from inspect import currentframe, getfullargspec
from typing import Any, Mapping, Set
from typing import Any, Mapping, NamedTuple, Set

from fontTools import subset, ttLib, unicodedata
from fontTools.designspaceLib import DesignSpaceDocument
Expand All @@ -15,7 +15,7 @@
from fontTools.pens.reverseContourPen import ReverseContourPen
from fontTools.pens.transformPen import TransformPen

from ufo2ft.constants import UNICODE_SCRIPT_ALIASES
from ufo2ft.constants import OPENTYPE_CATEGORIES_KEY, UNICODE_SCRIPT_ALIASES
from ufo2ft.errors import InvalidDesignSpaceData, InvalidFontData
from ufo2ft.fontInfoData import getAttrWithFallback

Expand Down Expand Up @@ -713,3 +713,54 @@ def collapse_varscalar(varscalar, threshold=0):
if not any(abs(v - values[0]) > threshold for v in values[1:]):
return list(varscalar.values.values())[0]
return varscalar


class OpenTypeCategories(NamedTuple):
unassigned: frozenset[str]
base: frozenset[str]
ligature: frozenset[str]
mark: frozenset[str]
component: frozenset[str]

@classmethod
def load(cls, font):
"""Return 'public.openTypeCategories' values as a tuple of sets of
unassigned, bases, ligatures, marks, components."""
unassigned, bases, ligatures, marks, components = (
set(),
set(),
set(),
set(),
set(),
)
openTypeCategories = font.lib.get(OPENTYPE_CATEGORIES_KEY, {})
# Handle case where we are a variable feature writer
if not openTypeCategories and isinstance(font, DesignSpaceDocument):
font = font.sources[0].font
openTypeCategories = font.lib.get(OPENTYPE_CATEGORIES_KEY, {})

for glyphName, category in openTypeCategories.items():
if category == "unassigned":
unassigned.add(glyphName)
elif category == "base":
bases.add(glyphName)
elif category == "ligature":
ligatures.add(glyphName)
elif category == "mark":
marks.add(glyphName)
elif category == "component":
components.add(glyphName)
else:
logging.getLogger("ufo2ft").warning(
f"The '{OPENTYPE_CATEGORIES_KEY}' value of {glyphName} in "
f"{font.info.familyName} {font.info.styleName} is '{category}' "
"when it should be 'unassigned', 'base', 'ligature', 'mark' "
"or 'component'."
)
return cls(
frozenset(unassigned),
frozenset(bases),
frozenset(ligatures),
frozenset(marks),
frozenset(components),
)
31 changes: 30 additions & 1 deletion tests/filters/propagateAnchors_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,25 @@
("addComponent", ("macroncomb", (1, 0, 0, 1, 175, 0))),
],
},
{
"name": "r",
"width": 350,
"outline": [
("moveTo", ((0, 0),)),
("lineTo", ((0, 300),)),
("lineTo", ((175, 300),)),
("closePath", ()),
],
"anchors": [(175, 300, "top"), (175, 0, "bottom")],
},
{
"name": "rcombbelow",
"width": 0,
"outline": [
("addComponent", ("r", (0.5, 0, 0, 0.5, -100, -100))),
],
"anchors": [(0, 0, "_bottom")],
},
]
}
]
Expand All @@ -120,6 +139,12 @@ def font(request, FontClass):
getattr(pen, operator)(*operands)
for x, y, name in param.get("anchors", []):
glyph.appendAnchor(dict(x=x, y=y, name=name))
# classify as 'mark' all glyphs with zero width and 'comb' in their name
font.lib["public.openTypeCategories"] = {
g["name"]: "mark"
for g in request.param["glyphs"]
if g.get("width", 0) == 0 and "comb" in g["name"]
}
return font


Expand Down Expand Up @@ -149,6 +174,10 @@ def font(request, FontClass):
],
{"a_a"},
),
# the composite glyph is a mark with anchors, hence propagation is not performed,
# i.e. 'top' and 'bottom' are *not* copied to 'rcombbelow':
# https://github.com/googlefonts/ufo2ft/issues/802
"rcombbelow": ([("_bottom", 0, 0)], set()),
}


Expand All @@ -173,7 +202,7 @@ def test_include_one_glyph_at_a_time(self, font, name):
def test_whole_font(self, font):
philter = PropagateAnchorsFilter()
modified = philter(font)
assert modified == set(EXPECTED)
assert modified == {k for k in EXPECTED if k in EXPECTED[k][1]}
for name, (expected_anchors, _) in EXPECTED.items():
assert [(a.name, a.x, a.y) for a in font[name].anchors] == expected_anchors

Expand Down
Loading