Center-/middle-align text with PIL?

Question:

How would I center-align (and middle-vertical-align) text when using PIL?

Asked By: Phillip B Oldham

||

Answers:

Use the textsize method (see docs) to figure out the dimensions of your text object before actually drawing it. Then draw it starting at the appropriate coordinates.

Answered By: Arkady

Deprecation Warning: textsize is deprecated and will be removed in Pillow 10 (2023-07-01). Use textbbox or textlength instead.

Code using textbbox instead of textsize.

from PIL import Image, ImageDraw, ImageFont


def create_image(size, bgColor, message, font, fontColor):
    W, H = size
    image = Image.new('RGB', size, bgColor)
    draw = ImageDraw.Draw(image)
    _, _, w, h = draw.textbbox((0, 0), message, font=font)
    draw.text(((W-w)/2, (H-h)/2), message, font=font, fill=fontColor)
    return image
myFont = ImageFont.truetype('Roboto-Regular.ttf', 16)
myMessage = 'Hello World'
myImage = create_image((300, 200), 'yellow', myMessage, myFont, 'black')
myImage.save('hello_world.png', "PNG")

Result

Result


Use Draw.textsize method to calculate text size and re-calculate position accordingly.

Here is an example:

from PIL import Image, ImageDraw

W, H = (300,200)
msg = "hello"

im = Image.new("RGBA",(W,H),"yellow")
draw = ImageDraw.Draw(im)
w, h = draw.textsize(msg)
draw.text(((W-w)/2,(H-h)/2), msg, fill="black")

im.save("hello.png", "PNG")

and the result:

image with centered text

If your fontsize is different, include the font like this:

myFont = ImageFont.truetype("my-font.ttf", 16)
draw.textsize(msg, font=myFont)
Answered By: sastanin

Here is some example code which uses textwrap to split a long line into pieces, and then uses the textsize method to compute the positions.

from PIL import Image, ImageDraw, ImageFont
import textwrap

astr = '''The rain in Spain falls mainly on the plains.'''
para = textwrap.wrap(astr, width=15)

MAX_W, MAX_H = 200, 200
im = Image.new('RGB', (MAX_W, MAX_H), (0, 0, 0, 0))
draw = ImageDraw.Draw(im)
font = ImageFont.truetype(
    '/usr/share/fonts/truetype/msttcorefonts/Arial.ttf', 18)

current_h, pad = 50, 10
for line in para:
    w, h = draw.textsize(line, font=font)
    draw.text(((MAX_W - w) / 2, current_h), line, font=font)
    current_h += h + pad

im.save('test.png')

enter image description here

Answered By: unutbu

One shall note that the Draw.textsize method is inaccurate. I was working with low pixels images, and after some testing, it turned out that textsize considers every character to be 6 pixel wide, whereas an I takes max. 2 pixels and a W takes min. 8 pixels (in my case). And so, depending on my text, it was or wasn’t centered at all. Though, I guess “6” was an average, so if you’re working with long texts and big images, it should still be ok.

But now, if you want some real accuracy, you better use the getsize method of the font object you’re going to use:

arial = ImageFont.truetype("arial.ttf", 9)
w,h = arial.getsize(msg)
draw.text(((W-w)/2,(H-h)/2), msg, font=arial, fill="black")

As used in Edilio’s link.

Answered By: Sindarus

The PIL docs for ImageDraw.text are a good place to start, but don’t answer your question.

Below is an example of how to center the text in an arbitrary bounding box, as opposed to the center of an image. The bounding box is defined as: (x1, y1) = upper left corner and (x2, y2) = lower right corner.

from PIL import Image, ImageDraw, ImageFont

# Create blank rectangle to write on
image = Image.new('RGB', (300, 300), (63, 63, 63, 0))
draw = ImageDraw.Draw(image)

message = 'Stuck innthe middlenwith you'

bounding_box = [20, 30, 110, 160]
x1, y1, x2, y2 = bounding_box  # For easy reading

font = ImageFont.truetype('Consolas.ttf', size=12)

# Calculate the width and height of the text to be drawn, given font size
w, h = draw.textsize(message, font=font)

# Calculate the mid points and offset by the upper left corner of the bounding box
x = (x2 - x1 - w)/2 + x1
y = (y2 - y1 - h)/2 + y1

# Write the text to the image, where (x,y) is the top left corner of the text
draw.text((x, y), message, align='center', font=font)

