Using CSS Utility Classes In CFDocument In ColdFusion
I'm not a huge fan of CSS frameworks that base their entire rendering strategy on utility classes. But, as I've been working on generating PDFs at PAI Industries using the CFDocument
tag in ColdFusion / Lucee CFML, I've found that CSS utility classes have been immensely helpful!
Normally, when I author CSS in my web applications, I like to put most of my CSS into Less CSS stylesheets, where I can create simple design systems that bring a consistent look-and-feel to the app (at least, that's the intent). With PDFs, however, I'm finding that there is very little consistency in the look-and-feel across the organization. Plus, throw in documents that have to adhere to an existing ISO standard, and you can't just willy-nilly change the design of a PDF to fit your branding.
Which is to say, using CSS utility classes to tweak every aspect of a PDF has been very handy. Of course, it's not as simple as copy-pasting the CSS utility classes out of a framework like Bootstrap or Tailwind—the CFDocument
tag is very much not "the modern web". It's support of CSS is extremely limited; and, is more akin to rendering HTML Emails (hello table-based layouts!).
Plus, each implementation of CFDocument
is different. Meaning, the underlying engine in Adobe ColdFusion (ACF) is different than the engine in Lucee CFML; and, the Lucee, itself, has two different engines: "modern" and "classic". So, what I'm demonstrating in this post isn't a specific set of utilities, but rather, an approach to building CSS utility classes.
Aside: if you want to step outside of the native ColdFusion implementations, other technologies like WKHtmlToPDF and headless Chrome can be used to achieve more modern rendering.
For the most part, a CSS utility class is just a short-hand notation for the raw CSS you could have written in a style
attribute. This isn't entirely true once you start throwing in mechanics like media queries; but, for the sake of this discussion, it's true.
Meaning, instead of having to write raw styles like this:
style="width: 50% ; font-weight: bold ; font-size: 24px ;"
... you could use the following CSS utility classes:
class="w-50 fw-bold fs-24"
The goal isn't to create something magical—the goal is simply to provide an easy and light-weight way to apply custom styling to a PDF document. Right now, my utility classes pertain to a limited set of CSS properties. I'm using Bootstrap-inspired values in which t
, b
, s
, and e
refer to the "top", "bottom", "start" (left), and "end" (right) sides of the box-model, respectively.
Mostly, I'm just using CFLoop
to create index-based CSS class names. Here's my base <style>
tag that I pull into the CFDocument
via CFInclude
. I've been testing this in the Lucee CFML engine with the Modern PDF rendering:
<cfoutput>
<style type="text/css">
<!---
Note: styles don't seem to cascade properly in the PDF implementation; as such,
I'm brute-force applying a base style to all elements with zero specificity.
--->
* {
box-sizing: border-box ;
color: black ;
font-family: arial, verdana, sans-serif ;
font-size: 14px ;
font-weight: 400 ;
line-height: 1.4 ;
margin-bottom: 0 ;
margin-top: 0 ;
}
body {
margin: 0 ;
padding: 0 ;
}
h1, h2, h3, h4, h5, th, dt, strong, {
font-weight: 700 ;
}
.fw-normal {
font-weight: 400 ;
}
.fw-bold {
font-weight: 700 ;
}
table {
border-collapse: collapse ;
border-spacing: 0 ;
width: 100% ;
}
th, td {
padding: 0 ;
text-align: left ;
vertical-align: top ;
}
[align="right"] {
text-align: right !important ;
}
[align="center"] {
text-align: center !important ;;
}
[valign="bottom"] {
vertical-align: bottom !important ;
}
[valign="middle"] {
vertical-align: middle !important ;
}
img {
border: 0 ;
display: block ;
}
<!--- Bootstrap inspired utility classes. ---->
.d-block { display: block ; }
.text-start { text-align: left ; }
.text-center { text-align: center ; }
.text-end { text-align: right ; }
.no-wrap { white-space: nowrap ; }
<!--- Padding utilities. --->
<cfloop index="i" from="0" to="50">
.p-#i# { padding: #i#px ; }
.pt-#i# { padding-top: #i#px !important ; }
.pb-#i# { padding-bottom: #i#px !important ; }
.ps-#i# { padding-left: #i#px !important ; }
.pe-#i# { padding-right: #i#px !important ; }
</cfloop>
<!--- Margin utilities. --->
<cfloop index="i" from="0" to="50">
.m-#i# { margin: #i#px ; }
.mt-#i# { margin-top: #i#px !important ; }
.mb-#i# { margin-bottom: #i#px !important ; }
.ms-#i# { margin-left: #i#px !important ; }
.me-#i# { margin-right: #i#px !important ; }
</cfloop>
<!--- Font-size utilities. --->
<cfloop index="i" from="1" to="50">
.fs-#i# { font-size: #i#px ; }
</cfloop>
<!--- Line-height utilities (1...3 increments of 0.1). --->
<cfloop index="i" from="0" to="20">
.lh-#i# { line-height: #numberFormat( 1 + ( i * 0.1 ), "0.00" )# ; }
</cfloop>
<!--- Border utilities. --->
<cfloop index="i" from="0" to="10">
.b-#i# { border: #i#px solid black ; }
.bt-#i# { border-top: #i#px solid black !important ; }
.bb-#i# { border-bottom: #i#px solid black !important ; }
.bs-#i# { border-left: #i#px solid black !important ; }
.be-#i# { border-right: #i#px solid black !important ; }
</cfloop>
<!--- Width utilities. --->
<cfloop index="i" from="1" to="100">
.w-#i# { width: #i#% ; }
</cfloop>
<!--- Rule utilities. --->
hr {
border: 1px solid black ;
border-width: 1px 0 0 0 ;
margin: 26px 0 ;
}
<cfloop index="i" from="1" to="10">
.hr-#i# { border-top-width: #i#px ; }
</cfloop>
<!--- Color utilities. --->
<cfloop index="i" item="h" array="#reMatch( '.', '0123456789ABCDEF' )#">
.black-#i# { color: ###h##h##h# ; }
.red-#i# { color: ##f#h##h# ; }
.blue-#i# { color: ###h##h#f ; }
</cfloop>
</style>
</cfoutput>
As you can see, there's no magic here. It's just a lot of brute-force looping and using the index variable to define a dynamic CSS class name. Within a CFDocument
tag, I can then CFInclude
this template into the top of the PDF and use these dynamic class names in my HTML markup.
In the following code, note that I'm still using table-based layouts. As I mentioned above, the CSS support in PDFs (via CFDocument
) is very limited. Certainly there's no CSS Flexbox or CSS Grid support. At work, I try to put role="presentation"
on these tables; but, I've omitted that for this demo.
<cfoutput>
<cfdocument
format = "pdf"
type = "modern"
pageType = "custom"
pageWidth = "10"
pageHeight = "11"
orientation = "landscape"
margin = "#{ top: 0.3, bottom: 0.3, left: 0.3, right: 0.3 }#"
unit = "in">
<!--- Includeing my CSS utilities. --->
<cfinclude template="./styles.cfm" />
<!--- Includeing my CSS utilities. --->
<h1 class="fs-30 mb-26 text-center">
CSS Utility Classes In CFDocument In ColdFusion
</h1>
<table border="1" class="bt-3 bb-3 mb-35">
<tr>
<cfloop index="i" from="1" to="30" step="3">
<td align="center" class="w-10 pt-#i# fs-20">
pt-#i#
</td>
</cfloop>
</tr>
<tr>
<cfloop index="i" from="1" to="30" step="3">
<td align="center" valign="bottom" class="pb-#i# fs-20">
pb-#i#
</td>
</cfloop>
</tr>
</table>
<table class="mb-35">
<tr>
<cfloop index="i" from="0" to="10">
<td valign="middle" align="center" class="b-#i# bb-1 p-10 fs-20">
b-#i#
<em class="d-block lh-1">bb-1</em>
</td>
</cfloop>
</tr>
</table>
<cfloop index="i" from="1" to="10">
<hr class="hr-#i# w-#( i * 10 )#" />
</cfloop>
<table border="1" class="mt-50">
<tr>
<cfloop index="i" from="10" to="50" step="5">
<td valign="middle" class="fs-#i# text-center">
fs-#i#
</td>
</cfloop>
</tr>
</table>
<table border="1" class="mt-50">
<cfloop array="#[ 'black', 'red', 'blue' ]#" index="color">
<tr>
<cfloop index="i" from="1" to="16">
<td class="text-center fw-bold pt-5 pb-5 #color#-#i#">
#color#-#i#
</td>
</cfloop>
</tr>
</cfloop>
</table>
</cfdocument>
</cfoutput>
Again, there's no magic here—I'm just using the CSS utility classes to apply specific styles to specific HTML elements. And, when we run this Lucee CFML 6.2 code with the "modern"
PDF rendering engine, we get the following output:
Since the CFDocument
tag has limited CSS support, creating these has involved some trial-and-error. For example, the opacity
property isn't supported (in this particular PDF engine); and, neither is the opacity channel in hex-colors or rgba-colors. As such, to get a spectrum of black colors to work, I'm just iterating over a set number of hex codes.
That said, I've been very pleased with what this limited set of CSS utility classes allows me to do in a PDF in ColdFusion. And, of course, if I need to have something super specific, I can always create custom CSS classes or use an inline style
attribute. The CSS utility classes are merely a base upon which to build.
Want to use code from this post? Check out the license.
Reader Comments
This is why I moved to weasyprint; it has css3 support!
Thanks as always for sharing your deep dive with us!
Making cfdocument PDFs always makes me feel a nostalgia for the old days of web dev!
@Tom,
Wow, I've never heard of WeasyPrint before; but, it looks really robust. And it seems like it's had a ton of work and support put into it - seems they even have it available as Docker images and AWS Lambda functions.
How are you running it on your end? What kind of set up do you have?
@Ken,
100% When ColdFusion introduces the Image generation and the PDF generation, it was like, "dang, this stuff is unreal!" I've been playing around a lot with HTMX lately to try and return to more of that traditional (ie, "old school") approach to web development. We'll see how far I go - it's a big mind shift.
@Ben
We install it as part of our docker base image, then the app can just call via commandline (like wkhtmlpdf etc). It means you have to install some dependencies to get it working (especially from say an Alpine image) but we haven't needed to spin it out into it's own service yet.
I just go so sick of flying saucer vs legacy lucee PDF generation - the handling of padding and margins was a dark art and a mess.
But I did exactly what you've done, and create a "test page" with all the grids I'd need and went from there.
The only catch is sometimes headers / footers / dynamic page counts etc use CSS for placement, so you need to get savvy with @page and the various other bits.
@Tom,
Very cool - I'll have to try it out locally in Docker. I do prefer to keep things as simple as possible, I'm still in my "microservices recovery PTSD" phase of life 🤣 If I can keep everything together, I'm happy.
Re: padding and margin being a dark art, I actually just ran into this last week. For reasons that I don't understand, Flying Saucer (Lucee "modern" rendering) just will not put any
margin
after animg
tag, even if the image isdisplay:block
. I would end up having to wrap the image in atd
and applyingpadding
to the table cell.Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →