Image API — `ImageHandle` · `Engine`
Diese Seite ist derzeit nur englisch — die JSDoc-Kommentare in der Engine sind englisch verfasst. Eine deutsche Auto-Generierung folgt, sobald die Methoden mit
@apidoc-de-Tags ausgestattet sind. — Zur Übersicht
All pixel-processing operations live on ImageHandle (the value you get back from Engine.loadImage() / Engine.createImage()). The Engine global is the entry point for loading, creating, and saving images, plus the canvas / path / animation / tool / timer factories.
Classes
ImageHandle
A handle to an image stored in the Rust image registry.
Most methods return this for fluent chaining:
const img = Engine.loadImage("input.png");
img.brightness(1.2).gaussianBlur(1.5).save("out.png");
Size-changing operations (crop, resize, pad, rotateExpand, warpPolar,
warpPerspective) update width and height automatically.
Accessors
height
Get Signature
get height(): number;
Current image height in pixels. Updated automatically by size-changing ops.
Returns
number
Set Signature
set height(v): void;
Parameters
| Parameter | Type |
|---|---|
v |
number |
Returns
void
id
Get Signature
get id(): number;
Internal handle id (read-only). Needed for cross-image ops like blend/applyMask.
Returns
number
width
Get Signature
get width(): number;
Current image width in pixels. Updated automatically by size-changing ops.
Returns
number
Set Signature
set width(v): void;
Parameters
| Parameter | Type |
|---|---|
v |
number |
Returns
void
Methods
_setHeightAndWidthUnsafe()
_setHeightAndWidthUnsafe(h, w): void;
Parameters
| Parameter | Type |
|---|---|
h |
number |
w |
number |
Returns
void
addNoise()
addNoise(opts?, region?): this;
Add synthetic sensor noise — the core degradation primitive for building model-training data (Real-ESRGAN / GFPGAN style: take a clean image, degrade it, train the network to invert it).
Unlike filmGrain (uniform mono grain only), this exposes the distribution shapes a degradation pipeline actually needs:
"gaussian"— normal noise, constant standard deviationsigma. The classic additive read-noise."uniform"— flat noise in±sigma(same asfilmGrain)."poisson"— shot noise whose strength scales with√pixel: bright regions get noisier, shadows stay clean. Modelled as the standard Gaussian approximation of Poisson statistics;sigmais the overall gain.
Parameters
| Parameter | Type | Description |
|---|---|---|
opts? |
{ color?: boolean; seed?: number; sigma?: number; type?: "gaussian" | "uniform" | "poisson"; } |
- |
opts.color? |
boolean |
false (default) shares one sample across RGB (luminance grain); true perturbs each channel independently (chroma noise). Alpha is never touched. |
opts.seed? |
number |
Integer ≥ 0 makes the noise reproducible: the same call on the same image gives the same result, bit for bit. Omit it for fresh noise on every run. Set it whenever the output goes into a test, a diff or a published example. |
opts.sigma? |
number |
Noise strength (≈ [0..1]). Default 0.05. |
opts.type? |
"gaussian" | "uniform" | "poisson" |
Distribution. Default "gaussian". |
region? |
Rect | RegionOpts |
Optional region to limit the effect to. |
Returns
this
this
Note
Without seed the result varies between runs.
Example
// Real-ESRGAN-style degradation step
img.gaussianBlur(1.5)
.resize(img.width / 2, img.height / 2)
.addNoise({ type: "poisson", sigma: 0.08, color: true });
adjustAlpha()
adjustAlpha(
value,
mode?,
region?): this;
Adjust the alpha (transparency) channel.
Parameters
| Parameter | Type | Description |
|---|---|---|
value |
number |
Target value; meaning depends on mode. |
mode |
AlphaMode |
AlphaMode.Set = set directly; Mul = multiply; Add = add (clamped). Default: Set. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
anisotropicBlur()
anisotropicBlur(sigmaX, sigmaY): this;
Anisotropic Gaussian blur — independent sigmas on the X and Y
axes. Useful for lens-style streak effects, fake shallow depth
of field, or just a "stretched lights" look. When sigmaX === sigmaY this matches gaussianBlur(sigmaX). Either sigma
≤ 0 disables that axis (so a horizontal-only or
vertical-only smear is just anisotropicBlur(8, 0) /
anisotropicBlur(0, 8)).
Parameters
| Parameter | Type | Description |
|---|---|---|
sigmaX |
number |
Horizontal Gaussian sigma in pixels. |
sigmaY |
number |
Vertical Gaussian sigma in pixels. |
Returns
this
this
Example
// Horizontal motion-streak hint — vertical detail intact.
img.anisotropicBlur(12, 1);
// Camera-shake-style stretch in both axes, more horizontal.
img.anisotropicBlur(8, 3);
anisotropicKuwahara()
anisotropicKuwahara(radius, region?): this;
Anisotropic Kuwahara filter (Kyprianidis 2009) — the
state-of-the-art painterly stylisation. Where kuwahara uses
four axis-aligned quadrants, this version drives the kernel
geometry from the local image structure: brush strokes
follow the dominant edge direction (hair runs along hair,
ridges along ridges) and stretch into long, thin shapes in
highly-directional regions. Result: noticeably closer to a
real painting than the classical version, especially around
faces, fabric and natural texture.
Cost is ~3-5× the classical Kuwahara at the same radius —
we additionally compute a structure tensor (smoothed
gradient outer product) before the per-pixel sector
accumulation. radius 6-12 is typical; larger looks more
"broad-stroke".
Parameters
| Parameter | Type | Description |
|---|---|---|
radius |
number |
Maximum kernel half-extent in pixels (≥ 1). |
region? |
Rect | RegionOpts |
- |
Returns
this
this
Example
img.anisotropicKuwahara(8); // painterly portrait
img.bilateralBlur(5, 0.25, 8)
.anisotropicKuwahara(10)
.saturation(1.10); // pre-smooth for cleaner strokes
applyMask()
applyMask(mask): this;
Apply a luminance mask — the mask image's greyscale value sets this image's alpha.
Parameters
| Parameter | Type | Description |
|---|---|---|
mask |
ImageHandle |
Mask image (must have the same dimensions). |
Returns
this
this
artisticBold()
artisticBold(size?): this;
Ink-drawing effect — black-on-white outline silhouette built
from canny(0.1, 0.2) → dilate(size) → invert. Kept as a
convenience alias.
Parameters
| Parameter | Type | Default value | Description |
|---|---|---|---|
size |
number |
3 |
Dilation radius. Larger = thicker ink lines. Default 3. |
Returns
this
this
Deprecated
Convenience alias. New code should compose the
three primitives explicitly so the intent is
visible:
img.canny(0.1, 0.2).dilate(size).invert().
Or for a softer / shaded sketch look, use
img.pencilSketch(strength).
autocrop()
autocrop(opts?): this;
Trim a uniform-colour or transparent border. Finds the smallest
rectangle containing every non-background pixel and crops to
it. Updates width and height. When the whole image is
background, returns this unchanged.
Default behaviour (no opts): trim transparent margins
(α ≤ 0.001).
Parameters
| Parameter | Type | Description |
|---|---|---|
opts? |
{ color?: Color; padding?: number; tolerance?: number; } |
- |
opts.color? |
Color |
If set, trim that solid colour instead of transparency (Pixel or hex string). α=0 pixels are also treated as background so mixed transparent + flat-colour borders work without a second pass. |
opts.padding? |
number |
Pixels of margin to keep around the detected box. Default 0. Clamped to image bounds. |
opts.tolerance? |
number |
How close to the background a pixel can be before it counts as content. In [0, 1]. Default 0.001 (alpha) / 0.05 (color). |
Returns
this
this
Example
// Drop transparent edges
Engine.loadImage("logo.png").autocrop().save("logo_tight.png");
// Drop a white frame around a scan
Engine.loadImage("scan.jpg")
.autocrop({ color: "#ffffff", tolerance: 0.04, padding: 8 })
.save("trimmed.jpg");
bilateralBlur()
bilateralBlur(
d,
sigmaColor,
sigmaSpace,
region?): this;
Bilateral blur — edge-preserving smoothing.
Parameters
| Parameter | Type | Description |
|---|---|---|
d |
number |
Diameter of pixel neighbourhood. |
sigmaColor |
number |
Color sigma (larger = smoother colors). |
sigmaSpace |
number |
Space sigma (larger = wider area). |
region? |
Rect | RegionOpts |
- |
Returns
this
this
blend()
blend(
other,
factor,
mode?): this;
Blend another image onto this one. Both images must have the same dimensions.
Parameters
| Parameter | Type | Description |
|---|---|---|
other |
ImageHandle |
The image to blend in. |
factor |
number |
Blend weight ∈ [0..1]: 0 = only self, 1 = only other. |
mode? |
BlendMode |
Blend mode (default: Normal = 0). |
Returns
this
this
blendAt()
blendAt(
other,
at,
factor,
mode?): this;
Blend another image onto this one at a specific pixel position.
The other image may be smaller — it is composited at at.
Parameters
| Parameter | Type | Description |
|---|---|---|
other |
ImageHandle |
The image to composite. |
at |
Px |
Pixel-space offset, e.g. px(100, 50) or px(0, 0). |
factor |
number |
Blend weight ∈ [0..1]. |
mode? |
BlendMode |
Blend mode (default: Normal = 0). |
Returns
this
this
boxBlur()
boxBlur(size, region?): this;
Box blur (average filter).
Parameters
| Parameter | Type | Description |
|---|---|---|
size |
number |
Kernel half-size in pixels. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
brightness()
brightness(factor, region?): this;
Adjust brightness.
Parameters
| Parameter | Type | Description |
|---|---|---|
factor |
number |
Multiplier: 1.0 = unchanged, > 1.0 = brighter, < 1.0 = darker. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
canny()
canny(
low,
high,
region?): this;
Canny edge detection (two-threshold).
Parameters
| Parameter | Type | Description |
|---|---|---|
low |
number |
Low hysteresis threshold ∈ [0..1]. |
high |
number |
High hysteresis threshold ∈ [0..1]. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
clone()
clone(): ImageHandle;
Create a deep copy of this handle. The clone is independent — operations on it do not affect the original.
Returns
A new ImageHandle with its own pixel data.
Throws
If the clone fails.
colorJitter()
colorJitter(
brightness,
contrast,
saturation,
hue,
region?): this;
Randomized color jitter for data augmentation. Each parameter applies random variation in the given range.
Parameters
| Parameter | Type | Description |
|---|---|---|
brightness |
number |
Brightness jitter magnitude. |
contrast |
number |
Contrast jitter magnitude. |
saturation |
number |
Saturation jitter magnitude. |
hue |
number |
Hue jitter magnitude (degrees). |
region? |
Rect | RegionOpts |
- |
Returns
this
this
Note
Non-deterministic — result varies between runs.
colorMatch()
colorMatch(reference): this;
Reinhard per-channel mean/std colour transfer — shifts this
image's tone to match reference. Per channel:
new = (old - mean_self) * std_ref / std_self + mean_ref
Alpha-weighted, so transparent borders (e.g. from
warpPerspective outside-source areas) don't skew the source
mean toward zero. Useful for blending AI-restored faces
(GFPGAN, CodeFormer) back into a photo's skin-tone
neighbourhood: pass the original aligned face crop before
warping the restored version back. Dimensions must match.
Parameters
| Parameter | Type |
|---|---|
reference |
ImageHandle |
Returns
this
Example
const aligned = orig.clone().warpPerspective(512, 512, fwd);
const restored = Engine.tool('gfpgan-restore').apply(aligned);
restored.colorMatch(aligned); // match orig face's tone
aligned.free();
colorTransfer()
colorTransfer(reference): this;
Reinhard color transfer — match this image's per-channel mean and standard deviation in log-LMS space to a reference image's. Useful for harmonising photos shot under different lighting, matching screenshot tones across OS themes, or warming/cooling a target to fit a hero shot.
Pixels with α = 0 are skipped (left untouched).
If you transfer the same reference onto many targets, prefer
Engine.colorStats(ref) once → target.colorTransfer(stats)
per target, so the reference stats aren't recomputed every time.
Parameters
| Parameter | Type | Description |
|---|---|---|
reference |
LmsStats | ImageHandle |
Either an ImageHandle (stats computed on the spot) or a precomputed LmsStats object from Engine.colorStats(). |
Returns
this
this
Example
const warm = Engine.loadImage("hero_warm.jpg");
Engine.loadImage("cold_screenshot.png")
.colorTransfer(warm)
.save("harmonised.png");
compare()
compare(reference, opts?): object;
Compare this image against a reference and get the numbers back.
differenceMap() shows where two images disagree; this says
how much. Sizes must match — a mismatch throws rather than
quietly returning something meaningless.
Returned values, all computed on the [0..1] range the engine works in:
mse— mean squared error. 0 means identical.psnr— peak signal-to-noise ratio in dB,10·log₁₀(1/mse). Higher is closer; identical images giveInfinity. Roughly: 50 dB is invisible, 40 dB is hard to spot, below 30 dB you can see it.mae— mean absolute error, less dominated by outliers.maxDiff— the single worst channel deviation.differingPixels/differingFraction— how many pixels exceedtolerancein any channel.
Parameters
| Parameter | Type | Description |
|---|---|---|
reference |
ImageHandle |
Image to compare against. Must be the same size. |
opts? |
{ alpha?: boolean; tolerance?: number; } |
- |
opts.alpha? |
boolean |
Count the alpha channel too. Default true: for a regression check you want it, since two images can share every colour and differ only in transparency. |
opts.tolerance? |
number |
Per-channel distance in [0..1] above which a pixel counts as differing. Default 0, so any difference at all counts. Only affects differingPixels / differingFraction. |
Returns
object
Metrics object. Does not modify the image.
| Name | Type |
|---|---|
differingFraction |
number |
differingPixels |
number |
mae |
number |
maxDiff |
number |
mse |
number |
psnr |
number |
Examples
// Regression check against a reference rendering
const shot = Engine.loadImage(INPUT);
const m = shot.compare(Engine.loadImage("reference.png"));
if (m.psnr < 40) {
throw new Error(`drifted: ${m.psnr.toFixed(2)} dB, ` +
`${m.differingPixels} pixels`);
}
// Pick the smallest quality that still looks right.
//
// The probe has to be written AND read back: `save()` encodes to
// a file but leaves the handle in memory untouched, so comparing
// against it would measure nothing at all.
const src = Engine.loadImage(INPUT);
for (const q of [90, 80, 70, 60, 50]) {
src.clone().save("/tmp/probe.webp", { format: "webp", quality: q });
const probe = Engine.loadImage("/tmp/probe.webp");
const psnr = src.compare(probe).psnr;
probe.free();
if (psnr < 42) { console.log(`stop at ${q}`); break; }
}
contrast()
contrast(factor, region?): this;
Adjust contrast.
Parameters
| Parameter | Type | Description |
|---|---|---|
factor |
number |
1.0 = unchanged, > 1.0 = higher contrast, 0.0 = flat grey. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
convolve()
convolve(
kernel,
kw?,
kh?): this;
Apply a custom 2-D convolution kernel.
The kernel is a flat row-major array applied to the RGB channels. Alpha is preserved. Output is clamped to [0..1].
If kw and kh are omitted the kernel is assumed to be square
(sqrt(kernel.length) × sqrt(kernel.length)).
Parameters
| Parameter | Type | Description |
|---|---|---|
kernel |
number[] |
Flat row-major kernel values. |
kw? |
number |
Kernel width (default: Math.sqrt(kernel.length)). |
kh? |
number |
Kernel height (default: same as kw). |
Returns
this
this
Examples
img.convolve([0, -1, 0, -1, 5, -1, 0, -1, 0]);
img.emboss();
img.convolve(new Array(9).fill(1/9), 9, 1);
crop()
crop(
x,
y,
w,
h): this;
Crop a rectangular region.
Updates width and height.
Parameters
| Parameter | Type | Description |
|---|---|---|
x |
number |
Left edge in pixels. |
y |
number |
Top edge in pixels. |
w |
number |
Crop width in pixels. |
h |
number |
Crop height in pixels. |
Returns
this
this
differenceMap()
differenceMap(reference): this;
Per-pixel L2 RGB difference between this image and reference —
replaces this image with a grayscale heatmap of the distance.
Output: R = G = B = min(1, sqrt(dr² + dg² + db²) / sqrt(3)),
A = 1.0 (the /√3 normalisation maps black-vs-white to 1.0).
Both images must have the same dimensions; mismatched sizes leave the image untouched.
Phase 1a of the Hertzmann painterly pipeline (the "where does the canvas disagree with the reference" target for stroke placement). Independently useful for:
- Before/after diff visualisations.
- Quality-metric heatmaps (where MSE concentrates).
- Motion-detection gates ("did anything change?").
Parameters
| Parameter | Type | Description |
|---|---|---|
reference |
ImageHandle |
Image to compare against. |
Returns
this
this
Example
const before = Engine.loadImage(INPUT);
const after = before.clone().bilateralBlur(9, 0.1, 8);
const diff = before.clone().differenceMap(after);
diff.save(OUTPUT); // bright = the blur changed something
dilate()
dilate(size, region?): this;
Morphological dilation — expands bright regions.
Parameters
| Parameter | Type | Description |
|---|---|---|
size |
number |
Structuring element radius in pixels. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
dither()
dither(region?): this;
Floyd–Steinberg error-diffusion dithering (reduces to 1-bit per channel).
Parameters
| Parameter | Type |
|---|---|
region? |
Rect | RegionOpts |
Returns
this
this
dominantColors()
dominantColors(count?): object[];
The most common colours, counted over a coarse 16-step grid.
Each entry carries the average of the real colours in its cell, not the cell centre — otherwise you would be shown colours the image does not contain. Mostly transparent pixels are skipped, so a cut-out subject is not described by the colour of the background it no longer has.
Parameters
| Parameter | Type | Default value | Description |
|---|---|---|---|
count |
number |
5 |
How many colours to return. Default 5. |
Returns
object[]
drawText()
drawText(
text,
x,
y,
opts?): this;
Draw a single line of text onto the image at pixel (x, y).
The y-coordinate is the text baseline (SVG default), not
the top of the type. Two open-source variable fonts are
embedded with the engine and always available:
"Inter"(also responds to"sans-serif")"JetBrains Mono"(also"monospace") Custom fonts can be loaded withEngine.loadFont(path).
Parameters
| Parameter | Type | Description |
|---|---|---|
text |
string |
The string to draw. < > & are auto-escaped. |
x |
number |
X coordinate in pixels. |
y |
number |
Y baseline coordinate in pixels. |
opts? |
| MlcFont | { anchor?: "start" | "middle" | "end"; color?: Color; font?: string; lineHeight?: number; maxWidth?: number; size?: number; textAlign?: TextAlign; } |
- |
Returns
this
this
dropShadow()
dropShadow(
dx?,
dy?,
blur?,
color?,
strength?): this;
Drop shadow — a coloured, blurred, offset silhouette behind
the image. Composes tint → blur → blendAt at offset → blendAt sharp on top in one call.
Note: the image stays the same size — pixels of the shadow
that fall outside the original frame are clipped. If you
need a wider canvas, pad() first then call dropShadow.
Parameters
| Parameter | Type | Default value | Description |
|---|---|---|---|
dx |
number |
5 |
Horizontal offset in pixels (positive → shadow to the right). Default 5. |
dy |
number |
5 |
Vertical offset in pixels (positive → down). Default 5. |
blur |
number |
5 |
Gaussian sigma of the shadow softness. Default 5. |
color |
Color |
'#000000' |
Shadow colour. Hex / Pixel / tuple. Default "#000000". |
strength |
number |
1.0 |
Shadow opacity, ∈ [0..1]. Default 1.0. |
Returns
this
this
Example
img.dropShadow(4, 4, 6, "#000000", 0.6);
img.pad(20, 20, 20, 20).dropShadow(8, 12, 10, "#000", 0.5);
emboss()
emboss(): this;
Emboss effect — classic relief kernel that makes the image look carved.
Applies the kernel [-2,-1,0, -1,1,1, 0,1,2]. Its weights sum to 1,
so the photo keeps its colours and gains a relief along directional
edges — it is not the grey "stone" look. For that, take the colour
out first: img.grayscale().emboss().
Returns
this
this
erode()
erode(size, region?): this;
Morphological erosion — shrinks bright regions.
Parameters
| Parameter | Type | Description |
|---|---|---|
size |
number |
Structuring element radius in pixels. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
fillRectangle()
fillRectangle(
x,
y,
w,
h,
color): this;
Fill a rectangular region with a single colour, in-place. The rect is clipped to the image bounds; a fully off-screen rect is a silent no-op.
Accepts the same Color shapes as Engine.createColoredImage:
Pixel, hex string, or [r,g,b,(a)] array (0..1 floats).
Parameters
| Parameter | Type |
|---|---|
x |
number |
y |
number |
w |
number |
h |
number |
color |
Color |
Returns
this
Example
img.fillRectangle(10, 10, 100, 80, [1, 0, 0, 1]); // red box
img.fillRectangle(0, 0, img.width, img.height, "#ffffff"); // whiteout
filmGrain()
filmGrain(amount, region?): this;
Film grain — adds random noise for analog look.
Parameters
| Parameter | Type | Description |
|---|---|---|
amount |
number |
Noise intensity ∈ [0..1]. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
Note
Non-deterministic — result varies between runs.
fisheye()
fisheye(
center,
radius,
amount): this;
Fisheye lens distortion.
Parameters
| Parameter | Type | Description |
|---|---|---|
center |
Px |
Center of the distortion in pixel space, e.g. px(img.width/2, img.height/2). |
radius |
number |
Effect radius in pixels. |
amount |
number |
Distortion strength (positive = barrel, negative = pincushion). |
Returns
this
this
flipH()
flipH(): this;
Flip horizontally (mirror left↔right).
Returns
this
this
flipV()
flipV(): this;
Flip vertically (mirror top↔bottom).
Returns
this
this
forEachPixel()
forEachPixel(fn): this;
Iterate every pixel, calling fn(pixel, x, y).
If fn returns a Pixel, that value is written back.
⚠ Very slow for large images — intended for algorithmic / creative use.
Parameters
| Parameter | Type | Description |
|---|---|---|
fn |
(pixel, x, y) => void | Pixel |
`(pixel: Pixel, x: number, y: number) => Pixel |
Returns
this
this
free()
free(): void;
Release the underlying Rust image buffer. After calling free(), do not use this handle.
Calling free() explicitly is the deterministic option (memory
released immediately). When omitted, the FinalizationRegistry in
handle-registry.ts releases the buffer on the next GC pass — fine
for one-shot scripts, but for tight loops call .free() (or
Engine.gc() once per iteration) to keep peak RSS bounded.
Returns
void
gaussianBlur()
gaussianBlur(sigma, region?): this;
Gaussian blur.
Parameters
| Parameter | Type | Description |
|---|---|---|
sigma |
number |
Standard deviation in pixels. Higher = more blur. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
getMeta()
getMeta(key): unknown;
Read a single top-level metadata key. Returns the parsed value
(any JSON type) or null if the key is unset.
Parameters
| Parameter | Type |
|---|---|
key |
string |
Returns
unknown
Example
img.getMeta("opticscript").width; // sub-access via JS chaining
img.getMeta("dataset"); // user-set free-form value
getPixel()
getPixel(at): Pixel;
Read a single pixel. Returns a Pixel with r/g/b/a in [0..1].
Parameters
| Parameter | Type | Description |
|---|---|---|
at |
Px |
Pixel position, e.g. px(100, 50). |
Returns
glow()
glow(
blur?,
color?,
strength?): this;
Outer glow — soft luminous halo BEHIND the silhouette, extending past the alpha edges. Builds a tinted, blurred, alpha-faded copy of this image and stacks the sharp original on top so the body keeps its original colours and only the region outside the silhouette is coloured.
Implemented as a JS composition of existing primitives:
tint → blur → adjustAlpha(strength, Mul) → blendAt halo behind → blendAt sharp on top. Best on inputs with alpha
edges (post-rmbg, isolated subjects, rendered SVG, text).
On a fully opaque source the sharp top layer covers the
halo entirely — the call is effectively a no-op.
Parameters
| Parameter | Type | Default value | Description |
|---|---|---|---|
blur |
number |
15 |
Gaussian sigma of the halo in pixels. Default 15. Larger = softer / wider halo. |
color |
Color |
'#ffffff' |
Halo tint colour. Hex string, Pixel, or [r,g,b,a] tuple. Default "#ffffff". |
strength |
number |
1.0 |
Halo opacity, ∈ [0..1]. Default 1.0. |
Returns
this
this
Example
img.glow(15, "#ffffff"); // classic white halo
img.glow(8, "#ff80ff", 0.6); // soft magenta border
grayscale()
grayscale(region?): this;
Convert to greyscale using luminance weights.
Parameters
| Parameter | Type |
|---|---|
region? |
Rect | RegionOpts |
Returns
this
this
histogram()
histogram(bins?): object;
Per-channel histogram with a chosen number of bins.
Deliberately coarse by default. 256 bins are meant for an eye looking at a curve; 16 or 32 are what a script — or a language model — can actually summarise into a sentence about exposure and contrast.
const h = img.histogram(16);
const dark = (h.r[0] + h.r[1]) / (img.width * img.height);
if (dark > 0.4) console.log("this image is mostly shadow");
Parameters
| Parameter | Type | Default value | Description |
|---|---|---|---|
bins |
number |
16 |
Number of buckets per channel, 1..256. Default 16. |
Returns
object
| Name | Type |
|---|---|
a |
number[] |
b |
number[] |
bins |
number |
g |
number[] |
r |
number[] |
hue()
hue(delta, region?): this;
Rotate hue by delta degrees.
Parameters
| Parameter | Type | Description |
|---|---|---|
delta |
number |
Degrees to shift hue, e.g. 180 inverts hue. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
invert()
invert(region?): this;
Invert all color channels (negative effect). Alpha is preserved.
Parameters
| Parameter | Type |
|---|---|
region? |
Rect | RegionOpts |
Returns
this
this
kaleidoscope()
kaleidoscope(
center,
segments,
rotation): this;
Kaleidoscope — mirror-slice symmetry.
Parameters
| Parameter | Type | Description |
|---|---|---|
center |
Px |
Center of symmetry in pixel space, e.g. px(img.width/2, img.height/2). |
segments |
number |
Number of mirror segments. |
rotation |
number |
Initial rotation offset in degrees. |
Returns
this
this
kuwahara()
kuwahara(radius, region?): this;
Classical Kuwahara filter — the modern "oil-painting" /
painterly effect. For each pixel the surrounding
(2·radius+1)² window is split into four overlapping
quadrants; the quadrant with the lowest luminance variance
wins and its mean RGB becomes the output. Result: flat
regions stay flat, edges snap, and the image gets that
characteristic "brush-zone" look without the cubist
colour-flattening that the older oilPaint (histogram-
based) introduces.
Recommended for new uses. radius 4–8 is a soft sketch,
10–20 a clear painting. Cost is O(W·H·radius²).
Parameters
| Parameter | Type | Description |
|---|---|---|
radius |
number |
Window half-extent in pixels (≥ 1). |
region? |
Rect | RegionOpts |
- |
Returns
this
this
Example
img.kuwahara(8); // soft painterly look
img.bilateralBlur(7, 0.3, 12)
.kuwahara(12)
.saturation(1.15); // pre-smooth then paint
mark()
mark(id, opts?): this;
Mark a photo as yours — an invisible watermark that survives what happens to photos on the web: resizing, cropping and JPEG re-encoding. Requires OpticScript Pro; reading marks with readMark is free.
The id is repeated in tiles across the whole image (a quarter of the short side each), with a sync pattern that lets readMark find scale and crop again, and error correction plus a checksum so it never returns a wrong id.
Measured on 30.09.2026 on 24 photos (512–1600 px), default strength, about 43 dB:
| after | read |
|---|---|
| JPEG q90 / q75 / q50 | 24/24 |
| scaled to 95 %, 84 %, long side 800 px | 24/24 |
| scaled to 50 % | 23/24 |
| border cropped 1–50 px | 23–24/24 |
| centre square, 1080 px (Instagram) | 24/24 |
| 70 % crop, then scaled to 70 % | 21/24 |
| 60 % crop | 20/24 |
| rotated 90° / 180°, mirrored (with resize and JPEG) | 24/24 |
| sharpen, median, blur 1–2 px | 23–24/24 |
addNoise() default (σ 0.05) · σ 0.10 |
20/24 · 11/24 |
strong denoise (nonLocalMeans(7, 3, 0.08)) |
11/24 |
Limits: a slight tilt (a few degrees) is not recognised — turns by 90° steps and mirroring are; the checked image should keep at least about 40 % of the short side and 300 px. It is evidence, not copy protection — someone who knows the method can weaken it. Mark last, after any editing.
Parameters
| Parameter | Type | Description |
|---|---|---|
id |
string |
16 hex digits (64 bit) or a text of 1–8 bytes. |
opts? |
{ strength?: number; } |
- |
opts.strength? |
number |
Default 0.5. Higher survives harsher treatment and is easier to see. |
Returns
this
this
Throws
Without a Pro licence, or when the short side is under 256 px.
Example
const photo = Engine.loadImage(INPUT);
photo.resize(2048, 1365); // edit first
photo.mark("a3f29c810badf00d"); // then mark
photo.save(OUTPUT, { quality: 90 });
medianBlur()
medianBlur(radius): this;
Median filter — replaces each pixel's R/G/B channels with the
median of the channel values in a (2·radius+1)² window.
The classic salt-and-pepper denoiser: a single rogue
bright/dark pixel is gone after one pass because the median
ignores outliers that the mean would smear into neighbours.
For Gaussian-style sensor noise prefer bilateralBlur or
nonLocalMeans — those preserve fine texture better.
radius 1-2 typical for impulse cleanup; larger values lose
detail quickly. Cost O(W·H·radius²·log(radius)). Alpha
preserved.
Parameters
| Parameter | Type | Description |
|---|---|---|
radius |
number |
Window half-extent in pixels (≥ 1). |
Returns
this
this
mergeMeta()
mergeMeta(key, partial): this;
Shallow-merge a partial object into an existing metadata key.
If the key was unset or held a non-object, partial becomes
the new value. Convenience for the common
setMeta(k, { ...getMeta(k), ...partial }) pattern.
Parameters
| Parameter | Type |
|---|---|
key |
string |
partial |
Record<string, unknown> |
Returns
this
this
Example
img.mergeMeta("exif", { iso: 400, aperture: 2.8 });
meta()
meta(): Record<string, unknown>;
Return the full metadata blob attached to this image as a fresh
JS object. Auto-populated entries live under the opticscript.*
namespace (fileSize, mime, format, width, height, sourcePath, …);
EXIF / DICOM / TIFF parsers add their own siblings; user-set
data lives wherever you put it via setMeta.
The returned object is a snapshot — mutating it does not
affect storage. Use setMeta(key, value) to write back.
Returns
Record<string, unknown>
Example
const img = Engine.loadImage("photo.jpg");
const m = img.meta();
console.log(m.opticscript.mime); // "image/jpeg"
console.log(m.opticscript.fileSize); // 845231
montageH()
montageH(...others): ImageHandle;
Stack this image and one or more others side-by-side horizontally. The result height equals the tallest image; shorter images are top-aligned.
Parameters
| Parameter | Type | Description |
|---|---|---|
...others |
ImageHandle[] |
One or more ImageHandles to place to the right. |
Returns
New ImageHandle containing all images concatenated horizontally.
montageV()
montageV(...others): ImageHandle;
Stack this image and one or more others vertically. The result width equals the widest image; narrower images are left-aligned.
Parameters
| Parameter | Type | Description |
|---|---|---|
...others |
ImageHandle[] |
One or more ImageHandles to place below. |
Returns
New ImageHandle containing all images concatenated vertically.
motionBlur()
motionBlur(angleDeg, length): this;
Motion blur — directional linear blur along angleDeg for
length pixels. Simulates camera or subject motion. Single
pass; cost scales with the pixel length of the trail.
angleDeg is measured counter-clockwise from the +x axis:
0 blurs horizontally, 90 vertically, 45 along the
diagonal.
Parameters
| Parameter | Type | Description |
|---|---|---|
angleDeg |
number |
Direction of motion in degrees. |
length |
number |
Trail length in pixels (0 = no-op). |
Returns
this
this
Example
// 30° camera shake, 24 px trail.
img.motionBlur(30, 24);
// Pure horizontal motion (race car).
img.motionBlur(0, 40);
nonLocalMeans()
nonLocalMeans(
searchRadius,
patchRadius,
h): this;
Non-Local Means denoiser — the gold-standard for natural-photo
Gaussian noise. For each pixel the filter searches a window of
nearby pixels and averages them, weighting each neighbour by
patch similarity (small patch around centre vs. small
patch around candidate). Similar patches occur naturally
throughout a clean image but noise differs sample-to-sample,
so the weighted average preserves real structure (edges,
texture) while cancelling the noise. Markedly cleaner output
than bilateralBlur for sensor noise.
Integral-image accelerated — cost is O(W·H·search²) regardless of patch size; ~2-5 s for a 1500×1000 image at default params (search=7 / patch=3 / h=0.08).
Parameters
| Parameter | Type | Description |
|---|---|---|
searchRadius |
number |
Outer search-window half-extent (typical 5-10). |
patchRadius |
number |
Patch half-extent for similarity (typical 2-3). |
h |
number |
Filtering parameter; larger = more aggressive smoothing (typical 0.05-0.15 for [0..1] data). |
Returns
this
this
Example
img.nonLocalMeans(7, 3, 0.08); // strong denoise
img.nonLocalMeans(5, 2, 0.05); // gentle, fast
oilPaint()
oilPaint(
radius,
levels,
region?): this;
Oil paint effect (legacy histogram-based) — quantizes to
dominant colors in local windows. Kept for backward
compatibility; new code should prefer kuwahara(radius)
which produces a softer, more painterly look without the
cubist colour-flattening this method introduces.
Parameters
| Parameter | Type | Description |
|---|---|---|
radius |
number |
Local window radius in pixels. |
levels |
number |
Number of intensity levels (e.g. 8). |
region? |
Rect | RegionOpts |
- |
Returns
this
this
Deprecated
Prefer kuwahara(radius) for new code.
orientationField()
orientationField(smoothSigma?): this;
Replace this image with an HSV-encoded visualisation of its local edge orientation, derived from the smoothed structure tensor on luminance:
- Hue = edge direction φ (mapped from [0, π] to a full colour wheel — same physical edge in either direction maps to the same hue).
- Saturation = coherence (
(λ₁ − λ₂) / (λ₁ + λ₂)) — high in clearly oriented regions, low in flat or noisy regions. - Value = 1.0 throughout, so flat regions read as bright grayscale and oriented regions burst into colour.
Phase 1b of the Hertzmann painterly pipeline. Same tensor math
as anisotropicKuwahara but exposed as a standalone op for
any flow-based effect. Visualisation is intentionally lossy —
a raw-vector variant will follow when stroke generation needs
precise vectors.
Parameters
| Parameter | Type | Default value | Description |
|---|---|---|---|
smoothSigma |
number |
2.0 |
Gaussian sigma for tensor smoothing (typical 1.0–4.0). Larger = blockier, more coherent fields. Default 2.0. |
Returns
this
this
Example
const flow = Engine.loadImage(INPUT).orientationField(2.0);
flow.save(OUTPUT); // colour wheel = local edge direction
pad()
pad(
top,
bottom,
left,
right,
color?,
border?): this;
Pad the image with a border.
Updates width and height.
Parameters
| Parameter | Type | Description |
|---|---|---|
top |
number |
Top padding in pixels. |
bottom |
number |
Bottom padding in pixels. |
left |
number |
Left padding in pixels. |
right |
number |
Right padding in pixels. |
color? |
PadColor |
Border color: a Pixel, a CSS hex string ("#rgb" / "#rgba" / "#rrggbb" / "#rrggbbaa"), a CSS colour name ("tomato", "transparent"), or a BorderMode number for non-constant modes (Reflect/Wrap/Replicate). Omit for transparent padding (default). An unreadable colour throws — it never falls back to a fill you did not ask for. |
border? |
BorderMode |
BorderMode override when color is a Pixel or string (default: Constant). |
Returns
this
this
padBottom()
padBottom(size, color?): this;
Pad bottom only. Shorthand for pad(0, size, 0, 0, color). Updates height.
Parameters
| Parameter | Type |
|---|---|
size |
number |
color? |
PadColor |
Returns
this
padLeft()
padLeft(size, color?): this;
Pad left side only. Shorthand for pad(0, 0, size, 0, color). Updates width.
Parameters
| Parameter | Type |
|---|---|
size |
number |
color? |
PadColor |
Returns
this
padRight()
padRight(size, color?): this;
Pad right side only. Shorthand for pad(0, 0, 0, size, color). Updates width.
Parameters
| Parameter | Type |
|---|---|
size |
number |
color? |
PadColor |
Returns
this
padTop()
padTop(size, color?): this;
Pad top only. Shorthand for pad(size, 0, 0, 0, color). Updates height.
Parameters
| Parameter | Type |
|---|---|
size |
number |
color? |
PadColor |
Returns
this
pencilSketch()
pencilSketch(strength): this;
Pencil-sketch effect — the classic "colour dodge" trick from
Photoshop / GIMP tutorials: convert to greyscale, invert,
blur, then divide the original grey by 1 - blurred_inverted.
Soft natural pencil strokes with the smooth tonal shading
graphite gives, no edge-detection harshness. Output is
greyscale; alpha preserved.
strength is the Gaussian sigma applied to the inverted
copy:
- 5–10 → tight, fine pencil hatching feel
- 15–30 → broader graphite shading
- < 1 → almost identical to plain greyscale
Parameters
| Parameter | Type | Description |
|---|---|---|
strength |
number |
Gaussian sigma for the inverted-blur step. |
Returns
this
this
Example
img.pencilSketch(20); // soft graphite
img.gaussianBlur(0.5)
.pencilSketch(15)
.brightness(1.05); // pre-soften then sketch
perceptualHash()
perceptualHash(): string;
64-bit perceptual hash (dHash) as 16 hex characters.
Returned as a string, not a number: 64 bits do not survive a
JavaScript number, and a silently rounded hash is worse than
none. Compare two of them with Engine.hashDistance().
Survives rescaling, moderate sharpening and a different JPEG quality. It does not survive fine repeating patterns — dither, halftone screens, tiny text — where downscaling to 9x8 aliases; see docs/sampling_and_rotation.md for why that class of image is hard.
Returns
string
pixelate()
pixelate(size, region?): this;
Pixelate — mosaic effect.
Parameters
| Parameter | Type | Description |
|---|---|---|
size |
number |
Block size in pixels. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
posterize()
posterize(levels, region?): this;
Posterize — reduce to N discrete color levels per channel.
Parameters
| Parameter | Type | Description |
|---|---|---|
levels |
number |
Number of levels (e.g. 4 = retro poster look). |
region? |
Rect | RegionOpts |
- |
Returns
this
this
premultiplyAlpha()
premultiplyAlpha(region?): this;
Pre-multiply RGB by alpha in-place. Rewrites (R, G, B, A) as
(R*A, G*A, B*A, A) so fully-transparent pixels carry RGB=0
and partially-transparent pixels are scaled correspondingly.
Use after rmbg/autocrop and before any geometry op that uses
bilinear interpolation (warpPerspective, stampAt, resize, …) to
stop background-colour bleed at silhouette edges. The original
RGB on transparent pixels (where rmbg only flipped α to 0)
would otherwise leak in at every soft edge.
Parameters
| Parameter | Type |
|---|---|
region? |
Rect | RegionOpts |
Returns
this
this
Example
Engine.loadImage(INPUT)
.rmbg()
.premultiplyAlpha()
.stampAt(bg, quad, 0.85);
protect()
protect(opts?): object;
Protect a photo as yours: sign an entry in your register and mark
the photo with its id — in one step. Requires OpticScript Pro
and a key set up once (Engine.provenance.init("Your Name"), or
mlcos-protect init --name "Your Name").
The entry holds your name, the time, the title, the photo's perceptual hash and size, and is signed with your key. The id in the mark is the fingerprint of that signed entry — so anyone with the photo and the entry can check, without any secret, that this id belongs to exactly this entry (see verify).
Not available on a shared server (the register belongs to a person, not to the server account).
Parameters
| Parameter | Type | Description |
|---|---|---|
opts? |
{ strength?: number; title?: string; } |
- |
opts.strength? |
number |
Mark strength, default 0.5 (see mark). |
opts.title? |
string |
Shown in the register; usually the file name. |
Returns
object
{ id, owner, fingerprint, title, created, psnr }
| Name | Type |
|---|---|
created |
string |
fingerprint |
string |
id |
string |
owner |
string |
psnr |
number |
title |
string |
Example
const photo = Engine.loadImage(INPUT);
const r = photo.protect({ title: "lake.jpg" });
photo.save(OUTPUT, { quality: 92 });
console.log(`registered as ${r.id}`);
readMark()
readMark(): object;
Look for a mark written by mark. Free in every edition.
found is only true when the sync pattern and the checksum agree
— an unmarked image returns found: false, never a made-up id.
sync is the strength of the sync peak (unmarked photos stay
below about 6; found needs 7 and a matching checksum). orientation says whether
the image was turned or mirrored since it was marked ("none",
"rotated 90 cw", "rotated 180", "rotated 90 ccw", "mirrored
horizontally", "mirrored vertically", "transposed",
"anti-transposed"). cell is the tile cell size found, in
pixels of this image — compared with the cell at marking time
(short side / 128) it shows how much the image was scaled.
Returns
object
{ found, id, text, sync, cell, orientation } — id as 16 hex
digits, text when the id was a short text.
| Name | Type |
|---|---|
cell |
number |
found |
boolean |
id |
string | null |
orientation |
string |
sync |
number |
text |
string | null |
Example
const m = Engine.loadImage(INPUT).readMark();
console.log(m.found ? `marked: ${m.text ?? m.id}` : "no mark found");
readWatermark()
readWatermark(): object;
Read a watermark back out.
confidence says how unanimous the blocks were: 1.0 means every
block agreed, 0.5 means the read was a coin flip. An unmarked
image still returns a payload — noise decoded as if it were
data — so always check confidence rather than trusting the
string. Above roughly 0.9 the mark is really there; below 0.75 it
is not.
Returns
object
{ payload, confidence }. Does not modify the image.
| Name | Type |
|---|---|
confidence |
number |
payload |
string |
Example
const { payload, confidence } = Engine.loadImage(INPUT).readWatermark();
if (confidence > 0.9) console.log(`came from build ${payload}`);
else console.log("no watermark found");
rectFromCenter()
rectFromCenter(
at,
w,
h): Rect;
Build a normalized rect centered at a pixel position.
Parameters
| Parameter | Type | Description |
|---|---|---|
at |
Px |
Center position in pixel space. |
w |
number |
Width in pixels. |
h |
number |
Height in pixels. |
Returns
[x, y, w, h] all ∈ [0..1]
rectFromPixels()
rectFromPixels(
at,
w,
h): Rect;
Build a normalized [x, y, w, h] rect from a pixel origin and pixel dimensions.
Parameters
| Parameter | Type | Description |
|---|---|---|
at |
Px |
Top-left corner in pixel space. |
w |
number |
Width in pixels. |
h |
number |
Height in pixels. |
Returns
[x, y, w, h] all ∈ [0..1]
rectFull()
rectFull(): Rect;
Return a normalized rect covering the full image: [0, 0, 1, 1].
Returns
removeWhiteBackground()
removeWhiteBackground(opts?): this;
Knock out the bright background of a sketch / lineart by
driving alpha from luminance: pixels brighter than threshold
become transparent, darker pixels keep their colour and alpha.
RGB is preserved — only alpha changes.
Use this for black-on-white sketches where U²-Net (rmbg tool) struggles. Deterministic, no model — just luminance maths.
Parameters
| Parameter | Type | Description |
|---|---|---|
opts? |
{ feather?: number; threshold?: number; } |
- |
opts.feather? |
number |
Width of the soft edge in luminance units. 0 = hard binary cut, 0.05 = ~13/255 ramp. Default 0.05. |
opts.threshold? |
number |
Luminance cut-off ∈ [0..1]. Default 0.95 (≈ 242/255). Anything brighter goes transparent. |
Returns
this
this
resize()
resize(
w,
h,
interp?): this;
Resize to exact dimensions.
Updates width and height.
Shrinking averages rather than point-samples: the filter widens with the reduction factor, so a pattern finer than the target grid turns into its average instead of moiré. Enlarging reduces to ordinary bilinear interpolation. Both run over premultiplied alpha, so a soft transparent edge keeps its colour.
Sampling is half-pixel-centred, the same convention PIL, OpenCV and ffmpeg use. Before 2.3.0 it was corner-aligned, which shifted the image by half a pixel and stretched it slightly — output from 2.3.0 on will not be byte-identical to older runs.
Parameters
| Parameter | Type | Description |
|---|---|---|
w |
number |
Target width in pixels. |
h |
number |
Target height in pixels. |
interp? |
Interp |
Interp.Nearest keeps hard pixel edges (no filtering — the right choice for pixel art and for nearest-neighbour upscaling). Anything else uses the filtered path described above; default is Interp.Bilinear. |
Returns
this
this
Example
// Thumbnail without moiré on fine detail
Engine.loadImage("screenshot.png").resize(320, 180).save("thumb.png");
// Pixel art, scaled up without smoothing
Engine.loadImage("sprite.png").resize(256, 256, Interp.Nearest).save("big.png");
rotate()
rotate(angle): this;
Rotate in-place. Canvas stays the same size — corners are clipped.
Use rotateExpand() to expand the canvas instead.
Parameters
| Parameter | Type | Description |
|---|---|---|
angle |
number |
Angle in degrees (counter-clockwise). |
Returns
this
this
rotateExpand()
rotateExpand(angle): this;
Rotate and expand the canvas so the full image fits without clipping.
Updates width and height.
Parameters
| Parameter | Type | Description |
|---|---|---|
angle |
number |
Rotation angle in degrees (counter-clockwise). |
Returns
this
this
saturation()
saturation(factor, region?): this;
Adjust color saturation.
Parameters
| Parameter | Type | Description |
|---|---|---|
factor |
number |
0.0 = greyscale, 1.0 = unchanged, > 1.0 = vivid. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
save()
save(path, opts?): this;
Save the image to a file on disk (or to a registered output slot).
Parameters
| Parameter | Type | Description |
|---|---|---|
path |
string |
Absolute / relative filesystem path (e.g. "out.png", "result.jpg") OR a //!OUTPUT: slot name in CLI mode OR mlcartifact://name.png to store it as an artifact (the new id goes to the log — use Engine.writeArtifact when the script needs it). On a server, file paths are refused. |
opts? |
{ format?: "png" | "jpeg" | "webp" | "avif"; lossy?: boolean; quality?: number; } |
Optional encoder hints. Quality is in [0..100] and applies to lossy encoders (JPEG, lossy WebP, AVIF). lossy flips WebP between lossless (default) and lossy. format ("png" |
opts.format? |
"png" | "jpeg" | "webp" | "avif" |
- |
opts.lossy? |
boolean |
- |
opts.quality? |
number |
- |
Returns
this
this for chaining.
Throws
If the write fails.
img.save("out.png"); // PNG, lossless
img.save("out.webp"); // WebP, lossless (default)
img.save("out.webp", { quality: 82 }); // WebP, lossy q=82
img.save("out.webp", { lossy: true, quality: 90 });
img.save("out.jpg", { quality: 95 }); // JPEG q=95
img.save(OUTPUT, { format: "webp", quality: 82 }); // playground: override UI select
saveTIFF()
saveTIFF(keyOrPath): this;
Save this image as a one-page TIFF. Distinct from save() because
TIFF goes through a separate code path (the tiff crate, not the
image crate's TIFF feature) so we can share the encoder with
AnimationHandle.saveTIFF() and offer multi-page TIFF later from
the same call shape.
RGBA8, uncompressed — preserves alpha exactly, no quality loss.
Use this when you need a single-page TIFF for a workflow that
expects them (scientific imaging, archival, certain printers).
For animation-style multi-page TIFFs see AnimationHandle.saveTIFF.
Parameters
| Parameter | Type | Description |
|---|---|---|
keyOrPath |
string |
Output slot or filesystem path. |
Returns
this
sepia()
sepia(region?): this;
Apply sepia tone (warm brownish tint).
Parameters
| Parameter | Type |
|---|---|
region? |
Rect | RegionOpts |
Returns
this
this
setMeta()
setMeta(key, value): this;
Set a top-level metadata key. Value can be any JSON-serialisable type: primitive, array, or nested object. Overwrites whatever was there before.
To update only some fields of a nested object, use mergeMeta
or do the JS spread yourself:
img.setMeta("exif", { ...img.getMeta("exif"), iso: 200 }).
On save, exif.* lands as EXIF in PNG, JPEG, WebP and AVIF. Every
other top-level key goes into an XMP packet — in PNG, JPEG and WebP
only: AVIF carries the EXIF part but drops free-form keys. Save
as PNG/JPEG/WebP if they must survive.
Parameters
| Parameter | Type |
|---|---|
key |
string |
value |
unknown |
Returns
this
this
Example
img.setMeta("dataset", "ct-batch-1");
img.setMeta("anon", { patientId: "OPAQUE-7F3A" });
setPixel()
setPixel(at, pixel): this;
Write a single pixel. Values are clamped to [0..1].
Parameters
| Parameter | Type | Description |
|---|---|---|
at |
Px |
Pixel position, e.g. px(100, 50). |
pixel |
Pixel |
A Pixel object. |
Returns
this
this for chaining.
shadowsHighlights()
shadowsHighlights(opts, region?): this;
Shadow / highlight recovery — tonal adjustment for photo editing.
Separates the image into dark and bright regions using luminance weighting and applies independent lift/suppression to each tonal range:
shadows > 0— lifts dark areas (opens under-exposed shadows).shadows < 0— crushes shadows (deepens blacks).highlights < 0— recovers blown highlights (pulls back bright areas).highlights > 0— brightens already-bright areas.
Both values are additive lifts ∈ [−1..1] weighted by (1 − L)² and L²
respectively, so the midtones are barely touched.
Parameters
| Parameter | Type | Description |
|---|---|---|
opts |
{ highlights?: number; shadows?: number; } |
- |
opts.highlights? |
number |
Highlight lift ∈ [−1..1]. Default 0 (no change). |
opts.shadows? |
number |
Shadow lift ∈ [−1..1]. Default 0 (no change). |
region? |
Rect | RegionOpts |
- |
Returns
this
this
Example
img.shadowsHighlights({ shadows: 0.25, highlights: -0.15 });
sharpen()
sharpen(amount, region?): this;
Unsharp-mask sharpening.
Parameters
| Parameter | Type | Description |
|---|---|---|
amount |
number |
Sharpening strength. 1.0 = moderate, > 2.0 = strong. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
sobel()
sobel(region?): this;
Sobel edge detection. Outputs edge magnitudes as greyscale.
Parameters
| Parameter | Type |
|---|---|
region? |
Rect | RegionOpts |
Returns
this
this
solarize()
solarize(threshold, region?): this;
Solarize — invert pixels above threshold (darkroom effect).
Parameters
| Parameter | Type | Description |
|---|---|---|
threshold |
number |
Inversion threshold ∈ [0..1]. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
stampAt()
stampAt(
src,
dstPoints,
factor?,
mode?,
interp?,
border?): this;
Perspective-place src onto this image at four destination corners.
Edge anti-aliasing is automatic. The warp samples src per
destination pixel; with the default WarpBorder.Transparent each
out-of-bounds bilinear tap contributes transparent, so a tilted
quad's silhouette interpolates opaque↔transparent and comes out
smooth. Pass border = WarpBorder.Clamp for hard edges (edge-
replicate); in that case pre-pad src with a 1-px transparent
border (src.pad(1,1,1,1)) if you still want a soft rim.
Parameters
| Parameter | Type | Description |
|---|---|---|
src |
ImageHandle |
The image to stamp. |
dstPoints |
[Px, Px, Px, Px] |
Four canvas-space corners [TL, TR, BR, BL] as Px values. |
factor? |
number |
Blend weight ∈ [0..1] (default 1.0). |
mode? |
BlendMode |
Blend mode (default BlendMode.Over). |
interp? |
Interp |
Interpolation mode (default Bilinear). |
border? |
WarpBorder |
Out-of-bounds mode (default WarpBorder.Transparent). |
Returns
this
this
stampGrid()
stampGrid(
content,
grid,
interp?,
factor?): this;
Forward complement of warpGrid: project content onto this image
through an R×C forward mesh.
Whereas warpGrid treats the nodes as the source UV at each uniform
destination grid point (inverse warp — useful for cylindrical/squeeze
effects on a single image), stampGrid treats them as the
destination position of each uniform source grid point — the
intuitive interpretation when you place a grid on a target surface in
the editor and want a second image to land inside it. Areas of the
base canvas not covered by any mesh cell keep their original pixels.
Internally, each of the (rows-1)×(cols-1) cells is rasterised as a
bilinear patch (inverse-bilinear per pixel inside the cell's bbox).
Adjacent cells share boundary nodes so they abut without gaps.
Parameters
| Parameter | Type | Default value | Description |
|---|---|---|---|
content |
ImageHandle |
undefined |
Image to stamp onto this canvas. |
grid |
{ cols: number; nodes: Record<string, number[]>; rows: number; } |
undefined |
WarpGrid object. nodes are normalised to this image's size (x = nodes[r][c*2] * this.width). |
grid.cols |
number |
undefined |
- |
grid.nodes |
Record<string, number[]> |
undefined |
- |
grid.rows |
number |
undefined |
- |
interp |
number |
1 |
0 = nearest-neighbour, 1 = bilinear (default). |
factor |
number |
1.0 |
Global opacity multiplier ∈ [0..1] (default 1). |
Returns
this
this
Example
// Project a poster onto a curved monitor in `scene`:
const scene = Engine.loadImage(REFERENCE);
const poster = Engine.loadImage(CONTENT);
scene.stampGrid(poster, { rows: ROWS, cols: COLS, nodes: NODES });
scene.save(OUTPUT);
stats()
stats(): object;
Summary numbers for this image, without comparing it to anything.
mean, min, max and stddev are per channel, in R G B A
order, on the [0..1] range.
sharpness is the variance of the Laplacian on luminance — the
standard blur measure from autofocus. It has no absolute scale:
use it to compare images, not as a threshold on its own. A flat
fill gives 0; fine detail gives a large number.
Returns
object
Stats object. Does not modify the image.
| Name | Type |
|---|---|
clippedHigh |
number[] |
clippedLow |
number[] |
max |
number[] |
mean |
number[] |
min |
number[] |
sharpness |
number |
stddev |
number[] |
transparentFraction |
number |
Example
const s = Engine.loadImage(INPUT).stats();
if (s.stddev[0] < 0.01) console.log("this screenshot is basically blank");
if (s.mean[3] < 1) console.log("has transparency");
swirl()
swirl(
center,
radius,
angle): this;
Swirl / twirl distortion — pixels rotate around a center point.
Parameters
| Parameter | Type | Description |
|---|---|---|
center |
Px |
Center of the swirl in pixel space, e.g. px(img.width/2, img.height/2). |
radius |
number |
Swirl radius in pixels. Pixels beyond this are unaffected. |
angle |
number |
Maximum rotation angle in degrees at the center. |
Returns
this
this
threshold()
threshold(value, region?): this;
Binary threshold — pixels above value become white, below become black.
Parameters
| Parameter | Type | Description |
|---|---|---|
value |
number |
Threshold ∈ [0..1]. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
tint()
tint(
color,
strength,
region?): this;
Tint the image with a color.
Parameters
| Parameter | Type | Description |
|---|---|---|
color |
Color |
- |
strength |
number |
Blend strength ∈ [0..1]. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
toSVG()
toSVG(optsOrKey?): string;
Trace the image to SVG using vtracer.
Parameters
| Parameter | Type | Description |
|---|---|---|
optsOrKey |
string | SVGOptions |
Either an SVGOptions config object, or a string output key (writes the SVG to that output slot and also returns it). All options are optional — omitted fields use the defaults from SVGOptions. |
Returns
string
unsetMeta()
unsetMeta(key): this;
Delete a metadata key, nested ones by dotted path
("exif.Artist"). No-op if the key was already absent.
Useful for stripping individual fields before export:
img.unsetMeta("exif.GPSInfo") drops the capture location —
every GPS field, however the reader stored them. (Before 2.11.1
dotted paths did nothing and the location stayed in the file.)
Parameters
| Parameter | Type |
|---|---|
key |
string |
Returns
this
this
verify()
verify(opts?): object;
Check a photo: read the mark and look its id up in your register — and in any exported registers you pass. Free in every edition.
verdict sums it up in a sentence. With an entry found,
signatureValid says whether the entry is untouched, and
hashDistance how far the photo is from the registered original
(0–10: the same picture; more: a crop or an edit).
Parameters
| Parameter | Type | Description |
|---|---|---|
opts? |
{ registers?: string[]; } |
- |
opts.registers? |
string[] |
Paths of exported registers (another photographer's), .json list or one entry per line. |
Returns
object
| Name | Type |
|---|---|
created? |
string |
fingerprint? |
string |
found |
boolean |
hashDistance? |
number |
id? |
string |
owner? |
string |
registered? |
boolean |
signatureValid? |
boolean |
sync |
number |
text? |
string | null |
title? |
string |
verdict |
string |
Example
const v = Engine.loadImage(INPUT).verify();
console.log(v.verdict);
if (v.registered) console.log(`${v.owner}, ${v.created}, key ${v.fingerprint}`);
vignette()
vignette(strength, region?): this;
Apply vignette (edge darkening).
Parameters
| Parameter | Type | Description |
|---|---|---|
strength |
number |
0.0 = none, 1.0 = strong dark border. |
region? |
Rect | RegionOpts |
- |
Returns
this
this
warpGrid()
warpGrid(grid, interp?): this;
Apply a Catmull-Rom mesh warp defined by a WarpGrid object.
The grid's sparse nodes map is expanded to a dense flat array before
being passed to the Rust engine. Rows absent from nodes are filled
with their identity (evenly-spaced) UV positions.
Parameters
| Parameter | Type | Default value | Description |
|---|---|---|---|
grid |
{ border?: WarpBorder; cols: number; nodes: Record<string, number[]>; rows: number; } |
undefined |
WarpGrid object (e.g. the WARPGRID global injected from the @warpgrid script block). |
grid.border? |
WarpBorder |
undefined |
- |
grid.cols |
number |
undefined |
- |
grid.nodes |
Record<string, number[]> |
undefined |
- |
grid.rows |
number |
undefined |
- |
interp |
number |
1 |
0 = nearest-neighbour, 1 = bilinear (default). |
Returns
this
this
warpPerspective()
warpPerspective(
dw,
dh,
matrix,
interp?,
border?): this;
Perspective (homography) warp.
Updates width and height.
The transform is src→dst (forward): for each source pixel it says where that pixel lands in the destination grid.
matrix accepts either:
- a Mat3 instance — the recommended path. Build one with
Matrix.Mat3.estimateSimilarity/Affine/Homography(...), or any gl-matrix construction. It is column-major; this wrapper transposes it to the engine's row-major layout for you. - a raw 9-element
number[]— treated as row-major[a,b,c, d,e,f, g,h,i]and passed straight through.
Out-of-bounds edges are anti-aliased by default: WarpBorder.Transparent
lets each bilinear tap that lands outside the source contribute
transparent, so a tilted quad's silhouette interpolates
opaque↔transparent instead of staircasing. Pass WarpBorder.Clamp
to replicate the edge texel instead (hard edges).
Parameters
| Parameter | Type | Description |
|---|---|---|
dw |
number |
Output width in pixels. |
dh |
number |
Output height in pixels. |
matrix |
number[] | Mat3 |
Mat3 or row-major number[9], src→dst forward. |
interp? |
Interp |
Interpolation method (default: Bilinear = 1). |
border? |
WarpBorder |
Out-of-bounds mode (default: WarpBorder.Transparent). |
Returns
this
this
warpPolar()
warpPolar(innerR?, outSize?): this;
Polar coordinate warp — maps the image onto a ring/donut shape.
The image's X axis maps to angle (0°→360°) and Y axis maps to radius.
Creates a "film roll" or "planet" effect.
Updates width and height.
Parameters
| Parameter | Type | Description |
|---|---|---|
innerR? |
number |
Inner ring radius as fraction of outer radius ∈ [0..1]. Default: 0.3. |
outSize? |
number |
Output image side length in pixels. 0 = auto (max of input dims). |
Returns
this
this
watermark()
watermark(payload, opts?): this;
Embed an invisible watermark into the pixels.
Unlike EXIF or a C2PA seal, this survives what platforms do to images: it lives in the pixel data, so stripping metadata does not remove it. Useful for tracing where a screenshot came from — a build number, a tenant id, a licence key.
The payload is carried by the ratio of two mid-frequency DCT coefficients per 8×8 block, repeated across the whole image, and read back by majority vote. Capacity is 8 characters — this holds an identifier, not a message. Longer input is truncated.
Measured on 09.09.2026 against a real photograph at 1280×720. Strength trades visibility against survival, and the default sits in the middle:
strength |
invisibility | survives JPEG down to |
|---|---|---|
| 0.3 | 46.5 dB | q=90 |
| 0.5 (default) | 45.3 dB | q=70 |
| 0.8 | 43.4 dB | q=50 |
| 1.0 | 42.3 dB | q=40 |
Most platforms re-encode somewhere around q=75–85, which the default clears.
It does not survive scaling or cropping. Resizing shifts the 8×8 grid and the payload is lost. Watermark last, after any resize. For photos published on the web, use mark — it survives resizing, cropping and re-encoding.
It is also not a tamper seal: a watermark cannot prove an image is unmodified, and anyone who knows the scheme can write one.
Parameters
| Parameter | Type | Description |
|---|---|---|
payload |
string |
Up to 8 characters. Longer is truncated. |
opts? |
{ strength?: number; } |
- |
opts.strength? |
number |
0..1, default 0.5. Higher survives harsher compression and is easier to see. |
Returns
this
this
Throws
When the image is smaller than 64 blocks (roughly 64×64).
Example
// Stamp every screenshot with the build it came from
const shot = Engine.loadImage(INPUT);
shot.resize(1280, 720); // resize FIRST
shot.watermark("b1421"); // then mark
shot.save(OUTPUT);
wave()
wave(
amplitudeX,
amplitudeY,
freqX,
freqY): this;
Sinusoidal wave distortion.
Parameters
| Parameter | Type | Description |
|---|---|---|
amplitudeX |
number |
Horizontal wave amplitude in pixels. |
amplitudeY |
number |
Vertical wave amplitude in pixels. |
freqX |
number |
Horizontal wave frequency (cycles per image width). |
freqY |
number |
Vertical wave frequency (cycles per image height). |
Returns
this
this