I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

View File

@ -0,0 +1 @@
"""Empty __init__.py file to signal Python this directory is a package."""

View 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

View 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()

View File

@ -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

View 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()

View 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()

View 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)

View File

@ -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", ())]

View 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

View 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 wont. 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 wont. 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 wont. 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 wont. 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)

View File

@ -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})]")

File diff suppressed because it is too large Load Diff

View 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),
],
)

View File

@ -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))

View File

@ -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

View 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)

View 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()

View 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

View File

@ -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)

View 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)

View File

@ -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")

View File

@ -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", ()

View 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,
)

View File

@ -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:])

View 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())

View File

@ -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

View 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()

View 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()

View 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)

View 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()