Hi all,

Here is a way to add text, see attachment. I've extended the test cases
with capitols with diacritics. On the list a question regarding this
arose, so it was good to double check this.

If possible, I would like to have this module included into PIL. Once I
had it revied, but would like to resubmit it again since the PIL list
has become more active again, especially regarding text overlays.

Regards,

Pander


Stani wrote:
> Op woensdag 14-01-2009 om 19:42 uur [tijdzone -0800], schreef Bernard
> Rankin:
>>
>>> Phatch has already a text action which has done the hard work already as
>>> well. Just start from that as a base and hack it to use a csv file as
>>> input instead of a fixed text. If you have played with python and PIL
>>> already, it should be not that hard.
>>>
>> Thanks for the help.
>>
>> I've taken a look at the tutorial and the existing text.py action.
>>
>> I am new to Python, and have never used PIL, but it does look do-able..
>>
>> A couple of questions on the existing text action:
>>
>> 1) Is the the "cache" dict  the only recommended way to maintain state 
>> between invocations?
> Yes.
>> 2) What is the "values" method of the Action class in the text.py file?
> The values method convert relative values (given by the user) into
> absolute values based on image properties (height, width, ...). For
> example for a pixel value a user can enter absolute pixels, metrics (cm,
> mm, ...) or a percentage. In the end we need absolute pixels to work
> with PIL. So metrics are converted to pixels by multiplying with the
> dpi. Percentage are calculated by multiplying with the appropriate image
> property. For example for a horizontal offset of 5%, we take 5% of the
> image width. The font size is based on the average of the image height
> and width.
> 
>> 3) What does the "global" statement in the init() method do, and why is it 
>> there?
> Phatch uses a very simple plugin mechanism. It imports all the actions
> at startup. In order not to load modules which might not be used and to
> keep one action limited to one file, we need a lazy import for modules
> which are only necessary when executing the action. Therefore they are
> grouped in an init method and global is used to make them globally
> available.
> 
>> 4) What is line 38 trying to do, and why is it in the if clause?
> If no (truetype) font is given, Phatch will use the default PIL font
> which AFAIK can not handle unicode (e.g. chinese characters). Therefore
> text needs to be converted to ascii. Phatch choses to replace a
> character with a ? if it can't be encoded.
>> Also, was this file based on the Watermark code?  Many if the code comments 
>> would indicate so.
> Indeed. Many actions are based on other ones. This is the fastest way to
> write new actions.
> 
>>  (Running out of Ubuntu Intrepid repo)
> If you start developing, please checkout phatch from the bazaar
> repository (bzr lp:phatch)
>> Where can I find further docs on the plug-in / action  API?
> I guess you've read already:
> http://photobatch.wikidot.com/writing-actions
> 
> The best is to study and understand the source of the other actions and
> see them in action.
>> Thank you
> You're welcome!
> 
> Stani
> 
> _______________________________________________
> Image-SIG maillist  -  Image-SIG@python.org
> http://mail.python.org/mailman/listinfo/image-sig

# -*- 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
#
# 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"

##
# 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:
            hash = md5()
            hash.update(text)
            hash.update(fontAlign)
            hash.update(fontPath)
            hash.update(str(fontSize))
            hash.update(fontColor)
            hash.update(str(width))
            hash.update(str(height))
            hash.update(color)
            hash.update(str(quality))
            hash.update('Label')
            filename = str(hash.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 width and height
        w = maxWidth
        if height == -1:
            height = len(lines) * h
        
        # create filename
        if filename == '' or fileName == None:
            hash = md5()
            hash.update(text)
            hash.update(fontAlign)
            hash.update(fontPath)
            hash.update(str(fontSize))
            hash.update(fontColor)
            hash.update(str(width))
            hash.update(str(height))
            hash.update(color)
            hash.update(str(quality))
            hash.update('Paragraph')
            filename = str(hash.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