Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Alec Irwin
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Alec Irwin

Creating A GraphicsMagick Playground With Docker, CommandBox, And Lucee CFML

By
Published in Comments (1)

When Adobe first introduced the CFImage tag in ColdFusion 8, I thought it was the bee's knees! Suddenly, this language that made everything else easy now made image manipulation easy as well. In the years since then, however, I've grown to view ColdFusion's image manipulation with a bit of hesitation. In fact, I'm interested in moving more of my image manipulation over to GraphicsMagick, a command-line utility that's completely dedicated to image manipulation. In order to get a sense of what that journey might entail, I wanted to create a little GraphicsMagick playground for myself using Docker, CommandBox, and Lucee CFML.

To get this started, I created a Dockerfile that extends one of the [CommandBox Docker images] provided Ortus Solutions. This gives me access to Lucee CFML and - via the CFConfig utility - an easy-peasy way to configure the Lucee Server. At this point, all I'm doing is installing the graphicsmagick package using apt-get:

# Use the CommandBox base image.
FROM ortussolutions/commandbox:lucee5

# Some of the GraphicsMagick features require additional libraries to be installed. Those
# can be seen here: http://www.graphicsmagick.org/README.html . That said, it appears
# that a good number of packages get automatically installed using the following apt-get.
# --
# TODO: Should I be pinning graphicsmagick and all of its dependencies?
RUN apt-get update && apt-get install -y \
	graphicsmagick \
	&& apt-get clean

I wanted to get this GraphicsMagick playground to use the same version of Lucee CFML that I use at work (5.2.9.40); however, I couldn't figure out how to do that. According to the Docker detail page, I think I'm supposed to be able to use the ENV variable, CFENGINE, to make this happen (ex, lucee@5.2.9.40). But, I kept ending up with a different version of Lucee in the running container.

Honestly, I'm a noob at Docker - so, I am sure that this feature is documented; only, I don't know enough about Docker to understand all of what I'm reading.

I also wanted to pin or lock my GraphicsMagick packages to a specific version so that the building of this Docker image would be deterministic. However, I'm even more unfamiliar with apt than I am with Docker - oh noes! As such, I wasn't sure how to go about this in the right way.

I understand that I can lock the graphicsmagick page like this:

graphicsmagick=1.3.28-2ubuntu0.1

However, when I ran the Docker build this way, I noticed that a whole bunch of other packages also get installed:

The following additional packages will be installed:

dbus fonts-droid-fallback fonts-noto-mono ghostscript gsfonts libapparmor1 libavahi-client3 libavahi-common-data libavahi-common3 libbsd0 libcups2 libcupsfilters1 libcupsimage2 libdbus-1-3 libgomp1 libgraphicsmagick-q16-3 libgs9 libgs9-common libicu60 libidn11 libijs-0.35 libjbig0 libjbig2dec0 libjpeg-turbo8 libjpeg8 liblcms2-2 libpaper-utils libpaper1 libtiff5 libwebp6 libwebpmux3 libwmf0.2-7 libx11-6 libx11-data libxau6 libxcb1 libxdmcp6 libxext6 libxml2 multiarch-support poppler-data

Suggested packages:

default-dbus-session-bus | dbus-session-bus fonts-noto ghostscript-x graphicsmagick-dbg cups-common liblcms2-utils libwmf0.2-7-gtk poppler-utils fonts-japanese-mincho | fonts-ipafont-mincho fonts-japanese-gothic | fonts-ipafont-gothic fonts-arphic-ukai fonts-arphic-uming fonts-nanum

Would I have to go pin each of those other apt packages as well? Or, does pinning the graphicsmagick package itself implicitly lock all those other packages that get automatically installed?

I hate how unsure I am about so much of the Docker stuff still.

Anyway, once I had my Dockerfile, I then set up my .cfconfig.json file to give my Lucee CFML server a password:

{
	"adminPassword": "password"
}

The CFConfig utility accepts dozens of additional configuration objects. But, for my purposes, I just want to make sure I can get into the Admin if I need to.

With my Dockerfile and my .cfconfig.json file created, I just needed to wire it all together with a docker-compose.yml file that mounts the current directly as volume inside the server's app-root:

version: "2.4"

