Skip to content

Repository files navigation

Image Converter WASM

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.

Features

  • 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.js and pkg/index.cjs, with shared logic in pkg/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 heic crate with its av1 feature. No C libraries, no JavaScript codec fallbacks.
  • HEIC output is unavailable by design and returns an explicit error.
  • Encode intents: Balanced, Archive, and Social select the quality, chroma, and metadata policy.
  • Privacy: Social removes 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.

Build

Prerequisites:

  1. The Rust toolchain pinned in rust-toolchain.toml (rustup reads it automatically).
  2. 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 yourscope

After that they are remembered, so every later build is just:

./build_wasm.sh

Build one module by name:

./build_wasm.sh jpeg

Full 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:

  1. Command line.
  2. AUTHOR_NAME / NPM_SCOPE environment variables.
  3. .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.
  4. 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.sh

Values 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
    ...

Verify

One command runs every source check:

node scripts/verify.mjs

That 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.js

It runs the full suite against both the ESM and CommonJS entries.

Maintainer Usage

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.

Public JavaScript API

init(options?)

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.

convertImage(fileBytes, ext, options)

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.

convertImageWithInfo(fileBytes, ext, options, onProgress?)

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;
}

Encode Intents

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.

Metadata Behaviour

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 tEXt for 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 colr boxes but this encoder path does not embed it into AVIF output.
  • ICC may be carried independently of keepMetadata to preserve colour fidelity; EXIF/XMP/IPTC preservation requires keepMetadata: true.

Limits

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.

Project Structure

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

Documentation

License

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.

Releases

Contributors

Languages