diff --git a/Lib/fontTools/pens/freetypePen.py b/Lib/fontTools/pens/freetypePen.py index 1cb171b04..870776bc7 100644 --- a/Lib/fontTools/pens/freetypePen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -2,7 +2,7 @@ """Pen to rasterize paths with FreeType.""" -__all__ = ['FreeTypePen'] +__all__ = ["FreeTypePen"] import os import ctypes @@ -15,19 +15,22 @@ import freetype from freetype.raw import FT_Outline_Get_Bitmap, FT_Outline_Get_BBox, FT_Outline_Get_CBox from freetype.ft_types import FT_Pos from freetype.ft_structs import FT_Vector, FT_BBox, FT_Bitmap, FT_Outline -from freetype.ft_enums import FT_OUTLINE_NONE, FT_OUTLINE_EVEN_ODD_FILL, FT_PIXEL_MODE_GRAY +from freetype.ft_enums import ( + FT_OUTLINE_NONE, + FT_OUTLINE_EVEN_ODD_FILL, + FT_PIXEL_MODE_GRAY, + FT_CURVE_TAG_ON, + FT_CURVE_TAG_CONIC, + FT_CURVE_TAG_CUBIC, +) from freetype.ft_errors import FT_Exception -from fontTools.pens.basePen import BasePen +from fontTools.pens.basePen import BasePen, PenError from fontTools.misc.roundTools import otRound from fontTools.misc.transform import Transform -Contour = collections.namedtuple('Contour', ('points', 'tags')) -LINE = 0b00000001 -CURVE = 0b00000011 -OFFCURVE = 0b00000010 -QCURVE = 0b00000001 -QOFFCURVE = 0b00000000 +Contour = collections.namedtuple("Contour", ("points", "tags")) + class FreeTypePen(BasePen): """Pen to rasterize paths with FreeType. Requires `freetype-py` module. @@ -114,21 +117,25 @@ class FreeTypePen(BasePen): """Converts the current contours to ``FT_Outline``. Args: - transform: A optional 6-tuple containing an affine transformation, + transform: An optional 6-tuple containing an affine transformation, or a ``Transform`` object from the ``fontTools.misc.transform`` module. evenOdd: Pass ``True`` for even-odd fill instead of non-zero. """ transform = transform or Transform() - if not hasattr(transform, 'transformPoint'): + if not hasattr(transform, "transformPoint"): transform = Transform(*transform) - nContours = len(self.contours) - n_points = sum((len(contour.points) for contour in self.contours)) + n_contours = len(self.contours) + n_points = sum((len(contour.points) for contour in self.contours)) points = [] for contour in self.contours: for point in contour.points: point = transform.transformPoint(point) - points.append(FT_Vector(FT_Pos(otRound(point[0] * 64)), FT_Pos(otRound(point[1] * 64)))) + points.append( + FT_Vector( + FT_Pos(otRound(point[0] * 64)), FT_Pos(otRound(point[1] * 64)) + ) + ) tags = [] for contour in self.contours: for tag in contour.tags: @@ -140,15 +147,17 @@ class FreeTypePen(BasePen): contours.append(contours_sum - 1) flags = FT_OUTLINE_EVEN_ODD_FILL if evenOdd else FT_OUTLINE_NONE return FT_Outline( - (ctypes.c_short)(nContours), + (ctypes.c_short)(n_contours), (ctypes.c_short)(n_points), - (FT_Vector * n_points)(*points), + (FT_Vector * n_points)(*points), (ctypes.c_ubyte * n_points)(*tags), - (ctypes.c_short * nContours)(*contours), - (ctypes.c_int)(flags) + (ctypes.c_short * n_contours)(*contours), + (ctypes.c_int)(flags), ) - def buffer(self, width=None, height=None, transform=None, contain=False, evenOdd=False): + def buffer( + self, width=None, height=None, transform=None, contain=False, evenOdd=False + ): """Renders the current contours within a bitmap buffer. Args: @@ -156,7 +165,7 @@ class FreeTypePen(BasePen): automatically fits to the bounding box of the contours. height: Image height of the bitmap in pixels. If omitted, it automatically fits to the bounding box of the contours. - transform: A optional 6-tuple containing an affine transformation, + transform: An optional 6-tuple containing an affine transformation, or a ``Transform`` object from the ``fontTools.misc.transform`` module. The bitmap size is not affected by this matrix. contain: If ``True``, the image size will be automatically expanded @@ -169,33 +178,54 @@ class FreeTypePen(BasePen): object of the resulted bitmap and ``size`` is a 2-tuple of its dimension. + :Notes: + The image size should always be given explicitly if you need to get + a proper glyph image. When ``width`` and ``height`` are omitted, it + forcifully fits to the bounding box and the side bearings get + cropped. If you pass ``0`` to both ``width`` and ``height`` and set + ``contain`` to ``True``, it expands to the bounding box while + maintaining the origin of the contours, meaning that LSB will be + maintained but RSB won’t. The difference between the two becomes + more obvious when rotate or skew transformation is applied. + :Example: .. code-block:: - + >> pen = FreeTypePen(None) >> glyph.draw(pen) >> buf, size = pen.buffer(width=500, height=1000) >> type(buf), len(buf), size (, 500000, (500, 1000)) - + """ transform = transform or Transform() - if not hasattr(transform, 'transformPoint'): + if not hasattr(transform, "transformPoint"): transform = Transform(*transform) contain_x, contain_y = contain or width is None, contain or height is None - width, height = width or 0, height or 0 if contain_x or contain_y: - bbox = self.bbox - bbox = transform.transformPoints((bbox[0:2], bbox[2:4])) - bbox = (*bbox[0], *bbox[1]) - bbox_size = bbox[2] - bbox[0], bbox[3] - bbox[1] dx, dy = transform.dx, transform.dy + bbox = self.bbox + p1, p2, p3, p4 = ( + transform.transformPoint((bbox[0], bbox[1])), + transform.transformPoint((bbox[2], bbox[1])), + transform.transformPoint((bbox[0], bbox[3])), + transform.transformPoint((bbox[2], bbox[3])), + ) + px, py = (p1[0], p2[0], p3[0], p4[0]), (p1[1], p2[1], p3[1], p4[1]) if contain_x: - dx = min(-dx, bbox[0]) * -1.0 - width = max(width, bbox_size[0]) + if width is None: + dx = dx - min(*px) + width = max(*px) - min(*px) + else: + dx = dx - min(min(*px), 0.0) + width = max(width, max(*px) - min(min(*px), 0.0)) if contain_y: - dy = min(-dy, bbox[1]) * -1.0 - height = max(height, bbox_size[1]) + if height is None: + dy = dy - min(*py) + height = max(*py) - min(*py) + else: + dy = dy - min(min(*py), 0.0) + height = max(height, max(*py) - min(min(*py), 0.0)) transform = Transform(*transform[:4], dx, dy) width, height = math.ceil(width), math.ceil(height) buf = ctypes.create_string_buffer(width * height) @@ -207,15 +237,19 @@ class FreeTypePen(BasePen): (ctypes.c_short)(256), (ctypes.c_ubyte)(FT_PIXEL_MODE_GRAY), (ctypes.c_char)(0), - (ctypes.c_void_p)(None) + (ctypes.c_void_p)(None), ) outline = self.outline(transform=transform, evenOdd=evenOdd) - err = FT_Outline_Get_Bitmap(freetype.get_handle(), ctypes.byref(outline), ctypes.byref(bitmap)) + err = FT_Outline_Get_Bitmap( + freetype.get_handle(), ctypes.byref(outline), ctypes.byref(bitmap) + ) if err != 0: raise FT_Exception(err) return buf.raw, (width, height) - def array(self, width=None, height=None, transform=None, contain=False, evenOdd=False): + def array( + self, width=None, height=None, transform=None, contain=False, evenOdd=False + ): """Returns the rendered contours as a numpy array. Requires `numpy`. Args: @@ -223,7 +257,7 @@ class FreeTypePen(BasePen): automatically fits to the bounding box of the contours. height: Image height of the bitmap in pixels. If omitted, it automatically fits to the bounding box of the contours. - transform: A optional 6-tuple containing an affine transformation, + transform: An optional 6-tuple containing an affine transformation, or a ``Transform`` object from the ``fontTools.misc.transform`` module. The bitmap size is not affected by this matrix. contain: If ``True``, the image size will be automatically expanded @@ -235,9 +269,19 @@ class FreeTypePen(BasePen): A ``numpy.ndarray`` object with a shape of ``(height, width)``. Each element takes a value in the range of ``[0.0, 1.0]``. + :Notes: + The image size should always be given explicitly if you need to get + a proper glyph image. When ``width`` and ``height`` are omitted, it + forcifully fits to the bounding box and the side bearings get + cropped. If you pass ``0`` to both ``width`` and ``height`` and set + ``contain`` to ``True``, it expands to the bounding box while + maintaining the origin of the contours, meaning that LSB will be + maintained but RSB won’t. The difference between the two becomes + more obvious when rotate or skew transformation is applied. + :Example: .. code-block:: - + >> pen = FreeTypePen(None) >> glyph.draw(pen) >> arr = pen.array(width=500, height=1000) @@ -245,10 +289,19 @@ class FreeTypePen(BasePen): (, (1000, 500)) """ import numpy as np - buf, size = self.buffer(width=width, height=height, transform=transform, contain=contain, evenOdd=evenOdd) - return np.frombuffer(buf, 'B').reshape((size[1], size[0])) / 255.0 - def show(self, width=None, height=None, transform=None, contain=False, evenOdd=False): + buf, size = self.buffer( + width=width, + height=height, + transform=transform, + contain=contain, + evenOdd=evenOdd, + ) + return np.frombuffer(buf, "B").reshape((size[1], size[0])) / 255.0 + + def show( + self, width=None, height=None, transform=None, contain=False, evenOdd=False + ): """Plots the rendered contours with `pyplot`. Requires `numpy` and `matplotlib`. @@ -257,7 +310,7 @@ class FreeTypePen(BasePen): automatically fits to the bounding box of the contours. height: Image height of the bitmap in pixels. If omitted, it automatically fits to the bounding box of the contours. - transform: A optional 6-tuple containing an affine transformation, + transform: An optional 6-tuple containing an affine transformation, or a ``Transform`` object from the ``fontTools.misc.transform`` module. The bitmap size is not affected by this matrix. contain: If ``True``, the image size will be automatically expanded @@ -265,19 +318,38 @@ class FreeTypePen(BasePen): rendering glyphs with negative sidebearings without clipping. evenOdd: Pass ``True`` for even-odd fill instead of non-zero. + :Notes: + The image size should always be given explicitly if you need to get + a proper glyph image. When ``width`` and ``height`` are omitted, it + forcifully fits to the bounding box and the side bearings get + cropped. If you pass ``0`` to both ``width`` and ``height`` and set + ``contain`` to ``True``, it expands to the bounding box while + maintaining the origin of the contours, meaning that LSB will be + maintained but RSB won’t. The difference between the two becomes + more obvious when rotate or skew transformation is applied. + :Example: .. code-block:: - + >> pen = FreeTypePen(None) >> glyph.draw(pen) >> pen.show(width=500, height=1000) """ from matplotlib import pyplot as plt - a = self.array(width=width, height=height, transform=transform, contain=contain, evenOdd=evenOdd) - plt.imshow(a, cmap='gray_r', vmin=0, vmax=1) + + a = self.array( + width=width, + height=height, + transform=transform, + contain=contain, + evenOdd=evenOdd, + ) + plt.imshow(a, cmap="gray_r", vmin=0, vmax=1) plt.show() - def image(self, width=None, height=None, transform=None, contain=False, evenOdd=False): + def image( + self, width=None, height=None, transform=None, contain=False, evenOdd=False + ): """Returns the rendered contours as a PIL image. Requires `Pillow`. Can be used to display a glyph image in Jupyter Notebook. @@ -286,7 +358,7 @@ class FreeTypePen(BasePen): automatically fits to the bounding box of the contours. height: Image height of the bitmap in pixels. If omitted, it automatically fits to the bounding box of the contours. - transform: A optional 6-tuple containing an affine transformation, + transform: An optional 6-tuple containing an affine transformation, or a ``Transform`` object from the ``fontTools.misc.transform`` module. The bitmap size is not affected by this matrix. contain: If ``True``, the image size will be automatically expanded @@ -298,9 +370,19 @@ class FreeTypePen(BasePen): A ``PIL.image`` object. The image is filled in black with alpha channel obtained from the rendered bitmap. + :Notes: + The image size should always be given explicitly if you need to get + a proper glyph image. When ``width`` and ``height`` are omitted, it + forcifully fits to the bounding box and the side bearings get + cropped. If you pass ``0`` to both ``width`` and ``height`` and set + ``contain`` to ``True``, it expands to the bounding box while + maintaining the origin of the contours, meaning that LSB will be + maintained but RSB won’t. The difference between the two becomes + more obvious when rotate or skew transformation is applied. + :Example: .. code-block:: - + >> pen = FreeTypePen(None) >> glyph.draw(pen) >> img = pen.image(width=500, height=1000) @@ -308,9 +390,16 @@ class FreeTypePen(BasePen): (, (500, 1000)) """ from PIL import Image - buf, size = self.buffer(width=width, height=height, transform=transform, contain=contain, evenOdd=evenOdd) - img = Image.new('L', size, 0) - img.putalpha(Image.frombuffer('L', size, buf)) + + buf, size = self.buffer( + width=width, + height=height, + transform=transform, + contain=contain, + evenOdd=evenOdd, + ) + img = Image.new("L", size, 0) + img.putalpha(Image.frombuffer("L", size, buf)) return img @property @@ -341,22 +430,28 @@ class FreeTypePen(BasePen): contour = Contour([], []) self.contours.append(contour) contour.points.append(pt) - contour.tags.append(LINE) + contour.tags.append(FT_CURVE_TAG_ON) def _lineTo(self, pt): + if not (self.contours and len(self.contours[-1].points) > 0): + raise PenError("Contour missing required initial moveTo") contour = self.contours[-1] contour.points.append(pt) - contour.tags.append(LINE) + contour.tags.append(FT_CURVE_TAG_ON) def _curveToOne(self, p1, p2, p3): - t1, t2, t3 = OFFCURVE, OFFCURVE, CURVE + if not (self.contours and len(self.contours[-1].points) > 0): + raise PenError("Contour missing required initial moveTo") + t1, t2, t3 = FT_CURVE_TAG_CUBIC, FT_CURVE_TAG_CUBIC, FT_CURVE_TAG_ON contour = self.contours[-1] for p, t in ((p1, t1), (p2, t2), (p3, t3)): contour.points.append(p) contour.tags.append(t) def _qCurveToOne(self, p1, p2): - t1, t2 = QOFFCURVE, QCURVE + if not (self.contours and len(self.contours[-1].points) > 0): + raise PenError("Contour missing required initial moveTo") + t1, t2 = FT_CURVE_TAG_CONIC, FT_CURVE_TAG_ON contour = self.contours[-1] for p, t in ((p1, t1), (p2, t2)): contour.points.append(p) diff --git a/Tests/pens/data/test_rotate.pgm b/Tests/pens/data/test_rotate.pgm new file mode 100644 index 000000000..14edd9fc8 Binary files /dev/null and b/Tests/pens/data/test_rotate.pgm differ diff --git a/Tests/pens/data/test_skew.pgm b/Tests/pens/data/test_skew.pgm new file mode 100644 index 000000000..5ba4a0671 Binary files /dev/null and b/Tests/pens/data/test_skew.pgm differ diff --git a/Tests/pens/freetypePen_test.py b/Tests/pens/freetypePen_test.py index 82b76c5f4..b6edc8bb9 100644 --- a/Tests/pens/freetypePen_test.py +++ b/Tests/pens/freetypePen_test.py @@ -1,23 +1,27 @@ import unittest import os +import math try: from fontTools.pens.freetypePen import FreeTypePen + FREETYPE_PY_AVAILABLE = True except ImportError: FREETYPE_PY_AVAILABLE = False from fontTools.misc.transform import Scale, Offset -DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data') +DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data") -def box(pen): - pen.moveTo((0, 0)) - pen.lineTo((0, 500)) - pen.lineTo((500, 500)) - pen.lineTo((500, 0)) + +def box(pen, offset=(0, 0)): + pen.moveTo((0 + offset[0], 0 + offset[1])) + pen.lineTo((0 + offset[0], 500 + offset[1])) + pen.lineTo((500 + offset[0], 500 + offset[1])) + pen.lineTo((500 + offset[0], 0 + offset[1])) pen.closePath() + def draw_cubic(pen): pen.moveTo((50, 0)) pen.lineTo((50, 500)) @@ -26,6 +30,7 @@ def draw_cubic(pen): pen.curveTo((450, 100), (350, 0), (200, 0)) pen.closePath() + def draw_quadratic(pen): pen.moveTo((50, 0)) pen.lineTo((50, 500)) @@ -34,6 +39,7 @@ def draw_quadratic(pen): pen.qCurveTo((450, 176), (388, 62), (274, 0), (200, 0)) pen.closePath() + def star(pen): pen.moveTo((0, 420)) pen.lineTo((1000, 420)) @@ -42,32 +48,38 @@ def star(pen): pen.lineTo((800, -200)) pen.closePath() + # For the PGM format, see the following resources: # https://en.wikipedia.org/wiki/Netpbm # http://netpbm.sourceforge.net/doc/pgm.html def load_pgm(filename): - with open(filename, 'rb') as fp: - assert fp.readline() == 'P5\n'.encode() - w, h = (int(c) for c in fp.readline().decode().rstrip().split(' ')) - assert fp.readline() == '255\n'.encode() + with open(filename, "rb") as fp: + assert fp.readline() == "P5\n".encode() + w, h = (int(c) for c in fp.readline().decode().rstrip().split(" ")) + assert fp.readline() == "255\n".encode() return fp.read(), (w, h) + def save_pgm(filename, buf, size): - with open(filename, 'wb') as fp: - fp.write('P5\n'.encode()) - fp.write('{0:d} {1:d}\n'.format(*size).encode()) - fp.write('255\n'.encode()) + with open(filename, "wb") as fp: + fp.write("P5\n".encode()) + fp.write("{0:d} {1:d}\n".format(*size).encode()) + fp.write("255\n".encode()) fp.write(buf) + # Assume the buffers are equal when PSNR > 38dB. See also: # Peak signal-to-noise ratio # https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio PSNR_THRESHOLD = 38.0 + def psnr(b1, b2): import math - mse = sum((c1-c2) * (c1-c2) for c1, c2 in zip(b1, b2)) / float(len(b1)) - return 10.0 * math.log10((255.0 ** 2) / float(mse)) if mse > 0 else math.inf + + mse = sum((c1 - c2) * (c1 - c2) for c1, c2 in zip(b1, b2)) / float(len(b1)) + return 10.0 * math.log10((255.0**2) / float(mse)) if mse > 0 else math.inf + @unittest.skipUnless(FREETYPE_PY_AVAILABLE, "freetype-py not installed") class FreeTypePenTest(unittest.TestCase): @@ -76,14 +88,14 @@ class FreeTypePenTest(unittest.TestCase): box(pen) width, height = 500, 500 buf1, _ = pen.buffer(width=width, height=height) - buf2 = b'\xff' * width * height + buf2 = b"\xff" * width * height self.assertEqual(buf1, buf2) def test_empty(self): pen = FreeTypePen(None) width, height = 500, 500 buf, size = pen.buffer(width=width, height=height) - self.assertEqual(b'\0' * size[0] * size[1], buf) + self.assertEqual(b"\0" * size[0] * size[1], buf) def test_bbox_and_cbox(self): pen = FreeTypePen(None) @@ -98,7 +110,7 @@ class FreeTypePenTest(unittest.TestCase): width, height = t.transformPoint((1000, 1000)) t = t.translate(0, 200) buf1, size1 = pen.buffer(width=width, height=height, transform=t, evenOdd=False) - buf2, size2 = load_pgm(os.path.join(DATA_DIR, 'test_non_zero_fill.pgm')) + buf2, size2 = load_pgm(os.path.join(DATA_DIR, "test_non_zero_fill.pgm")) self.assertEqual(len(buf1), len(buf2)) self.assertEqual(size1, size2) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) @@ -110,7 +122,7 @@ class FreeTypePenTest(unittest.TestCase): width, height = t.transformPoint((1000, 1000)) t = t.translate(0, 200) buf1, size1 = pen.buffer(width=width, height=height, transform=t, evenOdd=True) - buf2, size2 = load_pgm(os.path.join(DATA_DIR, 'test_even_odd_fill.pgm')) + buf2, size2 = load_pgm(os.path.join(DATA_DIR, "test_even_odd_fill.pgm")) self.assertEqual(len(buf1), len(buf2)) self.assertEqual(size1, size2) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) @@ -131,29 +143,93 @@ class FreeTypePenTest(unittest.TestCase): t = Scale(0.05, 0.05) width, height = 0, 0 buf1, size1 = pen.buffer(width=width, height=height, transform=t, contain=True) - buf2, size2 = load_pgm(os.path.join(DATA_DIR, 'test_non_zero_fill.pgm')) + buf2, size2 = load_pgm(os.path.join(DATA_DIR, "test_non_zero_fill.pgm")) self.assertEqual(len(buf1), len(buf2)) self.assertEqual(size1, size2) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) - def test_none_width(self): + def test_rotate(self): + pen = FreeTypePen(None) + box(pen) + t = Scale(0.05, 0.05).rotate(math.pi / 4.0).translate(1234, 5678) + width, height = None, None + buf1, size1 = pen.buffer(width=width, height=height, transform=t) + buf2, size2 = load_pgm(os.path.join(DATA_DIR, "test_rotate.pgm")) + self.assertEqual(len(buf1), len(buf2)) + self.assertEqual(size1, size2) + self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) + + def test_skew(self): + pen = FreeTypePen(None) + box(pen) + t = Scale(0.05, 0.05).skew(math.pi / 4.0).translate(1234, 5678) + width, height = None, None + buf1, size1 = pen.buffer(width=width, height=height, transform=t) + buf2, size2 = load_pgm(os.path.join(DATA_DIR, "test_skew.pgm")) + self.assertEqual(len(buf1), len(buf2)) + self.assertEqual(size1, size2) + self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) + + def test_none_size(self): pen = FreeTypePen(None) star(pen) - width, height = None, 1000 + width, height = None, None buf1, size = pen.buffer(width=width, height=height, transform=Offset(0, 200)) - buf2, _ = pen.buffer(width=1000, height=height, transform=Offset(0, 200)) + buf2, _ = pen.buffer(width=1000, height=1000, transform=Offset(0, 200)) self.assertEqual(size, (1000, 1000)) self.assertEqual(buf1, buf2) - def test_none_height(self): + pen = FreeTypePen(None) + box(pen, offset=(250, 250)) + width, height = None, None + buf1, size = pen.buffer(width=width, height=height) + buf2, _ = pen.buffer(width=500, height=500, transform=Offset(-250, -250)) + self.assertEqual(size, (500, 500)) + self.assertEqual(buf1, buf2) + + pen = FreeTypePen(None) + box(pen, offset=(-1234, -5678)) + width, height = None, None + buf1, size = pen.buffer(width=width, height=height) + buf2, _ = pen.buffer(width=500, height=500, transform=Offset(1234, 5678)) + self.assertEqual(size, (500, 500)) + self.assertEqual(buf1, buf2) + + def test_zero_size(self): pen = FreeTypePen(None) star(pen) - width, height = 1000, None - buf1, size = pen.buffer(width=width, height=height) - buf2, _ = pen.buffer(width=width, height=1000, transform=Offset(0, 200)) + width, height = 0, 0 + buf1, size = pen.buffer( + width=width, height=height, transform=Offset(0, 200), contain=True + ) + buf2, _ = pen.buffer( + width=1000, height=1000, transform=Offset(0, 200), contain=True + ) self.assertEqual(size, (1000, 1000)) self.assertEqual(buf1, buf2) -if __name__ == '__main__': + pen = FreeTypePen(None) + box(pen, offset=(250, 250)) + width, height = 0, 0 + buf1, size = pen.buffer(width=width, height=height, contain=True) + buf2, _ = pen.buffer( + width=500, height=500, transform=Offset(0, 0), contain=True + ) + self.assertEqual(size, (750, 750)) + self.assertEqual(buf1, buf2) + + pen = FreeTypePen(None) + box(pen, offset=(-1234, -5678)) + width, height = 0, 0 + buf1, size = pen.buffer(width=width, height=height, contain=True) + buf2, _ = pen.buffer( + width=500, height=500, transform=Offset(1234, 5678), contain=True + ) + self.assertEqual(size, (500, 500)) + self.assertEqual(buf1, buf2) + + +if __name__ == "__main__": import sys + sys.exit(unittest.main())