This might strike as something very simple, and I too thought it'd be, but it apparently isn't. I must've spent a week trying to make this work, but I for the love of me can't manage to do so.
What I need
I need to render any given string (only containing standard characters) with any given font (handwritten-like) in Python. The font must be loaded from a TTF file. I also need to be able to accurately detect its borders (get the exact start and end position of the text, vertically and horizontally), preferably before drawing it. Lastly, it'd really make my life easier if the output is an array which I can then keep processing, and not an image file written to disc.
What I've tried
Imagemagick bindings (namely Wand): Couldn't figure out how to get the text metrics before setting the image size and rendering the text on it.
Pango via Pycairo bindings: nearly inexistent documentation, couldn't figure out how to load a TrueType font from a file.
PIL (Pillow): The most promising option. I've managed to accurately calculate the height for any text (which surprisingly is not the height getsize
returns), but the width seems buggy for some fonts. Not only that, but those fonts with buggy width also get rendered incorrectly. Even when making the image large enough, they get cut off.
Here are some examples, with the text "Puzzling":
Font: Lovers Quarrel
Result:
Font: Miss Fajardose
Result:
This is the code I'm using to generate the images:
from PIL import Image, ImageDraw, ImageFont
import cv2
import numpy as np
import glob
import os
font_size = 75
font_paths = sorted(glob.glob('./fonts/*.ttf'))
text = "Puzzling"
background_color = 180
text_color = 50
color_variance = 60
cv2.namedWindow('display', 0)
for font_path in font_paths:
font = ImageFont.truetype(font_path, font_size)
text_width, text_height = font.getsize(text)
ascent, descent = font.getmetrics()
(width, baseline), (offset_x, offset_y) = font.font.getsize(text)
# +100 added to see that text gets cut off
PIL_image = Image.new('RGB', (text_width-offset_x+100, text_height-offset_y), color=0x888888)
draw = ImageDraw.Draw(PIL_image)
draw.text((-offset_x, -offset_y), text, font=font, fill=0)
cv2.imshow('display', np.array(PIL_image))
k = cv2.waitKey()
if chr(k & 255) == 'q':
break
Some questions
Are the fonts the problem? I've been told by some colleagues that might be it, but I don't think so, since they get rendered correctly by the Imagemagick via command line.
Is my code the problem? Am I doing something wrong which is causing the text to get cut off?
Lastly, is it a bug in PIL? In that case, which library do you recommend I use to solve my problem? Should I give Pango and Wand another try?
pyvips seems to do this correctly. I tried this:
$ python3
Python 3.7.3 (default, Apr 3 2019, 05:39:12)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pyvips
>>> x = pyvips.Image.text("Puzzling", dpi=300, font="Miss Fajardose", fontfile="/home/john/pics/MissFajardose-Regular.ttf")
>>> x.write_to_file("x.png")
To make:
The pyvips docs have a quick intro to the options:
https://libvips.github.io/pyvips/vimage.html#pyvips.Image.text
Or the C library docs have a lot more detail:
http://libvips.github.io/libvips/API/current/libvips-create.html#vips-text
It makes a one-band 8-bit image of the antialiased text which you can use for further processing, pass to NumPy or PIL, etc etc. There's a section in the intro on how to convert libvips images into arrays:
https://libvips.github.io/pyvips/intro.html#numpy-and-pil
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With