Skip to main content
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Aurélien Deleusière
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Aurélien Deleusière ( @adeleusiere )

Using ImgIX For On-Demand Image Processing At Web Scale In ColdFusion Applications

By on
Tags:

Almost every application that I've ever built has required thumbnail generation of some kind. And typically, I'll use ColdFusion as the image generation engine. But, I recently came across ImgIX, which bills itself as "Dynamic Imaging at Web Scale". It's basically a web proxy and CDN (Content Delivery Network) that sits between your servers and your users and can augment images, on the fly, based on the structure of your image URLs. This sounds very exciting, and I wanted to see how easy it would be to consume in ColdFusion.

View the ImgIX Web Proxy project on my GitHub account.

ImgIX works by providing different types of "source" domains. The type of source defines how ImgIX maps the requested URL onto the resource URL located on your origin servers. Of the three source types currently available, the one that most interests me is the "Web Proxy", which allows any public URL to be piped through the ImgIX processor.

Since the "Web Proxy" source is so open-ended, it requires URLs to be signed using a secret token and an MD5 hash. This signature is fairly easy to generate with ColdFusion's native hash() function and just gets appended to the request URL.

To encapsulate the busy-work of generating the secure URL, I created a simple ColdFusion component - ImgIXWebProxy.cfc. This stores your source domain and secret token and then exposes one method for generating the actual web proxy URLs.

component
	output = false
	hint = "I provide methods for creating signed web proxy URLs for the ImgIX API."
	{

	/**
	* I generate signed web proxy URLs for a source represented by the source domain
	* and secret token.
	*
	* @newDomain I am the source domain (including the protocol) for the web proxy.
	* @newToken I am the secret token associated with the source.
	* @output false
	*/
	public any function init(
		required string newDomain,
		required string newToken
		) {

		// Store the properties for all subsequent URLs.
		domain = newDomain;
		token = newToken;

		return( this );

	}


	// ---
	// PUBLIC METHODS.
	// ---


	/**
	* I build the ImgIX web proxy URL based on the given target URL and API commands.
	*
	* @targetUrl I am the URL that ImgIX will request as the "origin" URL.
	* @commands I am a struct OR a string of key-value API commands.
	*/
	public string function getWebProxyUrl(
		required string targetUrl,
		required any commands
		) {

		var encodedTargetUrl = urlEncodeComponent( targetUrl );

		var normalizedCommands = normalizeCommands( commands );

		var stringToSign = "#token#/#encodedTargetUrl#?#normalizedCommands#";

		var signature = lcase( hash( stringToSign ) );

		return( domain & "/" & encodedTargetUrl & "?" & normalizedCommands & "&s=" & signature );

	}


	// ---
	// PRIVATE METHODS.
	// ---


	/**
	* I normalize the ImgIX API commands as a string. If the input is a string, it is
	* simply returned. If it is a struct, it is flattened into an amphersand delimited list.
	*
	* @commands I am a string or struct of name-value pairs.
	* @output false
	*/
	private string function normalizeCommands( required any commands ) {

		if ( isSimpleValue( commands ) ) {

			return( commands );

		}

		if ( ! isStruct( commands ) ) {

			throw(
				type = "ImgIX.InvalidArgument",
				message = "The commands must either be a string or a struct."
			);

		}

		var pairs = [];

		for ( var command in commands ) {

			var name = lcase( command );
			var value = urlEncodeComponent( commands[ command ] );

			arrayAppend( pairs, "#name#=#value#" );

		}

		return( arrayToList( pairs, "&" ) );

	}


	/**
	* I encode the given URL for use as the "path" portion of the ImgIX web proxy url.
	*
	* @targetUrl I am the full URL (including protocol, excluding ImgIX commands).
	* @output false
	*/
	private string function urlEncodeComponent( required string targetUrl ) {

		targetUrl = urlEncodedFormat( targetUrl, "utf-8" );

		// At this point, we have a key that has been encoded too aggressively by
		// ColdFusion. Now, we have to go through and un-escape the characters that
		// will mess up the signature.

		// The following are "unreserved" characters in the RFC 3986 spec for Uniform
		// Resource Identifiers (URIs) - http://tools.ietf.org/html/rfc3986#section-2.3
		targetUrl = replace( targetUrl, "%2E", ".", "all" );
		targetUrl = replace( targetUrl, "%2D", "-", "all" );
		targetUrl = replace( targetUrl, "%5F", "_", "all" );
		targetUrl = replace( targetUrl, "%7E", "~", "all" );

		// Technically, the "/" characters can be encoded and will work. However, I just
		// don't like the way the URL looks with nothing but escaped path markers. This
		// one is just for personal preference.
		targetUrl = replace( targetUrl, "%2F", "/", "all" );

		// This one isn't necessary; but, I think it makes for a more attractive URL.
		targetUrl = replace( targetUrl, "%20", "+", "all" );

		return( targetUrl );

	}

}

