diff options
Diffstat (limited to 'Imaging/Scripts/pildriver.py')
-rw-r--r-- | Imaging/Scripts/pildriver.py | 523 |
1 files changed, 523 insertions, 0 deletions
diff --git a/Imaging/Scripts/pildriver.py b/Imaging/Scripts/pildriver.py new file mode 100644 index 0000000..089e0bd --- /dev/null +++ b/Imaging/Scripts/pildriver.py @@ -0,0 +1,523 @@ +#!/usr/bin/env python +"""PILdriver, an image-processing calculator using PIL. + +An instance of class PILDriver is essentially a software stack machine +(Polish-notation interpreter) for sequencing PIL image +transformations. The state of the instance is the interpreter stack. + +The only method one will normally invoke after initialization is the +`execute' method. This takes an argument list of tokens, pushes them +onto the instance's stack, and then tries to clear the stack by +successive evaluation of PILdriver operators. Any part of the stack +not cleaned off persists and is part of the evaluation context for +the next call of the execute method. + +PILDriver doesn't catch any exceptions, on the theory that these +are actually diagnostic information that should be interpreted by +the calling code. + +When called as a script, the command-line arguments are passed to +a PILDriver instance. If there are no command-line arguments, the +module runs an interactive interpreter, each line of which is split into +space-separated tokens and passed to the execute method. + +In the method descriptions below, a first line beginning with the string +`usage:' means this method can be invoked with the token that follows +it. Following <>-enclosed arguments describe how the method interprets +the entries on the stack. Each argument specification begins with a +type specification: either `int', `float', `string', or `image'. + +All operations consume their arguments off the stack (use `dup' to +keep copies around). Use `verbose 1' to see the stack state displayed +before each operation. + +Usage examples: + + `show crop 0 0 200 300 open test.png' loads test.png, crops out a portion +of its upper-left-hand corner and displays the cropped portion. + + `save rotated.png rotate 30 open test.tiff' loads test.tiff, rotates it +30 degrees, and saves the result as rotated.png (in PNG format). +""" +# by Eric S. Raymond <esr@thyrsus.com> +# $Id: pildriver.py 2813 2006-10-07 10:11:35Z fredrik $ + +# 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): + """usage: verbose <int:num> + + 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 evaluation 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): + """usage: clear + + Clear the stack. + """ + self.stack = [] + + def do_pop(self): + """usage: pop + + Discard the top element on the stack. + """ + top = self.stack[0] + self.stack = self.stack[1:] + return top + + def do_dup(self): + """usage: dup + + 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): + """usage: swap + + 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): + """usage: new <int:xsize> <int:ysize> <int:color>: + + Create and push a greyscale image of given size and color. + """ + xsize = int(self.do_pop()) + ysize = int(self.do_pop()) + color = int(self.do_pop()) + self.push(Image.new("L", (xsize, ysize), color)) + + def do_open(self): + """usage: open <string:filename> + + Open the indicated image, read it, push the image on the stack. + """ + self.push(Image.open(self.do_pop())) + + def do_blend(self): + """usage: blend <image:pic1> <image:pic2> <float:alpha> + + Replace two images and an alpha with the blended image. + """ + image1 = self.do_pop() + image2 = self.do_pop() + alpha = float(self.do_pop()) + self.push(Image.blend(image1, image2, alpha)) + + def do_composite(self): + """usage: composite <image:pic1> <image:pic2> <image:mask> + + Replace two images and a mask with their composite. + """ + image1 = self.do_pop() + image2 = self.do_pop() + mask = self.do_pop() + self.push(Image.composite(image1, image2, mask)) + + def do_merge(self): + """usage: merge <string:mode> <image:pic1> [<image:pic2> [<image:pic3> [<image:pic4>]]] + + Merge top-of stack images in a way described by the mode. + """ + mode = self.do_pop() + bandlist = [] + for band in mode: + bandlist.append(self.do_pop()) + self.push(Image.merge(mode, bandlist)) + + # Image class methods + + def do_convert(self): + """usage: convert <string:mode> <image:pic1> + + 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): + """usage: copy <image:pic1> + + Make and push a true copy of the top image. + """ + self.dup() + + def do_crop(self): + """usage: crop <int:left> <int:upper> <int:right> <int:lower> <image:pic1> + + Crop and push a rectangular region from the current image. + """ + left = int(self.do_pop()) + upper = int(self.do_pop()) + right = int(self.do_pop()) + lower = int(self.do_pop()) + image = self.do_pop() + self.push(image.crop((left, upper, right, lower))) + + def do_draft(self): + """usage: draft <string:mode> <int:xsize> <int:ysize> + + Configure the loader for a given mode and size. + """ + mode = self.do_pop() + xsize = int(self.do_pop()) + ysize = int(self.do_pop()) + self.push(self.draft(mode, (xsize, ysize))) + + def do_filter(self): + """usage: filter <string:filtername> <image:pic1> + + Process 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_getbbox(self): + """usage: getbbox + + Push left, upper, right, and lower pixel coordinates of the top image. + """ + bounding_box = self.do_pop().getbbox() + self.push(bounding_box[3]) + self.push(bounding_box[2]) + self.push(bounding_box[1]) + self.push(bounding_box[0]) + + def do_getextrema(self): + """usage: extrema + + Push minimum and maximum pixel values of the top image. + """ + extrema = self.do_pop().extrema() + self.push(extrema[1]) + self.push(extrema[0]) + + def do_offset(self): + """usage: offset <int:xoffset> <int:yoffset> <image:pic1> + + Offset the pixels in the top image. + """ + xoff = int(self.do_pop()) + yoff = int(self.do_pop()) + image = self.do_pop() + self.push(image.offset(xoff, yoff)) + + def do_paste(self): + """usage: paste <image:figure> <int:xoffset> <int:yoffset> <image:ground> + + Paste figure image into ground with upper left at given offsets. + """ + figure = self.do_pop() + xoff = int(self.do_pop()) + yoff = int(self.do_pop()) + ground = self.do_pop() + if figure.mode == "RGBA": + ground.paste(figure, (xoff, yoff), figure) + else: + ground.paste(figure, (xoff, yoff)) + self.push(ground) + + def do_resize(self): + """usage: resize <int:xsize> <int:ysize> <image:pic1> + + 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): + """usage: rotate <int:angle> <image:pic1> + + Rotate image through a given angle + """ + angle = int(self.do_pop()) + image = self.do_pop() + self.push(image.rotate(angle)) + + def do_save(self): + """usage: save <string:filename> <image:pic1> + + Save image with default options. + """ + filename = self.do_pop() + image = self.do_pop() + image.save(filename) + + def do_save2(self): + """usage: save2 <string:filename> <string:options> <image:pic1> + + 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): + """usage: show <image:pic1> + + Display and pop the top image. + """ + self.do_pop().show() + + def do_thumbnail(self): + """usage: thumbnail <int:xsize> <int:ysize> <image:pic1> + + 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): + """usage: transpose <string:operator> <image:pic1> + + 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): + """usage: format <image:pic1> + + Push the format of the top image onto the stack. + """ + self.push(self.pop().format) + + def do_mode(self): + """usage: mode <image:pic1> + + Push the mode of the top image onto the stack. + """ + self.push(self.pop().mode) + + def do_size(self): + """usage: size <image:pic1> + + 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): + """usage: invert <image:pic1> + + Invert the top image. + """ + import ImageChops + self.push(ImageChops.invert(self.do_pop())) + + def do_lighter(self): + """usage: lighter <image:pic1> <image:pic2> + + Pop the two top images, push an image of the lighter pixels of both. + """ + import ImageChops + image1 = self.do_pop() + image2 = self.do_pop() + self.push(ImageChops.lighter(image1, image2)) + + def do_darker(self): + """usage: darker <image:pic1> <image:pic2> + + Pop the two top images, push an image of the darker pixels of both. + """ + import ImageChops + image1 = self.do_pop() + image2 = self.do_pop() + self.push(ImageChops.darker(image1, image2)) + + def do_difference(self): + """usage: difference <image:pic1> <image:pic2> + + Pop the two top images, push the difference image + """ + import ImageChops + image1 = self.do_pop() + image2 = self.do_pop() + self.push(ImageChops.difference(image1, image2)) + + def do_multiply(self): + """usage: multiply <image:pic1> <image:pic2> + + Pop the two top images, push the multiplication image. + """ + import ImageChops + image1 = self.do_pop() + image2 = self.do_pop() + self.push(ImageChops.multiply(image1, image2)) + + def do_screen(self): + """usage: screen <image:pic1> <image:pic2> + + 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): + """usage: add <image:pic1> <image:pic2> <int:offset> <float:scale> + + Pop the two top images, produce the scaled sum with offset. + """ + import ImageChops + image1 = self.do_pop() + image2 = self.do_pop() + scale = float(self.do_pop()) + offset = int(self.do_pop()) + self.push(ImageChops.add(image1, image2, scale, offset)) + + def do_subtract(self): + """usage: subtract <image:pic1> <image:pic2> <int:offset> <float:scale> + + Pop the two top images, produce the scaled difference with offset. + """ + import ImageChops + image1 = self.do_pop() + image2 = self.do_pop() + scale = float(self.do_pop()) + offset = int(self.do_pop()) + self.push(ImageChops.subtract(image1, image2, scale, offset)) + + # ImageEnhance classes + + def do_color(self): + """usage: color <image:pic1> + + 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): + """usage: contrast <image:pic1> + + 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): + """usage: brightness <image:pic1> + + 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): + """usage: sharpness <image:pic1> + + 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 sys + try: + import readline + except ImportError: + pass # not available on all platforms + + # 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: |