aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/pens/ttGlyphPen.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/pens/ttGlyphPen.py')
-rw-r--r--Lib/fontTools/pens/ttGlyphPen.py283
1 files changed, 197 insertions, 86 deletions
diff --git a/Lib/fontTools/pens/ttGlyphPen.py b/Lib/fontTools/pens/ttGlyphPen.py
index e7841efc..5087e158 100644
--- a/Lib/fontTools/pens/ttGlyphPen.py
+++ b/Lib/fontTools/pens/ttGlyphPen.py
@@ -1,30 +1,31 @@
from array import array
-from fontTools.misc.fixedTools import MAX_F2DOT14, otRound, floatToFixedToFloat
+from typing import Any, Dict, Optional, Tuple
+from fontTools.misc.fixedTools import MAX_F2DOT14, floatToFixedToFloat
+from fontTools.misc.loggingTools import LogMixin
+from fontTools.pens.pointPen import AbstractPointPen
from fontTools.misc.roundTools import otRound
-from fontTools.pens.basePen import LoggingPen
-from fontTools.pens.transformPen import TransformPen
+from fontTools.pens.basePen import LoggingPen, PenError
+from fontTools.pens.transformPen import TransformPen, TransformPointPen
from fontTools.ttLib.tables import ttProgram
from fontTools.ttLib.tables._g_l_y_f import Glyph
from fontTools.ttLib.tables._g_l_y_f import GlyphComponent
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
-__all__ = ["TTGlyphPen"]
+__all__ = ["TTGlyphPen", "TTGlyphPointPen"]
-class TTGlyphPen(LoggingPen):
- """Pen used for drawing to a TrueType glyph.
-
- This pen can be used to construct or modify glyphs in a TrueType format
- font. After using the pen to draw, use the ``.glyph()`` method to retrieve
- a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
- """
-
- def __init__(self, glyphSet, handleOverflowingTransforms=True):
- """Construct a new pen.
+class _TTGlyphBasePen:
+ def __init__(
+ self,
+ glyphSet: Optional[Dict[str, Any]],
+ handleOverflowingTransforms: bool = True,
+ ) -> None:
+ """
+ Construct a new pen.
Args:
- glyphSet (ttLib._TTGlyphSet): A glyphset object, used to resolve components.
+ glyphSet (Dict[str, Any]): A glyphset object, used to resolve components.
handleOverflowingTransforms (bool): See below.
If ``handleOverflowingTransforms`` is True, the components' transform values
@@ -42,41 +43,152 @@ class TTGlyphPen(LoggingPen):
If False, no check is done and all components are translated unmodified
into the glyf table, followed by an inevitable ``struct.error`` once an
attempt is made to compile them.
+
+ If both contours and components are present in a glyph, the components
+ are decomposed.
"""
self.glyphSet = glyphSet
self.handleOverflowingTransforms = handleOverflowingTransforms
self.init()
- def init(self):
+ def _decompose(
+ self,
+ glyphName: str,
+ transformation: Tuple[float, float, float, float, float, float],
+ ):
+ tpen = self.transformPen(self, transformation)
+ getattr(self.glyphSet[glyphName], self.drawMethod)(tpen)
+
+ def _isClosed(self):
+ """
+ Check if the current path is closed.
+ """
+ raise NotImplementedError
+
+ def init(self) -> None:
self.points = []
self.endPts = []
self.types = []
self.components = []
- def _addPoint(self, pt, onCurve):
+ def addComponent(
+ self,
+ baseGlyphName: str,
+ transformation: Tuple[float, float, float, float, float, float],
+ identifier: Optional[str] = None,
+ **kwargs: Any,
+ ) -> None:
+ """
+ Add a sub glyph.
+ """
+ self.components.append((baseGlyphName, transformation))
+
+ def _buildComponents(self, componentFlags):
+ if self.handleOverflowingTransforms:
+ # we can't encode transform values > 2 or < -2 in F2Dot14,
+ # so we must decompose the glyph if any transform exceeds these
+ overflowing = any(
+ s > 2 or s < -2
+ for (glyphName, transformation) in self.components
+ for s in transformation[:4]
+ )
+ components = []
+ for glyphName, transformation in self.components:
+ if glyphName not in self.glyphSet:
+ self.log.warning(f"skipped non-existing component '{glyphName}'")
+ continue
+ if self.points or (self.handleOverflowingTransforms and overflowing):
+ # can't have both coordinates and components, so decompose
+ self._decompose(glyphName, transformation)
+ continue
+
+ component = GlyphComponent()
+ component.glyphName = glyphName
+ component.x, component.y = (otRound(v) for v in transformation[4:])
+ # quantize floats to F2Dot14 so we get same values as when decompiled
+ # from a binary glyf table
+ transformation = tuple(
+ floatToFixedToFloat(v, 14) for v in transformation[:4]
+ )
+ if transformation != (1, 0, 0, 1):
+ if self.handleOverflowingTransforms and any(
+ MAX_F2DOT14 < s <= 2 for s in transformation
+ ):
+ # clamp values ~= +2.0 so we can keep the component
+ transformation = tuple(
+ MAX_F2DOT14 if MAX_F2DOT14 < s <= 2 else s
+ for s in transformation
+ )
+ component.transform = (transformation[:2], transformation[2:])
+ component.flags = componentFlags
+ components.append(component)
+ return components
+
+ def glyph(self, componentFlags: int = 0x4) -> Glyph:
+ """
+ Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
+ """
+ if not self._isClosed():
+ raise PenError("Didn't close last contour.")
+ components = self._buildComponents(componentFlags)
+
+ glyph = Glyph()
+ glyph.coordinates = GlyphCoordinates(self.points)
+ glyph.coordinates.toInt()
+ glyph.endPtsOfContours = self.endPts
+ glyph.flags = array("B", self.types)
+ self.init()
+
+ if components:
+ # If both components and contours were present, they have by now
+ # been decomposed by _buildComponents.
+ glyph.components = components
+ glyph.numberOfContours = -1
+ else:
+ glyph.numberOfContours = len(glyph.endPtsOfContours)
+ glyph.program = ttProgram.Program()
+ glyph.program.fromBytecode(b"")
+
+ return glyph
+
+
+class TTGlyphPen(_TTGlyphBasePen, LoggingPen):
+ """
+ Pen used for drawing to a TrueType glyph.
+
+ This pen can be used to construct or modify glyphs in a TrueType format
+ font. After using the pen to draw, use the ``.glyph()`` method to retrieve
+ a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
+ """
+
+ drawMethod = "draw"
+ transformPen = TransformPen
+
+ def _addPoint(self, pt: Tuple[float, float], onCurve: int) -> None:
self.points.append(pt)
self.types.append(onCurve)
- def _popPoint(self):
+ def _popPoint(self) -> None:
self.points.pop()
self.types.pop()
- def _isClosed(self):
- return (
- (not self.points) or
- (self.endPts and self.endPts[-1] == len(self.points) - 1))
+ def _isClosed(self) -> bool:
+ return (not self.points) or (
+ self.endPts and self.endPts[-1] == len(self.points) - 1
+ )
- def lineTo(self, pt):
+ def lineTo(self, pt: Tuple[float, float]) -> None:
self._addPoint(pt, 1)
- def moveTo(self, pt):
- assert self._isClosed(), '"move"-type point must begin a new contour.'
+ def moveTo(self, pt: Tuple[float, float]) -> None:
+ if not self._isClosed():
+ raise PenError('"move"-type point must begin a new contour.')
self._addPoint(pt, 1)
- def curveTo(self, *points):
+ def curveTo(self, *points) -> None:
raise NotImplementedError
- def qCurveTo(self, *points):
+ def qCurveTo(self, *points) -> None:
assert len(points) >= 1
for pt in points[:-1]:
self._addPoint(pt, 0)
@@ -85,7 +197,7 @@ class TTGlyphPen(LoggingPen):
if points[-1] is not None:
self._addPoint(points[-1], 1)
- def closePath(self):
+ def closePath(self) -> None:
endPt = len(self.points) - 1
# ignore anchors (one-point paths)
@@ -103,72 +215,71 @@ class TTGlyphPen(LoggingPen):
self.endPts.append(endPt)
- def endPath(self):
+ def endPath(self) -> None:
# TrueType contours are always "closed"
self.closePath()
- def addComponent(self, glyphName, transformation):
- self.components.append((glyphName, transformation))
- def _buildComponents(self, componentFlags):
- if self.handleOverflowingTransforms:
- # we can't encode transform values > 2 or < -2 in F2Dot14,
- # so we must decompose the glyph if any transform exceeds these
- overflowing = any(s > 2 or s < -2
- for (glyphName, transformation) in self.components
- for s in transformation[:4])
- components = []
- for glyphName, transformation in self.components:
- if glyphName not in self.glyphSet:
- self.log.warning(
- "skipped non-existing component '%s'", glyphName
- )
- continue
- if (self.points or
- (self.handleOverflowingTransforms and overflowing)):
- # can't have both coordinates and components, so decompose
- tpen = TransformPen(self, transformation)
- self.glyphSet[glyphName].draw(tpen)
- continue
+class TTGlyphPointPen(_TTGlyphBasePen, LogMixin, AbstractPointPen):
+ """
+ Point pen used for drawing to a TrueType glyph.
- component = GlyphComponent()
- component.glyphName = glyphName
- component.x, component.y = (otRound(v) for v in transformation[4:])
- # quantize floats to F2Dot14 so we get same values as when decompiled
- # from a binary glyf table
- transformation = tuple(
- floatToFixedToFloat(v, 14) for v in transformation[:4]
- )
- if transformation != (1, 0, 0, 1):
- if (self.handleOverflowingTransforms and
- any(MAX_F2DOT14 < s <= 2 for s in transformation)):
- # clamp values ~= +2.0 so we can keep the component
- transformation = tuple(MAX_F2DOT14 if MAX_F2DOT14 < s <= 2
- else s for s in transformation)
- component.transform = (transformation[:2], transformation[2:])
- component.flags = componentFlags
- components.append(component)
- return components
+ This pen can be used to construct or modify glyphs in a TrueType format
+ font. After using the pen to draw, use the ``.glyph()`` method to retrieve
+ a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
+ """
- def glyph(self, componentFlags=0x4):
- """Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph."""
- assert self._isClosed(), "Didn't close last contour."
+ drawMethod = "drawPoints"
+ transformPen = TransformPointPen
- components = self._buildComponents(componentFlags)
+ def init(self) -> None:
+ super().init()
+ self._currentContourStartIndex = None
- glyph = Glyph()
- glyph.coordinates = GlyphCoordinates(self.points)
- glyph.coordinates.toInt()
- glyph.endPtsOfContours = self.endPts
- glyph.flags = array("B", self.types)
- self.init()
+ def _isClosed(self) -> bool:
+ return self._currentContourStartIndex is None
- if components:
- glyph.components = components
- glyph.numberOfContours = -1
+ def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None:
+ """
+ Start a new sub path.
+ """
+ if not self._isClosed():
+ raise PenError("Didn't close previous contour.")
+ self._currentContourStartIndex = len(self.points)
+
+ def endPath(self) -> None:
+ """
+ End the current sub path.
+ """
+ # TrueType contours are always "closed"
+ if self._isClosed():
+ raise PenError("Contour is already closed.")
+ if self._currentContourStartIndex == len(self.points):
+ raise PenError("Tried to end an empty contour.")
+ self.endPts.append(len(self.points) - 1)
+ self._currentContourStartIndex = None
+
+ def addPoint(
+ self,
+ pt: Tuple[float, float],
+ segmentType: Optional[str] = None,
+ smooth: bool = False,
+ name: Optional[str] = None,
+ identifier: Optional[str] = None,
+ **kwargs: Any,
+ ) -> None:
+ """
+ Add a point to the current sub path.
+ """
+ if self._isClosed():
+ raise PenError("Can't add a point to a closed contour.")
+ if segmentType is None:
+ self.types.append(0) # offcurve
+ elif segmentType in ("qcurve", "line", "move"):
+ self.types.append(1) # oncurve
+ elif segmentType == "curve":
+ raise NotImplementedError("cubic curves are not supported")
else:
- glyph.numberOfContours = len(glyph.endPtsOfContours)
- glyph.program = ttProgram.Program()
- glyph.program.fromBytecode(b"")
+ raise AssertionError(segmentType)
- return glyph
+ self.points.append(pt)