Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Code Block
/**
 * A batch image manipulation utility
 *
 * A wrote this script just to get groovy, batch manipulate images in about
 * 240 lines of code (without this comment)!!!.
 *
 * commands:
 * values ending with '%' means size relative to image size.
 * values ending with 'px' means values in absolute pixels.
 * values without postfix use default notation.
 *
 * expressions:
 * scale(width,height)      * height is optional(use width) e.g: scale(50%) == scale(50%,50%)
 * fit(width,height)        * relative scale the image until if fits (defaut as scale)
 *                          * bounds of the given box, usefull for generating of thumbnails.
 * rotate(degrees,x,y)      * the rotation position x and y are optional (default is 50%)
 *
 * TODO: move(x,y)          * move the image within its own bounds (can be done with margin)
 *                          * y is optional(same height)
 * TODO: color(type)        * color transformation
 * TODO: shear(degrees,x,y) * x and y is optional
 * margin(x,y,x2,y2)        * add margins to image (resize image canvas), this operation can't
  *                          * be used on a headless environment.
 * parameters:
 * -d                       * working directory (default current directory)
 * -e                       * execute expressions from command line.
 * -f                       * execute expressions from file.
 * -p                       * file mathing pattern default is \.png|\.jpg
 * -q                       * output file pattern can use {0} .. {9}
  *                          * backreferences from the input pattern. default: output/{0}
 * -h                       * help, nothing special (maybe this doc using heredoc)
 *
 * Example generate thumbnails(take *.png from images fit them in a 100X100 box,
 * add 10px margin, put them in the thumbnail dir.)
 *
 * $ groovy image.groovy -d images -e "fit(100px,100px) margin(5)" -p "(.*)\.png" -q "thumbnail/{1}.png"
 *
 * @author Philip Van Bogaert alias tbone
 */

import java.io.*;
import javax.imageio.*;
import java.awt.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.util.*;


class GroovyImage {
    property File srcDir = new File(".");

    operations = [];

    property pattern = ~".*(\\.png|\\.jpg)";
    property outputPattern = "output/{0}";

    void addOperation(command) {

        matcher = command =~ "([a-z]+)\\((.*)\\).*";
        matcher.find();

        method = matcher.group(1);
        args = matcher.group(2).split(",").toList();

        switch(method) {
            case "scale": // vertical,horizontal
                operations.add([parseAndScale,argsLength(args,2)]);
                break;

            case "rotate": // degrees,x,y
                operations.add([parseAndRotate,argsLength(args,3)]);
                break;

            case "margin": // left,top,right,bottom
                operations.add([parseAndMargin,argsLength(args,4)]);
                break;

            case "fit": // width,height
                operations.add([parseAndFit,argsLength(args,2)]);
                break;
        }
    }

    BufferedImage parseAndRotate(image,degrees,x,y) {
        parsedRadians = 0;
        try {
            parsedRadians = Math.toRadians(Double.parseDouble(degrees));
        }
        catch(NumberFormatException except) {
        }

        parsedX = parseValue(x,image.width,true,"50%");
        parsedY = parseValue(y,image.height,true,parsedX);

        return rotate(image,parsedRadians,parsedX,parsedY);
    }

    BufferedImage rotate(image,radians,x,y) {
        transform = new AffineTransform();
        transform.rotate(radians,x,y);
        op = new AffineTransformOp(transform,AffineTransformOp.TYPE_BILINEAR);
        return op.filter(image,null);
    }

    BufferedImage parseAndScale(image,horizontal,vertical) {
        parsedHorizontal =  parseValue(horizontal,image.width,false,"100%");
        parsedVertical =  parseValue(vertical,image.height,false,parsedHorizontal);
        return scale(image,parsedHorizontal,parsedVertical);
    }

    BufferedImage scale(image,horizontal,vertical) {
        transform = new AffineTransform();
        transform.scale(horizontal,vertical);
        op = new AffineTransformOp(transform,AffineTransformOp.TYPE_BILINEAR);
        return op.filter(image,null);
    }

    BufferedImage parseAndMargin(image,left,top,right,bottom) {
        parsedLeft = parseValue(left,image.width,true,"0px");
        parsedTop =  parseValue(top,image.height,true,parsedLeft);
        parsedRight = parseValue(right,image.width,true,parsedLeft);
        parsedBottom = parseValue(bottom,image.height,true,parsedTop);
        return margin(image,parsedLeft,parsedTop,parsedRight,parsedBottom);
    }

