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!

+1


source to share


2 answers


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])

      

+1


source


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)

      

+1


source







All Articles