# Draw the bounding box to show that this works
draw.rectangle([x1, y1, x2, y2])

image.show()
image.save('text_center_multiline.png')

The output shows the text centered vertically and horizontally in the bounding box.

Whether you have a single or multiline message no longer matters, as PIL incorporated the align='center' parameter. However, it is for multiline text only. If the message is a single line, it needs to be manually centered. If the message is multiline, align='center' does the work for you on subsequent lines, but you still have to manually center the text block. Both of these cases are solved at once in the code above.

Answered By: aaronpenne

A simple solution if you’re using PIL 8.0.0 or above: text anchors

width, height = # image width and height
draw = ImageDraw.draw(my_image)

draw.text((width/2, height/2), "my text", font=my_font, anchor="mm")

mm means to use the middle of the text as anchor, both horizontally and vertically.

See the anchors page for other kinds of anchoring. For example if you only want to center horizontally you may want to use ma.

Answered By: makeworld

This is a simple example to add a text in the center of the image

from PIL import Image, ImageDraw, ImageFilter

msg = "hello"

img = Image.open('image.jpg')
W, H = img.size
box_image = img.filter(ImageFilter.BoxBlur(4))
draw = ImageDraw.Draw(box_image)
w, h = draw.textsize(msg)
draw.text(((W - w) / 2, (H - h) / 2), msg, fill="black")

box_image.show()
Answered By: Praveen Kumar

All the other answers did NOT take text ascender into consideration.

Here’s a backport of ImageDraw.text(..., anchor="mm"). Not sure if it’s fully compatible with anchor="mm", cause I haven’t tested the other kwargs like spacing, stroke_width yet. But I ensure you this offset fix works for me.

from PIL import ImageDraw
from PIL import __version__ as pil_ver

PILLOW_VERSION = tuple([int(_) for _ in pil_ver.split(".")[:3]])


def draw_anchor_mm_text(
    im,
    xy,
    # args shared by ImageDraw.textsize() and .text()
    text,
    font=None,
    spacing=4,
    direction=None,
    features=None,
    language=None,
    stroke_width=0,
    # ImageDraw.text() exclusive args
    **kwargs,
):
    """
    Draw center middle-aligned text. Basically a backport of 
    ImageDraw.text(..., anchor="mm"). 

    :param PIL.Image.Image im:
    :param tuple xy: center of text
    :param unicode text:
    ...
    """
    draw = ImageDraw.Draw(im)
    # Text anchor is firstly implemented in Pillow 8.0.0.
    if PILLOW_VERSION >= (8, 0, 0):
        kwargs.update(anchor="mm")
    else:
        kwargs.pop("anchor", None)  # let it defaults to "la"
        if font is None:
            font = draw.getfont()
        # anchor="mm" middle-middle coord xy -> "left-ascender" coord x'y'
        # offset_y = ascender - top, https://stackoverflow.com/a/46220683/5101148
        # WARN: ImageDraw.textsize() return text size with offset considered.
        w, h = draw.textsize(
            text,
            font=font,
            spacing=spacing,
            direction=direction,
            features=features,
            language=language,
            stroke_width=stroke_width,
        )
        offset = font.getoffset(text)
        w, h = w - offset[0], h - offset[1]
        xy = (xy[0] - w / 2 - offset[0], xy[1] - h / 2 - offset[1])
    draw.text(
        xy,
        text,
        font=font,
        spacing=spacing,
        direction=direction,
        features=features,
        language=language,
        stroke_width=stroke_width,
        **kwargs,
    )

Refs

Answered By: Simba

Using a combination of anchor="mm" and align="center" works wonders. Example

draw.text(
        xy=(width / 2, height / 2),
        text="centered",
        fill="#000000",
        font=font,
        anchor="mm",
        align="center"
    )

Note: Tested where font is an ImageFont class object constructed as such:

ImageFont.truetype('path/to/font.ttf', 32)

Answered By: alphazwest

if you are using the default font then you can use this simple calculation

draw.text((newimage.width/2-len(text)*3, 5), text,fill="black", align ="center",anchor="mm") 

the main thing is
you have to divide the image width by 2 then get the length of the string you want and multiply it by 3 and subtract it from the division result

newimage.width/2-len(text)*3 #this is X position

**this answer is an estimation for the default font size used if you use a custom font then the multiplier must be changed accordingly. in the default case it is 3

Answered By: Somen Das
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.