Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 28 additions & 29 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
30 changes: 15 additions & 15 deletions crates/sqzer-codecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
# `<text>` in an SVG needs fonts; on desktop targets `resvg` reads the
Expand All @@ -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`
Expand All @@ -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
Expand Down
26 changes: 13 additions & 13 deletions crates/sqzer-codecs/src/avif/decode.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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};
Expand All @@ -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,
};
Expand Down Expand Up @@ -244,21 +244,21 @@ fn decode_av1(obu: &[u8]) -> Result<Picture> {
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)),
Expand Down
6 changes: 2 additions & 4 deletions crates/sqzer-codecs/src/avif/mod.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down
1 change: 0 additions & 1 deletion crates/sqzer-codecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/sqzer-codecs/src/native/avif.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
8 changes: 4 additions & 4 deletions crates/sqzer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0011-browser-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading