[Image-SIG] Proof-of-concept version of pildriver

Eric S. Raymond esr@snark.thyrsus.com
Sat, 18 Jul 1998 12:08:48 -0400


--ibTvN161/egqYuK8
Content-Type: text/plain; charset=us-ascii

Here's the first alpha version.  It's not complete, and there are some
things in it that don't yet work right (either due to underlying PIL
problems or because I don't understand intended usage).  It should
demonstrate, however, that pildriver has the potential to replace
several of the ad-hoc scripts in the present distribution with a
single simple utility that serves as an image batch processor, a test
driver, and a tutorial exerciser.

I'd have documented this, but I don't know what the preferred 
format for PIL documentation is, or if there is one.

Feedback, comments, and corrections are solicited.
-- 
		<a href="http://www.tuxedo.org/~esr">Eric S. Raymond</a>

To make inexpensive guns impossible to get is to say that you're
putting a money test on getting a gun.  It's racism in its worst form.
        -- Roy Innis, president of the Congress of Racial Equality (CORE), 1988

--ibTvN161/egqYuK8
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename=pildriver

#!/usr/bin/env python
#
# pildriver -- an image-processing calculator for PIL
#
# by Eric S. Raymond <esr@thyrsus.com>
# $Id: pildriver,v 1.1 1998/07/18 16:08:25 esr Exp $
#
# This is essentially a Polish-notation interpreter for sequencing
# PIL image transformations.  All operations consume their arguments
# off the stack (use `dup' to keep copies around).  Use `verbose 1`
# to see the stack state before each operation.
#
# PILDriver doesn't catch any exceptions, on the theory that these
# are actually diagnostic information that should be interpreted by
# the calling code.
#
# TO DO:
# 1. Add PILFont capabilities, once that's documented.
# 2. Add PILDraw operations.
# 3. Add support for composing and decomposing multiple-image files.
#

import Image, string

