Source code for imgwrench.cli

# -*- coding: utf-8 -*-

"""Command Line Interface for Image Wrench."""

import os
from pathlib import Path

import click
from PIL import Image

from .info import ImageInfo
from .commands.blackwhite import cli_blackwhite
from .commands.collage import cli_collage
from .commands.colorfix import cli_colorfix
from .commands.crop import cli_crop
from .commands.dither import cli_dither
from .commands.filmstrip import cli_filmstrip
from .commands.flip import cli_flip
from .commands.frame import cli_frame
from .commands.framecrop import cli_framecrop
from .commands.grid import cli_grid
from .commands.quad import cli_quad
from .commands.resize import cli_resize
from .commands.save import cli_save
from .commands.stack import cli_stack


def _xmp_from_image(img, xmp_marker=b"http://ns.adobe.com/xap/1.0/"):
    if hasattr(img, "applist"):
        for key, val in img.applist:
            if key == "APP1" and val.startswith(xmp_marker):
                return val


def _write_xmp_to_image(path, xmp):
    with open(path, "rb") as f:
        raw_data = f.read()
    app1_start = raw_data.rfind(b"\xFF\xE1")
    if app1_start > 0:
        app1_raw_len = raw_data[app1_start + 2 : app1_start + 4]
        app1_len = int.from_bytes(app1_raw_len, "big")
        app1_end = app1_start + 2 + app1_len
        with open(path, "wb") as f:
            f.write(raw_data[:app1_end])
            f.write(b"\xFF\xE1")
            f.write((len(xmp) + 2).to_bytes(2, "big"))
            f.write(xmp)
            f.write(raw_data[app1_end:])


def _load_image(fname, i, preserve_exif):
    """Load an image from file system and rotate according to exif"""
    img = Image.open(fname)
    info = ImageInfo(fname, i, img.info.get("exif"), _xmp_from_image(img))
    # do not rotate image if exif is preserved
    # (otherwise it would be rotated twice)
    if not preserve_exif and hasattr(img, "_getexif"):
        orientation = 0x0112
        exif = img._getexif()
        if exif is not None and orientation in exif:
            orientation = exif[orientation]
            rotations = {3: Image.ROTATE_180, 6: Image.ROTATE_270, 8: Image.ROTATE_90}
            if orientation in rotations:
                img = img.transpose(rotations[orientation])
    if img.mode != "RGB":
        return img.convert("RGB"), info
    return img, info


def _repeat(it, n):
    """Repeat every element of it n times

    >>> list(_repeat([1, 2, 3], 4))
    [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3]
    """
    for el in it:
        for i in range(n):
            yield el


@click.group(name="imgwrench", chain=True)
@click.option(
    "-i",
    "--image-list",
    type=click.File(mode="r"),
    default="-",
    help="File containing paths to images for processing, " + "defaults to stdin",
)
@click.option(
    "-r",
    "--repeat",
    type=click.INT,
    default=1,
    show_default=True,
    help="repeat every image in input sequence",
)
@click.option(
    "-p",
    "--prefix",
    type=click.STRING,
    default="img_",
    show_default=True,
    help="prefix for all output filenames before numbering",
)
@click.option(
    "-d",
    "--digits",
    type=click.INT,
    default=4,
    show_default=True,
    help="number of digits for file numbering",
)
@click.option(
    "-c",
    "--increment",
    type=click.INT,
    default=1,
    show_default=True,
    help="increment for file numbering",
)
@click.option(
    "-k",
    "--keep-names",
    is_flag=True,
    default=False,
    show_default=True,
    help="keep original file names instead of numbering",
)
@click.option(
    "-f",
    "--force-overwrite",
    is_flag=True,
    default=False,
    show_default=True,
    help="force overwriting output image file if it exists",
)
@click.option(
    "-o",
    "--outdir",
    type=click.Path(
        exists=False, file_okay=False, dir_okay=True, writable=True, resolve_path=True
    ),
    show_default=True,
    default=".",
    help="output directory",
)
@click.option(
    "-q",
    "--quality",
    type=click.INT,
    default=88,
    show_default=True,
    help="quality of the output images, integer 0 - 100",
)
@click.option(
    "-e",
    "--preserve-exif",
    is_flag=True,
    default=False,
    show_default=True,
    help="preserve image exif and xmp metadata if available",
)
@click.option(
    "-j",
    "--jpg/--png",
    default=True,
    show_default=True,
    help="save output images in JPEG format (otherwise PNG)",
)
def cli_imgwrench(
    image_list,
    repeat,
    prefix,
    digits,
    increment,
    keep_names,
    force_overwrite,
    outdir,
    quality,
    preserve_exif,
    jpg,
):
    """A highly opinionated image processor for the commandline.
    Multiple subcommands can be executed sequentially to form
    a processing pipeline."""
    param = dict(**locals())
    del param["image_list"]
    click.echo("Preparing imgwrench pipeline with parameters {}".format(param))


[docs]@cli_imgwrench.resultcallback() def pipeline( image_processors, image_list, repeat, prefix, increment, digits, keep_names, force_overwrite, outdir, quality, preserve_exif, jpg, ): def _load_images(): with image_list: for i, line in enumerate(_repeat(image_list, repeat)): path = Path(line.strip()).resolve() img, info = _load_image(path, i, preserve_exif) click.echo("<- Processing {}...".format(info)) yield info, img images = _load_images() # connecting pipeline image processors for image_processor in image_processors: images = image_processor(images) os.makedirs(outdir, exist_ok=True) click.echo("--- Executing pipeline ---") # executing pipeline ext = "jpg" if jpg else "png" fmt = "{}{:0" + str(digits) + "d}." + ext for i, (info, processed_image) in enumerate(images): newfname = info.fname if keep_names else fmt.format(prefix, i * increment) outpath = os.path.join(outdir, newfname) if not force_overwrite and os.path.exists(outpath): raise Exception( ("{} already exists; use --force-overwrite " + "to overwrite").format( outpath ) ) args = dict(quality=quality) if preserve_exif and info.exif: args["exif"] = info.exif processed_image.save(outpath, **args) if preserve_exif and jpg and info.xmp: _write_xmp_to_image(outpath, info.xmp) click.echo("-> Saved {}".format(outpath)) click.echo("--- Pipeline execution completed ---")
cli_imgwrench.add_command(cli_blackwhite) cli_imgwrench.add_command(cli_collage) cli_imgwrench.add_command(cli_colorfix) cli_imgwrench.add_command(cli_crop) cli_imgwrench.add_command(cli_filmstrip) cli_imgwrench.add_command(cli_flip) cli_imgwrench.add_command(cli_frame) cli_imgwrench.add_command(cli_framecrop) cli_imgwrench.add_command(cli_grid) cli_imgwrench.add_command(cli_quad) cli_imgwrench.add_command(cli_resize) cli_imgwrench.add_command(cli_save) cli_imgwrench.add_command(cli_stack) cli_imgwrench.add_command(cli_dither)