简体   繁体   English

PIL - 在图像上绘制多行文本

[英]PIL - draw multiline text on image

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.我尝试在图像底部添加文本,实际上我已经完成了,但是如果我的文本比图像宽度长,它会从两侧被切割,为了简化我希望文本在多行中,如果它是长于图像宽度。 Here is my code:这是我的代码:

FOREGROUND = (255, 255, 255)
WIDTH = 375
HEIGHT = 50
TEXT = 'Chyba najwyższy czas zadać to pytanie na śniadanie \n Chyba najwyższy czas zadać to pytanie na śniadanie'
font_path = '/Library/Fonts/Arial.ttf'
font = ImageFont.truetype(font_path, 14, encoding='unic')
text = TEXT.decode('utf-8')
(width, height) = font.getsize(text)

x = Image.open('media/converty/image.png')
y = ImageOps.expand(x,border=2,fill='white')
y = ImageOps.expand(y,border=30,fill='black')

w, h = y.size
bg = Image.new('RGBA', (w, 1000), "#000000")

W, H = bg.size
xo, yo = (W-w)/2, (H-h)/2
bg.paste(y, (xo, 0, xo+w, h))
draw = ImageDraw.Draw(bg)
draw.text(((w - width)/2, w), text, font=font, fill=FOREGROUND)


bg.show()
bg.save('media/converty/test.png')

You could use textwrap.wrap to break text into a list of strings, each at most width characters long:您可以使用textwrap.wraptext分解为字符串列表,每个字符串的长度最多width字符:

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

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.接受的答案在不测量字体的情况下包装文本(最多 40 个字符,无论字体大小和框宽是多少),因此结果只是近似值,可能很容易溢出或填充不足。

Here is a simple library which solves the problem correctly: https://gist.github.com/turicas/1455973这是一个正确解决问题的简单库: https ://gist.github.com/turicas/1455973

For a complete working example using unutbu 's trick (tested with Python 3.6 and Pillow 5.3.0):对于使用unutbu技巧的完整工作示例(使用 Python 3.6 和 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:结果:

在此处输入图像描述

All recommendations about textwrap usage fail to determine correct width for non-monospaced fonts (as Arial, used in topic example code).所有关于textwrap使用的建议都无法确定非等宽字体的正确宽度(如 Arial,在主题示例代码中使用)。

I've wrote simple helper class to wrap text regarding to real font letters sizing:我编写了简单的帮助类来包装有关实际字体字母大小的文本:

from PIL import Image, ImageDraw

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.但大多数情况下应该没问题。

This function will split the text into rows that are at most max length long when made in font font , then it creates a transparent image with the text on it.此功能会将text拆分为max长度的行,当以 font font制作时,它会创建一个带有文本的透明图像。

def split_text(text, font, max)
    text=text.split(" ")
    total=0
    result=[]
    line=""
    for part in text:
        if total+font.getsize(f"{part} ")[0]<max:
            line+=f"{part} "
            total+=font.getsize(part)[0]
        else:
            line=line.rstrip()
            result.append(line)
            line=f"{part} "
            total=font.getsize(f"{part} ")[0]
    line=line.rstrip()
    result.append(line)
    image=new("RGBA", (max, font.getsize("gL")[1]*len(result)), (0, 0, 0, 0))
    imageDrawable=Draw(image)
    position=0
    for line in result:
        imageDrawable.text((0, position), line, font)
        position+=font.getsize("gL")[1]
    return image

A minimal example, keep adding words until it exceeds the maximum width limit.一个最小的例子,不断添加单词,直到超过最大宽度限制。 The function get_line returns the current line and remaining words, which can again be used in loop, as in draw_lines function below. get_line 函数返回当前行和剩余的单词,它们可以再次循环使用,如下面的 draw_lines 函数。

def get_line(words, width_limit):
    # get text which can fit in one line, remains is list of words left over
    line_width = 0
    line = ''
    i = 0
    while i < len(words) and (line_width + FONT.getsize(words[i])[0]) < width_limit:
        if i == 0:
            line = line + words[i]
        else:
            line = line + ' ' + words[i]
        i = i + 1
        line_width = FONT.getsize(line)[0]
    remains = []
    if i < len(words):
        remains = words[i:len(words)]
    return line, remains


def draw_lines(text, text_box):
    # add some margin to avoid touching borders
    box_width = text_box[1][0] - text_box[0][0] - (2*MARGIN)
    text_x = text_box[0][0] + MARGIN
    text_y = text_box[0][1] + MARGIN
    words = text.split(' ')
    while words:
        line, words = get_line(words, box_width)
        width, height = FONT.getsize(line)
        im_draw.text((text_x, text_y), line, font=FONT, fill=FOREGROUND)
        text_y += height

Easiest solution is to use textwrap + multiline_text function最简单的解决方案是使用 textwrap + multiline_text function

from PIL import Image, ImageDraw
import textwrap

lines = textwrap.wrap("your long text", width=20)
draw.multiline_text((x,y), '\n'.join(lines))

You could use PIL.ImageDraw.Draw.multiline_text() .您可以使用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.您甚至可以使用相同的参数名称设置spacingalign

text = textwrap.fill("test ",width=35)
self.draw.text((x, y), text, font=font, fill="Black")

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM