Skip to content

Commit

Permalink
Merge pull request #3460 from fonttools/decompose-filter-pen
Browse files Browse the repository at this point in the history
add filter pens that decompose components
  • Loading branch information
anthrotype committed Mar 5, 2024
2 parents 963aeec + f15857c commit a3b9edd
Show file tree
Hide file tree
Showing 5 changed files with 517 additions and 34 deletions.
50 changes: 42 additions & 8 deletions Lib/fontTools/pens/basePen.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from typing import Tuple, Dict

from fontTools.misc.loggingTools import LogMixin
from fontTools.misc.transform import DecomposedTransform
from fontTools.misc.transform import DecomposedTransform, Identity

__all__ = [
"AbstractPen",
Expand Down Expand Up @@ -195,17 +195,40 @@ class DecomposingPen(LoggingPen):
By default a warning message is logged when a base glyph is missing;
set the class variable ``skipMissingComponents`` to False if you want
to raise a :class:`MissingComponentError` exception.
all instances of a sub-class to raise a :class:`MissingComponentError`
exception by default.
"""

skipMissingComponents = True
# alias error for convenience
MissingComponentError = MissingComponentError

def __init__(self, glyphSet):
"""Takes a single 'glyphSet' argument (dict), in which the glyphs
that are referenced as components are looked up by their name.
def __init__(
self,
glyphSet,
*args,
skipMissingComponents=None,
reverseFlipped=False,
**kwargs,
):
"""Takes a 'glyphSet' argument (dict), in which the glyphs that are referenced
as components are looked up by their name.
If the optional 'reverseFlipped' argument is True, components whose transformation
matrix has a negative determinant will be decomposed with a reversed path direction
to compensate for the flip.
The optional 'skipMissingComponents' argument can be set to True/False to
override the homonymous class attribute for a given pen instance.
"""
super(DecomposingPen, self).__init__()
super(DecomposingPen, self).__init__(*args, **kwargs)
self.glyphSet = glyphSet
self.skipMissingComponents = (
self.__class__.skipMissingComponents
if skipMissingComponents is None
else skipMissingComponents
)
self.reverseFlipped = reverseFlipped

def addComponent(self, glyphName, transformation):
"""Transform the points of the base glyph and draw it onto self."""
Expand All @@ -218,8 +241,19 @@ def addComponent(self, glyphName, transformation):
raise MissingComponentError(glyphName)
self.log.warning("glyph '%s' is missing from glyphSet; skipped" % glyphName)
else:
tPen = TransformPen(self, transformation)
glyph.draw(tPen)
pen = self
if transformation != Identity:
pen = TransformPen(pen, transformation)
if self.reverseFlipped:
# if the transformation has a negative determinant, it will
# reverse the contour direction of the component
a, b, c, d = transformation[:4]
det = a * d - b * c
if det < 0:
from fontTools.pens.reverseContourPen import ReverseContourPen

pen = ReverseContourPen(pen)
glyph.draw(pen)

def addVarComponent(self, glyphName, transformation, location):
# GlyphSet decomposes for us
Expand Down
86 changes: 82 additions & 4 deletions Lib/fontTools/pens/filterPen.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from fontTools.pens.basePen import AbstractPen
from fontTools.pens.pointPen import AbstractPointPen
from __future__ import annotations

from fontTools.pens.basePen import AbstractPen, DecomposingPen
from fontTools.pens.pointPen import AbstractPointPen, DecomposingPointPen
from fontTools.pens.recordingPen import RecordingPen


Expand Down Expand Up @@ -150,8 +152,8 @@ class FilterPointPen(_PassThruComponentsMixin, AbstractPointPen):
('endPath', (), {})
"""

def __init__(self, outPointPen):
self._outPen = outPointPen
def __init__(self, outPen):
self._outPen = outPen

def beginPath(self, **kwargs):
self._outPen.beginPath(**kwargs)
Expand All @@ -161,3 +163,79 @@ def endPath(self):

def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)


class _DecomposingFilterPenMixin:
"""Mixin class that decomposes components as regular contours.
Shared by both DecomposingFilterPen and DecomposingFilterPointPen.
Takes two required parameters, another (segment or point) pen 'outPen' to draw
with, and a 'glyphSet' dict of drawable glyph objects to draw components from.
The 'skipMissingComponents' and 'reverseFlipped' optional arguments work the
same as in the DecomposingPen/DecomposingPointPen. Both are False by default.
In addition, the decomposing filter pens also take the following two options:
'include' is an optional set of component base glyph names to consider for
decomposition; the default include=None means decompose all components no matter
the base glyph name).
'decomposeNested' (bool) controls whether to recurse decomposition into nested
components of components (this only matters when 'include' was also provided);
if False, only decompose top-level components included in the set, but not
also their children.
"""

# raises MissingComponentError if base glyph is not found in glyphSet
skipMissingComponents = False

def __init__(
self,
outPen,
glyphSet,
skipMissingComponents=None,
reverseFlipped=False,
include: set[str] | None = None,
decomposeNested: bool = True,
):
super().__init__(
outPen=outPen,
glyphSet=glyphSet,
skipMissingComponents=skipMissingComponents,
reverseFlipped=reverseFlipped,
)
self.include = include
self.decomposeNested = decomposeNested

def addComponent(self, baseGlyphName, transformation, **kwargs):
# only decompose the component if it's included in the set
if self.include is None or baseGlyphName in self.include:
# if we're decomposing nested components, temporarily set include to None
include_bak = self.include
if self.decomposeNested and self.include:
self.include = None
try:
super().addComponent(baseGlyphName, transformation, **kwargs)
finally:
if self.include != include_bak:
self.include = include_bak
else:
_PassThruComponentsMixin.addComponent(
self, baseGlyphName, transformation, **kwargs
)


class DecomposingFilterPen(_DecomposingFilterPenMixin, DecomposingPen, FilterPen):
"""Filter pen that draws components as regular contours."""

pass


class DecomposingFilterPointPen(
_DecomposingFilterPenMixin, DecomposingPointPen, FilterPointPen
):
"""Filter point pen that draws components as regular contours."""

pass
79 changes: 77 additions & 2 deletions Lib/fontTools/pens/pointPen.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
import math
from typing import Any, Optional, Tuple, Dict

from fontTools.pens.basePen import AbstractPen, PenError
from fontTools.misc.transform import DecomposedTransform
from fontTools.misc.loggingTools import LogMixin
from fontTools.pens.basePen import AbstractPen, MissingComponentError, PenError
from fontTools.misc.transform import DecomposedTransform, Identity

__all__ = [
"AbstractPointPen",
Expand Down Expand Up @@ -523,3 +524,77 @@ def addComponent(self, glyphName, transform, identifier=None, **kwargs):
if self.currentContour is not None:
raise PenError("Components must be added before or after contours")
self.pen.addComponent(glyphName, transform, identifier=identifier, **kwargs)


class DecomposingPointPen(LogMixin, AbstractPointPen):
"""Implements a 'addComponent' method that decomposes components
(i.e. draws them onto self as simple contours).
It can also be used as a mixin class (e.g. see DecomposingRecordingPointPen).
You must override beginPath, addPoint, endPath. You may
additionally override addVarComponent and addComponent.
By default a warning message is logged when a base glyph is missing;
set the class variable ``skipMissingComponents`` to False if you want
all instances of a sub-class to raise a :class:`MissingComponentError`
exception by default.
"""

skipMissingComponents = True
# alias error for convenience
MissingComponentError = MissingComponentError

def __init__(
self,
glyphSet,
*args,
skipMissingComponents=None,
reverseFlipped=False,
**kwargs,
):
"""Takes a 'glyphSet' argument (dict), in which the glyphs that are referenced
as components are looked up by their name.
If the optional 'reverseFlipped' argument is True, components whose transformation
matrix has a negative determinant will be decomposed with a reversed path direction
to compensate for the flip.
The optional 'skipMissingComponents' argument can be set to True/False to
override the homonymous class attribute for a given pen instance.
"""
super().__init__(*args, **kwargs)
self.glyphSet = glyphSet
self.skipMissingComponents = (
self.__class__.skipMissingComponents
if skipMissingComponents is None
else skipMissingComponents
)
self.reverseFlipped = reverseFlipped

def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
"""Transform the points of the base glyph and draw it onto self.
The `identifier` parameter and any extra kwargs are ignored.
"""
from fontTools.pens.transformPen import TransformPointPen

try:
glyph = self.glyphSet[baseGlyphName]
except KeyError:
if not self.skipMissingComponents:
raise MissingComponentError(baseGlyphName)
self.log.warning(
"glyph '%s' is missing from glyphSet; skipped" % baseGlyphName
)
else:
pen = self
if transformation != Identity:
pen = TransformPointPen(pen, transformation)
if self.reverseFlipped:
# if the transformation has a negative determinant, it will
# reverse the contour direction of the component
a, b, c, d = transformation[:4]
det = a * d - b * c
if a * d - b * c < 0:
pen = ReverseContourPointPen(pen)
glyph.drawPoints(pen)

0 comments on commit a3b9edd

Please sign in to comment.