-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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) |
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.
Well done!
Any chance the text box handles new lines in text?
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)
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' )
Fixed this by moving height += text_height
to the end of its loop in write_text_box
.
justify option not working properly with RTL languages like Arabic/Persian
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' )
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
+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.