|
from array import array |
|
from typing import Any, Callable, 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, PenError |
|
from fontTools.pens.transformPen import TransformPen, TransformPointPen |
|
from fontTools.ttLib.tables import ttProgram |
|
from fontTools.ttLib.tables._g_l_y_f import flagOnCurve, flagCubic |
|
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 |
|
from fontTools.ttLib.tables._g_l_y_f import dropImpliedOnCurvePoints |
|
import math |
|
|
|
|
|
__all__ = ["TTGlyphPen", "TTGlyphPointPen"] |
|
|
|
|
|
class _TTGlyphBasePen: |
|
def __init__( |
|
self, |
|
glyphSet: Optional[Dict[str, Any]], |
|
handleOverflowingTransforms: bool = True, |
|
) -> None: |
|
""" |
|
Construct a new pen. |
|
|
|
Args: |
|
glyphSet (Dict[str, Any]): A glyphset object, used to resolve components. |
|
handleOverflowingTransforms (bool): See below. |
|
|
|
If ``handleOverflowingTransforms`` is True, the components' transform values |
|
are checked that they don't overflow the limits of a F2Dot14 number: |
|
-2.0 <= v < +2.0. If any transform value exceeds these, the composite |
|
glyph is decomposed. |
|
|
|
An exception to this rule is done for values that are very close to +2.0 |
|
(both for consistency with the -2.0 case, and for the relative frequency |
|
these occur in real fonts). When almost +2.0 values occur (and all other |
|
values are within the range -2.0 <= x <= +2.0), they are clamped to the |
|
maximum positive value that can still be encoded as an F2Dot14: i.e. |
|
1.99993896484375. |
|
|
|
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 _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 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: |
|
|
|
|
|
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): |
|
|
|
self._decompose(glyphName, transformation) |
|
continue |
|
|
|
component = GlyphComponent() |
|
component.glyphName = glyphName |
|
component.x, component.y = (otRound(v) for v in transformation[4:]) |
|
|
|
|
|
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 |
|
): |
|
|
|
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 = 0x04, |
|
dropImpliedOnCurves: bool = False, |
|
*, |
|
round: Callable[[float], int] = otRound, |
|
) -> Glyph: |
|
""" |
|
Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph. |
|
|
|
Args: |
|
componentFlags: Flags to use for component glyphs. (default: 0x04) |
|
|
|
dropImpliedOnCurves: Whether to remove implied-oncurve points. (default: False) |
|
""" |
|
if not self._isClosed(): |
|
raise PenError("Didn't close last contour.") |
|
components = self._buildComponents(componentFlags) |
|
|
|
glyph = Glyph() |
|
glyph.coordinates = GlyphCoordinates(self.points) |
|
glyph.endPtsOfContours = self.endPts |
|
glyph.flags = array("B", self.types) |
|
self.init() |
|
|
|
if components: |
|
|
|
|
|
glyph.components = components |
|
glyph.numberOfContours = -1 |
|
else: |
|
glyph.numberOfContours = len(glyph.endPtsOfContours) |
|
glyph.program = ttProgram.Program() |
|
glyph.program.fromBytecode(b"") |
|
if dropImpliedOnCurves: |
|
dropImpliedOnCurvePoints(glyph) |
|
glyph.coordinates.toInt(round=round) |
|
|
|
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 __init__( |
|
self, |
|
glyphSet: Optional[Dict[str, Any]] = None, |
|
handleOverflowingTransforms: bool = True, |
|
outputImpliedClosingLine: bool = False, |
|
) -> None: |
|
super().__init__(glyphSet, handleOverflowingTransforms) |
|
self.outputImpliedClosingLine = outputImpliedClosingLine |
|
|
|
def _addPoint(self, pt: Tuple[float, float], tp: int) -> None: |
|
self.points.append(pt) |
|
self.types.append(tp) |
|
|
|
def _popPoint(self) -> None: |
|
self.points.pop() |
|
self.types.pop() |
|
|
|
def _isClosed(self) -> bool: |
|
return (not self.points) or ( |
|
self.endPts and self.endPts[-1] == len(self.points) - 1 |
|
) |
|
|
|
def lineTo(self, pt: Tuple[float, float]) -> None: |
|
self._addPoint(pt, flagOnCurve) |
|
|
|
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, flagOnCurve) |
|
|
|
def curveTo(self, *points) -> None: |
|
assert len(points) % 2 == 1 |
|
for pt in points[:-1]: |
|
self._addPoint(pt, flagCubic) |
|
|
|
|
|
if points[-1] is not None: |
|
self._addPoint(points[-1], 1) |
|
|
|
def qCurveTo(self, *points) -> None: |
|
assert len(points) >= 1 |
|
for pt in points[:-1]: |
|
self._addPoint(pt, 0) |
|
|
|
|
|
if points[-1] is not None: |
|
self._addPoint(points[-1], 1) |
|
|
|
def closePath(self) -> None: |
|
endPt = len(self.points) - 1 |
|
|
|
|
|
if endPt == 0 or (self.endPts and endPt == self.endPts[-1] + 1): |
|
self._popPoint() |
|
return |
|
|
|
if not self.outputImpliedClosingLine: |
|
|
|
startPt = 0 |
|
if self.endPts: |
|
startPt = self.endPts[-1] + 1 |
|
if self.points[startPt] == self.points[endPt]: |
|
self._popPoint() |
|
endPt -= 1 |
|
|
|
self.endPts.append(endPt) |
|
|
|
def endPath(self) -> None: |
|
|
|
self.closePath() |
|
|
|
|
|
class TTGlyphPointPen(_TTGlyphBasePen, LogMixin, AbstractPointPen): |
|
""" |
|
Point 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 = "drawPoints" |
|
transformPen = TransformPointPen |
|
|
|
def init(self) -> None: |
|
super().init() |
|
self._currentContourStartIndex = None |
|
|
|
def _isClosed(self) -> bool: |
|
return self._currentContourStartIndex is None |
|
|
|
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. |
|
""" |
|
|
|
if self._isClosed(): |
|
raise PenError("Contour is already closed.") |
|
if self._currentContourStartIndex == len(self.points): |
|
|
|
self._currentContourStartIndex = None |
|
return |
|
|
|
contourStart = self.endPts[-1] + 1 if self.endPts else 0 |
|
self.endPts.append(len(self.points) - 1) |
|
self._currentContourStartIndex = None |
|
|
|
|
|
flags = self.types |
|
for i in range(contourStart, len(flags)): |
|
if flags[i] == "curve": |
|
j = i - 1 |
|
if j < contourStart: |
|
j = len(flags) - 1 |
|
while flags[j] == 0: |
|
flags[j] = flagCubic |
|
j -= 1 |
|
flags[i] = flagOnCurve |
|
|
|
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) |
|
elif segmentType in ("line", "move"): |
|
self.types.append(flagOnCurve) |
|
elif segmentType == "qcurve": |
|
self.types.append(flagOnCurve) |
|
elif segmentType == "curve": |
|
self.types.append("curve") |
|
else: |
|
raise AssertionError(segmentType) |
|
|
|
self.points.append(pt) |
|
|