services:

  lucee:
    build: "."
    ports:
      # Server administrative URLs:
      # --
      # Server: http://localhost:8080/lucee/admin/server.cfm
      # Web: http://localhost:8080/lucee/admin/web.cfm
      - "8080:8080"
    volumes:
      - ".:/app"
    environment:
      APP_DIR: "/app/wwwroot"
      # We are using the CommandBox image as the base for our container. As such, we can
      # use the CFConfig utility to configure out ColdFusion settings (such as the Admin
      # password).
      cfconfigfile: "/app/.cfconfig.json"

Then, it was a mere docker-compose up --build and I was off to the races! In just seconds (not including the docker pull that takes place), I had a running Lucee CFML server on port :8080. Time to try out some GraphicsMagick.

To make sure everything was working, I created a Lucee CFML page that takes a given image and generates several thumbnails at various sizes using the CFExecute tag:

<cfscript>

	startedAt = getTickCount();

	sizes = [ 500, 400, 300, 200, 100, 50 ];

	for ( size in sizes ) {

		inputFilepath = expandPath( "../images/boop.jpg" );
		outputFilename = "output-#getTickCount()#-#size#.jpg";
		outputFilepath = expandPath( "./#outputFilename#" );

		// All of the image utilities are provided _through_ the GraphicsMagick binary.
		command = "gm";

		// We're going to use the Convert utility for this test.
		utility = "convert";

		// We want to be EXPLICIT about which input reader GraphicsMagick should use.
		// If we leave it up to "automatic detection", a malicious actor could fake
		// file-type and potentially exploit a weakness in a given reader.
		// --
		// READ MORE: http://www.graphicsmagick.org/security.html
		utilityReader = "jpg";

		// Setup the options for the Convert utility.
		commandOptions = [
			utility,
			// The -size options gives a hint to the JPEG decoder that the image is going
			// to be downscaled, allowing it to run faster by avoiding returning full-
			// resolution images to GraphicsMagick for the subsequent resize.
			"-size #size#x#size#",
			// Provide the source image to be read with the an explicit reader.
			( utilityReader & ":" & inputFilepath ),
			// We want to reduce the image so that it fits to a WIDTH of "Size" (using
			// the ^ minimum indicator and omitting the height geometry). This may create
			// a height that is larger than Size.
			"-thumbnail #size#x^",
			// Trim any Height off the thumbnail that exceeded Size.
			"-crop #size#x#size#",
			// Set the amount of compression to apply to the generated image.
			"-quality 80",
			// Reduce the contrast a bit (this test image is somewhat dark).
			"+contrast",
			// Remove all meta-data information from the file since this isn't needed for
			// the thumbnail. This will make the image smaller for web-delivery.
			"-strip",
			// Provide the output image file - the file-extension will determine the
			// format of the generated image.
			outputFilepath
		];

		// Execute GraphicsMagick on the command-line.
		execute
			name = command
			arguments = commandOptions.toList( " " )
			variable = "successResult"
			errorVariable = "errorResult"
			timeout = 5
		;

		// If the error variable has been populated, it means the CFExecute tag ran into
		// an error - let's dump-it-out and halt processing.
		if ( variables.keyExists( "errorVariable" ) && errorVariable.len() ) {

			dump( errorVariable );
			abort;

		}

		// Output the resized image along with its file-size.
		echo( "<p>" );
		echo( "<img  src='#outputFilename#' /> <br />" );
		echo( "Size: #size#px" );
		echo( " &mdash; " );
		echo( "Bytes: #numberFormat( getFileInfo( outputFilepath ).size )#" );
		echo( "</p>" );

	} // END: For-loop.

	echo( "Duration: #numberFormat( getTickCount() - startedAt )# ms" );

</cfscript>

This ColdFusion code iterates over the array of dimensions; and, for each dimension, thumbnails the original image and writes it to the response. And, when we run this CFML, we get the following browser output:

An image of Lucy being thumbnailed using GraphicsMagick inside a Docker container running Lucee CFML.

As you can see, I was able to use Lucee CFML to generate thumbnails of Lucy using a Docker container being managed by CommandBox!

I am sure that the Docker portions of this setup leave much to be desired - like I said, I'm a Docker noob. But, at the end of the day, I now have a Lucee CFML server that I can spin up in seconds in my local development environment. This should be sufficient for me to start playing around with recreating CFImage features in GraphicsMagick.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel