在PIL中如何将文本进行换行

23
我正在使用PIL在图像上绘制文本。如何将文本字符串自动换行?以下是我的代码:

...

text = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

image = Image.open("/tmp/background-image.jpg")
draw = ImageDraw.Draw(image)
font = ImageFont.truetype("/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf"), 50, encoding='unic')
draw.text((100, 100), text, font=font, fill="#aa0000")
image.save("/tmp/image.jpg")

导入textwrap标准库,只需在第六行将“text”替换为“textwrap.fill(text)”,即可动态完成。 - jacktrader
7个回答

23
你需要先将文本分成合适长度的行,然后逐行绘制。第二部分很容易实现,但如果使用了可变字体,则第一部分可能需要非常准确地实现。如果使用了固定字体,或者精度不那么重要,那么你可以使用textwrap模块将文本拆分为给定字符宽度的行:
margin = offset = 40
for line in textwrap.wrap(text, width=40):
    draw.text((margin, offset), line, font=font, fill="#aa0000")
    offset += font.getsize(line)[1]

3
这是按字符数拆分,而不是实际宽度 - 即为适应图像。 - User
4
如果有人在2018年仍在阅读此内容,您可以使用 "\n".join(text) 而不是对其进行迭代并创建偏移量。 - Harshith Thota
1
@Curtwagner1984 喜欢这个 "\n".join(textwrap.wrap(text, width=40)),接着绘制文本。 - Bond
我认为这不应该是最佳答案,因为它仅适用于等宽字体。 - drizzt

10

接受的解决方案是基于每行固定40个字符来换行,没有考虑盒子宽度(以像素为单位)和字体大小。这很容易导致填充不足或溢出。

以下是更好的解决方案 - 一个简单的代码片段,考虑到基于字体的宽度测量即可实现文本换行: https://gist.github.com/turicas/1455973


9

当然,如果您每次想换行时都手动添加`\n`是可以的。但如果您有不同的字符串,这并不是最好的方法,但它可以完全控制结果。 但是也有textwrap模块可用。 您可以这样使用:

import textwrap
texto = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
novo = textwrap.wrap(texto, width=20)
print(novo)

结果:

>>> 
['Lorem ipsum dolor', 'sit amet,', 'consectetur', 'adipisicing elit,', 'sed do eiusmod', 'tempor incididunt ut', 'labore et dolore', 'magna aliqua. Ut', 'enim ad minim', 'veniam, quis nostrud', 'exercitation ullamco', 'laboris nisi ut', 'aliquip ex ea', 'commodo consequat.', 'Duis aute irure', 'dolor in', 'reprehenderit in', 'voluptate velit esse', 'cillum dolore eu', 'fugiat nulla', 'pariatur. Excepteur', 'sint occaecat', 'cupidatat non', 'proident, sunt in', 'culpa qui officia', 'deserunt mollit anim', 'id est laborum.']

返回以你指定的宽度包装的先前字符串的术语列表。


9

力求简单并保持一致的行为方式。如其他答案中所述,textwrap 模块只能正确处理定宽字体(因此不是一种一致的解决方案)。以下是一个简单的函数,可以在不断开单词的情况下包装文本,并且适用于可变宽度字体。

from PIL import ImageFont


def get_wrapped_text(text: str, font: ImageFont.ImageFont,
                     line_length: int):
        lines = ['']
        for word in text.split():
            line = f'{lines[-1]} {word}'.strip()
            if font.getlength(line) <= line_length:
                lines[-1] = line
            else:
                lines.append(word)
        return '\n'.join(lines)


if __name__ == '__main__':
    font = ImageFont.truetype('arial.ttf', 12)
    text = 'An example line of text that will need to be wrapped.'
    wrapped_text = get_wrapped_text(text, font, line_length=70)
    # wrapped_text is 'An example\nline of text\nthat will\nneed to be\nwrapped.'

1
好主意,不幸的是代码出了问题,你需要使用 font.getlength(line) 而不是 font.getlength(text) - drizzt
@drizzt 很好的发现,我已经更新以修复这个错误。 - Chris Collett
为了保留已有的换行符,请使用 text.split(' ') - Giacomo Lacava
@GiacomoLacava 注意实现方式...会忽略其他空格字符,例如制表符 '\t'。你需要先按换行符拆分,然后在每行中按单词拆分。 - Chris Collett
1
为了修复代码中删除原始文本中换行符的问题,您可以像这样进行包装:def get_wrapped_text_nlfix(text: str, font: ImageFont.ImageFont, line_length: int): return "\n".join([get_wrapped_text(line, font, line_length) for line in text.splitlines()]) - Pux

