Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Using CFDocument And CFPDF To Generate PDFs With Different-Sized Pages In Lucee CFML 5.3.6.61

By Ben Nadel on
Tags: ColdFusion

When you use the CFDocument tag to generate a PDF in ColdFusion, you define the page-size on the root of the document. Which means, every page in the generated PDF uses the same size page. But, what if you wanted to generate a PDF in which each page (or related set of pages) needs a custom, content-specific size? It turns out that you can do this. But, you have to generate the uniquely-sized pages as their own PDF document; and then, combine them into a single PDF using the CFPDF tag. To try this, I put together a small demo in Lucee CFML 5.3.6.61.

The goal of this demo is take a directory of images and generate a PDF in which each image is rendered on a single page that is right-sized for the image. Which mean, as the images change size so do the PDF pages. Again, since each page is going to be a different size, we can't generate a single PDF document. Instead, we're going to read the dimensions of each image using imageInfo(); and then, generate a right-sized, single-page PDF for each image in the list. Once that's done, we'll use the merge action of the CFPDF tag to combine all of those right-sized PDFs into a single PDF.

Our biggest hurdle in this demo turns out to be calculating the size of the PDF page. Traditionally, the CFDocument tag only deals with in (inch) and cm (centimeter) units. The latest PDF rendering engine in Lucee CFML - Flying Saucer - technically adds a px (pixel) unit. However, whenever I went to use px as the unit, Lucee CFML would throw the following error:

The content height cannot be zero or less. Check your document margin definition.

NOTE: The PDF Extension that we have installed (1.0.0.80) is a little old. It's possible that above error has been fixed in a later version of the package.

As such, I had to stick with in units. Which means, I had to translate the pixel-dimensions of the image into inches. Of course, this isn't an exact science - the two units aren't connected in a consistent way, but rather depend on pixel-density. For this demo, I've decided to go with a pixel density of 150 ppi (pixels-per-inch). Which gives me the formula:

pixels / 150 = inches

Using this formula, let's look at the code that generates the individual PDF documents and then stitches them together:

<cfscript>

	withTempFolder(
		( tempFolder ) => {

			// For each image in the images folder, we're going to generate an individual
			// PDF document (with a single page in it that renders the given image). Each
			// PDF will be right-sized to fit the scaled-image.
			getImages().each(
				( imageFile, i ) => {

					var pdfImageFile = "#tempFolder#/#numberFormat( i, '0000' )#.pdf";
					var info = imageInfo( imageFile );

					// The image dimensions come back as pixels; but, there is no "px"
					// unit for PDF generation. As such, we're going to approximate "in"
					// values by using a 150-ppi (pixels per inch) density.
					// --
					// NOTE: Lucee did technically add a "px" unit when they switched to
					// the Flying Saucer PDF engine. However, I cannot for the life of me
					// even get it to render a document without throwing an error.
					var imageWidth = ceiling( info.width / 150 );
					var imageHeight = ceiling( info.height / 150 );

					// We're going to make the page dimensions slightly larger than the
					// image to produce a thin white-border around the image; but, also
					// because the sizing of the image is NOT PIXEL PERFECT and using
					// some margin helps stop one image from bleeding onto two pages.
					var margin = 0.15;
					var pageWidth = ( imageWidth + margin + margin );
					var pageheight = ( imageHeight + margin + margin );

					// NOTE: The marginBottom value is "0" to allow for slightly
					// inaccurate sizing of the image. Since the imageHeight is less than
					// the pageHeight, we'll still get an implicit bottom-margin.
					document
						format = "pdf"
						filename = pdfImageFile
						localUrl = true
						pageType = "custom"
						pageWidth = pageWidth
						pageHeight = pageHeight
						unit = "in"
						marginTop = margin
						marginRight = margin
						marginBottom = 0
						marginLeft = margin
						{

						```
						<cfoutput>
							<!doctype html>
							<html lang="en">
							<head>
								<style type="text/css">
									html,
									body {
										margin: 0 ;
										padding: 0 ;
									}
									img {
										display: block ;
										height: #imageHeight#in ;
										width: #imageWidth#in ;
									}
								</style>
							</head>
							<body>
								<img src="file:///#imageFile#" />
							</body>
							</html>
						</cfoutput>
						```

					}

				}
			);

			// Now that we have each image stored in its own, right-sized PDF, let's use
			// the CFPDF tag to stitch them all together into a single, multi-sized PDF.
			pdf
				action = "merge"
				destination = "./images.pdf"
				directory = tempFolder
				overwrite = true
				order = "name"
				ascending = "yes"
			;

		}
	);

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

	/**
	* I return an array of the full-file-paths to the images.
	*/
	public array function getImages() {

		var files = directoryList(
			path = "./images/",
			filter = "*.jpg|*.png"
		);

		return( files );

	}


	/**
	* I invoke the given callback, passing in a temp-directory in which scratch work can
	* be performed. The temp-directory will be cleaned-up automatically after the the
	* callback has been processed. Any value returned from the callback is automatically
	* passed back to the calling context.
	* 
	* @callback I am the callback being invoked with the temp-directory.
	*/
	public any function withTempFolder( required function callback ) {

		var tempFolder = expandPath( "./temp-#createUniqueId()#" );
		directoryCreate( tempFolder );

		try {

			return( callback( tempFolder ) );

		} finally {

			directoryDelete( tempFolder, true ); // True = Recurse.

		}

	}
	
</cfscript>

As you can see, we're looping over the collection of images using the .each() iterator. And, for each image, we're generating a uniquely-sized PDF using the CFDocument tag. Then, once that's done, we use the CFPDF tag to merge all of the PDFs that we generated inside the temp-directory. This leaves us with a single PDF that contains different-sized pages:

A PDF in which each page is a different size.

As you can see, each page in the generated PDF document is a different size - each one right-sized for the image that it is rendering.

At InVision, we don't do a lot of PDF work. However, we have some large enterprise clients who have expressed a need to have more control over PDF generation in their prototypes. As such, I'm trying to dust-off my PDF skills. Being able to generate a PDF with dynamically-sized pages may come in handy. And, it's nice to see that this is possible with CFDocument and CFPDF in Lucee CFML 5.3.6.61.



Reader Comments

What has two thumbs and hopes you leave a comment? This Guy! (Ben Nadel).

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.