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