From a27789c477f6d208710b03a7baa01227d3856e07 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Sat, 22 Jan 2022 07:00:45 +0900 Subject: [PATCH 1/6] freetypePen: handle rotate/skew transform The pen is designed to determine the bitmap size when omitted, which helps users to see an image somehow even when they have no idea how the arguments should be passed. And I realised that I didn't give enough thought to rotate/skew transforms in former PRs. This commit fixes the calculation of the bbox after transformation. Also tries to clear up how the autosizing options work in the docstring. Some minor fixes will follow. --- Lib/fontTools/pens/freetypePen.py | 84 +++++++++++++++++++++++------- Tests/pens/data/test_rotate.pgm | Bin 0 -> 1309 bytes Tests/pens/data/test_skew.pgm | Bin 0 -> 1263 bytes Tests/pens/freetypePen_test.py | 79 +++++++++++++++++++++++----- 4 files changed, 132 insertions(+), 31 deletions(-) create mode 100644 Tests/pens/data/test_rotate.pgm create mode 100644 Tests/pens/data/test_skew.pgm diff --git a/Lib/fontTools/pens/freetypePen.py b/Lib/fontTools/pens/freetypePen.py index 1cb171b044..705614f3ee 100644 --- a/Lib/fontTools/pens/freetypePen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -114,7 +114,7 @@ def outline(self, transform=None, evenOdd=False): """Converts the current contours to ``FT_Outline``. Args: - transform: A optional 6-tuple containing an affine transformation, + transform: An optional 6-tuple containing an affine transformation, or a ``Transform`` object from the ``fontTools.misc.transform`` module. evenOdd: Pass ``True`` for even-odd fill instead of non-zero. @@ -156,7 +156,7 @@ def buffer(self, width=None, height=None, transform=None, contain=False, evenOdd 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, + transform: An optional 6-tuple containing an affine transformation, or a ``Transform`` object from the ``fontTools.misc.transform`` module. The bitmap size is not affected by this matrix. contain: If ``True``, the image size will be automatically expanded @@ -169,33 +169,49 @@ def buffer(self, width=None, height=None, transform=None, contain=False, evenOdd object of the resulted bitmap and ``size`` is a 2-tuple of its dimension. + :Notes: + The image size should always be given explicitly if you need to get + a proper glyph image. When ``width`` and ``height`` are omitted, it + forcifully fits to the bounding box and the side bearings get + cropped. If you pass ``0`` to both ``width`` and ``height`` and set + ``contain`` to ``True``, it expands to the bounding box while + maintaining the origin of the contours, meaning that LSB will be + maintained but RSB won’t. The difference between the two becomes + more obvious when rotate or skew transformation is applied. + :Example: .. code-block:: - + >> pen = FreeTypePen(None) >> glyph.draw(pen) >> buf, size = pen.buffer(width=500, height=1000) >> type(buf), len(buf), size (, 500000, (500, 1000)) - + """ transform = transform or Transform() if not hasattr(transform, 'transformPoint'): transform = Transform(*transform) contain_x, contain_y = contain or width is None, contain or height is None - 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, dy = transform.dx, transform.dy + bbox = self.bbox + p1, p2, p3, p4 = transform.transformPoint((bbox[0], bbox[1])), transform.transformPoint((bbox[2], bbox[1])), transform.transformPoint((bbox[0], bbox[3])), transform.transformPoint((bbox[2], bbox[3])) + px, py = (p1[0], p2[0], p3[0], p4[0]), (p1[1], p2[1], p3[1], p4[1]) if contain_x: - dx = min(-dx, bbox[0]) * -1.0 - width = max(width, bbox_size[0]) + if width is None: + dx = dx - min(*px) + width = max(*px) - min(*px) + else: + dx = dx - min(min(*px), 0.0) + width = max(width, max(*px) - min(min(*px), 0.0)) if contain_y: - dy = min(-dy, bbox[1]) * -1.0 - height = max(height, bbox_size[1]) + if height is None: + dy = dy - min(*py) + height = max(*py) - min(*py) + else: + dy = dy - min(min(*py), 0.0) + height = max(height, max(*py) - min(min(*py), 0.0)) transform = Transform(*transform[:4], dx, dy) width, height = math.ceil(width), math.ceil(height) buf = ctypes.create_string_buffer(width * height) @@ -223,7 +239,7 @@ def array(self, width=None, height=None, transform=None, contain=False, evenOdd= 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, + transform: An optional 6-tuple containing an affine transformation, or a ``Transform`` object from the ``fontTools.misc.transform`` module. The bitmap size is not affected by this matrix. contain: If ``True``, the image size will be automatically expanded @@ -235,9 +251,19 @@ def array(self, width=None, height=None, transform=None, contain=False, evenOdd= A ``numpy.ndarray`` object with a shape of ``(height, width)``. Each element takes a value in the range of ``[0.0, 1.0]``. + :Notes: + The image size should always be given explicitly if you need to get + a proper glyph image. When ``width`` and ``height`` are omitted, it + forcifully fits to the bounding box and the side bearings get + cropped. If you pass ``0`` to both ``width`` and ``height`` and set + ``contain`` to ``True``, it expands to the bounding box while + maintaining the origin of the contours, meaning that LSB will be + maintained but RSB won’t. The difference between the two becomes + more obvious when rotate or skew transformation is applied. + :Example: .. code-block:: - + >> pen = FreeTypePen(None) >> glyph.draw(pen) >> arr = pen.array(width=500, height=1000) @@ -257,7 +283,7 @@ def show(self, width=None, height=None, transform=None, contain=False, evenOdd=F 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, + transform: An optional 6-tuple containing an affine transformation, or a ``Transform`` object from the ``fontTools.misc.transform`` module. The bitmap size is not affected by this matrix. contain: If ``True``, the image size will be automatically expanded @@ -265,9 +291,19 @@ def show(self, width=None, height=None, transform=None, contain=False, evenOdd=F rendering glyphs with negative sidebearings without clipping. evenOdd: Pass ``True`` for even-odd fill instead of non-zero. + :Notes: + The image size should always be given explicitly if you need to get + a proper glyph image. When ``width`` and ``height`` are omitted, it + forcifully fits to the bounding box and the side bearings get + cropped. If you pass ``0`` to both ``width`` and ``height`` and set + ``contain`` to ``True``, it expands to the bounding box while + maintaining the origin of the contours, meaning that LSB will be + maintained but RSB won’t. The difference between the two becomes + more obvious when rotate or skew transformation is applied. + :Example: .. code-block:: - + >> pen = FreeTypePen(None) >> glyph.draw(pen) >> pen.show(width=500, height=1000) @@ -286,7 +322,7 @@ def image(self, width=None, height=None, transform=None, contain=False, evenOdd= 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, + transform: An optional 6-tuple containing an affine transformation, or a ``Transform`` object from the ``fontTools.misc.transform`` module. The bitmap size is not affected by this matrix. contain: If ``True``, the image size will be automatically expanded @@ -298,9 +334,19 @@ def image(self, width=None, height=None, transform=None, contain=False, evenOdd= A ``PIL.image`` object. The image is filled in black with alpha channel obtained from the rendered bitmap. + :Notes: + The image size should always be given explicitly if you need to get + a proper glyph image. When ``width`` and ``height`` are omitted, it + forcifully fits to the bounding box and the side bearings get + cropped. If you pass ``0`` to both ``width`` and ``height`` and set + ``contain`` to ``True``, it expands to the bounding box while + maintaining the origin of the contours, meaning that LSB will be + maintained but RSB won’t. The difference between the two becomes + more obvious when rotate or skew transformation is applied. + :Example: .. code-block:: - + >> pen = FreeTypePen(None) >> glyph.draw(pen) >> img = pen.image(width=500, height=1000) diff --git a/Tests/pens/data/test_rotate.pgm b/Tests/pens/data/test_rotate.pgm new file mode 100644 index 0000000000000000000000000000000000000000..14edd9fc809faa6dbba9a6b993b7eebea30af598 GIT binary patch literal 1309 zcmaje%@Knj5Cve)98v={#KD77M5;t%R1cV8TC}Gf&YgN3+J#mCg;t4_$0Z1k{ z(#VB$(vU?e+VGt)yAobMCWcW-&Kal@1D!-gohYcR&mSRlf<%~v35zIUPzGIkE7B=1 d{RMm?#1nL!L;#Y>jWlv0oit>TKibBRJ^)ePaR&eZ literal 0 HcmV?d00001 diff --git a/Tests/pens/data/test_skew.pgm b/Tests/pens/data/test_skew.pgm new file mode 100644 index 0000000000000000000000000000000000000000..5ba4a067195f3400330f4646dbfbc8091b99b8dc GIT binary patch literal 1263 ycmWGA Date: Sat, 22 Jan 2022 07:06:12 +0900 Subject: [PATCH 2/6] freetypePen: raise PenError when missing moveTo --- Lib/fontTools/pens/freetypePen.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/pens/freetypePen.py b/Lib/fontTools/pens/freetypePen.py index 705614f3ee..8670ccab17 100644 --- a/Lib/fontTools/pens/freetypePen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -18,7 +18,7 @@ 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.pens.basePen import BasePen, PenError from fontTools.misc.roundTools import otRound from fontTools.misc.transform import Transform @@ -390,11 +390,15 @@ def _moveTo(self, pt): contour.tags.append(LINE) def _lineTo(self, pt): + if not (self.contours and len(self.contours[-1].points) > 0): + raise PenError('Contour missing required initial moveTo') contour = self.contours[-1] contour.points.append(pt) contour.tags.append(LINE) def _curveToOne(self, p1, p2, p3): + if not (self.contours and len(self.contours[-1].points) > 0): + raise PenError('Contour missing required initial moveTo') t1, t2, t3 = OFFCURVE, OFFCURVE, CURVE contour = self.contours[-1] for p, t in ((p1, t1), (p2, t2), (p3, t3)): @@ -402,6 +406,8 @@ def _curveToOne(self, p1, p2, p3): contour.tags.append(t) def _qCurveToOne(self, p1, p2): + if not (self.contours and len(self.contours[-1].points) > 0): + raise PenError('Contour missing required initial moveTo') t1, t2 = QOFFCURVE, QCURVE contour = self.contours[-1] for p, t in ((p1, t1), (p2, t2)): From 9eadd908c6ce4c2846f1db3fe7d248c847ab8047 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Sat, 22 Jan 2022 07:08:57 +0900 Subject: [PATCH 3/6] freetypePen: fix bits for cubic on-curve --- Lib/fontTools/pens/freetypePen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/pens/freetypePen.py b/Lib/fontTools/pens/freetypePen.py index 8670ccab17..e8c1f9812c 100644 --- a/Lib/fontTools/pens/freetypePen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -24,7 +24,7 @@ Contour = collections.namedtuple('Contour', ('points', 'tags')) LINE = 0b00000001 -CURVE = 0b00000011 +CURVE = 0b00000001 OFFCURVE = 0b00000010 QCURVE = 0b00000001 QOFFCURVE = 0b00000000 From cd5c16f4d4b3491baa04d5d889df52df03ebba66 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Sat, 22 Jan 2022 07:09:35 +0900 Subject: [PATCH 4/6] freetypePen: cosmetic changes --- Lib/fontTools/pens/freetypePen.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/pens/freetypePen.py b/Lib/fontTools/pens/freetypePen.py index e8c1f9812c..832896503b 100644 --- a/Lib/fontTools/pens/freetypePen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -122,7 +122,7 @@ def outline(self, transform=None, evenOdd=False): transform = transform or Transform() if not hasattr(transform, 'transformPoint'): transform = Transform(*transform) - nContours = len(self.contours) + n_contours = len(self.contours) n_points = sum((len(contour.points) for contour in self.contours)) points = [] for contour in self.contours: @@ -140,11 +140,11 @@ def outline(self, transform=None, evenOdd=False): contours.append(contours_sum - 1) flags = FT_OUTLINE_EVEN_ODD_FILL if evenOdd else FT_OUTLINE_NONE return FT_Outline( - (ctypes.c_short)(nContours), + (ctypes.c_short)(n_contours), (ctypes.c_short)(n_points), (FT_Vector * n_points)(*points), (ctypes.c_ubyte * n_points)(*tags), - (ctypes.c_short * nContours)(*contours), + (ctypes.c_short * n_contours)(*contours), (ctypes.c_int)(flags) ) From 48cbe7b054c22150b11e483d1c4ace0294394f9c Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Mon, 31 Jan 2022 20:55:33 +0900 Subject: [PATCH 5/6] freetypePen: prefer FT_CURVE_TAG enums --- Lib/fontTools/pens/freetypePen.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Lib/fontTools/pens/freetypePen.py b/Lib/fontTools/pens/freetypePen.py index 832896503b..5ce0cede42 100644 --- a/Lib/fontTools/pens/freetypePen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -15,7 +15,7 @@ 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_enums import FT_OUTLINE_NONE, FT_OUTLINE_EVEN_ODD_FILL, FT_PIXEL_MODE_GRAY, FT_CURVE_TAG_ON, FT_CURVE_TAG_CONIC, FT_CURVE_TAG_CUBIC from freetype.ft_errors import FT_Exception from fontTools.pens.basePen import BasePen, PenError @@ -23,11 +23,6 @@ from fontTools.misc.transform import Transform Contour = collections.namedtuple('Contour', ('points', 'tags')) -LINE = 0b00000001 -CURVE = 0b00000001 -OFFCURVE = 0b00000010 -QCURVE = 0b00000001 -QOFFCURVE = 0b00000000 class FreeTypePen(BasePen): """Pen to rasterize paths with FreeType. Requires `freetype-py` module. @@ -387,19 +382,19 @@ def _moveTo(self, pt): contour = Contour([], []) self.contours.append(contour) contour.points.append(pt) - contour.tags.append(LINE) + contour.tags.append(FT_CURVE_TAG_ON) def _lineTo(self, pt): if not (self.contours and len(self.contours[-1].points) > 0): raise PenError('Contour missing required initial moveTo') contour = self.contours[-1] contour.points.append(pt) - contour.tags.append(LINE) + contour.tags.append(FT_CURVE_TAG_ON) def _curveToOne(self, p1, p2, p3): if not (self.contours and len(self.contours[-1].points) > 0): raise PenError('Contour missing required initial moveTo') - t1, t2, t3 = OFFCURVE, OFFCURVE, CURVE + t1, t2, t3 = FT_CURVE_TAG_CUBIC, FT_CURVE_TAG_CUBIC, FT_CURVE_TAG_ON contour = self.contours[-1] for p, t in ((p1, t1), (p2, t2), (p3, t3)): contour.points.append(p) @@ -408,7 +403,7 @@ def _curveToOne(self, p1, p2, p3): def _qCurveToOne(self, p1, p2): if not (self.contours and len(self.contours[-1].points) > 0): raise PenError('Contour missing required initial moveTo') - t1, t2 = QOFFCURVE, QCURVE + t1, t2 = FT_CURVE_TAG_CONIC, FT_CURVE_TAG_ON contour = self.contours[-1] for p, t in ((p1, t1), (p2, t2)): contour.points.append(p) From 5f2c492635d046be53395a6a19098a92a6145b01 Mon Sep 17 00:00:00 2001 From: Takaaki Fuji Date: Mon, 31 Jan 2022 20:56:03 +0900 Subject: [PATCH 6/6] freetypePen: format code with Black --- Lib/fontTools/pens/freetypePen.py | 100 ++++++++++++++++++++++-------- Tests/pens/freetypePen_test.py | 79 ++++++++++++++--------- 2 files changed, 124 insertions(+), 55 deletions(-) diff --git a/Lib/fontTools/pens/freetypePen.py b/Lib/fontTools/pens/freetypePen.py index 5ce0cede42..870776bc7b 100644 --- a/Lib/fontTools/pens/freetypePen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -2,7 +2,7 @@ """Pen to rasterize paths with FreeType.""" -__all__ = ['FreeTypePen'] +__all__ = ["FreeTypePen"] import os import ctypes @@ -15,14 +15,22 @@ from freetype.raw import FT_Outline_Get_Bitmap, FT_Outline_Get_BBox, FT_Outline_Get_CBox from freetype.ft_types import FT_Pos from freetype.ft_structs import FT_Vector, FT_BBox, FT_Bitmap, FT_Outline -from freetype.ft_enums import FT_OUTLINE_NONE, FT_OUTLINE_EVEN_ODD_FILL, FT_PIXEL_MODE_GRAY, FT_CURVE_TAG_ON, FT_CURVE_TAG_CONIC, FT_CURVE_TAG_CUBIC +from freetype.ft_enums import ( + FT_OUTLINE_NONE, + FT_OUTLINE_EVEN_ODD_FILL, + FT_PIXEL_MODE_GRAY, + FT_CURVE_TAG_ON, + FT_CURVE_TAG_CONIC, + FT_CURVE_TAG_CUBIC, +) from freetype.ft_errors import FT_Exception from fontTools.pens.basePen import BasePen, PenError from fontTools.misc.roundTools import otRound from fontTools.misc.transform import Transform -Contour = collections.namedtuple('Contour', ('points', 'tags')) +Contour = collections.namedtuple("Contour", ("points", "tags")) + class FreeTypePen(BasePen): """Pen to rasterize paths with FreeType. Requires `freetype-py` module. @@ -115,15 +123,19 @@ def outline(self, transform=None, evenOdd=False): evenOdd: Pass ``True`` for even-odd fill instead of non-zero. """ transform = transform or Transform() - if not hasattr(transform, 'transformPoint'): + if not hasattr(transform, "transformPoint"): transform = Transform(*transform) n_contours = 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: point = transform.transformPoint(point) - points.append(FT_Vector(FT_Pos(otRound(point[0] * 64)), FT_Pos(otRound(point[1] * 64)))) + 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: @@ -137,13 +149,15 @@ def outline(self, transform=None, evenOdd=False): return FT_Outline( (ctypes.c_short)(n_contours), (ctypes.c_short)(n_points), - (FT_Vector * n_points)(*points), + (FT_Vector * n_points)(*points), (ctypes.c_ubyte * n_points)(*tags), (ctypes.c_short * n_contours)(*contours), - (ctypes.c_int)(flags) + (ctypes.c_int)(flags), ) - def buffer(self, width=None, height=None, 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: @@ -185,13 +199,18 @@ def buffer(self, width=None, height=None, transform=None, contain=False, evenOdd """ transform = transform or Transform() - if not hasattr(transform, 'transformPoint'): + if not hasattr(transform, "transformPoint"): transform = Transform(*transform) contain_x, contain_y = contain or width is None, contain or height is None if contain_x or contain_y: dx, dy = transform.dx, transform.dy bbox = self.bbox - p1, p2, p3, p4 = transform.transformPoint((bbox[0], bbox[1])), transform.transformPoint((bbox[2], bbox[1])), transform.transformPoint((bbox[0], bbox[3])), transform.transformPoint((bbox[2], bbox[3])) + p1, p2, p3, p4 = ( + transform.transformPoint((bbox[0], bbox[1])), + transform.transformPoint((bbox[2], bbox[1])), + transform.transformPoint((bbox[0], bbox[3])), + transform.transformPoint((bbox[2], bbox[3])), + ) px, py = (p1[0], p2[0], p3[0], p4[0]), (p1[1], p2[1], p3[1], p4[1]) if contain_x: if width is None: @@ -218,15 +237,19 @@ def buffer(self, width=None, height=None, transform=None, contain=False, evenOdd (ctypes.c_short)(256), (ctypes.c_ubyte)(FT_PIXEL_MODE_GRAY), (ctypes.c_char)(0), - (ctypes.c_void_p)(None) + (ctypes.c_void_p)(None), ) outline = self.outline(transform=transform, evenOdd=evenOdd) - err = FT_Outline_Get_Bitmap(freetype.get_handle(), ctypes.byref(outline), ctypes.byref(bitmap)) + err = FT_Outline_Get_Bitmap( + freetype.get_handle(), ctypes.byref(outline), ctypes.byref(bitmap) + ) if err != 0: raise FT_Exception(err) return buf.raw, (width, height) - def array(self, width=None, height=None, transform=None, contain=False, evenOdd=False): + def array( + self, width=None, height=None, transform=None, contain=False, evenOdd=False + ): """Returns the rendered contours as a numpy array. Requires `numpy`. Args: @@ -266,10 +289,19 @@ def array(self, width=None, height=None, transform=None, contain=False, evenOdd= (, (1000, 500)) """ import numpy as np - buf, size = self.buffer(width=width, height=height, transform=transform, contain=contain, evenOdd=evenOdd) - return np.frombuffer(buf, 'B').reshape((size[1], size[0])) / 255.0 - def show(self, width=None, height=None, transform=None, contain=False, evenOdd=False): + buf, size = self.buffer( + width=width, + height=height, + transform=transform, + contain=contain, + evenOdd=evenOdd, + ) + return np.frombuffer(buf, "B").reshape((size[1], size[0])) / 255.0 + + def show( + self, width=None, height=None, transform=None, contain=False, evenOdd=False + ): """Plots the rendered contours with `pyplot`. Requires `numpy` and `matplotlib`. @@ -304,11 +336,20 @@ def show(self, width=None, height=None, transform=None, contain=False, evenOdd=F >> pen.show(width=500, height=1000) """ from matplotlib import pyplot as plt - a = self.array(width=width, height=height, transform=transform, contain=contain, evenOdd=evenOdd) - plt.imshow(a, cmap='gray_r', vmin=0, vmax=1) + + a = self.array( + width=width, + height=height, + transform=transform, + contain=contain, + evenOdd=evenOdd, + ) + plt.imshow(a, cmap="gray_r", vmin=0, vmax=1) plt.show() - def image(self, width=None, height=None, transform=None, contain=False, evenOdd=False): + 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. @@ -349,9 +390,16 @@ def image(self, width=None, height=None, transform=None, contain=False, evenOdd= (, (500, 1000)) """ from PIL import Image - buf, size = self.buffer(width=width, height=height, transform=transform, contain=contain, evenOdd=evenOdd) - img = Image.new('L', size, 0) - img.putalpha(Image.frombuffer('L', size, buf)) + + buf, size = self.buffer( + width=width, + height=height, + transform=transform, + contain=contain, + evenOdd=evenOdd, + ) + img = Image.new("L", size, 0) + img.putalpha(Image.frombuffer("L", size, buf)) return img @property @@ -386,14 +434,14 @@ def _moveTo(self, pt): def _lineTo(self, pt): if not (self.contours and len(self.contours[-1].points) > 0): - raise PenError('Contour missing required initial moveTo') + raise PenError("Contour missing required initial moveTo") contour = self.contours[-1] contour.points.append(pt) contour.tags.append(FT_CURVE_TAG_ON) def _curveToOne(self, p1, p2, p3): if not (self.contours and len(self.contours[-1].points) > 0): - raise PenError('Contour missing required initial moveTo') + raise PenError("Contour missing required initial moveTo") t1, t2, t3 = FT_CURVE_TAG_CUBIC, FT_CURVE_TAG_CUBIC, FT_CURVE_TAG_ON contour = self.contours[-1] for p, t in ((p1, t1), (p2, t2), (p3, t3)): @@ -402,7 +450,7 @@ def _curveToOne(self, p1, p2, p3): def _qCurveToOne(self, p1, p2): if not (self.contours and len(self.contours[-1].points) > 0): - raise PenError('Contour missing required initial moveTo') + raise PenError("Contour missing required initial moveTo") t1, t2 = FT_CURVE_TAG_CONIC, FT_CURVE_TAG_ON contour = self.contours[-1] for p, t in ((p1, t1), (p2, t2)): diff --git a/Tests/pens/freetypePen_test.py b/Tests/pens/freetypePen_test.py index c194b82f68..b6edc8bb9e 100644 --- a/Tests/pens/freetypePen_test.py +++ b/Tests/pens/freetypePen_test.py @@ -4,21 +4,24 @@ try: from fontTools.pens.freetypePen import FreeTypePen + FREETYPE_PY_AVAILABLE = True except ImportError: FREETYPE_PY_AVAILABLE = False from fontTools.misc.transform import Scale, Offset -DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data') +DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data") + def box(pen, offset=(0, 0)): - pen.moveTo((0 + offset[0], 0 + offset[1])) - pen.lineTo((0 + offset[0], 500 + offset[1])) + pen.moveTo((0 + offset[0], 0 + offset[1])) + pen.lineTo((0 + offset[0], 500 + offset[1])) pen.lineTo((500 + offset[0], 500 + offset[1])) - pen.lineTo((500 + offset[0], 0 + offset[1])) + pen.lineTo((500 + offset[0], 0 + offset[1])) pen.closePath() + def draw_cubic(pen): pen.moveTo((50, 0)) pen.lineTo((50, 500)) @@ -27,6 +30,7 @@ def draw_cubic(pen): pen.curveTo((450, 100), (350, 0), (200, 0)) pen.closePath() + def draw_quadratic(pen): pen.moveTo((50, 0)) pen.lineTo((50, 500)) @@ -35,6 +39,7 @@ def draw_quadratic(pen): pen.qCurveTo((450, 176), (388, 62), (274, 0), (200, 0)) pen.closePath() + def star(pen): pen.moveTo((0, 420)) pen.lineTo((1000, 420)) @@ -43,32 +48,38 @@ def star(pen): pen.lineTo((800, -200)) pen.closePath() + # For the PGM format, see the following resources: # https://en.wikipedia.org/wiki/Netpbm # http://netpbm.sourceforge.net/doc/pgm.html def load_pgm(filename): - with open(filename, 'rb') as fp: - assert fp.readline() == 'P5\n'.encode() - w, h = (int(c) for c in fp.readline().decode().rstrip().split(' ')) - assert fp.readline() == '255\n'.encode() + with open(filename, "rb") as fp: + assert fp.readline() == "P5\n".encode() + w, h = (int(c) for c in fp.readline().decode().rstrip().split(" ")) + assert fp.readline() == "255\n".encode() return fp.read(), (w, h) + def save_pgm(filename, buf, size): - with open(filename, 'wb') as fp: - fp.write('P5\n'.encode()) - fp.write('{0:d} {1:d}\n'.format(*size).encode()) - fp.write('255\n'.encode()) + with open(filename, "wb") as fp: + fp.write("P5\n".encode()) + fp.write("{0:d} {1:d}\n".format(*size).encode()) + fp.write("255\n".encode()) fp.write(buf) + # 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): 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 + + 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): @@ -77,14 +88,14 @@ def test_draw(self): box(pen) width, height = 500, 500 buf1, _ = pen.buffer(width=width, height=height) - buf2 = b'\xff' * width * height + buf2 = b"\xff" * width * height self.assertEqual(buf1, buf2) def test_empty(self): pen = FreeTypePen(None) width, height = 500, 500 buf, size = pen.buffer(width=width, height=height) - self.assertEqual(b'\0' * size[0] * size[1], buf) + self.assertEqual(b"\0" * size[0] * size[1], buf) def test_bbox_and_cbox(self): pen = FreeTypePen(None) @@ -99,7 +110,7 @@ def test_non_zero_fill(self): width, height = t.transformPoint((1000, 1000)) t = t.translate(0, 200) 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')) + 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) @@ -111,7 +122,7 @@ def test_even_odd_fill(self): width, height = t.transformPoint((1000, 1000)) t = t.translate(0, 200) 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')) + 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) @@ -132,7 +143,7 @@ def test_contain(self): t = Scale(0.05, 0.05) width, height = 0, 0 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')) + 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) @@ -143,7 +154,7 @@ def test_rotate(self): t = Scale(0.05, 0.05).rotate(math.pi / 4.0).translate(1234, 5678) width, height = None, None buf1, size1 = pen.buffer(width=width, height=height, transform=t) - buf2, size2 = load_pgm(os.path.join(DATA_DIR, 'test_rotate.pgm')) + buf2, size2 = load_pgm(os.path.join(DATA_DIR, "test_rotate.pgm")) self.assertEqual(len(buf1), len(buf2)) self.assertEqual(size1, size2) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) @@ -154,7 +165,7 @@ def test_skew(self): t = Scale(0.05, 0.05).skew(math.pi / 4.0).translate(1234, 5678) width, height = None, None buf1, size1 = pen.buffer(width=width, height=height, transform=t) - buf2, size2 = load_pgm(os.path.join(DATA_DIR, 'test_skew.pgm')) + buf2, size2 = load_pgm(os.path.join(DATA_DIR, "test_skew.pgm")) self.assertEqual(len(buf1), len(buf2)) self.assertEqual(size1, size2) self.assertGreater(psnr(buf1, buf2), PSNR_THRESHOLD) @@ -164,7 +175,7 @@ def test_none_size(self): star(pen) width, height = None, None buf1, size = pen.buffer(width=width, height=height, transform=Offset(0, 200)) - buf2, _ = pen.buffer(width=1000, height=1000, transform=Offset(0, 200)) + buf2, _ = pen.buffer(width=1000, height=1000, transform=Offset(0, 200)) self.assertEqual(size, (1000, 1000)) self.assertEqual(buf1, buf2) @@ -172,7 +183,7 @@ def test_none_size(self): box(pen, offset=(250, 250)) width, height = None, None buf1, size = pen.buffer(width=width, height=height) - buf2, _ = pen.buffer(width=500, height=500, transform=Offset(-250, -250)) + buf2, _ = pen.buffer(width=500, height=500, transform=Offset(-250, -250)) self.assertEqual(size, (500, 500)) self.assertEqual(buf1, buf2) @@ -180,7 +191,7 @@ def test_none_size(self): box(pen, offset=(-1234, -5678)) width, height = None, None buf1, size = pen.buffer(width=width, height=height) - buf2, _ = pen.buffer(width=500, height=500, transform=Offset(1234, 5678)) + buf2, _ = pen.buffer(width=500, height=500, transform=Offset(1234, 5678)) self.assertEqual(size, (500, 500)) self.assertEqual(buf1, buf2) @@ -188,8 +199,12 @@ def test_zero_size(self): pen = FreeTypePen(None) star(pen) width, height = 0, 0 - buf1, size = pen.buffer(width=width, height=height, transform=Offset(0, 200), contain=True) - buf2, _ = pen.buffer(width=1000, height=1000, transform=Offset(0, 200), contain=True) + buf1, size = pen.buffer( + width=width, height=height, transform=Offset(0, 200), contain=True + ) + buf2, _ = pen.buffer( + width=1000, height=1000, transform=Offset(0, 200), contain=True + ) self.assertEqual(size, (1000, 1000)) self.assertEqual(buf1, buf2) @@ -197,7 +212,9 @@ def test_zero_size(self): box(pen, offset=(250, 250)) width, height = 0, 0 buf1, size = pen.buffer(width=width, height=height, contain=True) - buf2, _ = pen.buffer(width=500, height=500, transform=Offset(0, 0), contain=True) + buf2, _ = pen.buffer( + width=500, height=500, transform=Offset(0, 0), contain=True + ) self.assertEqual(size, (750, 750)) self.assertEqual(buf1, buf2) @@ -205,10 +222,14 @@ def test_zero_size(self): box(pen, offset=(-1234, -5678)) width, height = 0, 0 buf1, size = pen.buffer(width=width, height=height, contain=True) - buf2, _ = pen.buffer(width=500, height=500, transform=Offset(1234, 5678), contain=True) + buf2, _ = pen.buffer( + width=500, height=500, transform=Offset(1234, 5678), contain=True + ) self.assertEqual(size, (500, 500)) self.assertEqual(buf1, buf2) -if __name__ == '__main__': + +if __name__ == "__main__": import sys + sys.exit(unittest.main())