From e2fad4db1184b1ec338a6c5fdbae3fb23bf61b26 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Thu, 6 Jan 2022 09:00:53 +0900 Subject: [PATCH 01/25] Add FreeType-based Pen for rasterisation --- Lib/fontTools/pens/ftPen.py | 232 ++++++++++++++++++++++++++++++++++++ Tests/pens/ftPen_test.py | 105 ++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100755 Lib/fontTools/pens/ftPen.py create mode 100644 Tests/pens/ftPen_test.py diff --git a/Lib/fontTools/pens/ftPen.py b/Lib/fontTools/pens/ftPen.py new file mode 100755 index 0000000000..405bca5d09 --- /dev/null +++ b/Lib/fontTools/pens/ftPen.py @@ -0,0 +1,232 @@ +"""A pen that rasterises outlines with FreeType.""" + +__all__ = ['FTPen'] + +import os +import ctypes +import ctypes.util +import platform +import subprocess +import collections +import math + +from fontTools.pens.basePen import BasePen +from fontTools.misc.roundTools import otRound + +class FT_LibraryRec(ctypes.Structure): + _fields_ = [] + +FT_Library = ctypes.POINTER(FT_LibraryRec) +FT_Pos = ctypes.c_long + +class FT_Vector(ctypes.Structure): + _fields_ = [('x', FT_Pos), ('y', FT_Pos)] + +class FT_BBox(ctypes.Structure): + _fields_ = [('xMin', FT_Pos), ('yMin', FT_Pos), ('xMax', FT_Pos), ('yMax', FT_Pos)] + +class FT_Bitmap(ctypes.Structure): + _fields_ = [('rows', ctypes.c_int), ('width', ctypes.c_int), ('pitch', ctypes.c_int), ('buffer', ctypes.POINTER(ctypes.c_ubyte)), ('num_grays', ctypes.c_short), ('pixel_mode', ctypes.c_ubyte), ('palette_mode', ctypes.c_char), ('palette', ctypes.c_void_p)] + +class FT_Outline(ctypes.Structure): + _fields_ = [('n_contours', ctypes.c_short), ('n_points', ctypes.c_short), ('points', ctypes.POINTER(FT_Vector)), ('tags', ctypes.POINTER(ctypes.c_ubyte)), ('contours', ctypes.POINTER(ctypes.c_short)), ('flags', ctypes.c_int)] + +class FreeType(object): + + @staticmethod + def load_freetype_lib(): + lib_path = ctypes.util.find_library('freetype') + if lib_path: + return ctypes.cdll.LoadLibrary(lib_path) + if platform.system() == 'Darwin': + # Try again by searching inside the installation paths of Homebrew and MacPorts + # This workaround is needed if Homebrew has been installed to a non-standard location. + orig_dyld_path = os.environ.get('DYLD_LIBRARY_PATH') + for dyld_path_func in ( + lambda: os.path.join(subprocess.check_output(('brew', '--prefix'), universal_newlines=True).rstrip(), 'lib'), + lambda: os.path.join(os.path.dirname(os.path.dirname(subprocess.check_output(('which', 'port'), universal_newlines=True).rstrip())), 'lib') + ): + try: + dyld_path = dyld_path_func() + os.environ['DYLD_LIBRARY_PATH'] = ':'.join(os.environ.get('DYLD_LIBRARY_PATH', '').split(':') + [dyld_path]) + lib_path = ctypes.util.find_library('freetype') + if lib_path: + return ctypes.cdll.LoadLibrary(lib_path) + except CalledProcessError: + pass + finally: + if orig_dyld_path: + os.environ['DYLD_LIBRARY_PATH'] = orig_dyld_path + else: + os.environ.pop('DYLD_LIBRARY_PATH', None) + return None + + def __init__(self): + lib = self.load_freetype_lib() + self.handle = FT_Library() + self.FT_Init_FreeType = lib.FT_Init_FreeType + self.FT_Done_FreeType = lib.FT_Done_FreeType + self.FT_Library_Version = lib.FT_Library_Version + self.FT_Outline_Get_CBox = lib.FT_Outline_Get_CBox + self.FT_Outline_Get_BBox = lib.FT_Outline_Get_BBox + self.FT_Outline_Get_Bitmap = lib.FT_Outline_Get_Bitmap + self.raise_error_if_needed(self.FT_Init_FreeType(ctypes.byref(self.handle))) + + def raise_error_if_needed(self, err): + # See the reference for error codes: + # https://freetype.org/freetype2/docs/reference/ft2-error_code_values.html + if err != 0: + raise RuntimeError("FT_Error: 0x{0:02X}".format(err)) + + def __del__(self): + if self.handle: + self.FT_Done_FreeType(self.handle) + + @property + def version(self): + major, minor, patch = ctypes.c_int(), ctypes.c_int(), ctypes.c_int() + self.FT_Library_Version(self.handle, ctypes.byref(major), ctypes.byref(minor), ctypes.byref(patch)) + return "{0}.{1}.{2}".format(major.value, minor.value, patch.value) + +class FTPen(BasePen): + + ft = None + np = None + plt = None + Image = None + Contour = collections.namedtuple('Contour', ('points', 'tags')) + LINE = 0b00000001 + CURVE = 0b00000011 + OFFCURVE = 0b00000010 + QCURVE = 0b00000001 + QOFFCURVE = 0b00000000 + + def __init__(self, glyphSet): + if not self.__class__.ft: + self.__class__.ft = FreeType() + self.contours = [] + + def outline(self, offset=None, scale=None, even_odd=False): + # Convert the current contours to FT_Outline. + FT_OUTLINE_NONE = 0x0 + FT_OUTLINE_EVEN_ODD_FILL = 0x2 + offset = offset or (0, 0) + scale = scale or (1.0, 1.0) + 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: + points.append(FT_Vector(FT_Pos(otRound((point[0] + offset[0]) * scale[0] * 64)), FT_Pos(otRound((point[1] + offset[1]) * scale[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 even_odd 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=1000, ascender=880, descender=-120, even_odd=False, scale=None): + # Return a tuple with the bitmap buffer and its dimension. + FT_PIXEL_MODE_GRAY = 2 + scale = scale or (1.0, 1.0) + width = math.ceil(width * scale[0]) + height = math.ceil((ascender - descender) * scale[1]) + 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(offset=(0, -descender), even_odd=even_odd, scale=scale) + self.ft.raise_error_if_needed(self.ft.FT_Outline_Get_Bitmap(self.ft.handle, ctypes.byref(outline), ctypes.byref(bitmap))) + return buf.raw, (width, height) + + def array(self, width=1000, ascender=880, descender=-120, even_odd=False, scale=None): + # Return a numpy array. Each element takes values in the range of [0.0, 1.0]. + if not self.np: + import numpy as np + self.np = np + buf, size = self.buffer(width, ascender=ascender, descender=descender, even_odd=even_odd, scale=scale) + return self.np.frombuffer(buf, 'B').reshape((size[1], size[0])) / 255.0 + + def show(self, width=1000, ascender=880, descender=-120, even_odd=False, scale=None): + # Plot the image with matplotlib. + if not self.plt: + from matplotlib import pyplot + self.plt = pyplot + a = self.array(width, ascender=ascender, descender=descender, even_odd=even_odd, scale=scale) + self.plt.imshow(a, cmap='gray_r', vmin=0, vmax=1) + self.plt.show() + + def image(self, width=1000, ascender=880, descender=-120, even_odd=False, scale=None): + # Return a PIL image. + if not self.Image: + from PIL import Image as PILImage + self.Image = PILImage + buf, size = self.buffer(width, ascender=ascender, descender=descender, even_odd=even_odd, scale=scale) + img = self.Image.new('L', size, 0) + img.putalpha(self.Image.frombuffer('L', size, buf)) + return img + + def save(self, fp, width=1000, ascender=880, descender=-120, even_odd=False, scale=None, format=None, **kwargs): + # Save the image as a file. + img = self.image(width=width, ascender=ascender, descender=descender, even_odd=even_odd, scale=scale) + img.save(fp, format=format, **kwargs) + + @property + def bbox(self): + # Compute the exact bounding box of an outline. + bbox = FT_BBox() + outline = self.outline() + self.ft.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): + # Return an outline's ‘control box’. + cbox = FT_BBox() + outline = self.outline() + self.ft.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([], []) + self.contours.append(contour) + contour.points.append(pt) + contour.tags.append(self.LINE) + + def _lineTo(self, pt): + contour = self.contours[-1] + contour.points.append(pt) + contour.tags.append(self.LINE) + + def _curveToOne(self, p1, p2, p3): + t1, t2, t3 = self.OFFCURVE, self.OFFCURVE, self.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 + contour = self.contours[-1] + for p, t in ((p1, t1), (p2, t2)): + contour.points.append(p) + contour.tags.append(t) diff --git a/Tests/pens/ftPen_test.py b/Tests/pens/ftPen_test.py new file mode 100644 index 0000000000..2006bb35e1 --- /dev/null +++ b/Tests/pens/ftPen_test.py @@ -0,0 +1,105 @@ +import unittest + +try: + from ftPen import FTPen + FREETYPE_AVAILABLE = True +except ImportError: + FREETYPE_AVAILABLE = False + +def draw_cubic(pen): + pen.moveTo((50, 0)) + pen.lineTo((50, 500)) + pen.lineTo((200, 500)) + pen.curveTo((350, 500), (450, 400), (450, 250)) + pen.curveTo((450, 100), (350, 0), (200, 0)) + pen.closePath() + +def draw_quadratic(pen): + pen.moveTo((50, 0)) + pen.lineTo((50, 500)) + pen.lineTo((200, 500)) + pen.qCurveTo((274, 500), (388, 438), (450, 324), (450, 250)) + pen.qCurveTo((450, 176), (388, 62), (274, 0), (200, 0)) + pen.closePath() + +def star(pen): + pen.moveTo((0, 420)) + pen.lineTo((1000, 420)) + pen.lineTo((200, -200)) + pen.lineTo((500, 800)) + pen.lineTo((800, -200)) + pen.closePath() + +@unittest.skipUnless(FREETYPE_AVAILABLE, "freetype not installed") +class FTPenTest(unittest.TestCase): + def test_draw(self): + import base64, zlib + ZLIB_B64_BIN = 'eNrt3e1vleUdwPHf6QN2BEpPm9KWJa2Jh63DkAyqLwgUwxhLzDAsylwKGMgIWWG6hgSoyPaCKQNENCMDBTvBh1AKCps4I1OotFtcFF1ELEXrceumUFIeUrDQlh4Hbste7NWyvRj9fr//wifnuu7rOvd9XRH/aZ//XzZ4qaf7ZGfH8XePvHH4tZf3bHv4gSU1t0+qLM0L++/7/Prq0sn3X93xUO0dE4oT2kHM/9XldEvjhrqZqWwNMeb/rK9t79oFk5JKgsz/0enWhmUzCvUkmf+99O4V0wtERZl/Uceu5dNGKYsyv1amfducMeqizL/oxNaaMoVZ5rozza/V/ti0HKVZ5lc7t7PG53mY+dUGDi1N6c0yv1bb+snu08PMr9a5brzmvI7Wl2uOK/P6oqTmuPr2zc7THNeZjSnNeWP8gVnZmvOe41eVaM6b2RurNQeu3mrzNMd1qj5fc1zn1xRrjqt3U7nmuPq3V2qOa/D5iZrz9mmaUprzRvjNJZrjurB6pOa4uu7L1RzXRzUJzXG9PUVz3iP89mLNcZ2tzdIc15tVmvN25rYUaM5bt83XnFdLpea4eusSmuNqrtAcV89CzXntL9UcV/fdmvNqLNQc1ydTNcc1sCKhOa4Xk5rjSldpjuvyYs157RyhOa62cZrjunin5rgy9ZrzasjVHNfBAs1xtd+kOa7uKZrj6punOa/VmvPaktAc17PZmuPaO0xzXK8M1xxXS77muI4UaY7rWJnmuDoqNOehl2nOG96LNOc9yOVrzluyDdectzkzTHPeNmy25rieTWiOa4vmvFZrzmue5rj6pmiOq/smzXG1F2iO62Cu5rgaNOdVrzmuzJ2a47o4TnNcbSM0x7VTc16LNcd1uUpzXOmk5rheTGiOa4XmuAamao7rk0LNcTVqzutuzXF1l2qOa7/mvBZqjqunQnNczQnNcdVpjqu3UnNcLZrzmq85rq4CzXFt0RzXYJXmuN7M0hxXrea4zhZrjmu75rgyUzTH9XZCc1w1muP6KFdzXPdpjqtrpOa4VmuO60KJ5rg2a46rP6U5ribNeTuwEzXH9bzmuAYrNce1XXPeo3u55rg2aY6rt1hzXGs0x3U+X3Nc9ZrjOpWnOa5azXEd1ZxXtea4GjXH1VeiOa5VmuPqzNYc1yzNcR3QHFcmpTmujZrjOpOnOa7ZmuPapzlvLy6pOa5FmuN6XXPeEr1cc1z1muM6qjmv8ZrjWqc5rs6E5rgma45rvea42jTnldIc11LNcR3SHNdAgea4ajTHtVNzXOdyNMc1TXNcj2mOq11zXmWau1rTfMi3VXNcJzR3QtfcCV1zJ3TNndA1vw4bozmuOZrj2qY5rnbNcWVGaY5rmua4lmuOa5fmuDo051WgOa7pmuNaoTmu3ZrjSmvOq1BzXDM0x7VMc1wNmuNq1RzXac15JTXHNUlzXAs0x7VWc1x7NcfVpjmuvmzNcaU0xzVTc1x1muPaoDmuRs1xtWiOK605rssJzXEVa45rgua47tAcV63muB7SHNcOzXG9qjmu9zXHdVJzXJc055WnOa5SzXFVao5rkua4btccV43muJZojusBzXE9rDmubZrj2qM5rpc1x/Wa5rgOa47rDc1xHdEc17ua4zquOa4OzXF1ao7rpOa4ujXH1aM5rkua4xrUHNcVzR3bNfcZTnPXapq7J6P5dZd7r7z8j4WX/6Xy8p0JXr4bxct3IHn5rjMvv2ng5bdLvPxGkdeTmuPyzAFeni3CyzOEeHlWGC/PBOTl2Z+8POOXl2d58/LMflzezcHLO3h4edcWL+/U47VDc1zekcvLu7B5eec9rwma4yrWnFZfQnNa6dCcVovmuBo1x7VBc1x1muOaqTmusZrjlufZmtNqC81p7dMc11rNcS3QHNckzXElNad1OjSn1ao5rgbNcS3THNcMzXEVak4rHZrT2q05rnrNcU3XHFeB5rQ6QnNauzTHtVxzXNM0p5UZpTmt9tCc1jbNcc3RHNcYzWl9EJo7nWs+1KvRHFeZ5rROhOa0tmrudK6507nmQ6320JzWY5rj+obmtM7laE6rMTR3pab5EG8gqTmt5tCc1lLNcaU0p9UWmtNarzmuyZrT6kxoTmtdaE5rvOa0jobmtOo1p5Up15zW4dCc1iLNafUlNae1LzSnNVtzWmfzNKe1MTSnLc5TmtM6EJrTmqU5rc5szWmtCs1h9ZdoTqsxNKdVrTmt90JzWrWa0+rK05zW/aE5rPP5mtNaE5rD6h2tOa1NoTmsgXLNae0IzWFlKjWn9UJoTvuZT9ScVlNoDqs/pTmtzaE5rIslmtNaHZrDOj1Sc1o/Cs1hpYdpTmtOaA7rnYTmtKpDc1g7QnNY50ZrTmtxaA7rrSzNYQ3eEprD2hKaw+oq0JzW/NAcVmtoDqu3UnNadaE5rOaE5rB6bgzNYS0MzWG9FJrDOlOmOa3vheawdoXmsD4t1BzWlamhOaz60BzW/oTmsD5Ohuas+m4JzWEtCc1h7QzNYR0foTmsz24OzVll7grN3YzRfGj3VGgO61Cu5rDak6E5q+5UaA7bcq0OzWHdE5rD+mloDuuJ0BzWc1maw9qXE5qzOnBDaM6qdXhozupIfmjO6lhRaM6qoyw0h5FXhOawgf16+ZVr/j97fCsKzWGLtPzQHLYVMzw0h2243hCas3ouJzRn9URWaM7qwQjNUfXdE5qz6q4OzVm1p0JzVs3J0JzVU7mhOarMygjNUX12V2jO6vjNoTmrxhGhOWsj5t4IzVH96dbQnNVLydAc1ZWVidAc1ae3RWiOandRaI7qTE2E5qh+Uxaao+pZFKE5quYbQ3NUvUsToTmq1soIzUmdXpAIzUkNPp6M0JzUW7dGaE7q3JKs0BzVM6MjNCf1x6kRmpNKz0uE5qj1Wd2wCM1BXXxwZITmoAYeL43QnNSesRGag8rsrYrQnDSqP/21CM1B9f6iIkJzUOfXjo7QHFTXylERmoM6tvhLEZpz6m+6LVDhxTt/UhqhOWg1/tvvZEdozunso2ODGBa871ffzYvQnDOmt/ygMLAhV2YrK4IcDvyvG74e8FjgJx6pzorQnNKVw8u+ojfI/HzT3EKxQeYf/Hx6rtIc8w+fnDtGZY55R8O8LyvMMf9Qb5J5ek/9N5PKUsw/3nP/DLkp5t2/++Xyb7kcY5j3t/96/fcnFykJMO//8++bHl0666s5Gg5x876u4wef+dkPZ1WVJrQbWuaDly+cPfWXdPt77/yh9dArLzQ88uN753578rgxwwX7t/4Gpd/WjA==' + pen = FTPen(None) + draw_cubic(pen) + width, ascender, descender = 500, 500, 0 + buf1, _ = pen.buffer(width, ascender, descender) + buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) + self.assertEqual(buf1, buf2) + + def test_scale(self): + import base64, zlib + ZLIB_B64_BIN = 'eJy91r8NgkAUx/FLMHECCLMQC5gGK3UB4gJEXABhAbTAJXAJ7GkoBIornrQif/w2vvo+yXH37vFTqi/5rKYs8jhwDDVdMlp15ttM9NVFE2ZSiLShCYVI5VIhekeFSLKmQhIsZLixZaFdKqQyqZAQi9amQiIsOpsK8bHIsKgNKsTBIsAixiLHosCixKLB4vWHXfEv56fLb5B3Ce5E3u1XRQV+tXwy4OnDJxyeopUFhfYUFHsFRaqgSOGfUx+G65cSgPcNZlPGyRoBM0nmjJJMfdv+mpaa5+N+OW5W44vfouHQiw==' + pen = FTPen(None) + draw_cubic(pen) + width, ascender, descender = 500, 500, 0 + buf1, size = pen.buffer(width, ascender, descender, scale=(0.1, 0.1)) + buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) + self.assertEqual(buf1, buf2) + self.assertEqual(size, (50, 50)) + + def test_empty(self): + pen = FTPen(None) + width, ascender, descender = 500, 880, -120 + buf, size = pen.buffer(width, ascender, descender) + self.assertEqual(b'\0' * size[0] * size[1], buf) + + def test_bbox_and_cbox(self): + pen = FTPen(None) + draw_cubic(pen) + self.assertEqual(pen.bbox, (50.0, 0.0, 450.0, 500.0)) + self.assertEqual(pen.cbox, (50.0, 0.0, 450.0, 500.0)) + + def test_non_zero_fill(self): + import base64, zlib + ZLIB_B64_BIN = 'eJzt2L0NglAUhmESTZxA4yzGAqfRSl3AuABRF/BnAbTAJXAJ7GkoBAqKo4WNhbk3OZ4vknzvAk/g/nC5QcAYY4wxxhhrRfJZmaXJfjXqWBrving6tDZe1dufKV8NkSrqmxsieWhvSDO3N0SOPXtDjgBD9K/LbTShvSG5dgp7GBIBjEq54n0M2QKMWvcgXoZMAUYMMArVR8vPkBHAWAGMPcBIAEYKMDKAUQKMB8BAvCvEmCPmLmINIvYSwJ6I2NvPGuJ/vrWIMwPg7IM4wwHOovnA3GgmSsLDWGgJt3FSE07jZP7P2Sz1gusOQD3cLqPaaCety6h3xncyxWVmd7dU3m/Xw3rc/R3AGGOMsbb3BMrP0Is=' + pen = FTPen(None) + draw_cubic(pen) + width, ascender, descender = 1000, 800, -200 + buf1, size = pen.buffer(width, ascender, descender, scale=(0.1, 0.1), even_odd=False) + buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) + self.assertEqual(buf1, buf2) + + def test_even_odd_fill(self): + import base64, zlib + ZLIB_B64_BIN = 'eJzt2L0NglAUhmESTZxA4yzGAqfRSl3AuABRF/BnAbTAJXAJ7GkoBAqKo4WNhbk3OZ4vknzvAk/g/nC5QcAYY4wxxhhrRfJZmaXJfjXqWBrving6tDZe1dufKV8NkSrqmxsieWhvSDO3N0SOPXtDjgBD9K/LbTShvSG5dgp7GBIBjEq54n0M2QKMWvcgXoZMAUYMMArVR8vPkBHAWAGMPcBIAEYKMDKAUQKMB8BAvCvEmCPmLmINIvYSwJ6I2NvPGuJ/vrWIMwPg7IM4wwHOovnA3GgmSsLDWGgJt3FSE07jZP7P2Sz1gusOQD3cLqPaaCety6h3xncyxWVmd7dU3m/Xw3rc/R3AGGOMsbb3BMrP0Is=' + pen = FTPen(None) + draw_cubic(pen) + width, ascender, descender = 1000, 800, -200 + buf1, size = pen.buffer(width, ascender, descender, scale=(0.1, 0.1), even_odd=True) + buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) + self.assertEqual(buf1, buf2) + + def test_cubic_vs_quadratic(self): + # Assume the buffers are equal when PSNR > 38dB. + def psnr(b1, b2): + import math + 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)) + pen1, pen2 = FTPen(None), FTPen(None) + draw_cubic(pen1) + draw_quadratic(pen2) + width, ascender, descender = 500, 500, 0 + buf1, _ = pen1.buffer(width, ascender, descender) + buf2, _ = pen2.buffer(width, ascender, descender) + self.assertEqual(len(buf1), len(buf2)) + self.assertGreater(psnr(buf1, buf2), 38.0) + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) From 55dca28734595ab5a61c2b1f8432dc58afde5fe0 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Thu, 6 Jan 2022 17:49:43 +0900 Subject: [PATCH 02/25] Remove execute permission --- Lib/fontTools/pens/ftPen.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 Lib/fontTools/pens/ftPen.py diff --git a/Lib/fontTools/pens/ftPen.py b/Lib/fontTools/pens/ftPen.py old mode 100755 new mode 100644 From 46c580f00bdb3faaebb0bc2dc53f8e5fcfae7c19 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Thu, 6 Jan 2022 18:18:42 +0900 Subject: [PATCH 03/25] Fix import in unit test --- Tests/pens/ftPen_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/pens/ftPen_test.py b/Tests/pens/ftPen_test.py index 2006bb35e1..726e23bc6a 100644 --- a/Tests/pens/ftPen_test.py +++ b/Tests/pens/ftPen_test.py @@ -1,7 +1,7 @@ import unittest try: - from ftPen import FTPen + from fontTools.pens.ftPen import FTPen FREETYPE_AVAILABLE = True except ImportError: FREETYPE_AVAILABLE = False From 10a3ca8ea51c5744af8263b1c8717c83be92ed44 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Thu, 6 Jan 2022 19:07:26 +0900 Subject: [PATCH 04/25] Use freetype-py instead I didn't expect that the PyPI package contains the pre-built binary for each platform. Skip the test when freetype-py is not avaiable. --- Lib/fontTools/pens/ftPen.py | 97 +++++-------------------------------- Tests/pens/ftPen_test.py | 7 +-- 2 files changed, 16 insertions(+), 88 deletions(-) diff --git a/Lib/fontTools/pens/ftPen.py b/Lib/fontTools/pens/ftPen.py index 405bca5d09..336f36d915 100644 --- a/Lib/fontTools/pens/ftPen.py +++ b/Lib/fontTools/pens/ftPen.py @@ -4,93 +4,23 @@ import os import ctypes -import ctypes.util 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 +from freetype.ft_errors import FT_Exception + from fontTools.pens.basePen import BasePen from fontTools.misc.roundTools import otRound -class FT_LibraryRec(ctypes.Structure): - _fields_ = [] - -FT_Library = ctypes.POINTER(FT_LibraryRec) -FT_Pos = ctypes.c_long - -class FT_Vector(ctypes.Structure): - _fields_ = [('x', FT_Pos), ('y', FT_Pos)] - -class FT_BBox(ctypes.Structure): - _fields_ = [('xMin', FT_Pos), ('yMin', FT_Pos), ('xMax', FT_Pos), ('yMax', FT_Pos)] - -class FT_Bitmap(ctypes.Structure): - _fields_ = [('rows', ctypes.c_int), ('width', ctypes.c_int), ('pitch', ctypes.c_int), ('buffer', ctypes.POINTER(ctypes.c_ubyte)), ('num_grays', ctypes.c_short), ('pixel_mode', ctypes.c_ubyte), ('palette_mode', ctypes.c_char), ('palette', ctypes.c_void_p)] - -class FT_Outline(ctypes.Structure): - _fields_ = [('n_contours', ctypes.c_short), ('n_points', ctypes.c_short), ('points', ctypes.POINTER(FT_Vector)), ('tags', ctypes.POINTER(ctypes.c_ubyte)), ('contours', ctypes.POINTER(ctypes.c_short)), ('flags', ctypes.c_int)] - -class FreeType(object): - - @staticmethod - def load_freetype_lib(): - lib_path = ctypes.util.find_library('freetype') - if lib_path: - return ctypes.cdll.LoadLibrary(lib_path) - if platform.system() == 'Darwin': - # Try again by searching inside the installation paths of Homebrew and MacPorts - # This workaround is needed if Homebrew has been installed to a non-standard location. - orig_dyld_path = os.environ.get('DYLD_LIBRARY_PATH') - for dyld_path_func in ( - lambda: os.path.join(subprocess.check_output(('brew', '--prefix'), universal_newlines=True).rstrip(), 'lib'), - lambda: os.path.join(os.path.dirname(os.path.dirname(subprocess.check_output(('which', 'port'), universal_newlines=True).rstrip())), 'lib') - ): - try: - dyld_path = dyld_path_func() - os.environ['DYLD_LIBRARY_PATH'] = ':'.join(os.environ.get('DYLD_LIBRARY_PATH', '').split(':') + [dyld_path]) - lib_path = ctypes.util.find_library('freetype') - if lib_path: - return ctypes.cdll.LoadLibrary(lib_path) - except CalledProcessError: - pass - finally: - if orig_dyld_path: - os.environ['DYLD_LIBRARY_PATH'] = orig_dyld_path - else: - os.environ.pop('DYLD_LIBRARY_PATH', None) - return None - - def __init__(self): - lib = self.load_freetype_lib() - self.handle = FT_Library() - self.FT_Init_FreeType = lib.FT_Init_FreeType - self.FT_Done_FreeType = lib.FT_Done_FreeType - self.FT_Library_Version = lib.FT_Library_Version - self.FT_Outline_Get_CBox = lib.FT_Outline_Get_CBox - self.FT_Outline_Get_BBox = lib.FT_Outline_Get_BBox - self.FT_Outline_Get_Bitmap = lib.FT_Outline_Get_Bitmap - self.raise_error_if_needed(self.FT_Init_FreeType(ctypes.byref(self.handle))) - - def raise_error_if_needed(self, err): - # See the reference for error codes: - # https://freetype.org/freetype2/docs/reference/ft2-error_code_values.html - if err != 0: - raise RuntimeError("FT_Error: 0x{0:02X}".format(err)) - - def __del__(self): - if self.handle: - self.FT_Done_FreeType(self.handle) - - @property - def version(self): - major, minor, patch = ctypes.c_int(), ctypes.c_int(), ctypes.c_int() - self.FT_Library_Version(self.handle, ctypes.byref(major), ctypes.byref(minor), ctypes.byref(patch)) - return "{0}.{1}.{2}".format(major.value, minor.value, patch.value) - class FTPen(BasePen): - ft = None np = None plt = None Image = None @@ -102,14 +32,10 @@ class FTPen(BasePen): QOFFCURVE = 0b00000000 def __init__(self, glyphSet): - if not self.__class__.ft: - self.__class__.ft = FreeType() self.contours = [] def outline(self, offset=None, scale=None, even_odd=False): # Convert the current contours to FT_Outline. - FT_OUTLINE_NONE = 0x0 - FT_OUTLINE_EVEN_ODD_FILL = 0x2 offset = offset or (0, 0) scale = scale or (1.0, 1.0) n_contours = len(self.contours) @@ -139,7 +65,6 @@ def outline(self, offset=None, scale=None, even_odd=False): def buffer(self, width=1000, ascender=880, descender=-120, even_odd=False, scale=None): # Return a tuple with the bitmap buffer and its dimension. - FT_PIXEL_MODE_GRAY = 2 scale = scale or (1.0, 1.0) width = math.ceil(width * scale[0]) height = math.ceil((ascender - descender) * scale[1]) @@ -155,7 +80,9 @@ def buffer(self, width=1000, ascender=880, descender=-120, even_odd=False, scale (ctypes.c_void_p)(None) ) outline = self.outline(offset=(0, -descender), even_odd=even_odd, scale=scale) - self.ft.raise_error_if_needed(self.ft.FT_Outline_Get_Bitmap(self.ft.handle, ctypes.byref(outline), ctypes.byref(bitmap))) + 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=1000, ascender=880, descender=-120, even_odd=False, scale=None): @@ -195,7 +122,7 @@ def bbox(self): # Compute the exact bounding box of an outline. bbox = FT_BBox() outline = self.outline() - self.ft.FT_Outline_Get_BBox(ctypes.byref(outline), ctypes.byref(bbox)) + 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 @@ -203,7 +130,7 @@ def cbox(self): # Return an outline's ‘control box’. cbox = FT_BBox() outline = self.outline() - self.ft.FT_Outline_Get_CBox(ctypes.byref(outline), ctypes.byref(cbox)) + 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): diff --git a/Tests/pens/ftPen_test.py b/Tests/pens/ftPen_test.py index 726e23bc6a..e4c3cea245 100644 --- a/Tests/pens/ftPen_test.py +++ b/Tests/pens/ftPen_test.py @@ -2,9 +2,10 @@ try: from fontTools.pens.ftPen import FTPen - FREETYPE_AVAILABLE = True + import freetype + FREETYPE_PY_AVAILABLE = True except ImportError: - FREETYPE_AVAILABLE = False + FREETYPE_PY_AVAILABLE = False def draw_cubic(pen): pen.moveTo((50, 0)) @@ -30,7 +31,7 @@ def star(pen): pen.lineTo((800, -200)) pen.closePath() -@unittest.skipUnless(FREETYPE_AVAILABLE, "freetype not installed") +@unittest.skipUnless(FREETYPE_PY_AVAILABLE, "freetype-py not installed") class FTPenTest(unittest.TestCase): def test_draw(self): import base64, zlib From 288d9074974a32e4ce0e4cfd0b12266651501dd7 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Thu, 6 Jan 2022 22:07:33 +0900 Subject: [PATCH 05/25] Stop trying to cache modules pointlessly --- Lib/fontTools/pens/ftPen.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/Lib/fontTools/pens/ftPen.py b/Lib/fontTools/pens/ftPen.py index 336f36d915..2dae792730 100644 --- a/Lib/fontTools/pens/ftPen.py +++ b/Lib/fontTools/pens/ftPen.py @@ -21,9 +21,6 @@ class FTPen(BasePen): - np = None - plt = None - Image = None Contour = collections.namedtuple('Contour', ('points', 'tags')) LINE = 0b00000001 CURVE = 0b00000011 @@ -87,29 +84,23 @@ def buffer(self, width=1000, ascender=880, descender=-120, even_odd=False, scale def array(self, width=1000, ascender=880, descender=-120, even_odd=False, scale=None): # Return a numpy array. Each element takes values in the range of [0.0, 1.0]. - if not self.np: - import numpy as np - self.np = np + import numpy as np buf, size = self.buffer(width, ascender=ascender, descender=descender, even_odd=even_odd, scale=scale) - return self.np.frombuffer(buf, 'B').reshape((size[1], size[0])) / 255.0 + return np.frombuffer(buf, 'B').reshape((size[1], size[0])) / 255.0 def show(self, width=1000, ascender=880, descender=-120, even_odd=False, scale=None): # Plot the image with matplotlib. - if not self.plt: - from matplotlib import pyplot - self.plt = pyplot + from matplotlib import pyplot as plt a = self.array(width, ascender=ascender, descender=descender, even_odd=even_odd, scale=scale) - self.plt.imshow(a, cmap='gray_r', vmin=0, vmax=1) - self.plt.show() + plt.imshow(a, cmap='gray_r', vmin=0, vmax=1) + plt.show() def image(self, width=1000, ascender=880, descender=-120, even_odd=False, scale=None): # Return a PIL image. - if not self.Image: - from PIL import Image as PILImage - self.Image = PILImage + from PIL import Image buf, size = self.buffer(width, ascender=ascender, descender=descender, even_odd=even_odd, scale=scale) - img = self.Image.new('L', size, 0) - img.putalpha(self.Image.frombuffer('L', size, buf)) + img = Image.new('L', size, 0) + img.putalpha(Image.frombuffer('L', size, buf)) return img def save(self, fp, width=1000, ascender=880, descender=-120, even_odd=False, scale=None, format=None, **kwargs): From f7c29e89be17a81e61a4df5873783286f0a35f08 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Fri, 7 Jan 2022 22:28:24 +0900 Subject: [PATCH 06/25] Change arguments for positioning and dimension After experimenting with uharfbuzz for a while, I found out it was hard to handle top-to-bottom texts, so I gave up an idea to put an ascender or a descender value in the arguments. Instead, I simply expose 'offset', 'width' and 'height', which is way more straightforward than the previous design. In addition, 'contain' option is added to easily compensate and render glyphs such as combining accents or excessively tall letters. --- Lib/fontTools/pens/ftPen.py | 32 ++++++++++++++++++++------------ Tests/pens/ftPen_test.py | 35 ++++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/Lib/fontTools/pens/ftPen.py b/Lib/fontTools/pens/ftPen.py index 2dae792730..0b48e53dad 100644 --- a/Lib/fontTools/pens/ftPen.py +++ b/Lib/fontTools/pens/ftPen.py @@ -60,11 +60,19 @@ def outline(self, offset=None, scale=None, even_odd=False): (ctypes.c_int)(flags) ) - def buffer(self, width=1000, ascender=880, descender=-120, even_odd=False, scale=None): + 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. + offset_x, offset_y = offset or (0, 0) + if contain: + bbox = self.bbox + bbox_size = bbox[2] - bbox[0], bbox[3] - bbox[1] + offset_x = min(offset_x, bbox[0]) * -1 + width = max(width, bbox_size[0]) + offset_y = min(offset_y, bbox[1]) * -1 + height = max(height, bbox_size[1]) scale = scale or (1.0, 1.0) - width = math.ceil(width * scale[0]) - height = math.ceil((ascender - descender) * scale[1]) + width = math.ceil(width * scale[0]) + height = math.ceil(height * scale[1]) buf = ctypes.create_string_buffer(width * height) bitmap = FT_Bitmap( (ctypes.c_int)(height), @@ -76,36 +84,36 @@ def buffer(self, width=1000, ascender=880, descender=-120, even_odd=False, scale (ctypes.c_char)(0), (ctypes.c_void_p)(None) ) - outline = self.outline(offset=(0, -descender), even_odd=even_odd, scale=scale) + outline = self.outline(offset=(offset_x, offset_y), even_odd=even_odd, scale=scale) 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=1000, ascender=880, descender=-120, even_odd=False, scale=None): + 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]. import numpy as np - buf, size = self.buffer(width, ascender=ascender, descender=descender, even_odd=even_odd, scale=scale) + 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, width=1000, ascender=880, descender=-120, even_odd=False, scale=None): + def show(self, offset=None, width=1000, height=1000, even_odd=False, scale=None, contain=False): # Plot the image with matplotlib. from matplotlib import pyplot as plt - a = self.array(width, ascender=ascender, descender=descender, even_odd=even_odd, scale=scale) + 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, width=1000, ascender=880, descender=-120, even_odd=False, scale=None): + def image(self, offset=None, width=1000, height=1000, even_odd=False, scale=None, contain=False): # Return a PIL image. from PIL import Image - buf, size = self.buffer(width, ascender=ascender, descender=descender, even_odd=even_odd, scale=scale) + buf, size = self.buffer(offset=offset, width=width, height=height, even_odd=even_odd, scale=scale, contain=contain) img = Image.new('L', size, 0) img.putalpha(Image.frombuffer('L', size, buf)) return img - def save(self, fp, width=1000, ascender=880, descender=-120, even_odd=False, scale=None, format=None, **kwargs): + 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. - img = self.image(width=width, ascender=ascender, descender=descender, even_odd=even_odd, scale=scale) + img = self.image(offset=offset, width=width, height=height, even_odd=even_odd, scale=scale, contain=contain) img.save(fp, format=format, **kwargs) @property diff --git a/Tests/pens/ftPen_test.py b/Tests/pens/ftPen_test.py index e4c3cea245..44653392d2 100644 --- a/Tests/pens/ftPen_test.py +++ b/Tests/pens/ftPen_test.py @@ -38,8 +38,8 @@ def test_draw(self): ZLIB_B64_BIN = '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' pen = FTPen(None) draw_cubic(pen) - width, ascender, descender = 500, 500, 0 - buf1, _ = pen.buffer(width, ascender, descender) + offset, width, height = (0, 0), 500, 500 + buf1, _ = pen.buffer(offset=offset, width=width, height=height) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) self.assertEqual(buf1, buf2) @@ -48,16 +48,16 @@ def test_scale(self): ZLIB_B64_BIN = 'eJy91r8NgkAUx/FLMHECCLMQC5gGK3UB4gJEXABhAbTAJXAJ7GkoBIornrQif/w2vvo+yXH37vFTqi/5rKYs8jhwDDVdMlp15ttM9NVFE2ZSiLShCYVI5VIhekeFSLKmQhIsZLixZaFdKqQyqZAQi9amQiIsOpsK8bHIsKgNKsTBIsAixiLHosCixKLB4vWHXfEv56fLb5B3Ce5E3u1XRQV+tXwy4OnDJxyeopUFhfYUFHsFRaqgSOGfUx+G65cSgPcNZlPGyRoBM0nmjJJMfdv+mpaa5+N+OW5W44vfouHQiw==' pen = FTPen(None) draw_cubic(pen) - width, ascender, descender = 500, 500, 0 - buf1, size = pen.buffer(width, ascender, descender, scale=(0.1, 0.1)) + offset, width, height = (0, 0), 500, 500 + buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1)) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) self.assertEqual(buf1, buf2) self.assertEqual(size, (50, 50)) def test_empty(self): pen = FTPen(None) - width, ascender, descender = 500, 880, -120 - buf, size = pen.buffer(width, ascender, descender) + offset, width, height = (0, 0), 500, 500 + buf, size = pen.buffer(offset=offset, width=width, height=height) self.assertEqual(b'\0' * size[0] * size[1], buf) def test_bbox_and_cbox(self): @@ -71,8 +71,8 @@ def test_non_zero_fill(self): ZLIB_B64_BIN = 'eJzt2L0NglAUhmESTZxA4yzGAqfRSl3AuABRF/BnAbTAJXAJ7GkoBAqKo4WNhbk3OZ4vknzvAk/g/nC5QcAYY4wxxhhrRfJZmaXJfjXqWBrving6tDZe1dufKV8NkSrqmxsieWhvSDO3N0SOPXtDjgBD9K/LbTShvSG5dgp7GBIBjEq54n0M2QKMWvcgXoZMAUYMMArVR8vPkBHAWAGMPcBIAEYKMDKAUQKMB8BAvCvEmCPmLmINIvYSwJ6I2NvPGuJ/vrWIMwPg7IM4wwHOovnA3GgmSsLDWGgJt3FSE07jZP7P2Sz1gusOQD3cLqPaaCety6h3xncyxWVmd7dU3m/Xw3rc/R3AGGOMsbb3BMrP0Is=' pen = FTPen(None) draw_cubic(pen) - width, ascender, descender = 1000, 800, -200 - buf1, size = pen.buffer(width, ascender, descender, scale=(0.1, 0.1), even_odd=False) + offset, width, height = (0, 200), 1000, 1000 + buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1), even_odd=False) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) self.assertEqual(buf1, buf2) @@ -81,8 +81,8 @@ def test_even_odd_fill(self): ZLIB_B64_BIN = 'eJzt2L0NglAUhmESTZxA4yzGAqfRSl3AuABRF/BnAbTAJXAJ7GkoBAqKo4WNhbk3OZ4vknzvAk/g/nC5QcAYY4wxxhhrRfJZmaXJfjXqWBrving6tDZe1dufKV8NkSrqmxsieWhvSDO3N0SOPXtDjgBD9K/LbTShvSG5dgp7GBIBjEq54n0M2QKMWvcgXoZMAUYMMArVR8vPkBHAWAGMPcBIAEYKMDKAUQKMB8BAvCvEmCPmLmINIvYSwJ6I2NvPGuJ/vrWIMwPg7IM4wwHOovnA3GgmSsLDWGgJt3FSE07jZP7P2Sz1gusOQD3cLqPaaCety6h3xncyxWVmd7dU3m/Xw3rc/R3AGGOMsbb3BMrP0Is=' pen = FTPen(None) draw_cubic(pen) - width, ascender, descender = 1000, 800, -200 - buf1, size = pen.buffer(width, ascender, descender, scale=(0.1, 0.1), even_odd=True) + offset, width, height = (0, 200), 1000, 1000 + buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1), even_odd=True) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) self.assertEqual(buf1, buf2) @@ -95,12 +95,21 @@ def psnr(b1, b2): pen1, pen2 = FTPen(None), FTPen(None) draw_cubic(pen1) draw_quadratic(pen2) - width, ascender, descender = 500, 500, 0 - buf1, _ = pen1.buffer(width, ascender, descender) - buf2, _ = pen2.buffer(width, ascender, descender) + offset, width, height = (0, 0), 500, 500 + buf1, _ = pen1.buffer(offset=offset, width=width, height=height) + buf2, _ = pen2.buffer(offset=offset, width=width, height=height) self.assertEqual(len(buf1), len(buf2)) self.assertGreater(psnr(buf1, buf2), 38.0) + def test_contain(self): + import base64, zlib + ZLIB_B64_BIN = 'eJyVlKtvAkEQh5dHCEG0vQTbJgjOIVqB7VW0BoUsCQqHLElP4VCkV8sfQBpcqzCYYjEIDClNEFiaSxCEEB7bO/aWO2Bn2fmp22G+3ONjhhBxdB34AUzlBUt0v5GAtlpd4YgCpc84okXpBwqI2pTaUQxhUCf3GMJyiTcMMXKJHwSg010Q2iuMQGjvMkJdu7ZihLr2AvWirL3FCVXtrnAWVe0G3UdRu+UTIu2lBUVlUSJ3YwwwvnXuorXVgba2e7BQdaPWv6mG+Ms8/akA08fA+9/0zgO964NPFmucAxqx489cnMv650WBmcwvDIwyQteXXxDweQH9P8y1qH9tQv1OHkSEIQEIeT8FLCnAJzwY+bTzCQ9GPu2FU+DMtLdEhGza/QkPRjbthgiQTntgwodD/1qy5Ef7pnokUt8f4CWv85ZZ3j3mZ/wMLnlvpdNBmp3TA68ALnlPeDPBC4kmq0DamfBlOVgrL90apH0nfJI9LGYnEu2u8E7yuJrsgNod4dta+LQerm0B7Qa1c+Kb52yxdqufEgOEpPpC7WYcAgiJv/rX/4vPJ4U=' + pen = FTPen(None) + star(pen) + offset, width, height = (0, 0), 0, 0 + buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.05, 0.05), contain=True) + buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) + if __name__ == '__main__': import sys sys.exit(unittest.main()) From e4c1deb64fcf8790d455bcb4b4d0b501f54a05ca Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Fri, 7 Jan 2022 22:29:31 +0900 Subject: [PATCH 07/25] Add documentation Note that the example covers a ltr/rtl/ttb typesetting with uharfbuzz. --- Doc/source/pens/ftPen.rst | 8 ++ Doc/source/pens/index.rst | 1 + Lib/fontTools/pens/ftPen.py | 250 ++++++++++++++++++++++++++++++++---- 3 files changed, 236 insertions(+), 23 deletions(-) create mode 100644 Doc/source/pens/ftPen.rst diff --git a/Doc/source/pens/ftPen.rst b/Doc/source/pens/ftPen.rst new file mode 100644 index 0000000000..c5c8f7b504 --- /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 91175cf718..aafe40169a 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 0b48e53dad..594d12a783 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 fontTools.pens.basePen import BasePen from fontTools.misc.roundTools import otRound +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) - Contour = collections.namedtuple('Contour', ('points', 'tags')) - LINE = 0b00000001 - CURVE = 0b00000011 - OFFCURVE = 0b00000010 - QCURVE = 0b00000001 - QOFFCURVE = 0b00000000 + 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 @@ def outline(self, offset=None, scale=None, even_odd=False): 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 @@ def buffer(self, offset=None, width=1000, height=1000, even_odd=False, scale=Non 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 @@ def image(self, offset=None, width=1000, height=1000, even_odd=False, scale=None 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 @@ def bbox(self): @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) From dd72f7c3265cbfcf3985e16a726a8131a0ba1411 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Mon, 10 Jan 2022 18:19:58 +0900 Subject: [PATCH 08/25] Fix missing call to superclass __init__ --- Lib/fontTools/pens/ftPen.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/fontTools/pens/ftPen.py b/Lib/fontTools/pens/ftPen.py index 594d12a783..62ebf260b1 100644 --- a/Lib/fontTools/pens/ftPen.py +++ b/Lib/fontTools/pens/ftPen.py @@ -105,6 +105,7 @@ class FTPen(BasePen): """ def __init__(self, glyphSet): + BasePen.__init__(self, glyphset) self.contours = [] def outline(self, offset=None, scale=None, even_odd=False): From bf45aec95f0c81da761f8a9297db2356b7fa509a Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Mon, 10 Jan 2022 19:08:11 +0900 Subject: [PATCH 09/25] Fix typo --- Lib/fontTools/pens/ftPen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/pens/ftPen.py b/Lib/fontTools/pens/ftPen.py index 62ebf260b1..3cf7f4540b 100644 --- a/Lib/fontTools/pens/ftPen.py +++ b/Lib/fontTools/pens/ftPen.py @@ -105,7 +105,7 @@ class FTPen(BasePen): """ def __init__(self, glyphSet): - BasePen.__init__(self, glyphset) + BasePen.__init__(self, glyphSet) self.contours = [] def outline(self, offset=None, scale=None, even_odd=False): From e3694b602b81ac66ebbdc166d908282e7671745b Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Mon, 10 Jan 2022 23:25:09 +0900 Subject: [PATCH 10/25] Fix another typo in docstring --- Lib/fontTools/pens/ftPen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/pens/ftPen.py b/Lib/fontTools/pens/ftPen.py index 3cf7f4540b..36a61a5ea7 100644 --- a/Lib/fontTools/pens/ftPen.py +++ b/Lib/fontTools/pens/ftPen.py @@ -308,7 +308,7 @@ def save(self, fp, offset=None, width=1000, height=1000, even_odd=False, scale=N :Example: >>> pen = FTPen(None) >>> glyph.draw(pen) - >>> pen.save('glyph.png' width=500, height=1000) + >>> 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) From 9a9d0f0895a9d8a27ffd0decdfc946135cdd7bba Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Mon, 10 Jan 2022 23:43:41 +0900 Subject: [PATCH 11/25] Remove save() method As Khaled suggests, 'pen.image().save()' will do. --- Lib/fontTools/pens/ftPen.py | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/Lib/fontTools/pens/ftPen.py b/Lib/fontTools/pens/ftPen.py index 36a61a5ea7..8f7f1efc74 100644 --- a/Lib/fontTools/pens/ftPen.py +++ b/Lib/fontTools/pens/ftPen.py @@ -35,8 +35,8 @@ class FTPen(BasePen): 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. + 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 @@ -287,32 +287,6 @@ def image(self, offset=None, width=1000, height=1000, even_odd=False, scale=None img.putalpha(Image.frombuffer('L', size, buf)) return img - def save(self, fp, offset=None, width=1000, height=1000, even_odd=False, scale=None, contain=False, format=None, **kwargs): - """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): """Computes the exact bounding box of an outline. From d0dbbbd3c8e53579ec60d58f590fd78b94a4e936 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Mon, 10 Jan 2022 23:51:28 +0900 Subject: [PATCH 12/25] Rename to freetypePen/FreeTypePen --- .../pens/{ftPen.rst => freetypePen.rst} | 4 ++-- Doc/source/pens/index.rst | 2 +- .../pens/{ftPen.py => freetypePen.py} | 20 +++++++++---------- .../{ftPen_test.py => freetypePen_test.py} | 20 +++++++++---------- 4 files changed, 23 insertions(+), 23 deletions(-) rename Doc/source/pens/{ftPen.rst => freetypePen.rst} (58%) rename Lib/fontTools/pens/{ftPen.py => freetypePen.py} (97%) rename Tests/pens/{ftPen_test.py => freetypePen_test.py} (95%) diff --git a/Doc/source/pens/ftPen.rst b/Doc/source/pens/freetypePen.rst similarity index 58% rename from Doc/source/pens/ftPen.rst rename to Doc/source/pens/freetypePen.rst index c5c8f7b504..10b99a8857 100644 --- a/Doc/source/pens/ftPen.rst +++ b/Doc/source/pens/freetypePen.rst @@ -1,8 +1,8 @@ ########## -ftPen +freetypePen ########## -.. automodule:: fontTools.pens.ftPen +.. automodule:: fontTools.pens.freetypePen :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/index.rst b/Doc/source/pens/index.rst index aafe40169a..0f76b7238f 100644 --- a/Doc/source/pens/index.rst +++ b/Doc/source/pens/index.rst @@ -11,7 +11,7 @@ pens cocoaPen cu2quPen filterPen - ftPen + freetypePen momentsPen perimeterPen pointInsidePen diff --git a/Lib/fontTools/pens/ftPen.py b/Lib/fontTools/pens/freetypePen.py similarity index 97% rename from Lib/fontTools/pens/ftPen.py rename to Lib/fontTools/pens/freetypePen.py index 8f7f1efc74..313ee0f8f2 100644 --- a/Lib/fontTools/pens/ftPen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -2,7 +2,7 @@ """Pen to rasterize paths with FreeType.""" -__all__ = ['FTPen'] +__all__ = ['FreeTypePen'] import os import ctypes @@ -28,7 +28,7 @@ QCURVE = 0b00000001 QOFFCURVE = 0b00000000 -class FTPen(BasePen): +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 @@ -47,8 +47,8 @@ class FTPen(BasePen): show the glyph image of `fi` in a new window:: from fontTools.ttLib import TTFont - from fontTools.pens.ftPen import FTPen - pen = FTPen(None) + from fontTools.pens.freetypePen import FreeTypePen + pen = FreeTypePen(None) font = TTFont('SourceSansPro-Regular.otf') glyph = font.getGlyphSet()['fi'] glyph.draw(pen) @@ -59,7 +59,7 @@ class FTPen(BasePen): 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.freetypePen import FreeTypePen from fontTools.pens.transformPen import TransformPen from fontTools.misc.transform import Offset @@ -81,7 +81,7 @@ class FTPen(BasePen): hb.shape(font, buf, features) x, y = 0, 0 - pen = FTPen(None) + 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)) @@ -164,7 +164,7 @@ def buffer(self, offset=None, width=1000, height=1000, even_odd=False, scale=Non dimension. :Example: - >>> pen = FTPen(None) + >>> pen = FreeTypePen(None) >>> glyph.draw(pen) >>> buf, size = pen.buffer(width=500, height=1000) >>> type(buf), len(buf), size @@ -218,7 +218,7 @@ def array(self, offset=None, width=1000, height=1000, even_odd=False, scale=None Each element takes a value in the range of ``[0.0, 1.0]``. :Example: - >>> pen = FTPen(None) + >>> pen = FreeTypePen(None) >>> glyph.draw(pen) >>> arr = pen.array(width=500, height=1000) >>> type(a), a.shape @@ -245,7 +245,7 @@ def show(self, offset=None, width=1000, height=1000, even_odd=False, scale=None, rendering glyphs with negative sidebearings without clipping. :Example: - >>> pen = FTPen(None) + >>> pen = FreeTypePen(None) >>> glyph.draw(pen) >>> pen.show(width=500, height=1000) """ @@ -275,7 +275,7 @@ def image(self, offset=None, width=1000, height=1000, even_odd=False, scale=None channel obtained from the rendered bitmap. :Example: - >>> pen = FTPen(None) + >>> pen = FreeTypePen(None) >>> glyph.draw(pen) >>> img = pen.image(width=500, height=1000) >>> type(img), img.size diff --git a/Tests/pens/ftPen_test.py b/Tests/pens/freetypePen_test.py similarity index 95% rename from Tests/pens/ftPen_test.py rename to Tests/pens/freetypePen_test.py index 44653392d2..addc0e1280 100644 --- a/Tests/pens/ftPen_test.py +++ b/Tests/pens/freetypePen_test.py @@ -1,7 +1,7 @@ import unittest try: - from fontTools.pens.ftPen import FTPen + from fontTools.pens.freetypePen import FreeTypePen import freetype FREETYPE_PY_AVAILABLE = True except ImportError: @@ -32,11 +32,11 @@ def star(pen): pen.closePath() @unittest.skipUnless(FREETYPE_PY_AVAILABLE, "freetype-py not installed") -class FTPenTest(unittest.TestCase): +class FreeTypePenTest(unittest.TestCase): def test_draw(self): import base64, zlib ZLIB_B64_BIN = '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' - pen = FTPen(None) + pen = FreeTypePen(None) draw_cubic(pen) offset, width, height = (0, 0), 500, 500 buf1, _ = pen.buffer(offset=offset, width=width, height=height) @@ -46,7 +46,7 @@ def test_draw(self): def test_scale(self): import base64, zlib ZLIB_B64_BIN = 'eJy91r8NgkAUx/FLMHECCLMQC5gGK3UB4gJEXABhAbTAJXAJ7GkoBIornrQif/w2vvo+yXH37vFTqi/5rKYs8jhwDDVdMlp15ttM9NVFE2ZSiLShCYVI5VIhekeFSLKmQhIsZLixZaFdKqQyqZAQi9amQiIsOpsK8bHIsKgNKsTBIsAixiLHosCixKLB4vWHXfEv56fLb5B3Ce5E3u1XRQV+tXwy4OnDJxyeopUFhfYUFHsFRaqgSOGfUx+G65cSgPcNZlPGyRoBM0nmjJJMfdv+mpaa5+N+OW5W44vfouHQiw==' - pen = FTPen(None) + pen = FreeTypePen(None) draw_cubic(pen) offset, width, height = (0, 0), 500, 500 buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1)) @@ -55,13 +55,13 @@ def test_scale(self): self.assertEqual(size, (50, 50)) def test_empty(self): - pen = FTPen(None) + pen = FreeTypePen(None) offset, width, height = (0, 0), 500, 500 buf, size = pen.buffer(offset=offset, width=width, height=height) self.assertEqual(b'\0' * size[0] * size[1], buf) def test_bbox_and_cbox(self): - pen = FTPen(None) + pen = FreeTypePen(None) draw_cubic(pen) self.assertEqual(pen.bbox, (50.0, 0.0, 450.0, 500.0)) self.assertEqual(pen.cbox, (50.0, 0.0, 450.0, 500.0)) @@ -69,7 +69,7 @@ def test_bbox_and_cbox(self): def test_non_zero_fill(self): import base64, zlib ZLIB_B64_BIN = 'eJzt2L0NglAUhmESTZxA4yzGAqfRSl3AuABRF/BnAbTAJXAJ7GkoBAqKo4WNhbk3OZ4vknzvAk/g/nC5QcAYY4wxxhhrRfJZmaXJfjXqWBrving6tDZe1dufKV8NkSrqmxsieWhvSDO3N0SOPXtDjgBD9K/LbTShvSG5dgp7GBIBjEq54n0M2QKMWvcgXoZMAUYMMArVR8vPkBHAWAGMPcBIAEYKMDKAUQKMB8BAvCvEmCPmLmINIvYSwJ6I2NvPGuJ/vrWIMwPg7IM4wwHOovnA3GgmSsLDWGgJt3FSE07jZP7P2Sz1gusOQD3cLqPaaCety6h3xncyxWVmd7dU3m/Xw3rc/R3AGGOMsbb3BMrP0Is=' - pen = FTPen(None) + pen = FreeTypePen(None) draw_cubic(pen) offset, width, height = (0, 200), 1000, 1000 buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1), even_odd=False) @@ -79,7 +79,7 @@ def test_non_zero_fill(self): def test_even_odd_fill(self): import base64, zlib ZLIB_B64_BIN = 'eJzt2L0NglAUhmESTZxA4yzGAqfRSl3AuABRF/BnAbTAJXAJ7GkoBAqKo4WNhbk3OZ4vknzvAk/g/nC5QcAYY4wxxhhrRfJZmaXJfjXqWBrving6tDZe1dufKV8NkSrqmxsieWhvSDO3N0SOPXtDjgBD9K/LbTShvSG5dgp7GBIBjEq54n0M2QKMWvcgXoZMAUYMMArVR8vPkBHAWAGMPcBIAEYKMDKAUQKMB8BAvCvEmCPmLmINIvYSwJ6I2NvPGuJ/vrWIMwPg7IM4wwHOovnA3GgmSsLDWGgJt3FSE07jZP7P2Sz1gusOQD3cLqPaaCety6h3xncyxWVmd7dU3m/Xw3rc/R3AGGOMsbb3BMrP0Is=' - pen = FTPen(None) + pen = FreeTypePen(None) draw_cubic(pen) offset, width, height = (0, 200), 1000, 1000 buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1), even_odd=True) @@ -92,7 +92,7 @@ def psnr(b1, b2): import math 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)) - pen1, pen2 = FTPen(None), FTPen(None) + pen1, pen2 = FreeTypePen(None), FreeTypePen(None) draw_cubic(pen1) draw_quadratic(pen2) offset, width, height = (0, 0), 500, 500 @@ -104,7 +104,7 @@ def psnr(b1, b2): def test_contain(self): import base64, zlib ZLIB_B64_BIN = 'eJyVlKtvAkEQh5dHCEG0vQTbJgjOIVqB7VW0BoUsCQqHLElP4VCkV8sfQBpcqzCYYjEIDClNEFiaSxCEEB7bO/aWO2Bn2fmp22G+3ONjhhBxdB34AUzlBUt0v5GAtlpd4YgCpc84okXpBwqI2pTaUQxhUCf3GMJyiTcMMXKJHwSg010Q2iuMQGjvMkJdu7ZihLr2AvWirL3FCVXtrnAWVe0G3UdRu+UTIu2lBUVlUSJ3YwwwvnXuorXVgba2e7BQdaPWv6mG+Ms8/akA08fA+9/0zgO964NPFmucAxqx489cnMv650WBmcwvDIwyQteXXxDweQH9P8y1qH9tQv1OHkSEIQEIeT8FLCnAJzwY+bTzCQ9GPu2FU+DMtLdEhGza/QkPRjbthgiQTntgwodD/1qy5Ef7pnokUt8f4CWv85ZZ3j3mZ/wMLnlvpdNBmp3TA68ALnlPeDPBC4kmq0DamfBlOVgrL90apH0nfJI9LGYnEu2u8E7yuJrsgNod4dta+LQerm0B7Qa1c+Kb52yxdqufEgOEpPpC7WYcAgiJv/rX/4vPJ4U=' - pen = FTPen(None) + pen = FreeTypePen(None) star(pen) offset, width, height = (0, 0), 0, 0 buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.05, 0.05), contain=True) From 0e880260ae6e869c5b21dd04c2e2097f7218b37d Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Mon, 10 Jan 2022 23:55:28 +0900 Subject: [PATCH 13/25] Remove unnecessary import in test --- Tests/pens/freetypePen_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/pens/freetypePen_test.py b/Tests/pens/freetypePen_test.py index addc0e1280..4e3e93a0e3 100644 --- a/Tests/pens/freetypePen_test.py +++ b/Tests/pens/freetypePen_test.py @@ -2,7 +2,6 @@ try: from fontTools.pens.freetypePen import FreeTypePen - import freetype FREETYPE_PY_AVAILABLE = True except ImportError: FREETYPE_PY_AVAILABLE = False From 2c0ab2a05a470d5f988c97b63b9a9baccfd8ad8e Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Tue, 11 Jan 2022 00:33:44 +0900 Subject: [PATCH 14/25] Relax assertions for rendering tests There are possibilities that the rendering results may change among FreeType versions. I've already used the PSNR comparison for cubic vs quadratic testing, so I applied the same technique and threshold to all rendering tests to relax assertions. Also handles the case that MSE becomes zero. Optional dependencies are not needed for the tests. --- Tests/pens/freetypePen_test.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/Tests/pens/freetypePen_test.py b/Tests/pens/freetypePen_test.py index 4e3e93a0e3..cc2e6c83f3 100644 --- a/Tests/pens/freetypePen_test.py +++ b/Tests/pens/freetypePen_test.py @@ -30,6 +30,14 @@ def star(pen): pen.lineTo((800, -200)) pen.closePath() +# Assume the buffers are equal when PSNR > 38dB. +PSNR_THRESHOLD = 38.0 + +def psnr(b1, b2): + import math + 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 + @unittest.skipUnless(FREETYPE_PY_AVAILABLE, "freetype-py not installed") class FreeTypePenTest(unittest.TestCase): def test_draw(self): @@ -40,7 +48,8 @@ def test_draw(self): offset, width, height = (0, 0), 500, 500 buf1, _ = pen.buffer(offset=offset, width=width, height=height) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) - self.assertEqual(buf1, buf2) + self.assertEqual(len(buf1), len(buf2)) + self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) def test_scale(self): import base64, zlib @@ -50,8 +59,8 @@ def test_scale(self): offset, width, height = (0, 0), 500, 500 buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1)) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) - self.assertEqual(buf1, buf2) self.assertEqual(size, (50, 50)) + self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) def test_empty(self): pen = FreeTypePen(None) @@ -73,7 +82,8 @@ def test_non_zero_fill(self): offset, width, height = (0, 200), 1000, 1000 buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1), even_odd=False) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) - self.assertEqual(buf1, buf2) + self.assertEqual(len(buf1), len(buf2)) + self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) def test_even_odd_fill(self): import base64, zlib @@ -83,14 +93,10 @@ def test_even_odd_fill(self): offset, width, height = (0, 200), 1000, 1000 buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1), even_odd=True) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) - self.assertEqual(buf1, buf2) + self.assertEqual(len(buf1), len(buf2)) + self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) def test_cubic_vs_quadratic(self): - # Assume the buffers are equal when PSNR > 38dB. - def psnr(b1, b2): - import math - 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)) pen1, pen2 = FreeTypePen(None), FreeTypePen(None) draw_cubic(pen1) draw_quadratic(pen2) @@ -98,7 +104,7 @@ def psnr(b1, b2): buf1, _ = pen1.buffer(offset=offset, width=width, height=height) buf2, _ = pen2.buffer(offset=offset, width=width, height=height) self.assertEqual(len(buf1), len(buf2)) - self.assertGreater(psnr(buf1, buf2), 38.0) + self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) def test_contain(self): import base64, zlib @@ -108,6 +114,8 @@ def test_contain(self): offset, width, height = (0, 0), 0, 0 buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.05, 0.05), contain=True) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) + self.assertEqual(len(buf1), len(buf2)) + self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) if __name__ == '__main__': import sys From d399e93d959e91e96389270088b9324fbbc245b7 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Tue, 11 Jan 2022 01:06:19 +0900 Subject: [PATCH 15/25] Rename even_odd to evenOdd PointInsidePen already uses camelCase convention for the argument. --- Lib/fontTools/pens/freetypePen.py | 30 +++++++++++++++--------------- Tests/pens/freetypePen_test.py | 4 ++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Lib/fontTools/pens/freetypePen.py b/Lib/fontTools/pens/freetypePen.py index 313ee0f8f2..ec91250a7e 100644 --- a/Lib/fontTools/pens/freetypePen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -108,13 +108,13 @@ def __init__(self, glyphSet): BasePen.__init__(self, glyphSet) self.contours = [] - def outline(self, offset=None, scale=None, even_odd=False): + def outline(self, offset=None, scale=None, evenOdd=False): """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. + evenOdd: Pass ``True`` for even-odd fill instead of non-zero. """ offset = offset or (0, 0) scale = scale or (1.0, 1.0) @@ -133,7 +133,7 @@ def outline(self, offset=None, scale=None, even_odd=False): for contour in self.contours: contours_sum += len(contour.points) contours.append(contours_sum - 1) - flags = FT_OUTLINE_EVEN_ODD_FILL if even_odd else FT_OUTLINE_NONE + flags = FT_OUTLINE_EVEN_ODD_FILL if evenOdd else FT_OUTLINE_NONE return FT_Outline( (ctypes.c_short)(nContours), (ctypes.c_short)(n_points), @@ -143,7 +143,7 @@ def outline(self, offset=None, scale=None, even_odd=False): (ctypes.c_int)(flags) ) - def buffer(self, offset=None, width=1000, height=1000, even_odd=False, scale=None, contain=False): + def buffer(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None, contain=False): """Renders the current contours within a bitmap buffer. Args: @@ -153,7 +153,7 @@ def buffer(self, offset=None, width=1000, height=1000, even_odd=False, scale=Non 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. + evenOdd: 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. @@ -192,13 +192,13 @@ def buffer(self, offset=None, width=1000, height=1000, even_odd=False, scale=Non (ctypes.c_char)(0), (ctypes.c_void_p)(None) ) - outline = self.outline(offset=(offset_x, offset_y), even_odd=even_odd, scale=scale) + outline = self.outline(offset=(offset_x, offset_y), evenOdd=evenOdd, scale=scale) 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, offset=None, width=1000, height=1000, even_odd=False, scale=None, contain=False): + def array(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None, contain=False): """Returns the rendered contours as a numpy array. Requires `numpy`. Args: @@ -208,7 +208,7 @@ def array(self, offset=None, width=1000, height=1000, even_odd=False, scale=None 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. + evenOdd: 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. @@ -225,10 +225,10 @@ def array(self, offset=None, width=1000, height=1000, even_odd=False, scale=None (, (1000, 500)) """ import numpy as np - buf, size = self.buffer(offset=offset, width=width, height=height, even_odd=even_odd, scale=scale, contain=contain) + buf, size = self.buffer(offset=offset, width=width, height=height, evenOdd=evenOdd, 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): + def show(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None, contain=False): """Plots the rendered contours with `pyplot`. Requires `numpy` and `matplotlib`. @@ -239,7 +239,7 @@ def show(self, offset=None, width=1000, height=1000, even_odd=False, scale=None, 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. + evenOdd: 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. @@ -250,11 +250,11 @@ def show(self, offset=None, width=1000, height=1000, even_odd=False, scale=None, >>> 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) + a = self.array(offset=offset, width=width, height=height, evenOdd=evenOdd, 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): + def image(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None, contain=False): """Returns the rendered contours as a PIL image. Requires `Pillow`. Can be used to display a glyph image in Jupyter Notebook. @@ -265,7 +265,7 @@ def image(self, offset=None, width=1000, height=1000, even_odd=False, scale=None 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. + evenOdd: 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. @@ -282,7 +282,7 @@ def image(self, offset=None, width=1000, height=1000, even_odd=False, scale=None (, (500, 1000)) """ from PIL import Image - buf, size = self.buffer(offset=offset, width=width, height=height, even_odd=even_odd, scale=scale, contain=contain) + buf, size = self.buffer(offset=offset, width=width, height=height, evenOdd=evenOdd, scale=scale, contain=contain) img = Image.new('L', size, 0) img.putalpha(Image.frombuffer('L', size, buf)) return img diff --git a/Tests/pens/freetypePen_test.py b/Tests/pens/freetypePen_test.py index cc2e6c83f3..542c64c50d 100644 --- a/Tests/pens/freetypePen_test.py +++ b/Tests/pens/freetypePen_test.py @@ -80,7 +80,7 @@ def test_non_zero_fill(self): pen = FreeTypePen(None) draw_cubic(pen) offset, width, height = (0, 200), 1000, 1000 - buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1), even_odd=False) + buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1), evenOdd=False) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) self.assertEqual(len(buf1), len(buf2)) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) @@ -91,7 +91,7 @@ def test_even_odd_fill(self): pen = FreeTypePen(None) draw_cubic(pen) offset, width, height = (0, 200), 1000, 1000 - buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1), even_odd=True) + buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1), evenOdd=True) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) self.assertEqual(len(buf1), len(buf2)) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) From 3f26a1301a081e2758b6885bb2c33a86a6c51793 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Tue, 11 Jan 2022 00:43:16 +0900 Subject: [PATCH 16/25] Add freetype-py to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index b34d590e33..d061b77943 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ skia-pathops==0.7.2; platform_python_implementation != "PyPy" # this is only required to run Tests/cu2qu/{ufo,cli}_test.py ufoLib2==0.13.0 pyobjc==8.1; sys_platform == "darwin" +freetype-py==2.2.0 From 184a6152f6f8f64c309c189b08ca5b1be89ad7c9 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Tue, 11 Jan 2022 01:58:55 +0900 Subject: [PATCH 17/25] Opt out of running doctest --- Lib/fontTools/pens/freetypePen.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Lib/fontTools/pens/freetypePen.py b/Lib/fontTools/pens/freetypePen.py index ec91250a7e..319a9dd66a 100644 --- a/Lib/fontTools/pens/freetypePen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -164,10 +164,10 @@ def buffer(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None dimension. :Example: - >>> pen = FreeTypePen(None) - >>> glyph.draw(pen) - >>> buf, size = pen.buffer(width=500, height=1000) - >>> type(buf), len(buf), size + >> pen = FreeTypePen(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) @@ -218,10 +218,10 @@ def array(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None, Each element takes a value in the range of ``[0.0, 1.0]``. :Example: - >>> pen = FreeTypePen(None) - >>> glyph.draw(pen) - >>> arr = pen.array(width=500, height=1000) - >>> type(a), a.shape + >> pen = FreeTypePen(None) + >> glyph.draw(pen) + >> arr = pen.array(width=500, height=1000) + >> type(a), a.shape (, (1000, 500)) """ import numpy as np @@ -245,9 +245,9 @@ def show(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None, rendering glyphs with negative sidebearings without clipping. :Example: - >>> pen = FreeTypePen(None) - >>> glyph.draw(pen) - >>> pen.show(width=500, height=1000) + >> pen = FreeTypePen(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, evenOdd=evenOdd, scale=scale, contain=contain) @@ -275,10 +275,10 @@ def image(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None, channel obtained from the rendered bitmap. :Example: - >>> pen = FreeTypePen(None) - >>> glyph.draw(pen) - >>> img = pen.image(width=500, height=1000) - >>> type(img), img.size + >> pen = FreeTypePen(None) + >> glyph.draw(pen) + >> img = pen.image(width=500, height=1000) + >> type(img), img.size (, (500, 1000)) """ from PIL import Image From 42bc1257b416c7f2565f4463308d47316fb41a2e Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Tue, 11 Jan 2022 02:26:05 +0900 Subject: [PATCH 18/25] Add reference for PSNR --- Tests/pens/freetypePen_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/pens/freetypePen_test.py b/Tests/pens/freetypePen_test.py index 542c64c50d..44f37785a6 100644 --- a/Tests/pens/freetypePen_test.py +++ b/Tests/pens/freetypePen_test.py @@ -30,7 +30,9 @@ def star(pen): pen.lineTo((800, -200)) pen.closePath() -# Assume the buffers are equal when PSNR > 38dB. +# Assume the buffers are equal when PSNR > 38dB. See also: +# Peak signal-to-noise ratio +# https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio PSNR_THRESHOLD = 38.0 def psnr(b1, b2): From 6f7ef4a838535d73be2bc0862071a2c1d488a43f Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Tue, 11 Jan 2022 22:13:59 +0900 Subject: [PATCH 19/25] Prefer single transform matrix over offset/scale --- Lib/fontTools/pens/freetypePen.py | 88 ++++++++++++++++--------------- Tests/pens/freetypePen_test.py | 38 +++++++------ 2 files changed, 69 insertions(+), 57 deletions(-) diff --git a/Lib/fontTools/pens/freetypePen.py b/Lib/fontTools/pens/freetypePen.py index 319a9dd66a..d7d4181283 100644 --- a/Lib/fontTools/pens/freetypePen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -20,6 +20,7 @@ from fontTools.pens.basePen import BasePen from fontTools.misc.roundTools import otRound +from fontTools.misc.transform import Transform Contour = collections.namedtuple('Contour', ('points', 'tags')) LINE = 0b00000001 @@ -48,13 +49,14 @@ class FreeTypePen(BasePen): 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(offset=(0, -descender), width=width, height=height) + pen.show(width=width, height=height, transform=Offset(0, -descender)) Combining with `uharfbuzz`, you can typeset a chunk of glyphs in a pen:: @@ -98,7 +100,7 @@ class FreeTypePen(BasePen): offset = (-vhea_descender, -y) width = vhea_ascender - vhea_descender height = -y - pen.show(offset=offset, width=width, height=height, contain=contain) + 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. @@ -108,22 +110,25 @@ def __init__(self, glyphSet): BasePen.__init__(self, glyphSet) self.contours = [] - def outline(self, offset=None, scale=None, evenOdd=False): + def outline(self, transform=None, evenOdd=False): """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. + transform: A 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. """ - offset = offset or (0, 0) - scale = scale or (1.0, 1.0) + transform = transform or Transform() + if not hasattr(transform, 'transformPoint'): + transform = Transform(*transform) nContours = 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 = [] for contour in self.contours: for point in contour.points: - points.append(FT_Vector(FT_Pos(otRound((point[0] + offset[0]) * scale[0] * 64)), FT_Pos(otRound((point[1] + offset[1]) * scale[1] * 64)))) + 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: @@ -143,16 +148,15 @@ def outline(self, offset=None, scale=None, evenOdd=False): (ctypes.c_int)(flags) ) - def buffer(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None, contain=False): + def buffer(self, width=1000, height=1000, transform=None, evenOdd=False, contain=False): """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. + transform: A 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. evenOdd: 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 @@ -170,17 +174,20 @@ def buffer(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None >> type(buf), len(buf), size (, 500000, (500, 1000)) """ - offset_x, offset_y = offset or (0, 0) + transform = transform or Transform() + if not hasattr(transform, 'transformPoint'): + transform = Transform(*transform) if contain: - bbox = self.bbox + 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] - offset_x = min(offset_x, bbox[0]) * -1 - width = max(width, bbox_size[0]) - offset_y = min(offset_y, bbox[1]) * -1 - height = max(height, bbox_size[1]) - scale = scale or (1.0, 1.0) - width = math.ceil(width * scale[0]) - height = math.ceil(height * scale[1]) + dx = min(-transform.dx, bbox[0]) * -1.0 + dy = min(-transform.dy, bbox[1]) * -1.0 + width = max(width, bbox_size[0]) + height = max(height, bbox_size[1]) + 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), @@ -192,22 +199,21 @@ def buffer(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None (ctypes.c_char)(0), (ctypes.c_void_p)(None) ) - outline = self.outline(offset=(offset_x, offset_y), evenOdd=evenOdd, scale=scale) + 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, offset=None, width=1000, height=1000, evenOdd=False, scale=None, contain=False): + def array(self, width=1000, height=1000, transform=None, evenOdd=False, contain=False): """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. + transform: A 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. evenOdd: 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 @@ -225,20 +231,19 @@ def array(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None, (, (1000, 500)) """ import numpy as np - buf, size = self.buffer(offset=offset, width=width, height=height, evenOdd=evenOdd, scale=scale, contain=contain) + buf, size = self.buffer(width=width, height=height, transform=transform, evenOdd=evenOdd, contain=contain) return np.frombuffer(buf, 'B').reshape((size[1], size[0])) / 255.0 - def show(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None, contain=False): + def show(self, width=1000, height=1000, transform=None, evenOdd=False, contain=False): """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. + transform: A 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. evenOdd: 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 @@ -250,21 +255,20 @@ def show(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None, >> pen.show(width=500, height=1000) """ from matplotlib import pyplot as plt - a = self.array(offset=offset, width=width, height=height, evenOdd=evenOdd, scale=scale, contain=contain) + a = self.array(width=width, height=height, transform=transform, evenOdd=evenOdd, contain=contain) plt.imshow(a, cmap='gray_r', vmin=0, vmax=1) plt.show() - def image(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None, contain=False): + def image(self, width=1000, height=1000, transform=None, evenOdd=False, contain=False): """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. + transform: A 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. evenOdd: 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 @@ -282,7 +286,7 @@ def image(self, offset=None, width=1000, height=1000, evenOdd=False, scale=None, (, (500, 1000)) """ from PIL import Image - buf, size = self.buffer(offset=offset, width=width, height=height, evenOdd=evenOdd, scale=scale, contain=contain) + buf, size = self.buffer(width=width, height=height, transform=transform, evenOdd=evenOdd, contain=contain) img = Image.new('L', size, 0) img.putalpha(Image.frombuffer('L', size, buf)) return img diff --git a/Tests/pens/freetypePen_test.py b/Tests/pens/freetypePen_test.py index 44f37785a6..36e5c2d028 100644 --- a/Tests/pens/freetypePen_test.py +++ b/Tests/pens/freetypePen_test.py @@ -6,6 +6,8 @@ except ImportError: FREETYPE_PY_AVAILABLE = False +from fontTools.misc.transform import Scale + def draw_cubic(pen): pen.moveTo((50, 0)) pen.lineTo((50, 500)) @@ -47,8 +49,8 @@ def test_draw(self): ZLIB_B64_BIN = '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' pen = FreeTypePen(None) draw_cubic(pen) - offset, width, height = (0, 0), 500, 500 - buf1, _ = pen.buffer(offset=offset, width=width, height=height) + width, height = 500, 500 + buf1, _ = pen.buffer(width=width, height=height) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) self.assertEqual(len(buf1), len(buf2)) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) @@ -58,16 +60,17 @@ def test_scale(self): ZLIB_B64_BIN = 'eJy91r8NgkAUx/FLMHECCLMQC5gGK3UB4gJEXABhAbTAJXAJ7GkoBIornrQif/w2vvo+yXH37vFTqi/5rKYs8jhwDDVdMlp15ttM9NVFE2ZSiLShCYVI5VIhekeFSLKmQhIsZLixZaFdKqQyqZAQi9amQiIsOpsK8bHIsKgNKsTBIsAixiLHosCixKLB4vWHXfEv56fLb5B3Ce5E3u1XRQV+tXwy4OnDJxyeopUFhfYUFHsFRaqgSOGfUx+G65cSgPcNZlPGyRoBM0nmjJJMfdv+mpaa5+N+OW5W44vfouHQiw==' pen = FreeTypePen(None) draw_cubic(pen) - offset, width, height = (0, 0), 500, 500 - buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1)) + t = Scale(0.1, 0.1) + width, height = t.transformPoint((500, 500)) + buf1, size = pen.buffer(width=width, height=height, transform=t) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) self.assertEqual(size, (50, 50)) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) def test_empty(self): pen = FreeTypePen(None) - offset, width, height = (0, 0), 500, 500 - buf, size = pen.buffer(offset=offset, width=width, height=height) + width, height = 500, 500 + buf, size = pen.buffer(width=width, height=height) self.assertEqual(b'\0' * size[0] * size[1], buf) def test_bbox_and_cbox(self): @@ -81,8 +84,10 @@ def test_non_zero_fill(self): ZLIB_B64_BIN = 'eJzt2L0NglAUhmESTZxA4yzGAqfRSl3AuABRF/BnAbTAJXAJ7GkoBAqKo4WNhbk3OZ4vknzvAk/g/nC5QcAYY4wxxhhrRfJZmaXJfjXqWBrving6tDZe1dufKV8NkSrqmxsieWhvSDO3N0SOPXtDjgBD9K/LbTShvSG5dgp7GBIBjEq54n0M2QKMWvcgXoZMAUYMMArVR8vPkBHAWAGMPcBIAEYKMDKAUQKMB8BAvCvEmCPmLmINIvYSwJ6I2NvPGuJ/vrWIMwPg7IM4wwHOovnA3GgmSsLDWGgJt3FSE07jZP7P2Sz1gusOQD3cLqPaaCety6h3xncyxWVmd7dU3m/Xw3rc/R3AGGOMsbb3BMrP0Is=' pen = FreeTypePen(None) draw_cubic(pen) - offset, width, height = (0, 200), 1000, 1000 - buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1), evenOdd=False) + t = Scale(0.1, 0.1) + width, height = t.transformPoint((1000, 1000)) + t = t.translate(0, 200) + buf1, size = pen.buffer(width=width, height=height, transform=t, evenOdd=False) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) self.assertEqual(len(buf1), len(buf2)) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) @@ -92,8 +97,10 @@ def test_even_odd_fill(self): ZLIB_B64_BIN = 'eJzt2L0NglAUhmESTZxA4yzGAqfRSl3AuABRF/BnAbTAJXAJ7GkoBAqKo4WNhbk3OZ4vknzvAk/g/nC5QcAYY4wxxhhrRfJZmaXJfjXqWBrving6tDZe1dufKV8NkSrqmxsieWhvSDO3N0SOPXtDjgBD9K/LbTShvSG5dgp7GBIBjEq54n0M2QKMWvcgXoZMAUYMMArVR8vPkBHAWAGMPcBIAEYKMDKAUQKMB8BAvCvEmCPmLmINIvYSwJ6I2NvPGuJ/vrWIMwPg7IM4wwHOovnA3GgmSsLDWGgJt3FSE07jZP7P2Sz1gusOQD3cLqPaaCety6h3xncyxWVmd7dU3m/Xw3rc/R3AGGOMsbb3BMrP0Is=' pen = FreeTypePen(None) draw_cubic(pen) - offset, width, height = (0, 200), 1000, 1000 - buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.1, 0.1), evenOdd=True) + t = Scale(0.1, 0.1) + width, height = t.transformPoint((1000, 1000)) + t = t.translate(0, 200) + buf1, size = pen.buffer(width=width, height=height, transform=t, evenOdd=True) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) self.assertEqual(len(buf1), len(buf2)) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) @@ -102,9 +109,9 @@ def test_cubic_vs_quadratic(self): pen1, pen2 = FreeTypePen(None), FreeTypePen(None) draw_cubic(pen1) draw_quadratic(pen2) - offset, width, height = (0, 0), 500, 500 - buf1, _ = pen1.buffer(offset=offset, width=width, height=height) - buf2, _ = pen2.buffer(offset=offset, width=width, height=height) + width, height = 500, 500 + buf1, _ = pen1.buffer(width=width, height=height) + buf2, _ = pen2.buffer(width=width, height=height) self.assertEqual(len(buf1), len(buf2)) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) @@ -113,8 +120,9 @@ def test_contain(self): ZLIB_B64_BIN = 'eJyVlKtvAkEQh5dHCEG0vQTbJgjOIVqB7VW0BoUsCQqHLElP4VCkV8sfQBpcqzCYYjEIDClNEFiaSxCEEB7bO/aWO2Bn2fmp22G+3ONjhhBxdB34AUzlBUt0v5GAtlpd4YgCpc84okXpBwqI2pTaUQxhUCf3GMJyiTcMMXKJHwSg010Q2iuMQGjvMkJdu7ZihLr2AvWirL3FCVXtrnAWVe0G3UdRu+UTIu2lBUVlUSJ3YwwwvnXuorXVgba2e7BQdaPWv6mG+Ms8/akA08fA+9/0zgO964NPFmucAxqx489cnMv650WBmcwvDIwyQteXXxDweQH9P8y1qH9tQv1OHkSEIQEIeT8FLCnAJzwY+bTzCQ9GPu2FU+DMtLdEhGza/QkPRjbthgiQTntgwodD/1qy5Ef7pnokUt8f4CWv85ZZ3j3mZ/wMLnlvpdNBmp3TA68ALnlPeDPBC4kmq0DamfBlOVgrL90apH0nfJI9LGYnEu2u8E7yuJrsgNod4dta+LQerm0B7Qa1c+Kb52yxdqufEgOEpPpC7WYcAgiJv/rX/4vPJ4U=' pen = FreeTypePen(None) star(pen) - offset, width, height = (0, 0), 0, 0 - buf1, size = pen.buffer(offset=offset, width=width, height=height, scale=(0.05, 0.05), contain=True) + t = Scale(0.05, 0.05) + width, height = 0, 0 + buf1, size = pen.buffer(width=width, height=height, transform=t, contain=True) buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) self.assertEqual(len(buf1), len(buf2)) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) From e3bbf39a284324347311c3efd539d2a7ed60a896 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Tue, 11 Jan 2022 22:18:52 +0900 Subject: [PATCH 20/25] Move evenOdd to end in kwargs Should put dimension-related arguments into a group. --- Lib/fontTools/pens/freetypePen.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Lib/fontTools/pens/freetypePen.py b/Lib/fontTools/pens/freetypePen.py index d7d4181283..af76c0cba6 100644 --- a/Lib/fontTools/pens/freetypePen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -148,7 +148,7 @@ def outline(self, transform=None, evenOdd=False): (ctypes.c_int)(flags) ) - def buffer(self, width=1000, height=1000, transform=None, evenOdd=False, contain=False): + def buffer(self, width=1000, height=1000, transform=None, contain=False, evenOdd=False): """Renders the current contours within a bitmap buffer. Args: @@ -157,10 +157,10 @@ def buffer(self, width=1000, height=1000, transform=None, evenOdd=False, contain transform: A 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. - evenOdd: 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. + evenOdd: Pass ``True`` for even-odd fill instead of non-zero. Returns: A tuple of ``(buffer, size)``, where ``buffer`` is a ``bytes`` @@ -205,7 +205,7 @@ def buffer(self, width=1000, height=1000, transform=None, evenOdd=False, contain raise FT_Exception(err) return buf.raw, (width, height) - def array(self, width=1000, height=1000, transform=None, evenOdd=False, contain=False): + def array(self, width=1000, height=1000, transform=None, contain=False, evenOdd=False): """Returns the rendered contours as a numpy array. Requires `numpy`. Args: @@ -214,10 +214,10 @@ def array(self, width=1000, height=1000, transform=None, evenOdd=False, contain= transform: A 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. - evenOdd: 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. + evenOdd: Pass ``True`` for even-odd fill instead of non-zero. Returns: A ``numpy.ndarray`` object with a shape of ``(height, width)``. @@ -231,10 +231,10 @@ def array(self, width=1000, height=1000, transform=None, evenOdd=False, contain= (, (1000, 500)) """ import numpy as np - buf, size = self.buffer(width=width, height=height, transform=transform, evenOdd=evenOdd, contain=contain) + 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=1000, height=1000, transform=None, evenOdd=False, contain=False): + def show(self, width=1000, height=1000, transform=None, contain=False, evenOdd=False): """Plots the rendered contours with `pyplot`. Requires `numpy` and `matplotlib`. @@ -244,10 +244,10 @@ def show(self, width=1000, height=1000, transform=None, evenOdd=False, contain=F transform: A 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. - evenOdd: 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. + evenOdd: Pass ``True`` for even-odd fill instead of non-zero. :Example: >> pen = FreeTypePen(None) @@ -255,11 +255,11 @@ def show(self, width=1000, height=1000, transform=None, evenOdd=False, contain=F >> pen.show(width=500, height=1000) """ from matplotlib import pyplot as plt - a = self.array(width=width, height=height, transform=transform, evenOdd=evenOdd, contain=contain) + 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=1000, height=1000, transform=None, evenOdd=False, contain=False): + def image(self, width=1000, height=1000, 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. @@ -269,10 +269,10 @@ def image(self, width=1000, height=1000, transform=None, evenOdd=False, contain= transform: A 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. - evenOdd: 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. + evenOdd: Pass ``True`` for even-odd fill instead of non-zero. Returns: A ``PIL.image`` object. The image is filled in black with alpha @@ -286,7 +286,7 @@ def image(self, width=1000, height=1000, transform=None, evenOdd=False, contain= (, (500, 1000)) """ from PIL import Image - buf, size = self.buffer(width=width, height=height, transform=transform, evenOdd=evenOdd, contain=contain) + 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 From 072b4c8db033e556991d843c686b7722fdd77f51 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Tue, 11 Jan 2022 23:00:38 +0900 Subject: [PATCH 21/25] Fit to contents when image size omitted Eliminates the assumption of any specific metrics from the pen. It still gives some image without giving any parameters, thus it should be a good starting point for new users. --- Lib/fontTools/pens/freetypePen.py | 55 +++++++++++++++++++------------ Tests/pens/freetypePen_test.py | 20 ++++++++++- 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/Lib/fontTools/pens/freetypePen.py b/Lib/fontTools/pens/freetypePen.py index af76c0cba6..60ed304cf7 100644 --- a/Lib/fontTools/pens/freetypePen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -148,12 +148,14 @@ def outline(self, transform=None, evenOdd=False): (ctypes.c_int)(flags) ) - def buffer(self, width=1000, height=1000, 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. Args: - width: Image width of the bitmap in pixels. - height: Image height of the bitmap in pixels. + 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: A 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. @@ -166,7 +168,7 @@ def buffer(self, width=1000, height=1000, transform=None, contain=False, evenOdd 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 = FreeTypePen(None) >> glyph.draw(pen) @@ -177,15 +179,20 @@ def buffer(self, width=1000, height=1000, transform=None, contain=False, evenOdd transform = transform or Transform() if not hasattr(transform, 'transformPoint'): transform = Transform(*transform) - if contain: + 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: 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 = min(-transform.dx, bbox[0]) * -1.0 - dy = min(-transform.dy, bbox[1]) * -1.0 - width = max(width, bbox_size[0]) - height = max(height, bbox_size[1]) + dx, dy = transform.dx, transform.dy + if contain_x: + dx = min(-dx, bbox[0]) * -1.0 + width = max(width, bbox_size[0]) + if contain_y: + dy = min(-dy, bbox[1]) * -1.0 + height = max(height, bbox_size[1]) transform = Transform(*transform[:4], dx, dy) width, height = math.ceil(width), math.ceil(height) buf = ctypes.create_string_buffer(width * height) @@ -205,12 +212,14 @@ def buffer(self, width=1000, height=1000, transform=None, contain=False, evenOdd raise FT_Exception(err) return buf.raw, (width, height) - def array(self, width=1000, height=1000, 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`. Args: - width: Image width of the bitmap in pixels. - height: Image height of the bitmap in pixels. + 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: A 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. @@ -222,7 +231,7 @@ def array(self, width=1000, height=1000, transform=None, contain=False, evenOdd= 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 = FreeTypePen(None) >> glyph.draw(pen) @@ -234,13 +243,15 @@ def array(self, width=1000, height=1000, transform=None, contain=False, evenOdd= 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=1000, height=1000, transform=None, contain=False, evenOdd=False): + 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. - height: Image height of the bitmap in pixels. + 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: A 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. @@ -248,7 +259,7 @@ def show(self, width=1000, height=1000, transform=None, contain=False, evenOdd=F 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. - + :Example: >> pen = FreeTypePen(None) >> glyph.draw(pen) @@ -259,13 +270,15 @@ def show(self, width=1000, height=1000, transform=None, contain=False, evenOdd=F plt.imshow(a, cmap='gray_r', vmin=0, vmax=1) plt.show() - def image(self, width=1000, height=1000, 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`. Can be used to display a glyph image in Jupyter Notebook. Args: - width: Image width of the bitmap in pixels. - height: Image height of the bitmap in pixels. + 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: A 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. @@ -277,7 +290,7 @@ def image(self, width=1000, height=1000, transform=None, contain=False, evenOdd= Returns: A ``PIL.image`` object. The image is filled in black with alpha channel obtained from the rendered bitmap. - + :Example: >> pen = FreeTypePen(None) >> glyph.draw(pen) diff --git a/Tests/pens/freetypePen_test.py b/Tests/pens/freetypePen_test.py index 36e5c2d028..60cfd46985 100644 --- a/Tests/pens/freetypePen_test.py +++ b/Tests/pens/freetypePen_test.py @@ -6,7 +6,7 @@ except ImportError: FREETYPE_PY_AVAILABLE = False -from fontTools.misc.transform import Scale +from fontTools.misc.transform import Scale, Offset def draw_cubic(pen): pen.moveTo((50, 0)) @@ -127,6 +127,24 @@ def test_contain(self): self.assertEqual(len(buf1), len(buf2)) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) + def test_none_width(self): + pen = FreeTypePen(None) + star(pen) + width, height = None, 1000 + buf1, size = pen.buffer(width=width, height=height, transform=Offset(0, 200)) + buf2, _ = pen.buffer(width=1000, height=height, transform=Offset(0, 200)) + self.assertEqual(size, (1000, 1000)) + self.assertEqual(buf1, buf2) + + def test_none_height(self): + pen = FreeTypePen(None) + star(pen) + width, height = 1000, None + buf1, size = pen.buffer(width=width, height=height) + buf2, _ = pen.buffer(width=width, height=1000, transform=Offset(0, 200)) + self.assertEqual(size, (1000, 1000)) + self.assertEqual(buf1, buf2) + if __name__ == '__main__': import sys sys.exit(unittest.main()) From f87f75a4373902aea15106c56e7eb6c29a54bb86 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Wed, 12 Jan 2022 01:05:07 +0900 Subject: [PATCH 22/25] Replace base64-encoded images with PGM files Surprisingly I found Preview.app can still display PGM images. While somewhat legacy, it's a super straightforward format to (de)serialize. The images are scaled to 50x50 px and only consume 5KB in total. Makes more sense to human being than the previous base64-encoded zlib compressed data, plus we don't have to add pillow as a dependency. --- Tests/pens/data/test_even_odd_fill.pgm | Bin 0 -> 2513 bytes Tests/pens/data/test_non_zero_fill.pgm | Bin 0 -> 2513 bytes Tests/pens/freetypePen_test.py | 77 ++++++++++++++----------- 3 files changed, 43 insertions(+), 34 deletions(-) create mode 100644 Tests/pens/data/test_even_odd_fill.pgm create mode 100644 Tests/pens/data/test_non_zero_fill.pgm diff --git a/Tests/pens/data/test_even_odd_fill.pgm b/Tests/pens/data/test_even_odd_fill.pgm new file mode 100644 index 0000000000000000000000000000000000000000..70130f33974b541fa081a0addf5e7690a3b812c4 GIT binary patch literal 2513 zcmb7Gze^lJ6dv^uIV9%5N=VScBNl=Y*2W^2B57>2kr=SDvk??>m7SI3wDb=Ulh`Dc zjZLnwv9YjFG+<$?oH;~9Pkqjt+4=Ey=Pm0rGxL4#+nt;5y|*_R6{C^Es5oKB5L`w9zzb|&(ux<@Ue}8JwwGF3k>7S{xE{G}mxk++*LE~qkG!@k!?h%* z?aFYi2-!Y&8=t^UgU;00O2@(NoESax_P8X;agrd<-pj|8b|^|t;++tqc1r5~ZOf0o zhWuLZ762ZX+suJKUpfiB(ZZEuE}d>!mp>bWcnHfnN8kS_`2-tg>@C5-Q-kVQfdkcYCQoh~8ZZC)!0EBMpCAX-^7-XkU3^zU>ugOr+xx enP`N+?X`n}ut11`1JYVeShdTn(sS20hdl#d;V|?7 literal 0 HcmV?d00001 diff --git a/Tests/pens/data/test_non_zero_fill.pgm b/Tests/pens/data/test_non_zero_fill.pgm new file mode 100644 index 0000000000000000000000000000000000000000..698f6424e3ce23a7523326962881faf650c3c920 GIT binary patch literal 2513 zcmb7`J5K^Z6ooHB4B0~6#8#t)bR=l9wql_Tjg*>L*-=U&6&)23TYdmBc3RokXiGyu zW7LGgRC*5idZ7{H$~XT?hX`TA3G0RE$i5M;A+{%E&^A}Hg*}fLM&sKfhz)w{pv6H1eV+1 z6&!RdXXRLDpWh?yvpw?c%9|=y9@#OYaxA 38dB. See also: # Peak signal-to-noise ratio # https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio @@ -45,27 +72,12 @@ def psnr(b1, b2): @unittest.skipUnless(FREETYPE_PY_AVAILABLE, "freetype-py not installed") class FreeTypePenTest(unittest.TestCase): def test_draw(self): - import base64, zlib - ZLIB_B64_BIN = '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' pen = FreeTypePen(None) - draw_cubic(pen) + box(pen) width, height = 500, 500 buf1, _ = pen.buffer(width=width, height=height) - buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) - self.assertEqual(len(buf1), len(buf2)) - self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) - - def test_scale(self): - import base64, zlib - ZLIB_B64_BIN = 'eJy91r8NgkAUx/FLMHECCLMQC5gGK3UB4gJEXABhAbTAJXAJ7GkoBIornrQif/w2vvo+yXH37vFTqi/5rKYs8jhwDDVdMlp15ttM9NVFE2ZSiLShCYVI5VIhekeFSLKmQhIsZLixZaFdKqQyqZAQi9amQiIsOpsK8bHIsKgNKsTBIsAixiLHosCixKLB4vWHXfEv56fLb5B3Ce5E3u1XRQV+tXwy4OnDJxyeopUFhfYUFHsFRaqgSOGfUx+G65cSgPcNZlPGyRoBM0nmjJJMfdv+mpaa5+N+OW5W44vfouHQiw==' - pen = FreeTypePen(None) - draw_cubic(pen) - t = Scale(0.1, 0.1) - width, height = t.transformPoint((500, 500)) - buf1, size = pen.buffer(width=width, height=height, transform=t) - buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) - self.assertEqual(size, (50, 50)) - self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) + buf2 = b'\xff' * width * height + self.assertEqual(buf1, buf2) def test_empty(self): pen = FreeTypePen(None) @@ -80,29 +92,27 @@ def test_bbox_and_cbox(self): self.assertEqual(pen.cbox, (50.0, 0.0, 450.0, 500.0)) def test_non_zero_fill(self): - import base64, zlib - ZLIB_B64_BIN = 'eJzt2L0NglAUhmESTZxA4yzGAqfRSl3AuABRF/BnAbTAJXAJ7GkoBAqKo4WNhbk3OZ4vknzvAk/g/nC5QcAYY4wxxhhrRfJZmaXJfjXqWBrving6tDZe1dufKV8NkSrqmxsieWhvSDO3N0SOPXtDjgBD9K/LbTShvSG5dgp7GBIBjEq54n0M2QKMWvcgXoZMAUYMMArVR8vPkBHAWAGMPcBIAEYKMDKAUQKMB8BAvCvEmCPmLmINIvYSwJ6I2NvPGuJ/vrWIMwPg7IM4wwHOovnA3GgmSsLDWGgJt3FSE07jZP7P2Sz1gusOQD3cLqPaaCety6h3xncyxWVmd7dU3m/Xw3rc/R3AGGOMsbb3BMrP0Is=' pen = FreeTypePen(None) - draw_cubic(pen) - t = Scale(0.1, 0.1) + star(pen) + t = Scale(0.05, 0.05) width, height = t.transformPoint((1000, 1000)) t = t.translate(0, 200) - buf1, size = pen.buffer(width=width, height=height, transform=t, evenOdd=False) - buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) + 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')) self.assertEqual(len(buf1), len(buf2)) + self.assertEqual(size1, size2) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) def test_even_odd_fill(self): - import base64, zlib - ZLIB_B64_BIN = 'eJzt2L0NglAUhmESTZxA4yzGAqfRSl3AuABRF/BnAbTAJXAJ7GkoBAqKo4WNhbk3OZ4vknzvAk/g/nC5QcAYY4wxxhhrRfJZmaXJfjXqWBrving6tDZe1dufKV8NkSrqmxsieWhvSDO3N0SOPXtDjgBD9K/LbTShvSG5dgp7GBIBjEq54n0M2QKMWvcgXoZMAUYMMArVR8vPkBHAWAGMPcBIAEYKMDKAUQKMB8BAvCvEmCPmLmINIvYSwJ6I2NvPGuJ/vrWIMwPg7IM4wwHOovnA3GgmSsLDWGgJt3FSE07jZP7P2Sz1gusOQD3cLqPaaCety6h3xncyxWVmd7dU3m/Xw3rc/R3AGGOMsbb3BMrP0Is=' pen = FreeTypePen(None) - draw_cubic(pen) - t = Scale(0.1, 0.1) + star(pen) + t = Scale(0.05, 0.05) width, height = t.transformPoint((1000, 1000)) t = t.translate(0, 200) - buf1, size = pen.buffer(width=width, height=height, transform=t, evenOdd=True) - buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) + 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')) self.assertEqual(len(buf1), len(buf2)) + self.assertEqual(size1, size2) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) def test_cubic_vs_quadratic(self): @@ -116,15 +126,14 @@ def test_cubic_vs_quadratic(self): self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) def test_contain(self): - import base64, zlib - ZLIB_B64_BIN = 'eJyVlKtvAkEQh5dHCEG0vQTbJgjOIVqB7VW0BoUsCQqHLElP4VCkV8sfQBpcqzCYYjEIDClNEFiaSxCEEB7bO/aWO2Bn2fmp22G+3ONjhhBxdB34AUzlBUt0v5GAtlpd4YgCpc84okXpBwqI2pTaUQxhUCf3GMJyiTcMMXKJHwSg010Q2iuMQGjvMkJdu7ZihLr2AvWirL3FCVXtrnAWVe0G3UdRu+UTIu2lBUVlUSJ3YwwwvnXuorXVgba2e7BQdaPWv6mG+Ms8/akA08fA+9/0zgO964NPFmucAxqx489cnMv650WBmcwvDIwyQteXXxDweQH9P8y1qH9tQv1OHkSEIQEIeT8FLCnAJzwY+bTzCQ9GPu2FU+DMtLdEhGza/QkPRjbthgiQTntgwodD/1qy5Ef7pnokUt8f4CWv85ZZ3j3mZ/wMLnlvpdNBmp3TA68ALnlPeDPBC4kmq0DamfBlOVgrL90apH0nfJI9LGYnEu2u8E7yuJrsgNod4dta+LQerm0B7Qa1c+Kb52yxdqufEgOEpPpC7WYcAgiJv/rX/4vPJ4U=' pen = FreeTypePen(None) star(pen) t = Scale(0.05, 0.05) width, height = 0, 0 - buf1, size = pen.buffer(width=width, height=height, transform=t, contain=True) - buf2 = zlib.decompress(base64.b64decode(ZLIB_B64_BIN)) + 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')) self.assertEqual(len(buf1), len(buf2)) + self.assertEqual(size1, size2) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) def test_none_width(self): From d181ecaaaf1f7ff6876c8219a54f3da9953fb137 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Wed, 12 Jan 2022 01:26:51 +0900 Subject: [PATCH 23/25] Fix warnings when generating docs --- Doc/source/pens/freetypePen.rst | 4 ++-- Lib/fontTools/pens/freetypePen.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/source/pens/freetypePen.rst b/Doc/source/pens/freetypePen.rst index 10b99a8857..9b849a214f 100644 --- a/Doc/source/pens/freetypePen.rst +++ b/Doc/source/pens/freetypePen.rst @@ -1,6 +1,6 @@ -########## +########### freetypePen -########## +########### .. automodule:: fontTools.pens.freetypePen :inherited-members: diff --git a/Lib/fontTools/pens/freetypePen.py b/Lib/fontTools/pens/freetypePen.py index 60ed304cf7..26b6fd47ae 100644 --- a/Lib/fontTools/pens/freetypePen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -166,7 +166,7 @@ def buffer(self, width=None, height=None, transform=None, contain=False, evenOdd Returns: A tuple of ``(buffer, size)``, where ``buffer`` is a ``bytes`` - 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. :Example: From 6c5faa6be9993bc3b0354630ff7080fbadd5fcc6 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Wed, 12 Jan 2022 21:48:54 +0900 Subject: [PATCH 24/25] Add description to requirements section --- README.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.rst b/README.rst index 7e1f117c23..7b3f99597f 100644 --- a/README.rst +++ b/README.rst @@ -199,6 +199,13 @@ are required to unlock the extra features named "ufo", etc. * `reportlab `__: Python toolkit for generating PDFs and graphics. +- ``Lib/fontTools/pens/freetypePen.py`` + + Pen to drawing glyphs with FreeType as raster images, requires: + + * `freetype-py `__: Python binding + for the FreeType library. + How to make a new release ~~~~~~~~~~~~~~~~~~~~~~~~~ From c0c354ece201d15731d3a06ed74fb95a671f0ba6 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Wed, 12 Jan 2022 21:50:04 +0900 Subject: [PATCH 25/25] Add name to acknowledgements --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 7b3f99597f..6a35807cf4 100644 --- a/README.rst +++ b/README.rst @@ -245,9 +245,9 @@ aschmitz, Olivier Berten, Samyak Bhuta, Erik van Blokland, Petr van Blokland, Jelle Bosma, Sascha Brawer, Tom Byrer, Antonio Cavedoni, Frédéric Coiffier, Vincent Connare, David Corbett, Simon Cozens, Dave Crossland, Simon Daniels, Peter Dekkers, Behdad Esfahbod, Behnam Esfahbod, Hannes -Famira, Sam Fishman, Matt Fontaine, Yannis Haralambous, Greg Hitchcock, -Jeremie Hornus, Khaled Hosny, John Hudson, Denis Moyogo Jacquerye, Jack -Jansen, Tom Kacvinsky, Jens Kutilek, Antoine Leca, Werner Lemberg, Tal +Famira, Sam Fishman, Matt Fontaine, Takaaki Fuji, Yannis Haralambous, Greg +Hitchcock, Jeremie Hornus, Khaled Hosny, John Hudson, Denis Moyogo Jacquerye, +Jack Jansen, Tom Kacvinsky, Jens Kutilek, Antoine Leca, Werner Lemberg, Tal Leming, Peter Lofting, Cosimo Lupo, Masaya Nakamura, Dave Opstad, Laurence Penney, Roozbeh Pournader, Garret Rieger, Read Roberts, Guido van Rossum, Just van Rossum, Andreas Seidel, Georg Seifert, Chris