Hi all,

Here is version 0.3 of ImageText.py. I hope it can find a way into the
releases of PIL or contribute to PIL uses one wa or the other. This
version has lots of improvements suggested by PyLint and Python 2.6.
Have fun with it.

Regards,

Pander
# -*- coding: utf-8 -*-
#
# The Python Imaging Library.
# $Id: ImageText.py 2009-01-29 0.2 Pander$
#
# PIL text as images
#
# History:
# 2008-06-26 0.1 Pander initial publication to be reviewed
# 2009-01-29 0.2 Pander extended unicode tests with capitals with diacritics
# 2009-03-10 0.3 Pander upgraded to Python 2.6 and corrected PyLint warnings
#
# Inspired by http://plone.org/products/TextImage which is from the same author.
#
# Todo:
# - Implement functionality to use background image.
# - Implement workaround for problem with invisible first character for small
#   fonts, until this bug is fixed in ImageFont.
# - Implement unit test that can verify output images.
# - Implement returning result as a MIME string.
#
# See the README file for information on usage and redistribution.
#

import PIL.Image
import PIL.ImageDraw
import PIL.ImageFont
import os
from hashlib import md5

##
# The <b>ImageText</b> module defines a class with the same name.
# It offers static methods to create images displaying specified text by using
# the ImageText and ImageDraw classes.
# <p>
# Either labels or wrapped paragraphs for UTF-8 text can be generated with a
# range of parameters like output file (name and directory), font (align, path,
# size and color) and background (width, height, color, transparency) and more.
# <p>
# Effort has been made in setting sane default values for all parameters. See
# test cases.
#
# @see ImageDraw#ImageDraw.text
# @see ImageFont#ImageText.truetype
class ImageText:
    "PIL font wrapper"

##
# Default empty constructor
    def __init__(self):
        pass

##
# Get width and height for specified text if it would be rendered in specified font.
#
# @param text Text for which width will be measured. Default is set to ' '.
# @param font Font to be used to measure with.
# @return Text width and height in a list of integers.
    def getWidthHeight(font, text=' '):
        # determine text width and height
        return PIL.ImageDraw.Draw(PIL.Image.new(mode='RGB', size=(0, 0))).textsize(text, font=font)
    getWidthHeight = staticmethod(getWidthHeight)

##
# Get width for specified text if it would be rendered in specified font.
#
# @param text Text for which width will be measured.
# @param font Font to be used to measure with.
# @return Text width as integer.
    def getWidth(font, text=' '):
        # determine text width
        return PIL.ImageDraw.Draw(PIL.Image.new(mode='RGB', size=(0, 0))).textsize(text, font=font)[0]
    getWidth = staticmethod(getWidth)

##
# Generate label with specified text rendered in it.
#
# @param text Text that will be rendered as a label, i.e. a single line of text.
# @param filename Name of output file which will have to have '.png' extension. Default is MD5 hash according to unique set of specified parameters with '.png' extension.
# @param directory Directory to which output file will be writting. Default is the current directory '.'.
# @param fontAlign Align text lef, center, centre or right. Default is left.
# @param fontPath Path to font used for redering. When set to '' or None, a default font will be used.
# @param fontSize Font size used in rendering. Default is 16.
# @param fontColor Font color used in rendering Default is '#000000' which is black.
# @param widht Width of the resulting background image. Default is -1, which results in automatic setting.
# @param height Height of the resulting backgound image. Default is -1, which results in automatic setting.
# @param color Color of the resulting backgroun image like '#FF0000' or transparancy when set to 'transparent'.
# @param quality Image quality of output image file format. Default is 100 which means no data reduction.
# @param overwrite Should existing generated image be overwritten.
# @return String of path to generated image.
# @exception SyntaxError If align is not 'left', 'center', 'centre' or 'right'.
    def generateLabel(text='The quick brown fox jumps over the lazy dog.',
                 filename='',
                 directory='.',
                 fontAlign='left',
                 fontPath='',
                 fontSize=16,
                 fontColor='#000000',
                 width=-1,
                 height=-1,
                 color='transparent',
                 quality=100,
                 overwrite=False):
        # validate parameters
        fontAlign = fontAlign.lower()
        if fontAlign not in ('left', 'center', 'centre', 'right'):
            raise SyntaxError('Unsupported align ' + fontAlign)
        fontColor = fontColor.lower()
        color = color.lower()

        # configure font
        font = None
        if fontPath == '' or fontPath == None:
            font = PIL.ImageFont.load_default()
        else:
            font = PIL.ImageFont.truetype(fontPath, fontSize)
        
        # determine text width and height
        w, h = ImageText.getWidthHeight(font=font, text=text)
        if width == -1:
            width = w
        if height == -1:
            height = h

        # create filename
        if filename == '' or filename == None:
            md5hash = md5()
            md5hash.update(text)
            md5hash.update(fontAlign)
            md5hash.update(fontPath)
            md5hash.update(str(fontSize))
            md5hash.update(fontColor)
            md5hash.update(str(width))
            md5hash.update(str(height))
            md5hash.update(color)
            md5hash.update(str(quality))
            md5hash.update('Label')
            filename = str(md5hash.hexdigest()) + '.png'
        path = directory + '/' + filename
        if not overwrite and os.path.exists(path):
            return filename
                
        # align text
        x = 0
        if fontAlign == 'left':
            x = 0
        elif fontAlign in ('center', 'centre'):
            x = (width - w) / 2
        elif fontAlign == 'right':
            x = width - w
        y = (height - h) / 2

        image = None
        if color == 'transparent':
            image = PIL.Image.new(mode='RGBA', size=(width, height), color=(0,0,0,0))
        else:
            image = PIL.Image.new(mode='RGB', size=(width, height), color=color)
        image.info['quality'] = quality
        draw = PIL.ImageDraw.Draw(image)
        draw.text((x, y), text.decode('utf-8'), font=font, fill=fontColor)
        image.save(path, quality=quality)
        
        return filename
    generateLabel = staticmethod(generateLabel)

