Reputation: 8722
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:
and
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
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
Comment
I dont have your font, therefor your output might change.
Upvotes: 3