class PILDriver:

    verbose = 0

    def do_verbose(self):
        # Set verbosity flag from top of stack
        self.verbose = self.do_pop()

    # The evaluation stack (internal only)

    stack = []		# Stack of pending operations

    def push(self, item):
        # Push an argument onto the stack
        self.stack = [item] + self.stack

    def top(self):
        # Return the top-of-stack element
        return self.stack[0]

    # Stack manipulation (callable)

    def do_clear(self):
        # Clear the stack
        self.stack = []

    def do_pop(self):
        # Pop (and return) the top element on the stack
        top = self.stack[0]
        self.stack = self.stack[1:]
        return top

    def do_dup(self):
        # Duplicate the top-of-stack item
        if hasattr(self, 'format'):	# If it's an image, do a real copy 
            dup = self.stack[0].copy()
        else:
             dup = self.stack[0]
        self.stack = [dup] + self.stack

    def do_swap(self):
        # Swap the top-of-stack item with the next one down
        self.stack = [self.stack[1], self.stack[0]] + self.stack[2:]

    # Image module functions (callable)

    def do_new(self):
        # Create a greyscale image of given size and color, push on stack
        color = int(self.do_pop())
        ysize = int(self.do_pop())
        xsize = int(self.do_pop())
        self.push(Image.new("L", (xsize, ysize), color))

    def do_open(self):
        # Open the indicated image, read it, push it on the stack
    	self.push(Image.open(self.do_pop()))

    def do_blend(self):
        # Replace two images and an alpha with the blended image 
        alpha = float(self.do_pop())
        image2 = self.do_pop()
        image1 = self.do_pop()
        self.push(Image.blend(image1, image2, alpha))

    def do_composite(self):
        # Replace two images and a mask with their composite
        mask = self.do_pop()
        image2 = self.do_pop()
        image1 = self.do_pop()
        self.push(Image.composite(image1, image2, mask))

    def do_merge(self):
        # Merge top-of stack images in a way described by the mode
        bandlist = []
        mode = self.do_pop()
        for band in mode:
            bandlist.append(self.do_pop())
        bandlist.reverse()
        self.push(Image.merge(mode, bandlist))

    # Image class methods

    def do_convert(self):
        # Convert the top image to the given mode
        mode = self.do_pop()
        image = self.do_pop()
        self.push(image.convert(mode))

    def do_copy(self):
        # Make a true copy of the top image, push it on the stack
        self.dup()

    def do_crop(self):
        # Crop a rectangular region from the current image
        lower = int(self.do_pop())
        right = int(self.do_pop())
        upper = int(self.do_pop())
        left = int(self.do_pop())
        image = self.do_pop()
        self.push(image.crop((left, upper, right, lower)))

    def do_draft(self):
        # Configure the loader for a given mode
        ysize = int(self.do_pop())
        xsize = int(self.do_pop())
        mode = self.do_pop()
        self.push(self.draft(mode, (xsize, ysize)))

    def do_filter(self):
        # Filter the top image with the given filter
        import ImageFilter
        filter = eval("ImageFilter." + string.upper(self.do_pop()))
        image = self.do_pop()
        self.push(image.filter(filter))

    def do_offset():
        # Offset the top image
        yoff = int(self.do_pop())
        xoff = int(self.do_pop())
        image = self.do_pop()
        self.push(image.offset(xoff, yoff))

    def do_paste(self):
        # Interpret top image as figure, next down as ground
        yoff = int(self.do_pop())
        xoff = int(self.do_pop())
        figure = self.do_pop()
        ground = self.do_pop()
        if figure.mode == "RGBA":
            self.push(ground.paste(figure, (xoff, yoff), figure))
        else:
            self.push(ground.paste(figure, (xoff, yoff)))

    def do_resize(self):
        # Resize the top image
        ysize = int(self.do_pop())
        xsize = int(self.do_pop())
        image = self.do_pop()
        self.push(image.resize(xsize, ysize))

    def do_rotate(self):
        # Rotate image through a given angle
        angle = int(self.do_pop())
        image = self.do_pop()
        self.push(image.rotate(angle))

    def do_save(self):
        # Save image with default options
        filename = self.do_pop()
        image = self.do_pop()
        image.save(filename)

    def do_save2(self):
        # Save image with specified options
        filename = self.do_pop()
        options = self.do_pop()
        image = self.do_pop()
        image.save(filename, None, options)

    def do_show(self):
        # Display the top image in the stack
        self.pop().show()

    def do_thumbnail(self):
        # Modify the top image in the stack to contain a thumbnail of itself
        ysize = int(self.do_pop())
        xsize = int(self.do_pop())
        self.top().thumbnail((xsize, ysize))

    def do_transpose(self):
        # Transpose the top image
        transpose = string.upper(self.do_pop())
        image = self.do_pop()
        self.push(image.transpose(transpose))

    # Image attributes

    def do_format(self):
        # Push the format of the top image onto the stack
        self.push(self.pop().format)

    def do_mode(self):
        # Push the mode of the top image onto the stack
        self.push(self.pop().mode)

    def do_size(self):
        # Push the image size on the stack as (y, x)
        size = self.pop().size
        self.push(size[0])
        self.push(size[1])

    # ImageChops operations

    def do_invert(self):
        # Invert the top image
        import ImageChops
        self.push(ImageChops.invert(self.do_pop()))

    def do_lighter(self):
        # Pop the two top images, push an image of the lighter pixels of both
        import ImageChops
        image2 = self.do_pop()
        image1 = self.do_pop()
        self.push(ImageChops.lighter(image1, image2))

    def do_darker(self):
        # Pop the two top images, push an image of the darker pixels of both
        import ImageChops
        image2 = self.do_pop()
        image1 = self.do_pop()
        self.push(ImageChops.darker(image1, image2))

    def do_difference(self):
        # Pop the two top images, push the difference image
        import ImageChops
        image2 = self.do_pop()
        image1 = self.do_pop()
        self.push(ImageChops.difference(image1, image2))

    def do_multiply(self):
        # Pop the two top images, push the multiplication image
        import ImageChops
        image2 = self.do_pop()
        image1 = self.do_pop()
        self.push(ImageChops.multiply(image1, image2))

    def do_screen(self):
        # Pop the two top images, superimpose their inverted versions
        import ImageChops
        image2 = self.do_pop()
        image1 = self.do_pop()
        self.push(ImageChops.screen(image1, image2))

    def do_add(self):
        # Pop the two top images, produce the scaled sum with offset
        import ImageChops
        offset = float(self.do_pop())
        scale = float(self.do_pop())
        image2 = self.do_pop()
        image1 = self.do_pop()
        self.push(ImageChops.add(image1, image2, scale, offset))

    def do_subtract(self):
        # Pop the two top images, produce the scaled difference with offset
        import ImageChops
        offset = float(self.do_pop())
        scale = float(self.do_pop())
        image2 = self.do_pop()
        image1 = self.do_pop()
        self.push(ImageChops.subtract(image1, image2, scale, offset))

    # ImageEnhance classes

    def do_color(self):
        # Enhance color in the top image
        import ImageEnhance
        factor = float(self.do_pop())
        image = self.do_pop()
        enhancer = ImageEnhance.Color(image)
        self.push(enhancer.enhance(factor))

    def do_contrast(self):
        # Enhance contrast in the top image
        import ImageEnhance
        factor = float(self.do_pop())
        image = self.do_pop()
        enhancer = ImageEnhance.Color(image)
        self.push(enhancer.enhance(factor))

    def do_brightness(self):
        # Enhance brightness in the top image
        import ImageEnhance
        factor = float(self.do_pop())
        image = self.do_pop()
        enhancer = ImageEnhance.Color(image)
        self.push(enhancer.enhance(factor))

    def do_sharpness(self):
        # Enhance sharpness in the top image
        import ImageEnhance
        factor = float(self.do_pop())
        image = self.do_pop()
        enhancer = ImageEnhance.Color(image)
        self.push(enhancer.enhance(factor))

    # The interpreter loop

    def execute(self, list):
        # Interpret a list of PILDriver commands
        list.reverse()
        while len(list) > 0:
            self.push(list[0])
            list = list[1:]
            if self.verbose:
                print "Stack: " + `self.stack`
            top = self.top()
            if type(top) != type(""):
                continue;
            funcname = "do_" + top
            if not hasattr(self, funcname):
                continue
            else:
                self.do_pop()
                func = getattr(self, funcname)
                func()

if __name__ == '__main__': 
    import readline, sys

    # If we see command-line arguments, interpret them as a stack state
    # and execute.  Otherwise go interactive.

    driver = PILDriver()
    if len(sys.argv[1:]) > 0:
        driver.execute(sys.argv[1:])
    else:
        print "PILDriver says hello"
        while 1:
            try:
                line = raw_input('pildriver> ');
            except EOFError:
                print "\nPILDriver says goodbye"
                break
            driver.execute(string.split(line))
            print driver.stack

# The following sets edit modes for GNU EMACS
# Local Variables:
# mode:python
# End:

--ibTvN161/egqYuK8--