diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc98e23..0d4d8f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: # The vendored libjpeg-turbo inside jpegli's libjxl tree declares a # cmake floor that cmake 4 refuses; this lifts it. CMAKE_POLICY_VERSION_MINIMUM: "3.5" - # Not on musl. With the variable set, `re_rav1d`'s debug-build borrow + # Not on musl. With the variable set, `rav1d`'s debug-build borrow # tracking captures a backtrace on every borrow, and unwinding a # static musl binary is slow enough that the facade tests take 440 s # instead of 7 (measured 2026-09-22; the same binary runs in 0.6 s diff --git a/CHANGELOG.md b/CHANGELOG.md index cc6d44b..ca4536b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +* **codecs:** AVIF decodes on `wasm32`. The decoder is `sqzer-rav1d` on every target, in place of the archived `re_rav1d`: upstream `rav1d` `main` with its safe Rust API and a stand-in for the `libc` names `wasm32` lacks (ADR-0011 D2). A `wasm32` build now reads AVIF input, takes a perceptual target for AVIF output and defaults to AVIF for photos, like every other build. `--list-codecs` and `--json` name the decoder `rav1d` * **cli:** errors and warnings style flags and values the way `clap` does: `'--width'` quoted, in yellow where it names the problem and in green where it names the fix. `--color never`, `NO_COLOR` and a pipe keep the quotes without the colour. The `error` and `reason` fields of `--json` carry the same text with backticks around each flag ## 0.2.0 diff --git a/Cargo.lock b/Cargo.lock index 90d2485..1ef5059 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1865,29 +1865,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "re_rav1d" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5eb64c43c68024d96d99d52ef0525030d17bdcc6a6b4ddba048ec8f713c91fc" -dependencies = [ - "assert_matches", - "atomig", - "av-data", - "bitflags", - "cc", - "cfg-if", - "libc", - "nasm-rs", - "parking_lot", - "paste", - "raw-cpuid", - "static_assertions", - "strum", - "to_method", - "zerocopy 0.7.35", -] - [[package]] name = "read-fonts" version = "0.41.0" @@ -2165,10 +2142,10 @@ dependencies = [ "oxipng", "png", "ravif", - "re_rav1d", "resvg", "rgb", "sqzer-core", + "sqzer-rav1d", "tiff", "webpx", "yuv", @@ -2201,6 +2178,29 @@ dependencies = [ "sqzer-codecs", ] +[[package]] +name = "sqzer-rav1d" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "675eb2751acb31481c926e230087fd69ce497d5a6f8887ec5ba1c653c09b7cbb" +dependencies = [ + "assert_matches", + "atomig", + "av-data", + "bitflags", + "cc", + "cfg-if", + "libc", + "nasm-rs", + "parking_lot", + "paste", + "raw-cpuid", + "static_assertions", + "strum", + "to_method", + "zerocopy 0.7.35", +] + [[package]] name = "sqzer-wasm" version = "0.2.0" @@ -2237,23 +2237,22 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.3" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", "syn 2.0.119", ] diff --git a/README.md b/README.md index 9e1f9b9..a2ef042 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ Backends come in tiers, mirrored by Cargo features: ``` portable pure Rust, permissive licences, builds on wasm32. Always on. - JPEG (mozjpeg-rs / zune-jpeg), PNG (oxipng), AVIF (ravif / re_rav1d), + JPEG (mozjpeg-rs / zune-jpeg), PNG (oxipng), AVIF (ravif / rav1d), WebP (image-webp, lossless write), JXL decode (jxl-oxide). Input only: GIF (gif), TIFF (tiff), BMP, TGA, ICO, QOI and PNM (image, decoders only), SVG rasterised by resvg. @@ -192,7 +192,7 @@ agpl reserved. Never a default dependency, never in the library. > **Note**: Input-only formats decode to their first image: the first frame of an animated GIF, the first page of a TIFF, the largest entry of an ICO. An SVG is rasterised at its own size, one CSS pixel per pixel, with the system's fonts for text (none on wasm32); references to files on disk are ignored, `data:` URLs are rendered, `.svgz` is not read. TIFF files in CMYK or YCbCr are refused rather than converted. -> **Note**: AVIF decoding is desktop only. `rav1d` does not compile for `wasm32`, so the WASM build has no AVIF decoder and reports AVIF input as an unrecognised format. AVIF encoding builds everywhere, but a perceptual target needs the output decoded to score it, so on `wasm32` AVIF takes an explicit quality only and the default output format there is JPEG. +> **Note**: AVIF decodes on every target, `wasm32` included, through `sqzer-rav1d`: upstream `rav1d` `main` with its safe Rust API, which no upstream release carries yet, and a stand-in for the `libc` names `wasm32` lacks. The fork is dropped for `rav1d` itself once a release has both. [`docs/adr/0011-browser-build.md`](docs/adr/0011-browser-build.md) has the reasoning. > **Note**: `oxipng` is the one portable backend that is not pure Rust: its DEFLATE step is `libdeflate`, a vendored C library compiled by `cc` with no system package to install. It is compiled out on `wasm32`, where the plain `png` writer takes its place, so the WASM build stays C-free. [`docs/adr/0002-libdeflate-in-the-portable-tier.md`](docs/adr/0002-libdeflate-in-the-portable-tier.md) has the reasoning. diff --git a/ROADMAP.md b/ROADMAP.md index d3b82bc..3f80718 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -11,7 +11,7 @@ The "Squoosh replacement" is not complete without a page you can drop an image o - `sqzer-wasm`: the portable tier as a WebAssembly package, `sqzer` on npm. The API, the build and how it is published are [`docs/adr/0011-browser-build.md`](docs/adr/0011-browser-build.md). - A small drag-and-drop page on top of it, at `sqzer.dev`. -> **Note**: The browser build inherits the portable tier's licence gaps: lossy WebP and JPEG XL cannot be written. AVIF decoding and `oxipng` reach `wasm32` with ADR-0011; until that code lands, AVIF input is not decoded there and PNG goes through the plain `png` writer. +> **Note**: The browser build inherits the portable tier's licence gaps: lossy WebP and JPEG XL cannot be written. AVIF decodes on `wasm32` already. `oxipng` reaches it with ADR-0011; until that code lands, PNG there goes through the plain `png` writer. ## Coming from `rimage` diff --git a/crates/sqzer-codecs/Cargo.toml b/crates/sqzer-codecs/Cargo.toml index becd38b..183b502 100644 --- a/crates/sqzer-codecs/Cargo.toml +++ b/crates/sqzer-codecs/Cargo.toml @@ -35,11 +35,9 @@ portable = [ png = ["dep:png", "dep:oxipng", "dep:kamadak-exif"] jpeg = ["dep:mozjpeg-rs", "dep:zune-jpeg", "dep:kamadak-exif"] webp-lossless = ["dep:image-webp", "dep:kamadak-exif"] -# The AVIF encoder builds everywhere. The decoder is desktop only: `rav1d` -# does not compile for wasm32, so on that target this feature adds the -# encoder and nothing else. `moxcms` turns wide-gamut `nclx` primaries -# into an ICC profile for the colour stage. -avif = ["dep:avif-parse", "dep:re_rav1d", "dep:yuv", "dep:ravif", "dep:rgb", "dep:moxcms"] +# AVIF, encoder and decoder, on every target. `moxcms` turns wide-gamut +# `nclx` primaries into an ICC profile for the colour stage. +avif = ["dep:avif-parse", "dep:rav1d", "dep:yuv", "dep:ravif", "dep:rgb", "dep:moxcms"] jxl-decode = ["dep:jxl-oxide"] # HEIC recognised and its header read (size, rotation, ICC) with no # decoder: a portable build says "needs `native-heif`" for a HEIC instead @@ -122,6 +120,17 @@ image = { version = "0.25.10", optional = true, default-features = false } # renders embedded PNG, JPEG, GIF and WebP through crates already in the # tree. System fonts are loaded on desktop targets only, see below. resvg = { version = "0.48.1", optional = true, default-features = false, features = ["text", "raster-images"] } +# ISOBMFF demuxer. MPL-2.0, file-level copyleft, on the allow-list. +avif-parse = { version = "2.1.0", optional = true } +# The AV1 decoder. `sqzer-rav1d` (BSD-2-Clause) is upstream rav1d `main`, +# whose safe Rust API no upstream release carries yet, plus a stand-in for +# the `libc` names wasm32 lacks (ADR-0011 D2); the library is still named +# `rav1d`. Upstream 1.1 exposes only the C ABI, which this workspace's +# `forbid(unsafe_code)` cannot call. Assembly is off so no nasm is needed +# to build; the Rust fallback paths are used everywhere. +rav1d = { package = "sqzer-rav1d", version = "0.1.0", optional = true, default-features = false, features = ["bitdepth_8", "bitdepth_16"] } +# YUV to RGB, 8 to 16 bit. BSD-3-Clause OR Apache-2.0. +yuv = { version = "0.8.17", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # `` in an SVG needs fonts; on desktop targets `resvg` reads the @@ -133,15 +142,6 @@ resvg = { version = "0.48.1", optional = true, default-features = false, feature # `parallel` is off (single-threaded encoders, ADR-0001 D3), `zopfli` # (Apache-2.0, pure Rust) backs the top effort level. oxipng = { version = "10.2.1", optional = true, default-features = false, features = ["zopfli"] } -# ISOBMFF demuxer. MPL-2.0, file-level copyleft, on the allow-list. -avif-parse = { version = "2.1.0", optional = true } -# rerun's fork of rav1d (BSD-2-Clause) with a safe Rust API; upstream rav1d -# 1.1 exposes only the C ABI, which this workspace's `forbid(unsafe_code)` -# cannot call. Assembly is off so no nasm is needed to build; the Rust -# fallback paths are used everywhere. -re_rav1d = { version = "0.1.3", optional = true, default-features = false, features = ["bitdepth_8", "bitdepth_16"] } -# YUV to RGB, 8 to 16 bit. BSD-3-Clause OR Apache-2.0. -yuv = { version = "0.8.17", optional = true } # ---- native tier ---- # `libwebp` through imazen's safe wrapper. MIT OR Apache-2.0; `libwebp-sys` @@ -161,7 +161,7 @@ gamut-core = { version = "2.0.1", optional = true } # BSD-2-Clause; libaom itself is BSD-2-Clause plus the AOMedia patent # licence. cmake builds both libraries; x86 needs `nasm`. The default # `codec-dav1d` and `codec-rav1e` features are off: decoding stays with -# `re_rav1d` and rav1e is the portable tier's encoder already. +# `rav1d` and rav1e is the portable tier's encoder already. libavif = { version = "0.14.0", optional = true, default-features = false, features = ["codec-aom"] } # `jpegli` through Kornel's mozjpeg-style wrapper. BSD-3-Clause; # `jpegli-sys` (BSD-3-Clause) vendors libjxl 0.10's jpegli and highway and diff --git a/crates/sqzer-codecs/src/avif/decode.rs b/crates/sqzer-codecs/src/avif/decode.rs index f1abfec..f1be31f 100644 --- a/crates/sqzer-codecs/src/avif/decode.rs +++ b/crates/sqzer-codecs/src/avif/decode.rs @@ -1,9 +1,9 @@ -//! AVIF decoding: `avif-parse` (MPL-2.0) splits the container, `re_rav1d` +//! AVIF decoding: `avif-parse` (MPL-2.0) splits the container, `rav1d` //! (BSD-2) decodes the AV1 payloads, `yuv` (BSD-3/Apache) converts to RGB. //! -//! Desktop only. Upstream `rav1d` does not compile for wasm32, so this -//! module is `cfg`'d out there and the `avif` feature adds only the encoder -//! to the WASM build (ADR-0001 D6 names this as the accepted gap). +//! On every target, wasm32 included. The crate is `sqzer-rav1d`, upstream +//! `rav1d` `main` with its safe Rust API and a stand-in for the `libc` +//! names wasm32 lacks (ADR-0011 D2). //! //! `avif-parse` does not surface the `colr` box, `irot` and `imir` or the //! Exif and XMP items, so those come from the HEIF container walk in @@ -25,9 +25,9 @@ use std::io::Cursor; -use re_rav1d::dav1d::{ - Decoder as Av1Decoder, Error as Av1Error, Picture, PixelLayout, PlanarImageComponent, Settings, - pixel, +use rav1d::{ + Decoder as Av1Decoder, Picture, PixelLayout, PlanarImageComponent, Rav1dError as Av1Error, + Settings, pixel, }; use sqzer_core::codec::{Decoder, DecoderCaps, Format, FormatInfo, Tier}; use sqzer_core::image::{ColorType, Image, Orientation, Samples}; @@ -43,7 +43,7 @@ pub struct AvifDecoder; static DECODER_CAPS: DecoderCaps = DecoderCaps { format: Format::Avif, - name: "re_rav1d", + name: "rav1d", animation: false, tier: Tier::Portable, }; @@ -244,21 +244,21 @@ fn decode_av1(obu: &[u8]) -> Result { settings.set_n_threads(1); settings.set_max_frame_delay(1); let mut decoder = Av1Decoder::with_settings(&settings).map_err(codec_err)?; - match decoder.send_data(obu.to_vec(), None, None, None) { - Ok(()) | Err(Av1Error::Again) => {} + match decoder.send_data(obu.into(), None, None, None) { + Ok(()) | Err(Av1Error::TryAgain) => {} Err(e) => return Err(codec_err(e)), } loop { match decoder.get_picture() { Ok(picture) => return Ok(picture), - Err(Av1Error::Again) => match decoder.send_pending_data() { + Err(Av1Error::TryAgain) => match decoder.send_pending_data() { Ok(()) => { return decoder.get_picture().map_err(|e| match e { - Av1Error::Again => Error::Codec("AV1 payload holds no picture".into()), + Av1Error::TryAgain => Error::Codec("AV1 payload holds no picture".into()), other => codec_err(other), }); } - Err(Av1Error::Again) => {} + Err(Av1Error::TryAgain) => {} Err(e) => return Err(codec_err(e)), }, Err(e) => return Err(codec_err(e)), diff --git a/crates/sqzer-codecs/src/avif/mod.rs b/crates/sqzer-codecs/src/avif/mod.rs index 0b2a5c5..044dd61 100644 --- a/crates/sqzer-codecs/src/avif/mod.rs +++ b/crates/sqzer-codecs/src/avif/mod.rs @@ -1,10 +1,8 @@ //! AVIF. Encoding via `ravif` (BSD-3-Clause) over `rav1e`, on every -//! target. Decoding via `avif-parse`, `re_rav1d` and `yuv`, desktop only: -//! see [`AvifDecoder`] for why. +//! target. Decoding via `avif-parse`, `rav1d` and `yuv`, on every target +//! too: see [`AvifDecoder`]. -#[cfg(not(target_arch = "wasm32"))] mod decode; -#[cfg(not(target_arch = "wasm32"))] pub use decode::AvifDecoder; use ravif::{AlphaColorMode, BitDepth, ColorModel, Img}; diff --git a/crates/sqzer-codecs/src/lib.rs b/crates/sqzer-codecs/src/lib.rs index 36777ef..dff03cd 100644 --- a/crates/sqzer-codecs/src/lib.rs +++ b/crates/sqzer-codecs/src/lib.rs @@ -111,7 +111,6 @@ pub fn register_portable(reg: &mut Registry) { } #[cfg(feature = "avif")] { - #[cfg(not(target_arch = "wasm32"))] reg.register_decoder(avif::AvifDecoder); reg.register_encoder(avif::RavifEncoder); } diff --git a/crates/sqzer-codecs/src/native/avif.rs b/crates/sqzer-codecs/src/native/avif.rs index d89ddab..86d8b19 100644 --- a/crates/sqzer-codecs/src/native/avif.rs +++ b/crates/sqzer-codecs/src/native/avif.rs @@ -3,7 +3,7 @@ //! `libaom-sys` (both BSD-2-Clause; libaom itself is BSD-2-Clause plus the //! Alliance for Open Media patent licence). Both libraries are vendored and built with //! cmake; x86 builds need `nasm`. Takes over the AVIF format from `ravif` -//! when `native-avif` is on; decoding stays with `re_rav1d`. +//! when `native-avif` is on; decoding stays with `rav1d`. use libavif::{AvifImage, RgbPixels, YuvFormat}; use sqzer_core::codec::{CodecOption, Encoder, EncoderCaps, Format, Tier}; diff --git a/crates/sqzer/src/lib.rs b/crates/sqzer/src/lib.rs index 6e74b5f..34c1506 100644 --- a/crates/sqzer/src/lib.rs +++ b/crates/sqzer/src/lib.rs @@ -336,9 +336,9 @@ impl Sqzer { /// mode and skips the search. /// /// > **Note**: scoring needs the output format decodable in this - /// > build. On `wasm32` AVIF is encode-only, so a perceptual target - /// > for AVIF there returns [`sqzer_core::Error::Unsupported`], and - /// > the default format falls back to JPEG. + /// > build. A registry with an encoder and no decoder for a format + /// > returns [`sqzer_core::Error::Unsupported`] for a perceptual + /// > target there, and the default format steps around it. /// /// # Errors /// Unknown input, input this build recognises but cannot decode @@ -1297,7 +1297,7 @@ mod tests { assert_eq!(&out.bytes[8..12], b"WEBP"); } - /// The wasm32 shape: an AVIF encoder with no AVIF decoder. + /// An AVIF encoder with no AVIF decoder, as a narrowed registry can be. fn encode_only_avif() -> Registry { let mut narrow = Registry::new(); narrow.register_decoder(sqzer_codecs::png::PngDecoder); diff --git a/deny.toml b/deny.toml index 1c76c92..f5a1927 100644 --- a/deny.toml +++ b/deny.toml @@ -60,9 +60,9 @@ deny = [ yanked = "deny" ignore = [ # `paste` is archived, not vulnerable: a proc macro that only runs at - # build time. It reaches us through `re_rav1d` and `rav1e`. Drop this + # build time. It reaches us through `sqzer-rav1d` and `rav1e`. Drop this # entry when both have moved to a release without it. - { id = "RUSTSEC-2024-0436", reason = "unmaintained proc macro, build-time only, via re_rav1d and rav1e" }, + { id = "RUSTSEC-2024-0436", reason = "unmaintained proc macro, build-time only, via sqzer-rav1d and rav1e" }, ] [sources] diff --git a/docs/adr/0011-browser-build.md b/docs/adr/0011-browser-build.md index dcdd34c..647172a 100644 --- a/docs/adr/0011-browser-build.md +++ b/docs/adr/0011-browser-build.md @@ -198,7 +198,7 @@ That repository's Pages workflow deploys on every merge to its `main`, so a page ## 6. Action items -1. [ ] `github.com/sqzer-dev/rav1d`: branch `sqzer` on upstream `main` with the shim, published as `sqzer-rav1d`; the same shim as a pull request upstream. `sqzer-codecs` on it for every target; the `paste` ignore dropped if it can be; the AVIF fixtures decoded in the wasm test. +1. [ ] `github.com/sqzer-dev/rav1d`: branch `sqzer` on upstream `main` with the shim, published as `sqzer-rav1d`; the same shim as a pull request upstream. `sqzer-codecs` on it for every target; the `paste` ignore dropped if it can be; the AVIF fixtures decoded in the wasm test. (Published as `sqzer-rav1d` 0.1.0 on 2026-10-01 from one commit on upstream `d3d1cd6`: the crate aliases itself as `libc` on wasm32 and `src/wasm32_libc.rs` provides the names, so no use site changed. `sqzer-codecs` is on it for every target. Upstream still pulls `paste`, so the ignore stays. The upstream pull request is `memorysafety/rav1d#1505`, opened the same day: it keeps the C type names and defines them in the crate, where the fork aliases the crate as `libc`. Left open: the AVIF fixtures in the wasm test, which arrives with item 3, and retiring the fork once an upstream release has the Rust API and that fix.) 2. [ ] `oxipng` on wasm32 with `freestanding`, the `png` encoder removed, the golden PNG test running on wasm32 too; `.cargo/config.toml`. 3. [ ] The `sqzer-wasm` surface of D3 with `wasm-pack test --node` over the fixtures: every raster format in and out, an `EncoderUnavailable` for JPEG XL with `availableIn`, `onTrial` called once per trial, `maxPixels` refusing a 25 megapixel header, `fromPixels` round-tripping a fixture's pixels. `decodeAny` cannot run in Node; it is checked in Chrome, Firefox and Safari with item 5, including `createImageBitmap` on an SVG blob inside a Worker, which may need the main thread in some of them. 4. [ ] `publish-npm.yml`, the `dist` config and `dist generate`; the trusted publisher on npmjs.com; a rehearsal with `pr-run-mode = "upload"` on a branch, then the first publish with the 0.3.0 tag.