Skip to main content
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Sean Corfield
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Sean Corfield

Rendering QR Codes With Zxing And ColdFusion

By
Published in Comments (5)

Last week, I looked at rendering 1-dimensional bar codes with Zxing and ColdFusion. The Zxing Java library creates a really flexible abstraction. Basically, it provides a bunch of "writers" that can encode different barcode formats into a "bit matrix" (a 2D representation of on/off pixels); and then, it provides a way to generically render that bit matrix into an image. As a follow-up post, I wanted to look at rendering QR Codes since this flavor of barcode has really become quite popular over the last few years (especially since the pandemic).

One of the really cool features of the QR code rendering is that it can absorb a certain degree of "error correction". Which means, parts of the QR code can be obscured and the barcode scanner will still properly interpret the encoded data. The level of applied error correction determines how much of the QR code can be obscured without breaking the functionality. And, the greater the degree of error correction, the larger the QR code rendering has to be.

When you go to a restaurant and they provide a touch-free menu via a QR code; and, the restaurant has included its logo in the middle of the QR code; well, that's not part of the QR code specification. They are just taking advantage of the error correction functionality. So as long as the branding / logo doesn't obscure more than the error correction can account for, the QR code still works.

I had no idea this was how QR codes worked. So cool.

Just as with my previous Zxing and ColdFusion demo, I'm going to be using Lucee CFML's ability to load Java classes from a set of JAR files:

<cfscript>
	/**
	* I create a Java class from the given JAR files using an isolated classloader.
	*/
	private any function fromJars( required string classname ) {
		var jarPaths = [
			expandPath( "./vendor/com.google.zxing/3.5.3/core-3.5.3.jar" ),
			expandPath( "./vendor/com.google.zxing/3.5.3/javase-3.5.3.jar" )
		];
		return createObject( "java", classname, jarPaths );
	}
</cfscript>

Ok, let's start exploring.

Rending a QR Code With CSS Grid

As I mentioned above, Zxing works by providing a flexible abstraction that includes an intermediary data structure representation of the barcode pixels: BitMatrix. If we can access the location of the on/off pixels, we can use CSS Grid to render the QR code using nothing but HTML elements.

Note that I'm setting the error correction level to H, about 30%. This allows us to absolutely position a simulated logo in the center of the QR code rendering without breaking the QR code.

<cfscript>

	include "./utils.cfm";

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	ErrorCorrectionLevel = fromJars( "com.google.zxing.qrcode.decoder.ErrorCorrectionLevel" );
	EncodeHintType = fromJars( "com.google.zxing.EncodeHintType" );

	bitMatrix = fromJars( "com.google.zxing.qrcode.QRCodeWriter" ).encode(
		// Value we're encoding into the barcode / qr code.
		"https://www.youtube.com/watch?v=Aq5WXmQQooo",

		// Encoding settings.
		fromJars( "com.google.zxing.BarcodeFormat" ).QR_CODE,
		0, // Width (zero to use minimum size for rendered image).
		0, // Height (zero to use minimum size for rendered image).
		{
			// Error correction is the % of data that can be corrupted and still have the
			// QR-code be readable. Blows my mind that this is a thing!
			// --
			// H = ~30% correction
			// L = ~7% correction
			// M = ~15% correction
			// Q = ~25% correction
			"#EncodeHintType.ERROR_CORRECTION#": ErrorCorrectionLevel.H,
			// This is the size of the "quiet zone" around the barcode. The larger the
			// quiet zone, the easier it is to identify and scan.
			"#EncodeHintType.MARGIN#": 2
		}
	);

	// Since we passed-in zero as the width/height, the QR code writer will make the image
	// as small as possible. We can then inspect the bit-matrix to see what the dimensions
	// ended up being.
	barcodeWidth = bitMatrix.getWidth();
	barcodeHeight = bitMatrix.getHeight();

