freetypePen: handle rotate/skew transform

The pen is designed to determine the bitmap size when omitted, which
helps users to see an image somehow even when they have no idea how the
arguments should be passed. And I realised that I didn't give enough
thought to rotate/skew transforms in former PRs. This commit fixes the
calculation of the bbox after transformation. Also tries to clear up how
the autosizing options work in the docstring.

Some minor fixes will follow.
This commit is contained in:
Takaaki Fuji 2022-01-22 07:00:45 +09:00
parent 69fc06a1af
commit a27789c477
4 changed files with 132 additions and 31 deletions

View File

@ -114,7 +114,7 @@ class FreeTypePen(BasePen):
"""Converts the current contours to ``FT_Outline``. """Converts the current contours to ``FT_Outline``.
Args: 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`` or a ``Transform`` object from the ``fontTools.misc.transform``
module. module.
evenOdd: Pass ``True`` for even-odd fill instead of non-zero. 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. automatically fits to the bounding box of the contours.
height: Image height of the bitmap in pixels. If omitted, it height: Image height of the bitmap in pixels. If omitted, it
automatically fits to the bounding box of the contours. 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`` or a ``Transform`` object from the ``fontTools.misc.transform``
module. The bitmap size is not affected by this matrix. module. The bitmap size is not affected by this matrix.
contain: If ``True``, the image size will be automatically expanded 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 object of the resulted bitmap and ``size`` is a 2-tuple of its
dimension. 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 wont. The difference between the two becomes
more obvious when rotate or skew transformation is applied.
:Example: :Example:
.. code-block:: .. code-block::
>> pen = FreeTypePen(None) >> pen = FreeTypePen(None)
>> glyph.draw(pen) >> glyph.draw(pen)
>> buf, size = pen.buffer(width=500, height=1000) >> buf, size = pen.buffer(width=500, height=1000)
>> type(buf), len(buf), size >> type(buf), len(buf), size
(<class 'bytes'>, 500000, (500, 1000)) (<class 'bytes'>, 500000, (500, 1000))
""" """
transform = transform or Transform() transform = transform or Transform()
if not hasattr(transform, 'transformPoint'): if not hasattr(transform, 'transformPoint'):
transform = Transform(*transform) transform = Transform(*transform)
contain_x, contain_y = contain or width is None, contain or height is None 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: 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 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: if contain_x:
dx = min(-dx, bbox[0]) * -1.0 if width is None:
width = max(width, bbox_size[0]) 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: if contain_y:
dy = min(-dy, bbox[1]) * -1.0 if height is None:
height = max(height, bbox_size[1]) 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) transform = Transform(*transform[:4], dx, dy)
width, height = math.ceil(width), math.ceil(height) width, height = math.ceil(width), math.ceil(height)
buf = ctypes.create_string_buffer(width * height) buf = ctypes.create_string_buffer(width * height)
@ -223,7 +239,7 @@ class FreeTypePen(BasePen):
automatically fits to the bounding box of the contours. automatically fits to the bounding box of the contours.
height: Image height of the bitmap in pixels. If omitted, it height: Image height of the bitmap in pixels. If omitted, it
automatically fits to the bounding box of the contours. 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`` or a ``Transform`` object from the ``fontTools.misc.transform``
module. The bitmap size is not affected by this matrix. module. The bitmap size is not affected by this matrix.
contain: If ``True``, the image size will be automatically expanded 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)``. A ``numpy.ndarray`` object with a shape of ``(height, width)``.
Each element takes a value in the range of ``[0.0, 1.0]``. 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 wont. The difference between the two becomes
more obvious when rotate or skew transformation is applied.
:Example: :Example:
.. code-block:: .. code-block::
>> pen = FreeTypePen(None) >> pen = FreeTypePen(None)
>> glyph.draw(pen) >> glyph.draw(pen)
>> arr = pen.array(width=500, height=1000) >> arr = pen.array(width=500, height=1000)
@ -257,7 +283,7 @@ class FreeTypePen(BasePen):
automatically fits to the bounding box of the contours. automatically fits to the bounding box of the contours.
height: Image height of the bitmap in pixels. If omitted, it height: Image height of the bitmap in pixels. If omitted, it
automatically fits to the bounding box of the contours. 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`` or a ``Transform`` object from the ``fontTools.misc.transform``
module. The bitmap size is not affected by this matrix. module. The bitmap size is not affected by this matrix.
contain: If ``True``, the image size will be automatically expanded contain: If ``True``, the image size will be automatically expanded
@ -265,9 +291,19 @@ class FreeTypePen(BasePen):
rendering glyphs with negative sidebearings without clipping. rendering glyphs with negative sidebearings without clipping.
evenOdd: Pass ``True`` for even-odd fill instead of non-zero. 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 wont. The difference between the two becomes
more obvious when rotate or skew transformation is applied.
:Example: :Example:
.. code-block:: .. code-block::
>> pen = FreeTypePen(None) >> pen = FreeTypePen(None)
>> glyph.draw(pen) >> glyph.draw(pen)
>> pen.show(width=500, height=1000) >> pen.show(width=500, height=1000)
@ -286,7 +322,7 @@ class FreeTypePen(BasePen):
automatically fits to the bounding box of the contours. automatically fits to the bounding box of the contours.
height: Image height of the bitmap in pixels. If omitted, it height: Image height of the bitmap in pixels. If omitted, it
automatically fits to the bounding box of the contours. 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`` or a ``Transform`` object from the ``fontTools.misc.transform``
module. The bitmap size is not affected by this matrix. module. The bitmap size is not affected by this matrix.
contain: If ``True``, the image size will be automatically expanded 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 A ``PIL.image`` object. The image is filled in black with alpha
channel obtained from the rendered bitmap. 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 wont. The difference between the two becomes
more obvious when rotate or skew transformation is applied.
:Example: :Example:
.. code-block:: .. code-block::
>> pen = FreeTypePen(None) >> pen = FreeTypePen(None)
>> glyph.draw(pen) >> glyph.draw(pen)
>> img = pen.image(width=500, height=1000) >> img = pen.image(width=500, height=1000)

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,6 @@
import unittest import unittest
import os import os
import math
try: try:
from fontTools.pens.freetypePen import FreeTypePen 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') DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
def box(pen): def box(pen, offset=(0, 0)):
pen.moveTo((0, 0)) pen.moveTo((0 + offset[0], 0 + offset[1]))
pen.lineTo((0, 500)) pen.lineTo((0 + offset[0], 500 + offset[1]))
pen.lineTo((500, 500)) pen.lineTo((500 + offset[0], 500 + offset[1]))
pen.lineTo((500, 0)) pen.lineTo((500 + offset[0], 0 + offset[1]))
pen.closePath() pen.closePath()
def draw_cubic(pen): def draw_cubic(pen):
@ -136,24 +137,78 @@ class FreeTypePenTest(unittest.TestCase):
self.assertEqual(size1, size2) self.assertEqual(size1, size2)
self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) 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) pen = FreeTypePen(None)
star(pen) star(pen)
width, height = None, 1000 width, height = None, None
buf1, size = pen.buffer(width=width, height=height, transform=Offset(0, 200)) 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(size, (1000, 1000))
self.assertEqual(buf1, buf2) 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) pen = FreeTypePen(None)
star(pen) star(pen)
width, height = 1000, None width, height = 0, 0
buf1, size = pen.buffer(width=width, height=height) buf1, size = pen.buffer(width=width, height=height, transform=Offset(0, 200), contain=True)
buf2, _ = pen.buffer(width=width, height=1000, transform=Offset(0, 200)) buf2, _ = pen.buffer(width=1000, height=1000, transform=Offset(0, 200), contain=True)
self.assertEqual(size, (1000, 1000)) self.assertEqual(size, (1000, 1000))
self.assertEqual(buf1, buf2) 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__': if __name__ == '__main__':
import sys import sys
sys.exit(unittest.main()) sys.exit(unittest.main())