PIL how to scale text size in relation to the size of the image
Asked Answered
W

4

68

I'm trying to dynamically scale text to be placed on images of varying but known dimensions. The text will be applied as a watermark. Is there any way to scale the text in relation to the image dimensions? I don't require that the text take up the whole surface area, just to be visible enough so its easily identifiable and difficult to remove. I'm using Python Imaging Library version 1.1.7. on Linux.

I would like to be able to set the ratio of the text size to the image dimensions, say like 1/10 the size or something.

I have been looking at the font size attribute to change the size but I have had no luck in creating an algorithm to scale it. I'm wondering if there is a better way.

Any ideas on how I could achieve this?

Thanks

Waits answered 4/2, 2011 at 19:41 Comment(0)
T
109

You could just increment the font size until you find a fit. font.getsize() is the function that tells you how large the rendered text is.

from PIL import ImageFont, ImageDraw, Image

image = Image.open('hsvwheel.png')
draw = ImageDraw.Draw(image)
txt = "Hello World"
fontsize = 1  # starting font size

# portion of image width you want text width to be
img_fraction = 0.50

font = ImageFont.truetype("arial.ttf", fontsize)
while font.getsize(txt)[0] < img_fraction*image.size[0]:
    # iterate until the text size is just larger than the criteria
    fontsize += 1
    font = ImageFont.truetype("arial.ttf", fontsize)

# optionally de-increment to be sure it is less than criteria
fontsize -= 1
font = ImageFont.truetype("arial.ttf", fontsize)

print('final font size',fontsize)
draw.text((10, 25), txt, font=font) # put the text on the image
image.save('hsvwheel_txt.png') # save it

If this is not efficient enough for you, you can implement a root-finding scheme, but I'm guessing that the font.getsize() function is small potatoes compared to the rest of your image editing processes.

Told answered 4/2, 2011 at 20:43 Comment(2)
OSError: cannot open resource. Do I have to first download the font from somewhere?Joanjoana
getsize() was replaced with getbbox pillow.readthedocs.io/en/stable/releasenotes/…Herries
G
17

I know this is an old question that has already been answered with a solution that I too have used. Thanks, @Paul!

Though with increasing the font size by one for each iteration can be time-consuming (at least for me on my poor little server). So eg. small text (like "Foo") would take around 1 - 2 seconds, depending on the image size.

To solve that I adjusted Pauls code so that it searches for the number somewhat like a binary search.

breakpoint = img_fraction * photo.size[0]
jumpsize = 75
while True:
    if font.getsize(text)[0] < breakpoint:
        fontsize += jumpsize
    else:
        jumpsize = jumpsize // 2
        fontsize -= jumpsize
    font = ImageFont.truetype(font_path, fontsize)
    if jumpsize <= 1:
        break

Like this, it increases the font size until it's above the breakpoint and from there on out it goes up and down with (cutting the jump size in half with each down) until it has the right size.

With that, I could reduce the steps from around 200+ to about 10 and so from around 1-2 sec to 0.04 to 0.08 sec.

This is a drop-in replacement for Pauls code (for the while statement and the 2 lines after it because you already get the font correct font size in the while)

This was done in a few mins so any improvements are appreciated! I hope this can help some who are looking for a bit more performant friendly solution.

Gobi answered 19/5, 2020 at 12:26 Comment(2)
int(jumpsize / 2) can be replaced with jumpsize // 2.Woodshed
Thanks @MarkRansom I adjusted itGobi
D
12

In general when you change the font sizing its not going to be a linear change in size of the font.

Non-linear Scaling

Now this often depends on the software, fonts, etc... This example was taken from Typophile and uses LaTex + Computer Modern font. As you can see its not exactly a linear scaling. So if you are having trouble with non-linear font scaling then I'm not sure how to resolve it, but one suggestion maybe is to.

  1. Render the font as closely to the size that you want, then scale that up/down via regular image scaling algorithm...
  2. Just accept that it won't exactly be linear scaling and try to create some sort of table/algorithm that will select the closest point size for the font to match up with the image size.
Diverticulosis answered 4/2, 2011 at 20:33 Comment(3)
Thanks for the reply , The method suggested by Paul solved it. Thanks anyway though.Waits
My first thought looking at the image was "well if you don't scale the line height with the text, then of course it's not going to look linear". But comparing the length of the lines, you can see that the rendered length of the 6pt font is more than half of the 12pt, and the 5pt is more than half the length of the 10pt.Pinkster
TrueType fonts are scaled linearly though, only kerning will make the text deviate from that. LaTeX + Computer Modern is not using TrueType, Computer Modern is not even a vector font, it comes pre-rendered for different point sizes. In this case, it is the height of the font that scales linearly, but the aspect ratio of each letter changes with the size, note how at 5pt the x is wider than it is tall, whereas at 10pt it is taller than wide. TTF typically has a single letter shape that is isotropically scaled to the requested size.Xantha
C
3

Despite other answers saying that font size do not scale linearly, in all the examples that I tested they did scale linearly (within 1-2%).

So if you need a simpler and more efficient version that works within a few percent, you can copy/paste the following:

from PIL import ImageFont, ImageDraw, Image

def find_font_size(text, font, image, target_width_ratio):
    tested_font_size = 100
    tested_font = ImageFont.truetype(font, tested_font_size)
    observed_width, observed_height = get_text_size(text, image, tested_font)
    estimated_font_size = tested_font_size / (observed_width / image.width) * target_width_ratio
    return round(estimated_font_size)

def get_text_size(text, image, font):
    im = Image.new('RGB', (image.width, image.height))
    draw = ImageDraw.Draw(im)
    return draw.textsize(text, font)

The function find_font_size() can then be used like that (full example):

width_ratio = 0.5  # Portion of the image the text width should be (between 0 and 1)
font_family = "arial.ttf"
text = "Hello World"

image = Image.open('image.jpg')
editable_image = ImageDraw.Draw(image)
font_size = find_font_size(text, font_family, image, width_ratio)
font = ImageFont.truetype(font_family, font_size)
print(f"Font size found = {font_size} - Target ratio = {width_ratio} - Measured ratio = {get_text_size(text, image, font)[0] / image.width}")

editable_image.text((10, 10), text, font=font)
image.save('output.png')

Which for a 225x225 image would print:

>> Font size found = 22 - Target ratio = 0.5 - Measured ratio = 0.502

I tested find_font_size() with various fonts and picture sizes, and it worked in all cases.

If you want to know how this function works, basically tested_font_size is used to find out which ratio will be obtained if we use this specific font size to generate the text. Then, we use a cross-multiplication rule to get the targeted font size.

I tested different values for tested_font_size and found that as long as it's not too small, it does not make any difference.

Centroid answered 7/2, 2021 at 18:8 Comment(1)
textsize is deprecated : DeprecationWarning: textsize is deprecated and will be removed in Pillow 10 (2023-07-01). Use textbbox or textlength instead.Penknife

© 2022 - 2025 — McMap. All rights reserved.