BoxLang 🚀 A New JVM Dynamic Language Learn More...

cf-scrimage

v0.1.0 Modules

cf-scrimage

ForgeBox Version ForgeBox Downloads

A CFML wrapper around the Scrimage Java library (com.sksamuel.scrimage:scrimage-core: 4.5.4). It does the usual image work (resize, crop, rotate, scale, filters, watermarks, format conversion) plus WebP read/write, which is the main thing you can't get out of cf-Thumbnailator.

The API has two shapes. There's a mutable fluent builder for chained operations, and one-shot helpers for the everyday "resize this JPEG to that size and write it back" calls. Pick whichever fits the call site.

Optional capability buckets let you start with a 2 MB minimum footprint and add WebP, filters, extra formats, or rich EXIF metadata only when you actually need them.

Supported engines

  • Adobe ColdFusion 2016, 2018, 2021, 2023, 2025 - all on Java 11+
  • Lucee 5+
  • BoxLang 1.13+ on Java 21+

Java 11 is required. Scrimage 4.x is compiled for Java 11+. Adobe CF 2016 ships with Java 8, which throws UnsupportedClassVersionError the moment the Scrimage JARs load, so you need to point CF 2016 at a Java 11+ JRE. Most admins swap in OpenJDK 11 or 17 anyway for security. The bundled CommandBox server profiles take care of this: server-cf2016.json, server-cf2023.json, and server.json (CF 2021) all use OpenJDK 11, and server-cf2025.json uses OpenJDK 21. CF 2018 and newer typically already run on Java 11.

BoxLang 1.13+ needs Java 21, which server-boxlang.json sets via "javaVersion": "openjdk21_jre". BoxLang also loads JARs differently from ACF. Scrimage JARs go in BoxLang's runtime lib directory (WEB-INF/boxlang/lib/) instead of through this.javaSettings.loadPaths. Application.cfc sniffs the engine and skips loadPaths on BoxLang and Lucee when the JARs are already there. The per-engine details are in docs/boxlang-notes.md and docs/lucee-notes.md.

CF 2025 needs one extra thing because of Java 21's stricter module system: a list of --add-opens JVM args so ACF can reflect into the JDK's ImageIO classes. The bundled server-cf2025.json ships with that list baked in; if you're running CF 2025 outside CommandBox, copy the JVM.args value into your own startup config.

Install

box install

box install cf-scrimage

CommandBox fetches the wrapper CFC from ForgeBox. It doesn't pull the Scrimage JARs. You supply those separately. See below.

JAR placement

Drop the JARs into lib/ next to Application.cfc, or set SCRIMAGE_JAR_DIR to the directory that contains them. The wrapper resolves JARs from one of three places in order:

  1. SCRIMAGE_JAR_PATH env var or system property (full path to a single JAR)
  2. SCRIMAGE_JAR_DIR env var or system property (directory containing the JARs)
  3. ./lib/ next to Application.cfc

Which JARs you need depends on which capability buckets you want. Start with the Minimum bucket (three JARs, about 2 MB) and add from there. See docs/capabilities.md for the per-bucket JAR list with Maven coordinates and direct download URLs.

Capability buckets

The buckets are additive. Drop in only the JARs for what you're going to use.

Bucket Size Unlocks
Minimum~2 MBResize, crop, rotate, scale, watermark, JPEG/PNG/GIF read/write. No rich EXIF metadata.
+ Rich metadata+1 MBFull EXIF/IPTC/XMP in inspect().
+ Robust JPEG/PNG+1.5 MBCMYK JPEGs, Adobe JPEG variants, EXIF-rotated JPEGs decoded cleanly.
+ Extra formats+2 MBTIFF, PCX, PNM, TGA, IFF, SGI read/write; enhanced BMP.
+ Filters+1.3 MBfilter() builder op, applyFilter() one-shot, 46 named filters.
+ WebP / animated+21 MBWebP read/write, animated WebP, gifToWebp(). Most of the size is bundled native binaries.

Each bucket above the minimum is capability-gated. Methods that depend on a missing bucket still exist on the API surface, but they throw Scrimage.MissingDependency with a message listing exactly which JARs are needed.

See docs/capabilities.md for the full JAR list and download URLs.

Quickstart

One-shot helpers

scrim = new Scrimage();

// Resize preserving aspect ratio
r = scrim.resize("photo.jpg", "small.jpg", 320, 240);
// r.ok, r.width, r.height, r.sizeBytes, r.format, r.durationMs

