Pixelating An Image Using GraphicsMagick And Lucee CFML 5.2.9.31
The other day, while searching for some GraphicsMagick information, I came across a Stack Overflow post that was discussing an approach to pixelating part of an image. I don't really have a need to pixelate images in my day-to-day work; however, it's Friday the 13th; and, pixelation sounds like a fun little distraction from the current COVID-19 pandemic. So, I wanted to have a look at pixelating an image using GraphicsMagick and Lucee CFML 5.2.9.31.
ASIDE: I looked at pixelating an image using ColdFusion's
CFImage
tag 10-years ago. Check the comments in that post for a really easy way to do this using thenearest
interpolation method.
Looking around the web, pretty much every pixelation approach that I've read about uses the same technique: Scale the image down to a small dimension; then, scale it back up such that the lost pixel information results in a bigger, blockier outcome. The smaller the intermediary image, the greater the degree of pixelation.
With the GraphicsMagick convert
utility, there are several ways to scale an image down in size. The ones that I know about are:
-resize
(an alias for-geometry
)-geometry
-thumbnail
-scale
For the purposes of pixelation, we want to use the -scale
option. It uses a faster algorithm that trades quality for speed; which, in the context of pixelation, is exactly what we want. In fact, when scaling the image back up to its original size, only the -scale
option allows us to maintain that desired blocky visualization.
To explore this concept, I've created a simple Lucee CFML demo that allows me to pick an image and a level of pixelation. I then output the original image and the resultant pixelated image side-by-side.
<cfscript>
param name="url.image" type="string" default="beach-small.jpg";
param name="url.size" type="numeric" default="50";
startedAt = getTickCount();
// CAUTION: For the sake of the demo, I am not validating the input image. However,
// in a production setting, I would never allow an arbitrary filepath to be provided
// by the user! Not without some sort of validation.
inputFilename = url.image;
inputFilepath = expandPath( "../images/#inputFilename#" );
// In order to pixelate the image, we are going to scale it down and then scale it
// back up. To allows us to work with absolute pixel values rather than %, let's get
// the dimensions of the input image so that we know how much to scale it back up.
results = gm([
"identify",
"-format %w,%h,",
applyReader( inputFilepath )
]);
inputWidth = listGetAt( results, 1 );
inputHeight = listGetAt( results, 2 );
// Now, to pixelate the image, we are going to scale it DOWN to the given pixel size;
// then, scale it back UP to the original size. As we scale it back up, we won't be
// able to recover the lost pixel data, which will make the resultant image blocky.
gm([
"convert",
// This is the image we are inspecting.
applyReader( inputFilepath ),
// Scale the image size down to the desired pixel setting. We're using SCALE
// instead of RESIZE since it uses a simpler, faster algorithm.
// --
// NOTE: The "Wx" notation means that we're allowing the height to scale as
// needed in proportion to the given width.
"-scale #url.size#x",
// Now that we have a smaller, pixelated image, let's reduce the number of colors
// in an attempt to reduce the resultant file-size. Turn off dithering to get a
// more consistent color distribution.
"+dither",
"-colors 256",
// Scale the pixelated image back up to the original size. We HAVE TO USE SCALE
// here to keep the pixelation. If we try to use something like RESIZE, we will
// lose the pixelation effect.
// --
// NOTE: We're using the "!" to force absolute size of the resultant image.
"-scale #inputWidth#x#inputHeight#!",
expandPath( "./out.png" )
]);
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 "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 ) {
execute
name = "gm"
arguments = options.toList( " " )
variable = "local.successResult"
errorVariable = "local.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 ( local.keyExists( "errorVariable" ) && errorVariable.len() ) {
dump( errorVariable );
abort;
}
return( successResult ?: "" );
}
</cfscript>
<cfoutput>
<link rel="stylesheet" type="text/css" href="./demo.css">
<!--- Provide several degrees of image pixelation. --->
<cfloop index="size" array="#[ 50, 40, 30, 20, 10 ]#">
<p class="sizes">
<strong>#size#-px:</strong>
<a href="./index.cfm?size=#size#&image=beach-small.jpg">Beach</a> ,
<a href="./index.cfm?size=#size#&image=goose.jpg">Goose</a> ,
<a href="./index.cfm?size=#size#&image=face-square.jpg">Face</a> ,
<a href="./index.cfm?size=#size#&image=deer.jpg">Deer</a> ,
<a href="./index.cfm?size=#size#&image=sleeping.jpg">Sleeping</a> ,
<a href="./index.cfm?size=#size#&image=staring.jpg">Staring</a> ,
<a href="./index.cfm?size=#size#&image=engineering.jpg">Engineering</a> ,
<a href="./index.cfm?size=#size#&image=calm.jpg">Calm</a>
</p>
</cfloop>
<div class="images">
<img src="../images/#inputFilename#" />
<img src="./out.png" />
</div>
<p>
Duration: #numberFormat( duration )# ms
</p>
</cfoutput>
As you can see, there's not a whole lot to this demo: we're taking the image, scaling it down, reducing the colors, and then scaling it back up to its original size. And, when we run this ColdFusion code in the browser and walk through a couple of pixelation options, we get the following output:
As you can see, when we reduce the intermediary image down to an increasingly small size, scaling the image back up results in an increasingly pixelated image. And, since we are storing the resultant image as a PNG, the file-size is greatly reduced:
As you can see, the pixelated image in this case is 8.7KB where as the original was 541KB. It makes me wonder if you could use this technique in some sort of lazy-loading algorithm where the pixelated image is shown first; and then, the full-fidelity image is loaded in afterwards.
Anyway, just another fun little exploration of using GraphicsMagick inside Lucee CFML.
Want to use code from this post? Check out the license.
Reader Comments