4
使用textwrap。它可以在不断开单词的情况下工作。
import textwrap
from PIL import *
caption = "Obama warns far-left candidates says average American does not want to tear down the system"

wrapper = textwrap.TextWrapper(width=50) 
word_list = wrapper.wrap(text=caption) 
caption_new = ''
for ii in word_list[:-1]:
    caption_new = caption_new + ii + '\n'
caption_new += word_list[-1]

image = Image.open('obama.jpg')
draw = ImageDraw.Draw(image)

# Download the Font and Replace the font with the font file. 
font = ImageFont.truetype(text_font, size=font_size)
w,h = draw.textsize(caption_new, font=font)
W,H = image.size
x,y = 0.5*(W-w),0.90*H-h

image.save('output.png')

输入图像

在此输入图片描述

输出图像

在此输入图片描述


3

我不太喜欢使用另一个模块来实现这个功能;我希望只使用PIL中的实用工具就可以完成它。这个函数适用于Python 3.7。

我写了一个函数,根据像素宽度自动换行,然后检查像素高度——如果有无法容纳的单词,就会将其截断并添加省略号以显示省略(而不会超出限制):


from PIL import Image, ImageDraw, ImageFont

def text_wrap(text,font,writing,max_width,max_height):
    lines = [[]]
    words = text.split()
    for word in words:
        # try putting this word in last line then measure
        lines[-1].append(word)
        (w,h) = writing.multiline_textsize('\n'.join([' '.join(line) for line in lines]), font=font)
        if w > max_width: # too wide
            # take it back out, put it on the next line, then measure again
            lines.append([lines[-1].pop()])
            (w,h) = writing.multiline_textsize('\n'.join([' '.join(line) for line in lines]), font=font)
            if h > max_height: # too high now, cannot fit this word in, so take out - add ellipses
                lines.pop()
                # try adding ellipses to last word fitting (i.e. without a space)
                lines[-1][-1] += '...'
                # keep checking that this doesn't make the textbox too wide, 
                # if so, cycle through previous words until the ellipses can fit
                while writing.multiline_textsize('\n'.join([' '.join(line) for line in lines]),font=font)[0] > max_width:
                    lines[-1].pop()
                    if lines[-1]:
                        lines[-1][-1] += '...'
                    else:
                        lines[-1].append('...')
                break
    return '\n'.join([' '.join(line) for line in lines])

使用方法:

bg = Image.open('external/my_background.png')
ws = Image.open('external/my_overlay_with_alpha.png')

writing = ImageDraw.Draw(bg)

title = "My Title"
description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."


title_font = ImageFont.truetype("Arial Black.ttf", size=42)
desc_font = ImageFont.truetype("Arial Narrow Italic.ttf", size=16)

description_wrapped = text_wrap(description,desc_font,writing,160,124)

# write title and description
writing.text((20,5),title,font=title_font)
writing.text((140,120),description_wrapped,font=desc_font)

out = Image.alpha_composite(bg,ws)
out.save('mysubfolder/output.png')

out.show()

谢谢!不过请注意,当边界框太窄以至于只能容纳一个单词时,这段代码会出错。我用以下代码替换了内部 while 循环:lines[-1].pop() if lines[-1]: lines[-1][-1] += '...' else: lines[-1].append('...') - Bryany

0
def img_draw(img_name,tag_line):
    
    img = Image.open(img_name)
    Width_img =img.size
    print(Width_img[0]*0.07)

    wrapper = textwrap.TextWrapper(width=int(Width_img[0]*0.07))
    word_list = wrapper.wrap(text=tag_line)

    print(word_list)

    caption_new = ""

    for ii in word_list:
        caption_new = caption_new+ ii + '\n'

    print(caption_new)

    img2 = Image.open(img_name)
    draw = ImageDraw.Draw(img2)
    fnt = ImageFont.truetype("comicbd.ttf" , 25)
    
    draw.text((60,img.size[1]/1.5),caption_new ,font = fnt ,fill=(255, 0, 0))
    img2.save(img_name)

1
请尽量不要只发布代码答案。尝试包含解释您的解决方案是如何工作的。Solution: 解决方案: - an inconspicuous semicolon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接