Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@turicas
Created December 10, 2011 19:04
Show Gist options
  • Star 83 You must be signed in to star a gist
  • Fork 24 You must be signed in to fork a gist
  • Save turicas/1455973 to your computer and use it in GitHub Desktop.
Save turicas/1455973 to your computer and use it in GitHub Desktop.
Layer on top of Python Imaging Library (PIL) to write text in images easily
#!/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)
@josephkern
Copy link

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:
djrfxwkwsaair0k

Second one; too much coverage on the X axis:
djpsn3mvwaa7cjk

@josephkern
Copy link

@bsuvorov
Copy link

bsuvorov commented Dec 8, 2017

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.

@aaronneous
Copy link

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.

@2jl
Copy link

2jl commented Jun 2, 2018

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

@Pathen85
Copy link

Pathen85 commented Jul 9, 2018

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.

Copy link

ghost commented Aug 22, 2018

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.

Copy link

ghost commented Aug 22, 2018

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.

@StVl
Copy link

StVl commented Jan 15, 2019

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"

@leikoilja
Copy link

Awesome job!
Thank you @turicas and the community 👍 💯

@daninsky1
Copy link

Great script dude, thanks!

@CayoM
Copy link

CayoM commented Feb 24, 2020

really helpful tool here! got the job done properly!

@pojda
Copy link

pojda commented May 11, 2020

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

@GabrielePicco
Copy link

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:

  1. Download an image: curl https://i.imgur.com/XQCKcC9.jpg -o ./image.jpg

  2. Download a font: curl https://fonts.google.com/download?family=Roboto -o ./roboto.zip ; unzip ./roboto.zip -d ./Roboto

  3. 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:

68747470733a2f2f692e696d6775722e636f6d2f57433865704f672e6a7067

@simucentral
Copy link

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.

@jeffsdata
Copy link

@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,

@freqmand
Copy link

justify option not working properly with RTL languages like Arabic/Persian

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment