aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/pens/freetypePen.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/pens/freetypePen.py')
-rw-r--r--Lib/fontTools/pens/freetypePen.py458
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)