Generating Fallback Avatars Using CFImage And ColdFusion
Earlier this week, I talked about proxying Gravatar images in order to serve more aggressive Cache-Control
headers in ColdFusion. Another benefit of proxying Gravatar is that I can exert more control over what happens when the given user doesn't have a Gravatar image. Meaning, instead of using the current, default Arnold Schwarzenegger avatar, I might be able to generate a per-user custom avatar. As a first step in this exploration, I wanted to see if I could use the CFImage
tag / image functions in Adobe ColdFusion 2021 to generate name-based images.
Installing a Font: Roboto Mono
When you draw text to an image in ColdFusion, you can't just use any arbitrary font - you can only use fonts that the JVM (Java Virtual Machine) knows about. Furthermore, the JVM seems to need to know about the font at start-up time. Which means, if you install a new font on the system, you have to restart the ColdFusion process in order for said font to be consumable.
ASIDE: Paul Klinkenberg has a post on dynamically registering fonts at runtime so that you don't have to actually restart the ColdFusion process. Unfortunately, this seem to be a Lucee-only behavior. For reasons that are beyond me, his approach works fine in the latest Lucee CFML, but throws an error in the latest Adobe ColdFusion.
I decided to use Roboto Mono from Google Fonts. Locally, in my Dockerized development environment, I downloaded the font files and added this line to my Dockerfile
:
ADD ./fonts /usr/share/fonts/truetype |
This copies my ./fonts/roboto_mono
folder into my Adobe ColdFusion 2021 CommandBox image.
In production, which is both Windows and not Dockerized, I simply copied the .ttf
font files into my c:\Windows\Fonts
folder (and restarted the ColdFusion Application Server service).
Drawing Centered Text to the ColdFusion Image
When you draw text to an Image in ColdFusion, you provide the {x,y}
coordinate that the text will start at. Which is fine if you just want your text to be right-aligned. In my case, however, I want the text to be centered within the image, both vertically and horizontally. To do this, I need to "measure" the text as it will be rendered to the image; and then, adjust the {x,y}
coordinates of the text based on said measurement.
In the past, I've looked at measuring image text dimensions in order to render wrapped text in a ColdFusion image. However, in this case, I'm taking a cue from Ray Camden, who used the java.awt.font.TextLayout
class to find the bounding box of a given text value.
In the following ColdFusion component, AvatarGenerator.cfc
, I've encapsulated this measurement logic in a private method called, measureText()
. This takes the image
, the text I want to render, and the font properties I intend to use (ie, the Font name and the size). This method returns the bounding box measurements, which I then use in the image.drawText()
call:
component | |
accessors = true | |
output = false | |
hint = "I generate simple, initials-based avatars." | |
{ | |
// Define properties for dependency-injection. | |
property scratchDisk; | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
/** | |
* I generate an initials-based avatar and return the image binary. The image is always | |
* generated as a JPG in order to match what Gravatar uses. | |
*/ | |
public binary function generateAvatar( | |
required string initials, | |
required numeric size | |
) { | |
var imageData = withTempDirectory( | |
( tempDirectory ) => { | |
var image = imageNew( "", size, size, "rgb", "212121" ); | |
image.setDrawingColor( "ffffff" ); | |
image.setAntialiasing( true ); | |
var fontProperties = { | |
font: "Roboto Mono Regular", | |
size: ( fix( size / 3 ) - 2 ) | |
}; | |
// Since we don't know what text is going to be passed into the function, | |
// we need to "measure" the text that will be rendered to the image when | |
// using the given text value and font properties. We can then use this | |
// to center the text within the canvas size. | |
var bounds = measureText( image, initials, fontProperties ); | |
// Center text horizontally. | |
var x = ( ( size / 2 ) - ( bounds.width / 2 ) - bounds.xOffset ); | |
// Center text vertically. | |
var y = ( ( size / 2 ) + ( bounds.height / 2 ) ); | |
image.drawText( initials, x, y, fontProperties ); | |
// Save the image object to disk (Virtual File System in this case, for | |
// fast file I/O operations). | |
var imagePath = ( tempDirectory & "/avatar.jpg" ); | |
image.write( imagePath, 0.80 ); | |
return( fileReadBinary( imagePath ) ); | |
} | |
); | |
return( imageData ); | |
} | |
// --- | |
// PRIVATE METHODS. | |
// --- | |
/** | |
* I get the bounding box dimensions of the given text as it would appear written to | |
* the given image. | |
*/ | |
private struct function measureText( | |
required any image, | |
required string text, | |
required struct fontProperties | |
) { | |
var awtContext = image.getBufferedImage() | |
.getGraphics() | |
.getFontRenderContext() | |
; | |
// CAUTION: When decoding a font definition, you can use either a space (" ") or | |
// a dash ("-") delimiter. But, you cannot mix-and-match the two characters. As | |
// such, if you have a Font name which has spaces in it (ex, "Roboto Mono"), you | |
// MUST USE the dash delimiter in order to prevent Java from parsing the font name | |
// as a multi-item list. In this case, note that I am using the "-" because I know | |
// my font name doesn't contain a dash. | |
var awtFont = createObject( "java", "java.awt.Font" ) | |
.decode( "#fontProperties.font#-#fontProperties.size#" ) | |
; | |
var bounds = createObject( "java", "java.awt.font.TextLayout" ) | |
.init( text, awtFont, awtContext ) | |
.getBounds() | |
; | |
return({ | |
width: bounds.width, | |
height: bounds.height, | |
xOffset: bounds.x, | |
yOffset: bounds.y | |
}); | |
} | |
/** | |
* I execute the given callback, passing in a temporary directory that can be used for | |
* transient file IO. The temporary directory is deleted after the callback has been | |
* executed. | |
*/ | |
private any function withTempDirectory( required function callback ) { | |
var folderPath = ( scratchDisk & "/" & createUuid() ); | |
try { | |
directoryCreate( folderPath ); | |
return( callback( folderPath ) ); | |
} finally { | |
directoryDelete( folderPath, true ); | |
} | |
} | |
} |
This ColdFusion component has a single public method, generateAvatar()
, which takes the user's initials, such as "BN" for "Ben Nadel", and an image dimension and then renders the image and returns the image binary. Which means, I can pipe the binary response directly to the CFContent
tag's variable
attribute:
<cfscript> | |
// NOTE: I'm using RAM disk (Virtual File System) as the scratch disk for the image | |
// operations so that I get fast I/O performance. | |
generator = new AvatarGenerator() | |
.setScratchDisk( "ram://" ) | |
; | |
cfcontent( | |
type = "image/jpeg", | |
variable = generator.generateAvatar( "BN", 120 ) | |
); | |
</cfscript> |
Now, if I run this ColdFusion code, we can see the "BN" avatar being generated instantly and getting streamed to the browser:

Of course, you don't want to be generating images on-the-fly all the time. So, one thought might be to render these to disk first and then serve them up; or, I might just rely on the CDN (Content Delivery Caching) to cache them and prevent unnecessary processing. But, that's a thought for a different post.
Want to use code from this post? Check out the license.
Reader Comments
One thing I forgot to include in the post is some code to list out all the fonts that the JVM knows about:
When I run this in my Docker container locally, I get:
Now, I should point out that this is not necessarily the names of the fonts, I don't think - these are the "font families". So, for example, if I wanted to use
SansSerif
in theCFImage
tag, I can't actually use"SansSerif"
- that would lead to a ColdFusion error. Instead, I have to useSansSerif.plain
. Also, there is no"Roboto Mono"
- I would have to use"Roboto Mono Regular"
.There's a lot of trial-and-error with stuff (or, at least as someone how does know the JVM and AWT (Abstract Window Toolkit) very well).
Post A Comment — ❤️ I'd Love To Hear From You! ❤️