PIL how to scale text size in relation to the size of the image

Solution 1:

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.

Solution 2:

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.

Solution 3:

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.

Solution 4:

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.