From 1cb9fe1e76fdf8db97ced1c6a8e5e3bea1ffa5f0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 12 Aug 2014 10:35:33 +0200 Subject: Added imagepipe example. --- examples/imagepipe/README | 13 ++ examples/imagepipe/example01.jpg | Bin 0 -> 51677 bytes examples/imagepipe/example02.jpg | Bin 0 -> 39106 bytes examples/imagepipe/imagepipe.py | 266 +++++++++++++++++++++++++++++++++++++++ examples/imagepipe/setup.py | 16 +++ 5 files changed, 295 insertions(+) create mode 100644 examples/imagepipe/README create mode 100644 examples/imagepipe/example01.jpg create mode 100644 examples/imagepipe/example02.jpg create mode 100644 examples/imagepipe/imagepipe.py create mode 100644 examples/imagepipe/setup.py (limited to 'examples') diff --git a/examples/imagepipe/README b/examples/imagepipe/README new file mode 100644 index 0000000..91ec0cd --- /dev/null +++ b/examples/imagepipe/README @@ -0,0 +1,13 @@ +$ imagepipe_ + + imagepipe is an example application that implements some + multi commands that chain image processing instructions + together. + + This requires pillow. + +Usage: + + $ pip install --editable . + $ imagepipe open -i example01.jpg resize -w 128 display + $ imagepipe open -i example02.jpg blur save diff --git a/examples/imagepipe/example01.jpg b/examples/imagepipe/example01.jpg new file mode 100644 index 0000000..f2d9397 Binary files /dev/null and b/examples/imagepipe/example01.jpg differ diff --git a/examples/imagepipe/example02.jpg b/examples/imagepipe/example02.jpg new file mode 100644 index 0000000..b1f802e Binary files /dev/null and b/examples/imagepipe/example02.jpg differ diff --git a/examples/imagepipe/imagepipe.py b/examples/imagepipe/imagepipe.py new file mode 100644 index 0000000..37a1521 --- /dev/null +++ b/examples/imagepipe/imagepipe.py @@ -0,0 +1,266 @@ +import click +from functools import update_wrapper +from PIL import Image, ImageFilter, ImageEnhance + + +@click.group(chain=True) +def cli(): + """This script processes a bunch of images through pillow in a unix + pipe. One commands feeds into the next. + + Example: + + \b + imagepipe open -i example01.jpg resize -w 128 display + imagepipe open -i example02.jpg blur save + """ + + +@cli.resultcallback() +def process_commands(processors): + """This result callback is invoked with an iterable of all the chained + subcommands. As in this example each subcommand returns a function + we can chain them together to feed one into the other, similar to how + a pipe on unix works. + """ + # Start with an empty iterable. + stream = () + + # Pipe it through all stream processors. + for processor in processors: + stream = processor(stream) + + # Evaluate the stream and throw away the items. + for _ in stream: + pass + + +def processor(f): + """Helper decorator to rewrite a function so that it returns another + function from it. + """ + def new_func(*args, **kwargs): + def processor(stream): + return f(stream, *args, **kwargs) + return processor + return update_wrapper(new_func, f) + + +def generator(f): + """Similar to the :func:`processor` but passes through old values + unchanged and does not pass through the values as parameter. + """ + @processor + def new_func(stream, *args, **kwargs): + for item in stream: + yield item + for item in f(*args, **kwargs): + yield item + return update_wrapper(new_func, f) + + +def copy_filename(new, old): + new.filename = old.filename + return new + + +@cli.command('open') +@click.option('-i', '--image', 'images', type=click.Path(), + multiple=True, help='The image file to open.') +@generator +def open_cmd(images): + """Loads one or multiple images for processing. The input parameter + can be specified multiple times to load more than one image. + """ + for image in images: + try: + click.echo('Opening "%s"' % image) + if image == '-': + img = Image.open(click.get_binary_stdin()) + img.filename = '-' + else: + img = Image.open(image) + yield img + except Exception as e: + click.echo('Could not open image "%s": %s' % (image, e), err=True) + + +@cli.command('save') +@click.option('--filename', default='processed-%04d.png', type=click.Path(), + help='The format for the filename.', + show_default=True) +@processor +def save_cmd(images, filename): + """Saves all processed images to a series of files.""" + for idx, image in enumerate(images): + try: + fn = filename % (idx + 1) + click.echo('Saving "%s" as "%s"' % (image.filename, fn)) + yield image.save(fn) + except Exception as e: + click.echo('Could not save image "%s": %s' % + (image.filename, e), err=True) + + +@cli.command('display') +@processor +def display_cmd(images): + """Opens all images in an image viewer.""" + for image in images: + click.echo('Displaying "%s"' % image.filename) + image.show() + yield image + + +@cli.command('resize') +@click.option('-w', '--width', type=int, help='The new width of the image.') +@click.option('-h', '--height', type=int, help='The new height of the image.') +@processor +def resize_cmd(images, width, height): + """Resizes an image by fitting it into the box without changing + the aspect ratio. + """ + for image in images: + w, h = (width or image.size[0], height or image.size[1]) + click.echo('Resizing "%s" to %dx%d' % (image.filename, w, h)) + image.thumbnail((w, h)) + yield image + + +@cli.command('crop') +@click.option('-b', '--border', type=int, help='Crop the image from all ' + 'sides by this amount.') +@processor +def crop_cmd(images, border): + """Crops an image from all edges.""" + for image in images: + box = [0, 0, image.size[0], image.size[1]] + + if border is not None: + for idx, val in enumerate(box): + box[idx] = max(0, val - border) + click.echo('Cropping "%s" by %dpx' % (image.filename, border)) + yield copy_filename(image.crop(box), image) + else: + yield image + + +def convert_rotation(ctx, param, value): + if value is None: + return + value = value.lower() + if value in ('90', 'r', 'right'): + return (Image.ROTATE_90, 90) + if value in ('180', '-180'): + return (Image.ROTATE_180, 180) + if value in ('-90', '270', 'l', 'left'): + return (Image.ROTATE_270, 270) + raise click.BadParameter('invalid rotation "%s"' % value) + + +def convert_flip(ctx, param, value): + if value is None: + return + value = value.lower() + if value in ('lr', 'leftright'): + return (Image.FLIP_LEFT_RIGHT, 'left to right') + if value in ('tb', 'topbottom', 'upsidedown', 'ud'): + return (Image.FLIP_LEFT_RIGHT, 'top to bottom') + raise click.BadParameter('invalid flip "%s"' % value) + + +@cli.command('transpose') +@click.option('-r', '--rotate', callback=convert_rotation, + help='Rotates the image (in degrees)') +@click.option('-f', '--flip', callback=convert_flip, + help='Flips the image [LR / TB]') +@processor +def transpose_cmd(images, rotate, flip): + """Transposes an image by either rotating or flipping it.""" + for image in images: + if rotate is not None: + mode, degrees = rotate + click.echo('Rotate "%s" by %ddeg' % (image.filename, degrees)) + image = copy_filename(image.transpose(mode), image) + if flip is not None: + mode, direction = flip + click.echo('Flip "%s" %s' % (image.filename, direction)) + image = copy_filename(image.transpose(mode), image) + yield image + + +@cli.command('blur') +@click.option('-r', '--radius', default=2, show_default=True, + help='The blur radius.') +@processor +def blur_cmd(images, radius): + """Applies gaussian blur.""" + blur = ImageFilter.GaussianBlur(radius) + for image in images: + click.echo('Blurring "%s" by %dpx' % (image.filename, radius)) + yield copy_filename(image.filter(blur), image) + + +@cli.command('smoothen') +@click.option('-i', '--iterations', default=1, show_default=True, + help='How many iterations of the smoothen filter to run.') +@processor +def smoothen_cmd(images, iterations): + """Applies a smoothening filter.""" + for image in images: + click.echo('Smoothening "%s" %d time%s' % + (image.filename, iterations, iterations != 1 and 's' or '',)) + for x in xrange(iterations): + image = copy_filename(image.filter(ImageFilter.BLUR), image) + yield image + + +@cli.command('emboss') +@processor +def emboss_cmd(images): + """Embosses an image.""" + for image in images: + click.echo('Embossing "%s"' % image.filename) + yield copy_filename(image.filter(ImageFilter.EMBOSS), image) + + +@cli.command('sharpen') +@click.option('-f', '--factor', default=2.0, + help='Sharpens the image.', show_default=True) +@processor +def sharpen_cmd(images, factor): + """Sharpens an image.""" + for image in images: + click.echo('Sharpen "%s" by %f' % (image.filename, factor)) + enhancer = ImageEnhance.Sharpness(image) + yield copy_filename(enhancer.enhance(max(1.0, factor)), image) + + +@cli.command('paste') +@click.option('-l', '--left', default=0, help='Offset from left.') +@click.option('-r', '--right', default=0, help='Offset from right.') +@processor +def paste_cmd(images, left, right): + """Pastes the second image on the first image and leaves the rest + unchanged. + """ + imageiter = iter(images) + image = next(imageiter, None) + to_paste = next(imageiter, None) + + if to_paste is None: + if image is not None: + yield image + return + + click.echo('Paste "%s" on "%s"' % + (to_paste.filename, image.filename)) + mask = None + if to_paste.mode == 'RGBA' or 'transparency' in to_paste.info: + mask = to_paste + image.paste(to_paste, (left, right), mask) + image.filename += '+' + to_paste.filename + yield image + + for image in imageiter: + yield image diff --git a/examples/imagepipe/setup.py b/examples/imagepipe/setup.py new file mode 100644 index 0000000..82d521c --- /dev/null +++ b/examples/imagepipe/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup + +setup( + name='click-example-imagepipe', + version='1.0', + py_modules=['imagepipe'], + include_package_data=True, + install_requires=[ + 'Click', + 'pillow', + ], + entry_points=''' + [console_scripts] + imagepipe=imagepipe:cli + ''', +) -- cgit v1.2.1