Format-split Rust WebAssembly image conversion engine for browser and Node.js runtimes.
Version: v1.1.0 Runtime: Browser WebAssembly and Node.js License: AGPL-3.0-only
This is the source repository for the WASM conversion engine, intended for maintainers who build, audit, or modify the Rust pipeline and the generated JavaScript package.
The published npm package uses a separate README generated from
js-wrapper/README.npm.md. Keep this one focused on repository architecture, build
behaviour, and maintainer workflows.
- Format-split WASM modules: JPEG, PNG, WebP, and AVIF output modules are generated
independently under
pkg/formats/<format>/. - Lazy loading by output format: the wrapper loads only the module for the requested output. Note that each module still bundles the full decoder set, so the split saves encoder weight, not decoder weight.
- ESM and CommonJS entries:
pkg/index.jsandpkg/index.cjs, with shared logic inpkg/shared.js. - In-memory runtime model: bytes in, bytes out. No filesystem API anywhere.
- Supported input:
jpg,jpeg,png,webp,heic,heif,avif. Casing and leading dots are normalized. - Supported output: JPEG, PNG, WebP, AVIF.
- Pure Rust decoding: HEIC, HEIF, and AVIF input decode through the
heiccrate with itsav1feature. No C libraries, no JavaScript codec fallbacks. - HEIC output is unavailable by design and returns an explicit error.
- Encode intents:
Balanced,Archive, andSocialselect the quality, chroma, and metadata policy. - Privacy:
Socialremoves GPS and device-identity fields from EXIF, XMP, and IPTC. - Metadata extraction and preservation: EXIF, XMP, IPTC, and ICC where the container supports them.
- Crop and resize: percent crop and aspect-ratio-aware resize.
- Input hardening: file size, pixel budget, extension, and magic-byte validation run before decode on every export that consumes caller bytes.
- Staged progress callback and a per-conversion timeout guard.
Prerequisites:
- The Rust toolchain pinned in
rust-toolchain.toml(rustup reads it automatically). - Node.js 18 or newer.
Everything else — the wasm32-unknown-unknown target and the matching
wasm-bindgen-cli — is installed by the build script. The CLI version is read from
Cargo.lock, so it can never drift from the wasm-bindgen crate.
The build is a Node script, so Linux, macOS, and Windows run identical steps. Use the
shim for your platform — ./build_wasm.sh or build_wasm.bat — or call
node scripts/build.mjs directly. All three take the same arguments.
First build, give it your details:
./build_wasm.sh --author "Your Name" --scope yourscopeAfter that they are remembered, so every later build is just:
./build_wasm.shBuild one module by name:
./build_wasm.sh jpegFull option list:
./build_wasm.sh --help| Option | Meaning |
|---|---|
-a, --author <name> |
Package author. Required once, then remembered. |
-s, --scope <scope> |
npm scope without the @. Omit for an unscoped name. |
--no-scope |
Force an unscoped name, ignoring a remembered scope. |
--threads |
Enable the wasm-threads feature. |
--forget |
Discard the remembered author and scope. |
-h, --help |
Show usage. |
Author and scope resolve in this order, first match wins:
- Command line.
AUTHOR_NAME/NPM_SCOPEenvironment variables..buildrc.json, written the first time you pass them on the command line or answer the prompt. It is gitignored and local to your machine.- An interactive prompt.
The prompt only appears when a terminal is attached. An unattended build with nothing configured fails with a clear message instead of hanging, so CI stays safe:
AUTHOR_NAME="CI Bot" NPM_SCOPE=yourscope ./build_wasm.shValues passed on the command line are remembered; values from the environment are not, so a CI run leaves nothing behind.
--threads (or WASM_THREADS=1) enables the optional wasm-threads feature for the ESM
builds and rebuilds CommonJS as a non-threaded fallback. Threaded browser use requires
SharedArrayBuffer, COOP, and COEP from the host application.
The script outputs:
pkg/
index.js
index.cjs
index.d.ts
shared.js
package.json
README.md
PROJECT_VERSION
LICENSE
formats/
jpeg/
esm/index.js
esm/index.d.ts
esm/index_bg.wasm
cjs/index.cjs
cjs/index.d.ts
cjs/index_bg.wasm
...
One command runs every source check:
node scripts/verify.mjsThat covers formatting, clippy with -D warnings across all targets and features, the
full test suite, a per-feature check for each output format, and a wasm32-unknown-unknown
release check.
After building the package, smoke-test the real artifact:
node test-npm.jsIt runs the full suite against both the ESM and CommonJS entries.
import { convertImage, extractMetadata, getImageDimensions, init } from './pkg/index.js';
await init({ preload: ['Jpeg'] });
const input = new Uint8Array(await file.arrayBuffer());
const dimensions = await getImageDimensions(input, '.PNG', 'Jpeg');
const output = await convertImage(input, 'PNG', {
format: 'Jpeg',
quality: 82,
resize: true,
targetWidth: 1600,
resizeLockAspectRatio: true,
keepMetadata: true,
intent: 'Social',
});
const metadata = await extractMetadata(output, 'jpg', 'Jpeg');
const blob = new Blob([output], { type: 'image/jpeg' });getProjectVersion() returns the value embedded from PROJECT_VERSION, currently
v1.1.0.
Browser callers own filenames, Blob URLs, downloads, uploads, and persistence. This package does not receive or resolve output destinations.
type OutputFormat = 'Jpeg' | 'Png' | 'WebP' | 'Avif';
type WasmSource = string | URL | Response | ArrayBuffer | ArrayBufferView | WebAssembly.Module;
interface InitOptions {
wasmSources?: Partial<Record<Lowercase<OutputFormat>, WasmSource>>;
preload?: OutputFormat[];
}wasmSources overrides the default module URL per format. preload eagerly initializes
selected format modules.
Returns encoded output bytes.
type EncodeIntent = 'Balanced' | 'Archive' | 'Social';
interface ConvertOptions {
format: OutputFormat;
quality?: number;
pngCompressed?: boolean;
lossless?: boolean;
resize?: boolean;
targetWidth?: number;
targetHeight?: number;
resizeLockAspectRatio?: boolean;
crop?: boolean;
cropTop?: number;
cropBottom?: number;
cropLeft?: number;
cropRight?: number;
keepMetadata?: boolean;
intent?: EncodeIntent;
}Every field except format is optional and falls back to an engine default.
Adds the final dimensions.
interface ConversionResult {
bytes: Uint8Array;
width: number;
height: number;
}onProgress receives values from 0 to 1. Progress is stage-based, not codec-internal.
getImageDimensions(fileBytes, ext, format?) / getProjectVersion(format?) / extractMetadata(fileBytes, ext, format?)
interface ImageDimensions { width: number; height: number }
interface ImageMetadata {
exif?: number[] | null;
xmp?: number[] | null;
iptc?: number[] | null;
icc?: number[] | null;
}| Intent | Quality | JPEG chroma | AVIF speed | WebP | PNG | Metadata |
|---|---|---|---|---|---|---|
Balanced |
as given | 4:4:4 at >= 85 | 4 | lossy tuning | as asked | untouched |
Archive |
as given | always 4:4:4 | 2 | near-lossless at >= 95 | always high | untouched |
Social |
capped at 90 | always subsampled | 6 | lossy tuning | as asked | private fields removed |
Social removes GPS coordinates and telemetry, camera and lens serial numbers, the
camera owner's name, the image unique ID, and IPTC location datasets, across EXIF, XMP,
and IPTC. Authorship, copyright, and exposure data are preserved. See
src/convert/metadata/privacy.rs for the exact field registry.
Extraction and preservation are separate. extractMetadata(...) reports what is in the
source bytes; keepMetadata controls what is written to the output.
| Output | EXIF | XMP | IPTC | ICC |
|---|---|---|---|---|
| JPEG | Yes | Yes | Yes | Yes |
| PNG | Yes | Yes | Yes | Yes |
| WebP | Yes | Yes | No | Yes |
| AVIF | Yes | Yes | No | No |
- JPEG writes EXIF and XMP through APP1, IPTC through Photoshop APP13, ICC through APP2.
- PNG writes EXIF, XMP, IPTC-derived text, and ICC through PNG chunks, plus selected
fields projected into
tEXtfor OS visibility. - WebP writes EXIF, XMP, and ICCP RIFF chunks. IPTC has no native WebP chunk here.
- AVIF preserves EXIF and XMP. ICC is extractable from source
colrboxes but this encoder path does not embed it into AVIF output. - ICC may be carried independently of
keepMetadatato preserve colour fidelity; EXIF/XMP/IPTC preservation requireskeepMetadata: true.
| Limit | Value |
|---|---|
| Max file size | 256 MB |
| Max dimension per side | 16384 |
| Max pixels per frame | 8192 × 8192 |
| Conversion timeout | 300 s |
The pixel budget is the binding limit. It exists so an oversized image returns a
catchable error instead of exhausting the 32-bit WebAssembly heap and aborting the
module. See docs/CODE_ARCHITECTURE.md for the reasoning.
src/
lib.rs WASM exports and the shared input gate
state.rs Conversion options, output format, encode intent
error.rs Structured conversion errors
constants.rs Validation, metadata, and limit constants
convert/
mod.rs In-memory conversion pipeline
transform.rs Orientation, crop, resize
utils.rs NormalizedExt, magic bytes, PixelSource
encode/
mod.rs EncodeParams, the single quality policy
jpeg.rs JPEG encode and APP metadata injection
png.rs PNG encode and EXIF/XMP/IPTC/ICC chunks
webp.rs WebP encode and RIFF metadata chunks
avif.rs AVIF encode and ISOBMFF XMP injection
metadata/
mod.rs Metadata dispatcher and route notes
privacy.rs The privacy field registry and the strippers
jpeg.rs JPEG APP metadata extraction
png.rs PNG chunk metadata extraction
webp.rs WebP RIFF metadata extraction
isobmff.rs HEIC/AVIF EXIF, XMP, and ICC extraction
exif.rs EXIF normalization and XMP projection
tiff.rs TIFF/EXIF helper parser, not a public TIFF codec
icc.rs ICC extraction helper
xmp_iptc.rs XMP/IPTC parsing helpers
tests/
common/mod.rs Generated fixtures for the pipeline tests
pipeline.rs End-to-end coverage of the conversion pipeline
js-wrapper/
shared.js Option mapping and call sequence shared by both entries
index.js ESM entry
index.cjs CommonJS entry
index.d.ts Public TypeScript API
README.npm.md Consumer README copied into pkg/README.md
package.template.json npm package metadata
scripts/
build.mjs Cross-platform build and packaging
verify.mjs Full source verification
build_wasm.sh Shim for scripts/build.mjs
build_wasm.bat Shim for scripts/build.mjs
test-npm.js Smoke test for the generated pkg/
rust-toolchain.toml Pinned toolchain
- docs/CODE_ARCHITECTURE.md — internals, invariants, and the reasoning behind the non-obvious choices.
- docs/ARCHIVE.md — how to rebuild this project years from now.
- docs/PUBLISH_GUIDE.md — release and publish procedure.
- CHANGELOG.md — version history.
AGPL-3.0-only. See LICENSE.
The distributed WASM artifacts include heic and zenwebp, both
AGPL-3.0-only OR LicenseRef-Imazen-Commercial. The package cannot be relicensed as MIT
while they remain.