asd
This commit is contained in:
@ -0,0 +1 @@
|
||||
"""Empty __init__.py file to signal Python this directory is a package."""
|
||||
52
venv/lib/python3.12/site-packages/fontTools/pens/areaPen.py
Normal file
52
venv/lib/python3.12/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.12/site-packages/fontTools/pens/basePen.py
Normal file
475
venv/lib/python3.12/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.12/site-packages/fontTools/pens/cairoPen.py
Normal file
26
venv/lib/python3.12/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.12/site-packages/fontTools/pens/cocoaPen.py
Normal file
26
venv/lib/python3.12/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.12/site-packages/fontTools/pens/cu2quPen.py
Normal file
325
venv/lib/python3.12/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.12/site-packages/fontTools/pens/filterPen.py
Normal file
241
venv/lib/python3.12/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.12/site-packages/fontTools/pens/freetypePen.py
Normal file
462
venv/lib/python3.12/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})]")
|
||||
13745
venv/lib/python3.12/site-packages/fontTools/pens/momentsPen.c
Normal file
13745
venv/lib/python3.12/site-packages/fontTools/pens/momentsPen.c
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
882
venv/lib/python3.12/site-packages/fontTools/pens/momentsPen.py
Normal file
882
venv/lib/python3.12/site-packages/fontTools/pens/momentsPen.py
Normal file
@ -0,0 +1,882 @@
|
||||
from fontTools.pens.basePen import BasePen, OpenContourError
|
||||
|
||||
try:
|
||||
import cython
|
||||
|
||||
COMPILED = cython.compiled
|
||||
except (AttributeError, ImportError):
|
||||
# if cython not installed, use mock module with no-op decorators and types
|
||||
from fontTools.misc import cython
|
||||
|
||||
COMPILED = False
|
||||
|
||||
|
||||
__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
|
||||
600
venv/lib/python3.12/site-packages/fontTools/pens/pointPen.py
Normal file
600
venv/lib/python3.12/site-packages/fontTools/pens/pointPen.py
Normal file
@ -0,0 +1,600 @@
|
||||
"""
|
||||
=========
|
||||
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.
|
||||
"""
|
||||
|
||||
import math
|
||||
from typing import Any, Optional, Tuple, Dict
|
||||
|
||||
from fontTools.misc.loggingTools import LogMixin
|
||||
from fontTools.pens.basePen import AbstractPen, MissingComponentError, PenError
|
||||
from fontTools.misc.transform import DecomposedTransform, Identity
|
||||
|
||||
__all__ = [
|
||||
"AbstractPointPen",
|
||||
"BasePointToSegmentPen",
|
||||
"PointToSegmentPen",
|
||||
"SegmentToPointPen",
|
||||
"GuessSmoothPointPen",
|
||||
"ReverseContourPointPen",
|
||||
]
|
||||
|
||||
|
||||
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):
|
||||
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):
|
||||
"""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):
|
||||
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 = [("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 = []
|
||||
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=False):
|
||||
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):
|
||||
if guessSmooth:
|
||||
self.pen = GuessSmoothPointPen(pointPen)
|
||||
else:
|
||||
self.pen = pointPen
|
||||
self.contour = None
|
||||
|
||||
def _flushContour(self):
|
||||
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]
|
||||
det = a * d - b * c
|
||||
if a * d - b * c < 0:
|
||||
pen = ReverseContourPointPen(pen)
|
||||
glyph.drawPoints(pen)
|
||||
29
venv/lib/python3.12/site-packages/fontTools/pens/qtPen.py
Normal file
29
venv/lib/python3.12/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.12/site-packages/fontTools/pens/qu2cuPen.py
Normal file
105
venv/lib/python3.12/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.12/site-packages/fontTools/pens/recordingPen.py
Normal file
335
venv/lib/python3.12/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.12/site-packages/fontTools/pens/roundingPen.py
Normal file
130
venv/lib/python3.12/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=baseGlyphName,
|
||||
transformation=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,307 @@
|
||||
"""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))
|
||||
|
||||
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):
|
||||
self._update()
|
||||
|
||||
def _endPath(self):
|
||||
p0 = self._getCurrentPoint()
|
||||
if p0 != self._startPoint:
|
||||
raise OpenContourError("Glyph statistics not defined on open contours.")
|
||||
|
||||
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.12/site-packages/fontTools/pens/svgPathPen.py
Normal file
310
venv/lib/python3.12/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,68 @@
|
||||
# Copyright (c) 2009 Type Supply LLC
|
||||
# Author: Tal Leming
|
||||
|
||||
from fontTools.misc.roundTools import otRound, roundFunc
|
||||
from fontTools.misc.psCharStrings import T2CharString
|
||||
from fontTools.pens.basePen import BasePen
|
||||
from fontTools.cffLib.specializer import specializeCommands, commandsToProgram
|
||||
|
||||
|
||||
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, glyphSet, roundTolerance=0.5, CFF2=False):
|
||||
super(T2CharStringPen, self).__init__(glyphSet)
|
||||
self.round = roundFunc(roundTolerance)
|
||||
self._CFF2 = CFF2
|
||||
self._width = width
|
||||
self._commands = []
|
||||
self._p0 = (0, 0)
|
||||
|
||||
def _p(self, pt):
|
||||
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):
|
||||
self._commands.append(("rmoveto", self._p(pt)))
|
||||
|
||||
def _lineTo(self, pt):
|
||||
self._commands.append(("rlineto", self._p(pt)))
|
||||
|
||||
def _curveToOne(self, pt1, pt2, pt3):
|
||||
_p = self._p
|
||||
self._commands.append(("rrcurveto", _p(pt1) + _p(pt2) + _p(pt3)))
|
||||
|
||||
def _closePath(self):
|
||||
pass
|
||||
|
||||
def _endPath(self):
|
||||
pass
|
||||
|
||||
def getCharString(self, private=None, globalSubrs=None, optimize=True):
|
||||
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.12/site-packages/fontTools/pens/teePen.py
Normal file
55
venv/lib/python3.12/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.12/site-packages/fontTools/pens/transformPen.py
Normal file
115
venv/lib/python3.12/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.12/site-packages/fontTools/pens/ttGlyphPen.py
Normal file
335
venv/lib/python3.12/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.12/site-packages/fontTools/pens/wxPen.py
Normal file
29
venv/lib/python3.12/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()
|
||||
Reference in New Issue
Block a user