fonttools/Lib/fontTools/pens/ttGlyphPen.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

336 lines
12 KiB
Python
Raw Normal View History

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.
"""
2021-05-20 14:03:56 +01:00
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,
2021-05-20 12:30:08 +01:00
**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:
2021-05-20 12:30:08 +01:00
self.log.warning(f"skipped non-existing component '{glyphName}'")
continue
2021-05-20 12:30:08 +01:00
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 = 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:
# 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"")
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.
"""
2021-05-20 12:30:08 +01:00
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)
# last point is None if there are no on-curve points
if points[-1] is not None:
self._addPoint(points[-1], 1)
2021-02-21 10:20:59 +00:00
def qCurveTo(self, *points) -> None:
assert len(points) >= 1
for pt in points[:-1]:
self._addPoint(pt, 0)
# last point is None if there are no on-curve points
if points[-1] is not None:
self._addPoint(points[-1], 1)
def closePath(self) -> None:
endPt = len(self.points) - 1
# ignore anchors (one-point paths)
if endPt == 0 or (self.endPts and endPt == self.endPts[-1] + 1):
self._popPoint()
return
if not self.outputImpliedClosingLine:
# if first and last point on this path are the same, remove last
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:
# TrueType contours are always "closed"
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.
"""
2021-05-20 12:30:08 +01:00
drawMethod = "drawPoints"
transformPen = TransformPointPen
def init(self) -> None:
super().init()
self._currentContourStartIndex = None
def _isClosed(self) -> bool:
return self._currentContourStartIndex is None
2021-05-20 12:30:08 +01:00
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):
# ignore empty contours
self._currentContourStartIndex = None
return
contourStart = self.endPts[-1] + 1 if self.endPts else 0
self.endPts.append(len(self.points) - 1)
self._currentContourStartIndex = None
# Resolve types for any cubic segments
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,
2021-05-20 12:30:08 +01:00
**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)