Friday, November 07, 2008

ImageMagick DSL

I've been fighting with JAI/Java2D over the last day or two to manipulate (resize, crop, composite) some large images. I have working code that produces decent quality images, but I really have to crank up the heap space to avoid OutOfMemoryExceptions. If I try to process more than one or two images concurrently, OutOfMemoryExceptions are inevitable. Since this code is going to be called from a servlet, I'm expecting to handle multiple concurrent requests.

This is not a new problem and people have been tackling it in various ways. Since I was working in a server environment and have control over what applications are installed, I decided to use ImageMagick for the image manipulation. ImageMagick is great; I've used it quite often in various shell scripts.

There's basically two ways to work with ImageMagick from Java. You can use JMagick, a JNI layer over the ImageMagick interface, or you can use Runtime.exec() to call the ImageMagick command line application. I opted for the latter as it seemed simpler when I pushed the code from my Mac to my Linux server.

Since finding and invoking ImageMagick's convert command can be somewhat problematic, I decided to write a simple fluent API in Java to hide the details. The result allows you to invoke convert using method chaining:


ImageMagick convert = new ImageMagick(Arrays.asList("/opt/local/bin"));
convert.in(new File("in.jpeg"))
.option("-fuzz", "15%")
.option("-trim")
.out(new File("out.jpeg")).run();
convert.option("-resize", "250x").run(new File("in.jpeg"), new File("out.jpeg"));


You create a new ImageMagick object. As a convenience, you can pass in a list of additional paths to check for the convert command in the event that it isn't on the default path. If the convert command can't be found, the constructor throws an IllegalArgumentException.

Once you have an ImageMagick object, you can execute convert by chaining various method calls, ending in a run(). run() returns true if the command succeeds, false otherwise.

In less than 200 lines of Java code, I had a much nicer way to interact with ImageMagick. A fun experiment would be to take the code and implement an even nicer DSL in Groovy. methodMissing() would allow fluent method chaining on steroids:

convert.fuzz("15%").trim().run(in, out)

As Guillaume Laforge tweeted, using metaprogramming, categories, and syntactic sugar like named parameters you could end up with a full blown DSL that looks like this:

convert fuzz:15.pct, trim: true, in: file, out:file

7 comments:

Unknown said...

nice work. We are trying to automate sprites for a grails project and am thinking of adding a plugin for it. We are looking to use montage http://www.imagemagick.org/script/montage.php
Any chance we can take a look at your code?

Josh Reed said...

Sure: http://andrill.org/~jareed/blog/ImageMagick.jar

Source included in the jar.

There's not much to it. If you're going to do a Grail plugin, you might consider just porting the ImageMagick#findConvert() and ImageMagick#run() methods over to Groovy. Then with a little MOP magic, you could make it more DSL-like, e.g. convert.rotate(90).resize(50.pct).run()

Anyhow, feel free to modify/use the code however. If you can, blog about any enhancements/changes you make.

Cheers,
Josh

kevin__k1 said...

Hi Josh,

I'm trying to use your jar. I'm on Windows XP and I'm using Grails. I just want to do a:
convert logo.gif -crop 175x175+321+137 +repage logo2.gif

So I've tried a :

ImageMagick convert = new ImageMagick(Arrays.asList("c:\\dev\\tools\\imagemagick")); // it's my imagemagick installation path
convert.in(new File("logo.gif")).option("-crop","175x175+321+137")
.option("+repage").out(new File("logo2.gif")).run();

And I always have an exception « java.lang.IllegalArgumentException: ImageMagick not found »
Even when I write the path in different ways like with / instead of \\ etc.

Then you suggest « to port the ImageMagick#findConvert() and ImageMagick#run() methods over to Groovy ». What does it mean ?
Also, I don't get what a MOP magic is ^^

Thanks a lot !

Keyvan

Josh Reed said...

Hey Keyvan,

I'll dig out a Windows machine at home and see if I can figure out how to make it run. I'll post back here tonight.

As for the MOP magic, you could use Groovy's Meta Object Protocol (MOP) to do things do cool things like the dynamic finders on Domain classes in Grails.

Cheers,
Josh

Josh Reed said...

Hi Keyvan,

I tested the code out a bit more on Windows and put a version that appears to be working out at:
http://andrill.org/~jareed/blog/ImageMagick.jar

The source code is included in the JAR. It also has a Main Class so you can run it from the command line to see if it finds your ImageMagick:
java -jar ImageMagick.jar

Cheers,
Josh

kevin__k1 said...

Thanks a lot !! This works very well

adarsh adarsh said...

Thanks Josh, I never could get the runtime to work (problem with spaces in directory names), your code works great.