Web: leave the JPEG XL DNG decoder out of the wasm (0.88 MB, 23.1 MiB now)

The 0.6.0 web build went over packaging/web/package.sh's 24 MiB gate (26,980,503 bytes on
782bcb67). Today's landings added 1.34 MB; the biggest single item was jxl-oxide (#2047, JPEG
XL-compressed DNG): ~0.6 MB of jxl_* code plus brotli-decompressor and tables, 0.88 MB after
wasm-opt. 0c72d954 brought the wasm back to 25,144,413 bytes (21 KB under the gate) by deflating
catalogs and fonts; this takes it to 24,262,088 bytes (23.14 MiB, 904 KB of headroom).

jxl-oxide is now a native-only dependency of photocraft-raw. On wasm32 a JPEG XL DNG segment
returns RawError::Unsupported ("can't be opened in the browser version of PhotoCraft; open it
in the desktop app"), like the other cfg-gated web features; every other DNG compression and
raw format still opens in the browser. Native builds are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-09 22:36:03 -04:00
parent 0c72d95425
commit e633d17a09
3 changed files with 19 additions and 5 deletions

View File

@ -17,13 +17,14 @@ corpus = []
[dependencies]
# Adobe DNG compression 8: zlib (RFC 1950) with the TIFF predictor (pure-Rust backend).
flate2 = { workspace = true }
# JPEG XL-compressed DNG (DNG 1.7, e.g. Samsung Expert RAW). Pure Rust, MIT OR Apache-2.0. Pinned
# exactly: it parses untrusted files; moving the pin is a reviewed change (re-run the raw fuzz target).
jxl-oxide = { version = "=0.12.6", default-features = false }
# Parallel bands on native; wasm builds stay single-threaded.
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
rayon = { workspace = true }
# JPEG XL-compressed DNG (DNG 1.7, e.g. Samsung Expert RAW). Pure Rust, MIT OR Apache-2.0. Pinned
# exactly: it parses untrusted files; moving the pin is a reviewed change (re-run the raw fuzz target).
# Native only: it added 0.88 MB to the browser build's wasm, which has a hard size cap
# (packaging/web/package.sh); there JPEG XL DNG is reported as unsupported (src/jxl.rs).
jxl-oxide = { version = "=0.12.6", default-features = false, features = ["rayon"] }
[dev-dependencies]

View File

@ -2,7 +2,7 @@
A clean-room, pure-Rust camera raw decoder and developer. The crate is standalone (no workspace
dependencies; its decoding dependencies are `flate2` for Deflate DNG and `jxl-oxide`, a pure-Rust
JPEG XL decoder, for DNG 1.7), has no `unsafe`, does no I/O (`&[u8]` in), builds for `wasm32-unknown-unknown`
JPEG XL decoder, for DNG 1.7, native builds only), has no `unsafe`, does no I/O (`&[u8]` in), builds for `wasm32-unknown-unknown`
(sequential there, rayon-parallel on native) and never panics on hostile input: every offset is
bounds-checked and sizes are checked against `Limits` before allocating.
@ -58,7 +58,7 @@ camera colour tables were copied.
| Format | Status |
|---|---|
| DNG | Uncompressed (8–16 bit, packed or not), Deflate (compression 8, predictors 1 and 2), lossless JPEG and JPEG XL (DNG 1.7, e.g. Samsung Expert RAW; decoded by `jxl-oxide`); strips and tiles; CFA (Bayer) and LinearRaw; LinearizationTable, BlackLevel (+ repeat, DeltaH/V), WhiteLevel, ActiveArea, DefaultCrop, ColorMatrix1/2, CameraCalibration, ForwardMatrix, AnalogBalance, AsShotNeutral / AsShotWhiteXY, BaselineExposure, Orientation, OpcodeList2 GainMap (lens shading) |
| DNG | Uncompressed (8–16 bit, packed or not), Deflate (compression 8, predictors 1 and 2), lossless JPEG and JPEG XL (DNG 1.7, e.g. Samsung Expert RAW; decoded by `jxl-oxide`; not in wasm32 builds, where it is `Unsupported`); strips and tiles; CFA (Bayer) and LinearRaw; LinearizationTable, BlackLevel (+ repeat, DeltaH/V), WhiteLevel, ActiveArea, DefaultCrop, ColorMatrix1/2, CameraCalibration, ForwardMatrix, AnalogBalance, AsShotNeutral / AsShotWhiteXY, BaselineExposure, Orientation, OpcodeList2 GainMap (lens shading) |
| DNG (lossy JPEG, floating point; opcodes other than GainMap; ProfileGainTableMap) | Unsupported / not applied (reported) |
| CR2 | Lossless JPEG with slices, borders and as-shot white balance from the maker note, black measured on the masked border. CR2 has no CFA tag and the row phase varies by model, so it is measured from the data (the green diagonal), with a Canon model-ID table as the fallback (see `src/cr2.rs`) |
| CR2 sRAW / mRAW | Unsupported |

View File

@ -5,14 +5,26 @@
//! Expert RAW writes LinearRaw this way. Decoding is done by `jxl-oxide`, a pure-Rust decoder;
//! like heic-rs in `photocraft-heif`, every call into it runs under `catch_unwind` so a decoder
//! panic on a malformed file becomes [`RawError::Malformed`].
//!
//! The browser build (wasm32) leaves the decoder out: jxl-oxide added 0.88 MB to a wasm that
//! must stay under the per-file cap of static hosts (packaging/web/package.sh), for a format only
//! a few phones write. There, these segments are [`RawError::Unsupported`] with a clear message.
#[cfg(not(target_arch = "wasm32"))]
use jxl_oxide::{AllocTracker, JxlImage};
use crate::Limits;
use crate::error::{RawError, Result};
/// The browser build has no JPEG XL decoder (see the module docs).
#[cfg(target_arch = "wasm32")]
pub(crate) fn decode(_src: &[u8], _w: usize, _h: usize, _samples: usize, _bits: u32, _limits: &Limits) -> Result<Vec<u16>> {
Err(RawError::unsupported("JPEG XL-compressed DNG (DNG 1.7) can't be opened in the browser version of PhotoCraft; open it in the desktop app"))
}
/// Decodes one segment into `w * h * samples` interleaved samples at `bits` per sample (the
/// TIFF BitsPerSample, which DNG requires the codestream's bit depth to match).
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn decode(src: &[u8], w: usize, h: usize, samples: usize, bits: u32, limits: &Limits) -> Result<Vec<u16>> {
limits.check(w as u64, h as u64, samples as u64 * 2)?;
// jxl-oxide decodes into f32/i32 planes: allow a few times the output size, and never more
@ -54,6 +66,7 @@ pub(crate) fn decode(src: &[u8], w: usize, h: usize, samples: usize, bits: u32,
})
}
#[cfg(not(target_arch = "wasm32"))]
fn guarded<T>(f: impl FnOnce() -> Result<T>) -> Result<T> {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or_else(|_| Err(RawError::malformed("the JPEG XL decoder failed on this file")))
}