Clojure imaging WITHOUT java.desktop — decoding, encoding, transforms,
vector drawing, text, SVG rasterisation and plotting, all in-process through
the JDK Foreign Function & Memory API over a first-party Rust cdylib
(native/imaging-c: the image crate + resvg/usvg/tiny-skia).
Why it exists: javax.imageio, java.awt.image.BufferedImage and
Graphics2D drag the whole AWT/Java2D/ImageIO subgraph — service loaders,
sun.java2d.loops JNI blit primitives, the font manager — into a GraalVM
native image, which is both huge and, on a headless server, pointless. This
library replaces that surface with a ~5 MB cdylib and zero JNI: pure
downcalls, no subprocess, no -Djava.awt.headless, no fontconfig.
Everything is bytes in, bytes out. An Img is an opaque handle on the Rust
side; it is AutoCloseable (use with-open for tight loops) AND registered
with a java.lang.ref.Cleaner, so forgetting to close leaks nothing
permanently.
Run the JVM with --enable-native-access=ALL-UNNAMED so the foreign linker
may load the library without a restricted-method warning.
The library is resolved ONCE, lazily, the first time it's needed:
com.blockether.imaging.native.path system
property — an explicit path to the cdylib (used verbatim).prebuilds/<platform>/<lib>, shipped by
com.blockether/imaging-native-<platform>. This is the native-image path.imaging-native-<platform> jar resolved through
clojure.tools.deps (honouring Maven repos/mirrors/settings.xml),
extracted + cached. Disable with IMAGING_DISABLE_DOWNLOAD=1.<platform> in { linux-x64 linux-arm64 darwin-arm64 darwin-x64 windows-x64 }.
Clojure imaging WITHOUT `java.desktop` — decoding, encoding, transforms,
vector drawing, text, SVG rasterisation and plotting, all in-process through
the JDK Foreign Function & Memory API over a first-party Rust cdylib
(`native/imaging-c`: the `image` crate + `resvg`/`usvg`/`tiny-skia`).
Why it exists: `javax.imageio`, `java.awt.image.BufferedImage` and
`Graphics2D` drag the whole AWT/Java2D/ImageIO subgraph — service loaders,
`sun.java2d.loops` JNI blit primitives, the font manager — into a GraalVM
native image, which is both huge and, on a headless server, pointless. This
library replaces that surface with a ~5 MB cdylib and zero JNI: pure
downcalls, no subprocess, no `-Djava.awt.headless`, no fontconfig.
Everything is bytes in, bytes out. An `Img` is an opaque handle on the Rust
side; it is `AutoCloseable` (use `with-open` for tight loops) AND registered
with a `java.lang.ref.Cleaner`, so forgetting to close leaks nothing
permanently.
Run the JVM with `--enable-native-access=ALL-UNNAMED` so the foreign linker
may load the library without a restricted-method warning.
The library is resolved ONCE, lazily, the first time it's needed:
1. IMAGING_NATIVE_PATH env / `com.blockether.imaging.native.path` system
property — an explicit path to the cdylib (used verbatim).
2. A bundled classpath resource `prebuilds/<platform>/<lib>`, shipped by
`com.blockether/imaging-native-<platform>`. This is the native-image path.
3. A runtime download: the `imaging-native-<platform>` jar resolved through
`clojure.tools.deps` (honouring Maven repos/mirrors/settings.xml),
extracted + cached. Disable with IMAGING_DISABLE_DOWNLOAD=1.
`<platform>` in { linux-x64 linux-arm64 darwin-arm64 darwin-x64 windows-x64 }.A small, opinionated plotting DSL over com.blockether.imaging/plot.
The renderer lives in the Rust cdylib (native/imaging-c/src/plot.rs, drawn
with tiny-skia and text-shaped through usvg), so plotting needs NO
java.desktop, no headless AWT and no fonts on the host — four Noto faces are
embedded in the library.
Everything here is DATA: a figure is a map, a panel is a map, a series is a map. This namespace only builds those maps (and hands them to the renderer), so anything the DSL doesn't wrap yet you can still express by assoc'ing the raw key — unknown keys are passed straight through to the renderer.
(require '[com.blockether.imaging.plot :as plot])
(plot/save! "cpu.png"
(plot/figure {:title "CPU" :theme :dark :width 900 :height 400}
(plot/lines {:x-label "minute" :y-label "%"}
{:name "user" :x (range 60) :y user-samples}
{:name "sys" :x (range 60) :y sys-samples})))
Panel constructors: lines areas bars hbars stacked-bars scatter
steps histogram pie donut heatmap. Each takes an optional option map
first, then series maps (or plain sequences of numbers). figure composes
panels into a grid; a bare panel is itself a valid figure.
A small, opinionated plotting DSL over `com.blockether.imaging/plot`.
The renderer lives in the Rust cdylib (`native/imaging-c/src/plot.rs`, drawn
with tiny-skia and text-shaped through usvg), so plotting needs NO
`java.desktop`, no headless AWT and no fonts on the host — four Noto faces are
embedded in the library.
Everything here is DATA: a figure is a map, a panel is a map, a series is a
map. This namespace only builds those maps (and hands them to the renderer),
so anything the DSL doesn't wrap yet you can still express by assoc'ing the
raw key — unknown keys are passed straight through to the renderer.
```clojure
(require '[com.blockether.imaging.plot :as plot])
(plot/save! "cpu.png"
(plot/figure {:title "CPU" :theme :dark :width 900 :height 400}
(plot/lines {:x-label "minute" :y-label "%"}
{:name "user" :x (range 60) :y user-samples}
{:name "sys" :x (range 60) :y sys-samples})))
```
Panel constructors: `lines` `areas` `bars` `hbars` `stacked-bars` `scatter`
`steps` `histogram` `pie` `donut` `heatmap`. Each takes an optional option map
first, then series maps (or plain sequences of numbers). `figure` composes
panels into a grid; a bare panel is itself a valid figure.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |