Monday, November 28, 2022
HomeJavaScriptProducing Fallback Avatars Utilizing CFImage And ColdFusion

Producing Fallback Avatars Utilizing CFImage And ColdFusion


Earlier this week, I talked about proxying Gravatar photos as a way to serve extra aggressive Cache-Management headers in ColdFusion. One other good thing about proxying Gravatar is that I can exert extra management over what occurs when the given consumer does not have a Gravatar picture. That means, as a substitute of utilizing the present, default Arnold Schwarzenegger avatar, I would be capable of generate a per-user customized avatar. As a primary step on this exploration, I needed to see if I may use the CFImage tag / picture features in Adobe ColdFusion 2021 to generate name-based photos.

Paul Klinkenberg has a put up on dynamically registering fonts at runtime in order that you do not have to truly restart the ColdFusion course of. Sadly, this appear to be a Lucee-only conduct. For causes which might be past me, his method works high-quality within the newest Lucee CFML, however throws an error within the newest Adobe ColdFusion.

I made a decision to make use of Roboto Mono from Google Fonts. Regionally, in my Dockerized growth atmosphere, I downloaded the font recordsdata 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 picture.

In manufacturing, which is each Home windows and not Dockerized, I merely copied the .ttf font recordsdata into my c:WindowsFonts folder (and restarted the ColdFusion Software Server service).

measuring picture textual content dimensions as a way to render wrapped textual content in a ColdFusion picture. Nevertheless, on this case, I am taking a cue from Ray Camden, who used the java.awt.font.TextLayout class to search out the bounding field of a given textual content worth.

Within the following ColdFusion element, AvatarGenerator.cfc, I’ve encapsulated this measurement logic in a personal methodology known as, measureText(). This takes the picture, the textual content I need to render, and the font properties I intend to make use of (ie, the Font title and the dimensions). This methodology returns the bounding field measurements, which I then use within the picture.drawText() name:

element
	accessors = true
	output = false
	trace = "I generate easy, initials-based avatars."
	{

	// Outline properties for dependency-injection.
	property scratchDisk;

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

	/**
	* I generate an initials-based avatar and return the picture binary. The picture is all the time
	* generated as a JPG as a way to match what Gravatar makes use of.
	*/
	public binary perform generateAvatar(
		required string initials,
		required numeric measurement
		) {

		var imageData = withTempDirectory(
			( tempDirectory ) => {

				var picture = imageNew( "", measurement, measurement, "rgb", "212121" );
				picture.setDrawingColor( "ffffff" );
				picture.setAntialiasing( true );

				var fontProperties = {
					font: "Roboto Mono Common",
					measurement: ( repair( measurement / 3 ) - 2 )
				};
				// Since we do not know what textual content goes to be handed into the perform,
				// we have to "measure" the textual content that will probably be rendered to the picture when
				// utilizing the given textual content worth and font properties. We will then use this
				// to middle the textual content inside the canvas measurement.
				var bounds = measureText( picture, initials, fontProperties );
				// Heart textual content horizontally.
				var x = ( ( measurement / 2 ) - ( bounds.width / 2 ) - bounds.xOffset );
				// Heart textual content vertically.
				var y = ( ( measurement / 2 ) + ( bounds.peak / 2 ) );

				picture.drawText( initials, x, y, fontProperties );

				// Save the picture object to disk (Digital File System on this case, for
				// quick file I/O operations).
				var imagePath = ( tempDirectory & "/avatar.jpg" );
				picture.write( imagePath, 0.80 );

				return( fileReadBinary( imagePath ) );

			}
		);

		return( imageData );

	}

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

	/**
	* I get the bounding field dimensions of the given textual content as it will seem written to
	* the given picture.
	*/
	non-public struct perform measureText(
		required any picture,
		required string textual content,
		required struct fontProperties
		) {

		var awtContext = picture.getBufferedImage()
			.getGraphics()
			.getFontRenderContext()
		;
		// CAUTION: When decoding a font definition, you should utilize both an area (" ") or
		// a touch ("-") delimiter. However, you can not mix-and-match the 2 characters. As
		// such, when you have a Font title which has areas in it (ex, "Roboto Mono"), you
		// MUST USE the sprint delimiter as a way to stop Java from parsing the font title
		// as a multi-item record. On this case, be aware that I'm utilizing the "-" as a result of I do know
		// my font title does not comprise a touch.
		var awtFont = createObject( "java", "java.awt.Font" )
			.decode( "#fontProperties.font#-#fontProperties.measurement#" )
		;
		var bounds = createObject( "java", "java.awt.font.TextLayout" )
			.init( textual content, awtFont, awtContext )
			.getBounds()
		;

		return({
			width: bounds.width,
			peak: bounds.peak,
			xOffset: bounds.x,
			yOffset: bounds.y
		});

	}


	/**
	* I execute the given callback, passing in a brief listing that can be utilized for
	* transient file IO. The non permanent listing is deleted after the callback has been
	* executed.
	*/
	non-public any perform withTempDirectory( required perform callback ) {

		var folderPath = ( scratchDisk & "https://www.bennadel.com/" & createUuid() );

		attempt {

			directoryCreate( folderPath );

			return( callback( folderPath ) );

		} lastly {

			directoryDelete( folderPath, true );

		}

	}

}

This ColdFusion element has a single public methodology, generateAvatar(), which takes the consumer’s initials, corresponding to “BN” for “Ben Nadel”, and a picture dimension after which renders the picture and returns the picture binary. Which suggests, I can pipe the binary response on to the CFContent tag’s variable attribute:

<cfscript>

	// NOTE: I am utilizing RAM disk (Digital File System) because the scratch disk for the picture
	// operations in order that I get quick I/O efficiency.
	generator = new AvatarGenerator()
		.setScratchDisk( "ram://" )
	;

	cfcontent(
		sort = "picture/jpeg",
		variable = generator.generateAvatar( "BN", 120 )
	);

</cfscript>

Now, if I run this ColdFusion code, we are able to see the “BN” avatar being generated immediately and getting streamed to the browser:

Avatar with the initials, BN.

In fact, you do not need to be producing photos on-the-fly on a regular basis. So, one thought is perhaps to render these to disk first after which serve them up; or, I would simply depend on the CDN (Content material Supply Caching) to cache them and stop pointless processing. However, that is a thought for a special put up.

Wish to use code from this put up?
Try the license.



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments