282 lines
9.3 KiB
Python
282 lines
9.3 KiB
Python
# Copyright 2016 Google Inc. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from . import CUBIC_GLYPHS
|
|
from fontTools.pens.pointPen import PointToSegmentPen, SegmentToPointPen
|
|
from math import isclose
|
|
import unittest
|
|
|
|
|
|
class BaseDummyPen(object):
|
|
"""Base class for pens that record the commands they are called with."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.commands = []
|
|
|
|
def __str__(self):
|
|
"""Return the pen commands as a string of python code."""
|
|
return _repr_pen_commands(self.commands)
|
|
|
|
def addComponent(self, glyphName, transformation, **kwargs):
|
|
self.commands.append(('addComponent', (glyphName, transformation), kwargs))
|
|
|
|
|
|
class DummyPen(BaseDummyPen):
|
|
"""A SegmentPen that records the commands it's called with."""
|
|
|
|
def moveTo(self, pt):
|
|
self.commands.append(('moveTo', (pt,), {}))
|
|
|
|
def lineTo(self, pt):
|
|
self.commands.append(('lineTo', (pt,), {}))
|
|
|
|
def curveTo(self, *points):
|
|
self.commands.append(('curveTo', points, {}))
|
|
|
|
def qCurveTo(self, *points):
|
|
self.commands.append(('qCurveTo', points, {}))
|
|
|
|
def closePath(self):
|
|
self.commands.append(('closePath', tuple(), {}))
|
|
|
|
def endPath(self):
|
|
self.commands.append(('endPath', tuple(), {}))
|
|
|
|
|
|
class DummyPointPen(BaseDummyPen):
|
|
"""A PointPen that records the commands it's called with."""
|
|
|
|
def beginPath(self, **kwargs):
|
|
self.commands.append(('beginPath', tuple(), kwargs))
|
|
|
|
def endPath(self):
|
|
self.commands.append(('endPath', tuple(), {}))
|
|
|
|
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
|
|
kwargs['segmentType'] = str(segmentType) if segmentType else None
|
|
kwargs['smooth'] = smooth
|
|
kwargs['name'] = name
|
|
self.commands.append(('addPoint', (pt,), kwargs))
|
|
|
|
|
|
class DummyGlyph(object):
|
|
"""Provides a minimal interface for storing a glyph's outline data in a
|
|
SegmentPen-oriented way. The glyph's outline consists in the list of
|
|
SegmentPen commands required to draw it.
|
|
"""
|
|
|
|
# the SegmentPen class used to draw on this glyph type
|
|
DrawingPen = DummyPen
|
|
|
|
def __init__(self, glyph=None):
|
|
"""If another glyph (i.e. any object having a 'draw' method) is given,
|
|
its outline data is copied to self.
|
|
"""
|
|
self._pen = self.DrawingPen()
|
|
self.outline = self._pen.commands
|
|
if glyph:
|
|
self.appendGlyph(glyph)
|
|
|
|
def appendGlyph(self, glyph):
|
|
"""Copy another glyph's outline onto self."""
|
|
glyph.draw(self._pen)
|
|
|
|
def getPen(self):
|
|
"""Return the SegmentPen that can 'draw' on this glyph."""
|
|
return self._pen
|
|
|
|
def getPointPen(self):
|
|
"""Return a PointPen adapter that can 'draw' on this glyph."""
|
|
return PointToSegmentPen(self._pen)
|
|
|
|
def draw(self, pen):
|
|
"""Use another SegmentPen to replay the glyph's outline commands."""
|
|
if self.outline:
|
|
for cmd, args, kwargs in self.outline:
|
|
getattr(pen, cmd)(*args, **kwargs)
|
|
|
|
def drawPoints(self, pointPen):
|
|
"""Use another PointPen to replay the glyph's outline commands,
|
|
indirectly through an adapter.
|
|
"""
|
|
pen = SegmentToPointPen(pointPen)
|
|
self.draw(pen)
|
|
|
|
def __eq__(self, other):
|
|
"""Return True if 'other' glyph's outline is the same as self."""
|
|
if hasattr(other, 'outline'):
|
|
return self.outline == other.outline
|
|
elif hasattr(other, 'draw'):
|
|
return self.outline == self.__class__(other).outline
|
|
return NotImplemented
|
|
|
|
def __ne__(self, other):
|
|
"""Return True if 'other' glyph's outline is different from self."""
|
|
return not (self == other)
|
|
|
|
def approx(self, other, rel_tol=1e-12):
|
|
if hasattr(other, 'outline'):
|
|
outline2 == other.outline
|
|
elif hasattr(other, 'draw'):
|
|
outline2 = self.__class__(other).outline
|
|
else:
|
|
raise TypeError(type(other).__name__)
|
|
outline1 = self.outline
|
|
if len(outline1) != len(outline2):
|
|
return False
|
|
for (cmd1, arg1, kwd1), (cmd2, arg2, kwd2) in zip(outline1, outline2):
|
|
if cmd1 != cmd2:
|
|
return False
|
|
if kwd1 != kwd2:
|
|
return False
|
|
if arg1:
|
|
if isinstance(arg1[0], tuple):
|
|
if not arg2 or not isinstance(arg2[0], tuple):
|
|
return False
|
|
for (x1, y1), (x2, y2) in zip(arg1, arg2):
|
|
if (
|
|
not isclose(x1, x2, rel_tol=rel_tol) or
|
|
not isclose(y1, y2, rel_tol=rel_tol)
|
|
):
|
|
return False
|
|
elif arg1 != arg2:
|
|
return False
|
|
elif arg2:
|
|
return False
|
|
return True
|
|
|
|
def __str__(self):
|
|
"""Return commands making up the glyph's outline as a string."""
|
|
return str(self._pen)
|
|
|
|
|
|
class DummyPointGlyph(DummyGlyph):
|
|
"""Provides a minimal interface for storing a glyph's outline data in a
|
|
PointPen-oriented way. The glyph's outline consists in the list of
|
|
PointPen commands required to draw it.
|
|
"""
|
|
|
|
# the PointPen class used to draw on this glyph type
|
|
DrawingPen = DummyPointPen
|
|
|
|
def appendGlyph(self, glyph):
|
|
"""Copy another glyph's outline onto self."""
|
|
glyph.drawPoints(self._pen)
|
|
|
|
def getPen(self):
|
|
"""Return a SegmentPen adapter that can 'draw' on this glyph."""
|
|
return SegmentToPointPen(self._pen)
|
|
|
|
def getPointPen(self):
|
|
"""Return the PointPen that can 'draw' on this glyph."""
|
|
return self._pen
|
|
|
|
def draw(self, pen):
|
|
"""Use another SegmentPen to replay the glyph's outline commands,
|
|
indirectly through an adapter.
|
|
"""
|
|
pointPen = PointToSegmentPen(pen)
|
|
self.drawPoints(pointPen)
|
|
|
|
def drawPoints(self, pointPen):
|
|
"""Use another PointPen to replay the glyph's outline commands."""
|
|
if self.outline:
|
|
for cmd, args, kwargs in self.outline:
|
|
getattr(pointPen, cmd)(*args, **kwargs)
|
|
|
|
|
|
def _repr_pen_commands(commands):
|
|
"""
|
|
>>> print(_repr_pen_commands([
|
|
... ('moveTo', tuple(), {}),
|
|
... ('lineTo', ((1.0, 0.1),), {}),
|
|
... ('curveTo', ((1.0, 0.1), (2.0, 0.2), (3.0, 0.3)), {})
|
|
... ]))
|
|
pen.moveTo()
|
|
pen.lineTo((1, 0.1))
|
|
pen.curveTo((1, 0.1), (2, 0.2), (3, 0.3))
|
|
|
|
>>> print(_repr_pen_commands([
|
|
... ('beginPath', tuple(), {}),
|
|
... ('addPoint', ((1.0, 0.1),),
|
|
... {"segmentType":"line", "smooth":True, "name":"test", "z":1}),
|
|
... ]))
|
|
pen.beginPath()
|
|
pen.addPoint((1, 0.1), name='test', segmentType='line', smooth=True, z=1)
|
|
|
|
>>> print(_repr_pen_commands([
|
|
... ('addComponent', ('A', (1, 0, 0, 1, 0, 0)), {})
|
|
... ]))
|
|
pen.addComponent('A', (1, 0, 0, 1, 0, 0))
|
|
"""
|
|
s = []
|
|
for cmd, args, kwargs in commands:
|
|
if args:
|
|
if isinstance(args[0], tuple):
|
|
# cast float to int if there're no digits after decimal point,
|
|
# and round floats to 12 decimal digits (more than enough)
|
|
args = [
|
|
tuple((int(v) if int(v) == v else round(v, 12)) for v in pt)
|
|
for pt in args
|
|
]
|
|
args = ", ".join(repr(a) for a in args)
|
|
if kwargs:
|
|
kwargs = ", ".join("%s=%r" % (k, v)
|
|
for k, v in sorted(kwargs.items()))
|
|
if args and kwargs:
|
|
s.append("pen.%s(%s, %s)" % (cmd, args, kwargs))
|
|
elif args:
|
|
s.append("pen.%s(%s)" % (cmd, args))
|
|
elif kwargs:
|
|
s.append("pen.%s(%s)" % (cmd, kwargs))
|
|
else:
|
|
s.append("pen.%s()" % cmd)
|
|
return "\n".join(s)
|
|
|
|
|
|
class TestDummyGlyph(unittest.TestCase):
|
|
|
|
def test_equal(self):
|
|
# verify that the copy and the copy of the copy are equal to
|
|
# the source glyph's outline, as well as to each other
|
|
source = CUBIC_GLYPHS['a']
|
|
copy = DummyGlyph(source)
|
|
copy2 = DummyGlyph(copy)
|
|
self.assertEqual(source, copy)
|
|
self.assertEqual(source, copy2)
|
|
self.assertEqual(copy, copy2)
|
|
# assert equality doesn't hold any more after modification
|
|
copy.outline.pop()
|
|
self.assertNotEqual(source, copy)
|
|
self.assertNotEqual(copy, copy2)
|
|
|
|
|
|
class TestDummyPointGlyph(unittest.TestCase):
|
|
|
|
def test_equal(self):
|
|
# same as above but using the PointPen protocol
|
|
source = CUBIC_GLYPHS['a']
|
|
copy = DummyPointGlyph(source)
|
|
copy2 = DummyPointGlyph(copy)
|
|
self.assertEqual(source, copy)
|
|
self.assertEqual(source, copy2)
|
|
self.assertEqual(copy, copy2)
|
|
copy.outline.pop()
|
|
self.assertNotEqual(source, copy)
|
|
self.assertNotEqual(copy, copy2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|