</cfscript>
<cfoutput>

	<h1>
		Render QR Codes Using ColdFusion, Zxing, And CSS Grid
	</h1>

	<style type="text/css">
		.qr-code {
			border: 5px solid currentColor ;
			color: deeppink ;
			display: inline-grid ;
			grid-template-columns: repeat( #barcodeWidth#, 1fr ) ;
			position: relative ;

			/*
				I'm artificially increasing the size of the QR code by making the pixels
				(ie, grid cells) larger than 1px.
			*/
			& span {
				height: 8px ;
				width: 8px ;
			}

			& [data-on='true'] {
				background-color: currentColor ;
			}

			& .logo {
				height: 20% ;
				width: 20% ;

				background-color: dodgerblue ;
				border: 8px solid white ;
				border-radius: 100% ;
				left: 50% ;
				position: absolute ;
				top: 50% ;
				translate: -50% -50% ;
			}
		}
	</style>

	<div class="qr-code">
		<!--- Render each QR code pixel as a CSS grid cell. --->
		<cfloop index="row" from="1" to="#barcodeHeight#">
			<cfloop index="col" from="1" to="#barcodeWidth#">
				<span data-on="#bitMatrix.get( ( col - 1 ), ( row - 1 ) )#"></span>
			</cfloop>
		</cfloop>
		<!---
			Demonstrating that a logo can be overlaid on the QR code and it still works
			so long as the % of obscured data is below the error correction threshold.
		--->
		<span class="logo"></span>
	</div>

	<p>
		QR Code Image: #barcodeWidth# x #barcodeHeight# pixels
	</p>

</cfoutput>

Once we encode the URL value into the QR code, we get our two dimensional bit matrix. I'm then rendering that by using a nested loop:

  • For Each Row
    • For Each Column
      • Render pixel with on/off background color.

When we run this ColdFusion code, we get the following output:

QR code rendered with CSS grid.

This works, but it creates a heavy Document Object Model (DOM). If our QR code is 41 x 41 px, that gives us a DOM tree size of at least 1,600 nodes. The larger our encoded value, the larger our QR code becomes. Just something to keep in mind.

That said, one nice thing about rendering the QR code via CSS grid is that we can scale up the image without any blurring. While the QR code, in this case, is natively 41 x 41, I'm rendering the CSS grid at about 340 pixels (due to the size of each grid cell).

Rending a QR Code With SVG

Another DOM-oriented strategy that scales well (visually) is SVG. Since we can access the 2-dimensional layout of pixels, we can use the same nested loop to translate that into SVG rectangles. Here's the same demo, but with SVG instead of CSS grid.

When rendering to SVG, the important thing is to match the viewbox dimensions to the QR code bit matrix dimensions. This way, as the SVG scales-up, it doesn't distort the underlying encoding.

<cfscript>

	include "./utils.cfm";

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	ErrorCorrectionLevel = fromJars( "com.google.zxing.qrcode.decoder.ErrorCorrectionLevel" );
	EncodeHintType = fromJars( "com.google.zxing.EncodeHintType" );

	bitMatrix = fromJars( "com.google.zxing.qrcode.QRCodeWriter" ).encode(
		// Value we're encoding into the barcode / qr code.
		"https://www.youtube.com/watch?v=Aq5WXmQQooo",

		// Encoding settings.
		fromJars( "com.google.zxing.BarcodeFormat" ).QR_CODE,
		0, // Width (zero to use minimum size for rendered image).
		0, // Height (zero to use minimum size for rendered image).
		{
			// Error correction is the % of data that can be corrupted and still have the
			// QR-code be readable. Blows my mind that this is a thing!
			// --
			// H = ~30% correction
			// L = ~7% correction
			// M = ~15% correction
			// Q = ~25% correction
			"#EncodeHintType.ERROR_CORRECTION#": ErrorCorrectionLevel.H,
			// This is the size of the "quiet zone" around the barcode. The larger the
			// quiet zone, the easier it is to identify and scan.
			"#EncodeHintType.MARGIN#": 2
		}
	);

	// Since we passed-in zero as the width/height, the QR code writer will make the image
	// as small as possible. We can then inspect the bit-matrix to see what the dimensions
	// ended up being.
	barcodeWidth = bitMatrix.getWidth();
	barcodeHeight = bitMatrix.getHeight();

</cfscript>
<cfoutput>

	<h1>
		Render QR Codes Using ColdFusion, Zxing, And SVG
	</h1>

	<style type="text/css">
		.qr-code {
			border: 4px solid currentColor ;
			color: darkcyan ;
			height: 300px ;
			width: 300px ;

			& rect {
				fill: currentColor ;
			}
		}
	</style>

	<svg viewBox="0 0 #barcodeWidth# #barcodeHeight#" class="qr-code">
		<cfloop index="row" from="1" to="#barcodeHeight#">
			<cfloop index="col" from="1" to="#barcodeWidth#">
				<cfif bitMatrix.get( ( col - 1 ), ( row - 1 ) )>
					<rect
						x="#( col - 1 )#"
						y="#( row - 1 )#"
						width="1"
						height="1"
					/>
				</cfif>
			</cfloop>
		</cfloop>
	</svg>

	<p>
		QR Code Image: #barcodeWidth# x #barcodeHeight# pixels
	</p>

</cfoutput>

When we run this ColdFusion code, we get the following output:

QR code rendered with SVG.

This, again, gives us a fairly heavy DOM. But, you could easily envision creating a ColdFusion end-point that serves just the SVG DOM; then, you could use that SVG end-point as an <img> src attribute.

Rending a QR Code With a PNG Data URI

The Zxing provides the BitMatrix as an intermediary data format. It also provides a way to render that bit matrix to an image. This image can be written to a file, to a buffered image, or to an output stream. For the Data URI approach, rendering to an output stream is perfect. We can use a ByteArrayOutputStream and then encoded the resultant binary data as a Base64-encoded data URI.

To do this, I've created another utility method, matrixToBlob(). This method uses png by default and black/white for on/off pixels respectively.

<cfscript>

	/**
	* I convert the given barcode BitMatrix into a blob / binary object that represents
	* the rendered image in the given format.
	*/
	private binary function matrixToBlob(
		required any bitMatrix,
		string onColorHex = "000000",
		string offColorHex = "ffffff",
		string imageType = "png"
		) {

		// The default configuration uses "black" for the "on" pixels and "white" for the
		// "off" pixels. We can override with colors of our choosing. The configuration
		// object models these colors as a single INT with each byte representing the
		// Alpha, Red, Green, and Blue channels, respectively. If a non-alpha color is
		// passed-in (6 hex digits), then let's assume a fully opaque color and prepend it
		// with the "ff".
		if ( onColorHex.len() < 8 ) {

			onColorHex = "ff#onColorHex#";

		}

		if ( offColorHex.len() < 8 ) {

			offColorHex = "ff#offColorHex#";

		}

		var imageWriter = fromJars( "com.google.zxing.client.j2se.MatrixToImageWriter" );
		var imageConfig = fromJars( "com.google.zxing.client.j2se.MatrixToImageConfig" ).init(
			inputBaseN( onColorHex, 16 ),
			inputBaseN( offColorHex, 16 )
		);
		var byteStream = createObject( "java", "java.io.ByteArrayOutputStream" ).init();

		imageWriter.writeToStream( bitMatrix, imageType, byteStream, imageConfig );

		return byteStream.toByteArray();

	}

</cfscript>

Once we can convert the bit matrix into a blog, serializing it as an inline image becomes easy:

<cfscript>

	include "./utils.cfm";

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	ErrorCorrectionLevel = fromJars( "com.google.zxing.qrcode.decoder.ErrorCorrectionLevel" );
	EncodeHintType = fromJars( "com.google.zxing.EncodeHintType" );

	bitMatrix = fromJars( "com.google.zxing.qrcode.QRCodeWriter" ).encode(
		// Value we're encoding into the barcode / qr code.
		"https://www.youtube.com/watch?v=Aq5WXmQQooo",

		// Encoding settings.
		fromJars( "com.google.zxing.BarcodeFormat" ).QR_CODE,
		300, // Width.
		300, // Height.
		{
			// Error correction is the % of data that can be corrupted and still have the
			// QR-code be readable. Blows my mind that this is a thing!
			// --
			// H = ~30% correction
			// L = ~7% correction
			// M = ~15% correction
			// Q = ~25% correction
			"#EncodeHintType.ERROR_CORRECTION#": ErrorCorrectionLevel.H,
			// This is the size of the "quiet zone" around the barcode. The larger the
			// quiet zone, the easier it is to identify and scan.
			"#EncodeHintType.MARGIN#": 2
		}
	);

	// We passed-in the dimensions above; however, those are the "preferred" dimensions.
	// The image will be rendered larger if there's more data than will fit in the
	// preferred dimensions. As such, we need to double-check the image size.
	barcodeWidth = bitMatrix.getWidth();
	barcodeHeight = bitMatrix.getHeight();

	// Encode the PNG image data as base64 for our data URI.
	encodedImage = binaryEncode(
		matrixToBlob( bitMatrix, "0000cd" ), // Implicit PNG format.
		"base64"
	);

</cfscript>
<cfoutput>

	<h1>
		Render QR Codes Using ColdFusion, Zxing, And PNG Data URI
	</h1>

	<img
		src="data:image/png;base64,#encodedImage#"
		width="#barcodeWidth#"
		height="#barcodeHeight#"
		style="border: 4px solid ##0000cd ;"
	/>

	<p>
		QR Code Image: #barcodeWidth# x #barcodeHeight# pixels
	</p>

</cfoutput>

This time, instead of dealing with the bit matrix directly, we're having Zxing encode it as a PNG blob; and then we're serializing that blob into a data URI. And, when we run this ColdFusion code, we get the following output:

QR code rendered with PNG data URI.

Rending a QR Code With as a PNG With a Logo

As a final exploration, let's create a PNG of our QR code complete with a (simulated) logo in the center. To do this, we're going to take that PNG blob from above and use it as the source in an ImageNew() call. Then, we'll render a rounded rectangle to the center of the image before drawing the image to the browser.

<cfscript>

	include "./utils.cfm";

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	ErrorCorrectionLevel = fromJars( "com.google.zxing.qrcode.decoder.ErrorCorrectionLevel" );
	EncodeHintType = fromJars( "com.google.zxing.EncodeHintType" );

	bitMatrix = fromJars( "com.google.zxing.qrcode.QRCodeWriter" ).encode(
		// Value we're encoding into the barcode / qr code.
		"https://www.youtube.com/watch?v=Aq5WXmQQooo",

		// Encoding settings.
		fromJars( "com.google.zxing.BarcodeFormat" ).QR_CODE,
		400, // Width.
		400, // Height.
		{
			// Error correction is the % of data that can be corrupted and still have the
			// QR-code be readable. Blows my mind that this is a thing!
			// --
			// H = ~30% correction
			// L = ~7% correction
			// M = ~15% correction
			// Q = ~25% correction
			"#EncodeHintType.ERROR_CORRECTION#": ErrorCorrectionLevel.H,
			// This is the size of the "quiet zone" around the barcode. The larger the
			// quiet zone, the easier it is to identify and scan.
			"#EncodeHintType.MARGIN#": 2
		}
	);

	// We passed-in the dimensions above; however, those are the "preferred" dimensions.
	// The image will be rendered larger if there's more data than will fit within the
	// preferred dimensions. As such, we need to double-check the image size.
	barcodeWidth = bitMatrix.getWidth();
	barcodeHeight = bitMatrix.getHeight();

	// The matrix-to-image writer can output a BufferedImage. However, it seems that if we
	// use said buffered image as the ImageNew() source, it corrupts the underlying image
	// data in some way (that I don't understand). So, instead, we're going to convert the
	// BitMatrix to an image Blob and then use that as the source.
	image = imageNew(
		matrixToBlob( bitMatrix, "0000cd" ),
		"", // Dimensions are undefined when providing a source.
		"", // Dimensions are undefined when providing a source.
		"rgb", // Color model.
		"ffffff" // Background canvas color.
	);

	// QR code logos work by leveraging the error-correction functionality of the image
	// rendering. By using the "H" level error-correction above, it means that we can
	// obscure ~ 30% of the QR code and it will still be functional (mind blown!). So, as
	// long as our logo image doesn't cover more than this amount, it should be fine. For
	// this demo, we're going to simulate the logo with a rounded rectangle.
	rectWidth = 120;
	rectHeight = 120;
	rectX = fix( ( barcodeWidth / 2 ) - ( rectWidth / 2 ) );
	rectY = fix( ( barcodeHeight / 2 ) - ( rectHeight / 2 ) );

	image.setAntialiasing( true );
	image.setDrawingColor( "##c000ff" );
	image.drawRoundRect( rectX, rectY, rectWidth, rectHeight, 200, 200, true );

</cfscript>
<cfoutput>

	<h1>
		Render QR Codes Using ColdFusion, Zxing, And PNG
	</h1>

	<cfimage source="#image#" action="writeToBrowser" />
	<style type="text/css">
		img {
			border: 4px solid ##c000ff ;
			border-radius: 10px ;
		}
	</style>

	<p>
		QR Code Image: #barcodeWidth# x #barcodeHeight# pixels
	</p>

</cfoutput>

Again, we're using the matrixToBlob() to convert the Zxing BitMatrix into a binary value that represents the PNG rendering of the QR code. We could have written this binary to the file-system; but, instead we're using ColdFusion's writeToBrowser action to render it as a temporary file.

And, when we run this ColdFusion code, we get the following output:

QR code rendered with PNG with a simulated logo.

As you can see, the logo in the center can be quite large (when we have the error correction jacked up to 30%) and the QR code still works. In this exploration, I'm hard-coding the size of the logo. But, if you know which error correction level you are using, you could, in theory, scale the logo up-or-down to ensure that you don't cross the given obscuring threshold.

Epilogue on Rendering to a Buffered Image

In this blog post, I've been rendering the BitMatrix to an output stream / binary object. You can also render the BitMatrix to a buffered image. This is alluring because ColdFusion's ImageNew() call can use a buffered image as the source. However, thar be dragons.

In my experience (specifically tested with Lucee CFML), using the buffered image as the source creates problems. It seems to prevent the color space from working as expected. And, in some workflows, it seemed to completely prevent me from writing to the ColdFusion image object after it was initialized.

All of these problems go away when I started rendering to a blob (instead of a buffered image) and then use the blob as the ImageNew() source. I'm not sure if this is how Adobe ColdFusion would work; or, if this is something specific to Lucee's implementation of the image functions.

Want to use code from this post? Check out the license.

Reader Comments

272 Comments

This was a fascinating read! I know so much more about QR codes than I ever did before, and now I know how to generate them if I need to? Bonus! I always wondered how they incorporated the logo into a QR code...Mystery solved!

Great stuff 👏🙏

16,036 Comments

@Chris,

It's cool stuff - I think there's a lot of complicated math that goes into it; and the error correction kind of blows my mind. My brain can't even begin to guess at how that works. Luckily, someone else already figured it out :D

92 Comments

I haven't noticed this before, but the SVG image looks like a lot of individual squares. I can see white outlines throughout the image. I don't think I've ever noticed that before with SVG QR codes that I generate using Zint (command line).

For SVG, I target 600x600 output and then modify the SVG source code to disable "fixed size" and inject extra CSS styles (ie, width:100%) so that the symbol can be responsively resized within a container.

I tend not to include any of that extra customization when generating codes. A cosmetic border, padding, change of BG/FG color, absolutely positioning of a PNG in the middle or even fully rotation of the symbol can be performed post-generation using CSS. (NOTE: I'm using SVG codes for use on websites or for PDF generation using WKHTMLTOPDF. Your mileage may vary if using CFDocument or CFHtmlToPdf.)

16,036 Comments

@James,

Yeah, I also noticed the white lines in the SVG. I'm not sure why that is happening. The pixels are just being rendered as rect elements in the SVG. And I double-checked the output - it's not using random decimals or anything. I'm assuming it's just the scaling-up (the actual QR-code data is only 41x41, but I'm rendering it at like 300x300. That said, I thought SVG was infinitely scalable. So, I'm not sure where those are coming from.

re: SVG in PDF, I think I tried using it in the native Lucee PDF engine (Flying Saucer) and the SVG approach didn't work. But, the data-uri approach for PNGs did, so that's what I ended up going with anyway.

Of course, I know WKHtmlToPDF has better CSS support (and it sounds like Zint may also - I've not heard of that one before). Baby steps 🙂

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel