Skip to content

Instantly share code, notes, and snippets.

@pojda
Forked from josephkern/example_image_utils.py
Last active April 25, 2024 15:53
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save pojda/8bf989a0556845aaf4662cd34f21d269 to your computer and use it in GitHub Desktop.
Save pojda/8bf989a0556845aaf4662cd34f21d269 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>
from PIL import Image, ImageDraw, ImageFont
import PIL
class ImageText(object):
def __init__(self, filename_or_size_or_Image, mode='RGBA', background=(0, 0, 0, 0),
encoding='utf8'):
if isinstance(filename_or_size_or_Image, str):
self.filename = filename_or_size_or_Image
self.image = Image.open(self.filename)
self.size = self.image.size
elif isinstance(filename_or_size_or_Image, (list, tuple)):
self.size = filename_or_size_or_Image
self.image = Image.new(mode, self.size, color=background)
self.filename = None
elif isinstance(filename_or_size_or_Image, PIL.Image.Image):
self.image = filename_or_size_or_Image
self.size = self.image.size
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 show(self):
self.image.show()
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, xy, text, font_filename, font_size=11,
color=(0, 0, 0), max_width=None, max_height=None):
x, y = xy
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, xy, text, box_width, font_filename,
font_size=11, color=(0, 0, 0), place='left',
justify_last_line=False, position='top',
line_spacing=1.0):
x, y = xy
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] * line_spacing
last_line_bleed = 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]
if position == 'middle':
height = (self.size[1] - len(lines)*text_height + last_line_bleed)/2
height -= text_height # the loop below will fix this height
elif position == 'bottom':
height = self.size[1] - len(lines)*text_height + last_line_bleed
height -= text_height # the loop below will fix this height
else:
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)
@aashsach
Copy link

    if position == 'middle':
        height = (self.size[1] - len(lines)*text_height + last_line_bleed)/2
        height -= text_height # the loop below will fix this height
    elif position == 'bottom':
        height = self.size[1] - len(lines)*text_height + last_line_bleed
        height -= text_height  # the loop below will fix this height
    else:
        height -= y

variable "height" is undefined in the else condition.

@pojda
Copy link
Author

pojda commented Jun 16, 2020

You're right, I've never tested that condition. :) As soon as I get back to work on this project I'll fix that. Or if you have a suggestion just leave it here and I'll add it to the code.

@rich-in-dallas
Copy link

Just set height=y instead of height-=y. Simple typo.

@pojda
Copy link
Author

pojda commented Jun 19, 2020

Just set height=y instead of height-=y. Simple typo.

You're right! Just fixed.

@fnanni-0
Copy link

Actually it should be:
height = y - text_height

@fnanni-0
Copy link

fnanni-0 commented Jul 10, 2020

Also, you could add support for line breaks for the autofill feature by changing get_text_size to something like this

def get_text_size(self, font_filename, font_size, text): 
    total_size = [0, 0]
    lines = text.split('\n')
    for line in lines:
        font = ImageFont.truetype(font_filename, font_size)
        line_size = font.getsize(line)
        total_size[0] = max(total_size[0], line_size[0])
        total_size[1] += line_size[1]
    return tuple(total_size)

@rich-in-dallas
Copy link

The reason it is not height=y-text_height is because that condition is already taken care of by the 'bottom' alignment. This condition is the normal graphics condition of the upper right-hand corner being the origin of the graphic box.

@fnanni-0
Copy link

I'm not sure if I understand what you mean. Take this example...

img = ImageText((250, 250), background=(222, 222, 222, 255))
img.write_text_box(
    (0, 0), 
    'This is a phrase', 
    box_width=200, 
    font_filename=example_font,
    font_size=20, 
    color=(50, 50, 50),
    place='left',
    position='top'
)

This is what I get
wrong

This is what I expect to get
correct

@xtlc
Copy link

xtlc commented Oct 16, 2020

+1 fir fnanni-0's comment, I would expect the same thing. I love using your snippet though, saves me a lot of time tinkering with all the possible cases.

@pojda
Copy link
Author

pojda commented Oct 22, 2020

I gotta say I'm loving this experience, this is the first code I ever do that got this much attention 😅

I'll work on this in the next few days and see if I can add your inputs.

@maju80
Copy link

maju80 commented Jan 4, 2021

Well done!
Any chance the text box handles new lines in text?

@maju80
Copy link

maju80 commented Jan 5, 2021

alright, I just added one more function:

    def write_multi_line_text_box(self, xy, text, box_width, font_filename,
                       font_size=11, color=(0, 0, 0), place='left',
                       justify_last_line=False, position='top',
                       line_spacing=1.0):
        x, y = xy
        height = 0
        
        for l in text.splitlines(True):
            w, h=self.write_text_box((x, y+height), l, box_width, font_filename,
                       font_size, color, place,
                       justify_last_line, position,
                       line_spacing)
            
            height+=h
            if l=="\n":
                height+=self.get_text_size(font_filename, font_size,"dummy")[1]*line_spacing
        return (box_width, height - y)  
    

@TonyStew
Copy link

I'm not sure if I understand what you mean. Take this example...

img = ImageText((250, 250), background=(222, 222, 222, 255))
img.write_text_box(
    (0, 0), 
    'This is a phrase', 
    box_width=200, 
    font_filename=example_font,
    font_size=20, 
    color=(50, 50, 50),
    place='left',
    position='top'
)

This is what I get
wrong

This is what I expect to get
correct

Fixed this by moving height += text_height to the end of its loop in write_text_box.

@freqmand
Copy link

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

@XavierZambrano
Copy link

XavierZambrano commented Jun 16, 2023

I'm not sure if I understand what you mean. Take this example...

img = ImageText((250, 250), background=(222, 222, 222, 255))
img.write_text_box(
    (0, 0), 
    'This is a phrase', 
    box_width=200, 
    font_filename=example_font,
    font_size=20, 
    color=(50, 50, 50),
    place='left',
    position='top'
)

This is what I get
wrong

This is what I expect to get
correct

Change this:

        if position == 'middle':
            height = (self.size[1] - len(lines)*text_height + last_line_bleed)/2
            height -= text_height # the loop below will fix this height
        elif position == 'bottom':
            height = self.size[1] - len(lines)*text_height + last_line_bleed
            height -= text_height  # the loop below will fix this height
        else:
            height = y

to this:

        if position == 'middle':
            height = (self.size[1] - len(lines) * text_height + last_line_bleed) / 2
        elif position == 'bottom':
            height = self.size[1] - len(lines) * text_height + last_line_bleed
        else:
            height = y
        height -= text_height  # the loop below will fix this height

when the position was top the height not had -= text_height, so you never rest text_height and it creates the space between your text and the top

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