• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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