6
votes

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:

Lovers Quarrel Render

Font: Miss Fajardose

Result:

Miss Fajardose Render

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?

1
In command line ImageMagick, you can get font metrics using -debug annotate when creating the text. See imagemagick.org/Usage/text/#font_info. I do not know if that is available in Wand. But you could use a Python subprocess call to do that.fmw42
If you know the box the text needs to fit in, then stackoverflow.com/a/39557083/740553 is probably more what you're looking for.Mike 'Pomax' Kamermans
@fmw42 Thank you, I might be able to do something with that, although after doing some tests it seems like the metrics are not quite right either, in most cases PIL does better at calculating the height.kikones34
@Mike'Pomax'Kamermans I need a constant font size, and I need to know how much space it will take, not the other way around.kikones34
@AdriàRicoBlanes please add that into your "what I need" part of your post. That's not information that should be an afterthought in the comments =)Mike 'Pomax' Kamermans

1 Answers

4
votes

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:

enter image description here

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