@commoncase/scanline-image-transform
v0.0.1-experimental.0
Published
Bounded-memory JPEG and PNG to WebP transforms for Cloudflare Workers
Readme
Scanline Image Transform
Work in progress — an experimental, bounded-memory JPEG/PNG to WebP path for Cloudflare Workers.
Scanline Image Transform is a TypeScript and WebAssembly library maintained by Common Case. It decodes supported images incrementally and avoids allocating a complete decoded source bitmap, which makes it useful for exploring image processing inside the memory envelope of Cloudflare Workers.
The project is not published to npm yet. The API, supported-image matrix, memory behavior, and package boundaries may change while the implementation is being hardened.
What it does
- Accepts a
Uint8Array,ReadableStream<Uint8Array>, or async iterable as input. - Decodes baseline, single-scan JPEG and legal non-interlaced or Adam7 PNG inputs.
- Resizes with
containorcoversemantics using nearest-neighbor sampling. - Emits WebP, including alpha when the PNG source has transparency.
- Exposes transform metadata, conservative memory diagnostics, stable error codes, and
AbortSignalcancellation. - Includes a small Fetch-compatible Worker adapter for
POSTimage transforms.
The current pipeline is:
JPEG or PNG input stream
-> bounded scanline/pass decoder in WASM
-> resize into destination-sized YUV420(A) planes
-> stock libwebp encoder in WASM
-> WebP ReadableStreamThe input side is bounded, but the current WebP encoder still receives a complete resized destination picture. The public result is therefore currently one output chunk. A row-fed encoder is a future architecture slice, not a promise of this WIP release.
Current status
The current branch contains a working feasibility implementation and a v1 contract, not a stable production release. The main open architectural limitation is output-side memory: destination planes and stock libwebp encoder working memory scale with the requested output size.
The exact behavior is documented in the v1 contract. The repository also contains architecture decisions, benchmark reports, and input-variant plans.
Use from a checkout
The package is not published yet. The generated dist/ package boundary is
checked in so other repositories can consume an immutable Git revision during
development without a sibling workspace or a build-time native codec toolchain.
Use a local checkout when developing the package itself.
Prerequisites:
- Bun 1.3.14 or compatible.
- Emscripten with
emccavailable; CI uses 3.1.74. cjpegand JPEG development headers for the optional native probe (bun run test:native).
git clone https://github.com/commoncase/scanline-image-transform.git
cd scanline-image-transform
bun installUseful checks:
bun run typecheck # TypeScript typecheck
bun run test # Build WASM and run the test suite
bun run build:package # Build dist/ for package consumers
bun pm pack --dry-run # Inspect the package boundaryThe stress, native, and benchmark commands are available when the corresponding local toolchain is installed:
bun run test:stress
bun run test:native
bun run benchmark:memoryCore API
The core API accepts a streamed request body and returns a WebP stream:
import { transformImage } from "@commoncase/scanline-image-transform";
export async function resizeImage(request: Request): Promise<Response> {
if (!request.body) {
return new Response("Image body required", { status: 400 });
}
const result = await transformImage({
input: request.body,
outputFormat: "webp",
resize: {
width: 1200,
height: 1200,
fit: "contain",
},
quality: 82,
signal: request.signal,
});
return new Response(result.stream, {
headers: { "content-type": "image/webp" },
});
}result.metadata reports the input and output dimensions and formats. result.diagnostics reports input buffering, decoder and encoder WASM high-water marks, destination memory, and the conservative preEncoderPeakBytes accounting used by the benchmarks.
Worker adapter
For a small Cloudflare Worker endpoint, use the Fetch-compatible adapter:
import { createImageWorkerHandler } from "@commoncase/scanline-image-transform";
export default {
fetch: createImageWorkerHandler(),
};The adapter accepts POST requests with an image/jpeg or image/png body and these query parameters:
/transform?width=1200&height=1200&fit=contain&quality=82To try the repository's probe locally:
bun run dev
curl -X POST \
-H 'content-type: image/jpeg' \
--data-binary @input.jpg \
'http://localhost:8787/transform?width=1200&height=1200&fit=contain' \
--output output.webpThe probe Worker is for development and validation. Deploying it is optional:
bunx wrangler login
bun run deploy:dry
bun run deploySupported inputs and output
| Format | Current support | | --- | --- | | JPEG | 8-bit baseline, single-scan JPEG; EXIF orientations 1–8 are visually normalized | | PNG | Legal 1/2/4/8/16-bit grayscale, RGB, indexed, grayscale+alpha, and RGBA; non-interlaced and Adam7 interlaced | | Output | WebP only; PNG transparency is preserved as WebP alpha |
Progressive and multi-scan JPEG, unsupported PNG color/bit-depth combinations, and other image formats are rejected deliberately. Source EXIF, ICC, XMP, and comment metadata are not copied to the WebP result; recognized source color metadata is normalized to the sRGB output path.
Default limits
Limits can be overridden through transformImage options or Worker adapter options. They exist to make resource use explicit at the call boundary.
| Limit | Default | | --- | ---: | | Compressed input bytes | 50 MiB | | Source width or height | 8,192 pixels | | Source pixels | 50,000,000 | | Requested output width or height | 1,920 pixels | | Requested output pixels | 3,686,400 | | WebP quality | 82 (range 0–100) |
contain fits the source inside the requested rectangle and may produce a smaller output to preserve its aspect ratio. cover fills the requested dimensions and center-crops when the aspect ratios differ.
Worker errors use these stable status mappings:
| Condition | Status | | --- | ---: | | Invalid transform parameters | 400 | | Input or output limit exceeded | 413 | | Unsupported media or image format | 415 | | Malformed supported image | 422 |
Memory model
The decoders retain a bounded input window, one normalized source/pass row, and the previous PNG filter row. They do not allocate a complete source pixel bitmap. Adam7 PNG and oriented JPEG use output-sized scatter paths instead of reconstructing a full source frame.
The current encoder receives destination-sized YUV420 planes, plus a destination alpha plane for transparent PNG input. preEncoderPeakBytes is conservative local accounting that includes decoder memory, pending input, destination storage, and WebP WASM linear-memory high-water; it is not a direct Cloudflare per-request RSS measurement. See the output-memory benchmark for the measured trade-off.
Documentation
- v1 contract
- bounded source-memory ADR
- v1 implementation plan
- input-variant plan
- benchmark reports
- codec streaming research
Before a stable release
- Decide the stable package version and publish boundary.
- Continue validating memory behavior on the target Workers runtime.
- Decide whether a row-fed WebP encoder is needed to make output memory independent of destination size.
- Keep progressive/multi-scan JPEG rejected unless a decoder meets the source-memory gate.