// Center crop to exact dimensions
r = scrim.cropImage("photo.jpg", "thumb.jpg", 200, 200);

// Convert to WebP (requires WebP capability)
r = scrim.convertFormat("photo.jpg", "photo.webp", "webp");

Fluent builder

scrim = new Scrimage();

// Resize, apply sepia filter, write as WebP
scrim.of("photo.jpg")
     .size(320, 240)
     .filter("sepia")
     .outputQuality(0.85)
     .toFile("out.webp");

// Rotate and watermark, return result struct
r = scrim.of("photo.jpg")
         .rotate(90)
         .watermark("logo.png", "bottom_right", 0.5, 10)
         .toFile("stamped.jpg");

Immutable handle

scrim = new Scrimage();

img = scrim.load("photo.jpg");
// img.width(), img.height(), img.hasAlpha()

r = img.bound(320, 240).rotate(90).output("out.jpg");
// r.ok, r.width, r.height, r.sizeBytes, r.format, r.durationMs

// Each operation returns a new handle; the original is unchanged
img2 = img.filter("sepia");   // requires Filters capability

One-shot API

Each one-shot returns a result struct: ["ok": true, "destPath": ..., "width": ..., "height": ..., "sizeBytes": ..., "format": ..., "durationMs": ..., "capabilitiesUsed": [...]].

Method Signature Notes
resize (srcPath, destPath, width, height, opts) Aspect-preserving by default
scaleImage (srcPath, destPath, factor, opts) Uniform scale multiplier
rotateImage (srcPath, destPath, degrees, opts) Clockwise; negative goes the other way
cropImage (srcPath, destPath, width, height, positionName, opts) positionName defaults to "center"
watermarkImage (srcPath, destPath, wmPath, positionName, opacity, insets, opts) opacity 0..1; insets in pixels
convertFormat (srcPath, destPath, formatName, opts) Keeps dimensions, changes encoding
createThumbnail (srcPath, destPath, width, height, opts) Forces JPEG out, quality 0.85, EXIF orientation honored
batchResize (srcDir, destDir, width, height, opts) Returns ["results": [], "count": ..., "totalMs": ..., "totalBytes": ...]
applyFilter (srcPath, destPath, filterName, filterArgs, opts) Requires Filters capability; filterArgs array overrides defaults
applyTransform (srcPath, destPath, transformName, transformArgs, opts) Two transforms: background_gradient, dominant_gradient
compositeImages (srcPath, destPath, otherImagePath, compositeName, alpha, opts) Blend-mode merge; 21 modes available
gifToWebp (srcGif, destWebp, opts) Requires WebP capability; returns ["ok", "destPath", "sizeBytes", "format", "durationMs"]
inspect (srcPath) Returns width, height, format, sizeBytes, hasAlpha, exifOrientation. Adds metadata struct when Rich metadata capability is present.

The opts struct for resize/scale/rotate/crop/watermark/convert/createThumbnail accepts: quality, scaleMethod, useExifOrientation, allowOverwrite, outputFormat, outputFormatType, keepAspectRatio, exifPassthrough. Only keys you set get applied.

exifPassthrough

exifPassthrough defaults to false. When you set it to true, the wrapper copies the source JPEG's APP1/Exif segment into the destination JPEG after writing and forces the Orientation tag back to 1 so downstream viewers don't double-rotate the image. Only meaningful when both source and destination are JPEG. Silently skipped for other format combinations or when the source has no EXIF segment.

scrim.resize("photo.jpg", "small.jpg", 320, 240, ["exifPassthrough": true]);
// Make, Model, DateTimeOriginal, GPS tags all survive. Orientation is reset to 1.

Fluent builder reference

Setters return this. Terminals run the operation and return data.

Source:

of(srcPath | array | directory)

Sizing:

size(width, height)          scale down to fit, preserving aspect ratio
forceSize(width, height)     scale to exact dimensions regardless of aspect
width(value)                 set width, height calculated from aspect ratio
height(value)                set height, width calculated from aspect ratio
scale(factor)                uniform scale multiplier
keepAspectRatio(true|false)  when false, size() behaves like forceSize()

Geometry:

