Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Reading EXIF Orientation And Auto-Orienting Images Using GraphicsMagick And Lucee CFML 5.2.9.31

By Ben Nadel on
Tags: ColdFusion

This doesn't come up a lot at work; but, sometimes, a user will upload a photo from their phone. And, while the photo looks "fine" on the phone (and on their computer), it will render as rotated in the browser. This is because the photo was taken with a non-upright camera orientation. The camera stores this orientation setting in the Orientation EXIF (Exchangeable image file format) header of the image file. And so, if I could read the EXIF Orientation and "correct" it using GraphicsMagick and Lucee CFML 5.2.9.31.

It turns out, GraphicsMagick has an -auto-orient operation in the convert command. This will read the orientation from the input image; and then, rotate the image so that it is "correct side up". The resultant image will be stored with an Orientation of 1.

Of course, working with a JPEG image is almost never a lossless operation. As such, we don't necessarily want to run -auto-orient unless we actually have to. Really, it would good to check the input image to see if it contains a non-standard orientation; and then, only in those cases, rotate the image and accept the fact that there may be some degree of data-loss.

To read the EXIF data, we can use GraphicsMagick's identify command. The identiy command reads meta-data about the image and returns it as a String. We can manipulate the format of that String (ie, define which data it contains), by using the -format option.

To read the EXIF Orientation value, we can use:

-format %[EXIF:Orientation]

To explore this auto-orientation workflow, I've created a small GraphicsMagick demo in which I can select a JPG image. Then, using the identify command, I check to see if the image is rotated. And, if it is, I then run the convert command with the -auto-orient option:

