-
-
Save turicas/1455973 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python | |
# coding: utf-8 | |
# You need PIL <http://www.pythonware.com/products/pil/> to run this script | |
# Download unifont.ttf from <http://unifoundry.com/unifont.html> (or use | |
# any TTF you have) | |
# Copyright 2011 Álvaro Justen [alvarojusten at gmail dot com] | |
# License: GPL <http://www.gnu.org/copyleft/gpl.html> | |
from image_utils import ImageText | |
color = (50, 50, 50) | |
text = 'Python is a cool programming language. You should learn it!' | |
font = 'unifont.ttf' | |
img = ImageText((800, 600), background=(255, 255, 255, 200)) # 200 = alpha | |
#write_text_box will split the text in many lines, based on box_width | |
#`place` can be 'left' (default), 'right', 'center' or 'justify' | |
#write_text_box will return (box_width, box_calculed_height) so you can | |
#know the size of the wrote text | |
img.write_text_box((300, 50), text, box_width=200, font_filename=font, | |
font_size=15, color=color) | |
img.write_text_box((300, 125), text, box_width=200, font_filename=font, | |
font_size=15, color=color, place='right') | |
img.write_text_box((300, 200), text, box_width=200, font_filename=font, | |
font_size=15, color=color, place='center') | |
img.write_text_box((300, 275), text, box_width=200, font_filename=font, | |
font_size=15, color=color, place='justify') | |
#You don't need to specify text size: can specify max_width or max_height | |
# and tell write_text to fill the text in this space, so it'll compute font | |
# size automatically | |
#write_text will return (width, height) of the wrote text | |
img.write_text((100, 350), 'test fill', font_filename=font, | |
font_size='fill', max_height=150, color=color) | |
img.save('sample-imagetext.png') |
#!/usr/bin/env python | |
# coding: utf-8 | |
# Copyright 2011 Álvaro Justen [alvarojusten at gmail dot com] | |
# License: GPL <http://www.gnu.org/copyleft/gpl.html> | |
import Image | |
import ImageDraw | |
import ImageFont | |
class ImageText(object): | |
def __init__(self, filename_or_size, mode='RGBA', background=(0, 0, 0, 0), | |
encoding='utf8'): | |
if isinstance(filename_or_size, str): | |
self.filename = filename_or_size | |
self.image = Image.open(self.filename) | |
self.size = self.image.size | |
elif isinstance(filename_or_size, (list, tuple)): | |
self.size = filename_or_size | |
self.image = Image.new(mode, self.size, color=background) | |
self.filename = None | |
self.draw = ImageDraw.Draw(self.image) | |
self.encoding = encoding | |
def save(self, filename=None): | |
self.image.save(filename or self.filename) | |
def get_font_size(self, text, font, max_width=None, max_height=None): | |
if max_width is None and max_height is None: | |
raise ValueError('You need to pass max_width or max_height') | |
font_size = 1 | |
text_size = self.get_text_size(font, font_size, text) | |
if (max_width is not None and text_size[0] > max_width) or \ | |
(max_height is not None and text_size[1] > max_height): | |
raise ValueError("Text can't be filled in only (%dpx, %dpx)" % \ | |
text_size) | |
while True: | |
if (max_width is not None and text_size[0] >= max_width) or \ | |
(max_height is not None and text_size[1] >= max_height): | |
return font_size - 1 | |
font_size += 1 | |
text_size = self.get_text_size(font, font_size, text) | |
def write_text(self, (x, y), text, font_filename, font_size=11, | |
color=(0, 0, 0), max_width=None, max_height=None): | |
if isinstance(text, str): | |
text = text.decode(self.encoding) | |
if font_size == 'fill' and \ | |
(max_width is not None or max_height is not None): | |
font_size = self.get_font_size(text, font_filename, max_width, | |
max_height) | |
text_size = self.get_text_size(font_filename, font_size, text) | |
font = ImageFont.truetype(font_filename, font_size) | |
if x == 'center': | |
x = (self.size[0] - text_size[0]) / 2 | |
if y == 'center': | |
y = (self.size[1] - text_size[1]) / 2 | |
self.draw.text((x, y), text, font=font, fill=color) | |
return text_size | |
def get_text_size(self, font_filename, font_size, text): | |
font = ImageFont.truetype(font_filename, font_size) | |
return font.getsize(text) | |
def write_text_box(self, (x, y), text, box_width, font_filename, | |
font_size=11, color=(0, 0, 0), place='left', | |
justify_last_line=False): | |
lines = [] | |
line = [] | |
words = text.split() | |
for word in words: | |
new_line = ' '.join(line + [word]) | |
size = self.get_text_size(font_filename, font_size, new_line) | |
text_height = size[1] | |
if size[0] <= box_width: | |
line.append(word) | |
else: | |
lines.append(line) | |
line = [word] | |
if line: | |
lines.append(line) | |
lines = [' '.join(line) for line in lines if line] | |
height = y | |
for index, line in enumerate(lines): | |
height += text_height | |
if place == 'left': | |
self.write_text((x, height), line, font_filename, font_size, | |
color) | |
elif place == 'right': | |
total_size = self.get_text_size(font_filename, font_size, line) | |
x_left = x + box_width - total_size[0] | |
self.write_text((x_left, height), line, font_filename, | |
font_size, color) | |
elif place == 'center': | |
total_size = self.get_text_size(font_filename, font_size, line) | |
x_left = int(x + ((box_width - total_size[0]) / 2)) | |
self.write_text((x_left, height), line, font_filename, | |
font_size, color) | |
elif place == 'justify': | |
words = line.split() | |
if (index == len(lines) - 1 and not justify_last_line) or \ | |
len(words) == 1: | |
self.write_text((x, height), line, font_filename, font_size, | |
color) | |
continue | |
line_without_spaces = ''.join(words) | |
total_size = self.get_text_size(font_filename, font_size, | |
line_without_spaces) | |
space_width = (box_width - total_size[0]) / (len(words) - 1.0) | |
start_x = x | |
for word in words[:-1]: | |
self.write_text((start_x, height), word, font_filename, | |
font_size, color) | |
word_size = self.get_text_size(font_filename, font_size, | |
word) | |
start_x += word_size[0] + space_width | |
last_word_size = self.get_text_size(font_filename, font_size, | |
words[-1]) | |
last_word_x = x + box_width - last_word_size[0] | |
self.write_text((last_word_x, height), words[-1], font_filename, | |
font_size, color) | |
return (box_width, height - y) |
Great job!
For those who try it out quickly (like me) without reading too much, save yourself few minutes and change top level imports
from
import Image
import ImageDraw
import ImageFont
import PIL
from PIL import ImageFont
from PIL import Image
from PIL import ImageDraw
to work properly with latest PIL.
Thank you for this @turicas - it's super helpful! Would it be possible to include the ability to control line-breaks? For example, say I have a a multi-line string, I'd like the image output to reflect the line-formatting in the original multi-line string.
Hi @turicas , thanks for the code helped me a lot because I'm new to python, now I'm trying to do it with a for cycle (for a text to be inserted in several images) but I'm having problems with the code, please could you check my code and tell me where I fail. Thank you
`
import os
import PIL
from PIL import ImageFont
from PIL import Image
from PIL import ImageDraw
path = "C:/Users/san_2/Documents/Script/ARCPY/Imag/"
dirs = os.listdir( path )
def write():
for item in dirs:
if os.path.isfile(path+item):
im = Image.open(path+item)
f, e = os.path.splitext(path+item)
font_type = ImageFont.truetype('C:/Windows.old/Windows/Fonts/Arial.ttf',18)
draw = ImageDraw.Draw(im)
draw.text(xy=(20,550),text="Prueba uno",fill=(255,255,255),font=font_type)
draw.save(f + '_text.jpg', 'JPEG', quality=90)
write()
`
Great script. Thank you @turicas ! And thanks to @josephkern for making the Python3 adjustments. However, I noticed a little bug in the code. In the big for loop in write_text_box there is the line
height += text_height
right at the beginning. This line has to be called at the end of the for loop. Otherwise there is a margin with the height of one line above the first line. This means the text is not placed where it's supposed to be placed.
Additionally, I added
font_y_offset = font.getoffset(text)[1]
self.draw.text((x, y - font_y_offset), text, font=font, fill=color)
at the end of write_text. Without this there is always an additional padding above the first line. Compare this to this stackoverflow post.
I have also noticed a small margin on the left side of the text box but couldn't find its origin yet.
This is wonderful code! It's certainly solved the problem I had of properly displaying multiline text.
@aaronmkchan, I wrote up a (less than efficient) solution to the newline problem. It's less than efficient because of duplicated code, but it works for me and that's all I need right now. :-)
In the original, replace lines 71-83:
words = text.split()
for word in words:
new_line = ' '.join(line + [word])
size = self.get_text_size(font_filename, font_size, new_line)
text_height = size[1]
if size[0] <= box_width:
line.append(word)
else:
lines.append(line)
line = [word]
if line:
lines.append(line)
lines = [' '.join(line) for line in lines if line]
with:
words = text.split(' ')
for word in words:
if '\n' in word:
newline_words = word.split('\n')
new_line = ' '.join(line + [newline_words[0]])
size = self.get_text_size(font_filename, font_size, new_line)
text_height = size[1]
if size[0] <= box_width:
line.append(newline_words[0])
else:
lines.append(line)
line = [newline_words[0]]
lines.append(line)
if len(word.split('\n')) > 2:
for i in range(1, len(word.split('\n'))-1): lines.append([newline_words[i]])
line = [newline_words[-1]]
else:
new_line = ' '.join(line + [word])
size = self.get_text_size(font_filename, font_size, new_line)
text_height = size[1]
if size[0] <= box_width:
line.append(word)
else:
lines.append(line)
line = [word]
if line:
lines.append(line)
lines = [' '.join(line) for line in lines]
And I also want to echo @Pathen85's comment about moving the
height += text_height
line to the bottom of the for-loop, instead of having it at the top.
Another comment: since ImageText is its own class, you can't treat it like an Image. However, the program invokes Image and works with that. So if you add the method:
def get_image(self):
return self.image
into the class, you can use that to get back the Image it draws on, then proceed from there.
So, if you want to try it in python3 with custom font, you need to drop line for encode and decode path to font, another way you catch AttributeError: "str" is not have attribute "decode"
Awesome job!
Thank you @turicas and the community 👍 💯
Great script dude, thanks!
really helpful tool here! got the job done properly!
I expanded the work from @josephkern in a previous comment, and added support for a bunch of stuff:
- Vertical centering (middle aligning)
- Vertical bottom aligning
- Can now also send a PIL.Image instance instead of only image size or path
- Supports vertical line spacing too
- Fixed a bug where the first line would always be printed at the wrong Y position
Here it is for everyone: https://gist.github.com/pojda/8bf989a0556845aaf4662cd34f21d269
I recently had to implement the same thing. I have created a package on pypi which might come in handy.
You can add text to an image with this steps:
-
Download an image:
curl https://i.imgur.com/XQCKcC9.jpg -o ./image.jpg
-
Download a font:
curl https://fonts.google.com/download?family=Roboto -o ./roboto.zip ; unzip ./roboto.zip -d ./Roboto
-
pip install pynter
from pynter.pynter import generate_captioned
font_path = './Roboto/Roboto-Regular.ttf'
image_path = './image.jpg'
im = generate_captioned('China lands rover on Mars'.upper(), image_path=image_path, size=(1080, 1350),
font_path=font_path, filter_color=(0, 0, 0, 40))
im.show()
im.convert('RGB').save('drawn_image.jpg')
This will be the result:
Hi! Long Time!
I ran into an error.
After importing "from image_utils import ImageText" it says
def write_text(self, (x, y), text, font_filename, font_size=11,
^
SyntaxError: invalid syntax
The caret symbol is exactly below the first bracket of (x, y).
Any idea why? I am using Python 3.8.
Thank You so much.
@simucentral , you can't have (x,y) as an argument in Python 3.x.x. You'd have to split out x,y as individual arguments:
def write_text(self, x, y, text, font_filename, font_size=11,
justify option not working properly with RTL languages like Arabic/Persian
This is such an odd problem. I think the key here is the write_text_box function. I've been puzzling over this problem. I'll give it a try.
Here's some examples of pictures and quotes with some of my "guessing" I'll post some examples of what yours does. 👍 d
First one; doesn't fill enough (not enough coverage on the Y axis:
Second one; too much coverage on the X axis: