diff --git a/Lib/fontTools/pens/freetypePen.py b/Lib/fontTools/pens/freetypePen.py index 1cb171b04..705614f3e 100644 --- a/Lib/fontTools/pens/freetypePen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -114,7 +114,7 @@ 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. @@ -156,7 +156,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 +169,49 @@ 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'): 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) @@ -223,7 +239,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 +251,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) @@ -257,7 +283,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,9 +291,19 @@ 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) @@ -286,7 +322,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 +334,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) 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..c194b82f6 100644 --- a/Tests/pens/freetypePen_test.py +++ b/Tests/pens/freetypePen_test.py @@ -1,5 +1,6 @@ import unittest import os +import math try: from fontTools.pens.freetypePen import FreeTypePen @@ -11,11 +12,11 @@ from fontTools.misc.transform import Scale, Offset 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): @@ -136,24 +137,78 @@ class FreeTypePenTest(unittest.TestCase): 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) + 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())