8

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?

kikones34
  • 371
  • 9
  • 13
  • In command line ImageMagick, you can get font metrics using -debug annotate when creating the text. See https://www.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 Mar 07 '18 at 17:25
  • If you know the box the text needs to fit in, then https://stackoverflow.com/a/39557083/740553 is probably more what you're looking for. – Mike 'Pomax' Kamermans Mar 07 '18 at 18:48
  • @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 Mar 12 '18 at 11:33
  • @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 Mar 12 '18 at 11:33
  • @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 Mar 12 '18 at 15:01
  • 1
    Note: This is no longer the case when I tried it in the latest version of PIL, using the latest version of the linked fonts. PIL renders it properly now. – John Dec 06 '19 at 02:51

3 Answers3

5

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

jcupitt
  • 10,213
  • 2
  • 23
  • 39
  • Thank you for your answer, it works pretty well, but there are some fonts for which the height is incorrect (they get cut out). Imagemagick also had some problems with those fonts. I'm starting to think it might really be that the metrics are not properly specified, but in that case, I can't understand how PIL managed to properly render them. (Example: Ananda Hastakchyar). – kikones34 Mar 12 '18 at 12:24
  • Could you post a link to a font that fails? I get no results for https://fonts.google.com/?query=Ananda+Hastakchyar – jcupitt Mar 12 '18 at 12:56
  • Oh, found it here http://www.1001fonts.com/ananda-hastakchyar-font.html I'll update my answer. – jcupitt Mar 12 '18 at 13:08
  • 2
    With the font Ananda Hastakchyar, pyvips with libvips 8.6.3 fails to allow enough space along the top and bottom. This is because this hand-styled font deliberately scribbles outside the usual inking area -- if you try selecting the font in a word processor, for example, you'll find descenders on one line will overlap ascenders on the line below. I've fixed this in HEAD 8.6 and the improvement will be in 8.6.4, thanks for pointing this out. https://github.com/jcupitt/libvips/commit/878c77a035ef0a32db7c249ccb31118932e790d3 – jcupitt Mar 12 '18 at 18:21
  • According to pyvips docs for text[1], it can return an `Image` or "list[Image, Dict[str, mixed]]", but it doesn't explain when the second type is returned, and I'm not clear what the second type exactly mean `List[Union[Image, Dict[str, Any]]]` ? libvips docs for text doesn't indicate something like this. [1]: https://libvips.github.io/pyvips/vimage.html#pyvips.Image.text – John Dec 02 '19 at 05:23
  • Hello, you can read out the DPI that autofit selected. See the main C docs: https://libvips.github.io/libvips/API/current/libvips-create.html#vips-text .. example `pyvips.Image.text("hello", width=100, height=100, autofit_dpi=True)`. – jcupitt Dec 02 '19 at 15:04
  • In my test pyvips failed on the Squarrel font for to me unknown reason (probably not being able to load the font) and produced an image using some standard font. – Claudio May 25 '23 at 22:15
  • @claudio If you post a link to the font that failed I could have a go at fixing it. – jcupitt May 26 '23 at 19:32
  • @jcupitt The link to the font is given in the question. I have got the font that failed from that link. Sorry for not proper spelling of the font name in my previous comment ( should be Font: Lovers Quarrel) – Claudio May 27 '23 at 09:11
  • This works for me: `vips text x.png Puzzling --dpi 300 --fontfile LoversQuarrel-Regular.ttf --font LoversQuarrel` and gets the bounding box right. Did you select the font as well as specifying the fontfile? – jcupitt May 27 '23 at 10:24
0

Here's some code I created that works for me and is PIL-based. I found using getsize_multiline worked pretty well (and also drew the text using the ImageDraw.Draw multiline_text function).

from PIL import Image, ImageFont, ImageDraw, ImageColor

def text_to_image(
text: str,
font_filepath: str,
font_size: int,
color: (int, int, int), #color is in RGB
font_align="center"):

   font = ImageFont.truetype(font_filepath, size=font_size)
   box = font.getsize_multiline(text)
   img = Image.new("RGBA", (box[0], box[1]))
   draw = ImageDraw.Draw(img)
   draw_point = (0, 0)
   draw.multiline_text(draw_point, text, font=font, fill=color, align=font_align)
   return img
Sue Donhym
  • 101
  • 5
0

PIL (as of 2023-05-26) works for me OK:

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)
    #          DeprecationWarning: getsize is deprecated and will be 
    # removed in Pillow 10 (2023-07-01). Use getbbox or getlength 
    # instead: 
    x,y,w,h = font.getbbox(text) # int values
    text_width, text_height = w, h    
    # font.getlength(text) # a float value 
    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)
    PIL_image = Image.new('RGB', (text_width-offset_x, 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

Images saved using the OpenCV GUI showing them:

enter image description here

enter image description here

Claudio
  • 7,474
  • 3
  • 18
  • 48