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