up follow livre
This commit is contained in:
parent
b4b4398bb0
commit
3a7a3849ae
12242 changed files with 2564461 additions and 6914 deletions
|
|
@ -0,0 +1 @@
|
|||
"""Empty __init__.py file to signal Python this directory is a package."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
52
venv/lib/python3.13/site-packages/fontTools/pens/areaPen.py
Normal file
52
venv/lib/python3.13/site-packages/fontTools/pens/areaPen.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
"""Calculate the area of a glyph."""
|
||||
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
__all__ = ["AreaPen"]
|
||||
|
||||
|
||||
class AreaPen(BasePen):
|
||||
def __init__(self, glyphset=None):
|
||||
BasePen.__init__(self, glyphset)
|
||||
self.value = 0
|
||||
|
||||
def _moveTo(self, p0):
|
||||
self._p0 = self._startPoint = p0
|
||||
|
||||
def _lineTo(self, p1):
|
||||
x0, y0 = self._p0
|
||||
x1, y1 = p1
|
||||
self.value -= (x1 - x0) * (y1 + y0) * 0.5
|
||||
self._p0 = p1
|
||||
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
# https://github.com/Pomax/bezierinfo/issues/44
|
||||
p0 = self._p0
|
||||
x0, y0 = p0[0], p0[1]
|
||||
x1, y1 = p1[0] - x0, p1[1] - y0
|
||||
x2, y2 = p2[0] - x0, p2[1] - y0
|
||||
self.value -= (x2 * y1 - x1 * y2) / 3
|
||||
self._lineTo(p2)
|
||||
self._p0 = p2
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
# https://github.com/Pomax/bezierinfo/issues/44
|
||||
p0 = self._p0
|
||||
x0, y0 = p0[0], p0[1]
|
||||
x1, y1 = p1[0] - x0, p1[1] - y0
|
||||
x2, y2 = p2[0] - x0, p2[1] - y0
|
||||
x3, y3 = p3[0] - x0, p3[1] - y0
|
||||
self.value -= (x1 * (-y2 - y3) + x2 * (y1 - 2 * y3) + x3 * (y1 + 2 * y2)) * 0.15
|
||||
self._lineTo(p3)
|
||||
self._p0 = p3
|
||||
|
||||
def _closePath(self):
|
||||
self._lineTo(self._startPoint)
|
||||
del self._p0, self._startPoint
|
||||
|
||||
def _endPath(self):
|
||||
if self._p0 != self._startPoint:
|
||||
# Area is not defined for open contours.
|
||||
raise NotImplementedError
|
||||
del self._p0, self._startPoint
|
||||
475
venv/lib/python3.13/site-packages/fontTools/pens/basePen.py
Normal file
475
venv/lib/python3.13/site-packages/fontTools/pens/basePen.py
Normal file
|
|
@ -0,0 +1,475 @@
|
|||
"""fontTools.pens.basePen.py -- Tools and base classes to build pen objects.
|
||||
|
||||
The Pen Protocol
|
||||
|
||||
A Pen is a kind of object that standardizes the way how to "draw" outlines:
|
||||
it is a middle man between an outline and a drawing. In other words:
|
||||
it is an abstraction for drawing outlines, making sure that outline objects
|
||||
don't need to know the details about how and where they're being drawn, and
|
||||
that drawings don't need to know the details of how outlines are stored.
|
||||
|
||||
The most basic pattern is this::
|
||||
|
||||
outline.draw(pen) # 'outline' draws itself onto 'pen'
|
||||
|
||||
Pens can be used to render outlines to the screen, but also to construct
|
||||
new outlines. Eg. an outline object can be both a drawable object (it has a
|
||||
draw() method) as well as a pen itself: you *build* an outline using pen
|
||||
methods.
|
||||
|
||||
The AbstractPen class defines the Pen protocol. It implements almost
|
||||
nothing (only no-op closePath() and endPath() methods), but is useful
|
||||
for documentation purposes. Subclassing it basically tells the reader:
|
||||
"this class implements the Pen protocol.". An examples of an AbstractPen
|
||||
subclass is :py:class:`fontTools.pens.transformPen.TransformPen`.
|
||||
|
||||
The BasePen class is a base implementation useful for pens that actually
|
||||
draw (for example a pen renders outlines using a native graphics engine).
|
||||
BasePen contains a lot of base functionality, making it very easy to build
|
||||
a pen that fully conforms to the pen protocol. Note that if you subclass
|
||||
BasePen, you *don't* override moveTo(), lineTo(), etc., but _moveTo(),
|
||||
_lineTo(), etc. See the BasePen doc string for details. Examples of
|
||||
BasePen subclasses are fontTools.pens.boundsPen.BoundsPen and
|
||||
fontTools.pens.cocoaPen.CocoaPen.
|
||||
|
||||
Coordinates are usually expressed as (x, y) tuples, but generally any
|
||||
sequence of length 2 will do.
|
||||
"""
|
||||
|
||||
from typing import Tuple, Dict
|
||||
|
||||
from fontTools.misc.loggingTools import LogMixin
|
||||
from fontTools.misc.transform import DecomposedTransform, Identity
|
||||
|
||||
__all__ = [
|
||||
"AbstractPen",
|
||||
"NullPen",
|
||||
"BasePen",
|
||||
"PenError",
|
||||
"decomposeSuperBezierSegment",
|
||||
"decomposeQuadraticSegment",
|
||||
]
|
||||
|
||||
|
||||
class PenError(Exception):
|
||||
"""Represents an error during penning."""
|
||||
|
||||
|
||||
class OpenContourError(PenError):
|
||||
pass
|
||||
|
||||
|
||||
class AbstractPen:
|
||||
def moveTo(self, pt: Tuple[float, float]) -> None:
|
||||
"""Begin a new sub path, set the current point to 'pt'. You must
|
||||
end each sub path with a call to pen.closePath() or pen.endPath().
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def lineTo(self, pt: Tuple[float, float]) -> None:
|
||||
"""Draw a straight line from the current point to 'pt'."""
|
||||
raise NotImplementedError
|
||||
|
||||
def curveTo(self, *points: Tuple[float, float]) -> None:
|
||||
"""Draw a cubic bezier with an arbitrary number of control points.
|
||||
|
||||
The last point specified is on-curve, all others are off-curve
|
||||
(control) points. If the number of control points is > 2, the
|
||||
segment is split into multiple bezier segments. This works
|
||||
like this:
|
||||
|
||||
Let n be the number of control points (which is the number of
|
||||
arguments to this call minus 1). If n==2, a plain vanilla cubic
|
||||
bezier is drawn. If n==1, we fall back to a quadratic segment and
|
||||
if n==0 we draw a straight line. It gets interesting when n>2:
|
||||
n-1 PostScript-style cubic segments will be drawn as if it were
|
||||
one curve. See decomposeSuperBezierSegment().
|
||||
|
||||
The conversion algorithm used for n>2 is inspired by NURB
|
||||
splines, and is conceptually equivalent to the TrueType "implied
|
||||
points" principle. See also decomposeQuadraticSegment().
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def qCurveTo(self, *points: Tuple[float, float]) -> None:
|
||||
"""Draw a whole string of quadratic curve segments.
|
||||
|
||||
The last point specified is on-curve, all others are off-curve
|
||||
points.
|
||||
|
||||
This method implements TrueType-style curves, breaking up curves
|
||||
using 'implied points': between each two consequtive off-curve points,
|
||||
there is one implied point exactly in the middle between them. See
|
||||
also decomposeQuadraticSegment().
|
||||
|
||||
The last argument (normally the on-curve point) may be None.
|
||||
This is to support contours that have NO on-curve points (a rarely
|
||||
seen feature of TrueType outlines).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def closePath(self) -> None:
|
||||
"""Close the current sub path. You must call either pen.closePath()
|
||||
or pen.endPath() after each sub path.
|
||||
"""
|
||||
pass
|
||||
|
||||
def endPath(self) -> None:
|
||||
"""End the current sub path, but don't close it. You must call
|
||||
either pen.closePath() or pen.endPath() after each sub path.
|
||||
"""
|
||||
pass
|
||||
|
||||
def addComponent(
|
||||
self,
|
||||
glyphName: str,
|
||||
transformation: Tuple[float, float, float, float, float, float],
|
||||
) -> None:
|
||||
"""Add a sub glyph. The 'transformation' argument must be a 6-tuple
|
||||
containing an affine transformation, or a Transform object from the
|
||||
fontTools.misc.transform module. More precisely: it should be a
|
||||
sequence containing 6 numbers.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def addVarComponent(
|
||||
self,
|
||||
glyphName: str,
|
||||
transformation: DecomposedTransform,
|
||||
location: Dict[str, float],
|
||||
) -> None:
|
||||
"""Add a VarComponent sub glyph. The 'transformation' argument
|
||||
must be a DecomposedTransform from the fontTools.misc.transform module,
|
||||
and the 'location' argument must be a dictionary mapping axis tags
|
||||
to their locations.
|
||||
"""
|
||||
# GlyphSet decomposes for us
|
||||
raise AttributeError
|
||||
|
||||
|
||||
class NullPen(AbstractPen):
|
||||
"""A pen that does nothing."""
|
||||
|
||||
def moveTo(self, pt):
|
||||
pass
|
||||
|
||||
def lineTo(self, pt):
|
||||
pass
|
||||
|
||||
def curveTo(self, *points):
|
||||
pass
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
pass
|
||||
|
||||
def closePath(self):
|
||||
pass
|
||||
|
||||
def endPath(self):
|
||||
pass
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
pass
|
||||
|
||||
def addVarComponent(self, glyphName, transformation, location):
|
||||
pass
|
||||
|
||||
|
||||
class LoggingPen(LogMixin, AbstractPen):
|
||||
"""A pen with a ``log`` property (see fontTools.misc.loggingTools.LogMixin)"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MissingComponentError(KeyError):
|
||||
"""Indicates a component pointing to a non-existent glyph in the glyphset."""
|
||||
|
||||
|
||||
class DecomposingPen(LoggingPen):
|
||||
"""Implements a 'addComponent' method that decomposes components
|
||||
(i.e. draws them onto self as simple contours).
|
||||
It can also be used as a mixin class (e.g. see ContourRecordingPen).
|
||||
|
||||
You must override moveTo, lineTo, curveTo and qCurveTo. You may
|
||||
additionally override closePath, endPath and addComponent.
|
||||
|
||||
By default a warning message is logged when a base glyph is missing;
|
||||
set the class variable ``skipMissingComponents`` to False if you want
|
||||
all instances of a sub-class to raise a :class:`MissingComponentError`
|
||||
exception by default.
|
||||
"""
|
||||
|
||||
skipMissingComponents = True
|
||||
# alias error for convenience
|
||||
MissingComponentError = MissingComponentError
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
glyphSet,
|
||||
*args,
|
||||
skipMissingComponents=None,
|
||||
reverseFlipped=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Takes a 'glyphSet' argument (dict), in which the glyphs that are referenced
|
||||
as components are looked up by their name.
|
||||
|
||||
If the optional 'reverseFlipped' argument is True, components whose transformation
|
||||
matrix has a negative determinant will be decomposed with a reversed path direction
|
||||
to compensate for the flip.
|
||||
|
||||
The optional 'skipMissingComponents' argument can be set to True/False to
|
||||
override the homonymous class attribute for a given pen instance.
|
||||
"""
|
||||
super(DecomposingPen, self).__init__(*args, **kwargs)
|
||||
self.glyphSet = glyphSet
|
||||
self.skipMissingComponents = (
|
||||
self.__class__.skipMissingComponents
|
||||
if skipMissingComponents is None
|
||||
else skipMissingComponents
|
||||
)
|
||||
self.reverseFlipped = reverseFlipped
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
"""Transform the points of the base glyph and draw it onto self."""
|
||||
from fontTools.pens.transformPen import TransformPen
|
||||
|
||||
try:
|
||||
glyph = self.glyphSet[glyphName]
|
||||
except KeyError:
|
||||
if not self.skipMissingComponents:
|
||||
raise MissingComponentError(glyphName)
|
||||
self.log.warning("glyph '%s' is missing from glyphSet; skipped" % glyphName)
|
||||
else:
|
||||
pen = self
|
||||
if transformation != Identity:
|
||||
pen = TransformPen(pen, transformation)
|
||||
if self.reverseFlipped:
|
||||
# if the transformation has a negative determinant, it will
|
||||
# reverse the contour direction of the component
|
||||
a, b, c, d = transformation[:4]
|
||||
det = a * d - b * c
|
||||
if det < 0:
|
||||
from fontTools.pens.reverseContourPen import ReverseContourPen
|
||||
|
||||
pen = ReverseContourPen(pen)
|
||||
glyph.draw(pen)
|
||||
|
||||
def addVarComponent(self, glyphName, transformation, location):
|
||||
# GlyphSet decomposes for us
|
||||
raise AttributeError
|
||||
|
||||
|
||||
class BasePen(DecomposingPen):
|
||||
"""Base class for drawing pens. You must override _moveTo, _lineTo and
|
||||
_curveToOne. You may additionally override _closePath, _endPath,
|
||||
addComponent, addVarComponent, and/or _qCurveToOne. You should not
|
||||
override any other methods.
|
||||
"""
|
||||
|
||||
def __init__(self, glyphSet=None):
|
||||
super(BasePen, self).__init__(glyphSet)
|
||||
self.__currentPoint = None
|
||||
|
||||
# must override
|
||||
|
||||
def _moveTo(self, pt):
|
||||
raise NotImplementedError
|
||||
|
||||
def _lineTo(self, pt):
|
||||
raise NotImplementedError
|
||||
|
||||
def _curveToOne(self, pt1, pt2, pt3):
|
||||
raise NotImplementedError
|
||||
|
||||
# may override
|
||||
|
||||
def _closePath(self):
|
||||
pass
|
||||
|
||||
def _endPath(self):
|
||||
pass
|
||||
|
||||
def _qCurveToOne(self, pt1, pt2):
|
||||
"""This method implements the basic quadratic curve type. The
|
||||
default implementation delegates the work to the cubic curve
|
||||
function. Optionally override with a native implementation.
|
||||
"""
|
||||
pt0x, pt0y = self.__currentPoint
|
||||
pt1x, pt1y = pt1
|
||||
pt2x, pt2y = pt2
|
||||
mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x)
|
||||
mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y)
|
||||
mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x)
|
||||
mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y)
|
||||
self._curveToOne((mid1x, mid1y), (mid2x, mid2y), pt2)
|
||||
|
||||
# don't override
|
||||
|
||||
def _getCurrentPoint(self):
|
||||
"""Return the current point. This is not part of the public
|
||||
interface, yet is useful for subclasses.
|
||||
"""
|
||||
return self.__currentPoint
|
||||
|
||||
def closePath(self):
|
||||
self._closePath()
|
||||
self.__currentPoint = None
|
||||
|
||||
def endPath(self):
|
||||
self._endPath()
|
||||
self.__currentPoint = None
|
||||
|
||||
def moveTo(self, pt):
|
||||
self._moveTo(pt)
|
||||
self.__currentPoint = pt
|
||||
|
||||
def lineTo(self, pt):
|
||||
self._lineTo(pt)
|
||||
self.__currentPoint = pt
|
||||
|
||||
def curveTo(self, *points):
|
||||
n = len(points) - 1 # 'n' is the number of control points
|
||||
assert n >= 0
|
||||
if n == 2:
|
||||
# The common case, we have exactly two BCP's, so this is a standard
|
||||
# cubic bezier. Even though decomposeSuperBezierSegment() handles
|
||||
# this case just fine, we special-case it anyway since it's so
|
||||
# common.
|
||||
self._curveToOne(*points)
|
||||
self.__currentPoint = points[-1]
|
||||
elif n > 2:
|
||||
# n is the number of control points; split curve into n-1 cubic
|
||||
# bezier segments. The algorithm used here is inspired by NURB
|
||||
# splines and the TrueType "implied point" principle, and ensures
|
||||
# the smoothest possible connection between two curve segments,
|
||||
# with no disruption in the curvature. It is practical since it
|
||||
# allows one to construct multiple bezier segments with a much
|
||||
# smaller amount of points.
|
||||
_curveToOne = self._curveToOne
|
||||
for pt1, pt2, pt3 in decomposeSuperBezierSegment(points):
|
||||
_curveToOne(pt1, pt2, pt3)
|
||||
self.__currentPoint = pt3
|
||||
elif n == 1:
|
||||
self.qCurveTo(*points)
|
||||
elif n == 0:
|
||||
self.lineTo(points[0])
|
||||
else:
|
||||
raise AssertionError("can't get there from here")
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
n = len(points) - 1 # 'n' is the number of control points
|
||||
assert n >= 0
|
||||
if points[-1] is None:
|
||||
# Special case for TrueType quadratics: it is possible to
|
||||
# define a contour with NO on-curve points. BasePen supports
|
||||
# this by allowing the final argument (the expected on-curve
|
||||
# point) to be None. We simulate the feature by making the implied
|
||||
# on-curve point between the last and the first off-curve points
|
||||
# explicit.
|
||||
x, y = points[-2] # last off-curve point
|
||||
nx, ny = points[0] # first off-curve point
|
||||
impliedStartPoint = (0.5 * (x + nx), 0.5 * (y + ny))
|
||||
self.__currentPoint = impliedStartPoint
|
||||
self._moveTo(impliedStartPoint)
|
||||
points = points[:-1] + (impliedStartPoint,)
|
||||
if n > 0:
|
||||
# Split the string of points into discrete quadratic curve
|
||||
# segments. Between any two consecutive off-curve points
|
||||
# there's an implied on-curve point exactly in the middle.
|
||||
# This is where the segment splits.
|
||||
_qCurveToOne = self._qCurveToOne
|
||||
for pt1, pt2 in decomposeQuadraticSegment(points):
|
||||
_qCurveToOne(pt1, pt2)
|
||||
self.__currentPoint = pt2
|
||||
else:
|
||||
self.lineTo(points[0])
|
||||
|
||||
|
||||
def decomposeSuperBezierSegment(points):
|
||||
"""Split the SuperBezier described by 'points' into a list of regular
|
||||
bezier segments. The 'points' argument must be a sequence with length
|
||||
3 or greater, containing (x, y) coordinates. The last point is the
|
||||
destination on-curve point, the rest of the points are off-curve points.
|
||||
The start point should not be supplied.
|
||||
|
||||
This function returns a list of (pt1, pt2, pt3) tuples, which each
|
||||
specify a regular curveto-style bezier segment.
|
||||
"""
|
||||
n = len(points) - 1
|
||||
assert n > 1
|
||||
bezierSegments = []
|
||||
pt1, pt2, pt3 = points[0], None, None
|
||||
for i in range(2, n + 1):
|
||||
# calculate points in between control points.
|
||||
nDivisions = min(i, 3, n - i + 2)
|
||||
for j in range(1, nDivisions):
|
||||
factor = j / nDivisions
|
||||
temp1 = points[i - 1]
|
||||
temp2 = points[i - 2]
|
||||
temp = (
|
||||
temp2[0] + factor * (temp1[0] - temp2[0]),
|
||||
temp2[1] + factor * (temp1[1] - temp2[1]),
|
||||
)
|
||||
if pt2 is None:
|
||||
pt2 = temp
|
||||
else:
|
||||
pt3 = (0.5 * (pt2[0] + temp[0]), 0.5 * (pt2[1] + temp[1]))
|
||||
bezierSegments.append((pt1, pt2, pt3))
|
||||
pt1, pt2, pt3 = temp, None, None
|
||||
bezierSegments.append((pt1, points[-2], points[-1]))
|
||||
return bezierSegments
|
||||
|
||||
|
||||
def decomposeQuadraticSegment(points):
|
||||
"""Split the quadratic curve segment described by 'points' into a list
|
||||
of "atomic" quadratic segments. The 'points' argument must be a sequence
|
||||
with length 2 or greater, containing (x, y) coordinates. The last point
|
||||
is the destination on-curve point, the rest of the points are off-curve
|
||||
points. The start point should not be supplied.
|
||||
|
||||
This function returns a list of (pt1, pt2) tuples, which each specify a
|
||||
plain quadratic bezier segment.
|
||||
"""
|
||||
n = len(points) - 1
|
||||
assert n > 0
|
||||
quadSegments = []
|
||||
for i in range(n - 1):
|
||||
x, y = points[i]
|
||||
nx, ny = points[i + 1]
|
||||
impliedPt = (0.5 * (x + nx), 0.5 * (y + ny))
|
||||
quadSegments.append((points[i], impliedPt))
|
||||
quadSegments.append((points[-2], points[-1]))
|
||||
return quadSegments
|
||||
|
||||
|
||||
class _TestPen(BasePen):
|
||||
"""Test class that prints PostScript to stdout."""
|
||||
|
||||
def _moveTo(self, pt):
|
||||
print("%s %s moveto" % (pt[0], pt[1]))
|
||||
|
||||
def _lineTo(self, pt):
|
||||
print("%s %s lineto" % (pt[0], pt[1]))
|
||||
|
||||
def _curveToOne(self, bcp1, bcp2, pt):
|
||||
print(
|
||||
"%s %s %s %s %s %s curveto"
|
||||
% (bcp1[0], bcp1[1], bcp2[0], bcp2[1], pt[0], pt[1])
|
||||
)
|
||||
|
||||
def _closePath(self):
|
||||
print("closepath")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pen = _TestPen(None)
|
||||
pen.moveTo((0, 0))
|
||||
pen.lineTo((0, 100))
|
||||
pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0))
|
||||
pen.closePath()
|
||||
|
||||
pen = _TestPen(None)
|
||||
# testing the "no on-curve point" scenario
|
||||
pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None)
|
||||
pen.closePath()
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
from fontTools.misc.arrayTools import updateBounds, pointInRect, unionRect
|
||||
from fontTools.misc.bezierTools import calcCubicBounds, calcQuadraticBounds
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
__all__ = ["BoundsPen", "ControlBoundsPen"]
|
||||
|
||||
|
||||
class ControlBoundsPen(BasePen):
|
||||
"""Pen to calculate the "control bounds" of a shape. This is the
|
||||
bounding box of all control points, so may be larger than the
|
||||
actual bounding box if there are curves that don't have points
|
||||
on their extremes.
|
||||
|
||||
When the shape has been drawn, the bounds are available as the
|
||||
``bounds`` attribute of the pen object. It's a 4-tuple::
|
||||
|
||||
(xMin, yMin, xMax, yMax).
|
||||
|
||||
If ``ignoreSinglePoints`` is True, single points are ignored.
|
||||
"""
|
||||
|
||||
def __init__(self, glyphSet, ignoreSinglePoints=False):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
self.ignoreSinglePoints = ignoreSinglePoints
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
self.bounds = None
|
||||
self._start = None
|
||||
|
||||
def _moveTo(self, pt):
|
||||
self._start = pt
|
||||
if not self.ignoreSinglePoints:
|
||||
self._addMoveTo()
|
||||
|
||||
def _addMoveTo(self):
|
||||
if self._start is None:
|
||||
return
|
||||
bounds = self.bounds
|
||||
if bounds:
|
||||
self.bounds = updateBounds(bounds, self._start)
|
||||
else:
|
||||
x, y = self._start
|
||||
self.bounds = (x, y, x, y)
|
||||
self._start = None
|
||||
|
||||
def _lineTo(self, pt):
|
||||
self._addMoveTo()
|
||||
self.bounds = updateBounds(self.bounds, pt)
|
||||
|
||||
def _curveToOne(self, bcp1, bcp2, pt):
|
||||
self._addMoveTo()
|
||||
bounds = self.bounds
|
||||
bounds = updateBounds(bounds, bcp1)
|
||||
bounds = updateBounds(bounds, bcp2)
|
||||
bounds = updateBounds(bounds, pt)
|
||||
self.bounds = bounds
|
||||
|
||||
def _qCurveToOne(self, bcp, pt):
|
||||
self._addMoveTo()
|
||||
bounds = self.bounds
|
||||
bounds = updateBounds(bounds, bcp)
|
||||
bounds = updateBounds(bounds, pt)
|
||||
self.bounds = bounds
|
||||
|
||||
|
||||
class BoundsPen(ControlBoundsPen):
|
||||
"""Pen to calculate the bounds of a shape. It calculates the
|
||||
correct bounds even when the shape contains curves that don't
|
||||
have points on their extremes. This is somewhat slower to compute
|
||||
than the "control bounds".
|
||||
|
||||
When the shape has been drawn, the bounds are available as the
|
||||
``bounds`` attribute of the pen object. It's a 4-tuple::
|
||||
|
||||
(xMin, yMin, xMax, yMax)
|
||||
"""
|
||||
|
||||
def _curveToOne(self, bcp1, bcp2, pt):
|
||||
self._addMoveTo()
|
||||
bounds = self.bounds
|
||||
bounds = updateBounds(bounds, pt)
|
||||
if not pointInRect(bcp1, bounds) or not pointInRect(bcp2, bounds):
|
||||
bounds = unionRect(
|
||||
bounds, calcCubicBounds(self._getCurrentPoint(), bcp1, bcp2, pt)
|
||||
)
|
||||
self.bounds = bounds
|
||||
|
||||
def _qCurveToOne(self, bcp, pt):
|
||||
self._addMoveTo()
|
||||
bounds = self.bounds
|
||||
bounds = updateBounds(bounds, pt)
|
||||
if not pointInRect(bcp, bounds):
|
||||
bounds = unionRect(
|
||||
bounds, calcQuadraticBounds(self._getCurrentPoint(), bcp, pt)
|
||||
)
|
||||
self.bounds = bounds
|
||||
26
venv/lib/python3.13/site-packages/fontTools/pens/cairoPen.py
Normal file
26
venv/lib/python3.13/site-packages/fontTools/pens/cairoPen.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"""Pen to draw to a Cairo graphics library context."""
|
||||
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
__all__ = ["CairoPen"]
|
||||
|
||||
|
||||
class CairoPen(BasePen):
|
||||
"""Pen to draw to a Cairo graphics library context."""
|
||||
|
||||
def __init__(self, glyphSet, context):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
self.context = context
|
||||
|
||||
def _moveTo(self, p):
|
||||
self.context.move_to(*p)
|
||||
|
||||
def _lineTo(self, p):
|
||||
self.context.line_to(*p)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
self.context.curve_to(*p1, *p2, *p3)
|
||||
|
||||
def _closePath(self):
|
||||
self.context.close_path()
|
||||
26
venv/lib/python3.13/site-packages/fontTools/pens/cocoaPen.py
Normal file
26
venv/lib/python3.13/site-packages/fontTools/pens/cocoaPen.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
__all__ = ["CocoaPen"]
|
||||
|
||||
|
||||
class CocoaPen(BasePen):
|
||||
def __init__(self, glyphSet, path=None):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
if path is None:
|
||||
from AppKit import NSBezierPath
|
||||
|
||||
path = NSBezierPath.bezierPath()
|
||||
self.path = path
|
||||
|
||||
def _moveTo(self, p):
|
||||
self.path.moveToPoint_(p)
|
||||
|
||||
def _lineTo(self, p):
|
||||
self.path.lineToPoint_(p)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
self.path.curveToPoint_controlPoint1_controlPoint2_(p3, p1, p2)
|
||||
|
||||
def _closePath(self):
|
||||
self.path.closePath()
|
||||
325
venv/lib/python3.13/site-packages/fontTools/pens/cu2quPen.py
Normal file
325
venv/lib/python3.13/site-packages/fontTools/pens/cu2quPen.py
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import operator
|
||||
from fontTools.cu2qu import curve_to_quadratic, curves_to_quadratic
|
||||
from fontTools.pens.basePen import decomposeSuperBezierSegment
|
||||
from fontTools.pens.filterPen import FilterPen
|
||||
from fontTools.pens.reverseContourPen import ReverseContourPen
|
||||
from fontTools.pens.pointPen import BasePointToSegmentPen
|
||||
from fontTools.pens.pointPen import ReverseContourPointPen
|
||||
|
||||
|
||||
class Cu2QuPen(FilterPen):
|
||||
"""A filter pen to convert cubic bezier curves to quadratic b-splines
|
||||
using the FontTools SegmentPen protocol.
|
||||
|
||||
Args:
|
||||
|
||||
other_pen: another SegmentPen used to draw the transformed outline.
|
||||
max_err: maximum approximation error in font units. For optimal results,
|
||||
if you know the UPEM of the font, we recommend setting this to a
|
||||
value equal, or close to UPEM / 1000.
|
||||
reverse_direction: flip the contours' direction but keep starting point.
|
||||
stats: a dictionary counting the point numbers of quadratic segments.
|
||||
all_quadratic: if True (default), only quadratic b-splines are generated.
|
||||
if False, quadratic curves or cubic curves are generated depending
|
||||
on which one is more economical.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
other_pen,
|
||||
max_err,
|
||||
reverse_direction=False,
|
||||
stats=None,
|
||||
all_quadratic=True,
|
||||
):
|
||||
if reverse_direction:
|
||||
other_pen = ReverseContourPen(other_pen)
|
||||
super().__init__(other_pen)
|
||||
self.max_err = max_err
|
||||
self.stats = stats
|
||||
self.all_quadratic = all_quadratic
|
||||
|
||||
def _convert_curve(self, pt1, pt2, pt3):
|
||||
curve = (self.current_pt, pt1, pt2, pt3)
|
||||
result = curve_to_quadratic(curve, self.max_err, self.all_quadratic)
|
||||
if self.stats is not None:
|
||||
n = str(len(result) - 2)
|
||||
self.stats[n] = self.stats.get(n, 0) + 1
|
||||
if self.all_quadratic:
|
||||
self.qCurveTo(*result[1:])
|
||||
else:
|
||||
if len(result) == 3:
|
||||
self.qCurveTo(*result[1:])
|
||||
else:
|
||||
assert len(result) == 4
|
||||
super().curveTo(*result[1:])
|
||||
|
||||
def curveTo(self, *points):
|
||||
n = len(points)
|
||||
if n == 3:
|
||||
# this is the most common case, so we special-case it
|
||||
self._convert_curve(*points)
|
||||
elif n > 3:
|
||||
for segment in decomposeSuperBezierSegment(points):
|
||||
self._convert_curve(*segment)
|
||||
else:
|
||||
self.qCurveTo(*points)
|
||||
|
||||
|
||||
class Cu2QuPointPen(BasePointToSegmentPen):
|
||||
"""A filter pen to convert cubic bezier curves to quadratic b-splines
|
||||
using the FontTools PointPen protocol.
|
||||
|
||||
Args:
|
||||
other_point_pen: another PointPen used to draw the transformed outline.
|
||||
max_err: maximum approximation error in font units. For optimal results,
|
||||
if you know the UPEM of the font, we recommend setting this to a
|
||||
value equal, or close to UPEM / 1000.
|
||||
reverse_direction: reverse the winding direction of all contours.
|
||||
stats: a dictionary counting the point numbers of quadratic segments.
|
||||
all_quadratic: if True (default), only quadratic b-splines are generated.
|
||||
if False, quadratic curves or cubic curves are generated depending
|
||||
on which one is more economical.
|
||||
"""
|
||||
|
||||
__points_required = {
|
||||
"move": (1, operator.eq),
|
||||
"line": (1, operator.eq),
|
||||
"qcurve": (2, operator.ge),
|
||||
"curve": (3, operator.eq),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
other_point_pen,
|
||||
max_err,
|
||||
reverse_direction=False,
|
||||
stats=None,
|
||||
all_quadratic=True,
|
||||
):
|
||||
BasePointToSegmentPen.__init__(self)
|
||||
if reverse_direction:
|
||||
self.pen = ReverseContourPointPen(other_point_pen)
|
||||
else:
|
||||
self.pen = other_point_pen
|
||||
self.max_err = max_err
|
||||
self.stats = stats
|
||||
self.all_quadratic = all_quadratic
|
||||
|
||||
def _flushContour(self, segments):
|
||||
assert len(segments) >= 1
|
||||
closed = segments[0][0] != "move"
|
||||
new_segments = []
|
||||
prev_points = segments[-1][1]
|
||||
prev_on_curve = prev_points[-1][0]
|
||||
for segment_type, points in segments:
|
||||
if segment_type == "curve":
|
||||
for sub_points in self._split_super_bezier_segments(points):
|
||||
on_curve, smooth, name, kwargs = sub_points[-1]
|
||||
bcp1, bcp2 = sub_points[0][0], sub_points[1][0]
|
||||
cubic = [prev_on_curve, bcp1, bcp2, on_curve]
|
||||
quad = curve_to_quadratic(cubic, self.max_err, self.all_quadratic)
|
||||
if self.stats is not None:
|
||||
n = str(len(quad) - 2)
|
||||
self.stats[n] = self.stats.get(n, 0) + 1
|
||||
new_points = [(pt, False, None, {}) for pt in quad[1:-1]]
|
||||
new_points.append((on_curve, smooth, name, kwargs))
|
||||
if self.all_quadratic or len(new_points) == 2:
|
||||
new_segments.append(["qcurve", new_points])
|
||||
else:
|
||||
new_segments.append(["curve", new_points])
|
||||
prev_on_curve = sub_points[-1][0]
|
||||
else:
|
||||
new_segments.append([segment_type, points])
|
||||
prev_on_curve = points[-1][0]
|
||||
if closed:
|
||||
# the BasePointToSegmentPen.endPath method that calls _flushContour
|
||||
# rotates the point list of closed contours so that they end with
|
||||
# the first on-curve point. We restore the original starting point.
|
||||
new_segments = new_segments[-1:] + new_segments[:-1]
|
||||
self._drawPoints(new_segments)
|
||||
|
||||
def _split_super_bezier_segments(self, points):
|
||||
sub_segments = []
|
||||
# n is the number of control points
|
||||
n = len(points) - 1
|
||||
if n == 2:
|
||||
# a simple bezier curve segment
|
||||
sub_segments.append(points)
|
||||
elif n > 2:
|
||||
# a "super" bezier; decompose it
|
||||
on_curve, smooth, name, kwargs = points[-1]
|
||||
num_sub_segments = n - 1
|
||||
for i, sub_points in enumerate(
|
||||
decomposeSuperBezierSegment([pt for pt, _, _, _ in points])
|
||||
):
|
||||
new_segment = []
|
||||
for point in sub_points[:-1]:
|
||||
new_segment.append((point, False, None, {}))
|
||||
if i == (num_sub_segments - 1):
|
||||
# the last on-curve keeps its original attributes
|
||||
new_segment.append((on_curve, smooth, name, kwargs))
|
||||
else:
|
||||
# on-curves of sub-segments are always "smooth"
|
||||
new_segment.append((sub_points[-1], True, None, {}))
|
||||
sub_segments.append(new_segment)
|
||||
else:
|
||||
raise AssertionError("expected 2 control points, found: %d" % n)
|
||||
return sub_segments
|
||||
|
||||
def _drawPoints(self, segments):
|
||||
pen = self.pen
|
||||
pen.beginPath()
|
||||
last_offcurves = []
|
||||
points_required = self.__points_required
|
||||
for i, (segment_type, points) in enumerate(segments):
|
||||
if segment_type in points_required:
|
||||
n, op = points_required[segment_type]
|
||||
assert op(len(points), n), (
|
||||
f"illegal {segment_type!r} segment point count: "
|
||||
f"expected {n}, got {len(points)}"
|
||||
)
|
||||
offcurves = points[:-1]
|
||||
if i == 0:
|
||||
# any off-curve points preceding the first on-curve
|
||||
# will be appended at the end of the contour
|
||||
last_offcurves = offcurves
|
||||
else:
|
||||
for pt, smooth, name, kwargs in offcurves:
|
||||
pen.addPoint(pt, None, smooth, name, **kwargs)
|
||||
pt, smooth, name, kwargs = points[-1]
|
||||
if pt is None:
|
||||
assert segment_type == "qcurve"
|
||||
# special quadratic contour with no on-curve points:
|
||||
# we need to skip the "None" point. See also the Pen
|
||||
# protocol's qCurveTo() method and fontTools.pens.basePen
|
||||
pass
|
||||
else:
|
||||
pen.addPoint(pt, segment_type, smooth, name, **kwargs)
|
||||
else:
|
||||
raise AssertionError("unexpected segment type: %r" % segment_type)
|
||||
for pt, smooth, name, kwargs in last_offcurves:
|
||||
pen.addPoint(pt, None, smooth, name, **kwargs)
|
||||
pen.endPath()
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation):
|
||||
assert self.currentPath is None
|
||||
self.pen.addComponent(baseGlyphName, transformation)
|
||||
|
||||
|
||||
class Cu2QuMultiPen:
|
||||
"""A filter multi-pen to convert cubic bezier curves to quadratic b-splines
|
||||
in a interpolation-compatible manner, using the FontTools SegmentPen protocol.
|
||||
|
||||
Args:
|
||||
|
||||
other_pens: list of SegmentPens used to draw the transformed outlines.
|
||||
max_err: maximum approximation error in font units. For optimal results,
|
||||
if you know the UPEM of the font, we recommend setting this to a
|
||||
value equal, or close to UPEM / 1000.
|
||||
reverse_direction: flip the contours' direction but keep starting point.
|
||||
|
||||
This pen does not follow the normal SegmentPen protocol. Instead, its
|
||||
moveTo/lineTo/qCurveTo/curveTo methods take a list of tuples that are
|
||||
arguments that would normally be passed to a SegmentPen, one item for
|
||||
each of the pens in other_pens.
|
||||
"""
|
||||
|
||||
# TODO Simplify like 3e8ebcdce592fe8a59ca4c3a294cc9724351e1ce
|
||||
# Remove start_pts and _add_moveTO
|
||||
|
||||
def __init__(self, other_pens, max_err, reverse_direction=False):
|
||||
if reverse_direction:
|
||||
other_pens = [
|
||||
ReverseContourPen(pen, outputImpliedClosingLine=True)
|
||||
for pen in other_pens
|
||||
]
|
||||
self.pens = other_pens
|
||||
self.max_err = max_err
|
||||
self.start_pts = None
|
||||
self.current_pts = None
|
||||
|
||||
def _check_contour_is_open(self):
|
||||
if self.current_pts is None:
|
||||
raise AssertionError("moveTo is required")
|
||||
|
||||
def _check_contour_is_closed(self):
|
||||
if self.current_pts is not None:
|
||||
raise AssertionError("closePath or endPath is required")
|
||||
|
||||
def _add_moveTo(self):
|
||||
if self.start_pts is not None:
|
||||
for pt, pen in zip(self.start_pts, self.pens):
|
||||
pen.moveTo(*pt)
|
||||
self.start_pts = None
|
||||
|
||||
def moveTo(self, pts):
|
||||
self._check_contour_is_closed()
|
||||
self.start_pts = self.current_pts = pts
|
||||
self._add_moveTo()
|
||||
|
||||
def lineTo(self, pts):
|
||||
self._check_contour_is_open()
|
||||
self._add_moveTo()
|
||||
for pt, pen in zip(pts, self.pens):
|
||||
pen.lineTo(*pt)
|
||||
self.current_pts = pts
|
||||
|
||||
def qCurveTo(self, pointsList):
|
||||
self._check_contour_is_open()
|
||||
if len(pointsList[0]) == 1:
|
||||
self.lineTo([(points[0],) for points in pointsList])
|
||||
return
|
||||
self._add_moveTo()
|
||||
current_pts = []
|
||||
for points, pen in zip(pointsList, self.pens):
|
||||
pen.qCurveTo(*points)
|
||||
current_pts.append((points[-1],))
|
||||
self.current_pts = current_pts
|
||||
|
||||
def _curves_to_quadratic(self, pointsList):
|
||||
curves = []
|
||||
for current_pt, points in zip(self.current_pts, pointsList):
|
||||
curves.append(current_pt + points)
|
||||
quadratics = curves_to_quadratic(curves, [self.max_err] * len(curves))
|
||||
pointsList = []
|
||||
for quadratic in quadratics:
|
||||
pointsList.append(quadratic[1:])
|
||||
self.qCurveTo(pointsList)
|
||||
|
||||
def curveTo(self, pointsList):
|
||||
self._check_contour_is_open()
|
||||
self._curves_to_quadratic(pointsList)
|
||||
|
||||
def closePath(self):
|
||||
self._check_contour_is_open()
|
||||
if self.start_pts is None:
|
||||
for pen in self.pens:
|
||||
pen.closePath()
|
||||
self.current_pts = self.start_pts = None
|
||||
|
||||
def endPath(self):
|
||||
self._check_contour_is_open()
|
||||
if self.start_pts is None:
|
||||
for pen in self.pens:
|
||||
pen.endPath()
|
||||
self.current_pts = self.start_pts = None
|
||||
|
||||
def addComponent(self, glyphName, transformations):
|
||||
self._check_contour_is_closed()
|
||||
for trans, pen in zip(transformations, self.pens):
|
||||
pen.addComponent(glyphName, trans)
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
from fontTools.pens.filterPen import ContourFilterPen
|
||||
|
||||
|
||||
class ExplicitClosingLinePen(ContourFilterPen):
|
||||
"""A filter pen that adds an explicit lineTo to the first point of each closed
|
||||
contour if the end point of the last segment is not already the same as the first point.
|
||||
Otherwise, it passes the contour through unchanged.
|
||||
|
||||
>>> from pprint import pprint
|
||||
>>> from fontTools.pens.recordingPen import RecordingPen
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.lineTo((100, 0))
|
||||
>>> pen.lineTo((100, 100))
|
||||
>>> pen.closePath()
|
||||
>>> pprint(rec.value)
|
||||
[('moveTo', ((0, 0),)),
|
||||
('lineTo', ((100, 0),)),
|
||||
('lineTo', ((100, 100),)),
|
||||
('lineTo', ((0, 0),)),
|
||||
('closePath', ())]
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.lineTo((100, 0))
|
||||
>>> pen.lineTo((100, 100))
|
||||
>>> pen.lineTo((0, 0))
|
||||
>>> pen.closePath()
|
||||
>>> pprint(rec.value)
|
||||
[('moveTo', ((0, 0),)),
|
||||
('lineTo', ((100, 0),)),
|
||||
('lineTo', ((100, 100),)),
|
||||
('lineTo', ((0, 0),)),
|
||||
('closePath', ())]
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.curveTo((100, 0), (0, 100), (100, 100))
|
||||
>>> pen.closePath()
|
||||
>>> pprint(rec.value)
|
||||
[('moveTo', ((0, 0),)),
|
||||
('curveTo', ((100, 0), (0, 100), (100, 100))),
|
||||
('lineTo', ((0, 0),)),
|
||||
('closePath', ())]
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.curveTo((100, 0), (0, 100), (100, 100))
|
||||
>>> pen.lineTo((0, 0))
|
||||
>>> pen.closePath()
|
||||
>>> pprint(rec.value)
|
||||
[('moveTo', ((0, 0),)),
|
||||
('curveTo', ((100, 0), (0, 100), (100, 100))),
|
||||
('lineTo', ((0, 0),)),
|
||||
('closePath', ())]
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.curveTo((100, 0), (0, 100), (0, 0))
|
||||
>>> pen.closePath()
|
||||
>>> pprint(rec.value)
|
||||
[('moveTo', ((0, 0),)),
|
||||
('curveTo', ((100, 0), (0, 100), (0, 0))),
|
||||
('closePath', ())]
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.closePath()
|
||||
>>> pprint(rec.value)
|
||||
[('moveTo', ((0, 0),)), ('closePath', ())]
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.closePath()
|
||||
>>> pprint(rec.value)
|
||||
[('closePath', ())]
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.lineTo((100, 0))
|
||||
>>> pen.lineTo((100, 100))
|
||||
>>> pen.endPath()
|
||||
>>> pprint(rec.value)
|
||||
[('moveTo', ((0, 0),)),
|
||||
('lineTo', ((100, 0),)),
|
||||
('lineTo', ((100, 100),)),
|
||||
('endPath', ())]
|
||||
"""
|
||||
|
||||
def filterContour(self, contour):
|
||||
if (
|
||||
not contour
|
||||
or contour[0][0] != "moveTo"
|
||||
or contour[-1][0] != "closePath"
|
||||
or len(contour) < 3
|
||||
):
|
||||
return
|
||||
movePt = contour[0][1][0]
|
||||
lastSeg = contour[-2][1]
|
||||
if lastSeg and movePt != lastSeg[-1]:
|
||||
contour[-1:] = [("lineTo", (movePt,)), ("closePath", ())]
|
||||
241
venv/lib/python3.13/site-packages/fontTools/pens/filterPen.py
Normal file
241
venv/lib/python3.13/site-packages/fontTools/pens/filterPen.py
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from fontTools.pens.basePen import AbstractPen, DecomposingPen
|
||||
from fontTools.pens.pointPen import AbstractPointPen, DecomposingPointPen
|
||||
from fontTools.pens.recordingPen import RecordingPen
|
||||
|
||||
|
||||
class _PassThruComponentsMixin(object):
|
||||
def addComponent(self, glyphName, transformation, **kwargs):
|
||||
self._outPen.addComponent(glyphName, transformation, **kwargs)
|
||||
|
||||
|
||||
class FilterPen(_PassThruComponentsMixin, AbstractPen):
|
||||
"""Base class for pens that apply some transformation to the coordinates
|
||||
they receive and pass them to another pen.
|
||||
|
||||
You can override any of its methods. The default implementation does
|
||||
nothing, but passes the commands unmodified to the other pen.
|
||||
|
||||
>>> from fontTools.pens.recordingPen import RecordingPen
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = FilterPen(rec)
|
||||
>>> v = iter(rec.value)
|
||||
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> next(v)
|
||||
('moveTo', ((0, 0),))
|
||||
|
||||
>>> pen.lineTo((1, 1))
|
||||
>>> next(v)
|
||||
('lineTo', ((1, 1),))
|
||||
|
||||
>>> pen.curveTo((2, 2), (3, 3), (4, 4))
|
||||
>>> next(v)
|
||||
('curveTo', ((2, 2), (3, 3), (4, 4)))
|
||||
|
||||
>>> pen.qCurveTo((5, 5), (6, 6), (7, 7), (8, 8))
|
||||
>>> next(v)
|
||||
('qCurveTo', ((5, 5), (6, 6), (7, 7), (8, 8)))
|
||||
|
||||
>>> pen.closePath()
|
||||
>>> next(v)
|
||||
('closePath', ())
|
||||
|
||||
>>> pen.moveTo((9, 9))
|
||||
>>> next(v)
|
||||
('moveTo', ((9, 9),))
|
||||
|
||||
>>> pen.endPath()
|
||||
>>> next(v)
|
||||
('endPath', ())
|
||||
|
||||
>>> pen.addComponent('foo', (1, 0, 0, 1, 0, 0))
|
||||
>>> next(v)
|
||||
('addComponent', ('foo', (1, 0, 0, 1, 0, 0)))
|
||||
"""
|
||||
|
||||
def __init__(self, outPen):
|
||||
self._outPen = outPen
|
||||
self.current_pt = None
|
||||
|
||||
def moveTo(self, pt):
|
||||
self._outPen.moveTo(pt)
|
||||
self.current_pt = pt
|
||||
|
||||
def lineTo(self, pt):
|
||||
self._outPen.lineTo(pt)
|
||||
self.current_pt = pt
|
||||
|
||||
def curveTo(self, *points):
|
||||
self._outPen.curveTo(*points)
|
||||
self.current_pt = points[-1]
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
self._outPen.qCurveTo(*points)
|
||||
self.current_pt = points[-1]
|
||||
|
||||
def closePath(self):
|
||||
self._outPen.closePath()
|
||||
self.current_pt = None
|
||||
|
||||
def endPath(self):
|
||||
self._outPen.endPath()
|
||||
self.current_pt = None
|
||||
|
||||
|
||||
class ContourFilterPen(_PassThruComponentsMixin, RecordingPen):
|
||||
"""A "buffered" filter pen that accumulates contour data, passes
|
||||
it through a ``filterContour`` method when the contour is closed or ended,
|
||||
and finally draws the result with the output pen.
|
||||
|
||||
Components are passed through unchanged.
|
||||
"""
|
||||
|
||||
def __init__(self, outPen):
|
||||
super(ContourFilterPen, self).__init__()
|
||||
self._outPen = outPen
|
||||
|
||||
def closePath(self):
|
||||
super(ContourFilterPen, self).closePath()
|
||||
self._flushContour()
|
||||
|
||||
def endPath(self):
|
||||
super(ContourFilterPen, self).endPath()
|
||||
self._flushContour()
|
||||
|
||||
def _flushContour(self):
|
||||
result = self.filterContour(self.value)
|
||||
if result is not None:
|
||||
self.value = result
|
||||
self.replay(self._outPen)
|
||||
self.value = []
|
||||
|
||||
def filterContour(self, contour):
|
||||
"""Subclasses must override this to perform the filtering.
|
||||
|
||||
The contour is a list of pen (operator, operands) tuples.
|
||||
Operators are strings corresponding to the AbstractPen methods:
|
||||
"moveTo", "lineTo", "curveTo", "qCurveTo", "closePath" and
|
||||
"endPath". The operands are the positional arguments that are
|
||||
passed to each method.
|
||||
|
||||
If the method doesn't return a value (i.e. returns None), it's
|
||||
assumed that the argument was modified in-place.
|
||||
Otherwise, the return value is drawn with the output pen.
|
||||
"""
|
||||
return # or return contour
|
||||
|
||||
|
||||
class FilterPointPen(_PassThruComponentsMixin, AbstractPointPen):
|
||||
"""Baseclass for point pens that apply some transformation to the
|
||||
coordinates they receive and pass them to another point pen.
|
||||
|
||||
You can override any of its methods. The default implementation does
|
||||
nothing, but passes the commands unmodified to the other pen.
|
||||
|
||||
>>> from fontTools.pens.recordingPen import RecordingPointPen
|
||||
>>> rec = RecordingPointPen()
|
||||
>>> pen = FilterPointPen(rec)
|
||||
>>> v = iter(rec.value)
|
||||
>>> pen.beginPath(identifier="abc")
|
||||
>>> next(v)
|
||||
('beginPath', (), {'identifier': 'abc'})
|
||||
>>> pen.addPoint((1, 2), "line", False)
|
||||
>>> next(v)
|
||||
('addPoint', ((1, 2), 'line', False, None), {})
|
||||
>>> pen.addComponent("a", (2, 0, 0, 2, 10, -10), identifier="0001")
|
||||
>>> next(v)
|
||||
('addComponent', ('a', (2, 0, 0, 2, 10, -10)), {'identifier': '0001'})
|
||||
>>> pen.endPath()
|
||||
>>> next(v)
|
||||
('endPath', (), {})
|
||||
"""
|
||||
|
||||
def __init__(self, outPen):
|
||||
self._outPen = outPen
|
||||
|
||||
def beginPath(self, **kwargs):
|
||||
self._outPen.beginPath(**kwargs)
|
||||
|
||||
def endPath(self):
|
||||
self._outPen.endPath()
|
||||
|
||||
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
|
||||
self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
|
||||
|
||||
|
||||
class _DecomposingFilterPenMixin:
|
||||
"""Mixin class that decomposes components as regular contours.
|
||||
|
||||
Shared by both DecomposingFilterPen and DecomposingFilterPointPen.
|
||||
|
||||
Takes two required parameters, another (segment or point) pen 'outPen' to draw
|
||||
with, and a 'glyphSet' dict of drawable glyph objects to draw components from.
|
||||
|
||||
The 'skipMissingComponents' and 'reverseFlipped' optional arguments work the
|
||||
same as in the DecomposingPen/DecomposingPointPen. Both are False by default.
|
||||
|
||||
In addition, the decomposing filter pens also take the following two options:
|
||||
|
||||
'include' is an optional set of component base glyph names to consider for
|
||||
decomposition; the default include=None means decompose all components no matter
|
||||
the base glyph name).
|
||||
|
||||
'decomposeNested' (bool) controls whether to recurse decomposition into nested
|
||||
components of components (this only matters when 'include' was also provided);
|
||||
if False, only decompose top-level components included in the set, but not
|
||||
also their children.
|
||||
"""
|
||||
|
||||
# raises MissingComponentError if base glyph is not found in glyphSet
|
||||
skipMissingComponents = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
outPen,
|
||||
glyphSet,
|
||||
skipMissingComponents=None,
|
||||
reverseFlipped=False,
|
||||
include: set[str] | None = None,
|
||||
decomposeNested: bool = True,
|
||||
):
|
||||
super().__init__(
|
||||
outPen=outPen,
|
||||
glyphSet=glyphSet,
|
||||
skipMissingComponents=skipMissingComponents,
|
||||
reverseFlipped=reverseFlipped,
|
||||
)
|
||||
self.include = include
|
||||
self.decomposeNested = decomposeNested
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation, **kwargs):
|
||||
# only decompose the component if it's included in the set
|
||||
if self.include is None or baseGlyphName in self.include:
|
||||
# if we're decomposing nested components, temporarily set include to None
|
||||
include_bak = self.include
|
||||
if self.decomposeNested and self.include:
|
||||
self.include = None
|
||||
try:
|
||||
super().addComponent(baseGlyphName, transformation, **kwargs)
|
||||
finally:
|
||||
if self.include != include_bak:
|
||||
self.include = include_bak
|
||||
else:
|
||||
_PassThruComponentsMixin.addComponent(
|
||||
self, baseGlyphName, transformation, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class DecomposingFilterPen(_DecomposingFilterPenMixin, DecomposingPen, FilterPen):
|
||||
"""Filter pen that draws components as regular contours."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DecomposingFilterPointPen(
|
||||
_DecomposingFilterPenMixin, DecomposingPointPen, FilterPointPen
|
||||
):
|
||||
"""Filter point pen that draws components as regular contours."""
|
||||
|
||||
pass
|
||||
462
venv/lib/python3.13/site-packages/fontTools/pens/freetypePen.py
Normal file
462
venv/lib/python3.13/site-packages/fontTools/pens/freetypePen.py
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Pen to rasterize paths with FreeType."""
|
||||
|
||||
__all__ = ["FreeTypePen"]
|
||||
|
||||
import os
|
||||
import ctypes
|
||||
import platform
|
||||
import subprocess
|
||||
import collections
|
||||
import math
|
||||
|
||||
import freetype
|
||||
from freetype.raw import FT_Outline_Get_Bitmap, FT_Outline_Get_BBox, FT_Outline_Get_CBox
|
||||
from freetype.ft_types import FT_Pos
|
||||
from freetype.ft_structs import FT_Vector, FT_BBox, FT_Bitmap, FT_Outline
|
||||
from freetype.ft_enums import (
|
||||
FT_OUTLINE_NONE,
|
||||
FT_OUTLINE_EVEN_ODD_FILL,
|
||||
FT_PIXEL_MODE_GRAY,
|
||||
FT_CURVE_TAG_ON,
|
||||
FT_CURVE_TAG_CONIC,
|
||||
FT_CURVE_TAG_CUBIC,
|
||||
)
|
||||
from freetype.ft_errors import FT_Exception
|
||||
|
||||
from fontTools.pens.basePen import BasePen, PenError
|
||||
from fontTools.misc.roundTools import otRound
|
||||
from fontTools.misc.transform import Transform
|
||||
|
||||
Contour = collections.namedtuple("Contour", ("points", "tags"))
|
||||
|
||||
|
||||
class FreeTypePen(BasePen):
|
||||
"""Pen to rasterize paths with FreeType. Requires `freetype-py` module.
|
||||
|
||||
Constructs ``FT_Outline`` from the paths, and renders it within a bitmap
|
||||
buffer.
|
||||
|
||||
For ``array()`` and ``show()``, `numpy` and `matplotlib` must be installed.
|
||||
For ``image()``, `Pillow` is required. Each module is lazily loaded when the
|
||||
corresponding method is called.
|
||||
|
||||
Args:
|
||||
glyphSet: a dictionary of drawable glyph objects keyed by name
|
||||
used to resolve component references in composite glyphs.
|
||||
|
||||
Examples:
|
||||
If `numpy` and `matplotlib` is available, the following code will
|
||||
show the glyph image of `fi` in a new window::
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.pens.freetypePen import FreeTypePen
|
||||
from fontTools.misc.transform import Offset
|
||||
pen = FreeTypePen(None)
|
||||
font = TTFont('SourceSansPro-Regular.otf')
|
||||
glyph = font.getGlyphSet()['fi']
|
||||
glyph.draw(pen)
|
||||
width, ascender, descender = glyph.width, font['OS/2'].usWinAscent, -font['OS/2'].usWinDescent
|
||||
height = ascender - descender
|
||||
pen.show(width=width, height=height, transform=Offset(0, -descender))
|
||||
|
||||
Combining with `uharfbuzz`, you can typeset a chunk of glyphs in a pen::
|
||||
|
||||
import uharfbuzz as hb
|
||||
from fontTools.pens.freetypePen import FreeTypePen
|
||||
from fontTools.pens.transformPen import TransformPen
|
||||
from fontTools.misc.transform import Offset
|
||||
|
||||
en1, en2, ar, ja = 'Typesetting', 'Jeff', 'صف الحروف', 'たいぷせっと'
|
||||
for text, font_path, direction, typo_ascender, typo_descender, vhea_ascender, vhea_descender, contain, features in (
|
||||
(en1, 'NotoSans-Regular.ttf', 'ltr', 2189, -600, None, None, False, {"kern": True, "liga": True}),
|
||||
(en2, 'NotoSans-Regular.ttf', 'ltr', 2189, -600, None, None, True, {"kern": True, "liga": True}),
|
||||
(ar, 'NotoSansArabic-Regular.ttf', 'rtl', 1374, -738, None, None, False, {"kern": True, "liga": True}),
|
||||
(ja, 'NotoSansJP-Regular.otf', 'ltr', 880, -120, 500, -500, False, {"palt": True, "kern": True}),
|
||||
(ja, 'NotoSansJP-Regular.otf', 'ttb', 880, -120, 500, -500, False, {"vert": True, "vpal": True, "vkrn": True})
|
||||
):
|
||||
blob = hb.Blob.from_file_path(font_path)
|
||||
face = hb.Face(blob)
|
||||
font = hb.Font(face)
|
||||
buf = hb.Buffer()
|
||||
buf.direction = direction
|
||||
buf.add_str(text)
|
||||
buf.guess_segment_properties()
|
||||
hb.shape(font, buf, features)
|
||||
|
||||
x, y = 0, 0
|
||||
pen = FreeTypePen(None)
|
||||
for info, pos in zip(buf.glyph_infos, buf.glyph_positions):
|
||||
gid = info.codepoint
|
||||
transformed = TransformPen(pen, Offset(x + pos.x_offset, y + pos.y_offset))
|
||||
font.draw_glyph_with_pen(gid, transformed)
|
||||
x += pos.x_advance
|
||||
y += pos.y_advance
|
||||
|
||||
offset, width, height = None, None, None
|
||||
if direction in ('ltr', 'rtl'):
|
||||
offset = (0, -typo_descender)
|
||||
width = x
|
||||
height = typo_ascender - typo_descender
|
||||
else:
|
||||
offset = (-vhea_descender, -y)
|
||||
width = vhea_ascender - vhea_descender
|
||||
height = -y
|
||||
pen.show(width=width, height=height, transform=Offset(*offset), contain=contain)
|
||||
|
||||
For Jupyter Notebook, the rendered image will be displayed in a cell if
|
||||
you replace ``show()`` with ``image()`` in the examples.
|
||||
"""
|
||||
|
||||
def __init__(self, glyphSet):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
self.contours = []
|
||||
|
||||
def outline(self, transform=None, evenOdd=False):
|
||||
"""Converts the current contours to ``FT_Outline``.
|
||||
|
||||
Args:
|
||||
transform: An optional 6-tuple containing an affine transformation,
|
||||
or a ``Transform`` object from the ``fontTools.misc.transform``
|
||||
module.
|
||||
evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
|
||||
"""
|
||||
transform = transform or Transform()
|
||||
if not hasattr(transform, "transformPoint"):
|
||||
transform = Transform(*transform)
|
||||
n_contours = len(self.contours)
|
||||
n_points = sum((len(contour.points) for contour in self.contours))
|
||||
points = []
|
||||
for contour in self.contours:
|
||||
for point in contour.points:
|
||||
point = transform.transformPoint(point)
|
||||
points.append(
|
||||
FT_Vector(
|
||||
FT_Pos(otRound(point[0] * 64)), FT_Pos(otRound(point[1] * 64))
|
||||
)
|
||||
)
|
||||
tags = []
|
||||
for contour in self.contours:
|
||||
for tag in contour.tags:
|
||||
tags.append(tag)
|
||||
contours = []
|
||||
contours_sum = 0
|
||||
for contour in self.contours:
|
||||
contours_sum += len(contour.points)
|
||||
contours.append(contours_sum - 1)
|
||||
flags = FT_OUTLINE_EVEN_ODD_FILL if evenOdd else FT_OUTLINE_NONE
|
||||
return FT_Outline(
|
||||
(ctypes.c_short)(n_contours),
|
||||
(ctypes.c_short)(n_points),
|
||||
(FT_Vector * n_points)(*points),
|
||||
(ctypes.c_ubyte * n_points)(*tags),
|
||||
(ctypes.c_short * n_contours)(*contours),
|
||||
(ctypes.c_int)(flags),
|
||||
)
|
||||
|
||||
def buffer(
|
||||
self, width=None, height=None, transform=None, contain=False, evenOdd=False
|
||||
):
|
||||
"""Renders the current contours within a bitmap buffer.
|
||||
|
||||
Args:
|
||||
width: Image width of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
height: Image height of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
transform: An optional 6-tuple containing an affine transformation,
|
||||
or a ``Transform`` object from the ``fontTools.misc.transform``
|
||||
module. The bitmap size is not affected by this matrix.
|
||||
contain: If ``True``, the image size will be automatically expanded
|
||||
so that it fits to the bounding box of the paths. Useful for
|
||||
rendering glyphs with negative sidebearings without clipping.
|
||||
evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
|
||||
|
||||
Returns:
|
||||
A tuple of ``(buffer, size)``, where ``buffer`` is a ``bytes``
|
||||
object of the resulted bitmap and ``size`` is a 2-tuple of its
|
||||
dimension.
|
||||
|
||||
Notes:
|
||||
The image size should always be given explicitly if you need to get
|
||||
a proper glyph image. When ``width`` and ``height`` are omitted, it
|
||||
forcifully fits to the bounding box and the side bearings get
|
||||
cropped. If you pass ``0`` to both ``width`` and ``height`` and set
|
||||
``contain`` to ``True``, it expands to the bounding box while
|
||||
maintaining the origin of the contours, meaning that LSB will be
|
||||
maintained but RSB won’t. The difference between the two becomes
|
||||
more obvious when rotate or skew transformation is applied.
|
||||
|
||||
Example:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>>
|
||||
>> pen = FreeTypePen(None)
|
||||
>> glyph.draw(pen)
|
||||
>> buf, size = pen.buffer(width=500, height=1000)
|
||||
>> type(buf), len(buf), size
|
||||
(<class 'bytes'>, 500000, (500, 1000))
|
||||
"""
|
||||
transform = transform or Transform()
|
||||
if not hasattr(transform, "transformPoint"):
|
||||
transform = Transform(*transform)
|
||||
contain_x, contain_y = contain or width is None, contain or height is None
|
||||
if contain_x or contain_y:
|
||||
dx, dy = transform.dx, transform.dy
|
||||
bbox = self.bbox
|
||||
p1, p2, p3, p4 = (
|
||||
transform.transformPoint((bbox[0], bbox[1])),
|
||||
transform.transformPoint((bbox[2], bbox[1])),
|
||||
transform.transformPoint((bbox[0], bbox[3])),
|
||||
transform.transformPoint((bbox[2], bbox[3])),
|
||||
)
|
||||
px, py = (p1[0], p2[0], p3[0], p4[0]), (p1[1], p2[1], p3[1], p4[1])
|
||||
if contain_x:
|
||||
if width is None:
|
||||
dx = dx - min(*px)
|
||||
width = max(*px) - min(*px)
|
||||
else:
|
||||
dx = dx - min(min(*px), 0.0)
|
||||
width = max(width, max(*px) - min(min(*px), 0.0))
|
||||
if contain_y:
|
||||
if height is None:
|
||||
dy = dy - min(*py)
|
||||
height = max(*py) - min(*py)
|
||||
else:
|
||||
dy = dy - min(min(*py), 0.0)
|
||||
height = max(height, max(*py) - min(min(*py), 0.0))
|
||||
transform = Transform(*transform[:4], dx, dy)
|
||||
width, height = math.ceil(width), math.ceil(height)
|
||||
buf = ctypes.create_string_buffer(width * height)
|
||||
bitmap = FT_Bitmap(
|
||||
(ctypes.c_int)(height),
|
||||
(ctypes.c_int)(width),
|
||||
(ctypes.c_int)(width),
|
||||
(ctypes.POINTER(ctypes.c_ubyte))(buf),
|
||||
(ctypes.c_short)(256),
|
||||
(ctypes.c_ubyte)(FT_PIXEL_MODE_GRAY),
|
||||
(ctypes.c_char)(0),
|
||||
(ctypes.c_void_p)(None),
|
||||
)
|
||||
outline = self.outline(transform=transform, evenOdd=evenOdd)
|
||||
err = FT_Outline_Get_Bitmap(
|
||||
freetype.get_handle(), ctypes.byref(outline), ctypes.byref(bitmap)
|
||||
)
|
||||
if err != 0:
|
||||
raise FT_Exception(err)
|
||||
return buf.raw, (width, height)
|
||||
|
||||
def array(
|
||||
self, width=None, height=None, transform=None, contain=False, evenOdd=False
|
||||
):
|
||||
"""Returns the rendered contours as a numpy array. Requires `numpy`.
|
||||
|
||||
Args:
|
||||
width: Image width of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
height: Image height of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
transform: An optional 6-tuple containing an affine transformation,
|
||||
or a ``Transform`` object from the ``fontTools.misc.transform``
|
||||
module. The bitmap size is not affected by this matrix.
|
||||
contain: If ``True``, the image size will be automatically expanded
|
||||
so that it fits to the bounding box of the paths. Useful for
|
||||
rendering glyphs with negative sidebearings without clipping.
|
||||
evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
|
||||
|
||||
Returns:
|
||||
A ``numpy.ndarray`` object with a shape of ``(height, width)``.
|
||||
Each element takes a value in the range of ``[0.0, 1.0]``.
|
||||
|
||||
Notes:
|
||||
The image size should always be given explicitly if you need to get
|
||||
a proper glyph image. When ``width`` and ``height`` are omitted, it
|
||||
forcifully fits to the bounding box and the side bearings get
|
||||
cropped. If you pass ``0`` to both ``width`` and ``height`` and set
|
||||
``contain`` to ``True``, it expands to the bounding box while
|
||||
maintaining the origin of the contours, meaning that LSB will be
|
||||
maintained but RSB won’t. The difference between the two becomes
|
||||
more obvious when rotate or skew transformation is applied.
|
||||
|
||||
Example:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>>
|
||||
>> pen = FreeTypePen(None)
|
||||
>> glyph.draw(pen)
|
||||
>> arr = pen.array(width=500, height=1000)
|
||||
>> type(a), a.shape
|
||||
(<class 'numpy.ndarray'>, (1000, 500))
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
buf, size = self.buffer(
|
||||
width=width,
|
||||
height=height,
|
||||
transform=transform,
|
||||
contain=contain,
|
||||
evenOdd=evenOdd,
|
||||
)
|
||||
return np.frombuffer(buf, "B").reshape((size[1], size[0])) / 255.0
|
||||
|
||||
def show(
|
||||
self, width=None, height=None, transform=None, contain=False, evenOdd=False
|
||||
):
|
||||
"""Plots the rendered contours with `pyplot`. Requires `numpy` and
|
||||
`matplotlib`.
|
||||
|
||||
Args:
|
||||
width: Image width of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
height: Image height of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
transform: An optional 6-tuple containing an affine transformation,
|
||||
or a ``Transform`` object from the ``fontTools.misc.transform``
|
||||
module. The bitmap size is not affected by this matrix.
|
||||
contain: If ``True``, the image size will be automatically expanded
|
||||
so that it fits to the bounding box of the paths. Useful for
|
||||
rendering glyphs with negative sidebearings without clipping.
|
||||
evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
|
||||
|
||||
Notes:
|
||||
The image size should always be given explicitly if you need to get
|
||||
a proper glyph image. When ``width`` and ``height`` are omitted, it
|
||||
forcifully fits to the bounding box and the side bearings get
|
||||
cropped. If you pass ``0`` to both ``width`` and ``height`` and set
|
||||
``contain`` to ``True``, it expands to the bounding box while
|
||||
maintaining the origin of the contours, meaning that LSB will be
|
||||
maintained but RSB won’t. The difference between the two becomes
|
||||
more obvious when rotate or skew transformation is applied.
|
||||
|
||||
Example:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>>
|
||||
>> pen = FreeTypePen(None)
|
||||
>> glyph.draw(pen)
|
||||
>> pen.show(width=500, height=1000)
|
||||
"""
|
||||
from matplotlib import pyplot as plt
|
||||
|
||||
a = self.array(
|
||||
width=width,
|
||||
height=height,
|
||||
transform=transform,
|
||||
contain=contain,
|
||||
evenOdd=evenOdd,
|
||||
)
|
||||
plt.imshow(a, cmap="gray_r", vmin=0, vmax=1)
|
||||
plt.show()
|
||||
|
||||
def image(
|
||||
self, width=None, height=None, transform=None, contain=False, evenOdd=False
|
||||
):
|
||||
"""Returns the rendered contours as a PIL image. Requires `Pillow`.
|
||||
Can be used to display a glyph image in Jupyter Notebook.
|
||||
|
||||
Args:
|
||||
width: Image width of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
height: Image height of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
transform: An optional 6-tuple containing an affine transformation,
|
||||
or a ``Transform`` object from the ``fontTools.misc.transform``
|
||||
module. The bitmap size is not affected by this matrix.
|
||||
contain: If ``True``, the image size will be automatically expanded
|
||||
so that it fits to the bounding box of the paths. Useful for
|
||||
rendering glyphs with negative sidebearings without clipping.
|
||||
evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
|
||||
|
||||
Returns:
|
||||
A ``PIL.image`` object. The image is filled in black with alpha
|
||||
channel obtained from the rendered bitmap.
|
||||
|
||||
Notes:
|
||||
The image size should always be given explicitly if you need to get
|
||||
a proper glyph image. When ``width`` and ``height`` are omitted, it
|
||||
forcifully fits to the bounding box and the side bearings get
|
||||
cropped. If you pass ``0`` to both ``width`` and ``height`` and set
|
||||
``contain`` to ``True``, it expands to the bounding box while
|
||||
maintaining the origin of the contours, meaning that LSB will be
|
||||
maintained but RSB won’t. The difference between the two becomes
|
||||
more obvious when rotate or skew transformation is applied.
|
||||
|
||||
Example:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>>
|
||||
>> pen = FreeTypePen(None)
|
||||
>> glyph.draw(pen)
|
||||
>> img = pen.image(width=500, height=1000)
|
||||
>> type(img), img.size
|
||||
(<class 'PIL.Image.Image'>, (500, 1000))
|
||||
"""
|
||||
from PIL import Image
|
||||
|
||||
buf, size = self.buffer(
|
||||
width=width,
|
||||
height=height,
|
||||
transform=transform,
|
||||
contain=contain,
|
||||
evenOdd=evenOdd,
|
||||
)
|
||||
img = Image.new("L", size, 0)
|
||||
img.putalpha(Image.frombuffer("L", size, buf))
|
||||
return img
|
||||
|
||||
@property
|
||||
def bbox(self):
|
||||
"""Computes the exact bounding box of an outline.
|
||||
|
||||
Returns:
|
||||
A tuple of ``(xMin, yMin, xMax, yMax)``.
|
||||
"""
|
||||
bbox = FT_BBox()
|
||||
outline = self.outline()
|
||||
FT_Outline_Get_BBox(ctypes.byref(outline), ctypes.byref(bbox))
|
||||
return (bbox.xMin / 64.0, bbox.yMin / 64.0, bbox.xMax / 64.0, bbox.yMax / 64.0)
|
||||
|
||||
@property
|
||||
def cbox(self):
|
||||
"""Returns an outline's ‘control box’.
|
||||
|
||||
Returns:
|
||||
A tuple of ``(xMin, yMin, xMax, yMax)``.
|
||||
"""
|
||||
cbox = FT_BBox()
|
||||
outline = self.outline()
|
||||
FT_Outline_Get_CBox(ctypes.byref(outline), ctypes.byref(cbox))
|
||||
return (cbox.xMin / 64.0, cbox.yMin / 64.0, cbox.xMax / 64.0, cbox.yMax / 64.0)
|
||||
|
||||
def _moveTo(self, pt):
|
||||
contour = Contour([], [])
|
||||
self.contours.append(contour)
|
||||
contour.points.append(pt)
|
||||
contour.tags.append(FT_CURVE_TAG_ON)
|
||||
|
||||
def _lineTo(self, pt):
|
||||
if not (self.contours and len(self.contours[-1].points) > 0):
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
contour = self.contours[-1]
|
||||
contour.points.append(pt)
|
||||
contour.tags.append(FT_CURVE_TAG_ON)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
if not (self.contours and len(self.contours[-1].points) > 0):
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
t1, t2, t3 = FT_CURVE_TAG_CUBIC, FT_CURVE_TAG_CUBIC, FT_CURVE_TAG_ON
|
||||
contour = self.contours[-1]
|
||||
for p, t in ((p1, t1), (p2, t2), (p3, t3)):
|
||||
contour.points.append(p)
|
||||
contour.tags.append(t)
|
||||
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
if not (self.contours and len(self.contours[-1].points) > 0):
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
t1, t2 = FT_CURVE_TAG_CONIC, FT_CURVE_TAG_ON
|
||||
contour = self.contours[-1]
|
||||
for p, t in ((p1, t1), (p2, t2)):
|
||||
contour.points.append(p)
|
||||
contour.tags.append(t)
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
# Modified from https://github.com/adobe-type-tools/psautohint/blob/08b346865710ed3c172f1eb581d6ef243b203f99/python/psautohint/ufoFont.py#L800-L838
|
||||
import hashlib
|
||||
|
||||
from fontTools.pens.basePen import MissingComponentError
|
||||
from fontTools.pens.pointPen import AbstractPointPen
|
||||
|
||||
|
||||
class HashPointPen(AbstractPointPen):
|
||||
"""
|
||||
This pen can be used to check if a glyph's contents (outlines plus
|
||||
components) have changed.
|
||||
|
||||
Components are added as the original outline plus each composite's
|
||||
transformation.
|
||||
|
||||
Example: You have some TrueType hinting code for a glyph which you want to
|
||||
compile. The hinting code specifies a hash value computed with HashPointPen
|
||||
that was valid for the glyph's outlines at the time the hinting code was
|
||||
written. Now you can calculate the hash for the glyph's current outlines to
|
||||
check if the outlines have changed, which would probably make the hinting
|
||||
code invalid.
|
||||
|
||||
> glyph = ufo[name]
|
||||
> hash_pen = HashPointPen(glyph.width, ufo)
|
||||
> glyph.drawPoints(hash_pen)
|
||||
> ttdata = glyph.lib.get("public.truetype.instructions", None)
|
||||
> stored_hash = ttdata.get("id", None) # The hash is stored in the "id" key
|
||||
> if stored_hash is None or stored_hash != hash_pen.hash:
|
||||
> logger.error(f"Glyph hash mismatch, glyph '{name}' will have no instructions in font.")
|
||||
> else:
|
||||
> # The hash values are identical, the outline has not changed.
|
||||
> # Compile the hinting code ...
|
||||
> pass
|
||||
|
||||
If you want to compare a glyph from a source format which supports floating point
|
||||
coordinates and transformations against a glyph from a format which has restrictions
|
||||
on the precision of floats, e.g. UFO vs. TTF, you must use an appropriate rounding
|
||||
function to make the values comparable. For TTF fonts with composites, this
|
||||
construct can be used to make the transform values conform to F2Dot14:
|
||||
|
||||
> ttf_hash_pen = HashPointPen(ttf_glyph_width, ttFont.getGlyphSet())
|
||||
> ttf_round_pen = RoundingPointPen(ttf_hash_pen, transformRoundFunc=partial(floatToFixedToFloat, precisionBits=14))
|
||||
> ufo_hash_pen = HashPointPen(ufo_glyph.width, ufo)
|
||||
> ttf_glyph.drawPoints(ttf_round_pen, ttFont["glyf"])
|
||||
> ufo_round_pen = RoundingPointPen(ufo_hash_pen, transformRoundFunc=partial(floatToFixedToFloat, precisionBits=14))
|
||||
> ufo_glyph.drawPoints(ufo_round_pen)
|
||||
> assert ttf_hash_pen.hash == ufo_hash_pen.hash
|
||||
"""
|
||||
|
||||
def __init__(self, glyphWidth=0, glyphSet=None):
|
||||
self.glyphset = glyphSet
|
||||
self.data = ["w%s" % round(glyphWidth, 9)]
|
||||
|
||||
@property
|
||||
def hash(self):
|
||||
data = "".join(self.data)
|
||||
if len(data) >= 128:
|
||||
data = hashlib.sha512(data.encode("ascii")).hexdigest()
|
||||
return data
|
||||
|
||||
def beginPath(self, identifier=None, **kwargs):
|
||||
pass
|
||||
|
||||
def endPath(self):
|
||||
self.data.append("|")
|
||||
|
||||
def addPoint(
|
||||
self,
|
||||
pt,
|
||||
segmentType=None,
|
||||
smooth=False,
|
||||
name=None,
|
||||
identifier=None,
|
||||
**kwargs,
|
||||
):
|
||||
if segmentType is None:
|
||||
pt_type = "o" # offcurve
|
||||
else:
|
||||
pt_type = segmentType[0]
|
||||
self.data.append(f"{pt_type}{pt[0]:g}{pt[1]:+g}")
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
|
||||
tr = "".join([f"{t:+}" for t in transformation])
|
||||
self.data.append("[")
|
||||
try:
|
||||
self.glyphset[baseGlyphName].drawPoints(self)
|
||||
except KeyError:
|
||||
raise MissingComponentError(baseGlyphName)
|
||||
self.data.append(f"({tr})]")
|
||||
13466
venv/lib/python3.13/site-packages/fontTools/pens/momentsPen.c
Normal file
13466
venv/lib/python3.13/site-packages/fontTools/pens/momentsPen.c
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
879
venv/lib/python3.13/site-packages/fontTools/pens/momentsPen.py
Normal file
879
venv/lib/python3.13/site-packages/fontTools/pens/momentsPen.py
Normal file
|
|
@ -0,0 +1,879 @@
|
|||
from fontTools.pens.basePen import BasePen, OpenContourError
|
||||
|
||||
try:
|
||||
import cython
|
||||
except (AttributeError, ImportError):
|
||||
# if cython not installed, use mock module with no-op decorators and types
|
||||
from fontTools.misc import cython
|
||||
COMPILED = cython.compiled
|
||||
|
||||
|
||||
__all__ = ["MomentsPen"]
|
||||
|
||||
|
||||
class MomentsPen(BasePen):
|
||||
|
||||
def __init__(self, glyphset=None):
|
||||
BasePen.__init__(self, glyphset)
|
||||
|
||||
self.area = 0
|
||||
self.momentX = 0
|
||||
self.momentY = 0
|
||||
self.momentXX = 0
|
||||
self.momentXY = 0
|
||||
self.momentYY = 0
|
||||
|
||||
def _moveTo(self, p0):
|
||||
self._startPoint = p0
|
||||
|
||||
def _closePath(self):
|
||||
p0 = self._getCurrentPoint()
|
||||
if p0 != self._startPoint:
|
||||
self._lineTo(self._startPoint)
|
||||
|
||||
def _endPath(self):
|
||||
p0 = self._getCurrentPoint()
|
||||
if p0 != self._startPoint:
|
||||
raise OpenContourError("Glyph statistics is not defined on open contours.")
|
||||
|
||||
@cython.locals(r0=cython.double)
|
||||
@cython.locals(r1=cython.double)
|
||||
@cython.locals(r2=cython.double)
|
||||
@cython.locals(r3=cython.double)
|
||||
@cython.locals(r4=cython.double)
|
||||
@cython.locals(r5=cython.double)
|
||||
@cython.locals(r6=cython.double)
|
||||
@cython.locals(r7=cython.double)
|
||||
@cython.locals(r8=cython.double)
|
||||
@cython.locals(r9=cython.double)
|
||||
@cython.locals(r10=cython.double)
|
||||
@cython.locals(r11=cython.double)
|
||||
@cython.locals(r12=cython.double)
|
||||
@cython.locals(x0=cython.double, y0=cython.double)
|
||||
@cython.locals(x1=cython.double, y1=cython.double)
|
||||
def _lineTo(self, p1):
|
||||
x0, y0 = self._getCurrentPoint()
|
||||
x1, y1 = p1
|
||||
|
||||
r0 = x1 * y0
|
||||
r1 = x1 * y1
|
||||
r2 = x1**2
|
||||
r3 = r2 * y1
|
||||
r4 = y0 - y1
|
||||
r5 = r4 * x0
|
||||
r6 = x0**2
|
||||
r7 = 2 * y0
|
||||
r8 = y0**2
|
||||
r9 = y1**2
|
||||
r10 = x1**3
|
||||
r11 = y0**3
|
||||
r12 = y1**3
|
||||
|
||||
self.area += -r0 / 2 - r1 / 2 + x0 * (y0 + y1) / 2
|
||||
self.momentX += -r2 * y0 / 6 - r3 / 3 - r5 * x1 / 6 + r6 * (r7 + y1) / 6
|
||||
self.momentY += (
|
||||
-r0 * y1 / 6 - r8 * x1 / 6 - r9 * x1 / 6 + x0 * (r8 + r9 + y0 * y1) / 6
|
||||
)
|
||||
self.momentXX += (
|
||||
-r10 * y0 / 12
|
||||
- r10 * y1 / 4
|
||||
- r2 * r5 / 12
|
||||
- r4 * r6 * x1 / 12
|
||||
+ x0**3 * (3 * y0 + y1) / 12
|
||||
)
|
||||
self.momentXY += (
|
||||
-r2 * r8 / 24
|
||||
- r2 * r9 / 8
|
||||
- r3 * r7 / 24
|
||||
+ r6 * (r7 * y1 + 3 * r8 + r9) / 24
|
||||
- x0 * x1 * (r8 - r9) / 12
|
||||
)
|
||||
self.momentYY += (
|
||||
-r0 * r9 / 12
|
||||
- r1 * r8 / 12
|
||||
- r11 * x1 / 12
|
||||
- r12 * x1 / 12
|
||||
+ x0 * (r11 + r12 + r8 * y1 + r9 * y0) / 12
|
||||
)
|
||||
|
||||
@cython.locals(r0=cython.double)
|
||||
@cython.locals(r1=cython.double)
|
||||
@cython.locals(r2=cython.double)
|
||||
@cython.locals(r3=cython.double)
|
||||
@cython.locals(r4=cython.double)
|
||||
@cython.locals(r5=cython.double)
|
||||
@cython.locals(r6=cython.double)
|
||||
@cython.locals(r7=cython.double)
|
||||
@cython.locals(r8=cython.double)
|
||||
@cython.locals(r9=cython.double)
|
||||
@cython.locals(r10=cython.double)
|
||||
@cython.locals(r11=cython.double)
|
||||
@cython.locals(r12=cython.double)
|
||||
@cython.locals(r13=cython.double)
|
||||
@cython.locals(r14=cython.double)
|
||||
@cython.locals(r15=cython.double)
|
||||
@cython.locals(r16=cython.double)
|
||||
@cython.locals(r17=cython.double)
|
||||
@cython.locals(r18=cython.double)
|
||||
@cython.locals(r19=cython.double)
|
||||
@cython.locals(r20=cython.double)
|
||||
@cython.locals(r21=cython.double)
|
||||
@cython.locals(r22=cython.double)
|
||||
@cython.locals(r23=cython.double)
|
||||
@cython.locals(r24=cython.double)
|
||||
@cython.locals(r25=cython.double)
|
||||
@cython.locals(r26=cython.double)
|
||||
@cython.locals(r27=cython.double)
|
||||
@cython.locals(r28=cython.double)
|
||||
@cython.locals(r29=cython.double)
|
||||
@cython.locals(r30=cython.double)
|
||||
@cython.locals(r31=cython.double)
|
||||
@cython.locals(r32=cython.double)
|
||||
@cython.locals(r33=cython.double)
|
||||
@cython.locals(r34=cython.double)
|
||||
@cython.locals(r35=cython.double)
|
||||
@cython.locals(r36=cython.double)
|
||||
@cython.locals(r37=cython.double)
|
||||
@cython.locals(r38=cython.double)
|
||||
@cython.locals(r39=cython.double)
|
||||
@cython.locals(r40=cython.double)
|
||||
@cython.locals(r41=cython.double)
|
||||
@cython.locals(r42=cython.double)
|
||||
@cython.locals(r43=cython.double)
|
||||
@cython.locals(r44=cython.double)
|
||||
@cython.locals(r45=cython.double)
|
||||
@cython.locals(r46=cython.double)
|
||||
@cython.locals(r47=cython.double)
|
||||
@cython.locals(r48=cython.double)
|
||||
@cython.locals(r49=cython.double)
|
||||
@cython.locals(r50=cython.double)
|
||||
@cython.locals(r51=cython.double)
|
||||
@cython.locals(r52=cython.double)
|
||||
@cython.locals(r53=cython.double)
|
||||
@cython.locals(x0=cython.double, y0=cython.double)
|
||||
@cython.locals(x1=cython.double, y1=cython.double)
|
||||
@cython.locals(x2=cython.double, y2=cython.double)
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
x0, y0 = self._getCurrentPoint()
|
||||
x1, y1 = p1
|
||||
x2, y2 = p2
|
||||
|
||||
r0 = 2 * y1
|
||||
r1 = r0 * x2
|
||||
r2 = x2 * y2
|
||||
r3 = 3 * r2
|
||||
r4 = 2 * x1
|
||||
r5 = 3 * y0
|
||||
r6 = x1**2
|
||||
r7 = x2**2
|
||||
r8 = 4 * y1
|
||||
r9 = 10 * y2
|
||||
r10 = 2 * y2
|
||||
r11 = r4 * x2
|
||||
r12 = x0**2
|
||||
r13 = 10 * y0
|
||||
r14 = r4 * y2
|
||||
r15 = x2 * y0
|
||||
r16 = 4 * x1
|
||||
r17 = r0 * x1 + r2
|
||||
r18 = r2 * r8
|
||||
r19 = y1**2
|
||||
r20 = 2 * r19
|
||||
r21 = y2**2
|
||||
r22 = r21 * x2
|
||||
r23 = 5 * r22
|
||||
r24 = y0**2
|
||||
r25 = y0 * y2
|
||||
r26 = 5 * r24
|
||||
r27 = x1**3
|
||||
r28 = x2**3
|
||||
r29 = 30 * y1
|
||||
r30 = 6 * y1
|
||||
r31 = 10 * r7 * x1
|
||||
r32 = 5 * y2
|
||||
r33 = 12 * r6
|
||||
r34 = 30 * x1
|
||||
r35 = x1 * y1
|
||||
r36 = r3 + 20 * r35
|
||||
r37 = 12 * x1
|
||||
r38 = 20 * r6
|
||||
r39 = 8 * r6 * y1
|
||||
r40 = r32 * r7
|
||||
r41 = 60 * y1
|
||||
r42 = 20 * r19
|
||||
r43 = 4 * r19
|
||||
r44 = 15 * r21
|
||||
r45 = 12 * x2
|
||||
r46 = 12 * y2
|
||||
r47 = 6 * x1
|
||||
r48 = 8 * r19 * x1 + r23
|
||||
r49 = 8 * y1**3
|
||||
r50 = y2**3
|
||||
r51 = y0**3
|
||||
r52 = 10 * y1
|
||||
r53 = 12 * y1
|
||||
|
||||
self.area += (
|
||||
-r1 / 6
|
||||
- r3 / 6
|
||||
+ x0 * (r0 + r5 + y2) / 6
|
||||
+ x1 * y2 / 3
|
||||
- y0 * (r4 + x2) / 6
|
||||
)
|
||||
self.momentX += (
|
||||
-r11 * (-r10 + y1) / 30
|
||||
+ r12 * (r13 + r8 + y2) / 30
|
||||
+ r6 * y2 / 15
|
||||
- r7 * r8 / 30
|
||||
- r7 * r9 / 30
|
||||
+ x0 * (r14 - r15 - r16 * y0 + r17) / 30
|
||||
- y0 * (r11 + 2 * r6 + r7) / 30
|
||||
)
|
||||
self.momentY += (
|
||||
-r18 / 30
|
||||
- r20 * x2 / 30
|
||||
- r23 / 30
|
||||
- r24 * (r16 + x2) / 30
|
||||
+ x0 * (r0 * y2 + r20 + r21 + r25 + r26 + r8 * y0) / 30
|
||||
+ x1 * y2 * (r10 + y1) / 15
|
||||
- y0 * (r1 + r17) / 30
|
||||
)
|
||||
self.momentXX += (
|
||||
r12 * (r1 - 5 * r15 - r34 * y0 + r36 + r9 * x1) / 420
|
||||
+ 2 * r27 * y2 / 105
|
||||
- r28 * r29 / 420
|
||||
- r28 * y2 / 4
|
||||
- r31 * (r0 - 3 * y2) / 420
|
||||
- r6 * x2 * (r0 - r32) / 105
|
||||
+ x0**3 * (r30 + 21 * y0 + y2) / 84
|
||||
- x0
|
||||
* (
|
||||
r0 * r7
|
||||
+ r15 * r37
|
||||
- r2 * r37
|
||||
- r33 * y2
|
||||
+ r38 * y0
|
||||
- r39
|
||||
- r40
|
||||
+ r5 * r7
|
||||
)
|
||||
/ 420
|
||||
- y0 * (8 * r27 + 5 * r28 + r31 + r33 * x2) / 420
|
||||
)
|
||||
self.momentXY += (
|
||||
r12 * (r13 * y2 + 3 * r21 + 105 * r24 + r41 * y0 + r42 + r46 * y1) / 840
|
||||
- r16 * x2 * (r43 - r44) / 840
|
||||
- r21 * r7 / 8
|
||||
- r24 * (r38 + r45 * x1 + 3 * r7) / 840
|
||||
- r41 * r7 * y2 / 840
|
||||
- r42 * r7 / 840
|
||||
+ r6 * y2 * (r32 + r8) / 210
|
||||
+ x0
|
||||
* (
|
||||
-r15 * r8
|
||||
+ r16 * r25
|
||||
+ r18
|
||||
+ r21 * r47
|
||||
- r24 * r34
|
||||
- r26 * x2
|
||||
+ r35 * r46
|
||||
+ r48
|
||||
)
|
||||
/ 420
|
||||
- y0 * (r16 * r2 + r30 * r7 + r35 * r45 + r39 + r40) / 420
|
||||
)
|
||||
self.momentYY += (
|
||||
-r2 * r42 / 420
|
||||
- r22 * r29 / 420
|
||||
- r24 * (r14 + r36 + r52 * x2) / 420
|
||||
- r49 * x2 / 420
|
||||
- r50 * x2 / 12
|
||||
- r51 * (r47 + x2) / 84
|
||||
+ x0
|
||||
* (
|
||||
r19 * r46
|
||||
+ r21 * r5
|
||||
+ r21 * r52
|
||||
+ r24 * r29
|
||||
+ r25 * r53
|
||||
+ r26 * y2
|
||||
+ r42 * y0
|
||||
+ r49
|
||||
+ 5 * r50
|
||||
+ 35 * r51
|
||||
)
|
||||
/ 420
|
||||
+ x1 * y2 * (r43 + r44 + r9 * y1) / 210
|
||||
- y0 * (r19 * r45 + r2 * r53 - r21 * r4 + r48) / 420
|
||||
)
|
||||
|
||||
@cython.locals(r0=cython.double)
|
||||
@cython.locals(r1=cython.double)
|
||||
@cython.locals(r2=cython.double)
|
||||
@cython.locals(r3=cython.double)
|
||||
@cython.locals(r4=cython.double)
|
||||
@cython.locals(r5=cython.double)
|
||||
@cython.locals(r6=cython.double)
|
||||
@cython.locals(r7=cython.double)
|
||||
@cython.locals(r8=cython.double)
|
||||
@cython.locals(r9=cython.double)
|
||||
@cython.locals(r10=cython.double)
|
||||
@cython.locals(r11=cython.double)
|
||||
@cython.locals(r12=cython.double)
|
||||
@cython.locals(r13=cython.double)
|
||||
@cython.locals(r14=cython.double)
|
||||
@cython.locals(r15=cython.double)
|
||||
@cython.locals(r16=cython.double)
|
||||
@cython.locals(r17=cython.double)
|
||||
@cython.locals(r18=cython.double)
|
||||
@cython.locals(r19=cython.double)
|
||||
@cython.locals(r20=cython.double)
|
||||
@cython.locals(r21=cython.double)
|
||||
@cython.locals(r22=cython.double)
|
||||
@cython.locals(r23=cython.double)
|
||||
@cython.locals(r24=cython.double)
|
||||
@cython.locals(r25=cython.double)
|
||||
@cython.locals(r26=cython.double)
|
||||
@cython.locals(r27=cython.double)
|
||||
@cython.locals(r28=cython.double)
|
||||
@cython.locals(r29=cython.double)
|
||||
@cython.locals(r30=cython.double)
|
||||
@cython.locals(r31=cython.double)
|
||||
@cython.locals(r32=cython.double)
|
||||
@cython.locals(r33=cython.double)
|
||||
@cython.locals(r34=cython.double)
|
||||
@cython.locals(r35=cython.double)
|
||||
@cython.locals(r36=cython.double)
|
||||
@cython.locals(r37=cython.double)
|
||||
@cython.locals(r38=cython.double)
|
||||
@cython.locals(r39=cython.double)
|
||||
@cython.locals(r40=cython.double)
|
||||
@cython.locals(r41=cython.double)
|
||||
@cython.locals(r42=cython.double)
|
||||
@cython.locals(r43=cython.double)
|
||||
@cython.locals(r44=cython.double)
|
||||
@cython.locals(r45=cython.double)
|
||||
@cython.locals(r46=cython.double)
|
||||
@cython.locals(r47=cython.double)
|
||||
@cython.locals(r48=cython.double)
|
||||
@cython.locals(r49=cython.double)
|
||||
@cython.locals(r50=cython.double)
|
||||
@cython.locals(r51=cython.double)
|
||||
@cython.locals(r52=cython.double)
|
||||
@cython.locals(r53=cython.double)
|
||||
@cython.locals(r54=cython.double)
|
||||
@cython.locals(r55=cython.double)
|
||||
@cython.locals(r56=cython.double)
|
||||
@cython.locals(r57=cython.double)
|
||||
@cython.locals(r58=cython.double)
|
||||
@cython.locals(r59=cython.double)
|
||||
@cython.locals(r60=cython.double)
|
||||
@cython.locals(r61=cython.double)
|
||||
@cython.locals(r62=cython.double)
|
||||
@cython.locals(r63=cython.double)
|
||||
@cython.locals(r64=cython.double)
|
||||
@cython.locals(r65=cython.double)
|
||||
@cython.locals(r66=cython.double)
|
||||
@cython.locals(r67=cython.double)
|
||||
@cython.locals(r68=cython.double)
|
||||
@cython.locals(r69=cython.double)
|
||||
@cython.locals(r70=cython.double)
|
||||
@cython.locals(r71=cython.double)
|
||||
@cython.locals(r72=cython.double)
|
||||
@cython.locals(r73=cython.double)
|
||||
@cython.locals(r74=cython.double)
|
||||
@cython.locals(r75=cython.double)
|
||||
@cython.locals(r76=cython.double)
|
||||
@cython.locals(r77=cython.double)
|
||||
@cython.locals(r78=cython.double)
|
||||
@cython.locals(r79=cython.double)
|
||||
@cython.locals(r80=cython.double)
|
||||
@cython.locals(r81=cython.double)
|
||||
@cython.locals(r82=cython.double)
|
||||
@cython.locals(r83=cython.double)
|
||||
@cython.locals(r84=cython.double)
|
||||
@cython.locals(r85=cython.double)
|
||||
@cython.locals(r86=cython.double)
|
||||
@cython.locals(r87=cython.double)
|
||||
@cython.locals(r88=cython.double)
|
||||
@cython.locals(r89=cython.double)
|
||||
@cython.locals(r90=cython.double)
|
||||
@cython.locals(r91=cython.double)
|
||||
@cython.locals(r92=cython.double)
|
||||
@cython.locals(r93=cython.double)
|
||||
@cython.locals(r94=cython.double)
|
||||
@cython.locals(r95=cython.double)
|
||||
@cython.locals(r96=cython.double)
|
||||
@cython.locals(r97=cython.double)
|
||||
@cython.locals(r98=cython.double)
|
||||
@cython.locals(r99=cython.double)
|
||||
@cython.locals(r100=cython.double)
|
||||
@cython.locals(r101=cython.double)
|
||||
@cython.locals(r102=cython.double)
|
||||
@cython.locals(r103=cython.double)
|
||||
@cython.locals(r104=cython.double)
|
||||
@cython.locals(r105=cython.double)
|
||||
@cython.locals(r106=cython.double)
|
||||
@cython.locals(r107=cython.double)
|
||||
@cython.locals(r108=cython.double)
|
||||
@cython.locals(r109=cython.double)
|
||||
@cython.locals(r110=cython.double)
|
||||
@cython.locals(r111=cython.double)
|
||||
@cython.locals(r112=cython.double)
|
||||
@cython.locals(r113=cython.double)
|
||||
@cython.locals(r114=cython.double)
|
||||
@cython.locals(r115=cython.double)
|
||||
@cython.locals(r116=cython.double)
|
||||
@cython.locals(r117=cython.double)
|
||||
@cython.locals(r118=cython.double)
|
||||
@cython.locals(r119=cython.double)
|
||||
@cython.locals(r120=cython.double)
|
||||
@cython.locals(r121=cython.double)
|
||||
@cython.locals(r122=cython.double)
|
||||
@cython.locals(r123=cython.double)
|
||||
@cython.locals(r124=cython.double)
|
||||
@cython.locals(r125=cython.double)
|
||||
@cython.locals(r126=cython.double)
|
||||
@cython.locals(r127=cython.double)
|
||||
@cython.locals(r128=cython.double)
|
||||
@cython.locals(r129=cython.double)
|
||||
@cython.locals(r130=cython.double)
|
||||
@cython.locals(r131=cython.double)
|
||||
@cython.locals(r132=cython.double)
|
||||
@cython.locals(x0=cython.double, y0=cython.double)
|
||||
@cython.locals(x1=cython.double, y1=cython.double)
|
||||
@cython.locals(x2=cython.double, y2=cython.double)
|
||||
@cython.locals(x3=cython.double, y3=cython.double)
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
x0, y0 = self._getCurrentPoint()
|
||||
x1, y1 = p1
|
||||
x2, y2 = p2
|
||||
x3, y3 = p3
|
||||
|
||||
r0 = 6 * y2
|
||||
r1 = r0 * x3
|
||||
r2 = 10 * y3
|
||||
r3 = r2 * x3
|
||||
r4 = 3 * y1
|
||||
r5 = 6 * x1
|
||||
r6 = 3 * x2
|
||||
r7 = 6 * y1
|
||||
r8 = 3 * y2
|
||||
r9 = x2**2
|
||||
r10 = 45 * r9
|
||||
r11 = r10 * y3
|
||||
r12 = x3**2
|
||||
r13 = r12 * y2
|
||||
r14 = r12 * y3
|
||||
r15 = 7 * y3
|
||||
r16 = 15 * x3
|
||||
r17 = r16 * x2
|
||||
r18 = x1**2
|
||||
r19 = 9 * r18
|
||||
r20 = x0**2
|
||||
r21 = 21 * y1
|
||||
r22 = 9 * r9
|
||||
r23 = r7 * x3
|
||||
r24 = 9 * y2
|
||||
r25 = r24 * x2 + r3
|
||||
r26 = 9 * x2
|
||||
r27 = x2 * y3
|
||||
r28 = -r26 * y1 + 15 * r27
|
||||
r29 = 3 * x1
|
||||
r30 = 45 * x1
|
||||
r31 = 12 * x3
|
||||
r32 = 45 * r18
|
||||
r33 = 5 * r12
|
||||
r34 = r8 * x3
|
||||
r35 = 105 * y0
|
||||
r36 = 30 * y0
|
||||
r37 = r36 * x2
|
||||
r38 = 5 * x3
|
||||
r39 = 15 * y3
|
||||
r40 = 5 * y3
|
||||
r41 = r40 * x3
|
||||
r42 = x2 * y2
|
||||
r43 = 18 * r42
|
||||
r44 = 45 * y1
|
||||
r45 = r41 + r43 + r44 * x1
|
||||
r46 = y2 * y3
|
||||
r47 = r46 * x3
|
||||
r48 = y2**2
|
||||
r49 = 45 * r48
|
||||
r50 = r49 * x3
|
||||
r51 = y3**2
|
||||
r52 = r51 * x3
|
||||
r53 = y1**2
|
||||
r54 = 9 * r53
|
||||
r55 = y0**2
|
||||
r56 = 21 * x1
|
||||
r57 = 6 * x2
|
||||
r58 = r16 * y2
|
||||
r59 = r39 * y2
|
||||
r60 = 9 * r48
|
||||
r61 = r6 * y3
|
||||
r62 = 3 * y3
|
||||
r63 = r36 * y2
|
||||
r64 = y1 * y3
|
||||
r65 = 45 * r53
|
||||
r66 = 5 * r51
|
||||
r67 = x2**3
|
||||
r68 = x3**3
|
||||
r69 = 630 * y2
|
||||
r70 = 126 * x3
|
||||
r71 = x1**3
|
||||
r72 = 126 * x2
|
||||
r73 = 63 * r9
|
||||
r74 = r73 * x3
|
||||
r75 = r15 * x3 + 15 * r42
|
||||
r76 = 630 * x1
|
||||
r77 = 14 * x3
|
||||
r78 = 21 * r27
|
||||
r79 = 42 * x1
|
||||
r80 = 42 * x2
|
||||
r81 = x1 * y2
|
||||
r82 = 63 * r42
|
||||
r83 = x1 * y1
|
||||
r84 = r41 + r82 + 378 * r83
|
||||
r85 = x2 * x3
|
||||
r86 = r85 * y1
|
||||
r87 = r27 * x3
|
||||
r88 = 27 * r9
|
||||
r89 = r88 * y2
|
||||
r90 = 42 * r14
|
||||
r91 = 90 * x1
|
||||
r92 = 189 * r18
|
||||
r93 = 378 * r18
|
||||
r94 = r12 * y1
|
||||
r95 = 252 * x1 * x2
|
||||
r96 = r79 * x3
|
||||
r97 = 30 * r85
|
||||
r98 = r83 * x3
|
||||
r99 = 30 * x3
|
||||
r100 = 42 * x3
|
||||
r101 = r42 * x1
|
||||
r102 = r10 * y2 + 14 * r14 + 126 * r18 * y1 + r81 * r99
|
||||
r103 = 378 * r48
|
||||
r104 = 18 * y1
|
||||
r105 = r104 * y2
|
||||
r106 = y0 * y1
|
||||
r107 = 252 * y2
|
||||
r108 = r107 * y0
|
||||
r109 = y0 * y3
|
||||
r110 = 42 * r64
|
||||
r111 = 378 * r53
|
||||
r112 = 63 * r48
|
||||
r113 = 27 * x2
|
||||
r114 = r27 * y2
|
||||
r115 = r113 * r48 + 42 * r52
|
||||
r116 = x3 * y3
|
||||
r117 = 54 * r42
|
||||
r118 = r51 * x1
|
||||
r119 = r51 * x2
|
||||
r120 = r48 * x1
|
||||
r121 = 21 * x3
|
||||
r122 = r64 * x1
|
||||
r123 = r81 * y3
|
||||
r124 = 30 * r27 * y1 + r49 * x2 + 14 * r52 + 126 * r53 * x1
|
||||
r125 = y2**3
|
||||
r126 = y3**3
|
||||
r127 = y1**3
|
||||
r128 = y0**3
|
||||
r129 = r51 * y2
|
||||
r130 = r112 * y3 + r21 * r51
|
||||
r131 = 189 * r53
|
||||
r132 = 90 * y2
|
||||
|
||||
self.area += (
|
||||
-r1 / 20
|
||||
- r3 / 20
|
||||
- r4 * (x2 + x3) / 20
|
||||
+ x0 * (r7 + r8 + 10 * y0 + y3) / 20
|
||||
+ 3 * x1 * (y2 + y3) / 20
|
||||
+ 3 * x2 * y3 / 10
|
||||
- y0 * (r5 + r6 + x3) / 20
|
||||
)
|
||||
self.momentX += (
|
||||
r11 / 840
|
||||
- r13 / 8
|
||||
- r14 / 3
|
||||
- r17 * (-r15 + r8) / 840
|
||||
+ r19 * (r8 + 2 * y3) / 840
|
||||
+ r20 * (r0 + r21 + 56 * y0 + y3) / 168
|
||||
+ r29 * (-r23 + r25 + r28) / 840
|
||||
- r4 * (10 * r12 + r17 + r22) / 840
|
||||
+ x0
|
||||
* (
|
||||
12 * r27
|
||||
+ r30 * y2
|
||||
+ r34
|
||||
- r35 * x1
|
||||
- r37
|
||||
- r38 * y0
|
||||
+ r39 * x1
|
||||
- r4 * x3
|
||||
+ r45
|
||||
)
|
||||
/ 840
|
||||
- y0 * (r17 + r30 * x2 + r31 * x1 + r32 + r33 + 18 * r9) / 840
|
||||
)
|
||||
self.momentY += (
|
||||
-r4 * (r25 + r58) / 840
|
||||
- r47 / 8
|
||||
- r50 / 840
|
||||
- r52 / 6
|
||||
- r54 * (r6 + 2 * x3) / 840
|
||||
- r55 * (r56 + r57 + x3) / 168
|
||||
+ x0
|
||||
* (
|
||||
r35 * y1
|
||||
+ r40 * y0
|
||||
+ r44 * y2
|
||||
+ 18 * r48
|
||||
+ 140 * r55
|
||||
+ r59
|
||||
+ r63
|
||||
+ 12 * r64
|
||||
+ r65
|
||||
+ r66
|
||||
)
|
||||
/ 840
|
||||
+ x1 * (r24 * y1 + 10 * r51 + r59 + r60 + r7 * y3) / 280
|
||||
+ x2 * y3 * (r15 + r8) / 56
|
||||
- y0 * (r16 * y1 + r31 * y2 + r44 * x2 + r45 + r61 - r62 * x1) / 840
|
||||
)
|
||||
self.momentXX += (
|
||||
-r12 * r72 * (-r40 + r8) / 9240
|
||||
+ 3 * r18 * (r28 + r34 - r38 * y1 + r75) / 3080
|
||||
+ r20
|
||||
* (
|
||||
r24 * x3
|
||||
- r72 * y0
|
||||
- r76 * y0
|
||||
- r77 * y0
|
||||
+ r78
|
||||
+ r79 * y3
|
||||
+ r80 * y1
|
||||
+ 210 * r81
|
||||
+ r84
|
||||
)
|
||||
/ 9240
|
||||
- r29
|
||||
* (
|
||||
r12 * r21
|
||||
+ 14 * r13
|
||||
+ r44 * r9
|
||||
- r73 * y3
|
||||
+ 54 * r86
|
||||
- 84 * r87
|
||||
- r89
|
||||
- r90
|
||||
)
|
||||
/ 9240
|
||||
- r4 * (70 * r12 * x2 + 27 * r67 + 42 * r68 + r74) / 9240
|
||||
+ 3 * r67 * y3 / 220
|
||||
- r68 * r69 / 9240
|
||||
- r68 * y3 / 4
|
||||
- r70 * r9 * (-r62 + y2) / 9240
|
||||
+ 3 * r71 * (r24 + r40) / 3080
|
||||
+ x0**3 * (r24 + r44 + 165 * y0 + y3) / 660
|
||||
+ x0
|
||||
* (
|
||||
r100 * r27
|
||||
+ 162 * r101
|
||||
+ r102
|
||||
+ r11
|
||||
+ 63 * r18 * y3
|
||||
+ r27 * r91
|
||||
- r33 * y0
|
||||
- r37 * x3
|
||||
+ r43 * x3
|
||||
- r73 * y0
|
||||
- r88 * y1
|
||||
+ r92 * y2
|
||||
- r93 * y0
|
||||
- 9 * r94
|
||||
- r95 * y0
|
||||
- r96 * y0
|
||||
- r97 * y1
|
||||
- 18 * r98
|
||||
+ r99 * x1 * y3
|
||||
)
|
||||
/ 9240
|
||||
- y0
|
||||
* (
|
||||
r12 * r56
|
||||
+ r12 * r80
|
||||
+ r32 * x3
|
||||
+ 45 * r67
|
||||
+ 14 * r68
|
||||
+ 126 * r71
|
||||
+ r74
|
||||
+ r85 * r91
|
||||
+ 135 * r9 * x1
|
||||
+ r92 * x2
|
||||
)
|
||||
/ 9240
|
||||
)
|
||||
self.momentXY += (
|
||||
-r103 * r12 / 18480
|
||||
- r12 * r51 / 8
|
||||
- 3 * r14 * y2 / 44
|
||||
+ 3 * r18 * (r105 + r2 * y1 + 18 * r46 + 15 * r48 + 7 * r51) / 6160
|
||||
+ r20
|
||||
* (
|
||||
1260 * r106
|
||||
+ r107 * y1
|
||||
+ r108
|
||||
+ 28 * r109
|
||||
+ r110
|
||||
+ r111
|
||||
+ r112
|
||||
+ 30 * r46
|
||||
+ 2310 * r55
|
||||
+ r66
|
||||
)
|
||||
/ 18480
|
||||
- r54 * (7 * r12 + 18 * r85 + 15 * r9) / 18480
|
||||
- r55 * (r33 + r73 + r93 + r95 + r96 + r97) / 18480
|
||||
- r7 * (42 * r13 + r82 * x3 + 28 * r87 + r89 + r90) / 18480
|
||||
- 3 * r85 * (r48 - r66) / 220
|
||||
+ 3 * r9 * y3 * (r62 + 2 * y2) / 440
|
||||
+ x0
|
||||
* (
|
||||
-r1 * y0
|
||||
- 84 * r106 * x2
|
||||
+ r109 * r56
|
||||
+ 54 * r114
|
||||
+ r117 * y1
|
||||
+ 15 * r118
|
||||
+ 21 * r119
|
||||
+ 81 * r120
|
||||
+ r121 * r46
|
||||
+ 54 * r122
|
||||
+ 60 * r123
|
||||
+ r124
|
||||
- r21 * x3 * y0
|
||||
+ r23 * y3
|
||||
- r54 * x3
|
||||
- r55 * r72
|
||||
- r55 * r76
|
||||
- r55 * r77
|
||||
+ r57 * y0 * y3
|
||||
+ r60 * x3
|
||||
+ 84 * r81 * y0
|
||||
+ 189 * r81 * y1
|
||||
)
|
||||
/ 9240
|
||||
+ x1
|
||||
* (
|
||||
r104 * r27
|
||||
- r105 * x3
|
||||
- r113 * r53
|
||||
+ 63 * r114
|
||||
+ r115
|
||||
- r16 * r53
|
||||
+ 28 * r47
|
||||
+ r51 * r80
|
||||
)
|
||||
/ 3080
|
||||
- y0
|
||||
* (
|
||||
54 * r101
|
||||
+ r102
|
||||
+ r116 * r5
|
||||
+ r117 * x3
|
||||
+ 21 * r13
|
||||
- r19 * y3
|
||||
+ r22 * y3
|
||||
+ r78 * x3
|
||||
+ 189 * r83 * x2
|
||||
+ 60 * r86
|
||||
+ 81 * r9 * y1
|
||||
+ 15 * r94
|
||||
+ 54 * r98
|
||||
)
|
||||
/ 9240
|
||||
)
|
||||
self.momentYY += (
|
||||
-r103 * r116 / 9240
|
||||
- r125 * r70 / 9240
|
||||
- r126 * x3 / 12
|
||||
- 3 * r127 * (r26 + r38) / 3080
|
||||
- r128 * (r26 + r30 + x3) / 660
|
||||
- r4 * (r112 * x3 + r115 - 14 * r119 + 84 * r47) / 9240
|
||||
- r52 * r69 / 9240
|
||||
- r54 * (r58 + r61 + r75) / 9240
|
||||
- r55
|
||||
* (r100 * y1 + r121 * y2 + r26 * y3 + r79 * y2 + r84 + 210 * x2 * y1)
|
||||
/ 9240
|
||||
+ x0
|
||||
* (
|
||||
r108 * y1
|
||||
+ r110 * y0
|
||||
+ r111 * y0
|
||||
+ r112 * y0
|
||||
+ 45 * r125
|
||||
+ 14 * r126
|
||||
+ 126 * r127
|
||||
+ 770 * r128
|
||||
+ 42 * r129
|
||||
+ r130
|
||||
+ r131 * y2
|
||||
+ r132 * r64
|
||||
+ 135 * r48 * y1
|
||||
+ 630 * r55 * y1
|
||||
+ 126 * r55 * y2
|
||||
+ 14 * r55 * y3
|
||||
+ r63 * y3
|
||||
+ r65 * y3
|
||||
+ r66 * y0
|
||||
)
|
||||
/ 9240
|
||||
+ x1
|
||||
* (
|
||||
27 * r125
|
||||
+ 42 * r126
|
||||
+ 70 * r129
|
||||
+ r130
|
||||
+ r39 * r53
|
||||
+ r44 * r48
|
||||
+ 27 * r53 * y2
|
||||
+ 54 * r64 * y2
|
||||
)
|
||||
/ 3080
|
||||
+ 3 * x2 * y3 * (r48 + r66 + r8 * y3) / 220
|
||||
- y0
|
||||
* (
|
||||
r100 * r46
|
||||
+ 18 * r114
|
||||
- 9 * r118
|
||||
- 27 * r120
|
||||
- 18 * r122
|
||||
- 30 * r123
|
||||
+ r124
|
||||
+ r131 * x2
|
||||
+ r132 * x3 * y1
|
||||
+ 162 * r42 * y1
|
||||
+ r50
|
||||
+ 63 * r53 * x3
|
||||
+ r64 * r99
|
||||
)
|
||||
/ 9240
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from fontTools.misc.symfont import x, y, printGreenPen
|
||||
|
||||
printGreenPen(
|
||||
"MomentsPen",
|
||||
[
|
||||
("area", 1),
|
||||
("momentX", x),
|
||||
("momentY", y),
|
||||
("momentXX", x**2),
|
||||
("momentXY", x * y),
|
||||
("momentYY", y**2),
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Calculate the perimeter of a glyph."""
|
||||
|
||||
from fontTools.pens.basePen import BasePen
|
||||
from fontTools.misc.bezierTools import (
|
||||
approximateQuadraticArcLengthC,
|
||||
calcQuadraticArcLengthC,
|
||||
approximateCubicArcLengthC,
|
||||
calcCubicArcLengthC,
|
||||
)
|
||||
import math
|
||||
|
||||
|
||||
__all__ = ["PerimeterPen"]
|
||||
|
||||
|
||||
def _distance(p0, p1):
|
||||
return math.hypot(p0[0] - p1[0], p0[1] - p1[1])
|
||||
|
||||
|
||||
class PerimeterPen(BasePen):
|
||||
def __init__(self, glyphset=None, tolerance=0.005):
|
||||
BasePen.__init__(self, glyphset)
|
||||
self.value = 0
|
||||
self.tolerance = tolerance
|
||||
|
||||
# Choose which algorithm to use for quadratic and for cubic.
|
||||
# Quadrature is faster but has fixed error characteristic with no strong
|
||||
# error bound. The cutoff points are derived empirically.
|
||||
self._addCubic = (
|
||||
self._addCubicQuadrature if tolerance >= 0.0015 else self._addCubicRecursive
|
||||
)
|
||||
self._addQuadratic = (
|
||||
self._addQuadraticQuadrature
|
||||
if tolerance >= 0.00075
|
||||
else self._addQuadraticExact
|
||||
)
|
||||
|
||||
def _moveTo(self, p0):
|
||||
self.__startPoint = p0
|
||||
|
||||
def _closePath(self):
|
||||
p0 = self._getCurrentPoint()
|
||||
if p0 != self.__startPoint:
|
||||
self._lineTo(self.__startPoint)
|
||||
|
||||
def _lineTo(self, p1):
|
||||
p0 = self._getCurrentPoint()
|
||||
self.value += _distance(p0, p1)
|
||||
|
||||
def _addQuadraticExact(self, c0, c1, c2):
|
||||
self.value += calcQuadraticArcLengthC(c0, c1, c2)
|
||||
|
||||
def _addQuadraticQuadrature(self, c0, c1, c2):
|
||||
self.value += approximateQuadraticArcLengthC(c0, c1, c2)
|
||||
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
p0 = self._getCurrentPoint()
|
||||
self._addQuadratic(complex(*p0), complex(*p1), complex(*p2))
|
||||
|
||||
def _addCubicRecursive(self, c0, c1, c2, c3):
|
||||
self.value += calcCubicArcLengthC(c0, c1, c2, c3, self.tolerance)
|
||||
|
||||
def _addCubicQuadrature(self, c0, c1, c2, c3):
|
||||
self.value += approximateCubicArcLengthC(c0, c1, c2, c3)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
p0 = self._getCurrentPoint()
|
||||
self._addCubic(complex(*p0), complex(*p1), complex(*p2), complex(*p3))
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
"""fontTools.pens.pointInsidePen -- Pen implementing "point inside" testing
|
||||
for shapes.
|
||||
"""
|
||||
|
||||
from fontTools.pens.basePen import BasePen
|
||||
from fontTools.misc.bezierTools import solveQuadratic, solveCubic
|
||||
|
||||
|
||||
__all__ = ["PointInsidePen"]
|
||||
|
||||
|
||||
class PointInsidePen(BasePen):
|
||||
"""This pen implements "point inside" testing: to test whether
|
||||
a given point lies inside the shape (black) or outside (white).
|
||||
Instances of this class can be recycled, as long as the
|
||||
setTestPoint() method is used to set the new point to test.
|
||||
|
||||
:Example:
|
||||
.. code-block::
|
||||
|
||||
pen = PointInsidePen(glyphSet, (100, 200))
|
||||
outline.draw(pen)
|
||||
isInside = pen.getResult()
|
||||
|
||||
Both the even-odd algorithm and the non-zero-winding-rule
|
||||
algorithm are implemented. The latter is the default, specify
|
||||
True for the evenOdd argument of __init__ or setTestPoint
|
||||
to use the even-odd algorithm.
|
||||
"""
|
||||
|
||||
# This class implements the classical "shoot a ray from the test point
|
||||
# to infinity and count how many times it intersects the outline" (as well
|
||||
# as the non-zero variant, where the counter is incremented if the outline
|
||||
# intersects the ray in one direction and decremented if it intersects in
|
||||
# the other direction).
|
||||
# I found an amazingly clear explanation of the subtleties involved in
|
||||
# implementing this correctly for polygons here:
|
||||
# http://graphics.cs.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html
|
||||
# I extended the principles outlined on that page to curves.
|
||||
|
||||
def __init__(self, glyphSet, testPoint, evenOdd=False):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
self.setTestPoint(testPoint, evenOdd)
|
||||
|
||||
def setTestPoint(self, testPoint, evenOdd=False):
|
||||
"""Set the point to test. Call this _before_ the outline gets drawn."""
|
||||
self.testPoint = testPoint
|
||||
self.evenOdd = evenOdd
|
||||
self.firstPoint = None
|
||||
self.intersectionCount = 0
|
||||
|
||||
def getWinding(self):
|
||||
if self.firstPoint is not None:
|
||||
# always make sure the sub paths are closed; the algorithm only works
|
||||
# for closed paths.
|
||||
self.closePath()
|
||||
return self.intersectionCount
|
||||
|
||||
def getResult(self):
|
||||
"""After the shape has been drawn, getResult() returns True if the test
|
||||
point lies within the (black) shape, and False if it doesn't.
|
||||
"""
|
||||
winding = self.getWinding()
|
||||
if self.evenOdd:
|
||||
result = winding % 2
|
||||
else: # non-zero
|
||||
result = self.intersectionCount != 0
|
||||
return not not result
|
||||
|
||||
def _addIntersection(self, goingUp):
|
||||
if self.evenOdd or goingUp:
|
||||
self.intersectionCount += 1
|
||||
else:
|
||||
self.intersectionCount -= 1
|
||||
|
||||
def _moveTo(self, point):
|
||||
if self.firstPoint is not None:
|
||||
# always make sure the sub paths are closed; the algorithm only works
|
||||
# for closed paths.
|
||||
self.closePath()
|
||||
self.firstPoint = point
|
||||
|
||||
def _lineTo(self, point):
|
||||
x, y = self.testPoint
|
||||
x1, y1 = self._getCurrentPoint()
|
||||
x2, y2 = point
|
||||
|
||||
if x1 < x and x2 < x:
|
||||
return
|
||||
if y1 < y and y2 < y:
|
||||
return
|
||||
if y1 >= y and y2 >= y:
|
||||
return
|
||||
|
||||
dx = x2 - x1
|
||||
dy = y2 - y1
|
||||
t = (y - y1) / dy
|
||||
ix = dx * t + x1
|
||||
if ix < x:
|
||||
return
|
||||
self._addIntersection(y2 > y1)
|
||||
|
||||
def _curveToOne(self, bcp1, bcp2, point):
|
||||
x, y = self.testPoint
|
||||
x1, y1 = self._getCurrentPoint()
|
||||
x2, y2 = bcp1
|
||||
x3, y3 = bcp2
|
||||
x4, y4 = point
|
||||
|
||||
if x1 < x and x2 < x and x3 < x and x4 < x:
|
||||
return
|
||||
if y1 < y and y2 < y and y3 < y and y4 < y:
|
||||
return
|
||||
if y1 >= y and y2 >= y and y3 >= y and y4 >= y:
|
||||
return
|
||||
|
||||
dy = y1
|
||||
cy = (y2 - dy) * 3.0
|
||||
by = (y3 - y2) * 3.0 - cy
|
||||
ay = y4 - dy - cy - by
|
||||
solutions = sorted(solveCubic(ay, by, cy, dy - y))
|
||||
solutions = [t for t in solutions if -0.0 <= t <= 1.0]
|
||||
if not solutions:
|
||||
return
|
||||
|
||||
dx = x1
|
||||
cx = (x2 - dx) * 3.0
|
||||
bx = (x3 - x2) * 3.0 - cx
|
||||
ax = x4 - dx - cx - bx
|
||||
|
||||
above = y1 >= y
|
||||
lastT = None
|
||||
for t in solutions:
|
||||
if t == lastT:
|
||||
continue
|
||||
lastT = t
|
||||
t2 = t * t
|
||||
t3 = t2 * t
|
||||
|
||||
direction = 3 * ay * t2 + 2 * by * t + cy
|
||||
incomingGoingUp = outgoingGoingUp = direction > 0.0
|
||||
if direction == 0.0:
|
||||
direction = 6 * ay * t + 2 * by
|
||||
outgoingGoingUp = direction > 0.0
|
||||
incomingGoingUp = not outgoingGoingUp
|
||||
if direction == 0.0:
|
||||
direction = ay
|
||||
incomingGoingUp = outgoingGoingUp = direction > 0.0
|
||||
|
||||
xt = ax * t3 + bx * t2 + cx * t + dx
|
||||
if xt < x:
|
||||
continue
|
||||
|
||||
if t in (0.0, -0.0):
|
||||
if not outgoingGoingUp:
|
||||
self._addIntersection(outgoingGoingUp)
|
||||
elif t == 1.0:
|
||||
if incomingGoingUp:
|
||||
self._addIntersection(incomingGoingUp)
|
||||
else:
|
||||
if incomingGoingUp == outgoingGoingUp:
|
||||
self._addIntersection(outgoingGoingUp)
|
||||
# else:
|
||||
# we're not really intersecting, merely touching
|
||||
|
||||
def _qCurveToOne_unfinished(self, bcp, point):
|
||||
# XXX need to finish this, for now doing it through a cubic
|
||||
# (BasePen implements _qCurveTo in terms of a cubic) will
|
||||
# have to do.
|
||||
x, y = self.testPoint
|
||||
x1, y1 = self._getCurrentPoint()
|
||||
x2, y2 = bcp
|
||||
x3, y3 = point
|
||||
c = y1
|
||||
b = (y2 - c) * 2.0
|
||||
a = y3 - c - b
|
||||
solutions = sorted(solveQuadratic(a, b, c - y))
|
||||
solutions = [
|
||||
t for t in solutions if ZERO_MINUS_EPSILON <= t <= ONE_PLUS_EPSILON
|
||||
]
|
||||
if not solutions:
|
||||
return
|
||||
# XXX
|
||||
|
||||
def _closePath(self):
|
||||
if self._getCurrentPoint() != self.firstPoint:
|
||||
self.lineTo(self.firstPoint)
|
||||
self.firstPoint = None
|
||||
|
||||
def _endPath(self):
|
||||
"""Insideness is not defined for open contours."""
|
||||
raise NotImplementedError
|
||||
609
venv/lib/python3.13/site-packages/fontTools/pens/pointPen.py
Normal file
609
venv/lib/python3.13/site-packages/fontTools/pens/pointPen.py
Normal file
|
|
@ -0,0 +1,609 @@
|
|||
"""
|
||||
=========
|
||||
PointPens
|
||||
=========
|
||||
|
||||
Where **SegmentPens** have an intuitive approach to drawing
|
||||
(if you're familiar with postscript anyway), the **PointPen**
|
||||
is geared towards accessing all the data in the contours of
|
||||
the glyph. A PointPen has a very simple interface, it just
|
||||
steps through all the points in a call from glyph.drawPoints().
|
||||
This allows the caller to provide more data for each point.
|
||||
For instance, whether or not a point is smooth, and its name.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from fontTools.misc.loggingTools import LogMixin
|
||||
from fontTools.misc.transform import DecomposedTransform, Identity
|
||||
from fontTools.pens.basePen import AbstractPen, MissingComponentError, PenError
|
||||
|
||||
__all__ = [
|
||||
"AbstractPointPen",
|
||||
"BasePointToSegmentPen",
|
||||
"PointToSegmentPen",
|
||||
"SegmentToPointPen",
|
||||
"GuessSmoothPointPen",
|
||||
"ReverseContourPointPen",
|
||||
]
|
||||
|
||||
# Some type aliases to make it easier below
|
||||
Point = Tuple[float, float]
|
||||
PointName = Optional[str]
|
||||
# [(pt, smooth, name, kwargs)]
|
||||
SegmentPointList = List[Tuple[Optional[Point], bool, PointName, Any]]
|
||||
SegmentType = Optional[str]
|
||||
SegmentList = List[Tuple[SegmentType, SegmentPointList]]
|
||||
|
||||
|
||||
class AbstractPointPen:
|
||||
"""Baseclass for all PointPens."""
|
||||
|
||||
def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None:
|
||||
"""Start a new sub path."""
|
||||
raise NotImplementedError
|
||||
|
||||
def endPath(self) -> None:
|
||||
"""End the current sub path."""
|
||||
raise NotImplementedError
|
||||
|
||||
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."""
|
||||
raise NotImplementedError
|
||||
|
||||
def addComponent(
|
||||
self,
|
||||
baseGlyphName: str,
|
||||
transformation: Tuple[float, float, float, float, float, float],
|
||||
identifier: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Add a sub glyph."""
|
||||
raise NotImplementedError
|
||||
|
||||
def addVarComponent(
|
||||
self,
|
||||
glyphName: str,
|
||||
transformation: DecomposedTransform,
|
||||
location: Dict[str, float],
|
||||
identifier: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Add a VarComponent sub glyph. The 'transformation' argument
|
||||
must be a DecomposedTransform from the fontTools.misc.transform module,
|
||||
and the 'location' argument must be a dictionary mapping axis tags
|
||||
to their locations.
|
||||
"""
|
||||
# ttGlyphSet decomposes for us
|
||||
raise AttributeError
|
||||
|
||||
|
||||
class BasePointToSegmentPen(AbstractPointPen):
|
||||
"""
|
||||
Base class for retrieving the outline in a segment-oriented
|
||||
way. The PointPen protocol is simple yet also a little tricky,
|
||||
so when you need an outline presented as segments but you have
|
||||
as points, do use this base implementation as it properly takes
|
||||
care of all the edge cases.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.currentPath = None
|
||||
|
||||
def beginPath(self, identifier=None, **kwargs):
|
||||
if self.currentPath is not None:
|
||||
raise PenError("Path already begun.")
|
||||
self.currentPath = []
|
||||
|
||||
def _flushContour(self, segments: SegmentList) -> None:
|
||||
"""Override this method.
|
||||
|
||||
It will be called for each non-empty sub path with a list
|
||||
of segments: the 'segments' argument.
|
||||
|
||||
The segments list contains tuples of length 2:
|
||||
(segmentType, points)
|
||||
|
||||
segmentType is one of "move", "line", "curve" or "qcurve".
|
||||
"move" may only occur as the first segment, and it signifies
|
||||
an OPEN path. A CLOSED path does NOT start with a "move", in
|
||||
fact it will not contain a "move" at ALL.
|
||||
|
||||
The 'points' field in the 2-tuple is a list of point info
|
||||
tuples. The list has 1 or more items, a point tuple has
|
||||
four items:
|
||||
(point, smooth, name, kwargs)
|
||||
'point' is an (x, y) coordinate pair.
|
||||
|
||||
For a closed path, the initial moveTo point is defined as
|
||||
the last point of the last segment.
|
||||
|
||||
The 'points' list of "move" and "line" segments always contains
|
||||
exactly one point tuple.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def endPath(self) -> None:
|
||||
if self.currentPath is None:
|
||||
raise PenError("Path not begun.")
|
||||
points = self.currentPath
|
||||
self.currentPath = None
|
||||
if not points:
|
||||
return
|
||||
if len(points) == 1:
|
||||
# Not much more we can do than output a single move segment.
|
||||
pt, segmentType, smooth, name, kwargs = points[0]
|
||||
segments: SegmentList = [("move", [(pt, smooth, name, kwargs)])]
|
||||
self._flushContour(segments)
|
||||
return
|
||||
segments = []
|
||||
if points[0][1] == "move":
|
||||
# It's an open contour, insert a "move" segment for the first
|
||||
# point and remove that first point from the point list.
|
||||
pt, segmentType, smooth, name, kwargs = points[0]
|
||||
segments.append(("move", [(pt, smooth, name, kwargs)]))
|
||||
points.pop(0)
|
||||
else:
|
||||
# It's a closed contour. Locate the first on-curve point, and
|
||||
# rotate the point list so that it _ends_ with an on-curve
|
||||
# point.
|
||||
firstOnCurve = None
|
||||
for i in range(len(points)):
|
||||
segmentType = points[i][1]
|
||||
if segmentType is not None:
|
||||
firstOnCurve = i
|
||||
break
|
||||
if firstOnCurve is None:
|
||||
# Special case for quadratics: a contour with no on-curve
|
||||
# points. Add a "None" point. (See also the Pen protocol's
|
||||
# qCurveTo() method and fontTools.pens.basePen.py.)
|
||||
points.append((None, "qcurve", None, None, None))
|
||||
else:
|
||||
points = points[firstOnCurve + 1 :] + points[: firstOnCurve + 1]
|
||||
|
||||
currentSegment: SegmentPointList = []
|
||||
for pt, segmentType, smooth, name, kwargs in points:
|
||||
currentSegment.append((pt, smooth, name, kwargs))
|
||||
if segmentType is None:
|
||||
continue
|
||||
segments.append((segmentType, currentSegment))
|
||||
currentSegment = []
|
||||
|
||||
self._flushContour(segments)
|
||||
|
||||
def addPoint(
|
||||
self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
|
||||
):
|
||||
if self.currentPath is None:
|
||||
raise PenError("Path not begun")
|
||||
self.currentPath.append((pt, segmentType, smooth, name, kwargs))
|
||||
|
||||
|
||||
class PointToSegmentPen(BasePointToSegmentPen):
|
||||
"""
|
||||
Adapter class that converts the PointPen protocol to the
|
||||
(Segment)Pen protocol.
|
||||
|
||||
NOTE: The segment pen does not support and will drop point names, identifiers
|
||||
and kwargs.
|
||||
"""
|
||||
|
||||
def __init__(self, segmentPen, outputImpliedClosingLine: bool = False) -> None:
|
||||
BasePointToSegmentPen.__init__(self)
|
||||
self.pen = segmentPen
|
||||
self.outputImpliedClosingLine = outputImpliedClosingLine
|
||||
|
||||
def _flushContour(self, segments):
|
||||
if not segments:
|
||||
raise PenError("Must have at least one segment.")
|
||||
pen = self.pen
|
||||
if segments[0][0] == "move":
|
||||
# It's an open path.
|
||||
closed = False
|
||||
points = segments[0][1]
|
||||
if len(points) != 1:
|
||||
raise PenError(f"Illegal move segment point count: {len(points)}")
|
||||
movePt, _, _, _ = points[0]
|
||||
del segments[0]
|
||||
else:
|
||||
# It's a closed path, do a moveTo to the last
|
||||
# point of the last segment.
|
||||
closed = True
|
||||
segmentType, points = segments[-1]
|
||||
movePt, _, _, _ = points[-1]
|
||||
if movePt is None:
|
||||
# quad special case: a contour with no on-curve points contains
|
||||
# one "qcurve" segment that ends with a point that's None. We
|
||||
# must not output a moveTo() in that case.
|
||||
pass
|
||||
else:
|
||||
pen.moveTo(movePt)
|
||||
outputImpliedClosingLine = self.outputImpliedClosingLine
|
||||
nSegments = len(segments)
|
||||
lastPt = movePt
|
||||
for i in range(nSegments):
|
||||
segmentType, points = segments[i]
|
||||
points = [pt for pt, _, _, _ in points]
|
||||
if segmentType == "line":
|
||||
if len(points) != 1:
|
||||
raise PenError(f"Illegal line segment point count: {len(points)}")
|
||||
pt = points[0]
|
||||
# For closed contours, a 'lineTo' is always implied from the last oncurve
|
||||
# point to the starting point, thus we can omit it when the last and
|
||||
# starting point don't overlap.
|
||||
# However, when the last oncurve point is a "line" segment and has same
|
||||
# coordinates as the starting point of a closed contour, we need to output
|
||||
# the closing 'lineTo' explicitly (regardless of the value of the
|
||||
# 'outputImpliedClosingLine' option) in order to disambiguate this case from
|
||||
# the implied closing 'lineTo', otherwise the duplicate point would be lost.
|
||||
# See https://github.com/googlefonts/fontmake/issues/572.
|
||||
if (
|
||||
i + 1 != nSegments
|
||||
or outputImpliedClosingLine
|
||||
or not closed
|
||||
or pt == lastPt
|
||||
):
|
||||
pen.lineTo(pt)
|
||||
lastPt = pt
|
||||
elif segmentType == "curve":
|
||||
pen.curveTo(*points)
|
||||
lastPt = points[-1]
|
||||
elif segmentType == "qcurve":
|
||||
pen.qCurveTo(*points)
|
||||
lastPt = points[-1]
|
||||
else:
|
||||
raise PenError(f"Illegal segmentType: {segmentType}")
|
||||
if closed:
|
||||
pen.closePath()
|
||||
else:
|
||||
pen.endPath()
|
||||
|
||||
def addComponent(self, glyphName, transform, identifier=None, **kwargs):
|
||||
del identifier # unused
|
||||
del kwargs # unused
|
||||
self.pen.addComponent(glyphName, transform)
|
||||
|
||||
|
||||
class SegmentToPointPen(AbstractPen):
|
||||
"""
|
||||
Adapter class that converts the (Segment)Pen protocol to the
|
||||
PointPen protocol.
|
||||
"""
|
||||
|
||||
def __init__(self, pointPen, guessSmooth=True) -> None:
|
||||
if guessSmooth:
|
||||
self.pen = GuessSmoothPointPen(pointPen)
|
||||
else:
|
||||
self.pen = pointPen
|
||||
self.contour: Optional[List[Tuple[Point, SegmentType]]] = None
|
||||
|
||||
def _flushContour(self) -> None:
|
||||
pen = self.pen
|
||||
pen.beginPath()
|
||||
for pt, segmentType in self.contour:
|
||||
pen.addPoint(pt, segmentType=segmentType)
|
||||
pen.endPath()
|
||||
|
||||
def moveTo(self, pt):
|
||||
self.contour = []
|
||||
self.contour.append((pt, "move"))
|
||||
|
||||
def lineTo(self, pt):
|
||||
if self.contour is None:
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
self.contour.append((pt, "line"))
|
||||
|
||||
def curveTo(self, *pts):
|
||||
if not pts:
|
||||
raise TypeError("Must pass in at least one point")
|
||||
if self.contour is None:
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
for pt in pts[:-1]:
|
||||
self.contour.append((pt, None))
|
||||
self.contour.append((pts[-1], "curve"))
|
||||
|
||||
def qCurveTo(self, *pts):
|
||||
if not pts:
|
||||
raise TypeError("Must pass in at least one point")
|
||||
if pts[-1] is None:
|
||||
self.contour = []
|
||||
else:
|
||||
if self.contour is None:
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
for pt in pts[:-1]:
|
||||
self.contour.append((pt, None))
|
||||
if pts[-1] is not None:
|
||||
self.contour.append((pts[-1], "qcurve"))
|
||||
|
||||
def closePath(self):
|
||||
if self.contour is None:
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
if len(self.contour) > 1 and self.contour[0][0] == self.contour[-1][0]:
|
||||
self.contour[0] = self.contour[-1]
|
||||
del self.contour[-1]
|
||||
else:
|
||||
# There's an implied line at the end, replace "move" with "line"
|
||||
# for the first point
|
||||
pt, tp = self.contour[0]
|
||||
if tp == "move":
|
||||
self.contour[0] = pt, "line"
|
||||
self._flushContour()
|
||||
self.contour = None
|
||||
|
||||
def endPath(self):
|
||||
if self.contour is None:
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
self._flushContour()
|
||||
self.contour = None
|
||||
|
||||
def addComponent(self, glyphName, transform):
|
||||
if self.contour is not None:
|
||||
raise PenError("Components must be added before or after contours")
|
||||
self.pen.addComponent(glyphName, transform)
|
||||
|
||||
|
||||
class GuessSmoothPointPen(AbstractPointPen):
|
||||
"""
|
||||
Filtering PointPen that tries to determine whether an on-curve point
|
||||
should be "smooth", ie. that it's a "tangent" point or a "curve" point.
|
||||
"""
|
||||
|
||||
def __init__(self, outPen, error=0.05):
|
||||
self._outPen = outPen
|
||||
self._error = error
|
||||
self._points = None
|
||||
|
||||
def _flushContour(self):
|
||||
if self._points is None:
|
||||
raise PenError("Path not begun")
|
||||
points = self._points
|
||||
nPoints = len(points)
|
||||
if not nPoints:
|
||||
return
|
||||
if points[0][1] == "move":
|
||||
# Open path.
|
||||
indices = range(1, nPoints - 1)
|
||||
elif nPoints > 1:
|
||||
# Closed path. To avoid having to mod the contour index, we
|
||||
# simply abuse Python's negative index feature, and start at -1
|
||||
indices = range(-1, nPoints - 1)
|
||||
else:
|
||||
# closed path containing 1 point (!), ignore.
|
||||
indices = []
|
||||
for i in indices:
|
||||
pt, segmentType, _, name, kwargs = points[i]
|
||||
if segmentType is None:
|
||||
continue
|
||||
prev = i - 1
|
||||
next = i + 1
|
||||
if points[prev][1] is not None and points[next][1] is not None:
|
||||
continue
|
||||
# At least one of our neighbors is an off-curve point
|
||||
pt = points[i][0]
|
||||
prevPt = points[prev][0]
|
||||
nextPt = points[next][0]
|
||||
if pt != prevPt and pt != nextPt:
|
||||
dx1, dy1 = pt[0] - prevPt[0], pt[1] - prevPt[1]
|
||||
dx2, dy2 = nextPt[0] - pt[0], nextPt[1] - pt[1]
|
||||
a1 = math.atan2(dy1, dx1)
|
||||
a2 = math.atan2(dy2, dx2)
|
||||
if abs(a1 - a2) < self._error:
|
||||
points[i] = pt, segmentType, True, name, kwargs
|
||||
|
||||
for pt, segmentType, smooth, name, kwargs in points:
|
||||
self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
|
||||
|
||||
def beginPath(self, identifier=None, **kwargs):
|
||||
if self._points is not None:
|
||||
raise PenError("Path already begun")
|
||||
self._points = []
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self._outPen.beginPath(**kwargs)
|
||||
|
||||
def endPath(self):
|
||||
self._flushContour()
|
||||
self._outPen.endPath()
|
||||
self._points = None
|
||||
|
||||
def addPoint(
|
||||
self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
|
||||
):
|
||||
if self._points is None:
|
||||
raise PenError("Path not begun")
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self._points.append((pt, segmentType, False, name, kwargs))
|
||||
|
||||
def addComponent(self, glyphName, transformation, identifier=None, **kwargs):
|
||||
if self._points is not None:
|
||||
raise PenError("Components must be added before or after contours")
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self._outPen.addComponent(glyphName, transformation, **kwargs)
|
||||
|
||||
def addVarComponent(
|
||||
self, glyphName, transformation, location, identifier=None, **kwargs
|
||||
):
|
||||
if self._points is not None:
|
||||
raise PenError("VarComponents must be added before or after contours")
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self._outPen.addVarComponent(glyphName, transformation, location, **kwargs)
|
||||
|
||||
|
||||
class ReverseContourPointPen(AbstractPointPen):
|
||||
"""
|
||||
This is a PointPen that passes outline data to another PointPen, but
|
||||
reversing the winding direction of all contours. Components are simply
|
||||
passed through unchanged.
|
||||
|
||||
Closed contours are reversed in such a way that the first point remains
|
||||
the first point.
|
||||
"""
|
||||
|
||||
def __init__(self, outputPointPen):
|
||||
self.pen = outputPointPen
|
||||
# a place to store the points for the current sub path
|
||||
self.currentContour = None
|
||||
|
||||
def _flushContour(self):
|
||||
pen = self.pen
|
||||
contour = self.currentContour
|
||||
if not contour:
|
||||
pen.beginPath(identifier=self.currentContourIdentifier)
|
||||
pen.endPath()
|
||||
return
|
||||
|
||||
closed = contour[0][1] != "move"
|
||||
if not closed:
|
||||
lastSegmentType = "move"
|
||||
else:
|
||||
# Remove the first point and insert it at the end. When
|
||||
# the list of points gets reversed, this point will then
|
||||
# again be at the start. In other words, the following
|
||||
# will hold:
|
||||
# for N in range(len(originalContour)):
|
||||
# originalContour[N] == reversedContour[-N]
|
||||
contour.append(contour.pop(0))
|
||||
# Find the first on-curve point.
|
||||
firstOnCurve = None
|
||||
for i in range(len(contour)):
|
||||
if contour[i][1] is not None:
|
||||
firstOnCurve = i
|
||||
break
|
||||
if firstOnCurve is None:
|
||||
# There are no on-curve points, be basically have to
|
||||
# do nothing but contour.reverse().
|
||||
lastSegmentType = None
|
||||
else:
|
||||
lastSegmentType = contour[firstOnCurve][1]
|
||||
|
||||
contour.reverse()
|
||||
if not closed:
|
||||
# Open paths must start with a move, so we simply dump
|
||||
# all off-curve points leading up to the first on-curve.
|
||||
while contour[0][1] is None:
|
||||
contour.pop(0)
|
||||
pen.beginPath(identifier=self.currentContourIdentifier)
|
||||
for pt, nextSegmentType, smooth, name, kwargs in contour:
|
||||
if nextSegmentType is not None:
|
||||
segmentType = lastSegmentType
|
||||
lastSegmentType = nextSegmentType
|
||||
else:
|
||||
segmentType = None
|
||||
pen.addPoint(
|
||||
pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs
|
||||
)
|
||||
pen.endPath()
|
||||
|
||||
def beginPath(self, identifier=None, **kwargs):
|
||||
if self.currentContour is not None:
|
||||
raise PenError("Path already begun")
|
||||
self.currentContour = []
|
||||
self.currentContourIdentifier = identifier
|
||||
self.onCurve = []
|
||||
|
||||
def endPath(self):
|
||||
if self.currentContour is None:
|
||||
raise PenError("Path not begun")
|
||||
self._flushContour()
|
||||
self.currentContour = None
|
||||
|
||||
def addPoint(
|
||||
self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
|
||||
):
|
||||
if self.currentContour is None:
|
||||
raise PenError("Path not begun")
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self.currentContour.append((pt, segmentType, smooth, name, kwargs))
|
||||
|
||||
def addComponent(self, glyphName, transform, identifier=None, **kwargs):
|
||||
if self.currentContour is not None:
|
||||
raise PenError("Components must be added before or after contours")
|
||||
self.pen.addComponent(glyphName, transform, identifier=identifier, **kwargs)
|
||||
|
||||
|
||||
class DecomposingPointPen(LogMixin, AbstractPointPen):
|
||||
"""Implements a 'addComponent' method that decomposes components
|
||||
(i.e. draws them onto self as simple contours).
|
||||
It can also be used as a mixin class (e.g. see DecomposingRecordingPointPen).
|
||||
|
||||
You must override beginPath, addPoint, endPath. You may
|
||||
additionally override addVarComponent and addComponent.
|
||||
|
||||
By default a warning message is logged when a base glyph is missing;
|
||||
set the class variable ``skipMissingComponents`` to False if you want
|
||||
all instances of a sub-class to raise a :class:`MissingComponentError`
|
||||
exception by default.
|
||||
"""
|
||||
|
||||
skipMissingComponents = True
|
||||
# alias error for convenience
|
||||
MissingComponentError = MissingComponentError
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
glyphSet,
|
||||
*args,
|
||||
skipMissingComponents=None,
|
||||
reverseFlipped=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Takes a 'glyphSet' argument (dict), in which the glyphs that are referenced
|
||||
as components are looked up by their name.
|
||||
|
||||
If the optional 'reverseFlipped' argument is True, components whose transformation
|
||||
matrix has a negative determinant will be decomposed with a reversed path direction
|
||||
to compensate for the flip.
|
||||
|
||||
The optional 'skipMissingComponents' argument can be set to True/False to
|
||||
override the homonymous class attribute for a given pen instance.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.glyphSet = glyphSet
|
||||
self.skipMissingComponents = (
|
||||
self.__class__.skipMissingComponents
|
||||
if skipMissingComponents is None
|
||||
else skipMissingComponents
|
||||
)
|
||||
self.reverseFlipped = reverseFlipped
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
|
||||
"""Transform the points of the base glyph and draw it onto self.
|
||||
|
||||
The `identifier` parameter and any extra kwargs are ignored.
|
||||
"""
|
||||
from fontTools.pens.transformPen import TransformPointPen
|
||||
|
||||
try:
|
||||
glyph = self.glyphSet[baseGlyphName]
|
||||
except KeyError:
|
||||
if not self.skipMissingComponents:
|
||||
raise MissingComponentError(baseGlyphName)
|
||||
self.log.warning(
|
||||
"glyph '%s' is missing from glyphSet; skipped" % baseGlyphName
|
||||
)
|
||||
else:
|
||||
pen = self
|
||||
if transformation != Identity:
|
||||
pen = TransformPointPen(pen, transformation)
|
||||
if self.reverseFlipped:
|
||||
# if the transformation has a negative determinant, it will
|
||||
# reverse the contour direction of the component
|
||||
a, b, c, d = transformation[:4]
|
||||
if a * d - b * c < 0:
|
||||
pen = ReverseContourPointPen(pen)
|
||||
glyph.drawPoints(pen)
|
||||
29
venv/lib/python3.13/site-packages/fontTools/pens/qtPen.py
Normal file
29
venv/lib/python3.13/site-packages/fontTools/pens/qtPen.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
__all__ = ["QtPen"]
|
||||
|
||||
|
||||
class QtPen(BasePen):
|
||||
def __init__(self, glyphSet, path=None):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
if path is None:
|
||||
from PyQt5.QtGui import QPainterPath
|
||||
|
||||
path = QPainterPath()
|
||||
self.path = path
|
||||
|
||||
def _moveTo(self, p):
|
||||
self.path.moveTo(*p)
|
||||
|
||||
def _lineTo(self, p):
|
||||
self.path.lineTo(*p)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
self.path.cubicTo(*p1, *p2, *p3)
|
||||
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
self.path.quadTo(*p1, *p2)
|
||||
|
||||
def _closePath(self):
|
||||
self.path.closeSubpath()
|
||||
105
venv/lib/python3.13/site-packages/fontTools/pens/qu2cuPen.py
Normal file
105
venv/lib/python3.13/site-packages/fontTools/pens/qu2cuPen.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
# Copyright 2023 Behdad Esfahbod. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from fontTools.qu2cu import quadratic_to_curves
|
||||
from fontTools.pens.filterPen import ContourFilterPen
|
||||
from fontTools.pens.reverseContourPen import ReverseContourPen
|
||||
import math
|
||||
|
||||
|
||||
class Qu2CuPen(ContourFilterPen):
|
||||
"""A filter pen to convert quadratic bezier splines to cubic curves
|
||||
using the FontTools SegmentPen protocol.
|
||||
|
||||
Args:
|
||||
|
||||
other_pen: another SegmentPen used to draw the transformed outline.
|
||||
max_err: maximum approximation error in font units. For optimal results,
|
||||
if you know the UPEM of the font, we recommend setting this to a
|
||||
value equal, or close to UPEM / 1000.
|
||||
reverse_direction: flip the contours' direction but keep starting point.
|
||||
stats: a dictionary counting the point numbers of cubic segments.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
other_pen,
|
||||
max_err,
|
||||
all_cubic=False,
|
||||
reverse_direction=False,
|
||||
stats=None,
|
||||
):
|
||||
if reverse_direction:
|
||||
other_pen = ReverseContourPen(other_pen)
|
||||
super().__init__(other_pen)
|
||||
self.all_cubic = all_cubic
|
||||
self.max_err = max_err
|
||||
self.stats = stats
|
||||
|
||||
def _quadratics_to_curve(self, q):
|
||||
curves = quadratic_to_curves(q, self.max_err, all_cubic=self.all_cubic)
|
||||
if self.stats is not None:
|
||||
for curve in curves:
|
||||
n = str(len(curve) - 2)
|
||||
self.stats[n] = self.stats.get(n, 0) + 1
|
||||
for curve in curves:
|
||||
if len(curve) == 4:
|
||||
yield ("curveTo", curve[1:])
|
||||
else:
|
||||
yield ("qCurveTo", curve[1:])
|
||||
|
||||
def filterContour(self, contour):
|
||||
quadratics = []
|
||||
currentPt = None
|
||||
newContour = []
|
||||
for op, args in contour:
|
||||
if op == "qCurveTo" and (
|
||||
self.all_cubic or (len(args) > 2 and args[-1] is not None)
|
||||
):
|
||||
if args[-1] is None:
|
||||
raise NotImplementedError(
|
||||
"oncurve-less contours with all_cubic not implemented"
|
||||
)
|
||||
quadratics.append((currentPt,) + args)
|
||||
else:
|
||||
if quadratics:
|
||||
newContour.extend(self._quadratics_to_curve(quadratics))
|
||||
quadratics = []
|
||||
newContour.append((op, args))
|
||||
currentPt = args[-1] if args else None
|
||||
if quadratics:
|
||||
newContour.extend(self._quadratics_to_curve(quadratics))
|
||||
|
||||
if not self.all_cubic:
|
||||
# Add back implicit oncurve points
|
||||
contour = newContour
|
||||
newContour = []
|
||||
for op, args in contour:
|
||||
if op == "qCurveTo" and newContour and newContour[-1][0] == "qCurveTo":
|
||||
pt0 = newContour[-1][1][-2]
|
||||
pt1 = newContour[-1][1][-1]
|
||||
pt2 = args[0]
|
||||
if (
|
||||
pt1 is not None
|
||||
and math.isclose(pt2[0] - pt1[0], pt1[0] - pt0[0])
|
||||
and math.isclose(pt2[1] - pt1[1], pt1[1] - pt0[1])
|
||||
):
|
||||
newArgs = newContour[-1][1][:-1] + args
|
||||
newContour[-1] = (op, newArgs)
|
||||
continue
|
||||
|
||||
newContour.append((op, args))
|
||||
|
||||
return newContour
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
from Quartz.CoreGraphics import CGPathCreateMutable, CGPathMoveToPoint
|
||||
from Quartz.CoreGraphics import CGPathAddLineToPoint, CGPathAddCurveToPoint
|
||||
from Quartz.CoreGraphics import CGPathAddQuadCurveToPoint, CGPathCloseSubpath
|
||||
|
||||
|
||||
__all__ = ["QuartzPen"]
|
||||
|
||||
|
||||
class QuartzPen(BasePen):
|
||||
"""A pen that creates a CGPath
|
||||
|
||||
Parameters
|
||||
- path: an optional CGPath to add to
|
||||
- xform: an optional CGAffineTransform to apply to the path
|
||||
"""
|
||||
|
||||
def __init__(self, glyphSet, path=None, xform=None):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
if path is None:
|
||||
path = CGPathCreateMutable()
|
||||
self.path = path
|
||||
self.xform = xform
|
||||
|
||||
def _moveTo(self, pt):
|
||||
x, y = pt
|
||||
CGPathMoveToPoint(self.path, self.xform, x, y)
|
||||
|
||||
def _lineTo(self, pt):
|
||||
x, y = pt
|
||||
CGPathAddLineToPoint(self.path, self.xform, x, y)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
(x1, y1), (x2, y2), (x3, y3) = p1, p2, p3
|
||||
CGPathAddCurveToPoint(self.path, self.xform, x1, y1, x2, y2, x3, y3)
|
||||
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
(x1, y1), (x2, y2) = p1, p2
|
||||
CGPathAddQuadCurveToPoint(self.path, self.xform, x1, y1, x2, y2)
|
||||
|
||||
def _closePath(self):
|
||||
CGPathCloseSubpath(self.path)
|
||||
335
venv/lib/python3.13/site-packages/fontTools/pens/recordingPen.py
Normal file
335
venv/lib/python3.13/site-packages/fontTools/pens/recordingPen.py
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
"""Pen recording operations that can be accessed or replayed."""
|
||||
|
||||
from fontTools.pens.basePen import AbstractPen, DecomposingPen
|
||||
from fontTools.pens.pointPen import AbstractPointPen, DecomposingPointPen
|
||||
|
||||
|
||||
__all__ = [
|
||||
"replayRecording",
|
||||
"RecordingPen",
|
||||
"DecomposingRecordingPen",
|
||||
"DecomposingRecordingPointPen",
|
||||
"RecordingPointPen",
|
||||
"lerpRecordings",
|
||||
]
|
||||
|
||||
|
||||
def replayRecording(recording, pen):
|
||||
"""Replay a recording, as produced by RecordingPen or DecomposingRecordingPen,
|
||||
to a pen.
|
||||
|
||||
Note that recording does not have to be produced by those pens.
|
||||
It can be any iterable of tuples of method name and tuple-of-arguments.
|
||||
Likewise, pen can be any objects receiving those method calls.
|
||||
"""
|
||||
for operator, operands in recording:
|
||||
getattr(pen, operator)(*operands)
|
||||
|
||||
|
||||
class RecordingPen(AbstractPen):
|
||||
"""Pen recording operations that can be accessed or replayed.
|
||||
|
||||
The recording can be accessed as pen.value; or replayed using
|
||||
pen.replay(otherPen).
|
||||
|
||||
:Example:
|
||||
.. code-block::
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.pens.recordingPen import RecordingPen
|
||||
|
||||
glyph_name = 'dollar'
|
||||
font_path = 'MyFont.otf'
|
||||
|
||||
font = TTFont(font_path)
|
||||
glyphset = font.getGlyphSet()
|
||||
glyph = glyphset[glyph_name]
|
||||
|
||||
pen = RecordingPen()
|
||||
glyph.draw(pen)
|
||||
print(pen.value)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.value = []
|
||||
|
||||
def moveTo(self, p0):
|
||||
self.value.append(("moveTo", (p0,)))
|
||||
|
||||
def lineTo(self, p1):
|
||||
self.value.append(("lineTo", (p1,)))
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
self.value.append(("qCurveTo", points))
|
||||
|
||||
def curveTo(self, *points):
|
||||
self.value.append(("curveTo", points))
|
||||
|
||||
def closePath(self):
|
||||
self.value.append(("closePath", ()))
|
||||
|
||||
def endPath(self):
|
||||
self.value.append(("endPath", ()))
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
self.value.append(("addComponent", (glyphName, transformation)))
|
||||
|
||||
def addVarComponent(self, glyphName, transformation, location):
|
||||
self.value.append(("addVarComponent", (glyphName, transformation, location)))
|
||||
|
||||
def replay(self, pen):
|
||||
replayRecording(self.value, pen)
|
||||
|
||||
draw = replay
|
||||
|
||||
|
||||
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 required 'glyphSet' positional argument,
|
||||
a dictionary of glyph objects (i.e. with a 'draw' method) keyed
|
||||
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 MissingComponentError(KeyError) if base glyph is not found in glyphSet
|
||||
skipMissingComponents = False
|
||||
|
||||
|
||||
class RecordingPointPen(AbstractPointPen):
|
||||
"""PointPen recording operations that can be accessed or replayed.
|
||||
|
||||
The recording can be accessed as pen.value; or replayed using
|
||||
pointPen.replay(otherPointPen).
|
||||
|
||||
:Example:
|
||||
.. code-block::
|
||||
|
||||
from defcon import Font
|
||||
from fontTools.pens.recordingPen import RecordingPointPen
|
||||
|
||||
glyph_name = 'a'
|
||||
font_path = 'MyFont.ufo'
|
||||
|
||||
font = Font(font_path)
|
||||
glyph = font[glyph_name]
|
||||
|
||||
pen = RecordingPointPen()
|
||||
glyph.drawPoints(pen)
|
||||
print(pen.value)
|
||||
|
||||
new_glyph = font.newGlyph('b')
|
||||
pen.replay(new_glyph.getPointPen())
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.value = []
|
||||
|
||||
def beginPath(self, identifier=None, **kwargs):
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self.value.append(("beginPath", (), kwargs))
|
||||
|
||||
def endPath(self):
|
||||
self.value.append(("endPath", (), {}))
|
||||
|
||||
def addPoint(
|
||||
self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
|
||||
):
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self.value.append(("addPoint", (pt, segmentType, smooth, name), kwargs))
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self.value.append(("addComponent", (baseGlyphName, transformation), kwargs))
|
||||
|
||||
def addVarComponent(
|
||||
self, baseGlyphName, transformation, location, identifier=None, **kwargs
|
||||
):
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self.value.append(
|
||||
("addVarComponent", (baseGlyphName, transformation, location), kwargs)
|
||||
)
|
||||
|
||||
def replay(self, pointPen):
|
||||
for operator, args, kwargs in self.value:
|
||||
getattr(pointPen, operator)(*args, **kwargs)
|
||||
|
||||
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.
|
||||
|
||||
Factor is typically between 0 and 1. 0 means the first recording,
|
||||
1 means the second recording, and 0.5 means the average of the
|
||||
two recordings. Other values are possible, and can be useful to
|
||||
extrapolate. Defaults to 0.5.
|
||||
|
||||
Returns a generator with the new recording.
|
||||
"""
|
||||
if len(recording1) != len(recording2):
|
||||
raise ValueError(
|
||||
"Mismatched lengths: %d and %d" % (len(recording1), len(recording2))
|
||||
)
|
||||
for (op1, args1), (op2, args2) in zip(recording1, recording2):
|
||||
if op1 != op2:
|
||||
raise ValueError("Mismatched operations: %s, %s" % (op1, op2))
|
||||
if op1 == "addComponent":
|
||||
raise ValueError("Cannot interpolate components")
|
||||
else:
|
||||
mid_args = [
|
||||
(x1 + (x2 - x1) * factor, y1 + (y2 - y1) * factor)
|
||||
for (x1, y1), (x2, y2) in zip(args1, args2)
|
||||
]
|
||||
yield (op1, mid_args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pen = RecordingPen()
|
||||
pen.moveTo((0, 0))
|
||||
pen.lineTo((0, 100))
|
||||
pen.curveTo((50, 75), (60, 50), (50, 25))
|
||||
pen.closePath()
|
||||
from pprint import pprint
|
||||
|
||||
pprint(pen.value)
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
from fontTools.pens.basePen import BasePen
|
||||
from reportlab.graphics.shapes import Path
|
||||
|
||||
|
||||
__all__ = ["ReportLabPen"]
|
||||
|
||||
|
||||
class ReportLabPen(BasePen):
|
||||
"""A pen for drawing onto a ``reportlab.graphics.shapes.Path`` object."""
|
||||
|
||||
def __init__(self, glyphSet, path=None):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
if path is None:
|
||||
path = Path()
|
||||
self.path = path
|
||||
|
||||
def _moveTo(self, p):
|
||||
(x, y) = p
|
||||
self.path.moveTo(x, y)
|
||||
|
||||
def _lineTo(self, p):
|
||||
(x, y) = p
|
||||
self.path.lineTo(x, y)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
(x1, y1) = p1
|
||||
(x2, y2) = p2
|
||||
(x3, y3) = p3
|
||||
self.path.curveTo(x1, y1, x2, y2, x3, y3)
|
||||
|
||||
def _closePath(self):
|
||||
self.path.closePath()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print(
|
||||
"Usage: reportLabPen.py <OTF/TTF font> <glyphname> [<image file to create>]"
|
||||
)
|
||||
print(
|
||||
" If no image file name is created, by default <glyphname>.png is created."
|
||||
)
|
||||
print(" example: reportLabPen.py Arial.TTF R test.png")
|
||||
print(
|
||||
" (The file format will be PNG, regardless of the image file name supplied)"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
from reportlab.lib import colors
|
||||
|
||||
path = sys.argv[1]
|
||||
glyphName = sys.argv[2]
|
||||
if len(sys.argv) > 3:
|
||||
imageFile = sys.argv[3]
|
||||
else:
|
||||
imageFile = "%s.png" % glyphName
|
||||
|
||||
font = TTFont(path) # it would work just as well with fontTools.t1Lib.T1Font
|
||||
gs = font.getGlyphSet()
|
||||
pen = ReportLabPen(gs, Path(fillColor=colors.red, strokeWidth=5))
|
||||
g = gs[glyphName]
|
||||
g.draw(pen)
|
||||
|
||||
w, h = g.width, 1000
|
||||
from reportlab.graphics import renderPM
|
||||
from reportlab.graphics.shapes import Group, Drawing, scale
|
||||
|
||||
# Everything is wrapped in a group to allow transformations.
|
||||
g = Group(pen.path)
|
||||
g.translate(0, 200)
|
||||
g.scale(0.3, 0.3)
|
||||
|
||||
d = Drawing(w, h)
|
||||
d.add(g)
|
||||
|
||||
renderPM.drawToFile(d, imageFile, fmt="PNG")
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
from fontTools.misc.arrayTools import pairwise
|
||||
from fontTools.pens.filterPen import ContourFilterPen
|
||||
|
||||
|
||||
__all__ = ["reversedContour", "ReverseContourPen"]
|
||||
|
||||
|
||||
class ReverseContourPen(ContourFilterPen):
|
||||
"""Filter pen that passes outline data to another pen, but reversing
|
||||
the winding direction of all contours. Components are simply passed
|
||||
through unchanged.
|
||||
|
||||
Closed contours are reversed in such a way that the first point remains
|
||||
the first point.
|
||||
"""
|
||||
|
||||
def __init__(self, outPen, outputImpliedClosingLine=False):
|
||||
super().__init__(outPen)
|
||||
self.outputImpliedClosingLine = outputImpliedClosingLine
|
||||
|
||||
def filterContour(self, contour):
|
||||
return reversedContour(contour, self.outputImpliedClosingLine)
|
||||
|
||||
|
||||
def reversedContour(contour, outputImpliedClosingLine=False):
|
||||
"""Generator that takes a list of pen's (operator, operands) tuples,
|
||||
and yields them with the winding direction reversed.
|
||||
"""
|
||||
if not contour:
|
||||
return # nothing to do, stop iteration
|
||||
|
||||
# valid contours must have at least a starting and ending command,
|
||||
# can't have one without the other
|
||||
assert len(contour) > 1, "invalid contour"
|
||||
|
||||
# the type of the last command determines if the contour is closed
|
||||
contourType = contour.pop()[0]
|
||||
assert contourType in ("endPath", "closePath")
|
||||
closed = contourType == "closePath"
|
||||
|
||||
firstType, firstPts = contour.pop(0)
|
||||
assert firstType in ("moveTo", "qCurveTo"), (
|
||||
"invalid initial segment type: %r" % firstType
|
||||
)
|
||||
firstOnCurve = firstPts[-1]
|
||||
if firstType == "qCurveTo":
|
||||
# special case for TrueType paths contaning only off-curve points
|
||||
assert firstOnCurve is None, "off-curve only paths must end with 'None'"
|
||||
assert not contour, "only one qCurveTo allowed per off-curve path"
|
||||
firstPts = (firstPts[0],) + tuple(reversed(firstPts[1:-1])) + (None,)
|
||||
|
||||
if not contour:
|
||||
# contour contains only one segment, nothing to reverse
|
||||
if firstType == "moveTo":
|
||||
closed = False # single-point paths can't be closed
|
||||
else:
|
||||
closed = True # off-curve paths are closed by definition
|
||||
yield firstType, firstPts
|
||||
else:
|
||||
lastType, lastPts = contour[-1]
|
||||
lastOnCurve = lastPts[-1]
|
||||
if closed:
|
||||
# for closed paths, we keep the starting point
|
||||
yield firstType, firstPts
|
||||
if firstOnCurve != lastOnCurve:
|
||||
# emit an implied line between the last and first points
|
||||
yield "lineTo", (lastOnCurve,)
|
||||
contour[-1] = (lastType, tuple(lastPts[:-1]) + (firstOnCurve,))
|
||||
|
||||
if len(contour) > 1:
|
||||
secondType, secondPts = contour[0]
|
||||
else:
|
||||
# contour has only two points, the second and last are the same
|
||||
secondType, secondPts = lastType, lastPts
|
||||
|
||||
if not outputImpliedClosingLine:
|
||||
# if a lineTo follows the initial moveTo, after reversing it
|
||||
# will be implied by the closePath, so we don't emit one;
|
||||
# unless the lineTo and moveTo overlap, in which case we keep the
|
||||
# duplicate points
|
||||
if secondType == "lineTo" and firstPts != secondPts:
|
||||
del contour[0]
|
||||
if contour:
|
||||
contour[-1] = (lastType, tuple(lastPts[:-1]) + secondPts)
|
||||
else:
|
||||
# for open paths, the last point will become the first
|
||||
yield firstType, (lastOnCurve,)
|
||||
contour[-1] = (lastType, tuple(lastPts[:-1]) + (firstOnCurve,))
|
||||
|
||||
# we iterate over all segment pairs in reverse order, and yield
|
||||
# each one with the off-curve points reversed (if any), and
|
||||
# with the on-curve point of the following segment
|
||||
for (curType, curPts), (_, nextPts) in pairwise(contour, reverse=True):
|
||||
yield curType, tuple(reversed(curPts[:-1])) + (nextPts[-1],)
|
||||
|
||||
yield "closePath" if closed else "endPath", ()
|
||||
130
venv/lib/python3.13/site-packages/fontTools/pens/roundingPen.py
Normal file
130
venv/lib/python3.13/site-packages/fontTools/pens/roundingPen.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
from fontTools.misc.roundTools import noRound, otRound
|
||||
from fontTools.misc.transform import Transform
|
||||
from fontTools.pens.filterPen import FilterPen, FilterPointPen
|
||||
|
||||
|
||||
__all__ = ["RoundingPen", "RoundingPointPen"]
|
||||
|
||||
|
||||
class RoundingPen(FilterPen):
|
||||
"""
|
||||
Filter pen that rounds point coordinates and component XY offsets to integer. For
|
||||
rounding the component transform values, a separate round function can be passed to
|
||||
the pen.
|
||||
|
||||
>>> from fontTools.pens.recordingPen import RecordingPen
|
||||
>>> recpen = RecordingPen()
|
||||
>>> roundpen = RoundingPen(recpen)
|
||||
>>> roundpen.moveTo((0.4, 0.6))
|
||||
>>> roundpen.lineTo((1.6, 2.5))
|
||||
>>> roundpen.qCurveTo((2.4, 4.6), (3.3, 5.7), (4.9, 6.1))
|
||||
>>> roundpen.curveTo((6.4, 8.6), (7.3, 9.7), (8.9, 10.1))
|
||||
>>> roundpen.addComponent("a", (1.5, 0, 0, 1.5, 10.5, -10.5))
|
||||
>>> recpen.value == [
|
||||
... ('moveTo', ((0, 1),)),
|
||||
... ('lineTo', ((2, 3),)),
|
||||
... ('qCurveTo', ((2, 5), (3, 6), (5, 6))),
|
||||
... ('curveTo', ((6, 9), (7, 10), (9, 10))),
|
||||
... ('addComponent', ('a', (1.5, 0, 0, 1.5, 11, -10))),
|
||||
... ]
|
||||
True
|
||||
"""
|
||||
|
||||
def __init__(self, outPen, roundFunc=otRound, transformRoundFunc=noRound):
|
||||
super().__init__(outPen)
|
||||
self.roundFunc = roundFunc
|
||||
self.transformRoundFunc = transformRoundFunc
|
||||
|
||||
def moveTo(self, pt):
|
||||
self._outPen.moveTo((self.roundFunc(pt[0]), self.roundFunc(pt[1])))
|
||||
|
||||
def lineTo(self, pt):
|
||||
self._outPen.lineTo((self.roundFunc(pt[0]), self.roundFunc(pt[1])))
|
||||
|
||||
def curveTo(self, *points):
|
||||
self._outPen.curveTo(
|
||||
*((self.roundFunc(x), self.roundFunc(y)) for x, y in points)
|
||||
)
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
self._outPen.qCurveTo(
|
||||
*((self.roundFunc(x), self.roundFunc(y)) for x, y in points)
|
||||
)
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
xx, xy, yx, yy, dx, dy = transformation
|
||||
self._outPen.addComponent(
|
||||
glyphName,
|
||||
Transform(
|
||||
self.transformRoundFunc(xx),
|
||||
self.transformRoundFunc(xy),
|
||||
self.transformRoundFunc(yx),
|
||||
self.transformRoundFunc(yy),
|
||||
self.roundFunc(dx),
|
||||
self.roundFunc(dy),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class RoundingPointPen(FilterPointPen):
|
||||
"""
|
||||
Filter point pen that rounds point coordinates and component XY offsets to integer.
|
||||
For rounding the component scale values, a separate round function can be passed to
|
||||
the pen.
|
||||
|
||||
>>> from fontTools.pens.recordingPen import RecordingPointPen
|
||||
>>> recpen = RecordingPointPen()
|
||||
>>> roundpen = RoundingPointPen(recpen)
|
||||
>>> roundpen.beginPath()
|
||||
>>> roundpen.addPoint((0.4, 0.6), 'line')
|
||||
>>> roundpen.addPoint((1.6, 2.5), 'line')
|
||||
>>> roundpen.addPoint((2.4, 4.6))
|
||||
>>> roundpen.addPoint((3.3, 5.7))
|
||||
>>> roundpen.addPoint((4.9, 6.1), 'qcurve')
|
||||
>>> roundpen.endPath()
|
||||
>>> roundpen.addComponent("a", (1.5, 0, 0, 1.5, 10.5, -10.5))
|
||||
>>> recpen.value == [
|
||||
... ('beginPath', (), {}),
|
||||
... ('addPoint', ((0, 1), 'line', False, None), {}),
|
||||
... ('addPoint', ((2, 3), 'line', False, None), {}),
|
||||
... ('addPoint', ((2, 5), None, False, None), {}),
|
||||
... ('addPoint', ((3, 6), None, False, None), {}),
|
||||
... ('addPoint', ((5, 6), 'qcurve', False, None), {}),
|
||||
... ('endPath', (), {}),
|
||||
... ('addComponent', ('a', (1.5, 0, 0, 1.5, 11, -10)), {}),
|
||||
... ]
|
||||
True
|
||||
"""
|
||||
|
||||
def __init__(self, outPen, roundFunc=otRound, transformRoundFunc=noRound):
|
||||
super().__init__(outPen)
|
||||
self.roundFunc = roundFunc
|
||||
self.transformRoundFunc = transformRoundFunc
|
||||
|
||||
def addPoint(
|
||||
self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
|
||||
):
|
||||
self._outPen.addPoint(
|
||||
(self.roundFunc(pt[0]), self.roundFunc(pt[1])),
|
||||
segmentType=segmentType,
|
||||
smooth=smooth,
|
||||
name=name,
|
||||
identifier=identifier,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
|
||||
xx, xy, yx, yy, dx, dy = transformation
|
||||
self._outPen.addComponent(
|
||||
baseGlyphName,
|
||||
Transform(
|
||||
self.transformRoundFunc(xx),
|
||||
self.transformRoundFunc(xy),
|
||||
self.transformRoundFunc(yx),
|
||||
self.transformRoundFunc(yy),
|
||||
self.roundFunc(dx),
|
||||
self.roundFunc(dy),
|
||||
),
|
||||
identifier=identifier,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
"""Pen calculating area, center of mass, variance and standard-deviation,
|
||||
covariance and correlation, and slant, of glyph shapes."""
|
||||
|
||||
from math import sqrt, degrees, atan
|
||||
from fontTools.pens.basePen import BasePen, OpenContourError
|
||||
from fontTools.pens.momentsPen import MomentsPen
|
||||
|
||||
__all__ = ["StatisticsPen", "StatisticsControlPen"]
|
||||
|
||||
|
||||
class StatisticsBase:
|
||||
def __init__(self):
|
||||
self._zero()
|
||||
|
||||
def _zero(self):
|
||||
self.area = 0
|
||||
self.meanX = 0
|
||||
self.meanY = 0
|
||||
self.varianceX = 0
|
||||
self.varianceY = 0
|
||||
self.stddevX = 0
|
||||
self.stddevY = 0
|
||||
self.covariance = 0
|
||||
self.correlation = 0
|
||||
self.slant = 0
|
||||
|
||||
def _update(self):
|
||||
# XXX The variance formulas should never produce a negative value,
|
||||
# but due to reasons I don't understand, both of our pens do.
|
||||
# So we take the absolute value here.
|
||||
self.varianceX = abs(self.varianceX)
|
||||
self.varianceY = abs(self.varianceY)
|
||||
|
||||
self.stddevX = stddevX = sqrt(self.varianceX)
|
||||
self.stddevY = stddevY = sqrt(self.varianceY)
|
||||
|
||||
# Correlation(X,Y) = Covariance(X,Y) / ( stddev(X) * stddev(Y) )
|
||||
# https://en.wikipedia.org/wiki/Pearson_product-moment_correlation_coefficient
|
||||
if stddevX * stddevY == 0:
|
||||
correlation = float("NaN")
|
||||
else:
|
||||
# XXX The above formula should never produce a value outside
|
||||
# the range [-1, 1], but due to reasons I don't understand,
|
||||
# (probably the same issue as above), it does. So we clamp.
|
||||
correlation = self.covariance / (stddevX * stddevY)
|
||||
correlation = max(-1, min(1, correlation))
|
||||
self.correlation = correlation if abs(correlation) > 1e-3 else 0
|
||||
|
||||
slant = (
|
||||
self.covariance / self.varianceY if self.varianceY != 0 else float("NaN")
|
||||
)
|
||||
self.slant = slant if abs(slant) > 1e-3 else 0
|
||||
|
||||
|
||||
class StatisticsPen(StatisticsBase, MomentsPen):
|
||||
"""Pen calculating area, center of mass, variance and
|
||||
standard-deviation, covariance and correlation, and slant,
|
||||
of glyph shapes.
|
||||
|
||||
Note that if the glyph shape is self-intersecting, the values
|
||||
are not correct (but well-defined). Moreover, area will be
|
||||
negative if contour directions are clockwise."""
|
||||
|
||||
def __init__(self, glyphset=None):
|
||||
MomentsPen.__init__(self, glyphset=glyphset)
|
||||
StatisticsBase.__init__(self)
|
||||
|
||||
def _closePath(self):
|
||||
MomentsPen._closePath(self)
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
area = self.area
|
||||
if not area:
|
||||
self._zero()
|
||||
return
|
||||
|
||||
# Center of mass
|
||||
# https://en.wikipedia.org/wiki/Center_of_mass#A_continuous_volume
|
||||
self.meanX = meanX = self.momentX / area
|
||||
self.meanY = meanY = self.momentY / area
|
||||
|
||||
# Var(X) = E[X^2] - E[X]^2
|
||||
self.varianceX = self.momentXX / area - meanX * meanX
|
||||
self.varianceY = self.momentYY / area - meanY * meanY
|
||||
|
||||
# Covariance(X,Y) = (E[X.Y] - E[X]E[Y])
|
||||
self.covariance = self.momentXY / area - meanX * meanY
|
||||
|
||||
StatisticsBase._update(self)
|
||||
|
||||
|
||||
class StatisticsControlPen(StatisticsBase, BasePen):
|
||||
"""Pen calculating area, center of mass, variance and
|
||||
standard-deviation, covariance and correlation, and slant,
|
||||
of glyph shapes, using the control polygon only.
|
||||
|
||||
Note that if the glyph shape is self-intersecting, the values
|
||||
are not correct (but well-defined). Moreover, area will be
|
||||
negative if contour directions are clockwise."""
|
||||
|
||||
def __init__(self, glyphset=None):
|
||||
BasePen.__init__(self, glyphset)
|
||||
StatisticsBase.__init__(self)
|
||||
self._nodes = []
|
||||
|
||||
def _moveTo(self, pt):
|
||||
self._nodes.append(complex(*pt))
|
||||
self._startPoint = pt
|
||||
|
||||
def _lineTo(self, pt):
|
||||
self._nodes.append(complex(*pt))
|
||||
|
||||
def _qCurveToOne(self, pt1, pt2):
|
||||
for pt in (pt1, pt2):
|
||||
self._nodes.append(complex(*pt))
|
||||
|
||||
def _curveToOne(self, pt1, pt2, pt3):
|
||||
for pt in (pt1, pt2, pt3):
|
||||
self._nodes.append(complex(*pt))
|
||||
|
||||
def _closePath(self):
|
||||
p0 = self._getCurrentPoint()
|
||||
if p0 != self._startPoint:
|
||||
self._lineTo(self._startPoint)
|
||||
self._update()
|
||||
|
||||
def _endPath(self):
|
||||
p0 = self._getCurrentPoint()
|
||||
if p0 != self._startPoint:
|
||||
raise OpenContourError("Glyph statistics not defined on open contours.")
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
nodes = self._nodes
|
||||
n = len(nodes)
|
||||
|
||||
# Triangle formula
|
||||
self.area = (
|
||||
sum(
|
||||
(p0.real * p1.imag - p1.real * p0.imag)
|
||||
for p0, p1 in zip(nodes, nodes[1:] + nodes[:1])
|
||||
)
|
||||
/ 2
|
||||
)
|
||||
|
||||
# Center of mass
|
||||
# https://en.wikipedia.org/wiki/Center_of_mass#A_system_of_particles
|
||||
sumNodes = sum(nodes)
|
||||
self.meanX = meanX = sumNodes.real / n
|
||||
self.meanY = meanY = sumNodes.imag / n
|
||||
|
||||
if n > 1:
|
||||
# Var(X) = (sum[X^2] - sum[X]^2 / n) / (n - 1)
|
||||
# https://www.statisticshowto.com/probability-and-statistics/descriptive-statistics/sample-variance/
|
||||
self.varianceX = varianceX = (
|
||||
sum(p.real * p.real for p in nodes)
|
||||
- (sumNodes.real * sumNodes.real) / n
|
||||
) / (n - 1)
|
||||
self.varianceY = varianceY = (
|
||||
sum(p.imag * p.imag for p in nodes)
|
||||
- (sumNodes.imag * sumNodes.imag) / n
|
||||
) / (n - 1)
|
||||
|
||||
# Covariance(X,Y) = (sum[X.Y] - sum[X].sum[Y] / n) / (n - 1)
|
||||
self.covariance = covariance = (
|
||||
sum(p.real * p.imag for p in nodes)
|
||||
- (sumNodes.real * sumNodes.imag) / n
|
||||
) / (n - 1)
|
||||
else:
|
||||
self.varianceX = varianceX = 0
|
||||
self.varianceY = varianceY = 0
|
||||
self.covariance = covariance = 0
|
||||
|
||||
StatisticsBase._update(self)
|
||||
|
||||
|
||||
def _test(glyphset, upem, glyphs, quiet=False, *, control=False):
|
||||
from fontTools.pens.transformPen import TransformPen
|
||||
from fontTools.misc.transform import Scale
|
||||
|
||||
wght_sum = 0
|
||||
wght_sum_perceptual = 0
|
||||
wdth_sum = 0
|
||||
slnt_sum = 0
|
||||
slnt_sum_perceptual = 0
|
||||
for glyph_name in glyphs:
|
||||
glyph = glyphset[glyph_name]
|
||||
if control:
|
||||
pen = StatisticsControlPen(glyphset=glyphset)
|
||||
else:
|
||||
pen = StatisticsPen(glyphset=glyphset)
|
||||
transformer = TransformPen(pen, Scale(1.0 / upem))
|
||||
glyph.draw(transformer)
|
||||
|
||||
area = abs(pen.area)
|
||||
width = glyph.width
|
||||
wght_sum += area
|
||||
wght_sum_perceptual += pen.area * width
|
||||
wdth_sum += width
|
||||
slnt_sum += pen.slant
|
||||
slnt_sum_perceptual += pen.slant * width
|
||||
|
||||
if quiet:
|
||||
continue
|
||||
|
||||
print()
|
||||
print("glyph:", glyph_name)
|
||||
|
||||
for item in [
|
||||
"area",
|
||||
"momentX",
|
||||
"momentY",
|
||||
"momentXX",
|
||||
"momentYY",
|
||||
"momentXY",
|
||||
"meanX",
|
||||
"meanY",
|
||||
"varianceX",
|
||||
"varianceY",
|
||||
"stddevX",
|
||||
"stddevY",
|
||||
"covariance",
|
||||
"correlation",
|
||||
"slant",
|
||||
]:
|
||||
print("%s: %g" % (item, getattr(pen, item)))
|
||||
|
||||
if not quiet:
|
||||
print()
|
||||
print("font:")
|
||||
|
||||
print("weight: %g" % (wght_sum * upem / wdth_sum))
|
||||
print("weight (perceptual): %g" % (wght_sum_perceptual / wdth_sum))
|
||||
print("width: %g" % (wdth_sum / upem / len(glyphs)))
|
||||
slant = slnt_sum / len(glyphs)
|
||||
print("slant: %g" % slant)
|
||||
print("slant angle: %g" % -degrees(atan(slant)))
|
||||
slant_perceptual = slnt_sum_perceptual / wdth_sum
|
||||
print("slant (perceptual): %g" % slant_perceptual)
|
||||
print("slant (perceptual) angle: %g" % -degrees(atan(slant_perceptual)))
|
||||
|
||||
|
||||
def main(args):
|
||||
"""Report font glyph shape geometricsl statistics"""
|
||||
|
||||
if args is None:
|
||||
import sys
|
||||
|
||||
args = sys.argv[1:]
|
||||
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
"fonttools pens.statisticsPen",
|
||||
description="Report font glyph shape geometricsl statistics",
|
||||
)
|
||||
parser.add_argument("font", metavar="font.ttf", help="Font file.")
|
||||
parser.add_argument("glyphs", metavar="glyph-name", help="Glyph names.", nargs="*")
|
||||
parser.add_argument(
|
||||
"-y",
|
||||
metavar="<number>",
|
||||
help="Face index into a collection to open. Zero based.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--control",
|
||||
action="store_true",
|
||||
help="Use the control-box pen instead of the Green therem.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q", "--quiet", action="store_true", help="Only report font-wide statistics."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--variations",
|
||||
metavar="AXIS=LOC",
|
||||
default="",
|
||||
help="List of space separated locations. A location consist in "
|
||||
"the name of a variation axis, followed by '=' and a number. E.g.: "
|
||||
"wght=700 wdth=80. The default is the location of the base master.",
|
||||
)
|
||||
|
||||
options = parser.parse_args(args)
|
||||
|
||||
glyphs = options.glyphs
|
||||
fontNumber = int(options.y) if options.y is not None else 0
|
||||
|
||||
location = {}
|
||||
for tag_v in options.variations.split():
|
||||
fields = tag_v.split("=")
|
||||
tag = fields[0].strip()
|
||||
v = int(fields[1])
|
||||
location[tag] = v
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
font = TTFont(options.font, fontNumber=fontNumber)
|
||||
if not glyphs:
|
||||
glyphs = font.getGlyphOrder()
|
||||
_test(
|
||||
font.getGlyphSet(location=location),
|
||||
font["head"].unitsPerEm,
|
||||
glyphs,
|
||||
quiet=options.quiet,
|
||||
control=options.control,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
main(sys.argv[1:])
|
||||
310
venv/lib/python3.13/site-packages/fontTools/pens/svgPathPen.py
Normal file
310
venv/lib/python3.13/site-packages/fontTools/pens/svgPathPen.py
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
from typing import Callable
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
def pointToString(pt, ntos=str):
|
||||
return " ".join(ntos(i) for i in pt)
|
||||
|
||||
|
||||
class SVGPathPen(BasePen):
|
||||
"""Pen to draw SVG path d commands.
|
||||
|
||||
Args:
|
||||
glyphSet: a dictionary of drawable glyph objects keyed by name
|
||||
used to resolve component references in composite glyphs.
|
||||
ntos: a callable that takes a number and returns a string, to
|
||||
customize how numbers are formatted (default: str).
|
||||
|
||||
:Example:
|
||||
.. code-block::
|
||||
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.lineTo((1, 1))
|
||||
>>> pen.curveTo((2, 2), (3, 3), (4, 4))
|
||||
>>> pen.closePath()
|
||||
>>> pen.getCommands()
|
||||
'M0 0 1 1C2 2 3 3 4 4Z'
|
||||
|
||||
Note:
|
||||
Fonts have a coordinate system where Y grows up, whereas in SVG,
|
||||
Y grows down. As such, rendering path data from this pen in
|
||||
SVG typically results in upside-down glyphs. You can fix this
|
||||
by wrapping the data from this pen in an SVG group element with
|
||||
transform, or wrap this pen in a transform pen. For example:
|
||||
.. code-block:: python
|
||||
|
||||
spen = svgPathPen.SVGPathPen(glyphset)
|
||||
pen= TransformPen(spen , (1, 0, 0, -1, 0, 0))
|
||||
glyphset[glyphname].draw(pen)
|
||||
print(tpen.getCommands())
|
||||
"""
|
||||
|
||||
def __init__(self, glyphSet, ntos: Callable[[float], str] = str):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
self._commands = []
|
||||
self._lastCommand = None
|
||||
self._lastX = None
|
||||
self._lastY = None
|
||||
self._ntos = ntos
|
||||
|
||||
def _handleAnchor(self):
|
||||
"""
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.moveTo((10, 10))
|
||||
>>> pen._commands
|
||||
['M10 10']
|
||||
"""
|
||||
if self._lastCommand == "M":
|
||||
self._commands.pop(-1)
|
||||
|
||||
def _moveTo(self, pt):
|
||||
"""
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen._commands
|
||||
['M0 0']
|
||||
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((10, 0))
|
||||
>>> pen._commands
|
||||
['M10 0']
|
||||
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((0, 10))
|
||||
>>> pen._commands
|
||||
['M0 10']
|
||||
"""
|
||||
self._handleAnchor()
|
||||
t = "M%s" % (pointToString(pt, self._ntos))
|
||||
self._commands.append(t)
|
||||
self._lastCommand = "M"
|
||||
self._lastX, self._lastY = pt
|
||||
|
||||
def _lineTo(self, pt):
|
||||
"""
|
||||
# duplicate point
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((10, 10))
|
||||
>>> pen.lineTo((10, 10))
|
||||
>>> pen._commands
|
||||
['M10 10']
|
||||
|
||||
# vertical line
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((10, 10))
|
||||
>>> pen.lineTo((10, 0))
|
||||
>>> pen._commands
|
||||
['M10 10', 'V0']
|
||||
|
||||
# horizontal line
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((10, 10))
|
||||
>>> pen.lineTo((0, 10))
|
||||
>>> pen._commands
|
||||
['M10 10', 'H0']
|
||||
|
||||
# basic
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.lineTo((70, 80))
|
||||
>>> pen._commands
|
||||
['L70 80']
|
||||
|
||||
# basic following a moveto
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.lineTo((10, 10))
|
||||
>>> pen._commands
|
||||
['M0 0', ' 10 10']
|
||||
"""
|
||||
x, y = pt
|
||||
# duplicate point
|
||||
if x == self._lastX and y == self._lastY:
|
||||
return
|
||||
# vertical line
|
||||
elif x == self._lastX:
|
||||
cmd = "V"
|
||||
pts = self._ntos(y)
|
||||
# horizontal line
|
||||
elif y == self._lastY:
|
||||
cmd = "H"
|
||||
pts = self._ntos(x)
|
||||
# previous was a moveto
|
||||
elif self._lastCommand == "M":
|
||||
cmd = None
|
||||
pts = " " + pointToString(pt, self._ntos)
|
||||
# basic
|
||||
else:
|
||||
cmd = "L"
|
||||
pts = pointToString(pt, self._ntos)
|
||||
# write the string
|
||||
t = ""
|
||||
if cmd:
|
||||
t += cmd
|
||||
self._lastCommand = cmd
|
||||
t += pts
|
||||
self._commands.append(t)
|
||||
# store for future reference
|
||||
self._lastX, self._lastY = pt
|
||||
|
||||
def _curveToOne(self, pt1, pt2, pt3):
|
||||
"""
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.curveTo((10, 20), (30, 40), (50, 60))
|
||||
>>> pen._commands
|
||||
['C10 20 30 40 50 60']
|
||||
"""
|
||||
t = "C"
|
||||
t += pointToString(pt1, self._ntos) + " "
|
||||
t += pointToString(pt2, self._ntos) + " "
|
||||
t += pointToString(pt3, self._ntos)
|
||||
self._commands.append(t)
|
||||
self._lastCommand = "C"
|
||||
self._lastX, self._lastY = pt3
|
||||
|
||||
def _qCurveToOne(self, pt1, pt2):
|
||||
"""
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.qCurveTo((10, 20), (30, 40))
|
||||
>>> pen._commands
|
||||
['Q10 20 30 40']
|
||||
>>> from fontTools.misc.roundTools import otRound
|
||||
>>> pen = SVGPathPen(None, ntos=lambda v: str(otRound(v)))
|
||||
>>> pen.qCurveTo((3, 3), (7, 5), (11, 4))
|
||||
>>> pen._commands
|
||||
['Q3 3 5 4', 'Q7 5 11 4']
|
||||
"""
|
||||
assert pt2 is not None
|
||||
t = "Q"
|
||||
t += pointToString(pt1, self._ntos) + " "
|
||||
t += pointToString(pt2, self._ntos)
|
||||
self._commands.append(t)
|
||||
self._lastCommand = "Q"
|
||||
self._lastX, self._lastY = pt2
|
||||
|
||||
def _closePath(self):
|
||||
"""
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.closePath()
|
||||
>>> pen._commands
|
||||
['Z']
|
||||
"""
|
||||
self._commands.append("Z")
|
||||
self._lastCommand = "Z"
|
||||
self._lastX = self._lastY = None
|
||||
|
||||
def _endPath(self):
|
||||
"""
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.endPath()
|
||||
>>> pen._commands
|
||||
[]
|
||||
"""
|
||||
self._lastCommand = None
|
||||
self._lastX = self._lastY = None
|
||||
|
||||
def getCommands(self):
|
||||
return "".join(self._commands)
|
||||
|
||||
|
||||
def main(args=None):
|
||||
"""Generate per-character SVG from font and text"""
|
||||
|
||||
if args is None:
|
||||
import sys
|
||||
|
||||
args = sys.argv[1:]
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
"fonttools pens.svgPathPen", description="Generate SVG from text"
|
||||
)
|
||||
parser.add_argument("font", metavar="font.ttf", help="Font file.")
|
||||
parser.add_argument("text", metavar="text", nargs="?", help="Text string.")
|
||||
parser.add_argument(
|
||||
"-y",
|
||||
metavar="<number>",
|
||||
help="Face index into a collection to open. Zero based.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--glyphs",
|
||||
metavar="whitespace-separated list of glyph names",
|
||||
type=str,
|
||||
help="Glyphs to show. Exclusive with text option",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--variations",
|
||||
metavar="AXIS=LOC",
|
||||
default="",
|
||||
help="List of space separated locations. A location consist in "
|
||||
"the name of a variation axis, followed by '=' and a number. E.g.: "
|
||||
"wght=700 wdth=80. The default is the location of the base master.",
|
||||
)
|
||||
|
||||
options = parser.parse_args(args)
|
||||
|
||||
fontNumber = int(options.y) if options.y is not None else 0
|
||||
|
||||
font = TTFont(options.font, fontNumber=fontNumber)
|
||||
text = options.text
|
||||
glyphs = options.glyphs
|
||||
|
||||
location = {}
|
||||
for tag_v in options.variations.split():
|
||||
fields = tag_v.split("=")
|
||||
tag = fields[0].strip()
|
||||
v = float(fields[1])
|
||||
location[tag] = v
|
||||
|
||||
hhea = font["hhea"]
|
||||
ascent, descent = hhea.ascent, hhea.descent
|
||||
|
||||
glyphset = font.getGlyphSet(location=location)
|
||||
cmap = font["cmap"].getBestCmap()
|
||||
|
||||
if glyphs is not None and text is not None:
|
||||
raise ValueError("Options --glyphs and --text are exclusive")
|
||||
|
||||
if glyphs is None:
|
||||
glyphs = " ".join(cmap[ord(u)] for u in text)
|
||||
|
||||
glyphs = glyphs.split()
|
||||
|
||||
s = ""
|
||||
width = 0
|
||||
for g in glyphs:
|
||||
glyph = glyphset[g]
|
||||
|
||||
pen = SVGPathPen(glyphset)
|
||||
glyph.draw(pen)
|
||||
commands = pen.getCommands()
|
||||
|
||||
s += '<g transform="translate(%d %d) scale(1 -1)"><path d="%s"/></g>\n' % (
|
||||
width,
|
||||
ascent,
|
||||
commands,
|
||||
)
|
||||
|
||||
width += glyph.width
|
||||
|
||||
print('<?xml version="1.0" encoding="UTF-8"?>')
|
||||
print(
|
||||
'<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">'
|
||||
% (width, ascent - descent)
|
||||
)
|
||||
print(s, end="")
|
||||
print("</svg>")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
import doctest
|
||||
|
||||
sys.exit(doctest.testmod().failed)
|
||||
|
||||
sys.exit(main())
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
# Copyright (c) 2009 Type Supply LLC
|
||||
# Author: Tal Leming
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from fontTools.cffLib.specializer import commandsToProgram, specializeCommands
|
||||
from fontTools.misc.psCharStrings import T2CharString
|
||||
from fontTools.misc.roundTools import otRound, roundFunc
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
class T2CharStringPen(BasePen):
|
||||
"""Pen to draw Type 2 CharStrings.
|
||||
|
||||
The 'roundTolerance' argument controls the rounding of point coordinates.
|
||||
It is defined as the maximum absolute difference between the original
|
||||
float and the rounded integer value.
|
||||
The default tolerance of 0.5 means that all floats are rounded to integer;
|
||||
a value of 0 disables rounding; values in between will only round floats
|
||||
which are close to their integral part within the tolerated range.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
width: float | None,
|
||||
glyphSet: Dict[str, Any] | None,
|
||||
roundTolerance: float = 0.5,
|
||||
CFF2: bool = False,
|
||||
) -> None:
|
||||
super(T2CharStringPen, self).__init__(glyphSet)
|
||||
self.round = roundFunc(roundTolerance)
|
||||
self._CFF2 = CFF2
|
||||
self._width = width
|
||||
self._commands: List[Tuple[str | bytes, List[float]]] = []
|
||||
self._p0 = (0, 0)
|
||||
|
||||
def _p(self, pt: Tuple[float, float]) -> List[float]:
|
||||
p0 = self._p0
|
||||
pt = self._p0 = (self.round(pt[0]), self.round(pt[1]))
|
||||
return [pt[0] - p0[0], pt[1] - p0[1]]
|
||||
|
||||
def _moveTo(self, pt: Tuple[float, float]) -> None:
|
||||
self._commands.append(("rmoveto", self._p(pt)))
|
||||
|
||||
def _lineTo(self, pt: Tuple[float, float]) -> None:
|
||||
self._commands.append(("rlineto", self._p(pt)))
|
||||
|
||||
def _curveToOne(
|
||||
self,
|
||||
pt1: Tuple[float, float],
|
||||
pt2: Tuple[float, float],
|
||||
pt3: Tuple[float, float],
|
||||
) -> None:
|
||||
_p = self._p
|
||||
self._commands.append(("rrcurveto", _p(pt1) + _p(pt2) + _p(pt3)))
|
||||
|
||||
def _closePath(self) -> None:
|
||||
pass
|
||||
|
||||
def _endPath(self) -> None:
|
||||
pass
|
||||
|
||||
def getCharString(
|
||||
self,
|
||||
private: Dict | None = None,
|
||||
globalSubrs: List | None = None,
|
||||
optimize: bool = True,
|
||||
) -> T2CharString:
|
||||
commands = self._commands
|
||||
if optimize:
|
||||
maxstack = 48 if not self._CFF2 else 513
|
||||
commands = specializeCommands(
|
||||
commands, generalizeFirst=False, maxstack=maxstack
|
||||
)
|
||||
program = commandsToProgram(commands)
|
||||
if self._width is not None:
|
||||
assert (
|
||||
not self._CFF2
|
||||
), "CFF2 does not allow encoding glyph width in CharString."
|
||||
program.insert(0, otRound(self._width))
|
||||
if not self._CFF2:
|
||||
program.append("endchar")
|
||||
charString = T2CharString(
|
||||
program=program, private=private, globalSubrs=globalSubrs
|
||||
)
|
||||
return charString
|
||||
55
venv/lib/python3.13/site-packages/fontTools/pens/teePen.py
Normal file
55
venv/lib/python3.13/site-packages/fontTools/pens/teePen.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""Pen multiplexing drawing to one or more pens."""
|
||||
|
||||
from fontTools.pens.basePen import AbstractPen
|
||||
|
||||
|
||||
__all__ = ["TeePen"]
|
||||
|
||||
|
||||
class TeePen(AbstractPen):
|
||||
"""Pen multiplexing drawing to one or more pens.
|
||||
|
||||
Use either as TeePen(pen1, pen2, ...) or TeePen(iterableOfPens)."""
|
||||
|
||||
def __init__(self, *pens):
|
||||
if len(pens) == 1:
|
||||
pens = pens[0]
|
||||
self.pens = pens
|
||||
|
||||
def moveTo(self, p0):
|
||||
for pen in self.pens:
|
||||
pen.moveTo(p0)
|
||||
|
||||
def lineTo(self, p1):
|
||||
for pen in self.pens:
|
||||
pen.lineTo(p1)
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
for pen in self.pens:
|
||||
pen.qCurveTo(*points)
|
||||
|
||||
def curveTo(self, *points):
|
||||
for pen in self.pens:
|
||||
pen.curveTo(*points)
|
||||
|
||||
def closePath(self):
|
||||
for pen in self.pens:
|
||||
pen.closePath()
|
||||
|
||||
def endPath(self):
|
||||
for pen in self.pens:
|
||||
pen.endPath()
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
for pen in self.pens:
|
||||
pen.addComponent(glyphName, transformation)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from fontTools.pens.basePen import _TestPen
|
||||
|
||||
pen = TeePen(_TestPen(), _TestPen())
|
||||
pen.moveTo((0, 0))
|
||||
pen.lineTo((0, 100))
|
||||
pen.curveTo((50, 75), (60, 50), (50, 25))
|
||||
pen.closePath()
|
||||
115
venv/lib/python3.13/site-packages/fontTools/pens/transformPen.py
Normal file
115
venv/lib/python3.13/site-packages/fontTools/pens/transformPen.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
from fontTools.pens.filterPen import FilterPen, FilterPointPen
|
||||
|
||||
|
||||
__all__ = ["TransformPen", "TransformPointPen"]
|
||||
|
||||
|
||||
class TransformPen(FilterPen):
|
||||
"""Pen that transforms all coordinates using a Affine transformation,
|
||||
and passes them to another pen.
|
||||
"""
|
||||
|
||||
def __init__(self, outPen, transformation):
|
||||
"""The 'outPen' argument is another pen object. It will receive the
|
||||
transformed coordinates. The 'transformation' argument can either
|
||||
be a six-tuple, or a fontTools.misc.transform.Transform object.
|
||||
"""
|
||||
super(TransformPen, self).__init__(outPen)
|
||||
if not hasattr(transformation, "transformPoint"):
|
||||
from fontTools.misc.transform import Transform
|
||||
|
||||
transformation = Transform(*transformation)
|
||||
self._transformation = transformation
|
||||
self._transformPoint = transformation.transformPoint
|
||||
self._stack = []
|
||||
|
||||
def moveTo(self, pt):
|
||||
self._outPen.moveTo(self._transformPoint(pt))
|
||||
|
||||
def lineTo(self, pt):
|
||||
self._outPen.lineTo(self._transformPoint(pt))
|
||||
|
||||
def curveTo(self, *points):
|
||||
self._outPen.curveTo(*self._transformPoints(points))
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
if points[-1] is None:
|
||||
points = self._transformPoints(points[:-1]) + [None]
|
||||
else:
|
||||
points = self._transformPoints(points)
|
||||
self._outPen.qCurveTo(*points)
|
||||
|
||||
def _transformPoints(self, points):
|
||||
transformPoint = self._transformPoint
|
||||
return [transformPoint(pt) for pt in points]
|
||||
|
||||
def closePath(self):
|
||||
self._outPen.closePath()
|
||||
|
||||
def endPath(self):
|
||||
self._outPen.endPath()
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
transformation = self._transformation.transform(transformation)
|
||||
self._outPen.addComponent(glyphName, transformation)
|
||||
|
||||
|
||||
class TransformPointPen(FilterPointPen):
|
||||
"""PointPen that transforms all coordinates using a Affine transformation,
|
||||
and passes them to another PointPen.
|
||||
|
||||
For example::
|
||||
|
||||
>>> from fontTools.pens.recordingPen import RecordingPointPen
|
||||
>>> rec = RecordingPointPen()
|
||||
>>> pen = TransformPointPen(rec, (2, 0, 0, 2, -10, 5))
|
||||
>>> v = iter(rec.value)
|
||||
>>> pen.beginPath(identifier="contour-0")
|
||||
>>> next(v)
|
||||
('beginPath', (), {'identifier': 'contour-0'})
|
||||
|
||||
>>> pen.addPoint((100, 100), "line")
|
||||
>>> next(v)
|
||||
('addPoint', ((190, 205), 'line', False, None), {})
|
||||
|
||||
>>> pen.endPath()
|
||||
>>> next(v)
|
||||
('endPath', (), {})
|
||||
|
||||
>>> pen.addComponent("a", (1, 0, 0, 1, -10, 5), identifier="component-0")
|
||||
>>> next(v)
|
||||
('addComponent', ('a', <Transform [2 0 0 2 -30 15]>), {'identifier': 'component-0'})
|
||||
"""
|
||||
|
||||
def __init__(self, outPointPen, transformation):
|
||||
"""The 'outPointPen' argument is another point pen object.
|
||||
It will receive the transformed coordinates.
|
||||
The 'transformation' argument can either be a six-tuple, or a
|
||||
fontTools.misc.transform.Transform object.
|
||||
"""
|
||||
super().__init__(outPointPen)
|
||||
if not hasattr(transformation, "transformPoint"):
|
||||
from fontTools.misc.transform import Transform
|
||||
|
||||
transformation = Transform(*transformation)
|
||||
self._transformation = transformation
|
||||
self._transformPoint = transformation.transformPoint
|
||||
|
||||
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
|
||||
self._outPen.addPoint(
|
||||
self._transformPoint(pt), segmentType, smooth, name, **kwargs
|
||||
)
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation, **kwargs):
|
||||
transformation = self._transformation.transform(transformation)
|
||||
self._outPen.addComponent(baseGlyphName, transformation, **kwargs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from fontTools.pens.basePen import _TestPen
|
||||
|
||||
pen = TransformPen(_TestPen(None), (2, 0, 0.5, 2, -10, 0))
|
||||
pen.moveTo((0, 0))
|
||||
pen.lineTo((0, 100))
|
||||
pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0))
|
||||
pen.closePath()
|
||||
335
venv/lib/python3.13/site-packages/fontTools/pens/ttGlyphPen.py
Normal file
335
venv/lib/python3.13/site-packages/fontTools/pens/ttGlyphPen.py
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
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)
|
||||
29
venv/lib/python3.13/site-packages/fontTools/pens/wxPen.py
Normal file
29
venv/lib/python3.13/site-packages/fontTools/pens/wxPen.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
__all__ = ["WxPen"]
|
||||
|
||||
|
||||
class WxPen(BasePen):
|
||||
def __init__(self, glyphSet, path=None):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
if path is None:
|
||||
import wx
|
||||
|
||||
path = wx.GraphicsRenderer.GetDefaultRenderer().CreatePath()
|
||||
self.path = path
|
||||
|
||||
def _moveTo(self, p):
|
||||
self.path.MoveToPoint(*p)
|
||||
|
||||
def _lineTo(self, p):
|
||||
self.path.AddLineToPoint(*p)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
self.path.AddCurveToPoint(*p1 + p2 + p3)
|
||||
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
self.path.AddQuadCurveToPoint(*p1 + p2)
|
||||
|
||||
def _closePath(self):
|
||||
self.path.CloseSubpath()
|
||||
Loading…
Add table
Add a link
Reference in a new issue