CaaC: ImageMagick

Dockerize ImageMagick

Don’t litter my path

I’m not a germophobe, but I can’t stand a dirty $PATH. I like mine neat and clean, so that my tab completions are fast, sweet and within the realms of sanity. If you have no idea what I’m talking about, go to the previous installment of this series and read the full thing.

ImageMagick is an image manipulation tool that can “create, edit, compose, or convert bitmap images” and is typically used from the command line.1 Until version seven, the official package installers would place 11 (I said eleven!) executables under /usr/bin:

  • animate
  • compare
  • composite
  • conjure
  • convert
  • display
  • identify
  • import
  • mogrify
  • montage
  • stream

(Were you expecting me to write a description after each of those names? Forget about it. The documentation is your friend.)

In this article, I’ll show you how to dockerize ImageMagick 6 so that you can use the tool frictionlessly and still keep your $PATH pristine. As an extra, we’ll also see how to dockerize ImageMagick 7, which consolidates all those commands under a single command (magick).

ImageMagick 6

Here’s my Dockerfile (Dockerfile6):

FROM centos:7
RUN yum install -y ImageMagick
WORKDIR /imagemagick

You may be wondering why I didn’t include an ENTRYPOINT directive in this Dockerfile. There are eleven possible ImageMagick commands we may want to call, and Docker can take no more than one ENTRYPOINT, so it didn’t make sense to specify one in the Dockerfile. We can still specify an entrypoint when running a container, which is what we’ll do later on.

To build an image from that Dockerfile, just do the usual docker build with a tag:

docker build --tag imagemagick:6 --file Dockerfile6 .

As you probably know, when running a container out of a new image, we can always ask for a TTY and let Docker execute its default entrypoint (/bin/sh -c):

docker run --rm -ti -v $PWD:/imagemagick imagemagick:6

Once inside the container, I can confirm that all ImageMagick 6 commands are available to me:

[root@d70be5e83c81 imagemagick]# convert -version
Version: ImageMagick 6.7.8-9 2016-06-16 Q16 http://www.imagemagick.org
Copyright: Copyright (C) 1999-2012 ImageMagick Studio LLC
Features: OpenMP
[root@d70be5e83c81 imagemagick]# mogrify -help | grep mosaic
  -mosaic              create a mosaic from an image sequence

I don’t want a container TTY, though; I want to run the container as if it was a binary sitting on my local machine, like this:

command -option1 -option2 -option3

Part of the solution to the problem is using a shell alias, but a simple one-liner won’t cut it this time: we need a function in order to make this work.

So ugly, yet so functional

My solution was to append the following function to ~/.bash_aliases:

im6() {
    local command="$1"
    shift 1
    docker run --rm -v $PWD:/imagemagick --entrypoint $command imagemagick:6 "$@"
}

Before I explain the function, let me show you how to call it from the terminal:

$ im6 convert -version
Version: ImageMagick 6.7.8-9 2016-06-16 Q16 http://www.imagemagick.org
Copyright: Copyright (C) 1999-2012 ImageMagick Studio LLC
Features: OpenMP

All I had to do was prepend the word im6 to the ImageMagick command I wanted to execute. Another example:

im6 convert original.png converted.jpg

This call to the im6 function will execute the convert command and convert a PNG image located at the current directory into a JPEG image that will be placed at the same directory. Finally, a more complex example, with the mogrify command:

im6 mogrify -resize 554x710 -format jpg -colorize 0,0,30 -negate my-image.png

The resulting image, my-image.jpg, will have a size of 800x600 pixels, be covered in a thin coat of blue and have its RGB intensities negated (white becomes black, yellow becomes blue, etc.)

Back to the function definition, here’s what it does. It stores its first argument in a local variable (command)—that’s the ImageMagick command we want to invoke. We shift the first value from the parameter list so that we can pass the remaining parameters all at once to the docker run command. When running the Docker container, we use the command variable as the entrypoint, which has the effect of invoking the respective ImageMagick command and passing it the remaining parameters contained in the builtin @ variable. So, in our previous example, the command variable was assigned the value mogrify and the @ variable, after the parameter shift, ended up containing the string -resize 554x710 -format jpg -colorize 0,0,30 -negate my-image.png.

/img/2018/10/2018-10-23-caac-imagemagick/my-image-thumb.jpg

Expand to see how I've just ruined that image. (Courtesy of ImageMagick.)

That’s it for ImageMagick 6. There’s a newer version of ImageMagick, one that puts all those commands under a single name.

ImageMagick 7

As far as my research goes, ImageMagick 7 hasn’t yet been included in any package manager listing. Here’s the Dockerfile (Dockerfile7) I’m using to install it from the official RPM files:

FROM centos:7

RUN curl https://www.rpmfind.net/linux/opensuse/update/leap/42.3/oss/x86_64/libopenjp2-7-2.1.0-22.1.x86_64.rpm -o libopenjp2.rpm
RUN yum install -y libopenjp2.rpm

RUN curl https://imagemagick.org/download/linux/CentOS/x86_64/ImageMagick-libs-7.0.8-14.x86_64.rpm -o imagemagick-libs.rpm
RUN yum install -y imagemagick-libs.rpm

RUN curl https://imagemagick.org/download/linux/CentOS/x86_64/ImageMagick-7.0.8-14.x86_64.rpm -o imagemagick.rpm
RUN yum install -y imagemagick.rpm

WORKDIR /imagemagick
ENTRYPOINT ["magick"]

The first thing you’ll notice in this Dockerfile is that, this time around, I did specify an entrypoint. That’s because, as I said before, ImageMagick 7 consolidates all its previous commands (convert, mogrify etc.) in the new magick command. Another thing to note is that ImageMagick 7 requires some dependencies, like OpenJPEG 2 and the ImageMagick libraries, before it can be installed with success.

I want to tag this image in a way that I can easily tell it contains ImageMagick version seven:

docker build --tag imagemagick:7 --file Dockerfile7 .

To run a container from that Docker image, a one liner in my alias file will be more than enough:

alias im7='docker run --rm -v $PWD:/imagemagick imagemagick:7'

Note that I used single quotes in the alias definition. Had I used double quotes, bash would substitute $PWD at sourcing/declaration time instead of run time, which we absolutely don’t want.

Now I can use the alias from the terminal and do the same conversions I did before. Let’s take our mogrify example and rerun it with ImageMagick 7:

im7 mogrify -resize 554x710 -format jpg -colorize 0,0,30 -negate my-image.png

You should have the same resulting file (my-image.jpg) as before, but you’ll notice that the image quality is far superior for ImageMagick 7.

If you don’t plan on using ImageMagick 6, you can remove the im6 function from your alias file and rename im7 to imagemagick. This way, you’ll be using an alias that transparently maps to the regular imagemagick command, as if Docker wasn’t even involved. Pretty elegant, I find.

Conclusion

Forget Photoshop scripts. You don’t need an image processor (let alone a payed one) to edit your images; just write a good old shell script with a bunch of calls to your sexy, containerized ImageMagick and get the job done in a fraction of time.