darkhorse
darkhorse

Reputation: 8722

How to break to new line between words (not characters) in Pillow?

I'm trying to generate a poster using PIL in Python. The idea is that the width of the poster is fixed, but depending on the content, the height is set dynamically. Here's the code for that:

from PIL import Image, ImageDraw, ImageFont


# From https://stackoverflow.com/a/58176967/5056347

def break_fix(text, width, font, draw):
    """
    Fix line breaks in text.
    """
    if not text:
        return
    lo = 0
    hi = len(text)
    while lo < hi:
        mid = (lo + hi + 1) // 2
        t = text[:mid]
        w, h = draw.textsize(t, font=font)
        if w <= width:
            lo = mid
        else:
            hi = mid - 1
    t = text[:lo]
    w, h = draw.textsize(t, font=font)
    yield t, w, h
    yield from break_fix(text[lo:], width, font, draw)


# Edited from https://stackoverflow.com/a/58176967/5056347

def fit_text(img, text, color, font, x_start_offset=0, x_end_offset=0, center=False):
    """
    Fit text into container after applying line breaks. Returns the total 
    height taken up by the text, which can be used to create containers of 
    dynamic heights.
    """
    width = img.size[0] - x_start_offset - x_end_offset
    draw = ImageDraw.Draw(img)
    pieces = list(break_fix(text, width, font, draw))
    height = sum(p[2] for p in pieces)
    y = (img.size[1] - height) // 2
    h_taken_by_text = 0
    for t, w, h in pieces:
        if center:
            x = (img.size[0] - w) // 2
        else:
            x = x_start_offset
        draw.text((x, y), t, font=font, fill=color)
        new_width, new_height = draw.textsize(t, font=font)
        y += h
        h_taken_by_text += new_height
    return h_taken_by_text


def generate_text_section(width, text, color, font, x_start_offset, x_end_offset, v_spacing):
    """
    Generates an image for a text section.
    """
    # Calculate height using "fake" canvas
    img = Image.new('RGB', (width, 1), color='white')
    calc_height = fit_text(
        img, text.upper(), color, font, x_start_offset, x_end_offset, False
    )

    # Create real canvas and fit text
    img = Image.new('RGB', (width, calc_height + v_spacing), color='white')
    fit_text(
        img, text.upper(), color, font, x_start_offset, x_end_offset, False
    )

    return img


test_img_1 = generate_text_section(
    width=400,
    text='But I must explain to you',
    color='black',
    font=ImageFont.truetype('fonts/RobotoMono-Bold.ttf', 14),
    x_start_offset=30,
    x_end_offset=30,
    v_spacing=30
)
test_img_1.show()
test_img_1.save('1.png')


test_img_2 = generate_text_section(
    width=400,
    text='But I must explain to you how all this mistaken idea of denouncing',
    color='black',
    font=ImageFont.truetype('fonts/RobotoMono-Bold.ttf', 14),
    x_start_offset=30,
    x_end_offset=30,
    v_spacing=30
)
test_img_2.show()
test_img_2.save('2.png')

How it works is that I use the fit_text() method first to calculate the height required (after breaking the text that overflows), and then I use that height to create a canvas that can accommodate the text. This works fairly well. Here are the two images generated using the code above:

Image 1

and

Image 2

This is honestly great, but, the overflow is done between characters, so on the second image, the word MISTAKEN is broken between the lines. Ideally, I would like to break between words, or at least add a "-" before the line break.

Can anyone help me out with this? Full disclosure, both of those methods were edited from this answer.

Upvotes: 2

Views: 1184

Answers (1)

mosc9575
mosc9575

Reputation: 6337

I think the best is to modify break_fix():

def break_fix(text, width, font, draw):
    """
    Fix line breaks in text.
    """
    if not text:
        return
    if isinstance(text, str):
        text = text.split() # this creates a list of words

    lo = 0
    hi = len(text)
    while lo < hi:
        mid = (lo + hi + 1) // 2
        t = ' '.join(text[:mid]) # this makes a string again
        w, h = draw.textsize(t, font=font)
        if w <= width:
            lo = mid
        else:
            hi = mid - 1
    t = ' '.join(text[:lo]) # this makes a string again
    w, h = draw.textsize(t, font=font)
    yield t, w, h
    yield from break_fix(text[lo:], width, font, draw)

Third blindtext example:

test_img_3 = generate_text_section(
    width=400,
    text='''Lorem ipsum dolor sit amte, consectutuer adipiscing elit.
Ut purus elit vestibulumut, placerat, adpiscing vitae, felis. Curabutiur dictm gravida mauris.
Nam Arcu libero, nonummy eget, consectetuer id, vulputate a, magna. Donec vehicula auge eu newue.
Pellentesque habitant morbi tristique senectus es netus et malesuada fames ac turpis egestas. Mauris ut leo.
''',
    color='black',
    font=ImageFont.truetype('/home/jovyan/shared/fonts/SourceSansPro.ttf', 14),
    x_start_offset=30,
    x_end_offset=30,
    v_spacing=30
)
test_img_3.show()
test_img_3.save('2.png')

Output

Output

Comment

I dont have your font, therefor your output might change.

Upvotes: 3

Related Questions