Spaces:
Runtime error
Runtime error
# -*- coding: utf-8 -*- | |
"""Pen to rasterize paths with FreeType.""" | |
__all__ = ["FreeTypePen"] | |
import os | |
import ctypes | |
import platform | |
import subprocess | |
import collections | |
import math | |
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, | |
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, PenError | |
from fontTools.misc.roundTools import otRound | |
from fontTools.misc.transform import Transform | |
Contour = collections.namedtuple("Contour", ("points", "tags")) | |
class FreeTypePen(BasePen): | |
"""Pen to rasterize paths with FreeType. Requires `freetype-py` module. | |
Constructs ``FT_Outline`` from the paths, and renders it within a bitmap | |
buffer. | |
For ``array()`` and ``show()``, `numpy` and `matplotlib` must be installed. | |
For ``image()``, `Pillow` is required. Each module is lazily loaded when the | |
corresponding method is called. | |
Args: | |
glyphSet: a dictionary of drawable glyph objects keyed by name | |
used to resolve component references in composite glyphs. | |
:Examples: | |
If `numpy` and `matplotlib` is available, the following code will | |
show the glyph image of `fi` in a new window:: | |
from fontTools.ttLib import TTFont | |
from fontTools.pens.freetypePen import FreeTypePen | |
from fontTools.misc.transform import Offset | |
pen = FreeTypePen(None) | |
font = TTFont('SourceSansPro-Regular.otf') | |
glyph = font.getGlyphSet()['fi'] | |
glyph.draw(pen) | |
width, ascender, descender = glyph.width, font['OS/2'].usWinAscent, -font['OS/2'].usWinDescent | |
height = ascender - descender | |
pen.show(width=width, height=height, transform=Offset(0, -descender)) | |
Combining with `uharfbuzz`, you can typeset a chunk of glyphs in a pen:: | |
import uharfbuzz as hb | |
from fontTools.pens.freetypePen import FreeTypePen | |
from fontTools.pens.transformPen import TransformPen | |
from fontTools.misc.transform import Offset | |
en1, en2, ar, ja = 'Typesetting', 'Jeff', 'صف الحروف', 'たいぷせっと' | |
for text, font_path, direction, typo_ascender, typo_descender, vhea_ascender, vhea_descender, contain, features in ( | |
(en1, 'NotoSans-Regular.ttf', 'ltr', 2189, -600, None, None, False, {"kern": True, "liga": True}), | |
(en2, 'NotoSans-Regular.ttf', 'ltr', 2189, -600, None, None, True, {"kern": True, "liga": True}), | |
(ar, 'NotoSansArabic-Regular.ttf', 'rtl', 1374, -738, None, None, False, {"kern": True, "liga": True}), | |
(ja, 'NotoSansJP-Regular.otf', 'ltr', 880, -120, 500, -500, False, {"palt": True, "kern": True}), | |
(ja, 'NotoSansJP-Regular.otf', 'ttb', 880, -120, 500, -500, False, {"vert": True, "vpal": True, "vkrn": True}) | |
): | |
blob = hb.Blob.from_file_path(font_path) | |
face = hb.Face(blob) | |
font = hb.Font(face) | |
buf = hb.Buffer() | |
buf.direction = direction | |
buf.add_str(text) | |
buf.guess_segment_properties() | |
hb.shape(font, buf, features) | |
x, y = 0, 0 | |
pen = FreeTypePen(None) | |
for info, pos in zip(buf.glyph_infos, buf.glyph_positions): | |
gid = info.codepoint | |
transformed = TransformPen(pen, Offset(x + pos.x_offset, y + pos.y_offset)) | |
font.draw_glyph_with_pen(gid, transformed) | |
x += pos.x_advance | |
y += pos.y_advance | |
offset, width, height = None, None, None | |
if direction in ('ltr', 'rtl'): | |
offset = (0, -typo_descender) | |
width = x | |
height = typo_ascender - typo_descender | |
else: | |
offset = (-vhea_descender, -y) | |
width = vhea_ascender - vhea_descender | |
height = -y | |
pen.show(width=width, height=height, transform=Offset(*offset), contain=contain) | |
For Jupyter Notebook, the rendered image will be displayed in a cell if | |
you replace ``show()`` with ``image()`` in the examples. | |
""" | |
def __init__(self, glyphSet): | |
BasePen.__init__(self, glyphSet) | |
self.contours = [] | |
def outline(self, transform=None, evenOdd=False): | |
"""Converts the current contours to ``FT_Outline``. | |
Args: | |
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"): | |
transform = Transform(*transform) | |
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)) | |
) | |
) | |
tags = [] | |
for contour in self.contours: | |
for tag in contour.tags: | |
tags.append(tag) | |
contours = [] | |
contours_sum = 0 | |
for contour in self.contours: | |
contours_sum += len(contour.points) | |
contours.append(contours_sum - 1) | |
flags = FT_OUTLINE_EVEN_ODD_FILL if evenOdd else FT_OUTLINE_NONE | |
return FT_Outline( | |
(ctypes.c_short)(n_contours), | |
(ctypes.c_short)(n_points), | |
(FT_Vector * n_points)(*points), | |
(ctypes.c_ubyte * n_points)(*tags), | |
(ctypes.c_short * n_contours)(*contours), | |
(ctypes.c_int)(flags), | |
) | |
def buffer( | |
self, width=None, height=None, transform=None, contain=False, evenOdd=False | |
): | |
"""Renders the current contours within a bitmap buffer. | |
Args: | |
width: Image width of the bitmap in pixels. If omitted, it | |
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: 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 | |
so that it fits to the bounding box of the paths. Useful for | |
rendering glyphs with negative sidebearings without clipping. | |
evenOdd: Pass ``True`` for even-odd fill instead of non-zero. | |
Returns: | |
A tuple of ``(buffer, size)``, where ``buffer`` is a ``bytes`` | |
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 | |
(<class 'bytes'>, 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 | |
if contain_x or contain_y: | |
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 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: | |
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) | |
bitmap = FT_Bitmap( | |
(ctypes.c_int)(height), | |
(ctypes.c_int)(width), | |
(ctypes.c_int)(width), | |
(ctypes.POINTER(ctypes.c_ubyte))(buf), | |
(ctypes.c_short)(256), | |
(ctypes.c_ubyte)(FT_PIXEL_MODE_GRAY), | |
(ctypes.c_char)(0), | |
(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) | |
) | |
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 | |
): | |
"""Returns the rendered contours as a numpy array. Requires `numpy`. | |
Args: | |
width: Image width of the bitmap in pixels. If omitted, it | |
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: 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 | |
so that it fits to the bounding box of the paths. Useful for | |
rendering glyphs with negative sidebearings without clipping. | |
evenOdd: Pass ``True`` for even-odd fill instead of non-zero. | |
Returns: | |
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) | |
>> type(a), a.shape | |
(<class 'numpy.ndarray'>, (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 | |
): | |
"""Plots the rendered contours with `pyplot`. Requires `numpy` and | |
`matplotlib`. | |
Args: | |
width: Image width of the bitmap in pixels. If omitted, it | |
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: 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 | |
so that it fits to the bounding box of the paths. Useful for | |
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) | |
plt.show() | |
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. | |
Args: | |
width: Image width of the bitmap in pixels. If omitted, it | |
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: 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 | |
so that it fits to the bounding box of the paths. Useful for | |
rendering glyphs with negative sidebearings without clipping. | |
evenOdd: Pass ``True`` for even-odd fill instead of non-zero. | |
Returns: | |
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) | |
>> type(img), img.size | |
(<class 'PIL.Image.Image'>, (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)) | |
return img | |
def bbox(self): | |
"""Computes the exact bounding box of an outline. | |
Returns: | |
A tuple of ``(xMin, yMin, xMax, yMax)``. | |
""" | |
bbox = FT_BBox() | |
outline = self.outline() | |
FT_Outline_Get_BBox(ctypes.byref(outline), ctypes.byref(bbox)) | |
return (bbox.xMin / 64.0, bbox.yMin / 64.0, bbox.xMax / 64.0, bbox.yMax / 64.0) | |
def cbox(self): | |
"""Returns an outline's ‘control box’. | |
Returns: | |
A tuple of ``(xMin, yMin, xMax, yMax)``. | |
""" | |
cbox = FT_BBox() | |
outline = self.outline() | |
FT_Outline_Get_CBox(ctypes.byref(outline), ctypes.byref(cbox)) | |
return (cbox.xMin / 64.0, cbox.yMin / 64.0, cbox.xMax / 64.0, cbox.yMax / 64.0) | |
def _moveTo(self, pt): | |
contour = Contour([], []) | |
self.contours.append(contour) | |
contour.points.append(pt) | |
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(FT_CURVE_TAG_ON) | |
def _curveToOne(self, p1, p2, p3): | |
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): | |
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) | |
contour.tags.append(t) | |