##
# Generate paragraph with specified text wrapped and rendered in it.
#
# @param text Text that will be rendered as a label, i.e. a single line of text.
# @param filename Name of output file which will have to have '.png' extension. Default is MD5 hash according to unique set of specified parameters with '.png' extension.
# @param directory Directory to which output file will be writting. Default is the current directory '.'.
# @param fontAlign Align text lef, center, centre or right. Default is left.
# @param fontPath Path to font used for redering. When set to '' or None, a default font will be used.
# @param fontSize Font size used in rendering. Default is 16.
# @param fontColor Font color used in rendering Default is '#000000' which is black.
# @param widht Width of the resulting background image. Default is -1, which results in automatic setting.
# @param height Height of the resulting backgound image. Default is -1, which results in automatic setting.
# @param color Color of the resulting backgroun image like '#FF0000' or transparancy when set to 'transparent'.
# @param quality Image quality of output image file format. Default is 100 which means no data reduction.
# @param overwrite Should existing generated image be overwritten.
# @return String of path to generated image.
# @exception SyntaxError If align is not 'left', 'center', 'centre' or 'right'.
# @exception SyntaxError If unable to wrap text because width is too small.
    def generateParagraph(text='Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
                 filename='',
                 directory='.',
                 fontAlign='left',
                 fontPath='',
                 fontSize=16,
                 fontColor='#000000',
                 width=512,
                 height=-1,
                 color='transparent',
                 quality=100,
                 overwrite=False):
        # validate parameters
        if text.find(' ') == -1:
            return ImageText.generateLabel(text=text,
                 filename=filename,
                 directory=directory,
                 fontAlign=fontAlign,
                 fontPath=fontPath,
                 fontSize=fontSize,
                 fontColor=fontColor,
                 width=width,
                 height=height,
                 color=color,
                 quality=quality,
                 overwrite=overwrite)
        fontAlign = fontAlign.lower()
        if fontAlign not in ('left', 'center', 'centre', 'right'):
            raise SyntaxError('Unsupported align ' + fontAlign)
        fontColor = fontColor.lower()
        color = color.lower()

        # configure font
        font = None
        if fontPath == '' or fontPath == None:
            font = PIL.ImageFont.load_default()
        else:
            font = PIL.ImageFont.truetype(fontPath, fontSize)

        # determine lines
        words = text.split()
        lines = []
        first = True
        line = ''
        lineWidth = 0
        maxWidth = 0
        spaceWidth, h = ImageText.getWidthHeight(font=font)
        for word in words:
            wordWidth = ImageText.getWidth(font=font, text=word)
            if (wordWidth > width):
                raise SyntaxError('Unable to wrap text because width is too small')
            if first:
                first = False
                line = word
                lineWidth = wordWidth
            else:
                if (lineWidth + spaceWidth + wordWidth > width):
                    lines.append((lineWidth, line))
                    if (lineWidth > maxWidth):
                        maxWidth = lineWidth
                    line = word
                    lineWidth = wordWidth
                else:
                    line = line + ' ' + word
                    lineWidth = lineWidth + spaceWidth + wordWidth
        if not first:
            lines.append((lineWidth, line))
        
        # determine text height
        if height == -1:
            height = len(lines) * h
        
        # create filename
        if filename == '' or filename == None:
            md5hash = md5()
            md5hash.update(text)
            md5hash.update(fontAlign)
            md5hash.update(fontPath)
            md5hash.update(str(fontSize))
            md5hash.update(fontColor)
            md5hash.update(str(width))
            md5hash.update(str(height))
            md5hash.update(color)
            md5hash.update(str(quality))
            md5hash.update('Paragraph')
            filename = str(md5hash.hexdigest()) + '.png'
        path = directory + '/' + filename
        if not overwrite and os.path.exists(path):
            return filename
                
        # align text
        x = 0
        y = (height - len(lines) * h) / 2

        # create image
        image = None
        if color == 'transparent':
            image = PIL.Image.new(mode='RGBA', size=(width, height), color=(0,0,0,0))
        else:
            image = PIL.Image.new(mode='RGB', size=(width, height), color=color)
        image.info['quality'] = quality
        draw = PIL.ImageDraw.Draw(image)
        for line in lines:
            if fontAlign == 'left':
                x = 0
            elif fontAlign in ('center', 'centre'):
                x = (width - line[0]) / 2
            elif fontAlign == 'right':
                x = width - line[0]
            draw.text((x, y), line[1].decode('utf-8'), font=font, fill=fontColor)
            y = y + h
        image.save(path, quality=quality)
        
        return filename
    generateParagraph = staticmethod(generateParagraph)