rotate(degrees)              clockwise; 90/180/270 use lossless Java ops
flipX()                      horizontal mirror
flipY()                      vertical mirror
crop(positionName)           crop at position; size() provides the target dimensions
sourceRegion(x, y, w, h)     rectangular subimage by pixel coords
sourceRegion(posName, w, h)  positioned crop
pad(size, color)             add equal padding on all sides; color is hex (default black)
autocrop(colorTolerance)     remove solid-color borders; 0 = transparent, >0 = black with tolerance
trim(pixels)                 trim N pixels from all sides
zoom(factor)                 centered zoom
bound(width, height)         scale down to fit within box, preserving aspect ratio
cover(width, height, pos)    scale and crop to cover exact dimensions
fill(width, height, color)   fit into box, padding empty space with color

Adjustments:

brightness(factor)           1.0 = unchanged; >1 = brighter; <1 = darker
contrast(factor)             1.0 = unchanged

Composition:

watermark(wmPath, posName, opacity)         overlay watermark at position, opacity 0..1
watermark(wmPath, posName, opacity, insets) same with pixel inset from edge
overlay(overlayPath, posName)               overlay image without opacity blending

Filters (requires Filters capability):

filter(name)                 apply a named filter; see filter table below
filter(name, args)           apply a named filter with custom constructor args

Transforms:

transform(name)              apply a named transform; see transform table below
transform(name, args)        apply a named transform with dimension overrides (background_gradient only)

Composites (blend-mode merge with a second image):

composite(name, otherPath, alpha)    blend this image with otherPath using the named blend mode; alpha 0..1

Output controls:

outputFormat(name)           override format (overrides dest-path extension)
outputQuality(0..1)          encoder quality for JPEG and WebP
outputFormatType(subtype)    format-specific subtype string
useOriginalFormat()          force output to match source format
scaleMethod(name)            scaling algorithm; see scale-method table below
allowOverwrite(true|false)   default true
useExifOrientation(true|false) honor EXIF orientation on read; default false

Terminals:

toFile(destPath)             -> result struct (ok, destPath, width, height, sizeBytes, format, durationMs, capabilitiesUsed)
toFiles(destDir, prefix)     -> array of result structs; processes each source from of(array|dir)
toBytes(format)              -> java byte[]
asBufferedImage()            -> java.awt.image.BufferedImage
asImage()                    -> ScrimageImage handle wrapping the result
load(srcPath)                -> ScrimageImage handle for the source file (fluent side not used)

The builder is reusable: setters accumulate into an internal op list and terminals replay them. You can call a terminal more than once on the same chain.

Immutable handle reference (ScrimageImage)

scrim.load(srcPath) returns a ScrimageImage. Operations return a new ScrimageImage; the original is not modified.

Properties (no arguments):

width()              numeric
height()             numeric
hasAlpha()           boolean
bufferedImage()      java.awt.image.BufferedImage

Sizing operations:

resize(width, height, scaleMethod)    scale to exact dimensions
bound(width, height)                  scale down to fit within box (same as size() with keepAspectRatio=true)
scaleTo(width, height, scaleMethod)   alias for resize()
fit(width, height, color)             fit into box, pad with color
cover(width, height, positionName)    scale and crop to cover exact dimensions

Geometry operations:

crop(x, y, width, height)    rectangular subimage by pixel coords
rotate(degrees)               clockwise
flipX()
flipY()
pad(size, color)
autocrop(tolerance)
trim(pixels)
zoom(factor)

Adjustments:

brightness(factor)
contrast(factor)

Composition:

overlay(overlayPath, positionName)   overlay without opacity blending

Filters (requires Filters capability):

filter(filterName)
filter(filterName, args)     override default constructor args

Transforms:

transform(name)
transform(name, args)        args struct: {width: N, height: N} for background_gradient

Composites:

composite(name, otherImagePath, alpha)

Terminals:

output(destPath, opts)   -> result struct (ok, destPath, width, height, sizeBytes, format, durationMs)

opts for output() accepts quality to override the encoder quality for JPEG and WebP.

Reference values

Position names

center
top_left      top_center      top_right
left_center                   right_center
bottom_left   bottom_center   bottom_right

Used by crop, sourceRegion, watermark, overlay, cover, fill, and ScrimageImage.cover().

Scale-method names

Name Algorithm
default Lanczos3 (alias)
quality Lanczos3 (alias)
speed FastScale (alias)
fast_scale FastScale
bicubic Bicubic
bilinear Bilinear
progressive_bilinear Progressive
bspline BSpline
lanczos3 Lanczos3

Filter names (requires Filters capability)

46 filters are registered. Pass the name string to filter(), applyFilter(), or ScrimageImage.filter().

No-arg (default constructor):

