Skip to content

Instantly share code, notes, and snippets.

@timsayshey
Forked from bennadel/index.cfm
Created February 14, 2021 21:06
Show Gist options
  • Save timsayshey/f66c3f2c0774f0535a3b7076465f292e to your computer and use it in GitHub Desktop.
Save timsayshey/f66c3f2c0774f0535a3b7076465f292e to your computer and use it in GitHub Desktop.
Scaling An Image During A Draw Operation Using GraphicsMagick And Lucee CFML 5.2.9.31
<cfscript>
param name="url.image" type="string" default="beach-small.jpg";
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#" );
// As a first step, we need to figure out how large the input image is so that we can
// calculate the display-scale of each paste operation. To do that, we can use the
// Identify utility.
inputDimensions = getImageDimensions( inputFilepath );
inputWidth = inputDimensions.width;
inputHeight = inputDimensions.height;
// Setup the output filepath for our generated image.
// --
// NOTE: The file-extension of this output filename will be used by GraphicsMagick to
// figure out how to encode the final image.
outputFilename = "out.jpg";
outputFilepath = expandPath( "./#outputFilename#" );
// Define the dimensions of our demo image.
outputWidth = 650;
outputHeight = 300;
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Before we execute our Convert command, let's calculate the "-draw" sub-commands
// for each of the desired scaling sizes. These are the WIDTH dimensions at which we
// want to paste the input image into the output image:
drawWidths = [ 10, 50, 100, 200, 300 ];
// As we draw the input image at different widths, we need to keep a running offset
// for the X-coordinate of the operation (so that the different draw operations don't
// overlap each other).
runningX = 10;
// Map the widths onto the actual "-draw" sub-commands.
drawCommands = drawWidths.map(
( drawWidth ) => {
var drawScale = ( drawWidth / inputWidth );
var drawHeight = ( inputHeight * drawScale );
var drawX = runningX;
var drawY = 10;
// Update the running X-coordinate for the next iteration.
runningX += ( drawWidth + 10 );
// CAUTION: It is not possible to provide an explicit READER during a draw
// operation. To avoid potential security issues, the input should be
// sanitized ahead of time.
// --
// Read More: https://sourceforge.net/p/graphicsmagick/discussion/250738/thread/71a6a6a8e8/
return( "-draw 'image over #drawX#,#drawY# #drawWidth#,#drawHeight# #inputFilepath#'" );
}
);
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Now that we know where and at what sizes the input images are going to be pasted
// into the output image, we can use the Convert utility to compose the various
// image operations together.
result = gm([
"convert",
// Start with a "blank" canvas with the desired output dimensions.
"-size #outputWidth#x#outputHeight# xc:##fafafa",
// Now, let's "spread" the "draw" sub-commands into the current Convert.
drawCommands.toList( " " ),
// And, finally, output our composite image!
"-quality 75",
outputFilepath
]);
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 return the Width and Height of the image at the given path.
*
* @filepath I am the filepath being inspected.
*/
public struct function getImageDimensions( required string filepath ) {
// NOTE: Trailing comma after -format being included on purposes. It delimits
// multi-page images (like GIFs). While I don't have any GIFs in this demo, I am
// leaving it in so I don't forget why I have it.
var result = gm([
"identify",
"-format %w,%h,",
applyReader( filepath )
]);
var dimensions = result.listToArray();
return({
width: dimensions[ 1 ],
height: dimensions[ 2 ]
});
}
/**
* 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>
<h1>
Resizing The Image During The DRAW Command
</h1>
<p>
<img
src="./#outputFilename#"
width="#outputWidth#"
height="#outputHeight#"
style="border: 1px solid ##aaaaaa ; border-radius: 4px ;"
/>
</p>
<!--- List all of the images in the test directory. --->
<p style="width: 650px ;">
<cfloop query="#directoryList( path = '../images', listInfo = 'query', sort = 'name asc' )#">
<a href="#cgi.script_name#?image=#encodeForHtmlAttribute( name )#">
#encodeForHtml( name )#
</a>,
</cfloop>
</p>
<p>
Duration: #numberFormat( duration )# ms
</p>
</cfoutput>
<cfscript>
param name="url.image" type="string" default="beach-small.jpg";
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#" );
// As a first step, we need to figure out how large the input image is so that we can
// calculate the display-scale for each intermediary, scaled image. To do that, we
// can use the Identify utility.
inputDimensions = getImageDimensions( inputFilepath );
inputWidth = inputDimensions.width;
inputHeight = inputDimensions.height;
// Setup the output filepath for our generated image.
// --
// NOTE: The file-extension of this output filename will be used by GraphicsMagick to
// figure out how to encode the final image.
outputFilename = "out.jpg";
outputFilepath = expandPath( "./#outputFilename#" );
// Define the dimensions of our demo image.
outputWidth = 650;
outputHeight = 300;
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Before we execute our Convert command, let's calculate the "-draw" sub-commands
// for each of the desired scaling sizes. These are the WIDTH dimensions at which we
// want to paste the input image into the output image:
drawWidths = [ 10, 50, 100, 200, 300 ];
// As we draw the input image at different widths, we need to keep a running offset
// for the X-coordinate of the operation (so that the different draw operations don't
// overlap each other).
runningX = 10;
// Map the widths onto the actual "-draw" sub-commands.
drawCommands = drawWidths.map(
( drawWidth ) => {
var drawX = runningX;
var drawY = 10;
// NOTE: For the temporary image, I'm using MIFF - the lossless Magick Image
// File Format. This way, we don't lose quality from the intermediary image.
var tempFilepath = expandPath( "./temp-#drawWidth#.miff" );
// In this approach, rather than pasting an image at a scaled dimension,
// we're going to create an intermediary image at the desired dimensions.
// This gives us more control over how the input image is treated (such as
// changing the colorspace or using a specified resize algorithm).
gm([
"convert",
applyReader( inputFilepath ),
"-resize #drawWidth#x",
"-colorspace RGB",
tempFilepath
]);
// Update the running X-coordinate for the next iteration.
runningX += ( drawWidth + 10 );
// This time, since we've generated an intermediary, scaled image, we don't
// have to specify the DIMENSIONS of the paste operation. Instead, we can use
// "0,0" to tell GraphicsMagick to use the natural dimensions of the
// temporary image we just generated.
return( "-draw 'image over #drawX#,#drawY# 0,0 #tempFilepath#'" );
}
);
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Now that we know where and at what sizes the input images are going to be pasted
// into the output image, we can use the Convert utility to compose the various
// image operations together.
result = gm([
"convert",
// Start with a "blank" canvas with the desired output dimensions.
"-size #outputWidth#x#outputHeight# xc:##fafafa",
// Now, let's "spread" the "draw" sub-commands into the current Convert.
drawCommands.toList( " " ),
// And, finally, output our composite image!
"-quality 75",
outputFilepath
]);
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 return the Width and Height of the image at the given path.
*
* @filepath I am the filepath being inspected.
*/
public struct function getImageDimensions( required string filepath ) {
// NOTE: Trailing comma after -format being included on purposes. It delimits
// multi-page images (like GIFs). While I don't have any GIFs in this demo, I am
// leaving it in so I don't forget why I have it.
var result = gm([
"identify",
"-format %w,%h,",
applyReader( filepath )
]);
var dimensions = result.listToArray();
return({
width: dimensions[ 1 ],
height: dimensions[ 2 ]
});
}
/**
* 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>
<h1>
Resizing The Image Using An Intermediary File
</h1>
<p>
<img
src="./#outputFilename#"
width="#outputWidth#"
height="#outputHeight#"
style="border: 1px solid ##aaaaaa ; border-radius: 4px ;"
/>
</p>
<!--- List all of the images in the test directory. --->
<p style="width: 650px ;">
<cfloop query="#directoryList( path = '../images', listInfo = 'query', sort = 'name asc' )#">
<a href="#cgi.script_name#?image=#encodeForHtmlAttribute( name )#">
#encodeForHtml( name )#
</a>,
</cfloop>
</p>
<p>
Duration: #numberFormat( duration )# ms
</p>
</cfoutput>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment