Articles
cf-scrimage: a CFML wrapper for Scrimage with WebP, filters, and a dual API
A dual API wrapper supports the full Scrimage 4.x feature set.
A post in the ColdFusion Programmers Facebook group caught my eye the other day:
I am wondering as most blog owners don't have the expertise to convert jpg or png to webp and I want an automatic function that does it for them for Galaxie Blog. CF2025 and Lucee now supports webp, but I want to make the code backward compatible with CF2023, and that is what I personally use for my own hosting. I guess for now I am stuck with manually converting the images using Photoshop still and uploading them! - Gregory Alexander
This is the exact gap I want cf-scrimage to fill. CF 2025 added native WebP support and Lucee has had it for a bit, but most of us are still running CF 2021 or 2023 in production. If you wanted WebP from CFML before now, your options were ImageMagick shell-outs, a Photoshop step, or "use a different format." None of those are great when you're trying to make a blog engine like Galaxie do the right thing automatically for non-technical users.
cf-scrimage is a CFML wrapper around the Scrimage Java library. Scrimage 4.5.4 ships a WebP module that bundles the actual cwebp/dwebp/gif2webp binaries inside the JAR. Drop in three JARs and you can write WebP from CFML on Adobe CF 2016 forward, Lucee 5+, and BoxLang 1.13+. No native install, no shell-outs, no Photoshop trip.
It does more than WebP. Scrimage's full surface is 46 named filters, 21 composite blend modes, two dominant-color transforms, plus the usual sizing and geometry work. So anywhere you'd previously reach for cfimage, cf-Thumbnailator, or shelling out to ImageMagick, cf-scrimage covers it in one wrapper. (cf-Thumbnailator still has a place for the resize-and-watermark 80%; cf-scrimage is what you reach for when you also need WebP, filters, or composites.)
What it covers
cf-scrimage v0.1.0 wraps the full Scrimage 4.x feature set behind two CFC entry points:
scrim = new Scrimage();
// One-shot: the 80% of jobs
scrim.resize("photo.jpg", "small.jpg", 320, 240);
scrim.cropImage("photo.jpg", "thumb.jpg", 200, 200);
scrim.convertFormat("photo.jpg", "out.webp", "webp");
// Fluent builder: chain whatever you need
scrim.of("photo.jpg")
.size(320, 240)
.filter("sepia")
.outputQuality(0.85)
.toFile("out.webp");
// Immutable handle: each op returns a new image
img = scrim.load("photo.jpg");
img.bound(320, 240).rotate(90).output("rotated.webp");
The dual API took some thinking. Scrimage's natural shape is immutable and functional: every operation returns a new ImmutableImage. That maps cleanly to a CFC handle where each method returns a fresh wrapper. But cf-Thumbnailator users already have a mutable builder in their muscle memory, and most of the time you just want to call scrim.resize(src, dest, 320, 240) and move on.
I shipped both. The mutable builder is the primary entry point. The immutable handle is there when you want functional chaining or when you want to fork a loaded image into multiple branches without re-reading it from disk.
Capability buckets
Scrimage 4.x splits its features across four JARs:
scrimage-core(the base, around 250 KB)scrimage-filters(1.3 MB of named filters)scrimage-formats-extra(TIFF, PCX, PNM, TGA, IFF, SGI)scrimage-webp(21 MB, almost all of which is bundled native binaries)
Plus transitive dependencies (metadata-extractor, pngj, commons-io, TwelveMonkeys ImageIO, slf4j-api). The minimum useful install is around 2 MB. The full kit with WebP is around 28 MB.
I didn't want everybody who just needs resize to drag along 21 MB of WebP binaries. So cf-scrimage probes the classpath at construction time, builds a capability registry, and gates the methods that need optional JARs. If you call scrim.convertFormat("a.jpg", "b.webp", "webp") without scrimage-webp on the classpath, you get a typed exception with the exact JAR list you're missing, not a class-not-found stack trace:
Scrimage.MissingDependency
Capability 'webp' is not available. Required JAR(s):
scrimage-webp-4.5.4.jar + commons-lang3-3.20.0.jar + slf4j-api-2.0.18.jar
docs/capabilities.md has the per-bucket JAR list with Maven coordinates and direct download URLs.
Things that surprised me during development
Two real-world issues that took a while to figure out and might save someone else the same trouble.
Scrimage's image loader doesn't see CF's classpath. When Scrimage 4.x loads an image via ImmutableImage.loader().fromFile(...), its internal ImageIOReader uses javax.imageio.ImageIO.getImageReaders() against its own classloader. ColdFusion loads the JARs via this.javaSettings.loadPaths, which is a separate JavaDynamicClassLoader from the bootstrap classloader where the actual JPEG/PNG readers live. The net result: Scrimage finds zero decoders and decode fails.
The fix is to collect readers from CF's bootstrap classloader and inject them into Scrimage's loader explicitly:
variables._javaxReaders = _collectJavaxReaders();
return variables.JImmutableImage.loader()
.withJavaxImageReaders(variables._javaxReaders)
.fromFile(variables.JFile.init(javacast("string", srcPath)));
That's the kind of thing you only learn by hitting it.
CF 2025 plus Java 21 needs a list of --add-opens flags. Adobe ColdFusion 2025 supports Java 21. Java 21's stricter module access blocks reflection into javax.imageio.ImageIO internals by default. CF's wrapper code does that reflection. So out of the box, 25 of my 197 tests failed on CF 2025 with InaccessibleObjectException errors.
The fix is JVM args. server-cf2025.json ships with this incantation in JVM.args:
--add-opens=java.desktop/javax.imageio=ALL-UNNAMED
--add-opens=java.desktop/com.sun.imageio.plugins.png=ALL-UNNAMED
--add-opens=java.desktop/com.sun.imageio.plugins.jpeg=ALL-UNNAMED
[... and similar for gif, bmp, tiff, java.awt, java.lang ...]
After applying those, CF 2025 passes the same 197 tests as the rest of the engines. If you run CF 2025 outside CommandBox, copy the full JVM.args value out of server-cf2025.json into your own startup script.
Tested engines
cf-scrimage v0.1.0 passes 197 tests, 0 failures, 1 PENDING (formatsExtra, gated on optional TwelveMonkeys codec JARs) on:
- Adobe ColdFusion 2016, 2018, 2021, 2023, 2025
- Lucee 5.4.8
- BoxLang 1.13 (Java 21)
CF 2016 requires you to point its JRE at OpenJDK 11+ rather than the default Java 8 (Scrimage 4.x is compiled for Java 11+). Most production CF 2016 installs already do this for unrelated security reasons.
A look at the new toys
Three features cf-Thumbnailator doesn't have:
// 46 named filters
scrim.applyFilter("photo.jpg", "out.jpg", "sepia");
scrim.applyFilter("photo.jpg", "out.jpg", "sobel"); // edge detection
scrim.applyFilter("photo.jpg", "out.jpg", "border", [10]); // 10px border
scrim.applyFilter("photo.jpg", "out.jpg", "twirl");
scrim.applyFilter("photo.jpg", "out.jpg", "kaleidoscope");
// 21 composite blend modes (multiply, screen, color-burn, hard-light...)
scrim.compositeImages("photo.jpg", "out.jpg", "texture.jpg", "multiply", 0.8);
// 2 dominant-color transforms (gradient placeholders, blurhash-style)
scrim.applyTransform("photo.jpg", "bg.jpg", "background_gradient");
The composites and dominant-color transforms are useful for placeholder cards and hero-image backgrounds. If you've ever wanted a blurhash-style preview that uses the actual color palette of the image, the dominant gradient gets you most of the way there.
What's next
A few things I want to add or fix in v0.2.x:
- A
CaptionFilterwrapper that exposes Scrimage's text-overlay capability without the ten-argument constructor. The bare filter table can't represent it cleanly. - Better
VignetteFilterhandling. It needsTYPE_INT_ARGB_PREpixel storage which the JPEG load path doesn't produce. - A skill
compare.cfmagainst more rotated/EXIF-bearing source images. Right now the EXIF preservation column tells you what the wrapper preserves, but a richer fixture set would make it more useful.
The repo is at github.com/JamoCA/cf-scrimage. Issues, PRs, and "this didn't work on my engine" reports all welcome.
License notes
cf-scrimage itself is MIT. Scrimage 4.5.4 is Apache 2.0; its JARs carry that license intact when you bundle them. The native cwebp/dwebp/gif2webp binaries embedded in scrimage-webp are from Google's libwebp project, also under a BSD-style license.
Install with box install cf-scrimage. CommandBox pulls the wrapper from ForgeBox. You supply the Scrimage JARs from docs/capabilities.md depending on which buckets you want.