@wasmedia/wlibraw
v0.2.0
Published
LibRaw compiled to WebAssembly for browsers
Downloads
491
Maintainers
Readme
wlibraw
LibRaw — decoding of raw camera files (CR2/CR3, NEF, ARW, DNG,
RAF, ORF, RW2, and more) — compiled to WebAssembly for the browser. Part of the
wasmedia family of single-threaded + SIMD WASM ports that
run on any site without SharedArrayBuffer or COOP/COEP headers.
wlibraw handles decoding only: lens correction is wlensfun's job, general image processing is wvips's, and metadata writing is wexiftool's.
What it supports today
- Full raw decoding for every format LibRaw supports, via
libraw_unpack+libraw_dcraw_process. - Typed metadata: make/model (LibRaw-canonicalised), raw and output dimensions, ISO, shutter speed, aperture, focal length, timestamp, lens, CFA pattern, per-channel white balance multipliers, and embedded-thumbnail info.
- Embedded JPEG thumbnail extraction.
- Decode options matching LibRaw's own defaults: half-size, 8/16-bit output, output color space, demosaic quality, camera/auto white balance, brightness, highlight recovery, flip, four-color RGB.
- Deterministic cleanup via
Symbol.dispose(using file = wlibraw.open(...)). - LibRaw's own
dcraw_emucommand-line tool, run against in-memory files withexecute(). - A Web Worker,
@wasmedia/wlibraw/worker, that decodes off the main thread, andWLibRawWorker, a promise client for it. executeFiles(), a file-backeddcraw_emurun (host files under Node, OPFS in a worker) that keeps inputs and outputs out of JavaScript memory.- A browser playground (
npm run playground).
The full reference is in docs/api.md.
Install / build
Releases are published to npm as @wasmedia/wlibraw, with the prebuilt
wlibraw.wasm included, so using it needs no Docker or Emscripten:
npm install @wasmedia/wlibrawTo build it from source instead:
npm install
npm run build:native # compiles LibRaw + delegates to dist/wlibraw.{js,wasm} via Docker
npm run build:ts # compiles src/ to dist/*.js
npm test # or just: npm run build && npm testnpm run build:native requires Docker and GNU Make (not a local Emscripten install —
make builds and caches the builder image itself). A from-scratch build of LibRaw and its
delegates takes about two minutes on an Apple-silicon Mac, plus a one-time pull of the
Emscripten Docker image on first use. Later runs reuse fingerprinted
native/sources/.installed-* sentinels and rebuild only what changed. See
docs/building.md for the details, including the flags that must never
be removed and how to bump LibRaw.
npm test needs npm run build:native to have run at least once. dist/ is
git-ignored and the test suite (and src/loader.ts) import dist/wlibraw.js directly;
running npm test on a fresh clone without the native build first fails with a clear
message telling you to run npm run build:native.
Usage
import { initWLibraw } from '@wasmedia/wlibraw';
const wlibraw = await initWLibraw();
const buf = await fetch('/photo.cr2').then((r) => r.arrayBuffer());
using file = wlibraw.open(buf);
console.log(file.metadata.make, file.metadata.model, file.metadata.isoSpeed);
using image = file.decode({ halfSize: true, useCameraWb: true });
const { data, width, height } = image.toImageData(); // ImageData-compatible
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.getContext('2d')!.putImageData(new ImageData(data, width, height), 0, 0);file and image are freed automatically at the end of the block via using
(Symbol.dispose); call .close() / .free() directly, in a finally, if your
toolchain does not support using.
Runtime floor. The library itself does not need native Symbol.dispose: its own code
releases handles with try/finally, and it fills in Symbol.dispose when the runtime
lacks it, so the handle classes always have a real dispose key. Writing using in your
own code is a separate matter: it needs a runtime or transpiler that supports explicit
resource management (TypeScript 5.2+ lowers it). The browser checks cover
headless Chrome and WebKit (Safari's engine); Firefox has been checked by hand.
decode() may be called only once per opened file — LibRaw cannot re-run
libraw_unpack against the same handle. thumbnail() has no such restriction and
composes with decode() in either order:
using file = wlibraw.open(buf);
const thumb = file.thumbnail(); // fine before or after decode()
using image = file.decode();For a one-shot decode with no need to inspect metadata or the thumbnail first, use the convenience wrapper:
const pixels = wlibraw.decode(buf, { halfSize: true }); // { data, width, height, colors, bits }CLI: execute()
execute(args, files) runs LibRaw's own dcraw_emu sample against in-memory files and
returns its exit code, output and any files it wrote:
const data = new Uint8Array(await fetch('/photo.cr2').then((r) => r.arrayBuffer()));
const result = wlibraw.execute(['-h', 'photo.cr2'], [{ name: 'photo.cr2', data }]);
// { exitCode: 0, stdout: '', stderr: '', outputs: [{ name: 'photo.cr2.ppm', data }] }A failing command is a result, not an exception: exit code 1 is a usage error, and 2 means
an input could not be opened or decoded. Each call runs in a scratch directory that is
always removed afterwards. See docs/api.md for
input-name rules, -Z, and the sticky -v quirk.
For file-backed runs, executeFiles() does the same job against host files (Node) or OPFS
files (in a worker), so inputs and outputs are not held in JavaScript memory. See
docs/api.md.
Web Worker
A full-size decode can take long enough to freeze a page, so @wasmedia/wlibraw/worker runs it on a
worker thread:
import { WLibRawWorker } from '@wasmedia/wlibraw';
// Point at the built file. It must stay next to loader.js and wlibraw.{js,wasm}, as in
// the package's dist/.
const client = new WLibRawWorker(() => new Worker('/dist/worker.js', { type: 'module' }));
const rgba = await client.decode(buf, { halfSize: true }, { format: 'rgba8' });WLibRawWorker is a promise client for the worker. Each call is one-shot (init,
metadata, thumbnail, decode, execute or executeFiles), result buffers are transferred rather
than copied, and a failure rejects with a WLibRawError that keeps LibRaw's code and
fatal flag. If the worker dies or its module aborts, the client replaces it on the next
call. The underlying postMessage protocol is public too, for writing your own client. How a bundler resolves a
worker URL inside a package varies from setup to setup, and none has been verified here;
whatever you use, the worker must load dist/worker.js with dist/loader.js and
dist/wlibraw.{js,wasm} beside it. The protocol is in
docs/api.md.
Playground
npm run build # once
npm run playground # http://localhost:8000/The playground decodes a raw file you choose (or the bundled CC0 sample) entirely through
the worker. It shows metadata, the embedded preview and a canvas render, with controls for
the main decode options, and an Export TIFF button that writes a full-resolution TIFF
through executeFiles() to OPFS and offers it as a download (it is disabled, with an
explanation, where OPFS is unavailable). The local server listens on 127.0.0.1 only, answers GET and HEAD,
and deliberately sends no COOP/COEP headers, so every session checks that wlibraw works
without cross-origin isolation. Set PORT to use a different port. Every main pipeline
publishes the same page at https://libraw.wasmedia.org/.
Known limitations
- A wasm abort is still possible, and reports a synthetic code. The native build
compiles with
-fwasm-exceptions, so LibRaw's owntry/catchworks and ordinary failures return real LibRaw codes — a truncated file givesLIBRAW_IO_ERROR(-100009), a missing previewLIBRAW_NO_THUMBNAIL(-5), an unrecognised fileLIBRAW_FILE_UNSUPPORTED(-2), each with.fatalderived from LibRaw's own threshold. If the module aborts for a reason LibRaw does not convert — out of memory, or a genuine bug —WLibRawFilestill catches it and throws aWLibRawError, but with a wlibraw-internal sentinel.codeof-100099and.fatalforced true. Treat that code as "something went wrong below LibRaw", not as a LibRaw enum value. - Monochrome sensors (colors === 1) decode correctly through
toPixels()/toUint8Array()/toUint16Array(), andtoImageData()replicates the single channel across R/G/B, but no monochrome fixture exists in this repo's test suite (Leica M Monochrom / Phase One achromatic samples aren't available), so that path is covered by a synthetic unit test rather than a real decode. - Single-threaded. LibRaw is built without OpenMP, so wlibraw needs no
SharedArrayBufferor cross-origin isolation, but it is slower than a multi-threaded native LibRaw. A full-size decode of the 8 MP test CR2 takes about 0.6 s in Node on an Apple-silicon Mac. UsehalfSizeorthumbnail()when a preview is enough, and the Web Worker to keep a page responsive. - New LibRaw releases are not adopted automatically: a scheduled CI job files an issue when one appears, and the bump is done by hand. The playground is live at https://libraw.wasmedia.org/; it has been checked in Chrome, Firefox and WebKit (Safari's engine, via Playwright); the Safari app itself has not been driven.
Contributing
See CONTRIBUTING.md.
License
This project's licensing is split between the wrapper source and the distributed binary:
- Wrapper source — the code in
src/,native/bindings/, and the build tooling that this project authors — is licensed Apache-2.0, per theLICENSEfile. - The distributed artifact,
dist/wlibraw.wasm— is licensed LGPL-2.1, perLICENSE-libraw. LibRaw is dual-licensed upstream under LGPL-2.1 or CDDL-1.0 (seenative/sources/libraw-0.22.2/COPYRIGHT) and is statically linked into this artifact. Apache-2.0 is incompatible with both options for a combined work, so this project distributes the binary under LGPL-2.1. - Consumers of the published package are bound by LGPL-2.1, which is what the
licensefield inpackage.json(LGPL-2.1-only) declares. - wopfs — 0.2.0 (MIT) is vendored as JavaScript into
dist/vendor/wopfs/(not statically linked into the.wasm); its notice ships asdist/licenses/wopfs-LICENSE.txtand is linked from the playground footer.
wlibraw.wasm also statically links three other libraries. All three are permissively licensed (none imposes copyleft obligations beyond LGPL-2.1), but two of them require their notices to be carried into copies of the software, including a binary that bundles them — which this project's binary does:
zlib-ng — zlib license (see
native/sources/zlib-ng-2.2.2/LICENSE.md). Its notice-retention clause is scoped to "any source distribution," so it does not attach to the compiled binary. No obligation beyond LGPL-2.1.mozjpeg / libjpeg-turbo — IJG / Modified BSD / zlib licenses (see
native/sources/mozjpeg-4.1.5/LICENSE.md). Its own compliance summary requires that product documentation for a binary that statically links libjpeg-turbo carry the attribution:This software is based in part on the work of the Independent JPEG Group.
lcms2 — MIT License (see
native/sources/lcms2-2.16/LICENSE). Its permission notice applies to "all copies or substantial portions of the Software," with no source-only scoping, so it attaches to this statically-linked binary. The required notice:Copyright (c) 2023 Marti Maria Saguer
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
If you intend a different licensing arrangement for this project, review and update this section, LICENSE-libraw, and the license field in package.json together — they currently describe one consistent split.
