Wrap text in PIL

Question:

I’m using PIL to draw text on an image. How would I wrap a string of text. This is my code:

text = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

image = Image.open("/tmp/background-image.jpg")
draw = ImageDraw.Draw(image)
font = ImageFont.truetype("/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf"), 50, encoding='unic')
draw.text((100, 100), text, font=font, fill="#aa0000")
image.save("/tmp/image.jpg")
Asked By: Neil

||

Answers:

Well, you can do this manually, of course, using the n every time you want to wrap the text. It isn’t the best way if you have different string everytime but gives entire control over the result.
But there is also the textwrap module.
You can use it this way:

import textwrap
texto = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
novo = textwrap.wrap(texto, width=20)
print(novo)

Results:

>>> 
['Lorem ipsum dolor', 'sit amet,', 'consectetur', 'adipisicing elit,', 'sed do eiusmod', 'tempor incididunt ut', 'labore et dolore', 'magna aliqua. Ut', 'enim ad minim', 'veniam, quis nostrud', 'exercitation ullamco', 'laboris nisi ut', 'aliquip ex ea', 'commodo consequat.', 'Duis aute irure', 'dolor in', 'reprehenderit in', 'voluptate velit esse', 'cillum dolore eu', 'fugiat nulla', 'pariatur. Excepteur', 'sint occaecat', 'cupidatat non', 'proident, sunt in', 'culpa qui officia', 'deserunt mollit anim', 'id est laborum.']

Returns a list of terms on the previous string wrapped according to the width you determinated.

Answered By: jonathan.hepp

You will need to first split the text into lines of the right length, and then draw each line individually.

The second part is easy, but the first part may be quite tricky to do accurately if varible-width fonts are used. If fixed-width fonts are used, or if accuracy doesn’t matter that much, then you can just use the textwrap module to split the text into lines of a given character width:

margin = offset = 40
for line in textwrap.wrap(text, width=40):
    draw.text((margin, offset), line, font=font, fill="#aa0000")
    offset += font.getsize(line)[1]
Answered By: ekhumoro

The accepted solution wraps text basing on the fixed limit of 40 characters per line, not taking into account the box width (in pixels) nor font size. This may easily lead to underfill or overfill.

Here is a better solution – a simple code snippet to wrap text taking into account font-based width measurement:
https://gist.github.com/turicas/1455973

Answered By: pryma

Use textwrap. It works without breaking the words.

import textwrap
from PIL import *
caption = "Obama warns far-left candidates says average American does not want to tear down the system"

wrapper = textwrap.TextWrapper(width=50) 
word_list = wrapper.wrap(text=caption) 
caption_new = ''
for ii in word_list[:-1]:
    caption_new = caption_new + ii + 'n'
caption_new += word_list[-1]

image = Image.open('obama.jpg')
draw = ImageDraw.Draw(image)

# Download the Font and Replace the font with the font file. 
font = ImageFont.truetype(text_font, size=font_size)
w,h = draw.textsize(caption_new, font=font)
W,H = image.size
x,y = 0.5*(W-w),0.90*H-h

image.save('output.png')

Input Image
enter image description here

Output Image

enter image description here

Answered By: Trect

I didn’t much like the idea of using yet another module to achieve this; I wanted to make it work with just the utilities in PIL. This works in Python 3.7.

I wrote this function that just wraps text based on pixel width and then checks pixel height too – if there are words it cannot fit, it cuts them off and adds ellipses to show omission (in a way that doesn’t break the limits):


from PIL import Image, ImageDraw, ImageFont

def text_wrap(text,font,writing,max_width,max_height):
    lines = [[]]
    words = text.split()
    for word in words:
        # try putting this word in last line then measure
        lines[-1].append(word)
        (w,h) = writing.multiline_textsize('n'.join([' '.join(line) for line in lines]), font=font)
        if w > max_width: # too wide
            # take it back out, put it on the next line, then measure again
            lines.append([lines[-1].pop()])
            (w,h) = writing.multiline_textsize('n'.join([' '.join(line) for line in lines]), font=font)
            if h > max_height: # too high now, cannot fit this word in, so take out - add ellipses
                lines.pop()
                # try adding ellipses to last word fitting (i.e. without a space)
                lines[-1][-1] += '...'
                # keep checking that this doesn't make the textbox too wide, 
                # if so, cycle through previous words until the ellipses can fit
                while writing.multiline_textsize('n'.join([' '.join(line) for line in lines]),font=font)[0] > max_width:
                    lines[-1].pop()
                    if lines[-1]:
                        lines[-1][-1] += '...'
                    else:
                        lines[-1].append('...')
                break
    return 'n'.join([' '.join(line) for line in lines])

Usage:

bg = Image.open('external/my_background.png')
ws = Image.open('external/my_overlay_with_alpha.png')

writing = ImageDraw.Draw(bg)

title = "My Title"
description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."


title_font = ImageFont.truetype("Arial Black.ttf", size=42)
desc_font = ImageFont.truetype("Arial Narrow Italic.ttf", size=16)

description_wrapped = text_wrap(description,desc_font,writing,160,124)

# write title and description
writing.text((20,5),title,font=title_font)
writing.text((140,120),description_wrapped,font=desc_font)

out = Image.alpha_composite(bg,ws)
out.save('mysubfolder/output.png')

out.show()
Answered By: Chris Jones

Aim for simple and consistent behavior. The textwrap module, as noted in other answers, only handles fixed-width fonts correctly (it is therefore not a consistent solution). Here is a simple function to wrap your text without breaking words, and it works with variable-width fonts.

from PIL import ImageFont


def get_wrapped_text(text: str, font: ImageFont.ImageFont,
                     line_length: int):
        lines = ['']
        for word in text.split():
            line = f'{lines[-1]} {word}'.strip()
            if font.getlength(line) <= line_length:
                lines[-1] = line
            else:
                lines.append(word)
        return 'n'.join(lines)


if __name__ == '__main__':
    font = ImageFont.truetype('arial.ttf', 12)
    text = 'An example line of text that will need to be wrapped.'
    wrapped_text = get_wrapped_text(text, font, line_length=70)
    # wrapped_text is 'An examplenline of textnthat willnneed to benwrapped.'
Answered By: Chris Collett
def img_draw(img_name,tag_line):
    
    img = Image.open(img_name)
    Width_img =img.size
    print(Width_img[0]*0.07)

    wrapper = textwrap.TextWrapper(width=int(Width_img[0]*0.07))
    word_list = wrapper.wrap(text=tag_line)

    print(word_list)

    caption_new = ""

    for ii in word_list:
        caption_new = caption_new+ ii + 'n'

    print(caption_new)

    img2 = Image.open(img_name)
    draw = ImageDraw.Draw(img2)
    fnt = ImageFont.truetype("comicbd.ttf" , 25)
    
    draw.text((60,img.size[1]/1.5),caption_new ,font = fnt ,fill=(255, 0, 0))
    img2.save(img_name)
Answered By: Areeb Khoso
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.