python PIL draw multiline text on image

后端 未结 6 1091
野的像风
野的像风 2020-12-04 10:14

I try to add text at the bottom of image and actually I\'ve done it, but in case of my text is longer then image width it is cut from both sides, to simplify I would like te

相关标签:
6条回答
  • 2020-12-04 10:39

    All recommendations about textwrap usage fail to determine correct width for non-monospaced fonts (as Arial, used in topic example code).

    I've wrote simple helper class to wrap text regarding to real font letters sizing:

    class TextWrapper(object):
        """ Helper class to wrap text in lines, based on given text, font
            and max allowed line width.
        """
    
        def __init__(self, text, font, max_width):
            self.text = text
            self.text_lines = [
                ' '.join([w.strip() for w in l.split(' ') if w])
                for l in text.split('\n')
                if l
            ]
            self.font = font
            self.max_width = max_width
    
            self.draw = ImageDraw.Draw(
                Image.new(
                    mode='RGB',
                    size=(100, 100)
                )
            )
    
            self.space_width = self.draw.textsize(
                text=' ',
                font=self.font
            )[0]
    
        def get_text_width(self, text):
            return self.draw.textsize(
                text=text,
                font=self.font
            )[0]
    
        def wrapped_text(self):
            wrapped_lines = []
            buf = []
            buf_width = 0
    
            for line in self.text_lines:
                for word in line.split(' '):
                    word_width = self.get_text_width(word)
    
                    expected_width = word_width if not buf else \
                        buf_width + self.space_width + word_width
    
                    if expected_width <= self.max_width:
                        # word fits in line
                        buf_width = expected_width
                        buf.append(word)
                    else:
                        # word doesn't fit in line
                        wrapped_lines.append(' '.join(buf))
                        buf = [word]
                        buf_width = word_width
    
                if buf:
                    wrapped_lines.append(' '.join(buf))
                    buf = []
                    buf_width = 0
    
            return '\n'.join(wrapped_lines)
    

    Example usage:

    wrapper = TextWrapper(text, image_font_intance, 800)
    wrapped_text = wrapper.wrapped_text()
    

    It's probably not super-fast, because it renders whole text word by word, to determine words width. But for most cases it should be OK.

    0 讨论(0)
  • 2020-12-04 10:40

    You could use textwrap.wrap to break text into a list of strings, each at most width characters long:

    import textwrap
    lines = textwrap.wrap(text, width=40)
    y_text = h
    for line in lines:
        width, height = font.getsize(line)
        draw.text(((w - width) / 2, y_text), line, font=font, fill=FOREGROUND)
        y_text += height
    
    0 讨论(0)
  • 2020-12-04 10:44
    text = textwrap.fill("test ",width=35)
    self.draw.text((x, y), text, font=font, fill="Black")
    
    0 讨论(0)
  • 2020-12-04 10:49

    You could use PIL.ImageDraw.Draw.multiline_text().

    draw.multiline_text((WIDTH, HEIGHT), TEXT, fill=FOREGROUND, font=font)
    

    You even set spacing or align using the same param names.

    0 讨论(0)
  • 2020-12-04 10:54

    For a complete working example using unutbu's trick (tested with Python 3.6 and Pillow 5.3.0):

    from PIL import Image, ImageDraw, ImageFont
    import textwrap
    
    def draw_multiple_line_text(image, text, font, text_color, text_start_height):
        '''
        From unutbu on [python PIL draw multiline text on image](https://stackoverflow.com/a/7698300/395857)
        '''
        draw = ImageDraw.Draw(image)
        image_width, image_height = image.size
        y_text = text_start_height
        lines = textwrap.wrap(text, width=40)
        for line in lines:
            line_width, line_height = font.getsize(line)
            draw.text(((image_width - line_width) / 2, y_text), 
                      line, font=font, fill=text_color)
            y_text += line_height
    
    
    def main():
        '''
        Testing draw_multiple_line_text
        '''
        #image_width
        image = Image.new('RGB', (800, 600), color = (0, 0, 0))
        fontsize = 40  # starting font size
        font = ImageFont.truetype("arial.ttf", fontsize)
        text1 = "I try to add text at the bottom of image and actually I've done it, but in case of my text is longer then image width it is cut from both sides, to simplify I would like text to be in multiple lines if it is longer than image width."
        text2 = "You could use textwrap.wrap to break text into a list of strings, each at most width characters long"
    
        text_color = (200, 200, 200)
        text_start_height = 0
        draw_multiple_line_text(image, text1, font, text_color, text_start_height)
        draw_multiple_line_text(image, text2, font, text_color, 400)
        image.save('pil_text.png')
    
    if __name__ == "__main__":
        main()
        #cProfile.run('main()') # if you want to do some profiling
    

    Result:

    0 讨论(0)
  • 2020-12-04 11:03

    The accepted answer wraps text without measuring the font (max 40 characters, no matter what the font size and box width is), so the results are only approximate and may easily overfill or underfill the box.

    Here is a simple library which solves the problem correctly: https://gist.github.com/turicas/1455973

    0 讨论(0)
提交回复
热议问题