blur            bump            chrome          contour         crystallize
diffuse         dither          edge            emboss          gain_bias
glow            gotham          grayscale       hsb             invert
kaleidoscope    lens_blur       lens_flare      nashville       prewitt
rgb             roberts         rylanders       sepia           sharpen
sobel           solarize        sparkle         summer          swim
vintage

Single-arg with default (override with filter(name, [value])):

black_threshold   default 50.0    (double, range 0..100)
border            default 5       (int, thickness in pixels)
gaussian          default 3       (int, radius)
noise             default 25      (int)
oil               default 4       (int)
pixelate          default 10      (int, block size)
posterize         default 4       (int, levels)
quantize          default 16      (int, colors)
threshold         default 127     (int, 0..255)
twirl             default 1.0     (float, angle in radians)

Multi-arg (override with filter(name, [v1, v2, ...])):

colorize          default [255, 0, 0]   (int r, int g, int b)
motion_blur       default [5.0, 0.0]   (double distance, double angle)

String-arg (override with filter(name, ["text"])):

watermark_cover   default ""     (string text)
watermark_stamp   default ""     (string text)

Transforms

Two transforms are available. They ship in scrimage-core so no extra JAR is needed.

Name Constructor Notes
dominant_gradient no-argReplaces image pixels with a gradient derived from the dominant colors
background_gradient (width, height) Builds a gradient underlay sized to the image (or pass args struct with width/height keys)

Builder:

scrim.of("photo.jpg").size(400, 300).transform("dominant_gradient").toFile("gradient.jpg");
scrim.of("photo.jpg").size(400, 300).transform("background_gradient").toFile("bg.jpg");

One-shot:

scrim.applyTransform("photo.jpg", "out.jpg", "dominant_gradient");

Immutable handle:

scrim.load("photo.jpg").bound(400, 300).transform("dominant_gradient").output("out.jpg");

Composites

21 blend modes. The blend is between the current pipeline image (destination) and a second image (source). Both are normalized to TYPE_INT_ARGB before blending. alpha controls blend strength (0.0..1.0).

Name Java class
average AverageComposite
blue BlueComposite
color ColorComposite
color_burn ColorBurnComposite
color_dodge ColorDodgeComposite
diff DifferenceComposite
glow GlowComposite
green GreenComposite
hard_light HardLightComposite
heat HeatComposite
hue HueComposite
lighten LightenComposite
luminosity LuminosityComposite
multiply MultiplyComposite
negation NegationComposite
overlay OverlayComposite
red RedComposite
reflect ReflectComposite
saturation SaturationComposite
screen ScreenComposite
subtract SubtractComposite

Note: overlay here is the blend mode (OverlayComposite). The existing positional overlay() builder method (which places one image on top of another at a named position) is separate and unchanged.

Builder:

scrim.of("photo.jpg")
     .size(400, 300)
     .composite("multiply", "texture.jpg", 1.0)
     .toFile("blended.jpg");

One-shot:

scrim.compositeImages("photo.jpg", "out.jpg", "texture.jpg", "screen", 0.8);

Immutable handle:

scrim.load("photo.jpg").bound(400, 300).composite("multiply", "texture.jpg", 1.0).output("out.jpg");

Format names

Always available (Minimum bucket): jpg (alias jpeg), png, gif.

Requires Minimum bucket - note that bmp write requires the Extra formats bucket (BmpWriter lives in scrimage-formats-extra): bmp.

Requires Extra formats bucket: tiff, pnm, pcx, tga, sgi, iff.

Requires WebP bucket: webp.

WebP and native binaries

The scrimage-webp JAR bundles native cwebp, dwebp, and gif2webp binaries for:

  • linux-x64
  • linux-aarch64
  • mac-x64
  • mac-arm64
  • windows-x64

On first use, Scrimage extracts the platform-appropriate binaries to java.io.tmpdir and runs them as child processes. The CF server has to be allowed to fork external processes. Most ACF, Lucee, and BoxLang installations are. Locked-down sandbox hosts often aren't, and that's where this breaks.

If your architecture isn't in the list above (ARM Windows, Alpine/musl Linux, and so on), point Scrimage at your own cwebp/dwebp/gif2webp binaries:

  1. JVM property at startup: -Dcom.sksamuel.scrimage.webp.binary.dir=/path/to/binaries
  2. At runtime: scrim.setWebpBinaryDir("/path/to/binaries")

Heads up on option 2: if the JVM property was already set at startup, it's locked and the runtime call silently does nothing. Set it at startup when you can.

