diff options
Diffstat (limited to 'Lib/fontTools/pens/freetypePen.py')
-rw-r--r-- | Lib/fontTools/pens/freetypePen.py | 458 |
1 files changed, 458 insertions, 0 deletions
diff --git a/Lib/fontTools/pens/freetypePen.py b/Lib/fontTools/pens/freetypePen.py new file mode 100644 index 00000000..870776bc --- /dev/null +++ b/Lib/fontTools/pens/freetypePen.py @@ -0,0 +1,458 @@ +# -*- 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 + + @property + 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) + + @property + 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) |