freetypePen: format code with Black

This commit is contained in:
Takaaki Fuji 2022-01-31 20:56:03 +09:00
parent 48cbe7b054
commit 5f2c492635
2 changed files with 124 additions and 55 deletions

View File

@ -2,7 +2,7 @@
"""Pen to rasterize paths with FreeType."""
__all__ = ['FreeTypePen']
__all__ = ["FreeTypePen"]
import os
import ctypes
@ -15,14 +15,22 @@ import freetype
from freetype.raw import FT_Outline_Get_Bitmap, FT_Outline_Get_BBox, FT_Outline_Get_CBox
from freetype.ft_types import FT_Pos
from freetype.ft_structs import FT_Vector, FT_BBox, FT_Bitmap, FT_Outline
from freetype.ft_enums import FT_OUTLINE_NONE, FT_OUTLINE_EVEN_ODD_FILL, FT_PIXEL_MODE_GRAY, FT_CURVE_TAG_ON, FT_CURVE_TAG_CONIC, FT_CURVE_TAG_CUBIC
from freetype.ft_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,7 +123,7 @@ class FreeTypePen(BasePen):
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))
@ -123,7 +131,11 @@ class FreeTypePen(BasePen):
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:
@ -140,10 +152,12 @@ class FreeTypePen(BasePen):
(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 @@ class FreeTypePen(BasePen):
"""
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 @@ class FreeTypePen(BasePen):
(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 @@ class FreeTypePen(BasePen):
(<class 'numpy.ndarray'>, (1000, 500))
"""
import numpy as np
buf, size = self.buffer(width=width, height=height, transform=transform, contain=contain, evenOdd=evenOdd)
return np.frombuffer(buf, 'B').reshape((size[1], size[0])) / 255.0
def show(self, width=None, height=None, transform=None, contain=False, evenOdd=False):
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 @@ class FreeTypePen(BasePen):
>> 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 @@ class FreeTypePen(BasePen):
(<class 'PIL.Image.Image'>, (500, 1000))
"""
from PIL import Image
buf, size = self.buffer(width=width, height=height, transform=transform, contain=contain, evenOdd=evenOdd)
img = Image.new('L', size, 0)
img.putalpha(Image.frombuffer('L', size, buf))
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 @@ class FreeTypePen(BasePen):
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 @@ class FreeTypePen(BasePen):
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)):

View File

@ -4,13 +4,15 @@ import math
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]))
@ -19,6 +21,7 @@ def box(pen, offset=(0, 0)):
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 @@ class FreeTypePenTest(unittest.TestCase):
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 @@ class FreeTypePenTest(unittest.TestCase):
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 @@ class FreeTypePenTest(unittest.TestCase):
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 @@ class FreeTypePenTest(unittest.TestCase):
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 @@ class FreeTypePenTest(unittest.TestCase):
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 @@ class FreeTypePenTest(unittest.TestCase):
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)
@ -188,8 +199,12 @@ class FreeTypePenTest(unittest.TestCase):
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 @@ class FreeTypePenTest(unittest.TestCase):
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 @@ class FreeTypePenTest(unittest.TestCase):
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())