First call adds roughly 10-50 ms for the extraction; everything after that reuses the extracted binaries. Each WebP write spawns its own process, which is the documented Scrimage behavior.

Run scrim.verifyWebpNative() once on app boot to confirm the binaries actually execute on this host. It returns true on success and throws Scrimage.WebpBinaryError on failure, so you can fail fast instead of finding out at the first WebP upload.

inspect()

scrim = new Scrimage();
info = scrim.inspect("photo.jpg");
// info.width, info.height, info.format, info.sizeBytes, info.hasAlpha, info.exifOrientation

exifOrientation is the raw EXIF tag value (1..8) or 0 if the image has no EXIF orientation marker.

When the Rich metadata capability (metadata-extractor JAR) is present, inspect() also returns info.metadata - a struct keyed by directory name (e.g. "Exif IFD0", "IPTC", "XMP"), each containing an array of ["name": tagName, "value": tagValue] structs.

Capabilities

scrim = new Scrimage();
caps = scrim.capabilities();
// caps.summary    - array of capability names that are present
// caps.missing    - array of capability names that are absent
// caps.details    - struct with per-capability probe results
// caps.javaVendor - e.g. "Eclipse Adoptium"
// caps.javaVersion - e.g. "11.0.25"

Capability names: core, metadataExtractor, commonsIo, pngj, twelveMonkeysCore, twelveMonkeysJpeg, formatsExtra, filters, commonsLang3, slf4j, webp.

core is fatal - init() throws Scrimage.MissingDependency immediately if it is absent. All other capabilities degrade gracefully.

Errors

All exception types live under the Scrimage.* namespace.

Type When
Scrimage.MissingDependency Capability check failed. message lists the JAR(s) needed; detail shows the probed class.
Scrimage.SourceNotFound Source file or directory missing or unreadable.
Scrimage.UnknownFormat outputFormat name not in the format table for the available capabilities.
Scrimage.UnknownPosition Position name not in the table.
Scrimage.UnknownScalingMethod Scale-method name not in the table.
Scrimage.UnknownFilter Filter name not in the table (thrown when Filters capability is present; otherwise MissingDependency fires first).
Scrimage.UnknownTransform Transform name not in the table.
Scrimage.UnknownComposite Composite name not in the table.
Scrimage.OverwriteBlocked Destination file exists and allowOverwrite(false) is set.
Scrimage.InvalidArgument Numeric argument out of range (quality outside 0..1, opacity outside 0..1, etc.).
Scrimage.IOError Wraps java.io.IOException from the JAR or writer.
Scrimage.UnsupportedImage Source unreadable or wrong format for the codec.
Scrimage.WebpBinaryError cwebp/dwebp/gif2webp process failed. detail carries stderr from the binary when available.

Each throw carries message and detail. The type attribute is the value from the table, so you can cfcatch on the specific case:

try {
    r = scrim.of("photo.jpg").size(320, 240).toFile("out.webp");
} catch (Scrimage.MissingDependency e) {
    writeOutput("Install: " & e.message);
} catch (Scrimage.WebpBinaryError e) {
    writeOutput("WebP binary failed: " & e.detail);
}

Running the demo

box server start serverConfigFile=server.json

Then open http://localhost:8790/demo.cfm. The demo page shows canned examples for each capability bucket and a sandbox form where you can pick an operation, adjust inputs, and see the source and result side by side with the CFC code that produced each one.

The Lucee and BoxLang profiles use the same command with serverConfigFile=server-lucee.json or serverConfigFile=server-boxlang.json.

Running the tests

Start whichever server profile you want to test against, then open /tests/index.cfm in a browser. The page prints PASS/FAIL/PENDING lines for each assertion and a summary at the bottom. The response returns HTTP 500 if anything failed, so you can wire it into CI without parsing the HTML.

Tests are plain .cfm files with a tiny assert() helper. No TestBox, no MXUnit, nothing to install. Capability-gated tests skip cleanly when their JARs are absent (you see PENDING instead of FAIL), which means you can develop against the Minimum bucket without fighting red builds.

License

MIT. See LICENSE.

Scrimage 4.5.4 is licensed under Apache 2.0. The JARs in lib/ carry their upstream licenses intact.

$ box install cf-scrimage

No collaborators yet.
     
  • {{ getFullDate("2026-05-29T02:08:51Z") }}
  • {{ getFullDate("2026-05-29T02:08:52Z") }}
  • 13
  • 0