123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- 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:
- # 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 = 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.
- """
- 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)
- 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.
- """
- 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.
- """
- # 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,
- **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)
|