Merge branch 'master' into tests

This commit is contained in:
James Godfrey-Kittle 2016-04-21 11:30:13 -07:00
commit 57fa1f46f4
16 changed files with 1411 additions and 2 deletions

310
Lib/cu2qu/pens.py Normal file
View 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)

View 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

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

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

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

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

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

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

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

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

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

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

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

View File

@ -19,6 +19,10 @@ from setuptools import setup
setup( setup(
name='cu2qu', name='cu2qu',
version='1.0', version='1.0',
packages=['cu2qu'], packages=['cu2qu', 'cu2qu.test'],
package_dir={'': 'Lib'} package_dir={'': 'Lib'},
test_suite="cu2qu.test",
package_data={
'cu2qu.test': ['data/*/*.glif', 'data/*/*.plist'],
},
) )