From 47f9306fd06d5dee11acb84b108606f4c867222a Mon Sep 17 00:00:00 2001 From: ljkx <177671075+ljkx@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:20:40 +0200 Subject: [PATCH] Open HEIC/HEIF photos (#355) (#372) * Open HEIC/HEIF photos (#355) iPhones save HEIC by default and Photoshop opens it; PhotoCraft had no HEIF code, so .heic was missing from File > Open and failed to decode. Add Format::Heif to photocraft-codecs, decoded by heic-rs (pure Rust, forbid(unsafe), MIT OR Apache-2.0, wasm-ready): single pictures and grid-tiled photos, 8-bit to U8 and 10/12-bit to U16, auxiliary alpha, ICC, EXIF and XMP. The container's irot/imir/clap are applied and EXIF Orientation is rewritten to 1, so photos are never turned twice. Limits are checked against the declared size before decoding. Read-only (no pure-Rust HEVC encoder), listed in ASYMMETRIC_EXCEPTIONS. File > Open (desktop, web) and the batch list accept heic/heif/hif. Tests compare against Apple's own decode and a 16-bit source, cover orientation, metadata, limits and every truncation; fixtures from heic-rs and pillow-heif are attributed in ATTRIBUTION.md. Co-Authored-By: Claude Opus 5.5 * HEIF: pin heic-rs exactly and add a decode_heif fuzz target Review follow-up: heic-rs is young and parses untrusted files, so pin it (=0.1.1) and fuzz the HEIF path. The target covers both orientation paths and is meant to run with -timeout so a hang is reported, not only a panic. It is added to the CI fuzz matrix. Co-Authored-By: Claude Opus 5.5 * HEIF: a heic-rs panic is a malformed-file error, not a crash Fuzzing decode_heif found two out-of-range slices in heic-rs 0.1.1 on malformed files: - in its box parser (boxes.rs:130), reachable from a 72-byte file, even through the metadata-only probe; - during HEVC reconstruction (hevc/decode/recon.rs:70). The background job path catches panics, but the synchronous ones (jobs off, the CLI) did not. The HEIF codec now runs the decoder under catch_unwind and reports a panic as CodecError::Malformed, as gpu/health.rs already does around wgpu. The fuzz target also calls heic-rs directly, so its panics still reach the fuzzer. The 72-byte input is a regression test, which fails without the guard. Co-Authored-By: Claude Opus 5.5 * HEIF: move the decoder into an optional photocraft-heif crate The heic-rs decoding from #372 now lives in `crates/heif` (photocraft-heif, standalone, depends only on heic-rs =0.1.1): probe, decode with a pixel limit, container transforms, alpha, 8/10-bit, ICC/EXIF/XMP and the catch_unwind guard, behind a small API of its own (Info, Options, Decoded, Error). photocraft-codecs uses it behind a new non-default `heif` feature; the apps and photocraft-io forward it. HEIF detection, extensions and mime stay compiled in, so without the feature a HEIC file is still recognised and opening it is a clear "HEIC/HEIF support isn't included in this build of PhotoCraft" error. `cargo xtask layers` allows exactly codecs -> heif between standalone crates. The fixtures are no longer committed: `cargo xtask corpus --heif` fetches them from heic-rs (tests/fixtures, v0.1.1) and pillow-heif (tests/images) at pinned commits, verified against xtask/heif-corpus.sha256 (same bytes as the PR's copies). The fixture tests run with `--features corpus,heif` (`cargo xtask test-corpus` enables both). Co-Authored-By: Claude Opus 5.5 * Build CI and official binaries with HEIC support (--features heif) CI test and clippy, the FreeBSD job, and the macOS, Linux (and Flatpak), FreeBSD, Windows and web packaging all build with the `heif` feature; CI also tests codecs/io without it on Linux so the "not in this build" path stays covered. `cargo xtask wasm` also checks photocraft-codecs with `--features heif`. Co-Authored-By: Claude Opus 5.5 * Docs: HEIC open is an optional build feature, on in official builds Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- .github/workflows/ci.yml | 14 +- .github/workflows/freebsd.yml | 4 +- ATTRIBUTION.md | 1 + Cargo.lock | 17 ++ README.md | 2 +- apps/photocraft-cli/Cargo.toml | 5 + apps/photocraft-web/Cargo.toml | 5 + apps/photocraft-web/index.html | 2 +- apps/photocraft-web/src/web.rs | 2 +- apps/photocraft/Cargo.toml | 5 + apps/photocraft/src/services.rs | 2 +- book/src/formats/raster-formats.md | 2 +- crates/codecs/Cargo.toml | 5 + crates/codecs/README.md | 13 +- crates/codecs/fuzz/Cargo.toml | 11 +- .../codecs/fuzz/fuzz_targets/decode_heif.rs | 15 ++ crates/codecs/src/codecs/heif.rs | 103 +++++++++ crates/codecs/src/codecs/mod.rs | 1 + crates/codecs/src/format.rs | 56 ++++- crates/codecs/src/lib.rs | 11 +- crates/codecs/tests/detect_caps.rs | 23 ++ crates/codecs/tests/heif.rs | 194 ++++++++++++++++ crates/codecs/tests/malformed.rs | 15 +- crates/engine/src/file_cmds.rs | 4 +- crates/heif/Cargo.toml | 19 ++ crates/heif/README.md | 34 +++ crates/heif/src/lib.rs | 218 ++++++++++++++++++ crates/io/Cargo.toml | 2 + crates/io/tests/heif.rs | 65 ++++++ crates/io/tests/heif_unsupported.rs | 15 ++ docs/architecture.md | 9 +- docs/development.md | 5 +- docs/scorecard.md | 4 +- packaging/freebsd/package.sh | 2 +- .../flatpak/ai.storyteller.photocraft.yml | 2 +- packaging/linux/package.sh | 2 +- packaging/macos/package.sh | 2 +- packaging/windows/package.ps1 | 2 +- xtask/heif-corpus.sha256 | 9 + xtask/src/corpus.rs | 25 +- xtask/src/corpus_pins.rs | 44 +++- xtask/src/layers.rs | 29 +++ xtask/src/main.rs | 10 + 43 files changed, 960 insertions(+), 50 deletions(-) create mode 100644 crates/codecs/fuzz/fuzz_targets/decode_heif.rs create mode 100644 crates/codecs/src/codecs/heif.rs create mode 100644 crates/codecs/tests/heif.rs create mode 100644 crates/heif/Cargo.toml create mode 100644 crates/heif/README.md create mode 100644 crates/heif/src/lib.rs create mode 100644 crates/io/tests/heif.rs create mode 100644 crates/io/tests/heif_unsupported.rs create mode 100644 xtask/heif-corpus.sha256 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 246944ad1c..172ca5104b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,8 +51,15 @@ jobs: if: runner.os == 'Linux' run: cargo fmt --all -- --check + # `--features heif`: official builds open HEIC photos via the optional photocraft-heif crate. - name: Test - run: cargo test --workspace --locked + run: cargo test --workspace --locked --features heif + + # The same codecs without the optional decoder: HEIC must stay a clear "not in this build" + # error for distributors who leave it out. + - name: Test without heif + if: runner.os == 'Linux' + run: cargo test -p photocraft-heif -p photocraft-codecs -p photocraft-io --locked # The same tests built with the optional craft-fonts input (crates/text/build.rs), so the # Japanese font tests that skip without it run here. Linux only. @@ -69,10 +76,10 @@ jobs: env: CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts CRAFT_FONTS_REQUIRED: "1" - run: cargo test --workspace --locked + run: cargo test --workspace --locked --features heif - name: Clippy - run: cargo clippy --workspace --all-targets --locked -- -D warnings + run: cargo clippy --workspace --all-targets --locked --features heif -- -D warnings - name: Dependency layering run: cargo xtask layers @@ -136,6 +143,7 @@ jobs: - { crate: codecs, name: decode_jpeg } - { crate: codecs, name: decode_tiff } - { crate: codecs, name: decode_exr } + - { crate: codecs, name: decode_heif } - { crate: format, name: load } steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index a5b7f0a216..4e12adb6fa 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -51,8 +51,8 @@ jobs: # The checkout is synced in under another owner; let tests that call git read it. git config --global --add safe.directory "*" rustc --version - CARGO_PROFILE_DEV_DEBUG=0 cargo build --workspace --all-targets --locked - CARGO_PROFILE_DEV_DEBUG=0 cargo test --workspace --locked --exclude photocraft-ui-egui + CARGO_PROFILE_DEV_DEBUG=0 cargo build --workspace --all-targets --locked --features heif + CARGO_PROFILE_DEV_DEBUG=0 cargo test --workspace --locked --features heif --exclude photocraft-ui-egui CARGO_PROFILE_DEV_DEBUG=0 cargo test -p photocraft-ui-egui --locked --lib CARGO_PROFILE_DEV_DEBUG=0 cargo test -p photocraft-ui-egui --locked --doc # The VM's software Vulkan driver crashes (SIGSEGV) when several tests set up a GPU diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 98b4f39b8a..f055069b5b 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -73,6 +73,7 @@ next to the files: | `corpus/photoshop/` (256 PSDs) | Photoshop oracle corpus: smart filters, layer-style effects, type, adjustments in every mode and depth | PhotoCraft contributors (authored with Adobe Photoshop 2026 by a script) | [https://github.com/storytold/photocraft-corpus](https://github.com/storytold/photocraft-corpus) (`photoshop/`, with its generator and README) | MIT OR Apache-2.0 | | `corpus/psd-tools/` (309 files) | psd-tools test set | Kota Yamaguchi and contributors | [psd-tools `tests/psd_files`](https://github.com/psd-tools/psd-tools/tree/main/tests/psd_files) | MIT, Copyright (c) 2019 Kota Yamaguchi | | `corpus/psd/` (170 files) | Small selection of the psd-tools and ag-psd test files | Kota Yamaguchi; Agamnentzar | psd-tools (above) and [ag-psd `test/`](https://github.com/Agamnentzar/ag-psd/tree/master/test) | MIT (both) | +| `corpus/heif/` (9 files, 0.1 MB) | HEIC/HEIF test files: `heic-rs/` checkerboards, RGB strips and a grid-tiled photo with EXIF and XMP (synthetic pixels encoded by macOS `sips`, each `.ref.png` Apple's decode); `pillow-heif/` the 10-bit RGBA `RGBA_10__29x100.heif` and its source `RGBA_16__29x100.png` | Thomas Braun (heic-rs); Pillow-Heif contributors | [heic-rs `tests/fixtures`](https://github.com/tbraun96/heic-rs/tree/main/tests/fixtures), [pillow-heif `tests/images`](https://github.com/bigcat88/pillow_heif/tree/master/tests/images) | MIT OR Apache-2.0 (heic-rs); BSD-3-Clause (pillow-heif) | | `corpus/pngsuite/` | PngSuite | Willem van Schaik | | Public domain | Files copied into `corpus/` by hand (tiff, exr, raw) must be MIT, BSD or CC0. diff --git a/Cargo.lock b/Cargo.lock index d7e2486b0a..61a12e9034 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2030,6 +2030,15 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "heic-rs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf9dde634bb6d5b718bacd01bef80192f2334306b2f785519a0136cca7b9618" +dependencies = [ + "rayon", +] + [[package]] name = "hermit-abi" version = "0.5.3" @@ -3503,6 +3512,7 @@ dependencies = [ "image", "image-webp", "jpeg-encoder", + "photocraft-heif", "png 0.18.1", "proptest", "rayon", @@ -3618,6 +3628,13 @@ dependencies = [ "wgpu", ] +[[package]] +name = "photocraft-heif" +version = "0.3.0" +dependencies = [ + "heic-rs", +] + [[package]] name = "photocraft-io" version = "0.3.0" diff --git a/README.md b/README.md index 1c0049c6e9..7d4cc153b2 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ Every screenshot here is the real app at work on public-domain art, rendered off

πŸ—‚οΈ Formats

- PSD and PSB, plus PNG, JPEG, TIFF, WebP, GIF, BMP, TGA, ICO, QOI, PNM, OpenEXR, Radiance HDR and AVIF, with symmetric read and write at 8, 16 and 32 bits, and the native .pcraft format. + PSD and PSB, plus PNG, JPEG, TIFF, WebP, GIF, BMP, TGA, ICO, QOI, PNM, OpenEXR, Radiance HDR and AVIF, with symmetric read and write at 8, 16 and 32 bits, HEIC photos from iPhone and Mac (read; in official builds, an optional --features heif build feature), and the native .pcraft format.

πŸͺ„ The everyday essentials

diff --git a/apps/photocraft-cli/Cargo.toml b/apps/photocraft-cli/Cargo.toml index 8e07dcebb3..8e85e6f9c3 100644 --- a/apps/photocraft-cli/Cargo.toml +++ b/apps/photocraft-cli/Cargo.toml @@ -5,6 +5,11 @@ version.workspace = true edition.workspace = true license.workspace = true +[features] +# Open HEIC/HEIF photos (the optional photocraft-heif crate). Not default so distributors choose +# it; official builds (CI, packaging/, release) build with `--features heif`. +heif = ["photocraft-codecs/heif"] + [lints] workspace = true diff --git a/apps/photocraft-web/Cargo.toml b/apps/photocraft-web/Cargo.toml index f6ea819352..be1f764fa8 100644 --- a/apps/photocraft-web/Cargo.toml +++ b/apps/photocraft-web/Cargo.toml @@ -6,6 +6,11 @@ edition.workspace = true license.workspace = true publish = false +[features] +# Open HEIC/HEIF photos (the optional photocraft-heif crate). Not default so distributors choose +# it; official builds (CI, packaging/, release) build with `--features heif`. +heif = ["photocraft-codecs/heif"] + [lints] workspace = true diff --git a/apps/photocraft-web/index.html b/apps/photocraft-web/index.html index b9a19fb791..5e734006be 100644 --- a/apps/photocraft-web/index.html +++ b/apps/photocraft-web/index.html @@ -4,7 +4,7 @@ PhotoCraft - +