【问题标题】:PIL - draw multiline text on imagePIL - 在图像上绘制多行文本
【发布时间】:2011-12-03 15:16:26
【问题描述】:

我尝试在图像底部添加文本,实际上我已经做到了,但如果我的文本比图像宽度长,它会从两侧切割,为了简化我希望文本多行如果它长于图像宽度。这是我的代码:

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

【问题讨论】:

    标签: python image text python-imaging-library


    【解决方案1】:

    您可以使用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
    

    【讨论】:

    • 非常感谢!只需复制和粘贴,它就像一个魅力。你是最棒的:)
    • 40 代表什么?
    • @User 40 表示最大字符数。这意味着它在换行之前最多允许 40 个字符。但是,如果一个单词是 10 个字符,然后下一个是 31 个字符,它将在第一个单词之后立即换行,因为它无法容纳该行的第一个和第二个单词。
    • @teewuane 基于像素做这个怎么样?我们并不总是处理等宽字体
    • @User 如果要获取文本字符串的大小,可以使用 PIL.ImageDraw.ImageDraw.textsize 以像素为单位返回 (width, height) 的元组。你可以使用这个实现一个解决方案,在宽度超过某个阈值之前尝试最长的字符串
    【解决方案2】:
    text = textwrap.fill("test ",width=35)
    self.draw.text((x, y), text, font=font, fill="Black")
    

    【讨论】:

    • 欢迎来到 SO!你能解释一下这是如何解决这个问题的吗?对于其他用户来说,一些额外的文字会大有帮助。
    • 这是@unutbu 答案的更简洁版本。这是目前建议的最好的代码,再加上一点解释应该是公认的答案。
    【解决方案3】:

    您可以使用PIL.ImageDraw.Draw.multiline_text()

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

    您甚至可以使用相同的参数名称设置 spacingalign

    【讨论】:

    • 不行,我刚试过。你能详细说明用例吗?我将一个很长的字符串作为text 传递给它,但它没有换行到下一行。
    • @HassanBaig 你在字符串中使用了断点吗?例如:“Lorem Ipsum 只是印刷\n 和排版行业的虚拟\n 文本。”
    • 文本来自用户输入,因此他们没有使用中断来指定下一行。我必须自己处理溢出。
    • multiline_text 的第一个参数记录为“xy - 文本的左上角”。你必须自己做换行。
    • '\n'.join(textwrap.wrap(TEXT, width=15)) 将为您提供带有新行的文本 - 甚至来自用户输入。
    【解决方案4】:

    接受的答案在不测量字体的情况下换行(最多 40 个字符,无论字体大小和框宽是多少),因此结果只是近似值,可能很容易使框填满或不足。

    这是一个正确解决问题的简单库: https://gist.github.com/turicas/1455973

    【讨论】:

    • 这是最好的答案。我们应该尝试更改已接受的答案,因为它已在 6 年前被接受了..
    【解决方案5】:

    所有关于 textwrap 用法的建议都无法确定非等宽字体的正确宽度(如 Arial,在主题示例代码中使用)。

    我编写了简单的帮助类来包装有关实际字体大小的文本:

    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)
    

    示例用法:

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

    它可能不是超级快,因为它会逐字呈现整个文本,以确定字宽。但大多数情况下应该没问题。

    【讨论】:

      【解决方案6】:

      对于使用 unutbutrick 的完整工作示例(使用 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
      

      结果:

      【讨论】:

      • 应该是最佳答案
      【解决方案7】:

      这个函数会将text分割成最多为max长度的行,当字体为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
      

      【讨论】:

        猜你喜欢
        • 2018-02-03
        • 1970-01-01
        • 1970-01-01
        • 2017-07-07
        • 2012-11-08
        • 2012-07-09
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多