Skip to content

Commit

Permalink
[recordingPen] Add DecomposingRecordingPointPen, test new decomposing…
Browse files Browse the repository at this point in the history
… pen options
  • Loading branch information
anthrotype committed Mar 5, 2024
1 parent c0074ee commit cccc358
Showing 1 changed file with 137 additions and 20 deletions.
157 changes: 137 additions & 20 deletions Lib/fontTools/pens/recordingPen.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Pen recording operations that can be accessed or replayed."""

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


__all__ = [
Expand Down Expand Up @@ -85,28 +85,55 @@ class DecomposingRecordingPen(DecomposingPen, RecordingPen):
"""Same as RecordingPen, except that it doesn't keep components
as references, but draws them decomposed as regular contours.
The constructor takes a single 'glyphSet' positional argument,
The constructor takes a required 'glyphSet' positional argument,
a dictionary of glyph objects (i.e. with a 'draw' method) keyed
by thir name::
>>> class SimpleGlyph(object):
... def draw(self, pen):
... pen.moveTo((0, 0))
... pen.curveTo((1, 1), (2, 2), (3, 3))
... pen.closePath()
>>> class CompositeGlyph(object):
... def draw(self, pen):
... pen.addComponent('a', (1, 0, 0, 1, -1, 1))
>>> glyphSet = {'a': SimpleGlyph(), 'b': CompositeGlyph()}
>>> for name, glyph in sorted(glyphSet.items()):
... pen = DecomposingRecordingPen(glyphSet)
... glyph.draw(pen)
... print("{}: {}".format(name, pen.value))
a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())]
b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())]
by thir name; other arguments are forwarded to the DecomposingPen's
constructor::
>>> class SimpleGlyph(object):
... def draw(self, pen):
... pen.moveTo((0, 0))
... pen.curveTo((1, 1), (2, 2), (3, 3))
... pen.closePath()
>>> class CompositeGlyph(object):
... def draw(self, pen):
... pen.addComponent('a', (1, 0, 0, 1, -1, 1))
>>> class MissingComponent(object):
... def draw(self, pen):
... pen.addComponent('foobar', (1, 0, 0, 1, 0, 0))
>>> class FlippedComponent(object):
... def draw(self, pen):
... pen.addComponent('a', (-1, 0, 0, 1, 0, 0))
>>> glyphSet = {
... 'a': SimpleGlyph(),
... 'b': CompositeGlyph(),
... 'c': MissingComponent(),
... 'd': FlippedComponent(),
... }
>>> for name, glyph in sorted(glyphSet.items()):
... pen = DecomposingRecordingPen(glyphSet)
... try:
... glyph.draw(pen)
... except pen.MissingComponentError:
... pass
... print("{}: {}".format(name, pen.value))
a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())]
b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())]
c: []
d: [('moveTo', ((0, 0),)), ('curveTo', ((-1, 1), (-2, 2), (-3, 3))), ('closePath', ())]
>>> for name, glyph in sorted(glyphSet.items()):
... pen = DecomposingRecordingPen(
... glyphSet, skipMissingComponents=True, reverseFlipped=True,
... )
... glyph.draw(pen)
... print("{}: {}".format(name, pen.value))
a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())]
b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())]
c: []
d: [('moveTo', ((0, 0),)), ('lineTo', ((-3, 3),)), ('curveTo', ((-2, 2), (-1, 1), (0, 0))), ('closePath', ())]
"""

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


Expand Down Expand Up @@ -174,6 +201,96 @@ def replay(self, pointPen):
drawPoints = replay


class DecomposingRecordingPointPen(DecomposingPointPen, RecordingPointPen):
"""Same as RecordingPointPen, except that it doesn't keep components
as references, but draws them decomposed as regular contours.
The constructor takes a required 'glyphSet' positional argument,
a dictionary of pointPen-drawable glyph objects (i.e. with a 'drawPoints' method)
keyed by thir name; other arguments are forwarded to the DecomposingPointPen's
constructor::
>>> from pprint import pprint
>>> class SimpleGlyph(object):
... def drawPoints(self, pen):
... pen.beginPath()
... pen.addPoint((0, 0), "line")
... pen.addPoint((1, 1))
... pen.addPoint((2, 2))
... pen.addPoint((3, 3), "curve")
... pen.endPath()
>>> class CompositeGlyph(object):
... def drawPoints(self, pen):
... pen.addComponent('a', (1, 0, 0, 1, -1, 1))
>>> class MissingComponent(object):
... def drawPoints(self, pen):
... pen.addComponent('foobar', (1, 0, 0, 1, 0, 0))
>>> class FlippedComponent(object):
... def drawPoints(self, pen):
... pen.addComponent('a', (-1, 0, 0, 1, 0, 0))
>>> glyphSet = {
... 'a': SimpleGlyph(),
... 'b': CompositeGlyph(),
... 'c': MissingComponent(),
... 'd': FlippedComponent(),
... }
>>> for name, glyph in sorted(glyphSet.items()):
... pen = DecomposingRecordingPointPen(glyphSet)
... try:
... glyph.drawPoints(pen)
... except pen.MissingComponentError:
... pass
... pprint({name: pen.value})
{'a': [('beginPath', (), {}),
('addPoint', ((0, 0), 'line', False, None), {}),
('addPoint', ((1, 1), None, False, None), {}),
('addPoint', ((2, 2), None, False, None), {}),
('addPoint', ((3, 3), 'curve', False, None), {}),
('endPath', (), {})]}
{'b': [('beginPath', (), {}),
('addPoint', ((-1, 1), 'line', False, None), {}),
('addPoint', ((0, 2), None, False, None), {}),
('addPoint', ((1, 3), None, False, None), {}),
('addPoint', ((2, 4), 'curve', False, None), {}),
('endPath', (), {})]}
{'c': []}
{'d': [('beginPath', (), {}),
('addPoint', ((0, 0), 'line', False, None), {}),
('addPoint', ((-1, 1), None, False, None), {}),
('addPoint', ((-2, 2), None, False, None), {}),
('addPoint', ((-3, 3), 'curve', False, None), {}),
('endPath', (), {})]}
>>> for name, glyph in sorted(glyphSet.items()):
... pen = DecomposingRecordingPointPen(
... glyphSet, skipMissingComponents=True, reverseFlipped=True,
... )
... glyph.drawPoints(pen)
... pprint({name: pen.value})
{'a': [('beginPath', (), {}),
('addPoint', ((0, 0), 'line', False, None), {}),
('addPoint', ((1, 1), None, False, None), {}),
('addPoint', ((2, 2), None, False, None), {}),
('addPoint', ((3, 3), 'curve', False, None), {}),
('endPath', (), {})]}
{'b': [('beginPath', (), {}),
('addPoint', ((-1, 1), 'line', False, None), {}),
('addPoint', ((0, 2), None, False, None), {}),
('addPoint', ((1, 3), None, False, None), {}),
('addPoint', ((2, 4), 'curve', False, None), {}),
('endPath', (), {})]}
{'c': []}
{'d': [('beginPath', (), {}),
('addPoint', ((0, 0), 'curve', False, None), {}),
('addPoint', ((-3, 3), 'line', False, None), {}),
('addPoint', ((-2, 2), None, False, None), {}),
('addPoint', ((-1, 1), None, False, None), {}),
('endPath', (), {})]}
"""

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


def lerpRecordings(recording1, recording2, factor=0.5):
"""Linearly interpolate between two recordings. The recordings
must be decomposed, i.e. they must not contain any components.
Expand Down

0 comments on commit cccc358

Please sign in to comment.