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`.(document->markdown data)(document->markdown data opts)The Markdown of any document, as a String — read-document when all you
want is the text. Same options.
The Markdown of any document, as a String — `read-document` when all you want is the text. Same options.
(document-format data)(document-format data opts)Identify a document WITHOUT converting it: {:format :source :formats}.
:format is one of :doc :docx :odt :pdf :ppt :pptx :rtf :epub :excel :ods :odp :csv, or nil when nothing recognised the input. :source says which
evidence won — "content" (the container's own signature), "name" (the
extension of :name) or "format" (you said so).
The signature is asked FIRST, because a signature cannot lie and an
extension routinely does. CSV carries no signature at all, so a :name is
its only route.
Options: :name (file name or extension), :format (force it).
Identify a document WITHOUT converting it: `{:format :source :formats}`.
`:format` is one of `:doc :docx :odt :pdf :ppt :pptx :rtf :epub :excel :ods
:odp :csv`, or nil when nothing recognised the input. `:source` says which
evidence won — `"content"` (the container's own signature), `"name"` (the
extension of `:name`) or `"format"` (you said so).
The signature is asked FIRST, because a signature cannot lie and an
extension routinely does. CSV carries no signature at all, so a `:name` is
its only route.
Options: `:name` (file name or extension), `:format` (force it).(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-document data)(read-document data opts)Convert ANY document a human sent into GitHub-Flavored Markdown:
{:format :source :chars :markdown :assets}, and with :blocks the
STRUCTURE behind that Markdown too.
Word (.doc .docx .odt .rtf), PDF, EPUB, presentations (.ppt
.pptx .odp) and spreadsheets (.xls .xlsx .xlsm .xlsb .ods
.csv) all come back as one Markdown string, with their headings, lists,
tables and links intact — which is what an LLM can actually read. Nothing
here shells out to a headless office suite: anydoc parses every format in
pure Rust inside the cdylib.
Options: :format (force the parser, e.g. :csv), :name (the file name,
the only route for signature-less CSV), :assets (also return every
embedded binary as {:id :media-type :origin-part :size :bytes} with
:bytes a byte[]), :max-assets, :blocks.
:blocks true additionally returns:
:text — the document as PLAIN text. Same content, none of Markdown's
punctuation, so a matcher never trips over **March** or a |, and an
offset means a position in the DOCUMENT rather than in a rendering of it.:blocks — one entry per heading, paragraph, list item, table row, code
block and note, in reading order: {:index :kind :text :start :end :line :path} where :start/:end are CHARACTER offsets into :text, :line
is 1-based, and :path is the heading breadcrumb the block sits under.
A heading adds :level/:anchor, a list item :marker/:list-depth/
:checked, a code block :lang, a note :note/:note-kind, a quoted
block :quote-depth, and a table row :row/:is-header/:cells, each
cell {:column :name :text :start :end} — so a hit can be reported as
row 4, column Revenue.:pages — the page count for a PDF, nil for everything else.A PDF has no document model, so it never reports assets; its blocks come
from per-page extraction instead, and every one of them carries :page.
Convert ANY document a human sent into GitHub-Flavored Markdown:
`{:format :source :chars :markdown :assets}`, and with `:blocks` the
STRUCTURE behind that Markdown too.
Word (`.doc` `.docx` `.odt` `.rtf`), PDF, EPUB, presentations (`.ppt`
`.pptx` `.odp`) and spreadsheets (`.xls` `.xlsx` `.xlsm` `.xlsb` `.ods`
`.csv`) all come back as one Markdown string, with their headings, lists,
tables and links intact — which is what an LLM can actually read. Nothing
here shells out to a headless office suite: `anydoc` parses every format in
pure Rust inside the cdylib.
Options: `:format` (force the parser, e.g. `:csv`), `:name` (the file name,
the only route for signature-less CSV), `:assets` (also return every
embedded binary as `{:id :media-type :origin-part :size :bytes}` with
`:bytes` a `byte[]`), `:max-assets`, `:blocks`.
`:blocks true` additionally returns:
- `:text` — the document as PLAIN text. Same content, none of Markdown's
punctuation, so a matcher never trips over `**March**` or a `|`, and an
offset means a position in the DOCUMENT rather than in a rendering of it.
- `:blocks` — one entry per heading, paragraph, list item, table row, code
block and note, in reading order: `{:index :kind :text :start :end :line
:path}` where `:start`/`:end` are CHARACTER offsets into `:text`, `:line`
is 1-based, and `:path` is the heading breadcrumb the block sits under.
A heading adds `:level`/`:anchor`, a list item `:marker`/`:list-depth`/
`:checked`, a code block `:lang`, a note `:note`/`:note-kind`, a quoted
block `:quote-depth`, and a table row `:row`/`:is-header`/`:cells`, each
cell `{:column :name :text :start :end}` — so a hit can be reported as
row 4, column Revenue.
- `:pages` — the page count for a PDF, nil for everything else.
A PDF has no document model, so it never reports assets; its blocks come
from per-page extraction instead, and every one of them carries `:page`.(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 |