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 }.(adjust img opts)Photometric adjustment: :brightness :contrast :saturation :opacity
(1.0 = unchanged).
Photometric adjustment: `:brightness` `:contrast` `:saturation` `:opacity` (1.0 = unchanged).
(blank w h)(blank w h c)A new w x h image filled with c (default transparent).
A new `w` x `h` image filled with `c` (default transparent).
(close! img)Free an image handle now. Idempotent; Img is also AutoCloseable.
Free an image handle now. Idempotent; `Img` is also `AutoCloseable`.
(color c)A colour as the packed 0xRRGGBBAA long the cdylib takes. Accepts a packed
integer, "#rgb" / "#rrggbb" / "#rrggbbaa", a basic CSS colour name
(string or keyword), or [r g b] / [r g b a] with 0-255 channels (alpha may
also be a 0.0-1.0 double).
A colour as the packed `0xRRGGBBAA` long the cdylib takes. Accepts a packed integer, `"#rgb"` / `"#rrggbb"` / `"#rrggbbaa"`, a basic CSS colour name (string or keyword), or `[r g b]` / `[r g b a]` with 0-255 channels (alpha may also be a 0.0-1.0 double).
(convert data)(convert data opts)One-shot decode -> normalise -> re-encode: the "make this attachment safe to send" path, without ever materialising an image handle.
Options: :format :quality :max-width :max-height :width :height
:background (flatten alpha onto it) :filter :max-bytes (retry at lower
quality/scale until the output fits). Returns a byte[].
One-shot decode -> normalise -> re-encode: the "make this attachment safe to send" path, without ever materialising an image handle. Options: `:format` `:quality` `:max-width` `:max-height` `:width` `:height` `:background` (flatten alpha onto it) `:filter` `:max-bytes` (retry at lower quality/scale until the output fits). Returns a byte[].
(convolve img size kernel)(convolve img size kernel {:keys [scale offset] :or {scale 0 offset 0}})Correlate img with an odd size x size kernel (a seq of size*size
weights, row-major) — blur, sharpen, emboss, edge detect: Pillow's
ImageFilter.Kernel, and the same arithmetic.
:scale divides the weighted sum (0 or nil = no division) and :offset is
added afterwards; the result is rounded half-up and clamped to 0..255. Edge
samples clamp to the border pixel and the source ALPHA is carried through
untouched, so blurring never eats a mask.
Correlate `img` with an odd `size` x `size` `kernel` (a seq of `size*size` weights, row-major) — blur, sharpen, emboss, edge detect: Pillow's `ImageFilter.Kernel`, and the same arithmetic. `:scale` divides the weighted sum (0 or nil = no division) and `:offset` is added afterwards; the result is rounded half-up and clamped to 0..255. Edge samples clamp to the border pixel and the source ALPHA is carried through untouched, so blurring never eats a mask.
(decode data)Decode PNG/JPEG/WebP/GIF/BMP/TIFF/ICO/QOI/PNM/TGA/HDR/EXR bytes — or an
SVG/SVGZ document, sized the way a browser would (see svg-canvas) — into an
Img handle. Close it (with-open) when done.
Decode PNG/JPEG/WebP/GIF/BMP/TIFF/ICO/QOI/PNM/TGA/HDR/EXR bytes — or an SVG/SVGZ document, sized the way a browser would (see `svg-canvas`) — into an `Img` handle. Close it (`with-open`) when done.
(decode-gif data)Decode an animated (or still) GIF into {:width :height :loop-count :frames},
where each frame is {:delay-ms :rgba} with :rgba the straight RGBA8 rows
as a byte[] (width*height*4). :loop-count is the iteration count, -1 for
loop forever. The native multi-frame path — an animated GIF no longer needs a
hand-rolled Clojure codec.
Decode an animated (or still) GIF into `{:width :height :loop-count :frames}`,
where each frame is `{:delay-ms :rgba}` with `:rgba` the straight RGBA8 rows
as a byte[] (`width*height*4`). `:loop-count` is the iteration count, -1 for
loop forever. The native multi-frame path — an animated GIF no longer needs a
hand-rolled Clojure codec.(decode-video data)(decode-video data opts)Decode an MP4's H.264 track into
{:format :codec :encoding :width :height :source-width :source-height :frame-count :total-frames :duration-s :fps :frames}, where each frame is
{:index :timestamp-s :data} and :data is a byte[].
:encoding decides what those bytes are: "rgba" (default) is straight
RGBA8 rows, width*height*4, ready for from-pixels; anything encode
understands ("png", "jpeg", "webp", ...) is an ENCODED still, which is
far smaller and what a terminal or browser wants anyway.
Frames come out in DISPLAY order (B-frames are reordered), and RGBA is converted with the BT.601 limited-range matrix.
Options bound the work — the whole clip is decoded into memory otherwise:
:max-frames (stop after N), :stride (keep every Nth frame),
:max-dimension (downscale to fit), :filter, plus :quality for lossy
:encoding.
Decode an MP4's H.264 track into
`{:format :codec :encoding :width :height :source-width :source-height
:frame-count :total-frames :duration-s :fps :frames}`, where each frame is
`{:index :timestamp-s :data}` and `:data` is a byte[].
`:encoding` decides what those bytes are: `"rgba"` (default) is straight
RGBA8 rows, `width*height*4`, ready for `from-pixels`; anything `encode`
understands (`"png"`, `"jpeg"`, `"webp"`, ...) is an ENCODED still, which is
far smaller and what a terminal or browser wants anyway.
Frames come out in DISPLAY order (B-frames are reordered), and RGBA is
converted with the BT.601 limited-range matrix.
Options bound the work — the whole clip is decoded into memory otherwise:
`:max-frames` (stop after N), `:stride` (keep every Nth frame),
`:max-dimension` (downscale to fit), `:filter`, plus `:quality` for lossy
`:encoding`.(docx spec)A .docx document as bytes (hand-written WordprocessingML).
{:properties {…} :page {:width-pt 595 :height-pt 842 :landscape false :margin-pt 72} :blocks [{:type :heading :level 1 :text "Q3"} {:type :paragraph :style :Quote :align :center :bullet true :runs [{:text "revenue "} {:text "up" :bold true :color "#c00"}]} {:type :table :header true :borders true :rows [{:cells [{:text "a"} {:runs [{:text "b" :bold true}]}]}]} {:type :image :data <bytes> :width-pt 240 :alt "chart"} {:type :page-break}]}
:body is accepted as a synonym for :blocks. Font sizes are points,
spacing/indent *-pt keys are points; the module converts to half-points and
twips.
A `.docx` document as bytes (hand-written WordprocessingML).
`{:properties {…}
:page {:width-pt 595 :height-pt 842 :landscape false :margin-pt 72}
:blocks [{:type :heading :level 1 :text "Q3"}
{:type :paragraph :style :Quote :align :center :bullet true
:runs [{:text "revenue "} {:text "up" :bold true :color "#c00"}]}
{:type :table :header true :borders true
:rows [{:cells [{:text "a"} {:runs [{:text "b" :bold true}]}]}]}
{:type :image :data <bytes> :width-pt 240 :alt "chart"}
{:type :page-break}]}`
`:body` is accepted as a synonym for `:blocks`. Font sizes are points,
spacing/indent `*-pt` keys are points; the module converts to half-points and
twips.(draw! img ops)Run a batch of vector drawing ops on img, IN PLACE — this is the
Graphics2D replacement. One call per batch keeps FFI chatter out of drawing
loops; ops are applied in order and inherit the batch's style defaults.
Style keys (batch level or per op): :fill :stroke :stroke-width :dash
:cap (:butt :round :square) :join (:miter :round :bevel)
:size :family :weight :italic :letter-spacing :line-height.
Ops (:op):
:clear — fill everything with :fill
:clip — clip to :x :y :w :h (:op :reset-clip to drop it)
:line — :x1 :y1 :x2 :y2 (or :points)
:polyline / :polygon — :points [[x y] ...], :close
:rect — :x :y :w :h, optional :radius (rounded)
:circle — :cx :cy :r :ellipse — :cx :cy :rx :ry
:arc / :wedge — :cx :cy :r :r-inner :start :end (degrees)
:path — :d (SVG path data), :fill-rule
:text — :text :x :y :anchor (:start|:middle|:end) :baseline :rotate
:image — :image (an Img), :x :y :w :h :opacity
Returns img.
Run a batch of vector drawing ops on `img`, IN PLACE — this is the `Graphics2D` replacement. One call per batch keeps FFI chatter out of drawing loops; ops are applied in order and inherit the batch's style defaults. Style keys (batch level or per op): `:fill` `:stroke` `:stroke-width` `:dash` `:cap` (`:butt` `:round` `:square`) `:join` (`:miter` `:round` `:bevel`) `:size` `:family` `:weight` `:italic` `:letter-spacing` `:line-height`. Ops (`:op`): `:clear` — fill everything with `:fill` `:clip` — clip to `:x :y :w :h` (`:op :reset-clip` to drop it) `:line` — `:x1 :y1 :x2 :y2` (or `:points`) `:polyline` / `:polygon` — `:points [[x y] ...]`, `:close` `:rect` — `:x :y :w :h`, optional `:radius` (rounded) `:circle` — `:cx :cy :r` `:ellipse` — `:cx :cy :rx :ry` `:arc` / `:wedge` — `:cx :cy :r :r-inner :start :end` (degrees) `:path` — `:d` (SVG path data), `:fill-rule` `:text` — `:text :x :y :anchor (:start|:middle|:end) :baseline :rotate` `:image` — `:image` (an `Img`), `:x :y :w :h :opacity` Returns `img`.
(encode img)(encode img fmt)(encode img fmt quality)Encode to fmt (:png :jpeg :webp :gif :bmp :tiff :ico :qoi
:pnm :tga); returns a byte[]. quality (1-100, default 85) applies to the
lossy formats.
Encode to `fmt` (`:png` `:jpeg` `:webp` `:gif` `:bmp` `:tiff` `:ico` `:qoi` `:pnm` `:tga`); returns a byte[]. `quality` (1-100, default 85) applies to the lossy formats.
(encode-gif spec)Encode an animated GIF from {:width :height :loop-count :frames}, each frame
{:delay-ms :rgba} (straight RGBA8 byte[]). :loop-count -1/omitted = loop
forever. Returns a byte[].
Encode an animated GIF from `{:width :height :loop-count :frames}`, each frame
`{:delay-ms :rgba}` (straight RGBA8 byte[]). `:loop-count` -1/omitted = loop
forever. Returns a byte[].(flatten img)(flatten img bg)Composite onto an opaque background (default white).
Composite onto an opaque background (default white).
(flip img mode):horizontal/:h or :vertical/:v.
`:horizontal`/`:h` or `:vertical`/`:v`.
(fonts)Every font family known to the shared font database, as a vector of strings. The four Noto faces (sans regular/bold/italic + mono) are embedded, so text and SVG rendering never depend on system fonts.
Every font family known to the shared font database, as a vector of strings. The four Noto faces (sans regular/bold/italic + mono) are embedded, so text and SVG rendering never depend on system fonts.
(from-pixels rgba w h)An image from straight (non-premultiplied) RGBA8 rows — w*h*4 bytes.
An image from straight (non-premultiplied) RGBA8 rows — `w*h*4` bytes.
(get-pixel img x y)Packed 0xRRGGBBAA of one pixel, or nil when out of bounds.
Packed `0xRRGGBBAA` of one pixel, or nil when out of bounds.
(info img){:width :height :is-opaque :is-grayscale} — read off the handle's own RGBA8
pixels (:is-grayscale = every pixel has r = g = b), not off the file it came
from; probe answers that one.
`{:width :height :is-opaque :is-grayscale}` — read off the handle's own RGBA8
pixels (`:is-grayscale` = every pixel has r = g = b), not off the file it came
from; `probe` answers that one.(mp4? data)True when data looks like an ISO-BMFF/MP4 container (a ftyp box at byte
4). A sniff, not a validation — probe-video is the one that reads tracks.
True when `data` looks like an ISO-BMFF/MP4 container (a `ftyp` box at byte 4). A sniff, not a validation — `probe-video` is the one that reads tracks.
(office kind spec)Build an Office Open XML document from a declarative spec and return its
bytes. kind is :xlsx, :docx or :pptx. Pure Rust — rust_xlsxwriter for
workbooks, hand-written WordprocessingML/PresentationML for the rest; no POI,
no JVM document model, and it works in a native image.
See xlsx, docx and pptx for the per-kind spec.
Build an Office Open XML document from a declarative spec and return its bytes. `kind` is `:xlsx`, `:docx` or `:pptx`. Pure Rust — rust_xlsxwriter for workbooks, hand-written WordprocessingML/PresentationML for the rest; no POI, no JVM document model, and it works in a native image. See `xlsx`, `docx` and `pptx` for the per-kind spec.
(optimize data)(optimize data opts)Shrink an ENCODED image with the real, format-specific optimisers — oxipng (filter/heuristic search, colour-type and palette reduction, optional zopfli) for PNG, jpegtran-style lossless metadata stripping for JPEG, and a differencing re-encoder (gifsicle's algorithm) for GIF. Takes and returns encoded bytes.
LOSSLESS unless you ask otherwise: the pixels of a PNG or a JPEG come back
bit-identical unless :lossy, a :quality, a different :format, a
:max-width/:max-height or an unmeetable :max-bytes licenses a re-encode.
Options: :format (default: keep the input's) :level 0-6 effort
:lossy (palette-quantise PNG / re-encode JPEG) :colors :dither
:quality :progressive :strip (default true) :background
:max-width :max-height (downscale to fit, never enlarge)
:max-bytes (keep trying until it fits)
:force (return the optimised bytes even when they are bigger).
Returns a byte[] — the smaller of optimised and original unless :force.
Shrink an ENCODED image with the real, format-specific optimisers — oxipng (filter/heuristic search, colour-type and palette reduction, optional zopfli) for PNG, jpegtran-style lossless metadata stripping for JPEG, and a differencing re-encoder (gifsicle's algorithm) for GIF. Takes and returns encoded bytes. LOSSLESS unless you ask otherwise: the pixels of a PNG or a JPEG come back bit-identical unless `:lossy`, a `:quality`, a different `:format`, a `:max-width`/`:max-height` or an unmeetable `:max-bytes` licenses a re-encode. Options: `:format` (default: keep the input's) `:level` 0-6 effort `:lossy` (palette-quantise PNG / re-encode JPEG) `:colors` `:dither` `:quality` `:progressive` `:strip` (default true) `:background` `:max-width` `:max-height` (downscale to fit, never enlarge) `:max-bytes` (keep trying until it fits) `:force` (return the optimised bytes even when they are bigger). Returns a byte[] — the smaller of optimised and original unless `:force`.
(paste! dst src x y)(paste! dst src x y {:keys [blend] :or {blend true}})Draw src into dst at (x,y), IN PLACE. :blend false replaces the
destination pixels instead of alpha-compositing.
Draw `src` into `dst` at (x,y), IN PLACE. `:blend` false replaces the destination pixels instead of alpha-compositing.
(pixels img)The image's straight RGBA8 rows as a byte[].
The image's straight RGBA8 rows as a byte[].
(plot spec)Render a plot specification to an image. See com.blockether.imaging.plot for
the spec builders (line, bar, scatter, hist, pie, heatmap, …) and
the full vocabulary.
Render a plot specification to an image. See `com.blockether.imaging.plot` for the spec builders (`line`, `bar`, `scatter`, `hist`, `pie`, `heatmap`, …) and the full vocabulary.
(pptx spec)A .pptx presentation as bytes (hand-written PresentationML).
{:width 12192000 :height 6858000 ; EMU, 16:9 by default :properties {…} :layouts [{:name "Title Slide" :type :title :placeholders [{:type :ctrTitle :idx 0 :left :top :width :height :size :align}]}] :slides [{:layout 0 :background "#ffffff" :notes "…" :shapes [{:kind :textbox|:auto|:picture|:table|:chart|:connector :left :top :width :height :rotation :preset :roundRect :fill "#3366cc" :line {:color :width} :image {:data <bytes> :crop {:left :top :right :bottom}} :chart {:type :column :title "Revenue" :categories ["Q1" "Q2"] :legend {:position :bottom} :series [{:name "Sales" :values [3 7] :fill "4472C4"}]} :text-frame {:paragraphs [{:align :level :bullet :runs [{:text "hi" :bold true :size 1800}]}]}}]}]}
A chart :type is one of :column :bar :line :area :pie :doughnut :radar :scatter :bubble plus the *-stacked / *-percent-stacked variants. Each
chart becomes its own ppt/charts/chartN.xml part, so PowerPoint and
python-pptx see a real chart rather than a picture of one.
Lengths are EMU (914400 per inch), font sizes centipoints (1800 = 18 pt) —
exactly how python-pptx normalises its own Length/Pt values.
A `.pptx` presentation as bytes (hand-written PresentationML).
`{:width 12192000 :height 6858000 ; EMU, 16:9 by default
:properties {…}
:layouts [{:name "Title Slide" :type :title
:placeholders [{:type :ctrTitle :idx 0
:left :top :width :height :size :align}]}]
:slides [{:layout 0 :background "#ffffff" :notes "…"
:shapes [{:kind :textbox|:auto|:picture|:table|:chart|:connector
:left :top :width :height :rotation
:preset :roundRect :fill "#3366cc" :line {:color :width}
:image {:data <bytes> :crop {:left :top :right :bottom}}
:chart {:type :column :title "Revenue"
:categories ["Q1" "Q2"]
:legend {:position :bottom}
:series [{:name "Sales" :values [3 7]
:fill "4472C4"}]}
:text-frame {:paragraphs [{:align :level :bullet
:runs [{:text "hi" :bold true
:size 1800}]}]}}]}]}`
A chart `:type` is one of `:column :bar :line :area :pie :doughnut :radar
:scatter :bubble` plus the `*-stacked` / `*-percent-stacked` variants. Each
chart becomes its own `ppt/charts/chartN.xml` part, so PowerPoint and
`python-pptx` see a real chart rather than a picture of one.
Lengths are EMU (`914400` per inch), font sizes centipoints (`1800` = 18 pt) —
exactly how `python-pptx` normalises its own `Length`/`Pt` values.(probe data)Identify encoded bytes without a full decode:
{:format :width :height :bytes :is-animated :frames :color :channels :bits :is-grayscale :has-alpha}. The colour keys describe the SOURCE file — a
decoded Img is always RGBA8, so this is the only way to learn that a PNG
was 8-bit grayscale (:color "l8") or 16-bit. :color is one of "l8"
"la8" "rgb8" "rgba8" "l16" "la16" "rgb16" "rgba16"
"rgb32f" "rgba32f"; indexed/palette sources report what they expand
into. SVG documents report :format "svg" and rasterise as "rgba8".
Identify encoded bytes without a full decode:
`{:format :width :height :bytes :is-animated :frames :color :channels :bits
:is-grayscale :has-alpha}`. The colour keys describe the SOURCE file — a
decoded `Img` is always RGBA8, so this is the only way to learn that a PNG
was 8-bit grayscale (`:color "l8"`) or 16-bit. `:color` is one of `"l8"`
`"la8"` `"rgb8"` `"rgba8"` `"l16"` `"la16"` `"rgb16"` `"rgba16"`
`"rgb32f"` `"rgba32f"`; indexed/palette sources report what they expand
into. SVG documents report `:format "svg"` and rasterise as `"rgba8"`.(probe-video data)Identify an MP4 without decoding a frame:
{:format :codec :codec-kind :is-decodable :width :height :frames :duration-s :fps :timescale :has-audio}. :frames is the sample count and :width
/:height the container's declared track size — the bitstream's own size is
what decode-video reports. :is-decodable is false for a video track this
build cannot decode (HEVC, AV1, VP9); only H.264/AVC decodes.
Identify an MP4 without decoding a frame:
`{:format :codec :codec-kind :is-decodable :width :height :frames :duration-s
:fps :timescale :has-audio}`. `:frames` is the sample count and `:width`
/`:height` the container's declared track size — the bitstream's own size is
what `decode-video` reports. `:is-decodable` is false for a video track this
build cannot decode (HEVC, AV1, VP9); only H.264/AVC decodes.(quantize img)(quantize img {:keys [colors dither] :or {colors 256 dither false}})Median-cut colour quantisation with optional Floyd-Steinberg dithering — the
palette engine behind optimize's lossy PNG and GIF paths.
Returns {:width :height :palette :indices :transparent :rgba}: :palette a
vector of 0xRRGGBB ints, :indices one palette index per pixel (byte[]),
:transparent the palette slot used for fully transparent pixels (or nil),
:rgba the quantised image back as straight RGBA8 rows.
Median-cut colour quantisation with optional Floyd-Steinberg dithering — the
palette engine behind `optimize`'s lossy PNG and GIF paths.
Returns `{:width :height :palette :indices :transparent :rgba}`: `:palette` a
vector of `0xRRGGBB` ints, `:indices` one palette index per pixel (byte[]),
`:transparent` the palette slot used for fully transparent pixels (or nil),
`:rgba` the quantised image back as straight RGBA8 rows.(rank-filter img size rank)Per-channel rank filter over an odd size x size window: every channel
(alpha included) is sorted independently and the rank-th smallest kept.
:min / :median / :max are the named ranks — Pillow's MinFilter,
MedianFilter and MaxFilter; an integer picks the rank directly.
Per-channel rank filter over an odd `size` x `size` window: every channel (alpha included) is sorted independently and the `rank`-th smallest kept. `:min` / `:median` / `:max` are the named ranks — Pillow's `MinFilter`, `MedianFilter` and `MaxFilter`; an integer picks the rank directly.
(read-office data)(read-office data opts)Read an Office document back. Workbooks (.xlsx .xlsm .xlsb .xls
.ods) come back as {:kind :sheets [{:name :rows [[cell …] …]}]} with cells
already coerced to strings/numbers/booleans; .docx comes back as
{:kind :paragraphs [...]}.
A .pptx is read back as the mirror image of what pptx writes — the whole
DrawingML shape tree, not just its text:
{:kind :width :height :properties :slides [{:name :index :layout :notes :paragraphs :shapes [{:kind :name :left :top :width :height :rotation :fill {…} :line {…} :text-frame {…} :table {…} :chart {…} :image {…}}]}]}
so a deck can be read, edited and written again without losing its shapes.
Options: :sheet (name or index — read just that one), :header (treat the
first row as keys and return maps), :max-rows, and :with-images (base64
every embedded picture of a presentation; off by default).
Read an Office document back. Workbooks (`.xlsx` `.xlsm` `.xlsb` `.xls`
`.ods`) come back as `{:kind :sheets [{:name :rows [[cell …] …]}]}` with cells
already coerced to strings/numbers/booleans; `.docx` comes back as
`{:kind :paragraphs [...]}`.
A `.pptx` is read back as the mirror image of what `pptx` writes — the whole
DrawingML shape tree, not just its text:
`{:kind :width :height :properties
:slides [{:name :index :layout :notes :paragraphs
:shapes [{:kind :name :left :top :width :height :rotation
:fill {…} :line {…}
:text-frame {…} :table {…} :chart {…} :image {…}}]}]}`
so a deck can be read, edited and written again without losing its shapes.
Options: `:sheet` (name or index — read just that one), `:header` (treat the
first row as keys and return maps), `:max-rows`, and `:with-images` (base64
every embedded picture of a presentation; off by default).(register-font! font)Add a TTF/OTF/TTC (bytes/File/path/stream) to the font database for this process; returns the vector of families it provided.
Add a TTF/OTF/TTC (bytes/File/path/stream) to the font database for this process; returns the vector of families it provided.
(render-svg svg)(render-svg svg opts)Rasterise an SVG/SVGZ document (bytes, markup String, File, path, stream) with
resvg. Options: :width :height :scale :max-width :max-height
:background :dpi :font-family :font-size :base-url.
The canvas is REPAIRED by default: a document with a zero, negative or absent
size is sized the way a BROWSER would (see svg-canvas) instead of failing
resvg's size check. Pass :repair false for resvg's literal answer.
Rasterise an SVG/SVGZ document (bytes, markup String, File, path, stream) with resvg. Options: `:width` `:height` `:scale` `:max-width` `:max-height` `:background` `:dpi` `:font-family` `:font-size` `:base-url`. The canvas is REPAIRED by default: a document with a zero, negative or absent size is sized the way a BROWSER would (see `svg-canvas`) instead of failing resvg's size check. Pass `:repair false` for resvg's literal answer.
(resize img w h)(resize img w h filter)Resample to exactly w x h. filter: :nearest :triangle/:bilinear
:catmullrom/:bicubic :gaussian :lanczos3 (default), passed through.
Resample to exactly `w` x `h`. `filter`: `:nearest` `:triangle`/`:bilinear` `:catmullrom`/`:bicubic` `:gaussian` `:lanczos3` (default), passed through.
(rotate img degrees)(rotate img degrees {:keys [expand background]})Rotate counter-clockwise by degrees. :expand true grows the canvas to hold
the rotated image; :background fills behind it.
Rotate counter-clockwise by `degrees`. `:expand` true grows the canvas to hold the rotated image; `:background` fills behind it.
(save! img file)(save! img file {:keys [format quality] :as _opts})Encode img and write it to file; the format comes from the extension
unless :format says otherwise. Returns the file.
Encode `img` and write it to `file`; the format comes from the extension unless `:format` says otherwise. Returns the file.
(svg-canvas svg)(svg-canvas svg opts)How a BROWSER would size an SVG/SVGZ document (bytes, markup String, File,
path, stream): {:width :height :source :is-repaired :svg}.
resvg answers only for documents that declare a usable size: it refuses a zero
or negative one, and measures a size-less document from the ORIGIN, so a
figure drawn at x=50 gets 50px of dead margin. A browser falls back to the
viewBox, then to the ink actually painted. :source says which rule won —
"declared" (resvg's own resolved size), "view_box", "content" or
"default" — and :svg is the REPAIRED markup (a byte[], nil when the
document needed no repair), which render-svg/decode rasterise as drawn.
render-svg with :repair true does exactly this internally. Options:
:min-dimension (512) and :max-dimension (4096) bound an ink-framed canvas,
plus the parse options of render-svg.
How a BROWSER would size an SVG/SVGZ document (bytes, markup String, File,
path, stream): `{:width :height :source :is-repaired :svg}`.
resvg answers only for documents that declare a usable size: it refuses a zero
or negative one, and measures a size-less document from the ORIGIN, so a
figure drawn at x=50 gets 50px of dead margin. A browser falls back to the
`viewBox`, then to the ink actually painted. `:source` says which rule won —
`"declared"` (resvg's own resolved size), `"view_box"`, `"content"` or
`"default"` — and `:svg` is the REPAIRED markup (a byte[], nil when the
document needed no repair), which `render-svg`/`decode` rasterise as drawn.
`render-svg` with `:repair true` does exactly this internally. Options:
`:min-dimension` (512) and `:max-dimension` (4096) bound an ink-framed canvas,
plus the parse options of `render-svg`.(text-measure spec)Measure a text run without drawing it — same :text :size :family :weight :italic :letter-spacing :line-height keys as the :text draw op. Returns
{:width :height :x :y :lines}, the ink box relative to the first baseline.
Measure a text run without drawing it — same `:text :size :family :weight
:italic :letter-spacing :line-height` keys as the `:text` draw op. Returns
`{:width :height :x :y :lines}`, the ink box relative to the first baseline.(thumbnail img max-w max-h)Scale DOWN to fit inside max-w x max-h, preserving aspect ratio.
Scale DOWN to fit inside `max-w` x `max-h`, preserving aspect ratio.
(version)The cdylib's version string (crate version + backing engines).
The cdylib's version string (crate version + backing engines).
(video->gif data)(video->gif data opts)Transcode an MP4 straight to an animated GIF byte[] — the frames never cross
the FFI boundary, so this is the cheap way to make a clip watchable anywhere
a GIF is (a terminal, a chat wire that allows image/gif, a browser).
Options: the decode-video ones (:max-frames :stride :max-dimension
:filter) plus :fps (playback rate; default the source rate divided by
:stride) and :loop-count (-1/omitted = forever). GIF stores its delay in
CENTIseconds, so the frame rate is quantised to 1/100 s.
Transcode an MP4 straight to an animated GIF byte[] — the frames never cross the FFI boundary, so this is the cheap way to make a clip watchable anywhere a GIF is (a terminal, a chat wire that allows `image/gif`, a browser). Options: the `decode-video` ones (`:max-frames` `:stride` `:max-dimension` `:filter`) plus `:fps` (playback rate; default the source rate divided by `:stride`) and `:loop-count` (-1/omitted = forever). GIF stores its delay in CENTIseconds, so the frame rate is quantised to 1/100 s.
(xlsx spec)A .xlsx workbook as bytes, written by rust_xlsxwriter.
{:properties {:title :subject :author :manager :company :category :keywords :comments :status} :sheets [{:name "Sales" :columns [{:first 0 :last 3 :width 18 :format {…} :hidden false}] :rows [{:index 0 :height 22 :format {…}}] :freeze [row col] :autofilter [r1 c1 r2 c2] :merges [{:range [r1 c1 r2 c2] :value … :format {…}}] :images [{:row :col :data <bytes> :scale-x :scale-y}] :cells [{:row 0 :col 0 :value "Region" :format {:bold true}}]}]}
A cell's :type is optional — a string/number/boolean/nil is written as
itself; the explicit types are string number boolean formula
datetime date time url blank and rich (a run list of
[format? text]). A :format map (:bold :italic :underline :font :size :color :bg-color :align :valign :wrap :rotation :indent :border :num-format :locked …) is deduplicated into one workbook format automatically.
A `.xlsx` workbook as bytes, written by rust_xlsxwriter.
`{:properties {:title :subject :author :manager :company :category :keywords
:comments :status}
:sheets [{:name "Sales"
:columns [{:first 0 :last 3 :width 18 :format {…} :hidden false}]
:rows [{:index 0 :height 22 :format {…}}]
:freeze [row col] :autofilter [r1 c1 r2 c2]
:merges [{:range [r1 c1 r2 c2] :value … :format {…}}]
:images [{:row :col :data <bytes> :scale-x :scale-y}]
:cells [{:row 0 :col 0 :value "Region" :format {:bold true}}]}]}`
A cell's `:type` is optional — a string/number/boolean/nil is written as
itself; the explicit types are `string` `number` `boolean` `formula`
`datetime` `date` `time` `url` `blank` and `rich` (a run list of
`[format? text]`). A `:format` map (`:bold :italic :underline :font :size
:color :bg-color :align :valign :wrap :rotation :indent :border :num-format
:locked` …) is deduplicated into one workbook format automatically.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 |