Simple word wrapping algorithm for Python's PIL
PIL is a powerful image manipulation library for Python. It can do just about any low-level image transformation you'd want, everything from enhancing brightness levels, drawing basic geometric shapes, and even obscure tasks like calculating the root-mean-square of an image's histogram.
One practical application of PIL is to automate and quickly generate image files on-the-fly. A lot of web developers use it for this purpose, often to resize large images to a standard "thumbnail" version as for a user profile pic. PIL is stable and widely available, making it well suited for these tasks.
Recently I decided to use PIL to build images out of heavy text-based content. I needed it to generate images containing text and JPEG image data pulled from a Django database.
Drawing Text with PIL
PIL's ImageDraw module is the primary place to access drawing tools, like primitive lines, arcs, rectangles, et cetera. It also offers two methods related to text: text() and textsize(). Here is example usage for both:
>>> drawer = ImageDraw(img)
>>> drawer.text((0, 0), "Hello world!")
>>> w, h = drawer.textsize("Hello world!")
The text() method draws a string (or unicode, depending on the font) to the (x, y) position given in the first argument. Several keyword options are available for text(), but the most useful is font, which specifies an instance of PIL's own ImageFont class. ImageFont is able to load TrueType and OpenType font formats. If no font is specified, the default (very blocky, monospaced, may vary by system) font is used.
The textsize() method simply returns the width and height of some text in pixels. This can also takes a font keyword argument, which will calculate the size in the given font.
Calculating Text Positions
The basic text drawing method will start the text at the given (x, y) location and draw forever. If you want to make sure the string fits within the constraints of the image, you must do some additional work.
Wikipedia's word wrap article discusses two word wrapping algorithms: minimum length and minimum raggedness. The following Python code implements the former, which is arguably the least aesthetic, but is easier to implement.
This function draws the text string to the img Image
object at a position specified by the xpos and ypos keyword arguments. The
text will be given max_width pixels in width before it is wrapped and it
will wrap on whitespace boundaries.
def draw_word_wrap(img, text, xpos=0, ypos=0, max_width=130,
fill=(0,0,0), font=ImageFont.load_default()):
'''Draw the given ``text`` to the x and y position of the image, using
the minimum length word-wrapping algorithm to restrict the text to
a pixel width of ``max_width.``
'''
draw = ImageDraw(img)
text_size_x, text_size_y = draw.textsize(text, font=font)
remaining = max_width
space_width, space_height = draw.textsize(' ', font=font)
# use this list as a stack, push/popping each line
output_text = []
# split on whitespace...
for word in text.split(None):
word_width, word_height = draw.textsize(word, font=font)
if word_width + space_width > remaining:
output_text.append(word)
remaining = max_width - word_width
else:
if not output_text:
output_text.append(word)
else:
output = output_text.pop()
output += ' %s' % word
output_text.append(output)
remaining = remaining - (word_width + space_width)
for text in output_text:
draw.text((xpos, ypos), text, font=font, fill=fill)
ypos += text_size_y
This works by measuring each word, if it's longer than the remaining space
on the line, start a new line by pushing the word onto the output_text list.
If there's room for the word, and output_text is empty, start a line by
appending the word. Otherwise, get the current line from the list and add the
current word to the end of the line text. Either way, update the remaining
space at the end of every loop.
Every string in the output_text list represents a line of text that is less
than the max_width argument to the function. The final part is simple: loop
over each line and draw it to the image at the approriate coordinates. After
each line, update the ypos for the next line by adding the height of the
the text calculated for this font.