diff --git a/Doc/source/pens/ftPen.rst b/Doc/source/pens/ftPen.rst new file mode 100644 index 000000000..c5c8f7b50 --- /dev/null +++ b/Doc/source/pens/ftPen.rst @@ -0,0 +1,8 @@ +########## +ftPen +########## + +.. automodule:: fontTools.pens.ftPen + :inherited-members: + :members: + :undoc-members: diff --git a/Doc/source/pens/index.rst b/Doc/source/pens/index.rst index 91175cf71..aafe40169 100644 --- a/Doc/source/pens/index.rst +++ b/Doc/source/pens/index.rst @@ -11,6 +11,7 @@ pens cocoaPen cu2quPen filterPen + ftPen momentsPen perimeterPen pointInsidePen diff --git a/Lib/fontTools/pens/ftPen.py b/Lib/fontTools/pens/ftPen.py index 0b48e53da..594d12a78 100644 --- a/Lib/fontTools/pens/ftPen.py +++ b/Lib/fontTools/pens/ftPen.py @@ -1,4 +1,6 @@ -"""A pen that rasterises outlines with FreeType.""" +# -*- coding: utf-8 -*- + +"""Pen to rasterize paths with FreeType.""" __all__ = ['FTPen'] @@ -19,23 +21,103 @@ from freetype.ft_errors import FT_Exception from fontTools.pens.basePen import BasePen from fontTools.misc.roundTools import otRound -class FTPen(BasePen): +Contour = collections.namedtuple('Contour', ('points', 'tags')) +LINE = 0b00000001 +CURVE = 0b00000011 +OFFCURVE = 0b00000010 +QCURVE = 0b00000001 +QOFFCURVE = 0b00000000 - Contour = collections.namedtuple('Contour', ('points', 'tags')) - LINE = 0b00000001 - CURVE = 0b00000011 - OFFCURVE = 0b00000010 - QCURVE = 0b00000001 - QOFFCURVE = 0b00000000 +class FTPen(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()`` and ``save()``, `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.ftPen import FTPen + pen = FTPen(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(offset=(0, -descender), width=width, height=height) + + Combining with `uharfbuzz`, you can typeset a chunk of glyphs in a pen:: + + import uharfbuzz as hb + from fontTools.pens.ftPen import FTPen + 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 = FTPen(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(offset=offset, width=width, height=height, 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): self.contours = [] def outline(self, offset=None, scale=None, even_odd=False): - # Convert the current contours to FT_Outline. + """Converts the current contours to ``FT_Outline``. + + Args: + offset: A optional tuple of ``(x, y)`` used for translation. + scale: A optional tuple of ``(scale_x, scale_y)`` used for scaling. + even_odd: Pass ``True`` for even-odd fill instead of non-zero. + """ offset = offset or (0, 0) scale = scale or (1.0, 1.0) - n_contours = len(self.contours) + nContours = len(self.contours) n_points = sum((len(contour.points) for contour in self.contours)) points = [] for contour in self.contours: @@ -52,16 +134,41 @@ class FTPen(BasePen): contours.append(contours_sum - 1) flags = FT_OUTLINE_EVEN_ODD_FILL if even_odd else FT_OUTLINE_NONE return FT_Outline( - (ctypes.c_short)(n_contours), + (ctypes.c_short)(nContours), (ctypes.c_short)(n_points), (FT_Vector * n_points)(*points), (ctypes.c_ubyte * n_points)(*tags), - (ctypes.c_short * n_contours)(*contours), + (ctypes.c_short * nContours)(*contours), (ctypes.c_int)(flags) ) def buffer(self, offset=None, width=1000, height=1000, even_odd=False, scale=None, contain=False): - # Return a tuple with the bitmap buffer and its dimension. + """Renders the current contours within a bitmap buffer. + + Args: + offset: A optional tuple of ``(x, y)`` used for translation. + Typically ``(0, -descender)`` can be passed so that the glyph + image would not been clipped. + width: Image width of the bitmap in pixels. + height: Image height of the bitmap in pixels. + scale: A optional tuple of ``(scale_x, scale_y)`` used for scaling. + even_odd: Pass ``True`` for even-odd fill instead of non-zero. + 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. + + 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. + + :Example: + >>> pen = FTPen(None) + >>> glyph.draw(pen) + >>> buf, size = pen.buffer(width=500, height=1000) + >>> type(buf), len(buf), size + (, 500000, (500, 1000)) + """ offset_x, offset_y = offset or (0, 0) if contain: bbox = self.bbox @@ -91,20 +198,88 @@ class FTPen(BasePen): return buf.raw, (width, height) def array(self, offset=None, width=1000, height=1000, even_odd=False, scale=None, contain=False): - # Return a numpy array. Each element takes values in the range of [0.0, 1.0]. + """Returns the rendered contours as a numpy array. Requires `numpy`. + + Args: + offset: A optional tuple of ``(x, y)`` used for translation. + Typically ``(0, -descender)`` can be passed so that the glyph + image would not been clipped. + width: Image width of the bitmap in pixels. + height: Image height of the bitmap in pixels. + scale: A optional tuple of ``(scale_x, scale_y)`` used for scaling. + even_odd: Pass ``True`` for even-odd fill instead of non-zero. + 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. + + Returns: + A ``numpy.ndarray`` object with a shape of ``(height, width)``. + Each element takes a value in the range of ``[0.0, 1.0]``. + + :Example: + >>> pen = FTPen(None) + >>> glyph.draw(pen) + >>> arr = pen.array(width=500, height=1000) + >>> type(a), a.shape + (, (1000, 500)) + """ import numpy as np buf, size = self.buffer(offset=offset, width=width, height=height, even_odd=even_odd, scale=scale, contain=contain) return np.frombuffer(buf, 'B').reshape((size[1], size[0])) / 255.0 def show(self, offset=None, width=1000, height=1000, even_odd=False, scale=None, contain=False): - # Plot the image with matplotlib. + """Plots the rendered contours with `pyplot`. Requires `numpy` and + `matplotlib`. + + Args: + offset: A optional tuple of ``(x, y)`` used for translation. + Typically ``(0, -descender)`` can be passed so that the glyph + image would not been clipped. + width: Image width of the bitmap in pixels. + height: Image height of the bitmap in pixels. + scale: A optional tuple of ``(scale_x, scale_y)`` used for scaling. + even_odd: Pass ``True`` for even-odd fill instead of non-zero. + 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. + + :Example: + >>> pen = FTPen(None) + >>> glyph.draw(pen) + >>> pen.show(width=500, height=1000) + """ from matplotlib import pyplot as plt a = self.array(offset=offset, width=width, height=height, even_odd=even_odd, scale=scale, contain=contain) plt.imshow(a, cmap='gray_r', vmin=0, vmax=1) plt.show() def image(self, offset=None, width=1000, height=1000, even_odd=False, scale=None, contain=False): - # Return a PIL image. + """Returns the rendered contours as a PIL image. Requires `Pillow`. + Can be used to display a glyph image in Jupyter Notebook. + + Args: + offset: A optional tuple of ``(x, y)`` used for translation. + Typically ``(0, -descender)`` can be passed so that the glyph + image would not been clipped. + width: Image width of the bitmap in pixels. + height: Image height of the bitmap in pixels. + scale: A optional tuple of ``(scale_x, scale_y)`` used for scaling. + even_odd: Pass ``True`` for even-odd fill instead of non-zero. + 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. + + Returns: + A ``PIL.image`` object. The image is filled in black with alpha + channel obtained from the rendered bitmap. + + :Example: + >>> pen = FTPen(None) + >>> glyph.draw(pen) + >>> img = pen.image(width=500, height=1000) + >>> type(img), img.size + (, (500, 1000)) + """ from PIL import Image buf, size = self.buffer(offset=offset, width=width, height=height, even_odd=even_odd, scale=scale, contain=contain) img = Image.new('L', size, 0) @@ -112,13 +287,38 @@ class FTPen(BasePen): return img def save(self, fp, offset=None, width=1000, height=1000, even_odd=False, scale=None, contain=False, format=None, **kwargs): - # Save the image as a file. + """Saves the image as a file. Requires `Pillow`. + + Args: + fp: A filename (string), pathlib. Path object or file object. + offset: A optional tuple of ``(x, y)`` used for translation. + Typically ``(0, -descender)`` can be passed so that the glyph + image would not been clipped. + width: Image width of the bitmap in pixels. + height: Image height of the bitmap in pixels. + scale: A optional tuple of ``(scale_x, scale_y)`` used for scaling. + even_odd: Pass ``True`` for even-odd fill instead of non-zero. + 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. + format: Optional format override. If omitted, the format to use is + determined from the filename extension. + + :Example: + >>> pen = FTPen(None) + >>> glyph.draw(pen) + >>> pen.save('glyph.png' width=500, height=1000) + """ img = self.image(offset=offset, width=width, height=height, even_odd=even_odd, scale=scale, contain=contain) img.save(fp, format=format, **kwargs) @property def bbox(self): - # Compute the exact bounding box of an outline. + """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)) @@ -126,32 +326,36 @@ class FTPen(BasePen): @property def cbox(self): - # Return an outline's ‘control box’. + """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 = self.Contour([], []) + contour = Contour([], []) self.contours.append(contour) contour.points.append(pt) - contour.tags.append(self.LINE) + contour.tags.append(LINE) def _lineTo(self, pt): contour = self.contours[-1] contour.points.append(pt) - contour.tags.append(self.LINE) + contour.tags.append(LINE) def _curveToOne(self, p1, p2, p3): - t1, t2, t3 = self.OFFCURVE, self.OFFCURVE, self.CURVE + t1, t2, t3 = OFFCURVE, OFFCURVE, CURVE 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): - t1, t2 = self.QOFFCURVE, self.QCURVE + t1, t2 = QOFFCURVE, QCURVE contour = self.contours[-1] for p, t in ((p1, t1), (p2, t2)): contour.points.append(p)