Splitting a string into multiple lines according to character width (python)
I am drawing text over a base image via PIL
. One of the requirements is to overflow to the next line if the total width of all characters exceeds the width of the base image.
I am currently using textwrap.wrap(text, width=16)
to accomplish this. This width
determines the number of characters to fit on one line. Now the text can be anything as long as it was created by the user. So the problem is that hardcoding width
does not account for variability width
due to font type, font size, and character selection.
What I mean?
Okay, I guess what I'm using DejaVuSans.ttf
is size 14. A W
is 14 and "i" is 4. For a base image with a width of 400 up to 100 characters i
can be placed on one line. But only 29 W
characters. I need to formulate a smarter way to wrap to the next line, where the line breaks when the sum of the character widths exceeds the width of the base image.
Can anyone help me formulate this? An illustrative example would be great!
source to share
Since you know the width of each character, you must do this in a dictionary from which you will get the width to calculate the width of the string:
char_widths = {
'a': 9,
'b': 11,
'c': 13,
# ...and so on
}
From here, you can search for each letter and use that amount to check your width:
current_width = sum([char_widths[letter] for letter in word])
source to share
If accuracy is important to you, the best way to get the actual width of the text is to actually render it, since font metrics are not always linear with respect to kerning or font size (see here ), and therefore not easily predictable. We can approach the optimal breakpoint with the ImageFont method get_size
, which internally uses the rendering methods of the main font (see the PIL github )
def break_text(txt, font, max_width):
# We share the subset to remember the last finest guess over
# the text breakpoint and make it faster
subset = len(txt)
letter_size = None
text_size = len(txt)
while text_size > 0:
# Let find the appropriate subset size
while True:
width, height = font.getsize(txt[:subset])
letter_size = width / subset
# min/max(..., subset +/- 1) are to avoid looping infinitely over a wrong value
if width < max_width - letter_size and text_size >= subset: # Too short
subset = max(int(max_width * subset / width), subset + 1)
elif width > max_width: # Too large
subset = min(int(max_width * subset / width), subset - 1)
else: # Subset fits, we exit
break
yield txt[:subset]
txt = txt[subset:]
text_size = len(txt)
and use it like this:
from PIL import Image
from PIL import ImageFont
img = Image.new('RGBA', (100, 100), (255,255,255,0))
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("Helvetica", 12)
text = "This is a sample text to break because it is too long for the image"
for i, line in enumerate(break_text(text, font, 100)):
draw.text((0, 16*i), line, (255,255,255), font=font)
source to share