1*e1fe3e4aSElliott Hughes# Copyright 2016 Google Inc. All Rights Reserved. 2*e1fe3e4aSElliott Hughes# 3*e1fe3e4aSElliott Hughes# Licensed under the Apache License, Version 2.0 (the "License"); 4*e1fe3e4aSElliott Hughes# you may not use this file except in compliance with the License. 5*e1fe3e4aSElliott Hughes# You may obtain a copy of the License at 6*e1fe3e4aSElliott Hughes# 7*e1fe3e4aSElliott Hughes# http://www.apache.org/licenses/LICENSE-2.0 8*e1fe3e4aSElliott Hughes# 9*e1fe3e4aSElliott Hughes# Unless required by applicable law or agreed to in writing, software 10*e1fe3e4aSElliott Hughes# distributed under the License is distributed on an "AS IS" BASIS, 11*e1fe3e4aSElliott Hughes# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*e1fe3e4aSElliott Hughes# See the License for the specific language governing permissions and 13*e1fe3e4aSElliott Hughes# limitations under the License. 14*e1fe3e4aSElliott Hughes 15*e1fe3e4aSElliott Hughesfrom fontTools.pens.pointPen import PointToSegmentPen, SegmentToPointPen 16*e1fe3e4aSElliott Hughesfrom fontTools.ufoLib.glifLib import GlyphSet 17*e1fe3e4aSElliott Hughesfrom math import isclose 18*e1fe3e4aSElliott Hughesimport os 19*e1fe3e4aSElliott Hughesimport unittest 20*e1fe3e4aSElliott Hughes 21*e1fe3e4aSElliott Hughes 22*e1fe3e4aSElliott HughesDATADIR = os.path.join(os.path.dirname(__file__), "data") 23*e1fe3e4aSElliott HughesCUBIC_GLYPHS = GlyphSet(os.path.join(DATADIR, "cubic")) 24*e1fe3e4aSElliott HughesQUAD_GLYPHS = GlyphSet(os.path.join(DATADIR, "quadratic")) 25*e1fe3e4aSElliott Hughes 26*e1fe3e4aSElliott Hughes 27*e1fe3e4aSElliott Hughesclass BaseDummyPen(object): 28*e1fe3e4aSElliott Hughes """Base class for pens that record the commands they are called with.""" 29*e1fe3e4aSElliott Hughes 30*e1fe3e4aSElliott Hughes def __init__(self, *args, **kwargs): 31*e1fe3e4aSElliott Hughes self.commands = [] 32*e1fe3e4aSElliott Hughes 33*e1fe3e4aSElliott Hughes def __str__(self): 34*e1fe3e4aSElliott Hughes """Return the pen commands as a string of python code.""" 35*e1fe3e4aSElliott Hughes return _repr_pen_commands(self.commands) 36*e1fe3e4aSElliott Hughes 37*e1fe3e4aSElliott Hughes def addComponent(self, glyphName, transformation, **kwargs): 38*e1fe3e4aSElliott Hughes self.commands.append(("addComponent", (glyphName, transformation), kwargs)) 39*e1fe3e4aSElliott Hughes 40*e1fe3e4aSElliott Hughes 41*e1fe3e4aSElliott Hughesclass DummyPen(BaseDummyPen): 42*e1fe3e4aSElliott Hughes """A SegmentPen that records the commands it's called with.""" 43*e1fe3e4aSElliott Hughes 44*e1fe3e4aSElliott Hughes def moveTo(self, pt): 45*e1fe3e4aSElliott Hughes self.commands.append(("moveTo", (pt,), {})) 46*e1fe3e4aSElliott Hughes 47*e1fe3e4aSElliott Hughes def lineTo(self, pt): 48*e1fe3e4aSElliott Hughes self.commands.append(("lineTo", (pt,), {})) 49*e1fe3e4aSElliott Hughes 50*e1fe3e4aSElliott Hughes def curveTo(self, *points): 51*e1fe3e4aSElliott Hughes self.commands.append(("curveTo", points, {})) 52*e1fe3e4aSElliott Hughes 53*e1fe3e4aSElliott Hughes def qCurveTo(self, *points): 54*e1fe3e4aSElliott Hughes self.commands.append(("qCurveTo", points, {})) 55*e1fe3e4aSElliott Hughes 56*e1fe3e4aSElliott Hughes def closePath(self): 57*e1fe3e4aSElliott Hughes self.commands.append(("closePath", tuple(), {})) 58*e1fe3e4aSElliott Hughes 59*e1fe3e4aSElliott Hughes def endPath(self): 60*e1fe3e4aSElliott Hughes self.commands.append(("endPath", tuple(), {})) 61*e1fe3e4aSElliott Hughes 62*e1fe3e4aSElliott Hughes 63*e1fe3e4aSElliott Hughesclass DummyPointPen(BaseDummyPen): 64*e1fe3e4aSElliott Hughes """A PointPen that records the commands it's called with.""" 65*e1fe3e4aSElliott Hughes 66*e1fe3e4aSElliott Hughes def beginPath(self, **kwargs): 67*e1fe3e4aSElliott Hughes self.commands.append(("beginPath", tuple(), kwargs)) 68*e1fe3e4aSElliott Hughes 69*e1fe3e4aSElliott Hughes def endPath(self): 70*e1fe3e4aSElliott Hughes self.commands.append(("endPath", tuple(), {})) 71*e1fe3e4aSElliott Hughes 72*e1fe3e4aSElliott Hughes def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): 73*e1fe3e4aSElliott Hughes kwargs["segmentType"] = str(segmentType) if segmentType else None 74*e1fe3e4aSElliott Hughes kwargs["smooth"] = smooth 75*e1fe3e4aSElliott Hughes kwargs["name"] = name 76*e1fe3e4aSElliott Hughes self.commands.append(("addPoint", (pt,), kwargs)) 77*e1fe3e4aSElliott Hughes 78*e1fe3e4aSElliott Hughes 79*e1fe3e4aSElliott Hughesclass DummyGlyph(object): 80*e1fe3e4aSElliott Hughes """Provides a minimal interface for storing a glyph's outline data in a 81*e1fe3e4aSElliott Hughes SegmentPen-oriented way. The glyph's outline consists in the list of 82*e1fe3e4aSElliott Hughes SegmentPen commands required to draw it. 83*e1fe3e4aSElliott Hughes """ 84*e1fe3e4aSElliott Hughes 85*e1fe3e4aSElliott Hughes # the SegmentPen class used to draw on this glyph type 86*e1fe3e4aSElliott Hughes DrawingPen = DummyPen 87*e1fe3e4aSElliott Hughes 88*e1fe3e4aSElliott Hughes def __init__(self, glyph=None): 89*e1fe3e4aSElliott Hughes """If another glyph (i.e. any object having a 'draw' method) is given, 90*e1fe3e4aSElliott Hughes its outline data is copied to self. 91*e1fe3e4aSElliott Hughes """ 92*e1fe3e4aSElliott Hughes self._pen = self.DrawingPen() 93*e1fe3e4aSElliott Hughes self.outline = self._pen.commands 94*e1fe3e4aSElliott Hughes if glyph: 95*e1fe3e4aSElliott Hughes self.appendGlyph(glyph) 96*e1fe3e4aSElliott Hughes 97*e1fe3e4aSElliott Hughes def appendGlyph(self, glyph): 98*e1fe3e4aSElliott Hughes """Copy another glyph's outline onto self.""" 99*e1fe3e4aSElliott Hughes glyph.draw(self._pen) 100*e1fe3e4aSElliott Hughes 101*e1fe3e4aSElliott Hughes def getPen(self): 102*e1fe3e4aSElliott Hughes """Return the SegmentPen that can 'draw' on this glyph.""" 103*e1fe3e4aSElliott Hughes return self._pen 104*e1fe3e4aSElliott Hughes 105*e1fe3e4aSElliott Hughes def getPointPen(self): 106*e1fe3e4aSElliott Hughes """Return a PointPen adapter that can 'draw' on this glyph.""" 107*e1fe3e4aSElliott Hughes return PointToSegmentPen(self._pen) 108*e1fe3e4aSElliott Hughes 109*e1fe3e4aSElliott Hughes def draw(self, pen): 110*e1fe3e4aSElliott Hughes """Use another SegmentPen to replay the glyph's outline commands.""" 111*e1fe3e4aSElliott Hughes if self.outline: 112*e1fe3e4aSElliott Hughes for cmd, args, kwargs in self.outline: 113*e1fe3e4aSElliott Hughes getattr(pen, cmd)(*args, **kwargs) 114*e1fe3e4aSElliott Hughes 115*e1fe3e4aSElliott Hughes def drawPoints(self, pointPen): 116*e1fe3e4aSElliott Hughes """Use another PointPen to replay the glyph's outline commands, 117*e1fe3e4aSElliott Hughes indirectly through an adapter. 118*e1fe3e4aSElliott Hughes """ 119*e1fe3e4aSElliott Hughes pen = SegmentToPointPen(pointPen) 120*e1fe3e4aSElliott Hughes self.draw(pen) 121*e1fe3e4aSElliott Hughes 122*e1fe3e4aSElliott Hughes def __eq__(self, other): 123*e1fe3e4aSElliott Hughes """Return True if 'other' glyph's outline is the same as self.""" 124*e1fe3e4aSElliott Hughes if hasattr(other, "outline"): 125*e1fe3e4aSElliott Hughes return self.outline == other.outline 126*e1fe3e4aSElliott Hughes elif hasattr(other, "draw"): 127*e1fe3e4aSElliott Hughes return self.outline == self.__class__(other).outline 128*e1fe3e4aSElliott Hughes return NotImplemented 129*e1fe3e4aSElliott Hughes 130*e1fe3e4aSElliott Hughes def __ne__(self, other): 131*e1fe3e4aSElliott Hughes """Return True if 'other' glyph's outline is different from self.""" 132*e1fe3e4aSElliott Hughes return not (self == other) 133*e1fe3e4aSElliott Hughes 134*e1fe3e4aSElliott Hughes def approx(self, other, rel_tol=1e-12): 135*e1fe3e4aSElliott Hughes if hasattr(other, "outline"): 136*e1fe3e4aSElliott Hughes outline2 == other.outline 137*e1fe3e4aSElliott Hughes elif hasattr(other, "draw"): 138*e1fe3e4aSElliott Hughes outline2 = self.__class__(other).outline 139*e1fe3e4aSElliott Hughes else: 140*e1fe3e4aSElliott Hughes raise TypeError(type(other).__name__) 141*e1fe3e4aSElliott Hughes outline1 = self.outline 142*e1fe3e4aSElliott Hughes if len(outline1) != len(outline2): 143*e1fe3e4aSElliott Hughes return False 144*e1fe3e4aSElliott Hughes for (cmd1, arg1, kwd1), (cmd2, arg2, kwd2) in zip(outline1, outline2): 145*e1fe3e4aSElliott Hughes if cmd1 != cmd2: 146*e1fe3e4aSElliott Hughes return False 147*e1fe3e4aSElliott Hughes if kwd1 != kwd2: 148*e1fe3e4aSElliott Hughes return False 149*e1fe3e4aSElliott Hughes if arg1: 150*e1fe3e4aSElliott Hughes if isinstance(arg1[0], tuple): 151*e1fe3e4aSElliott Hughes if not arg2 or not isinstance(arg2[0], tuple): 152*e1fe3e4aSElliott Hughes return False 153*e1fe3e4aSElliott Hughes for (x1, y1), (x2, y2) in zip(arg1, arg2): 154*e1fe3e4aSElliott Hughes if not isclose(x1, x2, rel_tol=rel_tol) or not isclose( 155*e1fe3e4aSElliott Hughes y1, y2, rel_tol=rel_tol 156*e1fe3e4aSElliott Hughes ): 157*e1fe3e4aSElliott Hughes return False 158*e1fe3e4aSElliott Hughes elif arg1 != arg2: 159*e1fe3e4aSElliott Hughes return False 160*e1fe3e4aSElliott Hughes elif arg2: 161*e1fe3e4aSElliott Hughes return False 162*e1fe3e4aSElliott Hughes return True 163*e1fe3e4aSElliott Hughes 164*e1fe3e4aSElliott Hughes def __str__(self): 165*e1fe3e4aSElliott Hughes """Return commands making up the glyph's outline as a string.""" 166*e1fe3e4aSElliott Hughes return str(self._pen) 167*e1fe3e4aSElliott Hughes 168*e1fe3e4aSElliott Hughes 169*e1fe3e4aSElliott Hughesclass DummyPointGlyph(DummyGlyph): 170*e1fe3e4aSElliott Hughes """Provides a minimal interface for storing a glyph's outline data in a 171*e1fe3e4aSElliott Hughes PointPen-oriented way. The glyph's outline consists in the list of 172*e1fe3e4aSElliott Hughes PointPen commands required to draw it. 173*e1fe3e4aSElliott Hughes """ 174*e1fe3e4aSElliott Hughes 175*e1fe3e4aSElliott Hughes # the PointPen class used to draw on this glyph type 176*e1fe3e4aSElliott Hughes DrawingPen = DummyPointPen 177*e1fe3e4aSElliott Hughes 178*e1fe3e4aSElliott Hughes def appendGlyph(self, glyph): 179*e1fe3e4aSElliott Hughes """Copy another glyph's outline onto self.""" 180*e1fe3e4aSElliott Hughes glyph.drawPoints(self._pen) 181*e1fe3e4aSElliott Hughes 182*e1fe3e4aSElliott Hughes def getPen(self): 183*e1fe3e4aSElliott Hughes """Return a SegmentPen adapter that can 'draw' on this glyph.""" 184*e1fe3e4aSElliott Hughes return SegmentToPointPen(self._pen) 185*e1fe3e4aSElliott Hughes 186*e1fe3e4aSElliott Hughes def getPointPen(self): 187*e1fe3e4aSElliott Hughes """Return the PointPen that can 'draw' on this glyph.""" 188*e1fe3e4aSElliott Hughes return self._pen 189*e1fe3e4aSElliott Hughes 190*e1fe3e4aSElliott Hughes def draw(self, pen): 191*e1fe3e4aSElliott Hughes """Use another SegmentPen to replay the glyph's outline commands, 192*e1fe3e4aSElliott Hughes indirectly through an adapter. 193*e1fe3e4aSElliott Hughes """ 194*e1fe3e4aSElliott Hughes pointPen = PointToSegmentPen(pen) 195*e1fe3e4aSElliott Hughes self.drawPoints(pointPen) 196*e1fe3e4aSElliott Hughes 197*e1fe3e4aSElliott Hughes def drawPoints(self, pointPen): 198*e1fe3e4aSElliott Hughes """Use another PointPen to replay the glyph's outline commands.""" 199*e1fe3e4aSElliott Hughes if self.outline: 200*e1fe3e4aSElliott Hughes for cmd, args, kwargs in self.outline: 201*e1fe3e4aSElliott Hughes getattr(pointPen, cmd)(*args, **kwargs) 202*e1fe3e4aSElliott Hughes 203*e1fe3e4aSElliott Hughes 204*e1fe3e4aSElliott Hughesdef _repr_pen_commands(commands): 205*e1fe3e4aSElliott Hughes """ 206*e1fe3e4aSElliott Hughes >>> print(_repr_pen_commands([ 207*e1fe3e4aSElliott Hughes ... ('moveTo', tuple(), {}), 208*e1fe3e4aSElliott Hughes ... ('lineTo', ((1.0, 0.1),), {}), 209*e1fe3e4aSElliott Hughes ... ('curveTo', ((1.0, 0.1), (2.0, 0.2), (3.0, 0.3)), {}) 210*e1fe3e4aSElliott Hughes ... ])) 211*e1fe3e4aSElliott Hughes pen.moveTo() 212*e1fe3e4aSElliott Hughes pen.lineTo((1, 0.1)) 213*e1fe3e4aSElliott Hughes pen.curveTo((1, 0.1), (2, 0.2), (3, 0.3)) 214*e1fe3e4aSElliott Hughes 215*e1fe3e4aSElliott Hughes >>> print(_repr_pen_commands([ 216*e1fe3e4aSElliott Hughes ... ('beginPath', tuple(), {}), 217*e1fe3e4aSElliott Hughes ... ('addPoint', ((1.0, 0.1),), 218*e1fe3e4aSElliott Hughes ... {"segmentType":"line", "smooth":True, "name":"test", "z":1}), 219*e1fe3e4aSElliott Hughes ... ])) 220*e1fe3e4aSElliott Hughes pen.beginPath() 221*e1fe3e4aSElliott Hughes pen.addPoint((1, 0.1), name='test', segmentType='line', smooth=True, z=1) 222*e1fe3e4aSElliott Hughes 223*e1fe3e4aSElliott Hughes >>> print(_repr_pen_commands([ 224*e1fe3e4aSElliott Hughes ... ('addComponent', ('A', (1, 0, 0, 1, 0, 0)), {}) 225*e1fe3e4aSElliott Hughes ... ])) 226*e1fe3e4aSElliott Hughes pen.addComponent('A', (1, 0, 0, 1, 0, 0)) 227*e1fe3e4aSElliott Hughes """ 228*e1fe3e4aSElliott Hughes s = [] 229*e1fe3e4aSElliott Hughes for cmd, args, kwargs in commands: 230*e1fe3e4aSElliott Hughes if args: 231*e1fe3e4aSElliott Hughes if isinstance(args[0], tuple): 232*e1fe3e4aSElliott Hughes # cast float to int if there're no digits after decimal point, 233*e1fe3e4aSElliott Hughes # and round floats to 12 decimal digits (more than enough) 234*e1fe3e4aSElliott Hughes args = [ 235*e1fe3e4aSElliott Hughes ( 236*e1fe3e4aSElliott Hughes tuple((int(v) if int(v) == v else round(v, 12)) for v in pt) 237*e1fe3e4aSElliott Hughes if pt is not None 238*e1fe3e4aSElliott Hughes else None 239*e1fe3e4aSElliott Hughes ) 240*e1fe3e4aSElliott Hughes for pt in args 241*e1fe3e4aSElliott Hughes ] 242*e1fe3e4aSElliott Hughes args = ", ".join(repr(a) for a in args) 243*e1fe3e4aSElliott Hughes if kwargs: 244*e1fe3e4aSElliott Hughes kwargs = ", ".join("%s=%r" % (k, v) for k, v in sorted(kwargs.items())) 245*e1fe3e4aSElliott Hughes if args and kwargs: 246*e1fe3e4aSElliott Hughes s.append("pen.%s(%s, %s)" % (cmd, args, kwargs)) 247*e1fe3e4aSElliott Hughes elif args: 248*e1fe3e4aSElliott Hughes s.append("pen.%s(%s)" % (cmd, args)) 249*e1fe3e4aSElliott Hughes elif kwargs: 250*e1fe3e4aSElliott Hughes s.append("pen.%s(%s)" % (cmd, kwargs)) 251*e1fe3e4aSElliott Hughes else: 252*e1fe3e4aSElliott Hughes s.append("pen.%s()" % cmd) 253*e1fe3e4aSElliott Hughes return "\n".join(s) 254*e1fe3e4aSElliott Hughes 255*e1fe3e4aSElliott Hughes 256*e1fe3e4aSElliott Hughesclass TestDummyGlyph(unittest.TestCase): 257*e1fe3e4aSElliott Hughes def test_equal(self): 258*e1fe3e4aSElliott Hughes # verify that the copy and the copy of the copy are equal to 259*e1fe3e4aSElliott Hughes # the source glyph's outline, as well as to each other 260*e1fe3e4aSElliott Hughes source = CUBIC_GLYPHS["a"] 261*e1fe3e4aSElliott Hughes copy = DummyGlyph(source) 262*e1fe3e4aSElliott Hughes copy2 = DummyGlyph(copy) 263*e1fe3e4aSElliott Hughes self.assertEqual(source, copy) 264*e1fe3e4aSElliott Hughes self.assertEqual(source, copy2) 265*e1fe3e4aSElliott Hughes self.assertEqual(copy, copy2) 266*e1fe3e4aSElliott Hughes # assert equality doesn't hold any more after modification 267*e1fe3e4aSElliott Hughes copy.outline.pop() 268*e1fe3e4aSElliott Hughes self.assertNotEqual(source, copy) 269*e1fe3e4aSElliott Hughes self.assertNotEqual(copy, copy2) 270*e1fe3e4aSElliott Hughes 271*e1fe3e4aSElliott Hughes 272*e1fe3e4aSElliott Hughesclass TestDummyPointGlyph(unittest.TestCase): 273*e1fe3e4aSElliott Hughes def test_equal(self): 274*e1fe3e4aSElliott Hughes # same as above but using the PointPen protocol 275*e1fe3e4aSElliott Hughes source = CUBIC_GLYPHS["a"] 276*e1fe3e4aSElliott Hughes copy = DummyPointGlyph(source) 277*e1fe3e4aSElliott Hughes copy2 = DummyPointGlyph(copy) 278*e1fe3e4aSElliott Hughes self.assertEqual(source, copy) 279*e1fe3e4aSElliott Hughes self.assertEqual(source, copy2) 280*e1fe3e4aSElliott Hughes self.assertEqual(copy, copy2) 281*e1fe3e4aSElliott Hughes copy.outline.pop() 282*e1fe3e4aSElliott Hughes self.assertNotEqual(source, copy) 283*e1fe3e4aSElliott Hughes self.assertNotEqual(copy, copy2) 284*e1fe3e4aSElliott Hughes 285*e1fe3e4aSElliott Hughes 286*e1fe3e4aSElliott Hughesif __name__ == "__main__": 287*e1fe3e4aSElliott Hughes unittest.main() 288