From 217c5965c45f2b154c817f0fe0ab4232d0309cc5 Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Sat, 18 Jul 2026 20:05:56 -0400 Subject: [PATCH] chore(features,ci)!: consolidate the post-migration feature tree; add compile-boundary CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The migrations collapsed the five-tier feature hierarchy organically; this change is the consolidation pass. Audit result: the tree already matches docs/V1_DESIGN.md (full = all 17 formats + serde + experimental + hw; every direction feature gates real code). The six retained tier-4 aliases now name their gates in comments — gif/svg/ppm are permanent exceptions per AGENTS.md, tiff waits on gamut#299/#300 (rawshift#22), webp on gamut#302 (rawshift#24). The facade's full feature lists hw explicitly instead of only transitively. Compile-boundary CI lands as two jobs. compile-boundaries asserts that invalid feature x target combos FAIL with the exact compile_error text (hw-vaapi on aarch64-apple-darwin and musl, hw-videotoolbox and hw-mediacodec on linux-gnu) and that the valid combos plus the portable hw stub compile — all seven commands proven locally before landing. full-check builds --features full for the image crate and facade on four of the six tier-1 targets with hosted runners (linux x86_64/aarch64, macOS aarch64, windows msvc); ios/android are justified out in-file (no hosted cross toolchain for the C deps; their hw pins are covered by the boundary job). build.rs verified down to cfg-alias machinery only, check-cfg exact. Docs brought to delivered reality: V1_DESIGN de-hardcodes the stale pin hash and records the blocked-upstream matrix rows; the image README's format table credits the actual gamut backends; justfile loses --all-features (now a provable compile error: the hw pins are mutually exclusive by design) in favour of rawshift-image/full; MSRV references outside the CHANGELOG all read 1.92. Nine broken rustdoc intra-doc links fixed under the new -D warnings doc gate. BREAKING CHANGE: none at the API level; --all-features is no longer a valid build invocation by design (mutually exclusive hardware backend pins). --- .github/workflows/ci.yml | 100 +++++++++++++++++- .github/workflows/release-plz.yml | 6 +- DEVELOPMENT.md | 2 +- README.md | 5 +- crates/rawshift-image/Cargo.toml | 60 +++++++---- crates/rawshift-image/README.md | 98 ++++++++--------- .../rawshift-image/src/formats/dng_export.rs | 3 +- crates/rawshift-image/src/formats/standard.rs | 11 +- .../rawshift-image/src/transforms/opcodes.rs | 2 +- crates/rawshift-image/src/transforms/simd.rs | 3 +- crates/rawshift/Cargo.toml | 9 +- docs/V1_DESIGN.md | 37 +++++-- justfile | 28 +++-- 13 files changed, 253 insertions(+), 111 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 731636c..d3f853d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,9 +16,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - # Must match rust-toolchain.toml exactly ("1.90.0" not "1.90"): rustup + # Must match rust-toolchain.toml exactly ("1.92.0" not "1.92"): rustup # treats the two as different toolchain entries, so components installed - # under "1.90" are invisible when cargo activates "1.90.0" from the file. + # under "1.92" are invisible when cargo activates "1.92.0" from the file. - uses: dtolnay/rust-toolchain@master with: toolchain: "1.92.0" @@ -133,6 +133,102 @@ jobs: fi echo "OK: facade is free of rawshift-video" + # ── Compile boundaries (issue #34) ────────────────────────────────────────── + # The `hw-*` backend pins are *verified* feature flags: rawshift-hwdec holds + # a `compile_error!` for each explicit backend on any target its platform + # API does not exist on (docs/SUPPORT.md is the permanent matrix). This job + # proves the boundary in both directions: every invalid feature x target + # combination MUST fail to compile with that backend's `compile_error!` + # text, and the valid combinations (an explicit pin on its native target; + # the portable `hw` anywhere — including no-API targets, where it compiles + # the no-backend stub) MUST succeed. `cargo check --target` only needs the + # target's std, which `rustup target add` ships, and the `compile_error!` + # fires long before linking — so Apple and musl targets check fine from a + # Linux host. + compile-boundaries: + name: Compile boundaries (feature x target) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.92.0" + targets: aarch64-apple-darwin,x86_64-unknown-linux-musl + - uses: Swatinem/rust-cache@v2 + - name: Invalid combos must fail with the compile_error text + run: | + check_fails() { + local features="$1" target="$2" expect="$3" + echo "::group::EXPECT FAIL: --features ${features} --target ${target}" + if out=$(cargo check -p rawshift-image --no-default-features \ + --features "${features}" --target "${target}" 2>&1); then + echo "${out}" + echo "::error::--features ${features} --target ${target} compiled, but must fail" + exit 1 + fi + if ! grep -qF "${expect}" <<<"${out}"; then + echo "${out}" + echo "::error::--features ${features} --target ${target} failed, but not with the expected compile_error (\"${expect}\")" + exit 1 + fi + echo "OK: failed with the expected compile_error" + echo "::endgroup::" + } + check_fails hw-vaapi aarch64-apple-darwin "requires a linux-gnu target" + check_fails hw-vaapi x86_64-unknown-linux-musl "requires a linux-gnu target" + check_fails hw-videotoolbox x86_64-unknown-linux-gnu "requires an Apple target" + check_fails hw-mediacodec x86_64-unknown-linux-gnu "requires an Android target" + - name: Valid combos must succeed + run: | + # Explicit pin on its native target. + cargo check -p rawshift-image --no-default-features --features hw-vaapi --target x86_64-unknown-linux-gnu + # The portable `hw` is valid everywhere: it selects the native + # backend where one exists and compiles the no-backend stub (with a + # build-script warning) where none does (musl). + cargo check -p rawshift-image --no-default-features --features hw --target aarch64-apple-darwin + cargo check -p rawshift-image --no-default-features --features hw --target x86_64-unknown-linux-musl + + # ── `full` on tier-1 targets (issue #34) ──────────────────────────────────── + # docs/SUPPORT.md tier 1: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, + # aarch64-apple-darwin, aarch64-apple-ios, aarch64-linux-android, and + # x86_64-pc-windows-msvc. The four with native GitHub runners get an + # explicit `--features full` check here (the test matrix only exercises + # default features at the OS level). aarch64-apple-ios and + # aarch64-linux-android are excluded: `full` includes `jxl-encode` + # (cmake-built libjxl via gamut-jxl-sys) and `webp` (libwebp, C), and hosted + # runners have no cross C/C++ toolchain for those targets; their + # boundary-relevant half (the hw pins) is covered by compile-boundaries. + full-check: + name: Check (full) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + - os: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + - os: macos-latest + target: aarch64-apple-darwin + - os: windows-latest + target: x86_64-pc-windows-msvc + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.92.0" + - uses: Swatinem/rust-cache@v2 + # `full` pulls `jxl-encode` (gamut-jxl-sys cmake-builds libjxl; clang for + # its bindings). The macOS and Windows runners ship cmake + LLVM already. + - name: Install system libraries (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends cmake clang libclang-dev + - run: cargo check -p rawshift-image --features full + - run: cargo check -p rawshift --features full + coverage-report: name: Coverage Report runs-on: ubuntu-latest diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 80d7740..3ea9f78 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -22,11 +22,11 @@ jobs: with: fetch-depth: 0 persist-credentials: false - # Match rust-toolchain.toml exactly ("1.90.0"): rustup resolves the repo's + # Match rust-toolchain.toml exactly ("1.92.0"): rustup resolves the repo's # cargo to this toolchain regardless of what we install, so install it here. - uses: dtolnay/rust-toolchain@master with: - toolchain: "1.90.0" + toolchain: "1.92.0" - name: Run release-plz uses: release-plz/action@v0.5.129 with: @@ -56,7 +56,7 @@ jobs: persist-credentials: false - uses: dtolnay/rust-toolchain@master with: - toolchain: "1.90.0" + toolchain: "1.92.0" - name: Run release-plz uses: release-plz/action@v0.5.129 with: diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b0fdd2f..e3a4656 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -5,7 +5,7 @@ Contributor and maintainer guide for `rawshift`. For library design rules see ## Building & testing -The toolchain is pinned by [`rust-toolchain.toml`](rust-toolchain.toml) (1.90.0). +The toolchain is pinned by [`rust-toolchain.toml`](rust-toolchain.toml) (1.92.0). ```sh cargo build --workspace diff --git a/README.md b/README.md index f27b505..18f1f4f 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,9 @@ The image and video libraries each carry their own feature systems — restating them here would duplicate state that drifts. See each crate's README: - [`rawshift-image` feature flags](./crates/rawshift-image/README.md#feature-flags) - — a five-tier per-format system (bundles → formats → directions → - implementations → infrastructure) with selectable codec backends. + — a tiered per-format system (bundles → formats → directions → + infrastructure; gamut is the backend, plus six retained implementation + aliases for the permanent exceptions and blocked migrations). - [`rawshift-video` feature flags](./crates/rawshift-video/README.md#feature-flags) — mirrors the image tiers; currently gates no code (video is unimplemented). diff --git a/crates/rawshift-image/Cargo.toml b/crates/rawshift-image/Cargo.toml index 5f2eb10..0f727c7 100644 --- a/crates/rawshift-image/Cargo.toml +++ b/crates/rawshift-image/Cargo.toml @@ -115,19 +115,22 @@ tracing = { workspace = true, features = ["release_max_level_debug"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } [features] -# Feature flags are organised in five tiers, high-level to low-level. See the -# "Feature Flags" section of README.md for the full hierarchy and rationale. +# Feature flags — the consolidated post-migration tree. gamut is the backend: +# every gamut-backed direction feature pulls its `dep:gamut-*` directly, so +# the tier-4 implementation layer has collapsed to the six retained aliases +# documented in the tier-4 section below (permanent exceptions and blocked +# migrations only). See the "Feature Flags" section of README.md and the +# feature table in docs/V1_DESIGN.md. # # tier 1 bundles full, experimental, raw-stabilizing, … # tier 2 format jpeg, png, dng, … (decode + encode) -# tier 3 direction jpeg-decode, dng-encode, … (default impl alias) -# tier 4 implementation ppm-decode-zune, … (compressed only) -# tier 5 infrastructure ifd-parser, serde, zune-runtime, exif, … +# tier 3 direction jpeg-decode, dng-encode, … (pulls the backend) +# tier 4 implementation six retained aliases only (see section comment) +# tier 5 infrastructure ifd-parser, serde, exif, hw/hw-*, … # -# Only tier-4 features (plus RAW tier-3 features and the gamut-backed -# `jpeg-decode` / `jpeg-encode` / `png-decode` / `png-encode` / `jxl-decode` / -# `jxl-encode` / `avif-decode` / `avif-encode` / `heic-decode`, which have a -# single implementation) reference `dep:*`. +# `jxl-encode` is deliberately absent from `default`: it wraps the reference +# libjxl (cmake + C++ toolchain via gamut-jxl-sys), which a default build must +# not require. It is part of `jxl` and `full`. default = ["jpeg", "png", "webp", "jxl-decode", "gif-decode", "tiff-decode", "ppm-decode"] # ── Tier 1: bundle features ─────────────────────────────────────────────────── @@ -157,10 +160,13 @@ nef = ["nef-decode"] raf = ["raf-decode"] # ── Tier 3: direction features ──────────────────────────────────────────────── -# For compressed formats a direction feature is an alias for that -# format+direction's DEFAULT implementation (tier 4) — this is where the notion -# of a per-format default lives. RAW direction features reference infra/deps -# directly: RAW formats have a single in-repo implementation and no tier-4 layer. +# gamut-backed direction features pull their `dep:gamut-*` directly — gamut is +# the backend, so there is no implementation choice and no tier-4 layer. Only +# the six retained tier-4 aliases (webp/gif/tiff/svg/ppm — permanent +# exceptions or blocked migrations, see the tier-4 section) still route a +# direction feature through an implementation feature. RAW direction features +# reference infra/deps directly: RAW formats have a single in-repo +# implementation and no tier-4 layer. # JPEG is backed directly by gamut-jpeg (no tier-4 layer): like PNG # and the RAW direction features, each half has a single gamut-backed # implementation, so the format-direction feature itself pulls the dependency. @@ -231,14 +237,26 @@ dng-encode = ["dep:gamut-dng"] nef-decode = ["ifd-parser"] raf-decode = [] -# ── Tier 4: implementation features (compressed formats only) ───────────────── -# Named `format-direction-impl`. The only tier that pulls a `dep:*`. Each impl -# feature also enables its tier-3 direction feature, so it can be selected -# standalone. Multiple implementations of the same format+direction may be -# enabled at once; the active backend is chosen at the API level — see -# `DecodeOptions` and `EncodeOptions`. -# `zune-ppm` builds on the `zune-core` codec primitives, so its impl feature -# also pulls `zune-runtime`. +# ── Tier 4: implementation features (six retained aliases only) ─────────────── +# The gamut migrations collapsed this tier for every gamut-backed format — the +# direction features above pull their `dep:gamut-*` directly. The six flags +# below are the deliberate remainder; each names its gate: +# +# gif-decode-gif permanent exception (`gif`) — AGENTS.md upstream policy +# svg-decode-resvg permanent exception (`resvg`) — AGENTS.md upstream policy +# ppm-decode-zune permanent exception (`zune-ppm`) — AGENTS.md upstream +# policy; builds on `zune-core`, so also pulls +# `zune-runtime` +# tiff-decode-tiff blocked migration to gamut-tiff — gamut#299/#300, +# tracked by rawshift#22 (`blocked-upstream`) +# webp-decode-libwebp blocked migration to gamut-webp — gamut#302, +# tracked by rawshift#24 (`blocked-upstream`) +# webp-encode-libwebp blocked migration to gamut-webp — gamut#302, +# tracked by rawshift#24 (`blocked-upstream`) +# +# Each also enables its tier-3 direction feature, so it can be selected +# standalone. When a blocked migration unblocks, its alias dissolves into the +# direction feature exactly like the gamut-backed formats above. webp-decode-libwebp = ["webp-decode", "dep:libwebp-sys", "exif"] webp-encode-libwebp = ["webp-encode", "dep:libwebp-sys", "exif"] gif-decode-gif = ["gif-decode", "dep:gif"] diff --git a/crates/rawshift-image/README.md b/crates/rawshift-image/README.md index 592bd0b..78cbc35 100644 --- a/crates/rawshift-image/README.md +++ b/crates/rawshift-image/README.md @@ -14,41 +14,41 @@ or an explicit hardware-decode backend pin (`hw-*`). | Format | Decoding | Encoding | Notes | | ------------ | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------- | -| Sony ARW | Custom LJPEG (Stabilizing) | N/A | | -| Canon CR2 | Custom LJPEG (Incomplete) | N/A | No test fixtures. | -| Canon CR3 | Custom ISOBMFF parser (Incomplete) | N/A | Metadata only. CRX codec not implemented. | +| Sony ARW | [gamut-ifd](https://github.com/justin13888/gamut) structure + in-repo LJPEG (Stabilizing) | N/A | | +| Canon CR2 | [gamut-ifd](https://github.com/justin13888/gamut) structure + in-repo LJPEG (Incomplete) | N/A | No test fixtures. | +| Canon CR3 | In-repo ISOBMFF box walk + [gamut-ifd](https://github.com/justin13888/gamut) for embedded TIFF (Incomplete) | N/A | Metadata only. CRX codec not implemented. | | Canon CRW | Custom CIFF parser (Incomplete) | N/A | Detection only. No pixel decode. | -| Adobe DNG | [gamut-dng](https://github.com/justin13888/gamut) (Stabilizing) | Custom TIFF writer (Stabilizing) | Includes Apple ProRAW (DNG 1.7 + JXL). | -| Nikon NEF | Custom TIFF parser (Incomplete) | N/A | No test fixtures. | +| Adobe DNG | [gamut-dng](https://github.com/justin13888/gamut) (Stabilizing) | [gamut-dng](https://github.com/justin13888/gamut) `DngEncoder` (Stabilizing) | Includes Apple ProRAW (DNG 1.7 + JXL). | +| Nikon NEF | [gamut-ifd](https://github.com/justin13888/gamut) structure (Incomplete) | N/A | No test fixtures. | | Fujifilm RAF | Custom RAF parser (Incomplete) | N/A | No test fixtures. | | JPEG | [gamut-jpeg](https://github.com/justin13888/gamut) (Stable) | [gamut-jpeg](https://github.com/justin13888/gamut) (Stable) | Pure Rust. Decode: baseline + progressive, grayscale/YCbCr/RGB/CMYK/YCCK. Encode: baseline or progressive 8-bit DCT (quality/subsampling/restart/density); APP1/APP2 EXIF/XMP/ICC both ways. | | PNG | [gamut-png](https://github.com/justin13888/gamut) (Stable) | [gamut-png](https://github.com/justin13888/gamut) (Stable) | Pure Rust. Decode: every colour type/bit depth incl. Adam7, eXIf/iCCP/XMP extraction, resource guards. Encode: 8/16-bit RGB, eXIf/iCCP/XMP chunks. | -| WebP | [libwebp-sys](https://github.com/noxf/libwebp-sys) (Stable) | [libwebp-sys](https://github.com/noxf/libwebp-sys) (Stable) | C FFI bindings to libwebp. | -| GIF | [gif](https://github.com/image-rs/image-gif) (Stable) | Not planned | | -| TIFF | [tiff](https://github.com/image-rs/image-tiff) (Stable) | Not planned | | +| WebP | [libwebp-sys](https://github.com/noxf/libwebp-sys) (Stable) | [libwebp-sys](https://github.com/noxf/libwebp-sys) (Stable) | C FFI bindings to libwebp. gamut-webp migration blocked upstream ([gamut#302](https://github.com/justin13888/gamut/issues/302), tracked by [rawshift#24](https://github.com/justin13888/rawshift/issues/24)). | +| GIF | [gif](https://github.com/image-rs/image-gif) (Stable) | Not planned | Permanent exception to the gamut migration (AGENTS.md). | +| TIFF | [tiff](https://github.com/image-rs/image-tiff) (Stable) | Not planned | gamut-tiff migration blocked upstream ([gamut#299](https://github.com/justin13888/gamut/issues/299)/[#300](https://github.com/justin13888/gamut/issues/300), tracked by [rawshift#22](https://github.com/justin13888/rawshift/issues/22)). | | JXL | [gamut-jxl](https://github.com/justin13888/gamut) (Stable) | [gamut-jxl](https://github.com/justin13888/gamut) (Stable) | Decode is pure Rust (jxl-rs); encode wraps the reference libjxl, cmake-built and statically linked by gamut-jxl-sys. | | AVIF | [gamut-avif](https://github.com/justin13888/gamut) container/pipeline + [rawshift-hwdec](../rawshift-hwdec) hardware AV1 (Functional) | [gamut-avif](https://github.com/justin13888/gamut) (Functional) | Decode: container, metadata, and auxiliary enumeration always work; pixel decode needs a hardware AV1 backend (`hw`/`hw-*`, AV1 Profile 0) and reports `HwDecoderUnavailable` without one — software fallback is post-v1 ([gamut#259](https://github.com/justin13888/gamut/issues/259)); 10/12-bit presentation pending [gamut#303](https://github.com/justin13888/gamut/issues/303). Encode via gamut (pure Rust; 8-bit RGB, lossless/lossy AV1 intra, 4:4:4). 10/12-bit encode temporarily unavailable, pending [gamut#251](https://github.com/justin13888/gamut/issues/251). | | HEIC | [gamut-heic](https://github.com/justin13888/gamut) container/pipeline + [rawshift-hwdec](../rawshift-hwdec) hardware HEVC (Functional) | Not planned | Requires `heic` feature. Container, metadata, and auxiliary enumeration always work; pixel decode needs a hardware HEVC backend (`hw`/`hw-*`) and reports `HwDecoderUnavailable` without one. | -| SVG | [resvg/tiny-skia](https://github.com/linebender/resvg) (Functional) | Not planned | | -| PPM | [zune-ppm](https://github.com/etemesi254/zune-image/tree/dev/crates/zune-ppm) (Functional) | Not planned | Netpbm family: P5, P6, P7, PFM. | +| SVG | [resvg/tiny-skia](https://github.com/linebender/resvg) (Functional) | Not planned | Permanent exception to the gamut migration (AGENTS.md). | +| PPM | [zune-ppm](https://github.com/etemesi254/zune-image/tree/dev/crates/zune-ppm) (Functional) | Not planned | Netpbm family: P5, P6, P7, PFM. Permanent exception to the gamut migration (AGENTS.md). | Note on encoding support: for formats without encoding support, you may still take the decoded pixel data and metadata and encode it with your own logic. Note on implementations: the decoder/encoder library named for each compressed -format above is that format's **default implementation** — the one selected by -its direction feature flag (e.g. `jpeg-decode`). A format may gain alternative -implementations over time; see [Feature Flags](#feature-flags) for how -implementations are named and selected. +format above is that format's **only implementation** — the one selected by +its direction feature flag (e.g. `jpeg-decode`). gamut is the backend for +every migrated format; the exceptions are the retained tier-4 aliases listed +under [Feature Flags](#feature-flags). ## Feature Flags -Cargo features are organised in five tiers, from high-level bundles down to -individual library bindings. Each tier is defined purely in terms of the tier -below it; only tier-4 features (plus RAW tier-3 features and the gamut-backed -`jpeg-decode` / `jpeg-encode` / `png-decode` / `png-encode` / `jxl-decode` / -`jxl-encode` / `avif-decode` / `avif-encode` / `heic-decode`) pull in an -external crate. +Cargo features are organised in tiers, from high-level bundles down to +infrastructure flags. gamut is the backend: every gamut-backed direction +feature pulls its `gamut-*` crate directly, so the tier-4 implementation +layer has collapsed to six retained aliases — the permanent exceptions +(GIF/SVG/PPM) and the blocked migrations (TIFF/WebP) listed under tier 4 +below. 1. **Bundle features** — coarse, ready-made groupings. - `default` — `jpeg`, `png`, `webp`, `jxl-decode`, `gif-decode`, `tiff-decode`, `ppm-decode`. @@ -62,39 +62,39 @@ external crate. - `gif`, `tiff`, `heic`, `svg`, `ppm` — decode-only - `arw`, `cr2`, `cr3`, `crw`, `nef`, `raf` — RAW, decode-only 3. **Direction features** — one per format per direction. - - Compressed formats: `jpeg-decode`, `jpeg-encode`, `png-decode`, `png-encode`, - `webp-decode`, `webp-encode`, `jxl-decode`, `jxl-encode`, `gif-decode`, - `tiff-decode`, `avif-decode`, `avif-encode`, `heic-decode`, `svg-decode`, - `ppm-decode` — each is an **alias for that format+direction's default - implementation**. - This is where the per-format default is defined. Exception: `jpeg-decode`, - `jpeg-encode`, `png-decode`, `png-encode`, `jxl-decode`, `jxl-encode`, - `avif-decode`, `avif-encode`, and `heic-decode` each have a single - gamut-backed implementation + - gamut-backed formats: `jpeg-decode`, `jpeg-encode`, `png-decode`, + `png-encode`, `jxl-decode`, `jxl-encode`, `avif-decode`, `avif-encode`, + `heic-decode` — each has a single gamut-backed implementation (`gamut-jpeg` / `gamut-png` / `gamut-jxl` / `gamut-avif` / `gamut-heic`) - and pull it directly, with no tier-4 layer - below them. (`jxl-encode` wraps the - reference libjxl, which `gamut-jxl-sys` cmake-builds and links statically - — it needs cmake and a C++ toolchain. `avif-encode` is pure Rust: 8-bit - RGB, lossless or lossy AV1 intra; 10/12-bit AVIF encode is temporarily - unavailable, pending + and pulls it directly, with no tier-4 layer below it. (`jxl-encode` wraps + the reference libjxl, which `gamut-jxl-sys` cmake-builds and links + statically — it needs cmake and a C++ toolchain. `avif-encode` is pure + Rust: 8-bit RGB, lossless or lossy AV1 intra; 10/12-bit AVIF encode is + temporarily unavailable, pending [gamut#251](https://github.com/justin13888/gamut/issues/251). `avif-decode` is container/metadata pure Rust; its pixel decode needs a hardware AV1 backend — see the `hw` flags under tier 5.) + - Exception/blocked formats: `webp-decode`, `webp-encode`, `gif-decode`, + `tiff-decode`, `svg-decode`, `ppm-decode` — each is an alias for its + retained tier-4 implementation feature (see tier 4). - RAW formats: `arw-decode`, `cr2-decode`, `cr3-decode`, `crw-decode`, `dng-decode`, `dng-encode`, `nef-decode`, `raf-decode` — RAW formats have a single in-repo implementation, so there is no tier-4 layer below them. -4. **Implementation features** — *compressed formats only*, named - `format-direction-impl`. Each selects exactly one backend library and is the - only tier that pulls an external crate. Multiple implementations of the same - format+direction may be enabled simultaneously; the active backend is chosen - at the API level via `DecodeOptions` / `EncodeOptions`. - - `webp-decode-libwebp`, `webp-encode-libwebp` - - `gif-decode-gif`, `tiff-decode-tiff` - - `svg-decode-resvg` - - `ppm-decode-zune` +4. **Implementation features** — six retained aliases, named + `format-direction-impl`. The gamut migrations collapsed this tier for every + gamut-backed format; these remain, each for a named reason: + - `gif-decode-gif`, `svg-decode-resvg`, `ppm-decode-zune` — permanent + exceptions to the gamut migration (AGENTS.md upstream-first policy). + - `tiff-decode-tiff` — gamut-tiff migration blocked upstream + ([gamut#299](https://github.com/justin13888/gamut/issues/299)/[#300](https://github.com/justin13888/gamut/issues/300), + tracked by [rawshift#22](https://github.com/justin13888/rawshift/issues/22)). + - `webp-decode-libwebp`, `webp-encode-libwebp` — gamut-webp migration + blocked upstream + ([gamut#302](https://github.com/justin13888/gamut/issues/302), tracked by + [rawshift#24](https://github.com/justin13888/rawshift/issues/24)). 5. **Infrastructure / linking features** — cross-cutting, not tied to one format. - - `tiff-parser` — internal TIFF structure parser plus the public `TiffParser` API. + - `ifd-parser` — the gamut-ifd TIFF/IFD structure engine used by the + TIFF-based RAW decoders and format detection. - `serde` — `Serialize`/`Deserialize` for metadata and option types. - `zune-runtime` — `zune-core` codec primitives; pulled by zune-backed impls. - `exif` — typed EXIF read/write via the gamut metadata stack (`gamut-exif`, @@ -115,10 +115,10 @@ external crate. `rawshift-image` build links no decoder/metadata crate it does not use. Resolution example: enabling `default` pulls in `ppm` → `ppm-decode` → -`ppm-decode-zune` → the `zune-ppm` crate. To use a non-default implementation, -enable its tier-4 feature explicitly and select it per call through -`DecodeOptions` / `EncodeOptions`; the default implementation stays available -alongside it. +`ppm-decode-zune` → the `zune-ppm` crate. Every format+direction currently has +exactly one implementation (gamut for migrated formats, the retained alias's +backend otherwise), selected per call through `DecodeOptions` / +`EncodeOptions`. ## License diff --git a/crates/rawshift-image/src/formats/dng_export.rs b/crates/rawshift-image/src/formats/dng_export.rs index 2b0a4b9..8f3c766 100644 --- a/crates/rawshift-image/src/formats/dng_export.rs +++ b/crates/rawshift-image/src/formats/dng_export.rs @@ -65,7 +65,8 @@ pub fn export_dng_to_writer( /// Export an RGB image as a demosaiced linear DNG file. /// -/// Thin wrapper over [`export_dng_to_writer`] that creates the file at `path`. +/// Thin wrapper over `export_dng_to_writer` (crate-internal) that creates the +/// file at `path`. pub fn export_dng( path: &Path, image: &RgbImage, diff --git a/crates/rawshift-image/src/formats/standard.rs b/crates/rawshift-image/src/formats/standard.rs index bf65a83..3265ae7 100644 --- a/crates/rawshift-image/src/formats/standard.rs +++ b/crates/rawshift-image/src/formats/standard.rs @@ -176,8 +176,8 @@ impl std::fmt::Display for StandardFormat { /// Detect a standard image format from the first bytes of image data. /// /// Note: TIFF-based RAW formats (DNG, ARW, NEF, CR2) share the TIFF magic -/// bytes and will be detected as `StandardFormat::Tiff`. Use [`RawFile::open()`] -/// to distinguish RAW formats first. +/// bytes and will be detected as `StandardFormat::Tiff`. Use `RawFile::open()` +/// (available with any RAW format feature) to distinguish RAW formats first. /// /// Returns `None` if the format is not recognised or if `data` is too short. pub fn detect_standard_format(data: &[u8]) -> Option { @@ -1034,8 +1034,8 @@ impl DecodeOptions { /// order. 8-bit source images are scaled to 16-bit by multiplying by 257. /// /// # Errors -/// Returns [`RawError::ImageDecodeError`] on decode failure, or -/// [`RawError::UnsupportedFormat`] for formats without a decoder. +/// Returns the failing decoder's error (e.g. [`RawError::Format`]) on decode +/// failure, or [`RawError::Unsupported`] for formats without a decoder. pub fn decode_standard_image(data: &[u8], format: StandardFormat) -> RawResult { let decoded: RawResult = match format { #[cfg(feature = "gif-decode")] @@ -1510,7 +1510,8 @@ mod probe_tests { /// Extract EXIF metadata from a standard image without decoding pixel data. /// /// Reads embedded EXIF from image file bytes and maps the tags to the unified -/// [`ImageMetadata`] type. Returns a default (empty) [`ImageMetadata`] when +/// [`ImageMetadata`](crate::core::metadata::ImageMetadata) type. Returns a +/// default (empty) `ImageMetadata` when /// the format carries no EXIF or when the format is not supported for metadata /// extraction (e.g. GIF, SVG, APV). /// diff --git a/crates/rawshift-image/src/transforms/opcodes.rs b/crates/rawshift-image/src/transforms/opcodes.rs index 703a9cf..9025929 100644 --- a/crates/rawshift-image/src/transforms/opcodes.rs +++ b/crates/rawshift-image/src/transforms/opcodes.rs @@ -61,7 +61,7 @@ pub struct GainMap { pub map_origin_h: f64, /// Number of planes stored in the map data (1 = shared, 3 = per-channel) pub map_planes: u32, - /// Gain values, stored in [v][h][plane] order (row-major, f32 each) + /// Gain values, stored in `[v][h][plane]` order (row-major, f32 each) pub gain: Vec, } diff --git a/crates/rawshift-image/src/transforms/simd.rs b/crates/rawshift-image/src/transforms/simd.rs index 5f315bd..ffb6f4e 100644 --- a/crates/rawshift-image/src/transforms/simd.rs +++ b/crates/rawshift-image/src/transforms/simd.rs @@ -29,7 +29,8 @@ /// # Panics /// /// Does not panic. Any trailing samples that do not form a complete RGB -/// triplet are silently ignored (identical behaviour to [`chunks_exact`]). +/// triplet are silently ignored (identical behaviour to +/// [`chunks_exact`](slice::chunks_exact)). /// /// # Performance /// diff --git a/crates/rawshift/Cargo.toml b/crates/rawshift/Cargo.toml index aa80bbf..ffe7b9b 100644 --- a/crates/rawshift/Cargo.toml +++ b/crates/rawshift/Cargo.toml @@ -42,6 +42,9 @@ hw = ["rawshift-image?/hw"] hw-videotoolbox = ["rawshift-image?/hw-videotoolbox"] hw-vaapi = ["rawshift-image?/hw-vaapi"] hw-mediacodec = ["rawshift-image?/hw-mediacodec"] -# Everything: every image format, serde, and hardware decode. v1 is image-only -# — there is no `video` feature until `rawshift-video` has an implementation. -full = ["image", "rawshift-image/full", "serde"] +# Everything: every image format, serde, and hardware decode (`hw` is listed +# explicitly so the facade's own flag is active, matching the design contract +# `full` = all formats + serde + experimental + `hw`; `rawshift-image/full` +# already enables it transitively). v1 is image-only — there is no `video` +# feature until `rawshift-video` has an implementation. +full = ["image", "rawshift-image/full", "serde", "hw"] diff --git a/docs/V1_DESIGN.md b/docs/V1_DESIGN.md index 39756b3..5b7dcd8 100644 --- a/docs/V1_DESIGN.md +++ b/docs/V1_DESIGN.md @@ -6,8 +6,9 @@ upstream-first for every gap (see the policy in [AGENTS.md](../AGENTS.md)), hardware codestream decode for HEVC/AV1, and no video. **v1 is the clean final state: 0.x source compatibility is a non-goal.** -gamut is consumed as a git dependency pinned to commit -`295c89248b9130873fe40c99529a99769c590f98`. +gamut is consumed as a git dependency pinned to an exact commit hash in the +workspace `Cargo.toml` (see README "Bumping the gamut pin" for the +procedure); this document does not restate the hash. ## Workspace layout @@ -104,9 +105,9 @@ BitDepth (Sixteen where supported) }`. | --- | --- | --- | --- | | JPEG | ✅ | ✅ | gamut JPEG (upstream #28) | | PNG | ✅ | ✅ | gamut-png (decoder upstream) | -| WebP | ✅ | ✅ | gamut-webp | +| WebP | ✅ | ✅ | libwebp (`libwebp-sys`) — gamut-webp migration blocked upstream (gamut#302, tracked by rawshift#24) | | JXL | ✅ (pure Rust) | ✅ (libjxl via gamut-jxl-sys) | gamut-jxl | -| TIFF | ✅ | ✅ (new) | gamut-tiff | +| TIFF | ✅ | — | `tiff` crate — gamut-tiff migration (incl. new encode) blocked upstream (gamut#299/#300, tracked by rawshift#22) | | AVIF | ✅ hardware | ✅ (Rgb8 lossless now; 10/12-bit upstream) | gamut-avif container + rawshift-hwdec AV1 | | HEIC | ✅ hardware | — | gamut-heic container + rawshift-hwdec HEVC | | DNG | ✅ | ✅ | gamut-dng | @@ -116,7 +117,9 @@ BitDepth (Sixteen where supported) }`. ## Feature flags & compile boundaries -Defaults: `jpeg, png, webp, jxl, tiff, gif, ppm`. Formats compose +Defaults: `jpeg, png, webp, jxl-decode, gif, tiff, ppm` — `jxl-encode` is +excluded from defaults because it wraps the reference libjxl (cmake + C++ +toolchain via gamut-jxl-sys); it is part of `jxl` and `full`. Formats compose `-decode`/`-encode`. Bundles: `raw-stabilizing` (arw, dng), `raw-incomplete` (cr2, cr3, crw, nef, raf), `experimental`, `serde`, and `full` = all formats + serde + experimental + `hw`. @@ -133,9 +136,16 @@ Hardware backends are **verified feature flags**: (valid; pixel decode returns `HwDecoderUnavailable`). CI compiles the invalid combinations expecting failure and `full` on every -tier-1 target expecting success. Deleted feature axes: all per-implementation -flags, `zune-runtime`, `exif`, `container-embed`, `tiff-parser`, -`heic-vendored`, every `*-vendored` linking flag. +tier-1 target expecting success. Deleted feature axes: the per-implementation +flags of every gamut-backed format, `container-embed`, `tiff-parser` +(replaced by `ifd-parser` over gamut-ifd), `heic-vendored`, every +`*-vendored` linking flag. Retained (delivered reality, post-#34 audit): +six implementation aliases — `gif-decode-gif` / `svg-decode-resvg` / +`ppm-decode-zune` (permanent exceptions per AGENTS.md) and +`tiff-decode-tiff` / `webp-decode-libwebp` / `webp-encode-libwebp` (blocked +migrations: gamut#299/#300 via rawshift#22, gamut#302 via rawshift#24) — +plus the `zune-runtime` and `exif` infrastructure flags they and the +gamut metadata stack hang off. ## Hardware decode (rawshift-hwdec) @@ -174,12 +184,17 @@ the current parser's guarantees. ## Dependencies deleted -zune-jpeg, zune-png, zune-jpegxl, zune-core, jpeg-encoder, vendored jpegli -(+submodule/cc/cmake/bindgen), libwebp-sys, jxl-oxide, direct libjxl glue, -ravif, avif-serialize, libaom-sys, image, libheif-rs, tiff, little_exif, +zune-jpeg, zune-png, zune-jpegxl, jpeg-encoder, vendored jpegli +(+submodule/cc/cmake/bindgen), jxl-oxide, direct libjxl glue, +ravif, avif-serialize, libaom-sys, image, libheif-rs, little_exif, img-parts, binrw. `build.rs` shrinks to cfg aliases + feature/target verification. +Still present, pending blocked upstream migrations (post-#34 audit): +`libwebp-sys` (gamut-webp — gamut#302 via rawshift#24) and `tiff` +(gamut-tiff — gamut#299/#300 via rawshift#22). Permanent exceptions that +stay: `gif`, `resvg`, `zune-ppm` (+ its `zune-core` runtime). + ## Release git-pinned gamut means rawshift cannot publish to crates.io until gamut diff --git a/justfile b/justfile index 6eeee52..6909015 100644 --- a/justfile +++ b/justfile @@ -31,9 +31,10 @@ build-features features: build-image: cargo build -p rawshift --no-default-features --features image -# Build the video-only facade — verifies it pulls zero image crates +# Build the parked video crate directly — the facade has no `video` feature +# while rawshift-video is parked (see crates/rawshift/Cargo.toml) build-video: - cargo build -p rawshift --no-default-features --features video + cargo build -p rawshift-video --all-features # Run tests for the whole workspace (default features) — fetches fixtures first test: setup-test-data @@ -43,26 +44,31 @@ test: setup-test-data test-features features: cargo test -p rawshift-image --no-default-features --features "{{features}}" -# Run all workspace tests with all features +# Run all workspace tests with the full feature set. NOT --all-features: the +# explicit hw backend pins (hw-videotoolbox / hw-vaapi / hw-mediacodec) are +# mutually exclusive verified flags — enabling them together compile_error!s +# on every target by design. test-all: - cargo test --workspace --all-features + cargo test --workspace --features rawshift-image/full # Generate docs for the whole workspace doc: cargo doc --workspace --no-deps --open -# Check docs build (no open) +# Check docs build (no open). `rawshift-image/full`, not --all-features: the +# hw backend pins are mutually exclusive by design (compile_error!). doc-check: - cargo doc --workspace --no-deps --all-features + cargo doc --workspace --no-deps --features rawshift-image/full # Run doc tests doc-test: - cargo test --workspace --doc --all-features + cargo test --workspace --doc --features rawshift-image/full -# Pre-publish checks +# Pre-publish checks (`rawshift-image/full`, not --all-features: the hw +# backend pins are mutually exclusive by design) publish-check: - cargo clippy --workspace --all-targets --all-features -- -D warnings - cargo test --workspace --all-features + cargo clippy --workspace --all-targets --features rawshift-image/full -- -D warnings + cargo test --workspace --features rawshift-image/full cargo doc --workspace --no-deps cargo publish --dry-run -p rawshift-core cargo publish --dry-run -p rawshift-image @@ -90,4 +96,4 @@ coverage-report: # Run all fixture-based integration tests (fetches fixtures first) test-fixtures: setup-test-data - cargo test -p rawshift-image --features=full --test raw_decode_fixtures --test standard_decode_fixtures --test tiff_parser_tests --test dng_check + cargo test -p rawshift-image --features=full --test raw_decode_fixtures --test standard_decode_fixtures --test dng_check