web-dev-qa-db-ja.com

python PIL画像上に複数行のテキストを描画

画像の下部にテキストを追加しようとしましたが、実際にはそれを行っていますが、テキストが画像の幅よりも長い場合は、両端から切り取られているので、簡単にするために、テキストを複数行にしたい場合は、画像の幅より長い。これが私のコードです:

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

textwrap.wrap は、textを最大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
46
unutbu

受け入れられた回答は、フォントを測定せずにテキストを折り返します(フォントサイズとボックスの幅に関係なく、最大40文字)。そのため、結果は概算にすぎず、ボックスを簡単にオーバーフィルまたはアンダーフィルする可能性があります。

これは問題を正しく解決する単純なライブラリです: https://Gist.github.com/turicas/145597

11
pryma

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

ワード幅を決定するためにテキスト全体をワード単位でレンダリングするため、おそらく超高速ではありません。しかし、ほとんどの場合、問題はありません。

4

nutbtrick を使用した完全な動作例については、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

結果:

enter image description here

1