##
# Test code in main.
#  
if __name__ == '__main__':
    # tests for label

    # default
    print ImageText.generateLabel()
    # m- and x-width as well as total height with background color
    print ImageText.generateLabel(text='moooom', color='#ffffff')
    print ImageText.generateLabel(text='MbpqdM', color='#ffffff')
    # alignment
    print ImageText.generateLabel(text='left', color='#ffffff', width=128, overwrite=True)
    print ImageText.generateLabel(text='center', color='#ffffff', width=128, fontAlign='center', overwrite=True)
    print ImageText.generateLabel(text='right', color='#ffffff', width=128, fontAlign='right', overwrite=True)
    # unicode
    print ImageText.generateLabel(text='eèéëêøæüÈÉËÊ', filename='eèéëêøæüÈÉËÊ.png', color='#ffffff', fontPath='/usr/share/fonts/truetype/freefont/FreeSans.ttf')
    # sometimes first character of slim fonts are not displayed
    print ImageText.generateLabel(text='iiiiii', color='#ffffff')
    print ImageText.generateLabel(text=' iiiiii', color='#ffffff')
    print ImageText.generateLabel(text='iiiiii', color='#ffffff', fontPath='/usr/share/fonts/truetype/ttf-liberation/LiberationSerif-Regular.ttf')
    print ImageText.generateLabel(text=' iiiiii', color='#ffffff', fontPath='/usr/share/fonts/truetype/ttf-liberation/LiberationSerif-Regular.ttf')
    print ImageText.generateLabel(text='iiiiii=6*i without preceeding space', color='#ffffff')
    print ImageText.generateLabel(text='iiiiii=6*i without preceeding space', color='#ffffff', fontPath='/usr/share/fonts/truetype/ttf-liberation/LiberationSerif-Regular.ttf')
    print ImageText.generateLabel(text=' iiiiii=6*i with spaceding space', color='#ffffff', fontPath='/usr/share/fonts/truetype/ttf-liberation/LiberationSerif-Regular.ttf')
    print ImageText.generateLabel(text=' iiiiii=6*i with spaceding space', color='#ffffff')

    # tests for paragraph

    # default with overwriting already existing output
    print ImageText.generateParagraph(overwrite=True)
    # alignment
    print ImageText.generateParagraph(fontAlign='center', overwrite=True)
    print ImageText.generateParagraph(fontAlign='right', overwrite=True)
    print ImageText.generateParagraph(fontAlign='right', width=320, height=320, overwrite=True)
    # unicode
    print ImageText.generateParagraph(text='eèéëêøæüÈÉËÊ', filename='eèéëêøæüÈÉËÊ.png', color='#ffffff', fontPath='/usr/share/fonts/truetype/freefont/FreeSans.ttf')

# old code
#    def getDefaultFontPath():
#        # most common sans font on Linux
#        fontPath='/usr/share/fonts/truetype/freefont/FreeSans.ttf'
#        if not os.path.exists(fontPath):
#            # most common sans font on OSX
#            fontPath='/Library/Fonts/Arial.ttf'
#        if not os.path.exists(fontPath):
#            # most common sans font on Windows
#            fontPath = 'C:/WINDOWS/Fonts/arial.ttf'
#        if not os.path.exists(fontPath):
#            raise SyntaxError('Cound not find default font')
#        return fontPath
#    getDefaultFontPath = staticmethod(getDefaultFontPath)
_______________________________________________
Image-SIG maillist  -  Image-SIG@python.org
http://mail.python.org/mailman/listinfo/image-sig

Reply via email to