xref: /aosp_15_r20/external/fonttools/Tests/pens/utils.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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