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.expan开发者_开发技巧d(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:
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.
Here is a simple library which solves the problem correctly: 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):
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).
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.
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.
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
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()
.
draw.multiline_text((WIDTH, HEIGHT), TEXT, fill=FOREGROUND, font=font)
You even set spacing
or align
using the same param names.
NOTE: You need to wrap the text according to your image size vs desired font size.
text = textwrap.fill("test ",width=35)
self.draw.text((x, y), text, font=font, fill="Black")
精彩评论