Merge branch 'master' into tests
This commit is contained in:
commit
57fa1f46f4
310
Lib/cu2qu/pens.py
Normal file
310
Lib/cu2qu/pens.py
Normal file
@ -0,0 +1,310 @@
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from cu2qu import curve_to_quadratic
|
||||
from fontTools.pens.basePen import AbstractPen, decomposeSuperBezierSegment
|
||||
|
||||
try:
|
||||
from ufoLib.pointPen import AbstractPointPen, BasePointToSegmentPen
|
||||
from ufoLib.pointPen import PointToSegmentPen, SegmentToPointPen
|
||||
except ImportError:
|
||||
from robofab.pens.pointPen import AbstractPointPen, BasePointToSegmentPen
|
||||
from robofab.pens.adapterPens import PointToSegmentPen, SegmentToPointPen
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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
|
||||
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))
|
||||
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))
|
||||
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()
|
||||
for segment_type, points in 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))
|
||||
for (pt, smooth, name, kwargs) in points[:-1]:
|
||||
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)
|
||||
pen.endPath()
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation):
|
||||
assert self.currentPath is None
|
||||
self.pen.addComponent(baseGlyphName, transformation)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
(Copied from robofab.pens.reverseContourPointPen)
|
||||
|
||||
TODO(anthrotype) Move this to future "penBox" package?
|
||||
"""
|
||||
|
||||
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()
|
||||
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()
|
||||
for pt, nextSegmentType, smooth, name in contour:
|
||||
if nextSegmentType is not None:
|
||||
segmentType = lastSegmentType
|
||||
lastSegmentType = nextSegmentType
|
||||
else:
|
||||
segmentType = None
|
||||
pen.addPoint(pt, segmentType=segmentType, smooth=smooth, name=name)
|
||||
pen.endPath()
|
||||
|
||||
def beginPath(self):
|
||||
assert self.currentContour is None
|
||||
self.currentContour = []
|
||||
|
||||
def endPath(self):
|
||||
assert self.currentContour is not None
|
||||
self._flushContour()
|
||||
self.currentContour = None
|
||||
|
||||
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
|
||||
self.currentContour.append((pt, segmentType, smooth, name))
|
||||
|
||||
def addComponent(self, glyphName, transform):
|
||||
assert self.currentContour is None
|
||||
self.pen.addComponent(glyphName, transform)
|
||||
|
||||
|
||||
class ReverseContourPen(SegmentToPointPen):
|
||||
""" Same as 'ReverseContourPointPen' but using the SegmentPen protocol. """
|
||||
|
||||
def __init__(self, other_pen):
|
||||
adapter_point_pen = PointToSegmentPen(other_pen)
|
||||
reverse_point_pen = ReverseContourPointPen(adapter_point_pen)
|
||||
SegmentToPointPen.__init__(self, reverse_point_pen)
|
16
Lib/cu2qu/test/__init__.py
Normal file
16
Lib/cu2qu/test/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
import os
|
||||
try:
|
||||
from ufoLib.glifLib import GlyphSet
|
||||
except ImportError:
|
||||
from robofab.glifLib import GlyphSet
|
||||
|
||||
DATADIR = os.path.join(
|
||||
os.path.abspath(os.path.dirname(os.path.realpath(__file__))), 'data')
|
||||
CUBIC_GLYPHS = GlyphSet(os.path.join(DATADIR, 'cubic'))
|
||||
QUAD_GLYPHS = GlyphSet(os.path.join(DATADIR, 'quadratic'))
|
||||
|
||||
import unittest
|
||||
# Python 3 renamed 'assertRaisesRegexp' to 'assertRaisesRegex', and fires
|
||||
# deprecation warnings if a program uses the old name.
|
||||
if not hasattr(unittest.TestCase, 'assertRaisesRegex'):
|
||||
unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
|
58
Lib/cu2qu/test/data/cubic/A_.glif
Executable file
58
Lib/cu2qu/test/data/cubic/A_.glif
Executable file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<glyph name="A" format="1">
|
||||
<unicode hex="0041"/>
|
||||
<advance width="668"/>
|
||||
<outline>
|
||||
<contour>
|
||||
<point x="501" y="347" type="line"/>
|
||||
<point x="412" y="393"/>
|
||||
<point x="359" y="401"/>
|
||||
<point x="282" y="401" type="curve" smooth="yes"/>
|
||||
<point x="192" y="401"/>
|
||||
<point x="131" y="390"/>
|
||||
<point x="114" y="347" type="curve"/>
|
||||
<point x="119" y="347"/>
|
||||
<point x="127" y="330"/>
|
||||
<point x="127" y="316" type="curve" smooth="yes"/>
|
||||
<point x="127" y="292"/>
|
||||
<point x="102" y="277"/>
|
||||
<point x="71" y="277" type="curve" smooth="yes"/>
|
||||
<point x="37" y="277"/>
|
||||
<point x="9" y="296"/>
|
||||
<point x="9" y="335" type="curve" smooth="yes"/>
|
||||
<point x="9" y="403"/>
|
||||
<point x="95" y="458"/>
|
||||
<point x="213" y="458" type="curve" smooth="yes"/>
|
||||
<point x="278" y="458"/>
|
||||
<point x="428" y="439"/>
|
||||
<point x="526" y="385" type="curve"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point x="629" y="678" type="line"/>
|
||||
<point x="615" y="685"/>
|
||||
<point x="596" y="692"/>
|
||||
<point x="578" y="692" type="curve" smooth="yes"/>
|
||||
<point x="421" y="692"/>
|
||||
<point x="204" y="149"/>
|
||||
<point x="204" y="-58" type="curve" smooth="yes"/>
|
||||
<point x="204" y="-90"/>
|
||||
<point x="212" y="-105"/>
|
||||
<point x="218" y="-114" type="curve"/>
|
||||
<point x="164" y="-114"/>
|
||||
<point x="84" y="-112"/>
|
||||
<point x="84" y="-8" type="curve" smooth="yes"/>
|
||||
<point x="84" y="191"/>
|
||||
<point x="374" y="750"/>
|
||||
<point x="613" y="750" type="curve" smooth="yes"/>
|
||||
<point x="636" y="750"/>
|
||||
<point x="668" y="746"/>
|
||||
<point x="692" y="736" type="curve"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point x="391" y="0" type="line"/>
|
||||
<point x="547" y="736" type="line"/>
|
||||
<point x="692" y="736" type="line"/>
|
||||
<point x="535" y="0" type="line"/>
|
||||
</contour>
|
||||
</outline>
|
||||
</glyph>
|
9
Lib/cu2qu/test/data/cubic/A_acute.glif
Executable file
9
Lib/cu2qu/test/data/cubic/A_acute.glif
Executable file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<glyph name="Aacute" format="1">
|
||||
<unicode hex="00C1"/>
|
||||
<advance width="668"/>
|
||||
<outline>
|
||||
<component base="A"/>
|
||||
<component base="acute" xOffset="366" yOffset="250"/>
|
||||
</outline>
|
||||
</glyph>
|
56
Lib/cu2qu/test/data/cubic/E_acute.glif
Executable file
56
Lib/cu2qu/test/data/cubic/E_acute.glif
Executable file
@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<glyph name="E" format="1">
|
||||
<unicode hex="0045"/>
|
||||
<advance width="459"/>
|
||||
<outline>
|
||||
<contour>
|
||||
<point x="461" y="180" type="line"/>
|
||||
<point x="425" y="114"/>
|
||||
<point x="337" y="78"/>
|
||||
<point x="276" y="78" type="curve" smooth="yes"/>
|
||||
<point x="208" y="78"/>
|
||||
<point x="181" y="123"/>
|
||||
<point x="181" y="179" type="curve" smooth="yes"/>
|
||||
<point x="181" y="271"/>
|
||||
<point x="253" y="394"/>
|
||||
<point x="360" y="418" type="curve"/>
|
||||
<point x="288" y="437"/>
|
||||
<point x="259" y="494"/>
|
||||
<point x="259" y="552" type="curve" smooth="yes"/>
|
||||
<point x="259" y="622"/>
|
||||
<point x="302" y="693"/>
|
||||
<point x="361" y="693" type="curve" smooth="yes"/>
|
||||
<point x="388" y="693"/>
|
||||
<point x="416" y="678"/>
|
||||
<point x="416" y="637" type="curve" smooth="yes"/>
|
||||
<point x="416" y="603"/>
|
||||
<point x="397" y="558"/>
|
||||
<point x="372" y="550" type="curve"/>
|
||||
<point x="386" y="531"/>
|
||||
<point x="402" y="523"/>
|
||||
<point x="419" y="523" type="curve" smooth="yes"/>
|
||||
<point x="459" y="523"/>
|
||||
<point x="493" y="567"/>
|
||||
<point x="493" y="627" type="curve" smooth="yes"/>
|
||||
<point x="493" y="709"/>
|
||||
<point x="429" y="752"/>
|
||||
<point x="350" y="752" type="curve" smooth="yes"/>
|
||||
<point x="222" y="752"/>
|
||||
<point x="131" y="640"/>
|
||||
<point x="131" y="537" type="curve" smooth="yes"/>
|
||||
<point x="131" y="492"/>
|
||||
<point x="149" y="448"/>
|
||||
<point x="192" y="417" type="curve"/>
|
||||
<point x="77" y="389"/>
|
||||
<point x="7" y="263"/>
|
||||
<point x="7" y="158" type="curve" smooth="yes"/>
|
||||
<point x="7" y="64"/>
|
||||
<point x="63" y="-18"/>
|
||||
<point x="191" y="-18" type="curve" smooth="yes"/>
|
||||
<point x="310" y="-18"/>
|
||||
<point x="432" y="52"/>
|
||||
<point x="484" y="170" type="curve"/>
|
||||
</contour>
|
||||
<component base="acute" xOffset="210" yOffset="250"/>
|
||||
</outline>
|
||||
</glyph>
|
80
Lib/cu2qu/test/data/cubic/a.glif
Normal file
80
Lib/cu2qu/test/data/cubic/a.glif
Normal file
@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<glyph name="a" format="1">
|
||||
<unicode hex="0061"/>
|
||||
<advance width="1118"/>
|
||||
<outline>
|
||||
<contour>
|
||||
<point x="771" y="183" type="line"/>
|
||||
<point x="771" y="123"/>
|
||||
<point x="782" y="41"/>
|
||||
<point x="802" y="0" type="curve"/>
|
||||
<point x="1010" y="0" type="line"/>
|
||||
<point x="1010" y="17" type="line"/>
|
||||
<point x="984" y="76"/>
|
||||
<point x="971" y="166"/>
|
||||
<point x="971" y="238" type="curve" smooth="yes"/>
|
||||
<point x="971" y="738" type="line"/>
|
||||
<point x="971" y="981"/>
|
||||
<point x="803" y="1102"/>
|
||||
<point x="566" y="1102" type="curve" smooth="yes"/>
|
||||
<point x="301" y="1102"/>
|
||||
<point x="133" y="935"/>
|
||||
<point x="133" y="782" type="curve"/>
|
||||
<point x="333" y="782" type="line"/>
|
||||
<point x="333" y="867"/>
|
||||
<point x="421" y="945"/>
|
||||
<point x="554" y="945" type="curve" smooth="yes"/>
|
||||
<point x="697" y="945"/>
|
||||
<point x="771" y="864"/>
|
||||
<point x="771" y="740" type="curve"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point x="804" y="661" type="line"/>
|
||||
<point x="596" y="661" type="line"/>
|
||||
<point x="301" y="661"/>
|
||||
<point x="111" y="539"/>
|
||||
<point x="111" y="303" type="curve" smooth="yes"/>
|
||||
<point x="111" y="123"/>
|
||||
<point x="257" y="-20"/>
|
||||
<point x="480" y="-20" type="curve" smooth="yes"/>
|
||||
<point x="711" y="-20"/>
|
||||
<point x="857" y="170"/>
|
||||
<point x="874" y="277" type="curve"/>
|
||||
<point x="789" y="370" type="line"/>
|
||||
<point x="789" y="279"/>
|
||||
<point x="673" y="151"/>
|
||||
<point x="510" y="151" type="curve" smooth="yes"/>
|
||||
<point x="377" y="151"/>
|
||||
<point x="311" y="230"/>
|
||||
<point x="311" y="331" type="curve" smooth="yes"/>
|
||||
<point x="311" y="462"/>
|
||||
<point x="425" y="524"/>
|
||||
<point x="629" y="524" type="curve"/>
|
||||
<point x="806" y="524" type="line"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="top" x="582" y="1290" type="move"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="bottom" x="501" y="0" type="move"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="ogonek" x="974" y="0" type="move"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="rhalfring" x="700" y="1290" type="move"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="top_dd" x="1118" y="1600" type="move"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="bottom_dd" x="1118" y="-407" type="move"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="rhotichook" x="879" y="654" type="move"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="top0315" x="1118" y="1290" type="move"/>
|
||||
</contour>
|
||||
</outline>
|
||||
</glyph>
|
13
Lib/cu2qu/test/data/cubic/acute.glif
Executable file
13
Lib/cu2qu/test/data/cubic/acute.glif
Executable file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<glyph name="acute" format="1">
|
||||
<unicode hex="00B4"/>
|
||||
<advance width="316"/>
|
||||
<outline>
|
||||
<contour>
|
||||
<point x="120" y="561" type="line"/>
|
||||
<point x="195" y="561" type="line"/>
|
||||
<point x="316" y="750" type="line"/>
|
||||
<point x="211" y="750" type="line"/>
|
||||
</contour>
|
||||
</outline>
|
||||
</glyph>
|
16
Lib/cu2qu/test/data/cubic/contents.plist
Normal file
16
Lib/cu2qu/test/data/cubic/contents.plist
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>A</key>
|
||||
<string>A_.glif</string>
|
||||
<key>Aacute</key>
|
||||
<string>A_acute.glif</string>
|
||||
<key>Eacute</key>
|
||||
<string>E_acute.glif</string>
|
||||
<key>a</key>
|
||||
<string>a.glif</string>
|
||||
<key>acute</key>
|
||||
<string>acute.glif</string>
|
||||
</dict>
|
||||
</plist>
|
92
Lib/cu2qu/test/data/quadratic/A_.glif
Normal file
92
Lib/cu2qu/test/data/quadratic/A_.glif
Normal file
@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<glyph name="A" format="1">
|
||||
<unicode hex="0041"/>
|
||||
<advance width="668"/>
|
||||
<outline>
|
||||
<contour>
|
||||
<point x="501" y="347" type="line"/>
|
||||
<point x="456.5" y="370"/>
|
||||
<point x="386.50000000000006" y="393.16666666666674"/>
|
||||
<point x="320.5" y="401.0000000000001"/>
|
||||
<point x="282" y="401" type="qcurve" smooth="yes"/>
|
||||
<point x="214.5" y="401"/>
|
||||
<point x="126.75" y="379.25"/>
|
||||
<point x="114" y="347" type="qcurve"/>
|
||||
<point x="117.75" y="347"/>
|
||||
<point x="127" y="326.5"/>
|
||||
<point x="127" y="316" type="qcurve" smooth="yes"/>
|
||||
<point x="127" y="298"/>
|
||||
<point x="94.25" y="277"/>
|
||||
<point x="71" y="277" type="qcurve" smooth="yes"/>
|
||||
<point x="45.5" y="277"/>
|
||||
<point x="9" y="305.75"/>
|
||||
<point x="9" y="335" type="qcurve" smooth="yes"/>
|
||||
<point x="9" y="369"/>
|
||||
<point x="61.83333333333333" y="424.8333333333333"/>
|
||||
<point x="154" y="458"/>
|
||||
<point x="213" y="458" type="qcurve" smooth="yes"/>
|
||||
<point x="237.375" y="458"/>
|
||||
<point x="313.00520833333337" y="450.2916666666667"/>
|
||||
<point x="401.2447916666667" y="433.20833333333337"/>
|
||||
<point x="489.25" y="405.25"/>
|
||||
<point x="526" y="385" type="qcurve"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point x="629" y="678" type="line"/>
|
||||
<point x="618.5" y="683.25"/>
|
||||
<point x="591.5" y="692"/>
|
||||
<point x="578" y="692" type="qcurve" smooth="yes"/>
|
||||
<point x="544.3571428571429" y="692"/>
|
||||
<point x="471.67614188532553" y="631.7033527696793"/>
|
||||
<point x="398.9164237123422" y="527.9810495626823"/>
|
||||
<point x="330.9234693877552" y="396.2091836734695"/>
|
||||
<point x="272.54275996112733" y="251.76384839650146"/>
|
||||
<point x="228.6197764820214" y="110.02113702623913"/>
|
||||
<point x="204.00000000000009" y="-13.642857142857068"/>
|
||||
<point x="204" y="-58" type="qcurve" smooth="yes"/>
|
||||
<point x="204" y="-82"/>
|
||||
<point x="213.5" y="-107.25"/>
|
||||
<point x="218" y="-114" type="qcurve"/>
|
||||
<point x="197.75" y="-114"/>
|
||||
<point x="151.36458333333334" y="-109.60416666666667"/>
|
||||
<point x="110.13541666666666" y="-90.39583333333334"/>
|
||||
<point x="84" y="-47"/>
|
||||
<point x="84" y="-8" type="qcurve" smooth="yes"/>
|
||||
<point x="84" y="34.64285714285714"/>
|
||||
<point x="117.10762876579204" y="157.5352283770651"/>
|
||||
<point x="176.77793974732754" y="300.39552964042764"/>
|
||||
<point x="257.04591836734687" y="447.1479591836734"/>
|
||||
<point x="351.94655004859084" y="581.7167152575316"/>
|
||||
<point x="455.5148202137997" y="688.0259961127306"/>
|
||||
<point x="561.7857142857142" y="749.9999999999998"/>
|
||||
<point x="613" y="750" type="qcurve" smooth="yes"/>
|
||||
<point x="630.25" y="750"/>
|
||||
<point x="674" y="743.5"/>
|
||||
<point x="692" y="736" type="qcurve"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point x="391" y="0" type="line"/>
|
||||
<point x="547" y="736" type="line"/>
|
||||
<point x="692" y="736" type="line"/>
|
||||
<point x="535" y="0" type="line"/>
|
||||
</contour>
|
||||
</outline>
|
||||
<lib>
|
||||
<dict>
|
||||
<key>org.robofab.fontlab.guides</key>
|
||||
<dict>
|
||||
<key>hguides</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>angle</key>
|
||||
<integer>0</integer>
|
||||
<key>position</key>
|
||||
<integer>-180</integer>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>org.robofab.fontlab.mark</key>
|
||||
<integer>80</integer>
|
||||
</dict>
|
||||
</lib>
|
||||
</glyph>
|
63
Lib/cu2qu/test/data/quadratic/E_acute.glif
Normal file
63
Lib/cu2qu/test/data/quadratic/E_acute.glif
Normal file
@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<glyph name="E" format="1">
|
||||
<unicode hex="0045"/>
|
||||
<advance width="459"/>
|
||||
<outline>
|
||||
<contour>
|
||||
<point x="461" y="180" type="line"/>
|
||||
<point x="443" y="147"/>
|
||||
<point x="378.9166666666667" y="101.5"/>
|
||||
<point x="306.50000000000006" y="78"/>
|
||||
<point x="276" y="78" type="qcurve" smooth="yes"/>
|
||||
<point x="225" y="78"/>
|
||||
<point x="181" y="137"/>
|
||||
<point x="181" y="179" type="qcurve" smooth="yes"/>
|
||||
<point x="181" y="213.5"/>
|
||||
<point x="206.65104166666669" y="289.3854166666667"/>
|
||||
<point x="254.09895833333331" y="358.6145833333333"/>
|
||||
<point x="319.875" y="409"/>
|
||||
<point x="360" y="418" type="qcurve"/>
|
||||
<point x="306" y="432.25"/>
|
||||
<point x="259" y="508.5"/>
|
||||
<point x="259" y="552" type="qcurve" smooth="yes"/>
|
||||
<point x="259" y="587"/>
|
||||
<point x="285.41666666666663" y="651.6666666666667"/>
|
||||
<point x="331.5" y="693"/>
|
||||
<point x="361" y="693" type="qcurve" smooth="yes"/>
|
||||
<point x="381.25" y="693"/>
|
||||
<point x="416" y="667.75"/>
|
||||
<point x="416" y="637" type="qcurve" smooth="yes"/>
|
||||
<point x="416" y="611.5"/>
|
||||
<point x="390.75" y="556"/>
|
||||
<point x="372" y="550" type="qcurve"/>
|
||||
<point x="391.89473684210526" y="523"/>
|
||||
<point x="419" y="523" type="qcurve" smooth="yes"/>
|
||||
<point x="449" y="523"/>
|
||||
<point x="493" y="582"/>
|
||||
<point x="493" y="627" type="qcurve" smooth="yes"/>
|
||||
<point x="493" y="688.5"/>
|
||||
<point x="409.25" y="752"/>
|
||||
<point x="350" y="752" type="qcurve" smooth="yes"/>
|
||||
<point x="286" y="752"/>
|
||||
<point x="187.16666666666657" y="687.4166666666667"/>
|
||||
<point x="130.99999999999994" y="588.4999999999999"/>
|
||||
<point x="131" y="537" type="qcurve" smooth="yes"/>
|
||||
<point x="131" y="503.25"/>
|
||||
<point x="159.75" y="440.25"/>
|
||||
<point x="192" y="417" type="qcurve"/>
|
||||
<point x="134.5" y="403"/>
|
||||
<point x="51.58333333333333" y="319.58333333333337"/>
|
||||
<point x="6.999999999999977" y="210.5"/>
|
||||
<point x="7" y="158" type="qcurve" smooth="yes"/>
|
||||
<point x="7" y="111"/>
|
||||
<point x="45.66666666666666" y="30.83333333333333"/>
|
||||
<point x="126.99999999999997" y="-18.000000000000014"/>
|
||||
<point x="191" y="-18" type="qcurve" smooth="yes"/>
|
||||
<point x="250.5" y="-18"/>
|
||||
<point x="365.41666666666663" y="26.833333333333336"/>
|
||||
<point x="458" y="111"/>
|
||||
<point x="484" y="170" type="qcurve"/>
|
||||
</contour>
|
||||
<component base="acute" xOffset="210" yOffset="250"/>
|
||||
</outline>
|
||||
</glyph>
|
93
Lib/cu2qu/test/data/quadratic/a.glif
Normal file
93
Lib/cu2qu/test/data/quadratic/a.glif
Normal file
@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<glyph name="a" format="1">
|
||||
<unicode hex="0061"/>
|
||||
<advance width="1118"/>
|
||||
<outline>
|
||||
<contour>
|
||||
<point x="771" y="183" type="line"/>
|
||||
<point x="771" y="153"/>
|
||||
<point x="778.1666666666665" y="83.58333333333334"/>
|
||||
<point x="792" y="20.49999999999998"/>
|
||||
<point x="802" y="0" type="qcurve"/>
|
||||
<point x="1010" y="0" type="line"/>
|
||||
<point x="1010" y="17" type="line"/>
|
||||
<point x="990.5" y="61.25"/>
|
||||
<point x="971" y="184"/>
|
||||
<point x="971" y="238" type="qcurve" smooth="yes"/>
|
||||
<point x="971" y="738" type="line"/>
|
||||
<point x="971" y="859.5"/>
|
||||
<point x="867.25" y="1021.25"/>
|
||||
<point x="684.4999999999998" y="1101.9999999999995"/>
|
||||
<point x="566" y="1102" type="qcurve" smooth="yes"/>
|
||||
<point x="466.625" y="1102"/>
|
||||
<point x="306.8385416666667" y="1045.9739583333335"/>
|
||||
<point x="193.41145833333331" y="952.7760416666667"/>
|
||||
<point x="133" y="839.375"/>
|
||||
<point x="133" y="782" type="qcurve"/>
|
||||
<point x="333" y="782" type="line"/>
|
||||
<point x="333" y="824.5"/>
|
||||
<point x="388.08333333333337" y="898.9166666666667"/>
|
||||
<point x="487.50000000000006" y="945"/>
|
||||
<point x="554" y="945" type="qcurve" smooth="yes"/>
|
||||
<point x="661.25" y="945"/>
|
||||
<point x="771" y="833"/>
|
||||
<point x="771" y="740" type="qcurve"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point x="804" y="661" type="line"/>
|
||||
<point x="596" y="661" type="line"/>
|
||||
<point x="448.5" y="661"/>
|
||||
<point x="230.58333333333334" y="580.3333333333335"/>
|
||||
<point x="111.00000000000004" y="421"/>
|
||||
<point x="111" y="303" type="qcurve" smooth="yes"/>
|
||||
<point x="111" y="213"/>
|
||||
<point x="202.58333333333331" y="66.50000000000003"/>
|
||||
<point x="368.49999999999994" y="-19.99999999999999"/>
|
||||
<point x="480" y="-20" type="qcurve" smooth="yes"/>
|
||||
<point x="549.3000000000001" y="-20"/>
|
||||
<point x="666.664" y="20.413000000000004"/>
|
||||
<point x="760.4599999999999" y="86.76999999999998"/>
|
||||
<point x="828.5760000000001" y="165.96700000000004"/>
|
||||
<point x="868.8999999999999" y="244.90000000000003"/>
|
||||
<point x="874" y="277" type="qcurve"/>
|
||||
<point x="789" y="370" type="line"/>
|
||||
<point x="789" y="335.875"/>
|
||||
<point x="748.015625" y="259.765625"/>
|
||||
<point x="673.234375" y="192.984375"/>
|
||||
<point x="571.125" y="151"/>
|
||||
<point x="510" y="151" type="qcurve" smooth="yes"/>
|
||||
<point x="443.5" y="151"/>
|
||||
<point x="355.0833333333333" y="198.91666666666669"/>
|
||||
<point x="311.0000000000001" y="280.5"/>
|
||||
<point x="311" y="331" type="qcurve" smooth="yes"/>
|
||||
<point x="311" y="429.25"/>
|
||||
<point x="476" y="524"/>
|
||||
<point x="629" y="524" type="qcurve"/>
|
||||
<point x="806" y="524" type="line"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="top" x="582" y="1290" type="move"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="bottom" x="501" y="0" type="move"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="ogonek" x="974" y="0" type="move"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="rhalfring" x="700" y="1290" type="move"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="top_dd" x="1118" y="1600" type="move"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="bottom_dd" x="1118" y="-407" type="move"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="rhotichook" x="879" y="654" type="move"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<point name="top0315" x="1118" y="1290" type="move"/>
|
||||
</contour>
|
||||
</outline>
|
||||
</glyph>
|
13
Lib/cu2qu/test/data/quadratic/acute.glif
Executable file
13
Lib/cu2qu/test/data/quadratic/acute.glif
Executable file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<glyph name="acute" format="1">
|
||||
<unicode hex="00B4"/>
|
||||
<advance width="316"/>
|
||||
<outline>
|
||||
<contour>
|
||||
<point x="120" y="561" type="line"/>
|
||||
<point x="195" y="561" type="line"/>
|
||||
<point x="316" y="750" type="line"/>
|
||||
<point x="211" y="750" type="line"/>
|
||||
</contour>
|
||||
</outline>
|
||||
</glyph>
|
14
Lib/cu2qu/test/data/quadratic/contents.plist
Normal file
14
Lib/cu2qu/test/data/quadratic/contents.plist
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>A</key>
|
||||
<string>A_.glif</string>
|
||||
<key>Eacute</key>
|
||||
<string>E_acute.glif</string>
|
||||
<key>a</key>
|
||||
<string>a.glif</string>
|
||||
<key>acute</key>
|
||||
<string>acute.glif</string>
|
||||
</dict>
|
||||
</plist>
|
336
Lib/cu2qu/test/pens_test.py
Normal file
336
Lib/cu2qu/test/pens_test.py
Normal file
@ -0,0 +1,336 @@
|
||||
from __future__ import print_function, division, absolute_import
|
||||
import unittest
|
||||
|
||||
from cu2qu.pens import Cu2QuPen, Cu2QuPointPen
|
||||
from cu2qu.test import CUBIC_GLYPHS, QUAD_GLYPHS
|
||||
from cu2qu.test.utils import DummyGlyph, DummyPointGlyph
|
||||
from cu2qu.test.utils import DummyPen, DummyPointPen
|
||||
|
||||
MAX_ERR = 1.0
|
||||
|
||||
|
||||
class _TestPenMixin(object):
|
||||
"""Collection of tests that are shared by both the SegmentPen and the
|
||||
PointPen test cases, plus some helper methods.
|
||||
"""
|
||||
|
||||
def diff(self, expected, actual):
|
||||
import difflib
|
||||
expected = str(self.Glyph(expected)).splitlines(True)
|
||||
actual = str(self.Glyph(actual)).splitlines(True)
|
||||
diff = difflib.unified_diff(
|
||||
expected, actual, fromfile='expected', tofile='actual')
|
||||
return "".join(diff)
|
||||
|
||||
def convert_glyph(self, glyph, **kwargs):
|
||||
# draw source glyph onto a new glyph using a Cu2Qu pen and return it
|
||||
converted = self.Glyph()
|
||||
pen = getattr(converted, self.pen_getter_name)()
|
||||
quadpen = self.Cu2QuPen(pen, MAX_ERR, **kwargs)
|
||||
getattr(glyph, self.draw_method_name)(quadpen)
|
||||
return converted
|
||||
|
||||
def expect_glyph(self, source, expected):
|
||||
converted = self.convert_glyph(source)
|
||||
self.assertNotEqual(converted, source)
|
||||
if converted != expected:
|
||||
print(self.diff(expected, converted))
|
||||
self.fail("converted glyph is different from expected")
|
||||
|
||||
def test_convert_simple_glyph(self):
|
||||
self.expect_glyph(CUBIC_GLYPHS['a'], QUAD_GLYPHS['a'])
|
||||
self.expect_glyph(CUBIC_GLYPHS['A'], QUAD_GLYPHS['A'])
|
||||
|
||||
def test_convert_composite_glyph(self):
|
||||
source = CUBIC_GLYPHS['Aacute']
|
||||
converted = self.convert_glyph(source)
|
||||
# components don't change after quadratic conversion
|
||||
self.assertEqual(converted, source)
|
||||
|
||||
def test_convert_mixed_glyph(self):
|
||||
# this contains a mix of contours and components
|
||||
self.expect_glyph(CUBIC_GLYPHS['Eacute'], QUAD_GLYPHS['Eacute'])
|
||||
|
||||
def test_reverse_direction(self):
|
||||
for name in ('a', 'A', 'Eacute'):
|
||||
source = CUBIC_GLYPHS[name]
|
||||
normal_glyph = self.convert_glyph(source)
|
||||
reversed_glyph = self.convert_glyph(source, reverse_direction=True)
|
||||
|
||||
# the number of commands is the same, just their order is iverted
|
||||
self.assertTrue(
|
||||
len(normal_glyph.outline), len(reversed_glyph.outline))
|
||||
self.assertNotEqual(normal_glyph, reversed_glyph)
|
||||
|
||||
def test_stats(self):
|
||||
stats = {}
|
||||
for name in CUBIC_GLYPHS.keys():
|
||||
source = CUBIC_GLYPHS[name]
|
||||
self.convert_glyph(source, stats=stats)
|
||||
|
||||
self.assertTrue(stats)
|
||||
self.assertTrue('4' in stats)
|
||||
self.assertEqual(type(stats['4']), int)
|
||||
|
||||
def test_addComponent(self):
|
||||
pen = self.Pen()
|
||||
quadpen = self.Cu2QuPen(pen, MAX_ERR)
|
||||
quadpen.addComponent("a", (1, 2, 3, 4, 5.0, 6.0))
|
||||
|
||||
# components are passed through without changes
|
||||
self.assertEqual(str(pen).splitlines(), [
|
||||
"pen.addComponent('a', (1, 2, 3, 4, 5.0, 6.0))",
|
||||
])
|
||||
|
||||
|
||||
class TestCu2QuPen(unittest.TestCase, _TestPenMixin):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TestCu2QuPen, self).__init__(*args, **kwargs)
|
||||
self.Glyph = DummyGlyph
|
||||
self.Pen = DummyPen
|
||||
self.Cu2QuPen = Cu2QuPen
|
||||
self.pen_getter_name = 'getPen'
|
||||
self.draw_method_name = 'draw'
|
||||
|
||||
def test__check_contour_is_open(self):
|
||||
msg = "moveTo is required"
|
||||
quadpen = Cu2QuPen(DummyPen(), MAX_ERR)
|
||||
|
||||
with self.assertRaisesRegex(AssertionError, msg):
|
||||
quadpen.lineTo((0, 0))
|
||||
with self.assertRaisesRegex(AssertionError, msg):
|
||||
quadpen.qCurveTo((0, 0), (1, 1))
|
||||
with self.assertRaisesRegex(AssertionError, msg):
|
||||
quadpen.curveTo((0, 0), (1, 1), (2, 2))
|
||||
with self.assertRaisesRegex(AssertionError, msg):
|
||||
quadpen.closePath()
|
||||
with self.assertRaisesRegex(AssertionError, msg):
|
||||
quadpen.endPath()
|
||||
|
||||
quadpen.moveTo((0, 0)) # now it works
|
||||
quadpen.lineTo((1, 1))
|
||||
quadpen.qCurveTo((2, 2), (3, 3))
|
||||
quadpen.curveTo((4, 4), (5, 5), (6, 6))
|
||||
quadpen.closePath()
|
||||
|
||||
def test__check_contour_closed(self):
|
||||
msg = "closePath or endPath is required"
|
||||
quadpen = Cu2QuPen(DummyPen(), MAX_ERR)
|
||||
quadpen.moveTo((0, 0))
|
||||
|
||||
with self.assertRaisesRegex(AssertionError, msg):
|
||||
quadpen.moveTo((1, 1))
|
||||
with self.assertRaisesRegex(AssertionError, msg):
|
||||
quadpen.addComponent("a", (1, 0, 0, 1, 0, 0))
|
||||
|
||||
# it works if contour is closed
|
||||
quadpen.closePath()
|
||||
quadpen.moveTo((1, 1))
|
||||
quadpen.endPath()
|
||||
quadpen.addComponent("a", (1, 0, 0, 1, 0, 0))
|
||||
|
||||
def test_qCurveTo_no_points(self):
|
||||
quadpen = Cu2QuPen(DummyPen(), MAX_ERR)
|
||||
quadpen.moveTo((0, 0))
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
AssertionError, "illegal qcurve segment point count: 0"):
|
||||
quadpen.qCurveTo()
|
||||
|
||||
def test_qCurveTo_1_point(self):
|
||||
pen = DummyPen()
|
||||
quadpen = Cu2QuPen(pen, MAX_ERR)
|
||||
quadpen.moveTo((0, 0))
|
||||
quadpen.qCurveTo((1, 1))
|
||||
|
||||
self.assertEqual(str(pen).splitlines(), [
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.lineTo((1, 1))",
|
||||
])
|
||||
|
||||
def test_qCurveTo_more_than_1_point(self):
|
||||
pen = DummyPen()
|
||||
quadpen = Cu2QuPen(pen, MAX_ERR)
|
||||
quadpen.moveTo((0, 0))
|
||||
quadpen.qCurveTo((1, 1), (2, 2))
|
||||
|
||||
self.assertEqual(str(pen).splitlines(), [
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.qCurveTo((1, 1), (2, 2))",
|
||||
])
|
||||
|
||||
def test_curveTo_no_points(self):
|
||||
quadpen = Cu2QuPen(DummyPen(), MAX_ERR)
|
||||
quadpen.moveTo((0, 0))
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
AssertionError, "illegal curve segment point count: 0"):
|
||||
quadpen.curveTo()
|
||||
|
||||
def test_curveTo_1_point(self):
|
||||
pen = DummyPen()
|
||||
quadpen = Cu2QuPen(pen, MAX_ERR)
|
||||
quadpen.moveTo((0, 0))
|
||||
quadpen.curveTo((1, 1))
|
||||
|
||||
self.assertEqual(str(pen).splitlines(), [
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.lineTo((1, 1))",
|
||||
])
|
||||
|
||||
def test_curveTo_2_points(self):
|
||||
pen = DummyPen()
|
||||
quadpen = Cu2QuPen(pen, MAX_ERR)
|
||||
quadpen.moveTo((0, 0))
|
||||
quadpen.curveTo((1, 1), (2, 2))
|
||||
|
||||
self.assertEqual(str(pen).splitlines(), [
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.qCurveTo((1, 1), (2, 2))",
|
||||
])
|
||||
|
||||
def test_curveTo_3_points(self):
|
||||
pen = DummyPen()
|
||||
quadpen = Cu2QuPen(pen, MAX_ERR)
|
||||
quadpen.moveTo((0, 0))
|
||||
quadpen.curveTo((1, 1), (2, 2), (3, 3))
|
||||
|
||||
self.assertEqual(str(pen).splitlines(), [
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.qCurveTo((0.75, 0.75), (2.25, 2.25), (3, 3))",
|
||||
])
|
||||
|
||||
def test_curveTo_more_than_3_points(self):
|
||||
# a 'SuperBezier' as described in fontTools.basePen.AbstractPen
|
||||
pen = DummyPen()
|
||||
quadpen = Cu2QuPen(pen, MAX_ERR)
|
||||
quadpen.moveTo((0, 0))
|
||||
quadpen.curveTo((1, 1), (2, 2), (3, 3), (4, 4))
|
||||
|
||||
self.assertEqual(str(pen).splitlines(), [
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.qCurveTo((0.75, 0.75), (1.625, 1.625), (2, 2))",
|
||||
"pen.qCurveTo((2.375, 2.375), (3.25, 3.25), (4, 4))",
|
||||
])
|
||||
|
||||
def test_addComponent(self):
|
||||
pen = DummyPen()
|
||||
quadpen = Cu2QuPen(pen, MAX_ERR)
|
||||
quadpen.addComponent("a", (1, 2, 3, 4, 5.0, 6.0))
|
||||
|
||||
# components are passed through without changes
|
||||
self.assertEqual(str(pen).splitlines(), [
|
||||
"pen.addComponent('a', (1, 2, 3, 4, 5.0, 6.0))",
|
||||
])
|
||||
|
||||
def test_ignore_single_points(self):
|
||||
pen = DummyPen()
|
||||
quadpen = Cu2QuPen(pen, MAX_ERR, ignore_single_points=True)
|
||||
quadpen.moveTo((0, 0))
|
||||
quadpen.endPath()
|
||||
quadpen.moveTo((1, 1))
|
||||
quadpen.closePath()
|
||||
|
||||
# single-point contours were ignored, so the pen commands are empty
|
||||
self.assertFalse(pen.commands)
|
||||
|
||||
# redraw without ignoring single points
|
||||
quadpen.ignore_single_points = False
|
||||
quadpen.moveTo((0, 0))
|
||||
quadpen.endPath()
|
||||
quadpen.moveTo((1, 1))
|
||||
quadpen.closePath()
|
||||
|
||||
self.assertTrue(pen.commands)
|
||||
self.assertEqual(str(pen).splitlines(), [
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.endPath()",
|
||||
"pen.moveTo((1, 1))",
|
||||
"pen.closePath()"
|
||||
])
|
||||
|
||||
|
||||
class TestCu2QuPointPen(unittest.TestCase, _TestPenMixin):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TestCu2QuPointPen, self).__init__(*args, **kwargs)
|
||||
self.Glyph = DummyPointGlyph
|
||||
self.Pen = DummyPointPen
|
||||
self.Cu2QuPen = Cu2QuPointPen
|
||||
self.pen_getter_name = 'getPointPen'
|
||||
self.draw_method_name = 'drawPoints'
|
||||
|
||||
def test_super_bezier_curve(self):
|
||||
pen = DummyPointPen()
|
||||
quadpen = Cu2QuPointPen(pen, MAX_ERR)
|
||||
quadpen.beginPath()
|
||||
quadpen.addPoint((0, 0), segmentType="move")
|
||||
quadpen.addPoint((1, 1))
|
||||
quadpen.addPoint((2, 2))
|
||||
quadpen.addPoint((3, 3))
|
||||
quadpen.addPoint(
|
||||
(4, 4), segmentType="curve", smooth=False, name="up", selected=1)
|
||||
quadpen.endPath()
|
||||
|
||||
self.assertEqual(str(pen).splitlines(), """\
|
||||
pen.beginPath()
|
||||
pen.addPoint((0, 0), name=None, segmentType='move', smooth=False)
|
||||
pen.addPoint((0.75, 0.75), name=None, segmentType=None, smooth=False)
|
||||
pen.addPoint((1.625, 1.625), name=None, segmentType=None, smooth=False)
|
||||
pen.addPoint((2, 2), name=None, segmentType='qcurve', smooth=True)
|
||||
pen.addPoint((2.375, 2.375), name=None, segmentType=None, smooth=False)
|
||||
pen.addPoint((3.25, 3.25), name=None, segmentType=None, smooth=False)
|
||||
pen.addPoint((4, 4), name='up', segmentType='qcurve', selected=1, smooth=False)
|
||||
pen.endPath()""".splitlines())
|
||||
|
||||
def test__flushContour_restore_starting_point(self):
|
||||
pen = DummyPointPen()
|
||||
quadpen = Cu2QuPointPen(pen, MAX_ERR)
|
||||
|
||||
# collect the output of _flushContour before it's sent to _drawPoints
|
||||
new_segments = []
|
||||
def _drawPoints(segments):
|
||||
new_segments.extend(segments)
|
||||
Cu2QuPointPen._drawPoints(quadpen, segments)
|
||||
quadpen._drawPoints = _drawPoints
|
||||
|
||||
# a closed path (ie. no "move" segmentType)
|
||||
quadpen._flushContour([
|
||||
("curve", [
|
||||
((2, 2), False, None, {}),
|
||||
((1, 1), False, None, {}),
|
||||
((0, 0), False, None, {}),
|
||||
]),
|
||||
("curve", [
|
||||
((1, 1), False, None, {}),
|
||||
((2, 2), False, None, {}),
|
||||
((3, 3), False, None, {}),
|
||||
]),
|
||||
])
|
||||
|
||||
# the original starting point is restored: the last segment has become
|
||||
# the first
|
||||
self.assertEqual(new_segments[0][1][-1][0], (3, 3))
|
||||
self.assertEqual(new_segments[-1][1][-1][0], (0, 0))
|
||||
|
||||
new_segments = []
|
||||
# an open path (ie. starting with "move")
|
||||
quadpen._flushContour([
|
||||
("move", [
|
||||
((0, 0), False, None, {}),
|
||||
]),
|
||||
("curve", [
|
||||
((1, 1), False, None, {}),
|
||||
((2, 2), False, None, {}),
|
||||
((3, 3), False, None, {}),
|
||||
]),
|
||||
])
|
||||
|
||||
# the segment order stays the same before and after _flushContour
|
||||
self.assertEqual(new_segments[0][1][-1][0], (0, 0))
|
||||
self.assertEqual(new_segments[-1][1][-1][0], (3, 3))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
236
Lib/cu2qu/test/utils.py
Normal file
236
Lib/cu2qu/test/utils.py
Normal file
@ -0,0 +1,236 @@
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from cu2qu.test import CUBIC_GLYPHS
|
||||
try:
|
||||
from ufoLib.pointPen import PointToSegmentPen, SegmentToPointPen
|
||||
except ImportError:
|
||||
from robofab.pens.adapterPens import PointToSegmentPen, SegmentToPointPen
|
||||
import unittest
|
||||
|
||||
|
||||
class BaseDummyPen(object):
|
||||
"""Base class for pens that record the commands they are called with."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.commands = []
|
||||
|
||||
def __str__(self):
|
||||
"""Return the pen commands as a string of python code."""
|
||||
return _repr_pen_commands(self.commands)
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
self.commands.append(('addComponent', (glyphName, transformation), {}))
|
||||
|
||||
|
||||
class DummyPen(BaseDummyPen):
|
||||
"""A SegmentPen that records the commands it's called with."""
|
||||
|
||||
def moveTo(self, pt):
|
||||
self.commands.append(('moveTo', (pt,), {}))
|
||||
|
||||
def lineTo(self, pt):
|
||||
self.commands.append(('lineTo', (pt,), {}))
|
||||
|
||||
def curveTo(self, *points):
|
||||
self.commands.append(('curveTo', points, {}))
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
self.commands.append(('qCurveTo', points, {}))
|
||||
|
||||
def closePath(self):
|
||||
self.commands.append(('closePath', tuple(), {}))
|
||||
|
||||
def endPath(self):
|
||||
self.commands.append(('endPath', tuple(), {}))
|
||||
|
||||
|
||||
class DummyPointPen(BaseDummyPen):
|
||||
"""A PointPen that records the commands it's called with."""
|
||||
|
||||
def beginPath(self):
|
||||
self.commands.append(('beginPath', tuple(), {}))
|
||||
|
||||
def endPath(self):
|
||||
self.commands.append(('endPath', tuple(), {}))
|
||||
|
||||
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
|
||||
kwargs['segmentType'] = segmentType
|
||||
kwargs['smooth'] = smooth
|
||||
kwargs['name'] = name
|
||||
self.commands.append(('addPoint', (pt,), kwargs))
|
||||
|
||||
|
||||
class DummyGlyph(object):
|
||||
"""Provides a minimal interface for storing a glyph's outline data in a
|
||||
SegmentPen-oriented way. The glyph's outline consists in the list of
|
||||
SegmentPen commands required to draw it.
|
||||
"""
|
||||
|
||||
# the SegmentPen class used to draw on this glyph type
|
||||
DrawingPen = DummyPen
|
||||
|
||||
def __init__(self, glyph=None):
|
||||
"""If another glyph (i.e. any object having a 'draw' method) is given,
|
||||
its outline data is copied to self.
|
||||
"""
|
||||
self._pen = self.DrawingPen()
|
||||
self.outline = self._pen.commands
|
||||
if glyph:
|
||||
self.appendGlyph(glyph)
|
||||
|
||||
def appendGlyph(self, glyph):
|
||||
"""Copy another glyph's outline onto self."""
|
||||
glyph.draw(self._pen)
|
||||
|
||||
def getPen(self):
|
||||
"""Return the SegmentPen that can 'draw' on this glyph."""
|
||||
return self._pen
|
||||
|
||||
def getPointPen(self):
|
||||
"""Return a PointPen adapter that can 'draw' on this glyph."""
|
||||
return PointToSegmentPen(self._pen)
|
||||
|
||||
def draw(self, pen):
|
||||
"""Use another SegmentPen to replay the glyph's outline commands."""
|
||||
if self.outline:
|
||||
for cmd, args, kwargs in self.outline:
|
||||
getattr(pen, cmd)(*args, **kwargs)
|
||||
|
||||
def drawPoints(self, pointPen):
|
||||
"""Use another PointPen to replay the glyph's outline commands,
|
||||
indirectly through an adapter.
|
||||
"""
|
||||
pen = SegmentToPointPen(pointPen)
|
||||
self.draw(pen)
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Return True if 'other' glyph's outline is the same as self."""
|
||||
if hasattr(other, 'outline'):
|
||||
return self.outline == other.outline
|
||||
elif hasattr(other, 'draw'):
|
||||
return self.outline == self.__class__(other).outline
|
||||
return NotImplemented
|
||||
|
||||
def __ne__(self, other):
|
||||
"""Return True if 'other' glyph's outline is different from self."""
|
||||
return not (self == other)
|
||||
|
||||
def __str__(self):
|
||||
"""Return commands making up the glyph's outline as a string."""
|
||||
return str(self._pen)
|
||||
|
||||
|
||||
class DummyPointGlyph(DummyGlyph):
|
||||
"""Provides a minimal interface for storing a glyph's outline data in a
|
||||
PointPen-oriented way. The glyph's outline consists in the list of
|
||||
PointPen commands required to draw it.
|
||||
"""
|
||||
|
||||
# the PointPen class used to draw on this glyph type
|
||||
DrawingPen = DummyPointPen
|
||||
|
||||
def appendGlyph(self, glyph):
|
||||
"""Copy another glyph's outline onto self."""
|
||||
glyph.drawPoints(self._pen)
|
||||
|
||||
def getPen(self):
|
||||
"""Return a SegmentPen adapter that can 'draw' on this glyph."""
|
||||
return SegmentToPointPen(self._pen)
|
||||
|
||||
def getPointPen(self):
|
||||
"""Return the PointPen that can 'draw' on this glyph."""
|
||||
return self._pen
|
||||
|
||||
def draw(self, pen):
|
||||
"""Use another SegmentPen to replay the glyph's outline commands,
|
||||
indirectly through an adapter.
|
||||
"""
|
||||
pointPen = PointToSegmentPen(pen)
|
||||
self.drawPoints(pointPen)
|
||||
|
||||
def drawPoints(self, pointPen):
|
||||
"""Use another PointPen to replay the glyph's outline commands."""
|
||||
if self.outline:
|
||||
for cmd, args, kwargs in self.outline:
|
||||
getattr(pointPen, cmd)(*args, **kwargs)
|
||||
|
||||
|
||||
def _repr_pen_commands(commands):
|
||||
"""
|
||||
>>> print(_repr_pen_commands([
|
||||
... ('moveTo', tuple(), {}),
|
||||
... ('lineTo', ((1.0, 0.1),), {}),
|
||||
... ('curveTo', ((1.0, 0.1), (2.0, 0.2), (3.0, 0.3)), {})
|
||||
... ]))
|
||||
pen.moveTo()
|
||||
pen.lineTo((1, 0.1))
|
||||
pen.curveTo((1, 0.1), (2, 0.2), (3, 0.3))
|
||||
|
||||
>>> print(_repr_pen_commands([
|
||||
... ('beginPath', tuple(), {}),
|
||||
... ('addPoint', ((1.0, 0.1),),
|
||||
... {"segmentType":"line", "smooth":True, "name":"test", "z":1}),
|
||||
... ]))
|
||||
pen.beginPath()
|
||||
pen.addPoint((1, 0.1), name='test', segmentType='line', smooth=True, z=1)
|
||||
|
||||
>>> print(_repr_pen_commands([
|
||||
... ('addComponent', ('A', (1, 0, 0, 1, 0, 0)), {})
|
||||
... ]))
|
||||
pen.addComponent('A', (1, 0, 0, 1, 0, 0))
|
||||
"""
|
||||
s = []
|
||||
for cmd, args, kwargs in commands:
|
||||
if args:
|
||||
if isinstance(args[0], tuple):
|
||||
# cast float to int if there're no digits after decimal point
|
||||
args = [tuple((int(v) if int(v) == v else v) for v in pt)
|
||||
for pt in args]
|
||||
args = ", ".join(repr(a) for a in args)
|
||||
if kwargs:
|
||||
kwargs = ", ".join("%s=%r" % (k, v)
|
||||
for k, v in sorted(kwargs.items()))
|
||||
if args and kwargs:
|
||||
s.append("pen.%s(%s, %s)" % (cmd, args, kwargs))
|
||||
elif args:
|
||||
s.append("pen.%s(%s)" % (cmd, args))
|
||||
elif kwargs:
|
||||
s.append("pen.%s(%s)" % (cmd, kwargs))
|
||||
else:
|
||||
s.append("pen.%s()" % cmd)
|
||||
return "\n".join(s)
|
||||
|
||||
|
||||
class TestDummyGlyph(unittest.TestCase):
|
||||
|
||||
def test_equal(self):
|
||||
# verify that the copy and the copy of the copy are equal to
|
||||
# the source glyph's outline, as well as to each other
|
||||
source = CUBIC_GLYPHS['a']
|
||||
copy = DummyGlyph(source)
|
||||
copy2 = DummyGlyph(copy)
|
||||
self.assertEqual(source, copy)
|
||||
self.assertEqual(source, copy2)
|
||||
self.assertEqual(copy, copy2)
|
||||
# assert equality doesn't hold any more after modification
|
||||
copy.outline.pop()
|
||||
self.assertNotEqual(source, copy)
|
||||
self.assertNotEqual(copy, copy2)
|
||||
|
||||
|
||||
class TestDummyPointGlyph(unittest.TestCase):
|
||||
|
||||
def test_equal(self):
|
||||
# same as above but using the PointPen protocol
|
||||
source = CUBIC_GLYPHS['a']
|
||||
copy = DummyPointGlyph(source)
|
||||
copy2 = DummyPointGlyph(copy)
|
||||
self.assertEqual(source, copy)
|
||||
self.assertEqual(source, copy2)
|
||||
self.assertEqual(copy, copy2)
|
||||
copy.outline.pop()
|
||||
self.assertNotEqual(source, copy)
|
||||
self.assertNotEqual(copy, copy2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
8
setup.py
8
setup.py
@ -19,6 +19,10 @@ from setuptools import setup
|
||||
setup(
|
||||
name='cu2qu',
|
||||
version='1.0',
|
||||
packages=['cu2qu'],
|
||||
package_dir={'': 'Lib'}
|
||||
packages=['cu2qu', 'cu2qu.test'],
|
||||
package_dir={'': 'Lib'},
|
||||
test_suite="cu2qu.test",
|
||||
package_data={
|
||||
'cu2qu.test': ['data/*/*.glif', 'data/*/*.plist'],
|
||||
},
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user