This was needed for when we were supporting the old robofab.ufoLib where anchors were represented as single-point-with-a-name contours. With latest ufoLib, even when glif format is 1, anchors are stored separately and are not drawn with pens (only proper contours and components are passed on in the draw method). So there is no longer need to special-case single points. It's not the job of the cu2qu pens to filter those out.
235 lines
9.5 KiB
Python
235 lines
9.5 KiB
Python
from __future__ import print_function, division, absolute_import
|
|
from cu2qu import curve_to_quadratic
|
|
from fontTools.pens.basePen import AbstractPen, decomposeSuperBezierSegment
|
|
from fontTools.pens.reverseContourPen import ReverseContourPen
|
|
from ufoLib.pointPen import BasePointToSegmentPen
|
|
from ufoLib.pointPen import ReverseContourPointPen
|
|
|
|
|
|
class Cu2QuPen(AbstractPen):
|
|
""" A filter pen to convert cubic bezier curves to quadratic b-splines
|
|
using the FontTools SegmentPen protocol.
|
|
|
|
other_pen: another SegmentPen used to draw the transformed outline.
|
|
max_err: maximum approximation error in font units.
|
|
reverse_direction: flip the contours' direction but keep starting point.
|
|
stats: a dictionary counting the point numbers of quadratic segments.
|
|
ignore_single_points: don't emit contours containing only a single point
|
|
|
|
NOTE: The "ignore_single_points" argument is deprecated since v1.3.0,
|
|
which dropped Robofab subpport. It's no longer needed to special-case
|
|
UFO2-style anchors (aka "named points") when using ufoLib >= 2.0,
|
|
as these are no longer drawn onto pens as single-point contours,
|
|
but are handled separately as anchors.
|
|
"""
|
|
|
|
def __init__(self, other_pen, max_err, reverse_direction=False,
|
|
stats=None, ignore_single_points=False):
|
|
if reverse_direction:
|
|
self.pen = ReverseContourPen(other_pen)
|
|
else:
|
|
self.pen = other_pen
|
|
self.max_err = max_err
|
|
self.stats = stats
|
|
if ignore_single_points:
|
|
import warnings
|
|
warnings.warn("ignore_single_points is deprecated and "
|
|
"will be removed in future versions",
|
|
UserWarning, stacklevel=2)
|
|
self.ignore_single_points = ignore_single_points
|
|
self.start_pt = None
|
|
self.current_pt = None
|
|
|
|
def _check_contour_is_open(self):
|
|
if self.current_pt is None:
|
|
raise AssertionError("moveTo is required")
|
|
|
|
def _check_contour_is_closed(self):
|
|
if self.current_pt is not None:
|
|
raise AssertionError("closePath or endPath is required")
|
|
|
|
def _add_moveTo(self):
|
|
if self.start_pt is not None:
|
|
self.pen.moveTo(self.start_pt)
|
|
self.start_pt = None
|
|
|
|
def moveTo(self, pt):
|
|
self._check_contour_is_closed()
|
|
self.start_pt = self.current_pt = pt
|
|
if not self.ignore_single_points:
|
|
self._add_moveTo()
|
|
|
|
def lineTo(self, pt):
|
|
self._check_contour_is_open()
|
|
self._add_moveTo()
|
|
self.pen.lineTo(pt)
|
|
self.current_pt = pt
|
|
|
|
def qCurveTo(self, *points):
|
|
self._check_contour_is_open()
|
|
n = len(points)
|
|
if n == 1:
|
|
self.lineTo(points[0])
|
|
elif n > 1:
|
|
self._add_moveTo()
|
|
self.pen.qCurveTo(*points)
|
|
self.current_pt = points[-1]
|
|
else:
|
|
raise AssertionError("illegal qcurve segment point count: %d" % n)
|
|
|
|
def _curve_to_quadratic(self, pt1, pt2, pt3):
|
|
curve = (self.current_pt, pt1, pt2, pt3)
|
|
quadratic = curve_to_quadratic(curve, self.max_err)
|
|
if self.stats is not None:
|
|
n = str(len(quadratic) - 2)
|
|
self.stats[n] = self.stats.get(n, 0) + 1
|
|
self.qCurveTo(*quadratic[1:])
|
|
|
|
def curveTo(self, *points):
|
|
self._check_contour_is_open()
|
|
n = len(points)
|
|
if n == 3:
|
|
# this is the most common case, so we special-case it
|
|
self._curve_to_quadratic(*points)
|
|
elif n > 3:
|
|
for segment in decomposeSuperBezierSegment(points):
|
|
self._curve_to_quadratic(*segment)
|
|
elif n == 2:
|
|
self.qCurveTo(*points)
|
|
elif n == 1:
|
|
self.lineTo(points[0])
|
|
else:
|
|
raise AssertionError("illegal curve segment point count: %d" % n)
|
|
|
|
def closePath(self):
|
|
self._check_contour_is_open()
|
|
if self.start_pt is None:
|
|
# if 'start_pt' is _not_ None, we are ignoring single-point paths
|
|
self.pen.closePath()
|
|
self.current_pt = self.start_pt = None
|
|
|
|
def endPath(self):
|
|
self._check_contour_is_open()
|
|
if self.start_pt is None:
|
|
self.pen.endPath()
|
|
self.current_pt = self.start_pt = None
|
|
|
|
def addComponent(self, glyphName, transformation):
|
|
self._check_contour_is_closed()
|
|
self.pen.addComponent(glyphName, transformation)
|
|
|
|
|
|
class Cu2QuPointPen(BasePointToSegmentPen):
|
|
""" A filter pen to convert cubic bezier curves to quadratic b-splines
|
|
using the RoboFab PointPen protocol.
|
|
|
|
other_point_pen: another PointPen used to draw the transformed outline.
|
|
max_err: maximum approximation error in font units.
|
|
reverse_direction: reverse the winding direction of all contours.
|
|
stats: a dictionary counting the point numbers of quadratic segments.
|
|
"""
|
|
|
|
def __init__(self, other_point_pen, max_err, reverse_direction=False,
|
|
stats=None):
|
|
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
|
|
|
|
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)
|
|
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))
|
|
new_segments.append(["qcurve", 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 = []
|
|
for i, (segment_type, points) in enumerate(segments):
|
|
if segment_type in ("move", "line"):
|
|
assert len(points) == 1, (
|
|
"illegal line segment point count: %d" % len(points))
|
|
pt, smooth, name, kwargs = points[0]
|
|
pen.addPoint(pt, segment_type, smooth, name, **kwargs)
|
|
elif segment_type == "qcurve":
|
|
assert len(points) >= 2, (
|
|
"illegal qcurve segment point count: %d" % len(points))
|
|
offcurves = points[:-1]
|
|
if offcurves:
|
|
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]
|
|
pen.addPoint(pt, segment_type, smooth, name, **kwargs)
|
|
else:
|
|
# 'curve' segments must have been converted to 'qcurve' by now
|
|
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)
|