Merge pull request #2517 from takaakifuji/ftpen-transform-fix
freetypePen: fit to content when rotated/skewed
This commit is contained in:
commit
d1eca1006a
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
"""Pen to rasterize paths with FreeType."""
|
"""Pen to rasterize paths with FreeType."""
|
||||||
|
|
||||||
__all__ = ['FreeTypePen']
|
__all__ = ["FreeTypePen"]
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import ctypes
|
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.raw import FT_Outline_Get_Bitmap, FT_Outline_Get_BBox, FT_Outline_Get_CBox
|
||||||
from freetype.ft_types import FT_Pos
|
from freetype.ft_types import FT_Pos
|
||||||
from freetype.ft_structs import FT_Vector, FT_BBox, FT_Bitmap, FT_Outline
|
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 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.roundTools import otRound
|
||||||
from fontTools.misc.transform import Transform
|
from fontTools.misc.transform import Transform
|
||||||
|
|
||||||
Contour = collections.namedtuple('Contour', ('points', 'tags'))
|
Contour = collections.namedtuple("Contour", ("points", "tags"))
|
||||||
LINE = 0b00000001
|
|
||||||
CURVE = 0b00000011
|
|
||||||
OFFCURVE = 0b00000010
|
|
||||||
QCURVE = 0b00000001
|
|
||||||
QOFFCURVE = 0b00000000
|
|
||||||
|
|
||||||
class FreeTypePen(BasePen):
|
class FreeTypePen(BasePen):
|
||||||
"""Pen to rasterize paths with FreeType. Requires `freetype-py` module.
|
"""Pen to rasterize paths with FreeType. Requires `freetype-py` module.
|
||||||
@ -114,21 +117,25 @@ 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.
|
||||||
"""
|
"""
|
||||||
transform = transform or Transform()
|
transform = transform or Transform()
|
||||||
if not hasattr(transform, 'transformPoint'):
|
if not hasattr(transform, "transformPoint"):
|
||||||
transform = Transform(*transform)
|
transform = Transform(*transform)
|
||||||
nContours = len(self.contours)
|
n_contours = len(self.contours)
|
||||||
n_points = sum((len(contour.points) for contour in self.contours))
|
n_points = sum((len(contour.points) for contour in self.contours))
|
||||||
points = []
|
points = []
|
||||||
for contour in self.contours:
|
for contour in self.contours:
|
||||||
for point in contour.points:
|
for point in contour.points:
|
||||||
point = transform.transformPoint(point)
|
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 = []
|
tags = []
|
||||||
for contour in self.contours:
|
for contour in self.contours:
|
||||||
for tag in contour.tags:
|
for tag in contour.tags:
|
||||||
@ -140,15 +147,17 @@ class FreeTypePen(BasePen):
|
|||||||
contours.append(contours_sum - 1)
|
contours.append(contours_sum - 1)
|
||||||
flags = FT_OUTLINE_EVEN_ODD_FILL if evenOdd else FT_OUTLINE_NONE
|
flags = FT_OUTLINE_EVEN_ODD_FILL if evenOdd else FT_OUTLINE_NONE
|
||||||
return FT_Outline(
|
return FT_Outline(
|
||||||
(ctypes.c_short)(nContours),
|
(ctypes.c_short)(n_contours),
|
||||||
(ctypes.c_short)(n_points),
|
(ctypes.c_short)(n_points),
|
||||||
(FT_Vector * n_points)(*points),
|
(FT_Vector * n_points)(*points),
|
||||||
(ctypes.c_ubyte * n_points)(*tags),
|
(ctypes.c_ubyte * n_points)(*tags),
|
||||||
(ctypes.c_short * nContours)(*contours),
|
(ctypes.c_short * n_contours)(*contours),
|
||||||
(ctypes.c_int)(flags)
|
(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.
|
"""Renders the current contours within a bitmap buffer.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -156,7 +165,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,6 +178,16 @@ 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 won’t. The difference between the two becomes
|
||||||
|
more obvious when rotate or skew transformation is applied.
|
||||||
|
|
||||||
:Example:
|
:Example:
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
@ -180,22 +199,33 @@ class FreeTypePen(BasePen):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
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)
|
||||||
@ -207,15 +237,19 @@ class FreeTypePen(BasePen):
|
|||||||
(ctypes.c_short)(256),
|
(ctypes.c_short)(256),
|
||||||
(ctypes.c_ubyte)(FT_PIXEL_MODE_GRAY),
|
(ctypes.c_ubyte)(FT_PIXEL_MODE_GRAY),
|
||||||
(ctypes.c_char)(0),
|
(ctypes.c_char)(0),
|
||||||
(ctypes.c_void_p)(None)
|
(ctypes.c_void_p)(None),
|
||||||
)
|
)
|
||||||
outline = self.outline(transform=transform, evenOdd=evenOdd)
|
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:
|
if err != 0:
|
||||||
raise FT_Exception(err)
|
raise FT_Exception(err)
|
||||||
return buf.raw, (width, height)
|
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`.
|
"""Returns the rendered contours as a numpy array. Requires `numpy`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -223,7 +257,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,6 +269,16 @@ 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 won’t. The difference between the two becomes
|
||||||
|
more obvious when rotate or skew transformation is applied.
|
||||||
|
|
||||||
:Example:
|
:Example:
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
@ -245,10 +289,19 @@ class FreeTypePen(BasePen):
|
|||||||
(<class 'numpy.ndarray'>, (1000, 500))
|
(<class 'numpy.ndarray'>, (1000, 500))
|
||||||
"""
|
"""
|
||||||
import numpy as np
|
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
|
"""Plots the rendered contours with `pyplot`. Requires `numpy` and
|
||||||
`matplotlib`.
|
`matplotlib`.
|
||||||
|
|
||||||
@ -257,7 +310,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,6 +318,16 @@ 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 won’t. The difference between the two becomes
|
||||||
|
more obvious when rotate or skew transformation is applied.
|
||||||
|
|
||||||
:Example:
|
:Example:
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
@ -273,11 +336,20 @@ class FreeTypePen(BasePen):
|
|||||||
>> pen.show(width=500, height=1000)
|
>> pen.show(width=500, height=1000)
|
||||||
"""
|
"""
|
||||||
from matplotlib import pyplot as plt
|
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()
|
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`.
|
"""Returns the rendered contours as a PIL image. Requires `Pillow`.
|
||||||
Can be used to display a glyph image in Jupyter Notebook.
|
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.
|
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,6 +370,16 @@ 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 won’t. The difference between the two becomes
|
||||||
|
more obvious when rotate or skew transformation is applied.
|
||||||
|
|
||||||
:Example:
|
:Example:
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
@ -308,9 +390,16 @@ class FreeTypePen(BasePen):
|
|||||||
(<class 'PIL.Image.Image'>, (500, 1000))
|
(<class 'PIL.Image.Image'>, (500, 1000))
|
||||||
"""
|
"""
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
buf, size = self.buffer(width=width, height=height, transform=transform, contain=contain, evenOdd=evenOdd)
|
|
||||||
img = Image.new('L', size, 0)
|
buf, size = self.buffer(
|
||||||
img.putalpha(Image.frombuffer('L', size, buf))
|
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
|
return img
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -341,22 +430,28 @@ class FreeTypePen(BasePen):
|
|||||||
contour = Contour([], [])
|
contour = Contour([], [])
|
||||||
self.contours.append(contour)
|
self.contours.append(contour)
|
||||||
contour.points.append(pt)
|
contour.points.append(pt)
|
||||||
contour.tags.append(LINE)
|
contour.tags.append(FT_CURVE_TAG_ON)
|
||||||
|
|
||||||
def _lineTo(self, pt):
|
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 = self.contours[-1]
|
||||||
contour.points.append(pt)
|
contour.points.append(pt)
|
||||||
contour.tags.append(LINE)
|
contour.tags.append(FT_CURVE_TAG_ON)
|
||||||
|
|
||||||
def _curveToOne(self, p1, p2, p3):
|
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]
|
contour = self.contours[-1]
|
||||||
for p, t in ((p1, t1), (p2, t2), (p3, t3)):
|
for p, t in ((p1, t1), (p2, t2), (p3, t3)):
|
||||||
contour.points.append(p)
|
contour.points.append(p)
|
||||||
contour.tags.append(t)
|
contour.tags.append(t)
|
||||||
|
|
||||||
def _qCurveToOne(self, p1, p2):
|
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]
|
contour = self.contours[-1]
|
||||||
for p, t in ((p1, t1), (p2, t2)):
|
for p, t in ((p1, t1), (p2, t2)):
|
||||||
contour.points.append(p)
|
contour.points.append(p)
|
||||||
|
BIN
Tests/pens/data/test_rotate.pgm
Normal file
BIN
Tests/pens/data/test_rotate.pgm
Normal file
Binary file not shown.
BIN
Tests/pens/data/test_skew.pgm
Normal file
BIN
Tests/pens/data/test_skew.pgm
Normal file
Binary file not shown.
@ -1,23 +1,27 @@
|
|||||||
import unittest
|
import unittest
|
||||||
import os
|
import os
|
||||||
|
import math
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from fontTools.pens.freetypePen import FreeTypePen
|
from fontTools.pens.freetypePen import FreeTypePen
|
||||||
|
|
||||||
FREETYPE_PY_AVAILABLE = True
|
FREETYPE_PY_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
FREETYPE_PY_AVAILABLE = False
|
FREETYPE_PY_AVAILABLE = False
|
||||||
|
|
||||||
from fontTools.misc.transform import Scale, Offset
|
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))
|
def box(pen, offset=(0, 0)):
|
||||||
pen.lineTo((0, 500))
|
pen.moveTo((0 + offset[0], 0 + offset[1]))
|
||||||
pen.lineTo((500, 500))
|
pen.lineTo((0 + offset[0], 500 + offset[1]))
|
||||||
pen.lineTo((500, 0))
|
pen.lineTo((500 + offset[0], 500 + offset[1]))
|
||||||
|
pen.lineTo((500 + offset[0], 0 + offset[1]))
|
||||||
pen.closePath()
|
pen.closePath()
|
||||||
|
|
||||||
|
|
||||||
def draw_cubic(pen):
|
def draw_cubic(pen):
|
||||||
pen.moveTo((50, 0))
|
pen.moveTo((50, 0))
|
||||||
pen.lineTo((50, 500))
|
pen.lineTo((50, 500))
|
||||||
@ -26,6 +30,7 @@ def draw_cubic(pen):
|
|||||||
pen.curveTo((450, 100), (350, 0), (200, 0))
|
pen.curveTo((450, 100), (350, 0), (200, 0))
|
||||||
pen.closePath()
|
pen.closePath()
|
||||||
|
|
||||||
|
|
||||||
def draw_quadratic(pen):
|
def draw_quadratic(pen):
|
||||||
pen.moveTo((50, 0))
|
pen.moveTo((50, 0))
|
||||||
pen.lineTo((50, 500))
|
pen.lineTo((50, 500))
|
||||||
@ -34,6 +39,7 @@ def draw_quadratic(pen):
|
|||||||
pen.qCurveTo((450, 176), (388, 62), (274, 0), (200, 0))
|
pen.qCurveTo((450, 176), (388, 62), (274, 0), (200, 0))
|
||||||
pen.closePath()
|
pen.closePath()
|
||||||
|
|
||||||
|
|
||||||
def star(pen):
|
def star(pen):
|
||||||
pen.moveTo((0, 420))
|
pen.moveTo((0, 420))
|
||||||
pen.lineTo((1000, 420))
|
pen.lineTo((1000, 420))
|
||||||
@ -42,33 +48,39 @@ def star(pen):
|
|||||||
pen.lineTo((800, -200))
|
pen.lineTo((800, -200))
|
||||||
pen.closePath()
|
pen.closePath()
|
||||||
|
|
||||||
|
|
||||||
# For the PGM format, see the following resources:
|
# For the PGM format, see the following resources:
|
||||||
# https://en.wikipedia.org/wiki/Netpbm
|
# https://en.wikipedia.org/wiki/Netpbm
|
||||||
# http://netpbm.sourceforge.net/doc/pgm.html
|
# http://netpbm.sourceforge.net/doc/pgm.html
|
||||||
def load_pgm(filename):
|
def load_pgm(filename):
|
||||||
with open(filename, 'rb') as fp:
|
with open(filename, "rb") as fp:
|
||||||
assert fp.readline() == 'P5\n'.encode()
|
assert fp.readline() == "P5\n".encode()
|
||||||
w, h = (int(c) for c in fp.readline().decode().rstrip().split(' '))
|
w, h = (int(c) for c in fp.readline().decode().rstrip().split(" "))
|
||||||
assert fp.readline() == '255\n'.encode()
|
assert fp.readline() == "255\n".encode()
|
||||||
return fp.read(), (w, h)
|
return fp.read(), (w, h)
|
||||||
|
|
||||||
|
|
||||||
def save_pgm(filename, buf, size):
|
def save_pgm(filename, buf, size):
|
||||||
with open(filename, 'wb') as fp:
|
with open(filename, "wb") as fp:
|
||||||
fp.write('P5\n'.encode())
|
fp.write("P5\n".encode())
|
||||||
fp.write('{0:d} {1:d}\n'.format(*size).encode())
|
fp.write("{0:d} {1:d}\n".format(*size).encode())
|
||||||
fp.write('255\n'.encode())
|
fp.write("255\n".encode())
|
||||||
fp.write(buf)
|
fp.write(buf)
|
||||||
|
|
||||||
|
|
||||||
# Assume the buffers are equal when PSNR > 38dB. See also:
|
# Assume the buffers are equal when PSNR > 38dB. See also:
|
||||||
# Peak signal-to-noise ratio
|
# Peak signal-to-noise ratio
|
||||||
# https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio
|
# https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio
|
||||||
PSNR_THRESHOLD = 38.0
|
PSNR_THRESHOLD = 38.0
|
||||||
|
|
||||||
|
|
||||||
def psnr(b1, b2):
|
def psnr(b1, b2):
|
||||||
import math
|
import math
|
||||||
|
|
||||||
mse = sum((c1 - c2) * (c1 - c2) for c1, c2 in zip(b1, b2)) / float(len(b1))
|
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
|
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")
|
@unittest.skipUnless(FREETYPE_PY_AVAILABLE, "freetype-py not installed")
|
||||||
class FreeTypePenTest(unittest.TestCase):
|
class FreeTypePenTest(unittest.TestCase):
|
||||||
def test_draw(self):
|
def test_draw(self):
|
||||||
@ -76,14 +88,14 @@ class FreeTypePenTest(unittest.TestCase):
|
|||||||
box(pen)
|
box(pen)
|
||||||
width, height = 500, 500
|
width, height = 500, 500
|
||||||
buf1, _ = pen.buffer(width=width, height=height)
|
buf1, _ = pen.buffer(width=width, height=height)
|
||||||
buf2 = b'\xff' * width * height
|
buf2 = b"\xff" * width * height
|
||||||
self.assertEqual(buf1, buf2)
|
self.assertEqual(buf1, buf2)
|
||||||
|
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
pen = FreeTypePen(None)
|
pen = FreeTypePen(None)
|
||||||
width, height = 500, 500
|
width, height = 500, 500
|
||||||
buf, size = pen.buffer(width=width, height=height)
|
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):
|
def test_bbox_and_cbox(self):
|
||||||
pen = FreeTypePen(None)
|
pen = FreeTypePen(None)
|
||||||
@ -98,7 +110,7 @@ class FreeTypePenTest(unittest.TestCase):
|
|||||||
width, height = t.transformPoint((1000, 1000))
|
width, height = t.transformPoint((1000, 1000))
|
||||||
t = t.translate(0, 200)
|
t = t.translate(0, 200)
|
||||||
buf1, size1 = pen.buffer(width=width, height=height, transform=t, evenOdd=False)
|
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(len(buf1), len(buf2))
|
||||||
self.assertEqual(size1, size2)
|
self.assertEqual(size1, size2)
|
||||||
self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD)
|
self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD)
|
||||||
@ -110,7 +122,7 @@ class FreeTypePenTest(unittest.TestCase):
|
|||||||
width, height = t.transformPoint((1000, 1000))
|
width, height = t.transformPoint((1000, 1000))
|
||||||
t = t.translate(0, 200)
|
t = t.translate(0, 200)
|
||||||
buf1, size1 = pen.buffer(width=width, height=height, transform=t, evenOdd=True)
|
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(len(buf1), len(buf2))
|
||||||
self.assertEqual(size1, size2)
|
self.assertEqual(size1, size2)
|
||||||
self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD)
|
self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD)
|
||||||
@ -131,29 +143,93 @@ class FreeTypePenTest(unittest.TestCase):
|
|||||||
t = Scale(0.05, 0.05)
|
t = Scale(0.05, 0.05)
|
||||||
width, height = 0, 0
|
width, height = 0, 0
|
||||||
buf1, size1 = pen.buffer(width=width, height=height, transform=t, contain=True)
|
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(len(buf1), len(buf2))
|
||||||
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(
|
||||||
buf2, _ = pen.buffer(width=width, height=1000, transform=Offset(0, 200))
|
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(size, (1000, 1000))
|
||||||
self.assertEqual(buf1, buf2)
|
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
|
import sys
|
||||||
|
|
||||||
sys.exit(unittest.main())
|
sys.exit(unittest.main())
|
||||||
|
Loading…
x
Reference in New Issue
Block a user