<cfscript>

	param name="url.image" type="string" default="deer.jpg";

	startedAt = getTickCount();

	inputFilepath = expandPath( "../images/#url.image#" );

	// We can use the IDENTIFY command to read the Orientation header from the image's
	// EXIF (Exchangeable image file format) meta-data.
	result = gm([
		"identify",

		// Identify returns a String using a default format. We can use the "-format"
		// option to override this response. In this case, we want to return one of the
		// EXIF headers.
		"-format %[EXIF:Orientation]",

		// This is the image we are inspecting.
		applyReader( inputFilepath )
	]);

	// If the Orientation EXIF header is not available in the given photo, the result
	// will empty. We're expecting the result to be NUMERIC. As such, we can use val() to
	// coalesce undefined values to "0".
	// --
	// EXIF Orientation readings:
	// Taken from : https://sirv.com/help/articles/rotate-photos-to-be-upright/
	// -- 
	// 1 : 0 degrees - the correct orientation, no adjustment is required.
	// 2 : 0 degrees, mirrored - image has been flipped back-to-front.
	// 3 : 180 degrees - image is upside down.
	// 4 : 180 degrees, mirrored - image is upside down and flipped back-to-front.
	// 5 : 90 degrees - image is on its side.
	// 6 : 90 degrees, mirrored - image is on its side and flipped back-to-front.
	// 7 : 270 degrees - image is on its far side.
	// 8 : 270 degrees, mirrored - image is on its far side and flipped back-to-front.
	inputOrientation = val( result.trim() );

	// If the image has a non-correct orientation, let's use the CONVERT command to
	// auto-orient it as we copy it to the current directory (for the demo). This will
	// rotate the image so that it is correct-side-up.
	if ( inputOrientation > 1 ) {

		result = gm([
			"convert",

			// This is the image we are inspecting.
			applyReader( inputFilepath ),

			// Tell Graphicsmagick to use the image's EXIF data to automatically rotate
			// the image so that it is correct-side-upright.
			"-auto-orient",

			// Remove all profile and text-attribute data from the image. This can
			// reduce the file-size of the image (which is good for the web).
			"-strip",

			// CAUTION: Manipulating a JPG image is [almost never] LOSSLESS! When we
			// output the JPG, GraphicsMagick is defaulting to a quality of 75. As such,
			// auto-orienting may result in some degree of data loss.
			// --
			// "-quality 75",

			// Finally, write the file to the local directory.
			expandPath( "./out.jpg" )
		]);

	// If the image has the correct orientation - or if there is no orientation info -
	// let's just copy the file as-is to the current directory (for the demo).
	} else {

		fileCopy( inputFilepath, "./out.jpg" );

	}

	duration = ( getTickCount() - startedAt );

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I prefix the given filepath with an explicit reader. We want to be EXPLICIT about
	* which input reader GraphicsMagick should use when reading in an image. If we leave
	* it up to "automatic detection", a malicious actor could fake the file-type and
	* potentially exploit a weakness in a given reader. As such, we want to align the
	* reader with the articulated file-type.
	* 
	* READ MORE: http://www.graphicsmagick.org/security.html
	* 
	* @filepath I am the filepath being decorated.
	*/
	public string function applyReader( required string filepath ) {

		switch ( listLast( filepath, "." ).lcase() ) {
			case "jpg":
			case "jpeg":
				var reader = "jpg";
			break;
			case "gif":
				var reader = "gif";
			break;
			case "png":
				var reader = "png";
			break;
			default:
				throw( type = "UnsupportedImageFileExtension" );
			break;
		}

		return( reader & ":""" & filepath & """" );

	}


	/**
	* I execute the given options against the GM (GraphicsMagick) command-line tool. If
	* there is an error, the error is dumped-out and the processing is halted. If there
	* is no error, the standard-output is returned.
	* 
	* NOTE: Options are flattened using a space (" ").
	* 
	* @options I am the collection of options to apply.
	* @timeout I am the timeout to use during the execution.
	*/
	public string function gm(
		required array options,
		numeric timeout = 5
		) {

		execute
			name = "gm"
			arguments = options.toList( " " )
			variable = "local.successResult"
			errorVariable = "local.errorResult"
			timeout = timeout
		;

		// 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 ( local.keyExists( "errorVariable" ) && errorVariable.len() ) {

			dump( errorVariable );
			abort;

		}

		return( successResult ?: "" );

	}

</cfscript>
<cfoutput>

	<link rel="stylesheet" type="text/css" href="./demo.css">

	<h3>
		<strong>Orientation:</strong> #inputOrientation#

		<cfif ( inputOrientation gt 1 )>
			:: Applying "-auto-orient"
		</cfif>
	</h3>

	<p class="images">
		<img src="../images/#url.image#" width="315" />
		<img src="./out.jpg" width="315" />
	</p>

	<p>
		<strong>File Size:</strong>
		#numberFormat( getFileInfo( inputFilepath ).size )#
		&mdash;&gt;
		#numberFormat( getFileInfo( "./out.jpg" ).size )#
	</p>

	<p>
		<a href="#cgi.script_name#?image=deer.jpg">deer</a>,
		<a href="#cgi.script_name#?image=beach-small.jpg">beach-small</a>,
		<a href="#cgi.script_name#?image=bench.jpg">bench</a>
	</p>

	<p>
		Duration: #numberFormat( duration )# ms
	</p>

</cfoutput>

As you can see, I first read the Orientation of the image. And, if that EXIF header value is present, it will be returned as a number. If that number ends up being greater than 1, I rotate the image using -auto-orient as I copy the image into the demo directory.

If I run this ColdFusion code and select a JPG image that is oriented, I get the following output:

An image is rotated using the -auto-orient operation in GraphicsMagick convert utility in Lucee CFML.

As you can see from the image on the left, this photo of my dog was oriented with a counter-clockwise rotation. This resulted in an Orientation EXIF header of 6. As such, it gets run through the -auto-orient operation, leaving me with an image that is "properly" oriented.

Of course, rotating the JPEG image results in some data loss - notice that the size of the input image was 576Kb while the size of the output image was a mere 223Kb (a 50% reduction in file-size using GraphicsMagick's default -quality setting of 75). That said, when I look at the before-and-after photos with my naked eye, I don't see any loss in visual quality.

When dealing with user-generated images, there's always a fine line with how much you want to apply programmatic manipulation. After all, there has to be some assumption that the user - especially a Designer - made a calculated decision about the size and compression of an image vs. the quality of that image. That said, auto-orienting a photo taken on a phone does feel like a use-case where we can step-in and make some educated decisions on behalf of the user. And, it's nice to know that GraphicsMagick makes this super easy in Lucee CFML.



Reader Comments

What has two thumbs and hopes you leave a comment? This Guy! (Ben Nadel).

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.