blob: dced3c1be4824ce0daacd253182d0dc90a53a0ab [file] [log] [blame]
# 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()