    BufferedImage margin(image,left,top,right,bottom) {
        width = left + image.width + right;
        height = top + image.height + bottom;
        newImage = new BufferedImage(width.intValue(), height.intValue(),BufferedImage.TYPE_INT_ARGB);
        // createGraphics() needs a display, find workaround.
        graph = newImage.createGraphics();
        graph.drawImage(image,new AffineTransform(1.0d,0.0d,0.0d,1.0d,left,top),null);
        return newImage;
    }

    BufferedImage parseAndFit(image,width,height) {
        parsedWidth = parseValue(width,image.width,true,"100%");
        parsedHeight = parseValue(height,image.height,true,parsedWidth);

        imageRatio = image.width / image.height;
        fitRatio = parsedWidth / parsedHeight;

        if(fitRatio < imageRatio) {
            parsedHeight = image.height * (parsedWidth/image.width);
        } else {
            parsedWidth = image.width * (parsedHeight/image.height);
        }

        return parseAndScale(image,parsedWidth+"px",parsedHeight+"px");
    }

    BufferedImage manipulate(image) {
        for(operation in operations) {
            image = operation[0].call([image] + operation[1]);
        }
        return image;
    }

    void batch() {
        images = getImages();
        for(imageMap in images) {
            imageMap.image = manipulate(imageMap.image);
            storeImage(imageMap);
        }
    }


     Object getImages() {
        imageMaps = [];

        for(i in srcDir.listFiles()) {
            if(!i.isDirectory()) {
                subpath = i.path;
                if(subpath.startsWith(srcDir.path)) {
                    subpath = subpath.substring(srcDir.path.length());
                }
                matcher = subpath =~ pattern;
                if(matcher.find()) {
                    imageMaps.add(["file":i,"matcher":matcher]);
                }
            }
        }
        imageMaps.each({it["image"] = ImageIO.read(it["file"]); });
        return imageMaps;
    }


    void storeImage(imageMap) {
        groupIndex = 0;
        name = outputPattern;
        matcher = imageMap.matcher;
        while(groupIndex <= matcher.groupCount()) {
            name = name.replaceAll("\\{${groupIndex}\\}",matcher.group(groupIndex++));
        }
        type = name.substring(name.lastIndexOf(".")+1,name.length());
        file = new File(srcDir,name);
        file.mkdirs();
        ImageIO.write(imageMap.image,type,file);
    }

    static void main(args) {
        argList = args.toList();
        script ='';
        groovyImage = new GroovyImage();

        // command line parsing bit, NOTE: -h does System.exit(2)
        argAndClosure = ['-d':{groovyImage.srcDir = new File(it)},
                         '-q':{groovyImage.outputPattern = it},
                         '-p':{groovyImage.pattern = it},
                         '-h':{groovyImage.help()}];
        // parse non-conditional arguments
        parseMultipleCommandArgs(argList,argAndClosure);

        // expression,file,nothing
        if(!parseCommandArg(argList,'-e', {script = it}))  {
            parseCommandArg(argList,'-f',{script = new File(it).text});
        }

        // execution bit
        commands = script =~ "([a-z]{1,}\\([^)]*\\))";
        while(commands.find()) {
            groovyImage.addOperation(commands.group(1));
        }
        groovyImage.batch();

    }

    static boolean parseCommandArg(args,arg,closure) {
        index = args.indexOf(arg);

        if(index != -1 && index + 1 < args.size()) {
            closure.call(args[index + 1]);
            return true;
        } else {
            return false;
        }
    }

    static void parseMultipleCommandArgs(args,argAndClosureMap) {
        for(argAndClosure in argAndClosureMap) {
            parseCommandArg(args,argAndClosure.key,argAndClosure.value);
        }
    }

    void help() {
        println('usage: groovy image.groovy -i <inputDir> -o <outputDir> -e "<expressions>"');
        System.exit(2);
    }

    /**
     * absolute true  -> returns pixels.
     *          false -> returns relative decimal (e.g 1.0).
     */
    Number parseValue(value,size,absolute,defaultValue="0") {
        pattern = "(-?[0-9]+\\.?[0-9]*)(.*)";
        matcher = value =~ pattern;
        if(!matcher.find()) {
            matcher = defaultValue =~ pattern;
            matcher.find();
        }

        decimalValue = Double.parseDouble(matcher.group(1));
        type = matcher.group(2);

        if(absolute) { // pixels
            switch(type)  {
                case "%":
                    return (int) size * (decimalValue / 100);
                case "px":
                default:
                return (int) decimalValue;
            }
        }
        else { // scale
            switch(type) {
                case "px":
                    return decimalValue / size;
                case "%":
                    return decimalValue / 100;
                default:
                    return decimalValue;
            }
        }
    }

    Object argsLength(args,length) {
        if(args.size() < length) {
            while(args.size() < length) {
                args.add("");
            }
        } else {
            args = args.subList(0,length);
        }
        return args;
    }
}