The .getWebProxyUrl() method takes the origin URL and a set of commands to apply to the target image. These commands can be supplied in struct or string format. If they are supplied in string format, they are passed-through, as is; if they are supplied in struct format, they are normalized and serialized automatically. Personally, I find the struct format to be more readable.

To see this in action, I'm going to run a remote image through various ImgIX commands and then output it:

<cfscript>

	// This reads in a JSON configuration file that contains my secure token (so that I
	// don't have to have the credentials in the repository). This file contains a hash:
	// --
	// { "domain": "http://my_source_domain", "token": "my_source_token" }
	// --
	config = deserializeJson( fileRead( expandPath( "./config.json" ) ) );

	// Create an instance of our ImgIX Web Proxy component.
	imgIx = new lib.ImgIXWebProxy( config.domain, config.token );

	// This is the remote URL that we'll be piping through the ImgIX image processing.
	remoteUrl = "http://www.bennadel.com/images/header/kitt_hodsden_2.jpg";


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


	// Show the original with some text indicating the original nature of it.
	proxyUrl = imgIx.getWebProxyUrl(
		remoteUrl,
		{
			txt = "( Original Photo )",
			txtsize = 62,
			txtclr = "FFFFFF",
			txtalign = "middle,center",
			txtfont = "Futura Condensed Medium",
			txtshad = 15
		}
	);

	writeOutput( "<img src='#proxyUrl#' /><br /><br />" );


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


	// Add some mono-chromatic coloring.
	proxyUrl = imgIx.getWebProxyUrl(
		remoteUrl,
		{
			mono = "FF33CC"
		}
	);

	writeOutput( "<img src='#proxyUrl#' /><br /><br />" );


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


	// Add some pixelation (16px) and fix to a max-width of 300px.
	proxyUrl = imgIx.getWebProxyUrl(
		remoteUrl,
		{
			px = 16,
			w = 300
		}
	);

	writeOutput( "<img src='#proxyUrl#' /><br /><br />" );


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


	// Flip the image and add some "half-tone" (what ever that is). Notice that this
	// time, I'm using a string of commands instead of a hash. I htink we can all agree
	// that the struct-form is much more readable.
	proxyUrl = imgIx.getWebProxyUrl(
		remoteUrl,
		"flip=h&htn=4"
	);

	writeOutput( "<img src='#proxyUrl#' /><br /><br />" );

</cfscript>

The ImgIX API is pretty easy to consume. And, when we run the above code, we get the following page output:

Using the ImgIX image processing web proxy in ColdFusion.

What you can't see in this demo is how fast the image generation takes place. Granted, these are fairly small images; but, the speed of the image processing is impressive. Furthermore, I suspect that the original image gets cached locally (in ImgIX), which makes subsequent alterations to an existing image seem even faster (my theory).

ImgIX is a paid service; but, you get 10,000 free image renderings and 100GB of CDN bandwidth before your credit card starts getting charged. Pretty awesome! This is definitely a service that I'm going to continue to research.

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

