Created
April 17, 2020 12:28
-
-
Save bennadel/d3f6c867d8fdac04d9884c52efa34830 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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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