Reader Comments

354 Comments

I honestly don't remember. The example they used was products where they could quickly change size + color as well (so a blue dress could be shown as a red dress).

5 Comments

Hi Ben,
Thanks so much for the write-up, and for sharing your experience with our service! To answer your hunches, yes, imgix caches each transformation we render, so that performance is greatly improved the *second* time any unique version is requested. This speeds up performance exponentially. If you have any other questions, please definitely reach out!

81 Comments

ImgIX's image features seem a lot more powerful than what's built into ColdFusion 9, 10 or 11.

Could you use the ImgIX API to perform the optimizations and then save the image locally using ColdFusion? I'm interested in optimizing images (auto=format,enhance,redeye) and then hosting the image locally or for use in a ColdFusion-generated PDF.
http://www.imgix.com/docs/reference/automatic#param-auto

I don't see anything in their FAQ that addresses this type of usage and I would definitely be generating less than 10,000 image renderings per month. (I already use Google PageSpeed/IISpeed to cache, auto-convert and optimize image sizes based on device... I'm just interested in adding some back-end optimization features to some photo galleries.)

5 Comments

@Raymond, imgix is actually designed to save people from having to store multiple transformations of the same image; one of our main value points is that you only have to store one image (your original image) and then each new transformation is created on-demand, without destructing the original file or having to maintain huge image stores as your renders increase.

To be clear, only the first time we create that image is counted as a render. The second time it's requested, we deliver it from our cache, and it's passed through without having to be processed again, or counted against the render ticker.

354 Comments

@Dina: I get that but... shoot, even if you were Google itself, I'd not trust you to be up 100% of the time. If I knew that I needed a set of resized images from one source, I'm not sure why I'd keep calling your service for them.

You didn't say though - are we allowed to "keep" the new images? (Even if you don't think we need to. ;)

5 Comments

@Raymond, of course we wouldn't expect you to continue requesting images from us if you are simply looking to transform a handful of images in the same way and then be done with us. That's not really what our service is designed for, we are built more for transient content; instances where processing on-demand is the key. We enable customers to build responsive layouts without having to create various versions in advance. We also streamline processes that would normally call for batching.

To answer your other question, of course you are *allowed* to keep the transformed images; you always retain ownership of your content, we simply provide the service that transforms and delivers them.

15,674 Comments

@James,

I think all you would have to do is do a CFHTTP call to imgix and then imgix would call *back* to you to get the original. Kind of like some sort of processing loop-back.

@Ray,

Keep in mind that once it gets processed the first time, it's in the CDN cache; so, even if imgix were to go down, you would only lose service for *new* images; theoretically, existing images would continue to work properly (if they are not expired).

I'm start to learn that "stuff breaks all the time". And, the less we have to specialize in-house, maybe the better. But, that has bitten us once or twice, so I am not saying it is without problems.

354 Comments

Cool. Dina (and Ben, I apologize if this too far off topic, let me know) - in your pricing plans there is a price per render. If I'm doing a blur on an image, is that render charged every time or only once if the cached version is used?

5 Comments

@Raymond, it's only charged for the first time we process a unique version. If you request the same image with the same parameters multiple times, only the first request counts, as all subsequent requests are fulfilled from the cache.

If you want to talk specifics about your use case, feel free to email me directly; i'm firstname at imgix.com. Thanks!

15,674 Comments

@All,

I just posted a follow-up exploration in which I'm using Plupload to upload files directly from the client to Amazon S3; then, using Imgix to render the thumbnails using pre-signed Amazon S3 URLs:

www.bennadel.com/blog/2722-using-plupload-with-amazon-s3-and-imgix-in-angularjs.htm

So, the workflow, from an image standpoint, is basically:

User ==> Thumbnail ==> Imgix CDN ==> Imgix Render ==> Amazon S3 (origin)

It was pretty cool to see it in action; and to see how *fast* on-the-fly thumbnail changes are.

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