diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index e9120eb91..3cab26695 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -86,3 +86,6 @@ and the exact transfer revision are recorded in each vendored crate's SOURCE.jso | `crates/psd/tests/fixtures/atn/*.atn` (seven synthetic action sets, encoded field by field; `crates/psd/tests/fixtures/atn/README.md`) | PhotoCraft contributors | [PhotoCraft `crates/psd/tests/fixtures/atn`](https://github.com/storytold/photocraft/tree/63b641b55e54bf8f5c83efd977fdb16a2c255431/crates/psd/tests/fixtures/atn) | MIT OR Apache-2.0 | | `crates/lottie/tests/fixtures/` | EffectCraft contributors (original work: four hand-written Lottie JSON documents, written from the public Lottie format description; not exported from any Adobe product or third-party file) | [EffectCraft `crates/lottie/tests/fixtures`](https://github.com/storytold/effectcraft/tree/e18f48fb/crates/lottie/tests/fixtures) | MIT OR Apache-2.0 | | `crates/affinity/tests/support/corpus.tsv`, `crates/affinity/tests/support/affinity3-signatures.txt` (manifest and measured signatures only; the 51 documents are not bundled, `scripts/fetch-affinity-corpus.sh` downloads them outside the checkout) | The authors of each document: samuel-etver (vector-art), Seth A. Robinson (Patchy fixtures), Nick Beeuwsaert (AFDesignLoad), Jeremy Cantu (Branding), eviltwo (AssetStoreTemplate), Square (leakcanary) | each file's pinned upstream URL and SHA-256 in the manifest; list carried over from [VectorCraft `xtask/src/affinity_corpus.rs`](https://github.com/storytold/vectorcraft/blob/6e873b806c9d5b3f2b55f651df57bd97933b579a/xtask/src/affinity_corpus.rs) | CC0-1.0, MIT or Apache-2.0 (per file, in the manifest) | +| `crates/raw/docs/coverage/adobe-cameras.tsv` | Facts (maker, model, extension, profile, minimum Camera Raw version) parsed by LightCraft contributors from Adobe's published list | [Adobe Camera Raw supported cameras](https://helpx.adobe.com/camera-raw/desktop/dng-and-file-formats/camera-raw-plug-supported-cameras.html), Wayback capture 20261003133011; moved from [LightCraft `docs/raw`](https://github.com/storytold/lightcraft/tree/5ff567a4a2e067bef1b6bce152c11e7f727ccbaf/docs/raw) | Facts, not copyrightable expression; the file is MIT OR Apache-2.0 | +| `crates/raw/docs/coverage/{coverage,coverage-sweep,evidence,usage-weights}.tsv`, `coverage.py` | LightCraft contributors | [LightCraft `docs/raw`](https://github.com/storytold/lightcraft/tree/5ff567a4a2e067bef1b6bce152c11e7f727ccbaf/docs/raw) | MIT OR Apache-2.0 | +| `crates/raw/docs/raf-corpus.sha256` (hashes only; the files are not bundled) | The photographers who contributed each file to raw.pixls.us | [raw.pixls.us](https://raw.pixls.us/) | CC0-1.0 | diff --git a/Cargo.lock b/Cargo.lock index eaed45635..936a2f595 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1781,6 +1781,7 @@ version = "0.1.0" dependencies = [ "chrono", "craft-crw", + "craft-metadata", "craft-tiff", "jpeg-encoder 0.7.1", "jxl-oxide", diff --git a/NOTICE b/NOTICE index 91123023b..abb651318 100644 --- a/NOTICE +++ b/NOTICE @@ -93,7 +93,7 @@ crates/raw (craft-raw) is moved from LightCraft, with behaviour ported from two - LightCraft, crates/raw (src/lib.rs, dng.rs, dngwrite.rs, tiffraw.rs, unpack.rs, ljpeg.rs, jxl.rs, opcodes.rs, profile.rs, gaintable.rs, semantic.rs, preview.rs, vendor/crw.rs, vendor/hasselblad.rs, vendor.rs (shared level helpers), vendor/mrw.rs, vendor/erf.rs, vendor/mef.rs, vendor/kodak.rs, vendor/pef.rs, - vendor/srw.rs, vendor/cr2.rs, vendor/cr2/sraw.rs, vendor/nef.rs, vendor/nefc.rs, vendor/nef_lens.rs, docs/nikon-lens-corrections.md, vendor/rw2.rs, vendor/raf.rs, vendor/rafc.rs, vendor/raf_lens.rs, docs/raf-compression.md, docs/raf-corpus.sha256, vendor/orf.rs, docs/orf-compression.md, kelvin_presets.rs (the preset ordering), tests/it/vendor_robust.rs and their tests) and crates/meta/src/mrw.rs (the MRW block container), + vendor/srw.rs, vendor/cr2.rs, vendor/cr2/sraw.rs, vendor/nef.rs, vendor/nefc.rs, vendor/nef_lens.rs, docs/nikon-lens-corrections.md, vendor/rw2.rs, vendor/raf.rs, vendor/rafc.rs, vendor/raf_lens.rs, docs/raf-compression.md, docs/raf-corpus.sha256, vendor/orf.rs, docs/orf-compression.md, vendor/arw.rs, vendor/arw6.rs, vendor/arw6_curve.rs, vendor/arw6_testenc.rs, vendor/arw_lens.rs, vendor/arw_lens_rm6.rs, vendor/sr2.rs, llvc.rs, lens_metadata.rs, docs/arw6-compression.md, docs/sony-lens-corrections.md, docs/sony-ilce7rm6-lens-corrections.md, vendor/cr3.rs, vendor/crx.rs, vendor/crx_wavelet.rs, sparse.rs, saturation.rs (the CR3 white extension), docs/cr3.md, docs/raw/, kelvin_presets.rs (the preset ordering), tests/it/vendor_robust.rs and their tests) and crates/meta/src/mrw.rs (the MRW block container), revision 5ff567a4a2e067bef1b6bce152c11e7f727ccbaf. https://github.com/storytold/lightcraft Copyright (c) 2026 ArtCraft Team and the LightCraft contributors. diff --git a/README.md b/README.md index 2dd2c06e9..f50a8994b 100644 --- a/README.md +++ b/README.md @@ -153,10 +153,7 @@ craft- = { git = "https://github.com/storytold/craft-libs", rev = "= 2017] + rc = {k: sum(1 for r in recent if r["class"] == k) for k in classes} + print(f"\nModels added since 2017 (ACR 10+): {len(recent)}: " + ", ".join(f"{k} {rc[k]}" for k in classes)) + + # Use-weighted share of photographers whose camera fully works (decodes from sensor data). + weights = read_tsv("usage-weights.tsv") + by_cam = {(r["maker"], r["camera"]): r for r in rows} + total = works = verified_share = 0.0 + print("\n| Bucket | Weight | Camera fully works | of which verified bodies |") + print("|---|---:|---:|---:|") + for b in [w for w in weights if w["kind"] == "maker"]: + makers = b["makers_or_camera"].split(",") + popular = [w["makers_or_camera"] for w in weights if w["kind"] == "body" and w["bucket"] == b["bucket"]] + members = [r for r in rows if r["maker"] in makers] + pop_rows = [r for m in makers for (mk, cam), r in by_cam.items() if mk == m and cam in popular] + missing = set(popular) - {r["camera"] for r in pop_rows} + if missing: + print(f"usage-weights.tsv: unknown cameras {sorted(missing)}", file=sys.stderr) + return 1 + rest = [r for r in members if r["camera"] not in popular] + + def score(r): + if r["class"] == "verified": + return 1.0 + if r["class"] == "unverified": + return CONFIDENCE.get(r["format"], 0.7) + return 0.0 + + def mean(rs, f): + return sum(f(r) for r in rs) / len(rs) if rs else 0.0 + + if pop_rows: + s = 0.8 * mean(pop_rows, score) + 0.2 * mean(rest, score) + v = 0.8 * mean(pop_rows, lambda r: r["class"] == "verified") + 0.2 * mean(rest, lambda r: r["class"] == "verified") + else: + s, v = mean(members, score), mean(members, lambda r: r["class"] == "verified") + w = float(b["weight"]) + total += w + works += w * s + verified_share += w * v + print(f"| {b['bucket']} | {w:g}% | {100 * s:.0f}% | {100 * v:.0f}% |") + print(f"| **Weighted** | {total:g}% | **{100 * works / total:.0f}%** | **{100 * verified_share / total:.0f}%** |") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/crates/raw/docs/coverage/coverage.tsv b/crates/raw/docs/coverage/coverage.tsv new file mode 100644 index 000000000..c7a883509 --- /dev/null +++ b/crates/raw/docs/coverage/coverage.tsv @@ -0,0 +1,1448 @@ +# Generated by docs/raw/coverage.py; do not edit. +maker camera format year_added class reason +Apple iPhone 17e Front Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 17e Rear Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 17 Front Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 17 Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 17 Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 17 Pro Front Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 17 Pro Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 17 Pro Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 17 Pro Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 17 Pro Max Front Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 17 Pro Max Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 17 Pro Max Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 17 Pro Max Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone Air Front Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone Air Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16e Front Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16e Wide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16 Front Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16 Ultrawide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16 Wide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16 Plus Front Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16 Plus Ultrawide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16 Plus Wide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16 Pro Front Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16 Pro Telephoto Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16 Pro Ultrawide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16 Pro Wide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16 Pro Max Front Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16 Pro Max Telephoto Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16 Pro Max Ultrawide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 16 Pro Max WIde Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 15 Plus Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 15 Pro Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 15 Pro Front Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 15 Pro Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 15 Pro Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 15 Pro Max Front Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 15 Pro Max Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 15 Pro Max Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 15 Pro Max Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 14 Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 14 Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 14 Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 14 Plus Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 14 Plus Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 14 Plus Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 14 Pro Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 14 Pro Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 14 Pro Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 14 Pro Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 14 Pro Max Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 14 Pro Max Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 14 Pro Max Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 14 Pro Max Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 13 Front Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 13 Rear Ultrawide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 13 Rear Wide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 13 mini Front Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 13 mini Rear Ultrawide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 13 mini Rear Wide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 13 Pro Front Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 13 Pro Rear Telephoto Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 13 Pro Rear Ultrawide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 13 Pro Rear Wide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 13 Pro Max Front Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 13 Pro Max Rear Telephoto Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 13 Pro Max Rear Ultrawide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 13 Pro Max Rear Wide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 12 Rear Wide Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 12 Mini Rear Wide Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 12 Ultrawide camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 12 Mini Ultrawide camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 12 Pro Front Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 12 Pro Rear Wide Camera DNG 2020 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Apple iPhone 12 Pro Rear Telephoto Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 12 Pro Rear Ultra Wide Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 12 Pro Max Front Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 12 Pro Max Rear Wide Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 12 Pro Max Rear Telephoto Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 12 Pro Max Rear Ultra Wide Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 11 DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 11 Pro DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 11 Pro Max DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad Pro 9.7" (Wifi and Cellular) DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad Pro 11-inch (2nd generation) DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad Pro 11-inch (3rd generation) DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad Pro 11-inch (4th generation) Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad Pro 11-inch (2018 model) DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad Pro 11-inch M4 (2024 version) Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad Air 11-inch M2 (2024 version) Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad Pro 12.9-inch (4th generation) DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad Pro 12.9-inch (5th generation) DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad Pro 12.9-inch (6th generation) Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad Pro 12.9-inch (2018 model) DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad Pro 13-inch M4 (2024 version) Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad mini (6th Generation) Rear Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 6s DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 6s Plus DNG 2015 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Apple iPhone 7 DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 7 Plus DNG 2015 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Apple iPhone 8 DNG 2017 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone 8 Plus DNG 2017 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone SE (2020 version) DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone SE DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone X DNG 2017 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone XR DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone XS DNG 2018 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Apple iPhone XS Max DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad Air (5th generation) Rear Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone SE (3rd generation) Front Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPhone SE (3rd generation) Rear Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad (10th Generation) Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Apple iPad mini (A17 Pro) Rear Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Canon EOS-1D TIF 2003 unsupported TIF is not imported +Canon EOS-1D C CR2 2012 unverified CR2 lossless JPEG decoder +Canon EOS-1D Mark II CR2 2003 unverified CR2 lossless JPEG decoder +Canon EOS-1D Mark II N CR2 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS-1D Mark III CR2 2007 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS-1D Mark IV CR2 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS-1Ds TIF 2003 unsupported TIF is not imported +Canon EOS-1Ds Mark II CR2 2005 unverified CR2 lossless JPEG decoder +Canon EOS-1Ds Mark III CR2 2007 unverified CR2 lossless JPEG decoder +Canon EOS-1D X CR2 2012 unverified CR2 lossless JPEG decoder +Canon EOS-1D X Mark II CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS-1D X Mark III CR3 2019 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS 5D CR2 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 5D Mark II CR2 2008 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS-5D Mark III CR2 2012 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS-5D Mark IV (** see note) CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 5DS CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 5DS R CR2 2015 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS 6D CR2 2012 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS 6D Mark II CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 7D CR2 2008 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS 7D Mark II CR2 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 10D CRW 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES; 3 files) +Canon EOS 20D CR2 2003 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 20Da CR2 2005 unverified CR2 lossless JPEG decoder +Canon EOS 30D CR2 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 40D CR2 2007 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS 50D CR2 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 60D CR2 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 60Da CR2 2012 unverified CR2 lossless JPEG decoder +Canon EOS 70D CR2 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 77D (EOS 9000D) CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 80D CR2 2015 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS 90D CR3 2018 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS 200D (EOS Kiss X9, EOS Rebel SL2) CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 250D (EOS Rebel SL3, EOS 200D II, EOS Kiss X10) CR3 2018 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS D30 CRW 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS D60 CRW 2003 verified corpus: crates/raw/tests/it/corpus.rs; 18 maintainer files exact (craft-libs crates/crw/clean-room/03-verification.md) +Canon EOS 300D (EOS Digital Rebel, EOS Kiss Digital) CRW 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES; 3 files) +Canon EOS 100D (EOS Digital Rebel SL1, EOS Kiss X7) CR2 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 500D (EOS Digital Rebel T1i, EOS Kiss X3 Digital) CR2 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 550D (EOS Digital Rebel T2i, EOS Kiss X4 Digital) CR2 2010 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS 1100D (EOS Digital Rebel T3, EOS Kiss X50 Digital) CR2 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 600D (EOS Digital Rebel T3i, EOS Kiss X5 Digital) CR2 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 650D (EOS Digital Rebel T4i, EOS Kiss X4i) CR2 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 700D (EOS Digital Rebel T5i, EOS Kiss X7i) CR2 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 1200D (EOS Digital Rebel T5, EOS Kiss X70) CR2 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 1300D (EOS Digital Rebel T6, EOS Kiss X80) CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 1500D (EOS Rebel T7, EOS Kiss X90, EOS 2000D) CR2 2017 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 3000D (EOS Rebel T100, EOS 4000D) CR2 2017 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 750D (EOS Digital Rebel T6i, EOS Kiss X8i) CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 760D (EOS Digital Rebel T6s, EOS Kiss 8000D) CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 800D (EOS Digital Rebel T7i, EOS Kiss X9i) CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 850D (EOS Kiss X10i, EOS Rebel T8i) CR3 2019 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS 1000D (EOS Digital Rebel XS, EOS Kiss F) CR2 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 450D (EOS Digital Rebel XSi, EOS Kiss Digital X2) CR2 2007 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 350D (EOS Digital Rebel XT, EOS Kiss Digital N) CR2 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 400D (EOS Digital Rebel XTi, EOS Kiss Digital X) CR2 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS C50 CR3 2025 unverified CRX decoder: lossless and C-RAW 0x100 / 0x200 single-tile; other variants fall back to the preview +Canon EOS M CR2 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS M2 CR2 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS M200 CR3 2019 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS M3 CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS M5 CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS M50 Mark II CR3 2020 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS M6 CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS M6 Mark II CR3 2018 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS M10 CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS M50 (EOS Kiss M) CR3 2017 verified corpus: crates/raw/tests/it/cr3_corpus.rs (exact sensor hashes, docs/cr3.md) +Canon EOS M100 CR2 2017 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS R (** see note) CR3 2018 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R1 CR3 2023 unverified CRX decoder: lossless and C-RAW 0x100 / 0x200 single-tile; other variants fall back to the preview +Canon EOS R3 CR3 2021 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R5 CR3 2019 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R5 C CR3 2021 unverified CRX decoder: lossless and C-RAW 0x100 / 0x200 single-tile; other variants fall back to the preview +Canon EOS R5 Mark II CR3 2023 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hashes, docs/cr3.md); bulk: QP maps of 33,665 C-RAW files +Canon EOS R6 CR3 2019 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R6 V CR3 2025 unverified CRX decoder: lossless and C-RAW 0x100 / 0x200 single-tile; other variants fall back to the preview +Canon EOS R6 Mark II CR3 2022 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md; C-RAW with adaptive QP above 168 still opens as the preview) +Canon EOS R6 Mark III CR3 2025 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R7 CR3 2021 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md; lossless only, C-RAW unverified) +Canon EOS R8 Mark II CR3 2025 unverified CRX decoder: lossless and C-RAW 0x100 / 0x200 single-tile; other variants fall back to the preview +Canon EOS R10 CR3 2021 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS Ra CR3 2019 unverified CRX decoder: lossless and C-RAW 0x100 / 0x200 single-tile; other variants fall back to the preview +Canon EOS RP CR3 2018 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R8 CR3 2022 verified corpus: crates/raw/tests/it/cr3_corpus.rs (exact sensor hashes, docs/cr3.md) +Canon EOS R50 CR3 2022 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R50 V CR3 2024 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R100 CR3 2022 verified corpus: crates/raw/tests/it/cr3_corpus.rs (exact sensor hashes, docs/cr3.md) +Canon PowerShot 600 CRW 2003 preview pre-CIFF CRW is not decoded +Canon PowerShot A5 CRW 2003 preview pre-CIFF CRW is not decoded +Canon PowerShot A50 CRW 2003 preview pre-CIFF CRW is not decoded +Canon PowerShot G1 CRW 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon PowerShot G1 X CR2 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G1 X Mark II CR2 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G1 X Mark III CR2 2017 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G2 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables) +Canon PowerShot G3 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables) +Canon PowerShot G3 X CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G5 CRW 2003 preview CRW entropy anomaly (craft-libs crates/crw/clean-room/02-crw-format.md section 4); embedded JPEG +Canon PowerShot G5 X CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G5 X Mark II CR3 2018 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon PowerShot G6 CRW 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon PowerShot G7 X CR2 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G7 X Mark II CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G7 X Mark III CR3 2018 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon PowerShot G9 CR2 2007 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G9X CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G9 X Mark II CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G10 CR2 2008 unverified CR2 lossless JPEG decoder +Canon PowerShot G11 CR2 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G12 CR2 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G15 CR2 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G16 CR2 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot Pro 1 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables) +Canon PowerShot Pro70 CRW 2003 preview pre-CIFF CRW is not decoded +Canon PowerShot Pro90 IS CRW 2003 preview pre-CIFF CRW is not decoded +Canon PowerShot S30 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables) +Canon PowerShot S40 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables) +Canon PowerShot S45 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables) +Canon PowerShot S50 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables) +Canon PowerShot S60 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables) +Canon PowerShot S90 CR2 2008 unverified CR2 lossless JPEG decoder +Canon PowerShot S95 CR2 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot S70 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables) +Canon PowerShot S100 CR2 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot S100V CR2 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot S110 CR2 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot S120 CR2 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot SX1 IS CR2 2008 unverified CR2 lossless JPEG decoder +Canon PowerShot SX50 CR2 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot SX60 HS CR2 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot SX70 HS CR3 2018 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon PowerShot V1 CR3 2024 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Casio EXILIM EX-10 DNG 2013 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-100 DNG 2013 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-100F DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-100PRO DNG 2013 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-FC300S DNG 2012 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-FH100 DNG 2010 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR700 DNG 2012 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR710 DNG 2012 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR750 DNG 2012 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR800 DNG 2013 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR850 DNG 2013 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR1000 DNG 2012 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR1100 DNG 2013 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR1200 DNG 2013 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR1300 DNG 2013 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR1500 DNG 2013 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR3000 DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR3100 DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR3200 DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR3500 DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR3600 DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR3700 DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR4000 DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR4100 DNG 2017 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR5000 DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Casio EXILIM EX-ZR5100 DNG 2017 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Contax N Digital RAW 2003 preview RAW is opened from its embedded JPEG only +DJI DJI Mavic 2 Pro (Hasselblad L1D-20c) DNG 2020 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +DJI DJI AIR 2S DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Mini 2 DNG 2020 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +DJI DJI Air 3 Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Air 3 Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Mini 3 Pro DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Mini 4 Pro DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Mavic 3 Classic (Hasselblad L2D-20c) DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Mavic 3 Cine Super Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Mavic 3 Cine (Hasselblad L2D-20c) DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Mavic 3 Pro Super Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Mavic 3 Pro Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Mavic 3 Pro (Hasselblad L2D-20c) DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Mavic 3 (Hasselblad L2D-20c) DNG 2023 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +DJI DJI Osmo Pocket 3 D DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Air 3S Telephoto Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Air 3S Wide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Osmo Action 5 Pro DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Mavic 4 Pro (DJI FC9284) DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DJI DJI Mavic 4 Pro (DJI FC9287) DNG 2025 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +DJI DJI Mavic 4 Pro (Hasselblad L3D-100c) DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +DxO DxO ONE DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Epson R-D1 ERF 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Epson R-D1s ERF 2005 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Epson R-D1x ERF 2008 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm GFX 50R RAF 2017 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm GFX 50S RAF 2015 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm GFX 50S II RAF 2020 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm GFX 100 RAF 2018 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm GFX 100 II RAF 2022 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm GFX 100 II IR RAF 2024 unverified RAF decoder (uncompressed, lossless / lossy compressed, Bayer and X-Trans) +Fujifilm GFX 100 IR RAF 2020 unverified RAF decoder (uncompressed, lossless / lossy compressed, Bayer and X-Trans) +Fujifilm GFX 100S RAF 2020 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm GFX 100S II RAF 2023 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm GFX 100RF (Final Support) RAF 2024 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm FinePix E900 RAF 2005 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix F505 EXR RAF 2012 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix F550 EXR RAF 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix F600 EXR RAF 2010 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix F605 EXR RAF 2012 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix F700 RAF 2003 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix F770 EXR RAF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix F775 EXR RAF 2012 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix F800 EXR RAF 2012 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix F900 EXR RAF 2012 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix HS10 RAF 2010 unverified RAF decoder (uncompressed, lossless / lossy compressed, Bayer and X-Trans) +Fujifilm FinePix HS20 EXR RAF 2010 unverified RAF decoder (uncompressed, lossless / lossy compressed, Bayer and X-Trans) +Fujifilm FinePix HS22 EXR RAF 2013 unverified RAF decoder (uncompressed, lossless / lossy compressed, Bayer and X-Trans) +Fujifilm FinePix HS30 EXR RAF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix HS33 EXR RAF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix HS35 EXR RAF 2013 unverified RAF decoder (uncompressed, lossless / lossy compressed, Bayer and X-Trans) +Fujifilm FinePix HS50 EXR RAF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix IS-1 RAF 2007 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix IS Pro RAF 2008 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix S1 RAF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix S100fs RAF 2007 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix S2 Pro RAF 2003 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix S20 Pro RAF 2003 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix S200 EXR RAF 2010 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix S205 EXR RAF 2013 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix S3 Pro RAF 2005 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix S5 Pro RAF 2007 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix S5000 Z RAF 2003 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix S5200 (FinePix S5600) RAF 2005 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix S6000fd (FinePix S6500fd) RAF 2005 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix S7000 Z RAF 2003 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix S9000 (FinePix S9500) RAF 2005 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix S9100 (FinePix S9600) RAF 2005 preview pre-X FinePix RAF without a raw IFD +Fujifilm FinePix SL1000 RAF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix X10 RAF 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix X100 RAF 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix X-M1 RAF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix X-S1 RAF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X100F RAF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X100S RAF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X100T RAF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X100V** RAF 2019 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X100VI RAF 2023 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X20 RAF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X30 RAF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X70 RAF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-A1 RAF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-A2 RAF 2015 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-A3 RAF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-A5 RAF 2017 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-A7 RAF 2018 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-A10 RAF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-A20 RAF 2017 unverified RAF decoder (uncompressed, lossless / lossy compressed, Bayer and X-Trans) +Fujifilm X-E1 RAF 2012 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-E2 RAF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-E2S RAF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-E3 RAF 2017 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-E4 RAF 2020 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-E5 RAF 2024 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-H1 RAF 2017 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-H2 IR RAF 2024 unverified RAF decoder (uncompressed, lossless / lossy compressed, Bayer and X-Trans) +Fujifilm X-H2S RAF 2021 verified docs/raf-compression.md (supplied files, exact sensor arrays) +Fujifilm X-M5 RAF 2024 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-Pro1 RAF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-Pro2 RAF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-Pro3** RAF 2019 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-S10 RAF 2020 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-S20 RAF 2022 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T1 RAF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T1 IR RAF 2015 unverified RAF decoder (uncompressed, lossless / lossy compressed, Bayer and X-Trans) +Fujifilm X-T2 RAF 2015 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-T3 RAF 2018 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T4** RAF 2019 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-T5 RAF 2022 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-T10 RAF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T20 RAF 2015 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-T30 RAF 2018 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T30 II RAF 2021 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T30 III RAF 2022 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T50 RAF 2023 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-T100 RAF 2017 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T200 RAF 2019 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm XF1 RAF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm XF10 RAF 2017 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm XQ1 RAF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm XQ2 RAF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-H2 RAF 2021 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Google Pixel DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel XL DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 2 DNG 2017 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 2 XL DNG 2017 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Google Pixel 3 DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 3 Front Standard Camera DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 3 Front Wide Camera DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 3 XL DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 3 XL Front Standard Camera DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 3 XL Front Wide Camera DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 4 DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 4 XL DNG 2019 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Google Pixel 5 Front Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 5 Rear Main Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 5 Rear Ultra Wide Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 6 Front Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 6 Rear Main Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 6 Rear Ultrawide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 6 Pro Front Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 6 Pro Rear Main Camera DNG 2021 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Google Pixel 6 Pro Rear Telephoto Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 6 Pro Rear Ultrawide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 7 Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 7 Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 7 Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 7 Pro Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 7 Pro Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 7 Pro Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 7 Pro Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 4a Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 4a Rear Wide Camera DNG 2022 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Google Pixel 5a Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 5a Rear Main Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 5a Rear Ultra Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 6a Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 6a Rear Main Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 6a Rear Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 7a Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 7a Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 7a Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 8 Front Camera* DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 8 Ultrawide Camera* DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 8 Wide Camera* DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 8 Pro Front Camera* DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 8 Pro Telephoto Camera* DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 8 Pro Ultrawide Camera* DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 8 Pro Wide Camera* DNG 2023 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Google Pixel 8a Front Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 8a Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 8a Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9 Front Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9 Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9 Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9 Pro Front Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9 Pro Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9 Pro Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9 Pro Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9 Pro XL Front Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel Pixel 9 Pro XL Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel Pixel 9 Pro XL Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9 Pro XL Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9 Pro Fold Front Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9 Pro Fold Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9 Pro Fold Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9 Pro Fold Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9a Front Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9a Ultrawide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 9a Wide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Pro Front Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Pro Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Pro Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Pro Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Pro XL Front Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Pro XL Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Pro XL Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Pro XL Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Front Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Pro Fold Front Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Pro Fold Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Pro Fold Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel 10 Pro Fold Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel Fold Inner Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel Fold Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel Fold Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Google Pixel Fold Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +GoPro FUSION GPR 2017 unsupported GPR is not imported +GoPro HERO5 Black GPR 2015 unsupported GPR is not imported +GoPro HERO6 Black GPR 2017 unsupported GPR is not imported +GoPro HERO7 Black GPR 2018 unsupported GPR is not imported +GoPro HERO10 Black GPR 2021 unsupported GPR is not imported +GoPro HERO12 Black GPR 2024 unsupported GPR is not imported +GoPro HERO13 Black GPR 2024 unsupported GPR is not imported +GoPro MISSION 1 GPR 2025 unsupported GPR is not imported +GoPro MISSION 1 PRO GPR 2025 unsupported GPR is not imported +Hasselblad 503CWD 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad A6D-100c 3FR 2017 preview 3FR is opened from its embedded JPEG only +Hasselblad CF-22 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad CF-22MS 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad CF-39 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad CF-39MS 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad CFH-22 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad CFH-39 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad CFV 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad CFV-50c 3FR 2008 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); decode verified, not colour calibration +Hasselblad H2D DNG 2003 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Hasselblad H2D-22 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad H2D-39 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad H3D-22 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad H3D-39 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad H3DII-22 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad H3DII-31 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad H3DII-39 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad H3DII-39MS 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad H3DII-50 3FR 2008 preview 3FR is opened from its embedded JPEG only +Hasselblad H4D-40 3FR 2010 preview 3FR is opened from its embedded JPEG only +Hasselblad H4D-60 3FR 2010 preview 3FR is opened from its embedded JPEG only +Hasselblad H5D-40 3FR 2012 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); decode verified, not colour calibration +Hasselblad H5D-50 3FR 2012 preview 3FR is opened from its embedded JPEG only +Hasselblad H5D-50c 3FR 2013 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); decode verified, not colour calibration +Hasselblad H5D-60 3FR 2013 preview 3FR is opened from its embedded JPEG only +Hasselblad H6D-100c 3FR 2015 preview 3FR is opened from its embedded JPEG only +Hasselblad HV ARW 2013 unverified ARW decoder (uncompressed, cRAW, packed 12-bit, lossless, downsized YCbCr) +Hasselblad Lunar 3FR 2012 verified issue #1275: raw.pixls.us 4051 (a Sony NEX-7 ARW under Make HASSELBLAD) decodes from sensor data; black 512 and SR2SubIFD WB as on a NEX-7 +Hasselblad Stellar II 3FR 2015 preview 3FR is opened from its embedded JPEG only +Hasselblad X1D 3FR 2015 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); decode verified, not colour calibration +Hasselblad X2D II 100C (**) 3FR 2024 preview 3FR is opened from its embedded JPEG only +Hasselblad X1D II 50C 3FR 2018 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); decode verified, not colour calibration +Hasselblad X2D 100C 3FR 2021 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); decode verified, not colour calibration +Huawei Honor 20 Pro Rear Main Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Huawei Honor 20 Pro Rear Telephoto Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Huawei Honor 20 Pro Rear Wide Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Huawei Mate 10 Pro DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Huawei P30 Rear Main Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Huawei P30 Rear Telephoto Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Huawei P30 Rear Wide Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Huawei P30 Pro Rear Main Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Huawei P30 Pro Rear Telephoto Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Huawei P30 Pro Rear Wide Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Huawei P9 Lite DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Insta360 Ace Pro 2 DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Insta360 GO Ultra DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Kodak DCS 14n DCR 2003 unsupported DCR is not imported +Kodak DCS 720x DCR 2003 unsupported DCR is not imported +Kodak DCS 760 DCR 2003 unsupported DCR is not imported +Kodak DCS Pro 14nx DCR 2003 unsupported DCR is not imported +Kodak DCS Pro SLR/n DCR 2003 unsupported DCR is not imported +Kodak EasyShare P712 DCR 2005 unsupported DCR is not imported +Kodak EasyShare P850 KDC 2005 preview KDC is opened from its embedded JPEG only +Kodak EasyShare P880 KDC 2005 preview KDC is opened from its embedded JPEG only +Kodak EasyShare Z1015 IS KDC 2008 preview KDC is opened from its embedded JPEG only +Kodak EasyShare Z980 KDC 2008 preview KDC is opened from its embedded JPEG only +Kodak EasyShare Z981 RAW 2010 preview RAW is opened from its embedded JPEG only +Kodak EasyShare Z990 RAW 2010 preview RAW is opened from its embedded JPEG only +Konica Minolta Alpha-5 Digital (China) MRW 2005 unverified MRW decoder (12-bit packed, 16-bit words) +Konica Minolta Alpha Sweet Digital (Japan) MRW 2005 unverified MRW decoder (12-bit packed, 16-bit words) +Konica Minolta DiMAGE 5 MRW 2003 unverified MRW decoder (12-bit packed, 16-bit words) +Konica Minolta DiMAGE 7 MRW 2003 verified raw.pixls.us check: CC0 sample decoded and its WB / CFA matched to the embedded preview (vendor/mrw.rs) +Konica Minolta DiMAGE 7i MRW 2003 unverified MRW decoder (12-bit packed, 16-bit words) +Konica Minolta DiMAGE 7Hi MRW 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Konica Minolta DiMAGE A1 MRW 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Konica Minolta DiMAGE A2 MRW 2003 verified raw.pixls.us check: CC0 sample decoded and its WB / CFA matched to the embedded preview (vendor/mrw.rs) +Konica Minolta DiMAGE A200 MRW 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Konica Minolta Maxxum 5D Dynax 5D MRW 2005 unverified MRW decoder (12-bit packed, 16-bit words) +Konica Minolta Maxxum 7D Dynax 7D MRW 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Leaf AFi II 6 MOS 2008 preview MOS is opened from its embedded JPEG only +Leaf AFi II 7 MOS 2008 preview MOS is opened from its embedded JPEG only +Leaf Aptus 17 MOS 2007 preview MOS is opened from its embedded JPEG only +Leaf Aptus 22 MOS 2005 preview MOS is opened from its embedded JPEG only +Leaf Aptus 54s MOS 2007 preview MOS is opened from its embedded JPEG only +Leaf Aptus 65 MOS 2005 preview MOS is opened from its embedded JPEG only +Leaf Aptus 75 MOS 2005 preview MOS is opened from its embedded JPEG only +Leaf Aptus 75s MOS 2007 preview MOS is opened from its embedded JPEG only +Leaf Aptus-II 5 MOS 2008 preview MOS is opened from its embedded JPEG only +Leaf Aptus-II 6 MOS 2008 preview MOS is opened from its embedded JPEG only +Leaf Aptus-II 7 MOS 2008 preview MOS is opened from its embedded JPEG only +Leaf Aptus-II 8 MOS 2010 preview MOS is opened from its embedded JPEG only +Leaf Aptus-II 10R MOS 2010 preview MOS is opened from its embedded JPEG only +Leaf Aptus-II 12 MOS 2010 preview MOS is opened from its embedded JPEG only +Leaf Valeo 6 MOS 2003 preview MOS is opened from its embedded JPEG only +Leaf Valeo 11 MOS 2003 preview MOS is opened from its embedded JPEG only +Leaf Valeo 17 MOS 2005 preview MOS is opened from its embedded JPEG only +Leaf Valeo 22 MOS 2003 preview MOS is opened from its embedded JPEG only +Leaf Credo 40 IIQ 2012 preview IIQ is opened from its embedded JPEG only +Leaf Credo 50 IIQ 2013 preview IIQ is opened from its embedded JPEG only +Leaf Credo 60 IIQ 2012 preview IIQ is opened from its embedded JPEG only +Leaf Credo 80 IIQ 2012 preview IIQ is opened from its embedded JPEG only +Leica C (Typ 112) RWL 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica CL DNG 2017 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica C-Lux DNG 2017 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica D-Lux 2 RAW 2005 unverified Panasonic decoder, every raw format +Leica D-Lux 3 RAW 2005 verified PR #215: raw.pixls.us bulk check of every RW2 / RWL / RAW body +Leica D-Lux 4 RWL 2008 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica D-Lux 5 RWL 2010 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica D-Lux 6 RWL 2012 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica D-Lux 7 RWL 2018 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica D-Lux (Typ 109) RWL 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica D-Lux 8 DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica D-Lux 8 "100 Years of Leica" DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica Digilux 2 RAW 2003 verified PR #215: raw.pixls.us bulk check of every RW2 / RWL / RAW body +Leica Digilux 3 RAW 2005 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica Digital-Modul-R DNG 2003 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica Leitz Phone 3 DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica M8 DNG 2003 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash) +Leica M9 DNG 2008 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash) +Leica M10 DNG 2015 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica M10-D DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica M10-P DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica M10-R DNG 2019 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica M11-D DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica M (Typ 262) DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica M-D (Typ 262) DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica M-E DNG 2012 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica M-EV1 DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica M-P DNG 2013 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica M Monochrom DNG 2012 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash) +Leica M Monochrom (Typ 246) DNG 2015 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash) +Leica M10 Monochrom DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica M11 DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica M11 MONOCHROM DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica M11-P DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica Q (Typ 116) DNG 2015 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica Q2 DNG 2018 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica Q2 Monochrom DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica Q3 DNG 2022 verified recorded check 2026-10-11: 38 private Q3 DNGs (landscape and portrait) decoded from sensor data and developed at full size without error, mean CIE76 dE to the camera JPEG 6.3-13.9 at 400 px (not committed; docs/raw-parity.md) +Leica Q3 43 DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica Q3 Monochrom DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica S2 DNG 2010 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica S (Typ 007) DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica S3 DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica SL (Typ 601) DNG 2015 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica SL2 DNG 2019 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica SL2-S DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica SL3 DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica SL3-P DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica SL3-S DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica T (Typ 701) DNG 2013 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica TL DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica TL2 DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica V-LUX 1 RAW 2005 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica V-LUX 2 RAW 2010 unverified Panasonic decoder, every raw format +Leica V-LUX 3 RAW 2010 unverified Panasonic decoder, every raw format +Leica V-LUX 4 RWL 2012 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica V-LUX 5 RWL 2018 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica V-LUX (Typ 114) RWL 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica X2 RWL 2012 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica X (Typ 113) DNG 2013 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Leica X-U (Typ 113) DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG G7 ThinQ DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG G8 ThinQ Front Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG G8 ThinQ Rear Main Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG G8 ThinQ Rear Wide Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG V40 ThinQ Front Standard Camera DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG V40 ThinQ Rear Main Camera DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG V40 ThinQ Rear Wide Camera DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG V50s ThinQ Front Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG V50s ThinQ Rear Main Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG V50s ThinQ Rear Wide Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG V60 ThinQ Front Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG V60 ThinQ Rear Main Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG Velvet Front Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG Velvet Rear Main Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG Velvet Rear Wide Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG Wing Rear Main Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG Wing Front Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +LG Wing Rear Wide Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Light L16 DNG 2017 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Mamiya Mamiya DM22 MOS 2008 preview MOS is opened from its embedded JPEG only +Mamiya Mamiya DM28 MOS 2008 preview MOS is opened from its embedded JPEG only +Mamiya Mamiya DM33 MOS 2008 preview MOS is opened from its embedded JPEG only +Mamiya Mamiya DM56 MOS 2008 preview MOS is opened from its embedded JPEG only +Mamiya Mamiya M18 TIF 2008 unsupported TIF is not imported +Mamiya Mamiya M22 TIF 2008 unsupported TIF is not imported +Mamiya Mamiya M31 TIF 2008 unsupported TIF is not imported +Mamiya Mamiya DM40 MOS 2010 preview MOS is opened from its embedded JPEG only +Mamiya ZD MEF 2005 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Motorola Moto G Power (2021) Front Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Motorola Moto G Power (2021) Rear Macro Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Motorola Moto G Power (2021) Rear Wide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Nikon 1 J1 NEF 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 J2 NEF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 J3 NEF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 J4 NEF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 J5 NEF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 S1 NEF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 S2 NEF 2013 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon 1 V1 NEF 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 V2 NEF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 V3 NEF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon COOLPIX 5000 NEF 2003 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon COOLPIX 5400 NEF 2003 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon COOLPIX 5700 NEF 2003 unsupported bulk: raw.pixls.us CC0 file decodes wrongly (two stacked half-height fields, CMYG mosaic read as RGB; green vs camera JPEG r 0.12-0.36) +Nikon COOLPIX 8400 NEF 2005 unsupported bulk: raw.pixls.us CC0 file decodes wrongly (two stacked half-height fields, CMYG mosaic read as RGB; green vs camera JPEG r 0.12-0.36) +Nikon COOLPIX 8700 NEF 2003 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon COOLPIX 8800 NEF 2005 unsupported bulk: raw.pixls.us CC0 file decodes wrongly (two stacked half-height fields, CMYG mosaic read as RGB; green vs camera JPEG r 0.12-0.36) +Nikon COOLPIX A NEF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon COOLPIX A1000 NRW 2018 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon COOLPIX B700 NRW 2015 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Nikon COOLPIX P330 NRW 2012 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon COOLPIX P340 NRW 2013 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon COOLPIX P1000 NRW 2018 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon COOLPIX P1100 NRW 2024 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon COOLPIX P6000 NRW 2008 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon COOLPIX P7000 NRW 2010 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon COOLPIX P7100 NRW 2010 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon COOLPIX P7700 NRW 2012 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon COOLPIX P7800 NRW 2013 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon COOLPIX P950 NEF 2019 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D1 NEF 2003 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon D1H NEF 2003 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D1X NEF 2003 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon D2H NEF 2003 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D2Hs NEF 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D2X NEF 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D2Xs NEF 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3 NEF 2007 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3s NEF 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3X NEF 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D4 NEF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D4S NEF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D5 NEF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D40 NEF 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D40x NEF 2007 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D50 NEF 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D60 NEF 2007 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D6 NEF 2019 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); checked compressed NEF only; uncompressed remains preview-only (#671) +Nikon D70 NEF 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D70S NEF 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D80 NEF 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D90 NEF 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D100 NEF 2003 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon D200 NEF 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D300 NEF 2007 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D300s NEF 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D500 NEF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D600 NEF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D610 NEF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D700 NEF 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D750 NEF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D780 NEF 2019 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D800 NEF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D800E NEF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D810 NEF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D810A NEF 2015 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon D850 NEF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3000 NEF 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3100 NEF 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3200 NEF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3300 NEF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3400 NEF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3500 NEF 2018 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D5000 NEF 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D5100 NEF 2010 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Nikon D5200 NEF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D5300 NEF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D5500 NEF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D5600 NEF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D7000 NEF 2010 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Nikon D7100 NEF 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D7200 NEF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D7500 NEF 2015 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Nikon Df NEF 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z 30 NEF 2021 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z 50 NEF 2019 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z 50 II NEF 2024 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z 5 NEF 2019 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z 5 2 NEF 2024 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon Z 5 2C NEF 2025 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon Z 6 NEF 2018 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); checked compressed NEF only; uncompressed remains preview-only (#671) +Nikon Z 6 II NEF 2020 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z 6 III NEF 2023 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nikon Z 7 NEF 2018 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); checked compressed NEF only; uncompressed remains preview-only (#671) +Nikon Z 7 II NEF 2020 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z 8 NEF 2022 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); HE / HE* remain preview-only +Nikon Z 9 (*See note) NEF 2021 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z fc NEF 2020 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z f NEF 2022 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Nikon ZR NEF 2024 unverified NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only +Nokia Lumia 1020 DNG 2013 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Nokia HMD Global Nokia 9 PureView Front Camera DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Nokia HMD Global Nokia 9 PureView Rear Camera DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +OM Digital Solutions OM System OM-1 ORF 2021 preview compressed ORF is unsupported; embedded preview when available +OM Digital Solutions OM System OM-5 ORF 2022 preview compressed ORF is unsupported; embedded preview when available +OM Digital Solutions OM System TG - 7 ORF 2023 preview compressed ORF is unsupported; embedded preview when available +OM Digital Solutions OM System OM-1 MARK II ORF 2023 preview compressed ORF is unsupported; embedded preview when available +OM Digital Solutions OM System OM-3 ORF 2024 preview compressed ORF is unsupported; embedded preview when available +OM Digital Solutions OM-5 Mark II ORF 2024 preview compressed ORF is unsupported; embedded preview when available +OM Digital Solutions OM-3 Astro ORF 2025 preview compressed ORF is unsupported; embedded preview when available +Olympus AIR A01 ORF 2015 preview compressed ORF is unsupported; embedded preview when available +Olympus Camedia C-5050 Zoom ORF 2003 unverified uncompressed / packed ORF +Olympus Camedia C-5060 Zoom ORF 2003 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus Camedia C-7070 Wide Zoom ORF 2005 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus Camedia C-8080 Wide Zoom ORF 2003 unverified uncompressed / packed ORF +Olympus E-1 ORF 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Olympus E-3 ORF 2007 preview compressed ORF is unsupported; embedded preview when available +Olympus E-5 ORF 2010 preview compressed ORF is unsupported; embedded preview when available +Olympus E-10 ORF 2003 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash) +Olympus E-20 ORF 2003 unverified uncompressed / packed ORF +Olympus E-30 ORF 2008 preview compressed ORF is unsupported; embedded preview when available +Olympus E-420 ORF 2008 preview compressed ORF is unsupported; embedded preview when available +Olympus E-450 ORF 2008 preview compressed ORF is unsupported; embedded preview when available +Olympus E-520 ORF 2008 preview compressed ORF is unsupported; embedded preview when available +Olympus E-600 ORF 2010 preview compressed ORF is unsupported; embedded preview when available +Olympus E-620 ORF 2008 preview compressed ORF is unsupported; embedded preview when available +Olympus EVOLT E-300 ORF 2005 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus EVOLT E-330 ORF 2005 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus EVOLT E-400 ORF 2005 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Olympus EVOLT E-410 ORF 2007 preview compressed ORF is unsupported; embedded preview when available +Olympus EVOLT E-500 ORF 2005 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus EVOLT E-510 ORF 2007 preview compressed ORF is unsupported; embedded preview when available +Olympus OM-D E-M1 ORF 2013 preview compressed ORF support withdrawn pending provenance review; embedded JPEG +Olympus OM-D E-M1X ORF 2018 preview compressed ORF is unsupported; embedded preview when available +Olympus OM-D E-M1 Mark II ORF 2015 preview compressed ORF is unsupported; embedded preview when available +Olympus OM-D E-M1 Mark III ORF 2019 preview compressed ORF is unsupported; embedded preview when available +Olympus OM-D E-M5 ORF 2012 preview compressed ORF is unsupported; embedded preview when available +Olympus OM-D E-M5 Mark II ORF 2015 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus OM-D E-M5 Mark III ORF 2019 preview compressed ORF is unsupported; embedded preview when available +Olympus OM-D E-M10 ORF 2013 preview compressed ORF is unsupported; embedded preview when available +Olympus OM-D E-M10 Mark II ORF 2015 preview compressed ORF is unsupported; embedded preview when available +Olympus OM-D E-M10 Mark III ORF 2017 preview compressed ORF support withdrawn pending provenance review; embedded JPEG +Olympus OM-D E-M10 Mark III S ORF 2019 preview compressed ORF is unsupported; embedded preview when available +Olympus OM-D E-M10 Mark IV ORF 2019 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-P1 ORF 2008 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-P2 ORF 2008 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-P3 ORF 2010 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-P5 ORF 2013 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-P7 ORF 2020 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-PL1 ORF 2010 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-PL1s ORF 2010 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-PL10 ORF 2019 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-PL2 ORF 2010 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-PL3 ORF 2010 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-PL6 ORF 2013 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-PL5 ORF 2012 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-PL7 ORF 2013 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-PL8 ORF 2015 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-PL9 ORF 2017 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-PM1 ORF 2010 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN E-PM2 ORF 2012 preview compressed ORF is unsupported; embedded preview when available +Olympus PEN-F ORF 2015 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus SP-310 ORF 2005 preview compressed ORF is unsupported; embedded preview when available +Olympus SP-320 ORF 2005 preview compressed ORF is unsupported; embedded preview when available +Olympus SP-350 ORF 2005 preview compressed ORF is unsupported; embedded preview when available +Olympus SP-500 UZ ORF 2005 preview compressed ORF is unsupported; embedded preview when available +Olympus SP-510 UZ ORF 2005 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus SP-550 UZ ORF 2007 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus SP-560 UZ ORF 2007 preview compressed ORF is unsupported; embedded preview when available +Olympus SP-565 UZ ORF 2008 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus SP-570 UZ ORF 2007 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus Stylus 1 ORF 2013 preview compressed ORF is unsupported; embedded preview when available +Olympus Stylus 1s ORF 2013 preview compressed ORF is unsupported; embedded preview when available +Olympus Stylus SH-2 ORF 2015 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus Stylus SH-3 ORF 2015 preview compressed ORF is unsupported; embedded preview when available +Olympus Stylus Tough TG-4 ORF 2015 preview compressed ORF is unsupported; embedded preview when available +Olympus Stylus Tough TG-5 ORF 2015 preview compressed ORF is unsupported; embedded preview when available +Olympus Stylus Tough TG-6 ORF 2018 preview compressed ORF is unsupported; embedded preview when available +Olympus Stylus XZ-2 iHS ORF 2012 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Olympus Stylus XZ-10 ORF 2012 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus XZ-1 ORF 2010 preview compressed ORF is unsupported; embedded preview when available +OnePlus 7T Front Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +OnePlus 7T Rear Main Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +OnePlus 8 Front Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +OnePlus 8 Rear Main Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +OnePlus 8 Pro Front Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +OnePlus 8 Pro Rear Main Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +OnePlus 12 Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +OnePlus 12 Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +OnePlus 12 Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X7 Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X7 Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X7 Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X7 Ultra Super Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X7 Ultra Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X7 Ultra Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X7 Ultra Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8 Ultra Super Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8 Ultra Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8 Ultra Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8 Ultra Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8 Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8 Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8 Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8s Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8s Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8s Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8s+ Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8s+ Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8s+ Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8 Pro Super Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8 Pro Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8 Pro Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X8 Pro Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X9 Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X9 Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X9 Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X9 Pro Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X9 Pro Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X9 Pro Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X9s Pro Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X9s Pro Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X9s Pro Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X9 Ultra Super Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X9 Ultra Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X9 Ultra Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Oppo Find X9 Ultra Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Panasonic LUMIX AG-GH4 RW2 2013 unverified Panasonic decoder, every raw format +Panasonic LUMIX DC-FZ80 (DC-FZ82, DC-FZ85) RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-FZ80D (DC-FZ82D, DC-FZ85D) RW2 2023 unverified Panasonic decoder, every raw format +Panasonic LUMIX DC-FZ81 (DC-FZ83) RW2 2015 unverified Panasonic decoder, every raw format +Panasonic LUMIX DC-FZ1000M2 (DC-FZ10002) RW2 2018 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-G100 (DC-G110) RW2 2019 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-G9 RW2 2017 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-G99 (DC-G90, DC-G91, DC-G95) RW2 2018 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GF9 (DC-GX850, DC-GX800) RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GF10 (DC-GF90) RW2 2017 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GH5 RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GH5M2 RW2 2020 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GH5s RW2 2017 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GH6 RW2 2021 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GH7 RW2 2023 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GX880 RW2 2018 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-LX100 II RW2 2018 verified PR #215: raw.pixls.us bulk check of every RW2 / RWL / RAW body +Panasonic LUMIX DC-S1 RW2 2018 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S1H RW2 2018 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S1R RW2 2018 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S5 RW2 2020 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S9 RW2 2023 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-ZS70 (DC-TZ90, DC-TZ91, DC-TZ92, DC-TZ93) RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-ZS80 (DC-TZ95, DC-TZ96, DC-TZ97) RW2 2018 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-CM1 RW2 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-CM10 RW2 2015 unverified Panasonic decoder, every raw format +Panasonic LUMIX DMC-FZ1000 RW2 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FX150 RW2 2008 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ8 RAW 2007 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ18 RAW 2007 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ28 RW2 2008 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ30 RAW 2005 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ35 RW2 2008 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ38 RW2 2008 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ40 (DMC-FZ45) RW2 2010 verified PR #215: raw.pixls.us bulk check of every RW2 / RWL / RAW body +Panasonic LUMIX DMC-FZ50 RAW 2005 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ70 RW2 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ72 RW2 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ100 RW2 2010 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ150 RW2 2010 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ200 RW2 2012 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ2500 (DMC-FZ2000, DMC-FZH1) RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ300 RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ330 RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G1 RW2 2008 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G2 RW2 2010 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G3 RW2 2010 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G5 RW2 2012 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G6 RW2 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G7 RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G8 (DMC-G80, DMC-G81, DMC-G85) RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G10 RW2 2010 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GF1 RW2 2008 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GF2 RW2 2010 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GF3 RW2 2010 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GF5 RW2 2012 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GF6 RW2 2012 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GF7 RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GF8 RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GH1 RW2 2008 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GH2 RW2 2010 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GH3 RW2 2012 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GH4 RW2 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GM1 RW2 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GM1S RW2 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GM5 RW2 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GX1 RW2 2010 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GX7 RW2 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GX8 RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GX85 (DMC-GX80, DMC-GX7MK2) RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GX9 (DC-GX7MK3) RW2 2017 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-L1 RAW 2005 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-L10 RAW 2007 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LC1 RAW 2003 unverified Panasonic decoder, every raw format +Panasonic LUMIX DMC-LF1 RW2 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LX1 RAW 2005 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LX2 RAW 2005 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LX3 RW2 2008 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LX5 RW2 2010 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LX7 RW2 2012 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LX9 (DMC-LX10, DMC-LX15) RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LX100 RW2 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-TZ82 RW2 2015 unverified Panasonic decoder, every raw format +Panasonic LUMIX DMC-ZS40 (DMC-TZ60, DMC-TZ61) RW2 2013 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-ZS50 (DMC-TZ70, DMC-TZ71) RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-ZS60 (DMC-TZ80, DMC-TZ81, DMC-TZ85) RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-ZS100 (DMC-ZS110, DMC-TZ100, DMC-TZ101, DMC-TZ110, DMC-TX1) RW2 2015 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX ZS200 (DC-TX2, DC-TZ200, DC-TZ202, DC-TZ220, DC-ZS220) RW2 2017 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-G95D RW2 2021 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-G99D RW2 2021 unverified Panasonic decoder, every raw format +Panasonic LUMIX DC-TX2D (DC-TZ200D, DC-TZ202D, DC-TZ220D, DC-ZS200D, DC-ZS220D) RW2 2022 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-ZS80D (DC-TZ95D, DC-TZ96D) RW2 2022 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S5M2 RW2 2022 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S5M2X RW2 2022 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DCG9M2 RW2 2023 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-G100D RW2 2023 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S5D RW2 2024 unverified Panasonic decoder, every raw format +Panasonic LUMIX DC - G97 (DC-G99M2) RW2 2024 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC - TZ99 (DC - ZS99) RW2 2024 unverified Panasonic decoder, every raw format +Panasonic LUMIX DC-S1RM2 (Final Support) RW2 2024 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S1M2ES (Final Support) RW2 2024 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S1M2 RW2 2024 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-ZS300 (DC-TZ300, DC-TX3) RW2 2025 unverified Panasonic decoder, every raw format +Panasonic LUMIX DC-L10 RW2 2025 unverified Panasonic decoder, every raw format +Parrot Anafi DNG 2017 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Parrot ANAFI Ai DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Pentax 645D DNG 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax 645Z DNG 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax *ist D PEF 2003 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax *ist DL PEF 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax *ist DL2 PEF 2005 unverified PEF decoder (uncompressed, Huffman, packed 12-bit) +Pentax *ist DS PEF 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax *ist DS2 PEF 2005 unverified PEF decoder (uncompressed, Huffman, packed 12-bit) +Pentax K-01 DNG 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-1 DNG 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-1 II (* see note) DNG 2017 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Pentax K-3 DNG 2013 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Pentax K-3 II († see note) DNG 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-5 DNG 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-5 II DNG 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-5 IIs DNG 2012 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Pentax K-7 PEF 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-3 Mark III DNG 2020 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-3 Mark III Monochrome DNG 2022 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash) +Pentax K-30 DNG 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-50 DNG 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-70 PEF 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-500 DNG 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax KP DNG 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-r PEF 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-S1 DNG 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-S2 DNG 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-x PEF 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K10D DNG 2005 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Pentax K20D DNG 2007 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K100D PEF 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K100D Super PEF 2007 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K110D PEF 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K200D DNG 2007 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K2000 K-m PEF 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax KF PEF 2022 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax MX-1 DNG 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax Q DNG 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax Q7 DNG 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax Q10 DNG 2012 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Pentax QS-1 DNG 2013 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Pixii PIXII Camera (A1571) DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Phase One H 20 TIF 2007 unsupported TIF is not imported +Phase One H 25 TIF 2007 unsupported TIF is not imported +Phase One IQ 140 IIQ 2010 preview IIQ is opened from its embedded JPEG only +Phase One IQ 160 IIQ 2010 preview IIQ is opened from its embedded JPEG only +Phase One IQ 150 IIQ 2015 preview IIQ is opened from its embedded JPEG only +Phase One IQ 180 IIQ 2010 preview IIQ is opened from its embedded JPEG only +Phase One IQ 250 IIQ 2013 preview IIQ is opened from its embedded JPEG only +Phase One IQ 260 IIQ 2013 preview IIQ is opened from its embedded JPEG only +Phase One IQ 280 IIQ 2013 preview IIQ is opened from its embedded JPEG only +Phase One IQ3 50MP IIQ 2015 preview IIQ is opened from its embedded JPEG only +Phase One IQ3 60MP IIQ 2015 preview IIQ is opened from its embedded JPEG only +Phase One IQ3 80MP IIQ 2015 preview IIQ is opened from its embedded JPEG only +Phase One IQ3 100MP IIQ 2015 preview IIQ is opened from its embedded JPEG only +Phase One IQ3 100MP Trichromatic (*see note) IIQ 2017 preview IIQ is opened from its embedded JPEG only +Phase One IQ4 150MP (**see note) IIQ 2019 preview IIQ is opened from its embedded JPEG only +Phase One IQ4 150MP Achromatic (** see note) IIQ 2020 preview IIQ is opened from its embedded JPEG only +Phase One P 20 TIF 2007 unsupported TIF is not imported +Phase One P 20+ TIF 2007 unsupported TIF is not imported +Phase One P 21 TIF 2007 unsupported TIF is not imported +Phase One P 21+ TIF 2007 unsupported TIF is not imported +Phase One P 25 TIF 2007 unsupported TIF is not imported +Phase One P 25+ TIF 2007 unsupported TIF is not imported +Phase One P 30 TIF 2007 unsupported TIF is not imported +Phase One P 30+ TIF 2007 unsupported TIF is not imported +Phase One P 40+ IIQ 2010 preview IIQ is opened from its embedded JPEG only +Phase One P 45 TIF 2007 unsupported TIF is not imported +Phase One P 45+ TIF 2007 unsupported TIF is not imported +Phase One P 65+ IIQ 2010 preview IIQ is opened from its embedded JPEG only +Ricoh GR DNG 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Ricoh GR II DNG 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Ricoh GR III DNG 2018 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Ricoh GR IIIx DNG 2021 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Ricoh GR III HDF DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Ricoh GR IIIx HDF DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Ricoh GR IV DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Ricoh GR IV Monochrome DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Ricoh GR Digital DNG 2003 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Ricoh GR Digital II DNG 2003 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Ricoh GR Digital III DNG 2008 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Ricoh GR Digital IV DNG 2010 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Ricoh GX100 DNG 2003 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Ricoh GX200 DNG 2003 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash) +Ricoh GXR with GR LENS A12 28mm F2.5 DNG 2010 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Ricoh GXR with GR LENS A12 50mm F2.5 MACRO DNG 2008 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Ricoh GXR with GR LENS A16 24-85 F3.5-5.5 DNG 2010 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Ricoh GXR with GR LENS S10 24-72mm F2.5-4.4 VC DNG 2008 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Ricoh GXR with GR LENS Mount A12 DNG 2010 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Ricoh THETA Z1 DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung EK-GN100 SRW 2012 preview compressed SRW is not decoded +Samsung EK-GN110 SRW 2012 preview compressed SRW is not decoded +Samsung EK-GN120 SRW 2012 preview compressed SRW is not decoded +Samsung EK-KN120 SRW 2012 preview compressed SRW is not decoded +Samsung EX2F SRW 2012 unverified uncompressed / packed SRW +Samsung NX300 SRW 2012 preview compressed SRW is not decoded +Samsung NX500 SRW 2015 preview compressed SRW is not decoded +Samsung NX1000 SRW 2012 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Samsung NX1100 SRW 2012 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Samsung NX2000 SRW 2012 preview compressed SRW is not decoded +Samsung NX3000 SRW 2013 preview compressed SRW is not decoded +Samsung NX3300 SRW 2015 preview compressed SRW is not decoded +Samsung NX U SRW 2012 unverified uncompressed / packed SRW +Samsung Pro 815 DNG 2003 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung TL350 (WB2000) SRW 2010 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Samsung TL500 (EX1) SRW 2010 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Samsung Galaxy Note 9 DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Note 20 Rear Main Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Note 20 Ultra Rear Main Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S20 Front Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S20 Rear Main Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S20+ Front Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S20+ Rear Main Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S20 Ultra Front Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S20 Ultra Rear Main Camera DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S21 Rear Main Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S21 Rear Wide Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S21+ Rear Main Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S21+ Rear Wide Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S21 Ultra Rear Main Camera DNG 2020 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Samsung Galaxy S21 Ultra Rear Wide Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S21 Ultra Super Telephoto Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S21 Ultra Telephoto Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S6 DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S6 Edge DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S7 DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S7 Edge DNG 2015 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Samsung Galaxy S8 DNG 2017 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Samsung Galaxy S8+ DNG 2017 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Samsung Galaxy S9 DNG 2017 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S9+ DNG 2017 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold3 Rear Main Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold3 Rear Telephoto Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold3 Rear Ultrawide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S22 Front Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S22 Rear Telephoto Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S22 Rear Ultrawide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S22 Rear Wide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S22+ Front Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S22+ Rear Telephoto Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S22+ Rear Ultrawide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S22+ Rear Wide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S22 Ultra Front Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S22 Ultra Rear Super Telephoto Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S22 Ultra Rear Telephoto Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S22 Ultra Rear Ultrawide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S22 Ultra Rear Wide Camera DNG 2021 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Samsung Galaxy S23 Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S23 Rear Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S23 Rear Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S23 Rear Wide Camera DNG 2022 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Samsung Galaxy S23+ Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S23+ Rear Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S23+ Rear Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S23+ Rear Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S23 Ultra Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S23 Ultra Rear Super Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S23 Ultra Rear Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S23 Ultra Rear Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S23 Ultra Rear Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S24 Front Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S24 Rear Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S24 Rear Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S24 Rear Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S24+ Front Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S24+ Rear Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S24+ Rear Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S24+ Rear Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S24 Ultra Front Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S24 Ultra Rear Super Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S24 Ultra Rear Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S24 Ultra Rear Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S24 Ultra Rear Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S25 Front Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S25 Rear Telephoto Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S25 Rear Ultrawide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S25 Rear Wide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S25+ Front Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S25+ Rear Telephoto Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S25+ Rear Ultrawide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S25+ Rear Wide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S25 Ultra Front Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S25 Ultra Rear Super Telephoto Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S25 Ultra Rear Telephoto Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S25 Ultra Rear Ultrawide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy S25 Ultra Rear Wide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold4 Rear Telephoto Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold4 Rear Ultrawide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold4 Rear Wide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold5 Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold5 Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold5 Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold5 Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold6 Front Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold6 Rear Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold6 Rear Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold6 Rear Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold Special Edition Front Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold Special Edition Rear Telephoto Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold Special Edition Rear Ultrawide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold Special Edition Rear Wide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Flip6 Rear Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Flip6 Rear Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Flip7 FE Front Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Flip7 FE Rear Ultrawide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Flip7 FE Wide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Flip7 Front Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Flip7 Rear Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Flip7 Rear Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold7 Front Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold7 Rear Telephoto Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold7 Rear Ultrawide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold7 Under-Display Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Fold7 Rear Wide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Trifold Front Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Trifold Rear Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Trifold Rear Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Samsung Galaxy Z Trifold Rear Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sigma BF DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sigma DP1 X3F 2007 preview X3F is opened from its embedded JPEG only +Sigma DP1s X3F 2008 preview X3F is opened from its embedded JPEG only +Sigma DP2 X3F 2008 preview X3F is opened from its embedded JPEG only +Sigma fp DNG 2019 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Sigma fp L DNG 2020 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Sigma SD9 X3F 2003 preview X3F is opened from its embedded JPEG only +Sigma SD10 X3F 2003 preview X3F is opened from its embedded JPEG only +Sigma SD14 X3F 2007 preview X3F is opened from its embedded JPEG only +Skydio Skydio 2 DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 5 IV Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 5 IV Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 5 IV Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 V Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 V Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 V Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 V Zoom Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony A1 II (ILCE-1M2) (** see note) ARW 2024 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Sony A7 (ILCE-7) ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7 II (ILCE-A7M2) ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies +Sony A7 III (ILCE-7M3) ARW 2017 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7 IV (ILCE-7M4) ARW 2021 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7 V (ILCE-7M5) (** see note) ARW 2025 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Sony A7C (ILCE-7C) ARW 2020 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7C II (ILCE-7CM2) ARW 2023 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7CR (ILCE-7CR) ARW 2023 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7R (ILCE-7R) ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7R II (ILCE-7RM2) ARW 2015 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7R III (ILCE-7RM3) ARW 2017 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7R IV (ILCE-7RM4) ARW 2018 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7S (ILCE-7S) ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7S II (ILCE-7SM2) ARW 2015 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7s III (ILCE-7SM3) ARW 2020 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A9 (ILCE-9) ARW 2015 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A9 II (ILCE-9M2) ARW 2019 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A9 III (ILCE-9M3) ARW 2023 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A33 (SLT-A33) ARW 2010 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A35 (SLT-A35) ARW 2010 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A37 (SLT-A37) ARW 2012 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A55V (SLT-A55V) ARW 2010 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies +Sony A57 (SLT-A57) ARW 2012 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A58 (SLT-A58) ARW 2012 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A65 (SLT-A65) ARW 2010 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A68 (ILCA-68) ARW 2015 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A77 (SLT-A77) ARW 2010 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A77 II (ILCA-77M2) ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7R IIIA (ILCE-7RM3A) ARW 2020 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7R IVA (ILCE-7RM4A) ARW 2020 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7R V (ILCE-7RM5) ARW 2022 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7R VI (ILCE-7RM6) (** see note) ARW 2025 unverified ARW decoder (uncompressed, cRAW, packed 12-bit, lossless, downsized YCbCr) +Sony A99 (SLT-A99V) ARW 2012 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies +Sony A99 II (ILCA-99M2) ARW 2015 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A100 (DSLR-A100) ARW 2005 unsupported issue #702: regression, refused as not a readable photo +Sony A200 (DSLR-A200) ARW 2007 preview issue #535: preview-only on the raw.pixls.us sample +Sony A230 (DSLR-A230) ARW 2008 preview issue #535: preview-only on the raw.pixls.us sample +Sony A290 (DSLR-A290) ARW 2010 preview issue #535: preview-only on the raw.pixls.us sample +Sony A300 (DSLR-A300) ARW 2007 preview issue #535: preview-only on the raw.pixls.us sample +Sony A330 (DSLR A330) ARW 2008 preview issue #535: preview-only on the raw.pixls.us sample +Sony A350 (DSLR-A350) ARW 2007 preview issue #535: preview-only on the raw.pixls.us sample +Sony A380 (DSLR-A380) ARW 2008 preview issue #535: preview-only on the raw.pixls.us sample +Sony A390 (DSLR-A390) ARW 2010 preview issue #535: preview-only on the raw.pixls.us sample +Sony A450 (DSLR-A450) ARW 2010 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A500 (DSLR-A500) ARW 2008 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A550 (DSLR-A550) ARW 2008 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A560 (DSLR-A560) ARW 2010 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A580 (DSLR-A580) ARW 2010 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A700 (DSLR-A700) ARW 2007 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A850 (DSLR-A850) ARW 2008 preview issue #535: preview-only on the raw.pixls.us sample +Sony A900 (DSLR-A900) ARW 2008 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A3000 (ILCE-3000) ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A3500 (ILCE-3500) ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A5000 (ILCE-5000) ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A5100 (ILCE-5100) ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A6000 (ILCE-6000) ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A6100 (ILCE-6100) ARW 2019 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A6300 (ILCE-6300) ARW 2015 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A6400 (ILCE-6400) ARW 2018 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A6500 (ILCE-6500) ARW 2015 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A6600 (ILCE-6600) ARW 2019 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A6700 (ILCE-6700) ARW 2022 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony DSC-F828 SRF 2003 preview 2026-10-12 release CLI: raw.pixls.us DSC06227.SRF imports as preview-only through recursive and direct import (docs/raw-parity.md) +Sony DSC-HX95 ARW 2018 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony DSC-HX99 ARW 2018 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony DSC-V3 SRF 2005 unsupported SRF is discovered; sensor decoding unsupported, usable preview not verified on a real file +Sony DSC-RX100M7A ARW 2024 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Sony FX3 (ILME-FX3) ARW 2020 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony FX30 (ILME-FX30) ARW 2022 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-3 ARW 2010 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-3N ARW 2012 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-5 ARW 2010 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-5N ARW 2010 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-5R ARW 2012 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-5T ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-6 ARW 2012 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-7 ARW 2010 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-C3 ARW 2010 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-F3 ARW 2012 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-VG20 ARW 2012 unverified ARW decoder (uncompressed, cRAW, packed 12-bit, lossless, downsized YCbCr) +Sony NEX-VG30 ARW 2012 unverified ARW decoder (uncompressed, cRAW, packed 12-bit, lossless, downsized YCbCr) +Sony NEX-VG900 ARW 2012 unverified ARW decoder (uncompressed, cRAW, packed 12-bit, lossless, downsized YCbCr) +Sony RX0 (DSC-RX0) ARW 2017 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX0 II (DSC-RX0M2) ARW 2018 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX1 (DSC-RX1) ARW 2012 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX1R (DSC-RX1R) ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX1R II (DSC-RX1RM2) ARW 2015 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX10 (DSC-RX10) ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX10 II (DSC-RX10M2) ARW 2015 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX10 III (DSC-RX10M3) ARW 2015 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX10 IV (DSC-RX10M4) ARW 2017 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX10 V (DSC-RX10M5) (** see note) ARW 2025 unverified ARW decoder (uncompressed, cRAW, packed 12-bit, lossless, downsized YCbCr) +Sony RX100 (DSC-RX100) ARW 2012 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX100 II (DSC-RX100M2) ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX100 III (DSC-RX100M3) ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX100 IV (DSC-RX100M4) ARW 2015 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX100 V (DSC-RX100M5) ARW 2015 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX100 VA (DSC-RX100M5A) ARW 2017 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX100 VI (DSC-RX100M6) ARW 2017 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX100 VII (DSC-RX100M7) ARW 2018 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony QX1 (ILCE-QX1) ARW 2013 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony ZV-1 (DCZV1/B) ARW 2019 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony Alpha 1 (ILCE-1)* ARW 2020 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony Xperia 1 Wide-angle Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 II Telephoto Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 II Ultra wide-angle Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 II Wide-angle Camera DNG 2020 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 IV Wide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 IV Zoom Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 IV Ultrawide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 VI Front Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 VI Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 VI Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 VI Zoom Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 VII Front Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 VII Ultrawide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 VII Wide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 VII Zoom Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 III Telephoto Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 III Ultra wide-angle Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 1 III Wide-angle Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 5 III Super Telephoto Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 5 III Telephoto Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 5 III Ultra wide-angle Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 5 III Wide-angle Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 5 V Front Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 5 V Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia 5 V Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony Xperia Pro-I Rear Wide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Sony ZV-E10 ARW 2021 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony ZV-E1 ARW 2022 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony ZV-E10M2 ARW verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Sony ZV-1M2 ARW 2022 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Sony ZV-1A ARW 2024 unverified ARW decoder (uncompressed, cRAW, packed 12-bit, lossless, downsized YCbCr) +Sony ILX-LR1 ARW 2023 unverified ARW decoder (uncompressed, cRAW, packed 12-bit, lossless, downsized YCbCr) +Sony ILCE - 1M2 (Final Support) ARW 2024 unverified ARW decoder (uncompressed, cRAW, packed 12-bit, lossless, downsized YCbCr) +Sony ILCE - 6100A ARW 2024 unverified ARW decoder (uncompressed, cRAW, packed 12-bit, lossless, downsized YCbCr) +Sony ILCE - 6400A ARW 2024 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Sony FX2 (ILMEFX2) (** see note) ARW 2024 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Sony FX3A (ILMEFX3A) ARW 2024 unverified ARW decoder (uncompressed, cRAW, packed 12-bit, lossless, downsized YCbCr) +Sony RX1R III (DSC-RX1RM3) ARW 2024 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Xiaomi Mi 11 Lite Front Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Mi 11 Lite Rear Ultrawide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Mi 11 Lite Rear Wide Camera DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 12S DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 12S Pro DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 12S Ultra DNG 2021 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13 Rear Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13 Rear Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13 Rear Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13 Pro Rear Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13 Pro Rear Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13 Pro Rear Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13 Ultra Rear Super Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13 Ultra Rear Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13 Ultra Rear Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13 Ultra Rear Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13T Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13T Rear Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13T Rear Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13T Rear Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13T Pro Front Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13T Pro Rear Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13T Pro Rear Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 13T Pro Rear Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14 Rear Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14 Rear Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14 Rear Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14 Pro Rear Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14 Pro Rear Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14 Pro Rear Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14 Ultra Rear Super Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14 Ultra Rear Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14 Ultra Rear Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14 Ultra Rear Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14T Front Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14T Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14T Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14T Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14T Pro Front Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14T Pro Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14T Pro Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 14T Pro Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi MIX Fold 3 Rear Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi MIX Fold 3 Rear Super Telephoto Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi MIX Fold 3 Rear Ultrawide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi MIX Fold 3 Rear Wide Camera DNG 2022 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi Civi 4 Pro Rear Telephoto Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi Civi 4 Pro Rear Ultrawide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi Civi 4 Pro Rear Wide Camera DNG 2023 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 15 Rear Telephoto Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 15 Rear Ultrawide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 15 Rear Wide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 15 Pro Rear Telephoto Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 15 Pro Rear Ultrawide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 15 Pro Rear Wide Camera DNG 2024 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 17 Ultra Rear Telephoto Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 17 Ultra Rear Ultrawide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Xiaomi Xiaomi 17 Ultra Rear Wide Camera DNG 2025 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Yuneec CGO4 DNG 2015 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Yuneec ION L1 Pro DNG 2018 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) +Zeiss ZX1 DNG 2019 unverified DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw) diff --git a/crates/raw/docs/coverage/evidence.tsv b/crates/raw/docs/coverage/evidence.tsv new file mode 100644 index 000000000..de71f44c6 --- /dev/null +++ b/crates/raw/docs/coverage/evidence.tsv @@ -0,0 +1,580 @@ +# Per-model evidence that overrides the format rules in coverage.py. status: verified | preview | unsupported. +# "verified" means a real file from this body decoded from sensor data in a corpus test or a recorded bulk check (source column). +maker camera status source +Apple iPhone 12 Pro Rear Wide Camera verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS 40D verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS 550D (EOS Digital Rebel T2i, EOS Kiss X4 Digital) verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS 5D Mark II verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS 5DS R verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS 6D verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS 7D verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS 80D verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS M50 (EOS Kiss M) verified corpus: crates/raw/tests/it/cr3_corpus.rs (exact sensor hashes, docs/cr3.md) +Canon EOS R100 verified corpus: crates/raw/tests/it/cr3_corpus.rs (exact sensor hashes, docs/cr3.md) +Canon EOS R5 Mark II verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hashes, docs/cr3.md); bulk: QP maps of 33,665 C-RAW files +Canon EOS R8 verified corpus: crates/raw/tests/it/cr3_corpus.rs (exact sensor hashes, docs/cr3.md) +Canon EOS-1D X Mark III verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS 90D verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS 250D (EOS Rebel SL3, EOS 200D II, EOS Kiss X10) verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS 850D (EOS Kiss X10i, EOS Rebel T8i) verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS M200 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS M50 Mark II verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS M6 Mark II verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R (** see note) verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R3 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R5 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R6 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R6 Mark II verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md; C-RAW with adaptive QP above 168 still opens as the preview) +Canon EOS R6 Mark III verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R7 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md; lossless only, C-RAW unverified) +Canon EOS R10 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS RP verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R50 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS R50 V verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon PowerShot G5 X Mark II verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon PowerShot G7 X Mark III verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon PowerShot SX70 HS verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon PowerShot V1 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md) +Canon EOS-5D Mark III verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS 70D verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 800D (EOS Digital Rebel T7i, EOS Kiss X9i) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G9X verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 3000D (EOS Rebel T100, EOS 4000D) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 1000D (EOS Digital Rebel XS, EOS Kiss F) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G1 X Mark II verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 500D (EOS Digital Rebel T1i, EOS Kiss X3 Digital) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 400D (EOS Digital Rebel XTi, EOS Kiss Digital X) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot S110 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 1300D (EOS Digital Rebel T6, EOS Kiss X80) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 600D (EOS Digital Rebel T3i, EOS Kiss X5 Digital) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G1 X verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G16 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G3 X verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS M verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS-1D X Mark II verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 750D (EOS Digital Rebel T6i, EOS Kiss X8i) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 5DS verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot S100V verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 350D (EOS Digital Rebel XT, EOS Kiss Digital N) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 1200D (EOS Digital Rebel T5, EOS Kiss X70) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G11 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 100D (EOS Digital Rebel SL1, EOS Kiss X7) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G15 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS-1D Mark IV verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS M5 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot S100 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 650D (EOS Digital Rebel T4i, EOS Kiss X4i) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot SX50 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS M2 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS M3 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G12 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 6D Mark II verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS M10 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 700D (EOS Digital Rebel T5i, EOS Kiss X7i) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 1100D (EOS Digital Rebel T3, EOS Kiss X50 Digital) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 77D (EOS 9000D) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 1500D (EOS Rebel T7, EOS Kiss X90, EOS 2000D) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS-5D Mark IV (** see note) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS-1D Mark III verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 60D verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS-1D Mark II N verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS M100 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G5 X verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G9 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 5D verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS M6 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G7 X Mark II verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 7D Mark II verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot S120 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot S95 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G1 X Mark III verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 50D verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 20D verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G7 X verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 450D (EOS Digital Rebel XSi, EOS Kiss Digital X2) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot SX60 HS verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 200D (EOS Kiss X9, EOS Rebel SL2) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon PowerShot G9 X Mark II verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 760D (EOS Digital Rebel T6s, EOS Kiss 8000D) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Canon EOS 30D verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm GFX 100 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm GFX 100RF (Final Support) verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm GFX 100S verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm GFX 50S verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-A2 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-A5 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-E1 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-E5 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-H2 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-H2S verified docs/raf-compression.md (supplied files, exact sensor arrays) +Fujifilm X-M5 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-T2 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-T20 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-T4** verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-T5 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Fujifilm X-T50 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Google Pixel 2 XL verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Hasselblad Lunar verified issue #1275: raw.pixls.us 4051 (a Sony NEX-7 ARW under Make HASSELBLAD) decodes from sensor data; black 512 and SR2SubIFD WB as on a NEX-7 +Leica C (Typ 112) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica D-Lux (Typ 109) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica D-Lux 3 verified PR #215: raw.pixls.us bulk check of every RW2 / RWL / RAW body +Leica D-Lux 4 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica D-Lux 5 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica D-Lux 6 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica D-Lux 7 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica Digilux 2 verified PR #215: raw.pixls.us bulk check of every RW2 / RWL / RAW body +Leica Digilux 3 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica Q3 verified recorded check 2026-10-11: 38 private Q3 DNGs (landscape and portrait) decoded from sensor data and developed at full size without error, mean CIE76 dE to the camera JPEG 6.3-13.9 at 400 px (not committed; docs/raw-parity.md) +Leica V-LUX (Typ 114) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica V-LUX 1 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica V-LUX 4 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica V-LUX 5 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Leica M Monochrom verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash) +Leica CL verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica M10 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica C-Lux verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica M Monochrom (Typ 246) verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash) +Leica M8 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash) +Leica X2 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica Q2 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica SL (Typ 601) verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica M10-R verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica SL2 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Leica M9 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash) +Leica Q (Typ 116) verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon COOLPIX B700 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Nikon D5100 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Nikon D7000 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Nikon D7500 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Nikon Z f verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Nikon D700 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm XF10 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-A10 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D5300 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T100 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-Pro1 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 V1 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 J4 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm GFX 100S II verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D610 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D5 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D2H verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z 6 II verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon COOLPIX P1000 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix X100 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X30 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix X10 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D2Xs verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D4 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D800 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D780 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D850 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-E2S verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D500 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm GFX 50R verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Df verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-S10 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 V2 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 V3 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3s verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z fc verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D810 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-A3 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D40 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D800E verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3X verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X100V** verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D4S verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T30 III verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm XQ2 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T10 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D2X verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X70 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D80 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-H1 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 J5 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 J1 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D7200 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T30 II verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z 50 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D5000 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm XQ1 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix HS30 EXR verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D70S verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D5200 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon COOLPIX P1100 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-S20 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z 30 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-E2 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D60 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 J2 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D5600 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D300 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix SL1000 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-Pro3** verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-Pro2 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 S1 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X100VI verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T200 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3200 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T3 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix F550 EXR verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D5500 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-A7 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3300 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z 5 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon 1 J3 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z 9 (*See note) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon COOLPIX A1000 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X100S verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D2Hs verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D90 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D50 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3000 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X100T verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z 50 II verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D600 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm XF1 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D7100 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-E3 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-A1 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3500 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix HS50 EXR verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm GFX 100 II verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X100F verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D40x verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon COOLPIX A verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X20 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-E4 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm GFX 50S II verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D1H verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D200 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix HS33 EXR verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix S1 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon Z 7 II verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D70 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D300s verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix X-S1 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D750 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3100 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T1 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon D3400 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix X-M1 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm X-T30 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon COOLPIX P950 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Fujifilm FinePix F770 EXR verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Nikon COOLPIX 5700 unsupported bulk: raw.pixls.us CC0 file decodes wrongly (two stacked half-height fields, CMYG mosaic read as RGB; green vs camera JPEG r 0.12-0.36) +Nikon COOLPIX 8400 unsupported bulk: raw.pixls.us CC0 file decodes wrongly (two stacked half-height fields, CMYG mosaic read as RGB; green vs camera JPEG r 0.12-0.36) +Nikon COOLPIX 8800 unsupported bulk: raw.pixls.us CC0 file decodes wrongly (two stacked half-height fields, CMYG mosaic read as RGB; green vs camera JPEG r 0.12-0.36) +Olympus E-1 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Olympus EVOLT E-400 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Olympus Stylus XZ-2 iHS verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Panasonic LUMIX DC - G97 (DC-G99M2) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-FZ1000M2 (DC-FZ10002) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-FZ80 (DC-FZ82, DC-FZ85) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-G100 (DC-G110) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-G100D verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-G9 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-G95D verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-G99 (DC-G90, DC-G91, DC-G95) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GF10 (DC-GF90) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GF9 (DC-GX850, DC-GX800) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GH5 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GH5M2 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GH5s verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GH6 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GH7 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GX880 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-GX9 (DC-GX7MK3) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-LX100 II verified PR #215: raw.pixls.us bulk check of every RW2 / RWL / RAW body +Panasonic LUMIX DC-S1 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S1H verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S1M2 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S1M2ES (Final Support) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S1R verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S1RM2 (Final Support) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S5 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S5M2 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S5M2X verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-S9 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-TX2D (DC-TZ200D, DC-TZ202D, DC-TZ220D, DC-ZS200D, DC-ZS220D) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-ZS70 (DC-TZ90, DC-TZ91, DC-TZ92, DC-TZ93) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-ZS80 (DC-TZ95, DC-TZ96, DC-TZ97) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DC-ZS80D (DC-TZ95D, DC-TZ96D) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DCG9M2 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-CM1 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FX150 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ100 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ1000 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ150 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ18 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ200 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ2500 (DMC-FZ2000, DMC-FZH1) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ28 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ30 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ300 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ330 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ35 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ38 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ40 (DMC-FZ45) verified PR #215: raw.pixls.us bulk check of every RW2 / RWL / RAW body +Panasonic LUMIX DMC-FZ50 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ70 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ72 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-FZ8 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G1 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G10 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G2 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G3 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G5 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G6 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G7 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-G8 (DMC-G80, DMC-G81, DMC-G85) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GF1 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GF2 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GF3 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GF5 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GF6 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GF7 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GF8 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GH1 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GH2 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GH3 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GH4 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GM1 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GM1S verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GM5 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GX1 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GX7 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GX8 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-GX85 (DMC-GX80, DMC-GX7MK2) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-L1 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-L10 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LF1 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LX1 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LX100 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LX2 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LX3 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LX5 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LX7 verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-LX9 (DMC-LX10, DMC-LX15) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-ZS100 (DMC-ZS110, DMC-TZ100, DMC-TZ101, DMC-TZ110, DMC-TX1) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-ZS40 (DMC-TZ60, DMC-TZ61) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-ZS50 (DMC-TZ70, DMC-TZ71) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX DMC-ZS60 (DMC-TZ80, DMC-TZ81, DMC-TZ85) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Panasonic LUMIX ZS200 (DC-TX2, DC-TZ200, DC-TZ202, DC-TZ220, DC-ZS220) verified PR #215: 178 raw.pixls.us files from 118 Panasonic / Leica bodies, every RW2 format +Pentax K-3 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Pentax K-5 IIs verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Pentax K10D verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Ricoh GR III verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Pentax K-1 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax *ist DL verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Ricoh GX200 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash) +Pentax KP verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-3 Mark III Monochrome verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash) +Pentax K-70 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Ricoh GR IIIx verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K20D verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax *ist DS verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-3 Mark III verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-01 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K110D verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax Q7 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-S1 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax *ist D verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-30 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K200D verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-5 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-x verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Ricoh GR verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax 645D verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K100D verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-500 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K2000 K-m verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-50 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax 645Z verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-5 II verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax Q verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax MX-1 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K100D Super verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Ricoh GR II verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-S2 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-r verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax KF verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-3 II († see note) verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Pentax K-7 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check) +Samsung TL500 (EX1) verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Sony A100 (DSLR-A100) preview issue #702 fixed (#774): opens from its embedded JPEG; its raw (no CFA image IFD) is not decoded +Sony A200 (DSLR-A200) preview issue #535: preview-only on the raw.pixls.us sample +Sony A230 (DSLR-A230) preview issue #535: preview-only on the raw.pixls.us sample +Sony A290 (DSLR-A290) preview issue #535: preview-only on the raw.pixls.us sample +Sony A300 (DSLR-A300) preview issue #535: preview-only on the raw.pixls.us sample +Sony A3000 (ILCE-3000) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A33 (SLT-A33) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A330 (DSLR A330) preview issue #535: preview-only on the raw.pixls.us sample +Sony A35 (SLT-A35) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A350 (DSLR-A350) preview issue #535: preview-only on the raw.pixls.us sample +Sony A3500 (ILCE-3500) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A37 (SLT-A37) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A380 (DSLR-A380) preview issue #535: preview-only on the raw.pixls.us sample +Sony A390 (DSLR-A390) preview issue #535: preview-only on the raw.pixls.us sample +Sony A450 (DSLR-A450) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A500 (DSLR-A500) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A5000 (ILCE-5000) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A5100 (ILCE-5100) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A550 (DSLR-A550) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A55V (SLT-A55V) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies +Sony A560 (DSLR-A560) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A57 (SLT-A57) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A58 (SLT-A58) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A580 (DSLR-A580) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A6000 (ILCE-6000) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A6100 (ILCE-6100) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A6300 (ILCE-6300) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A6400 (ILCE-6400) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A65 (SLT-A65) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A6500 (ILCE-6500) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A6600 (ILCE-6600) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A6700 (ILCE-6700) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A68 (ILCA-68) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7 (ILCE-7) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7 II (ILCE-A7M2) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies +Sony A7 III (ILCE-7M3) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7 IV (ILCE-7M4) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A700 (DSLR-A700) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A77 (SLT-A77) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A77 II (ILCA-77M2) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7C (ILCE-7C) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7C II (ILCE-7CM2) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7CR (ILCE-7CR) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7R (ILCE-7R) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7R II (ILCE-7RM2) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7R III (ILCE-7RM3) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7R IIIA (ILCE-7RM3A) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7R IV (ILCE-7RM4) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7R IVA (ILCE-7RM4A) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7R V (ILCE-7RM5) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7S (ILCE-7S) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7S II (ILCE-7SM2) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A7s III (ILCE-7SM3) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A850 (DSLR-A850) preview issue #535: preview-only on the raw.pixls.us sample +Sony A9 (ILCE-9) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A9 II (ILCE-9M2) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A9 III (ILCE-9M3) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A900 (DSLR-A900) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony A99 (SLT-A99V) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies +Sony A99 II (ILCA-99M2) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony Alpha 1 (ILCE-1)* verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony DSC-F828 preview 2026-10-12 release CLI: raw.pixls.us DSC06227.SRF imports as preview-only through recursive and direct import (docs/raw-parity.md) +Sony DSC-HX95 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony DSC-HX99 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony DSC-V3 unsupported SRF is discovered; sensor decoding unsupported, usable preview not verified on a real file +Sony FX3 (ILME-FX3) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony FX30 (ILME-FX30) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-3 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-3N verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-5 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-5N verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-5R verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-5T verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-6 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-7 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-C3 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony NEX-F3 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony QX1 (ILCE-QX1) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX0 (DSC-RX0) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX0 II (DSC-RX0M2) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX1 (DSC-RX1) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX10 (DSC-RX10) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX10 II (DSC-RX10M2) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX10 III (DSC-RX10M3) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX10 IV (DSC-RX10M4) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX100 (DSC-RX100) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX100 II (DSC-RX100M2) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX100 III (DSC-RX100M3) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX100 IV (DSC-RX100M4) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX100 V (DSC-RX100M5) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX100 VA (DSC-RX100M5A) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX100 VI (DSC-RX100M6) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX100 VII (DSC-RX100M7) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX1R (DSC-RX1R) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony RX1R II (DSC-RX1RM2) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony ZV-1 (DCZV1/B) verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony ZV-E1 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony ZV-E10 verified issue #535: raw.pixls.us bulk check of all 100 Sony bodies (decodes; BL/WL/crop/WB vs ExifTool) +Sony ZV-1M2 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Sony ZV-E10M2 verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Sony RX1R III (DSC-RX1RM3) verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Sony A1 II (ILCE-1M2) (** see note) verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Sony DSC-RX100M7A verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Sony A7 V (ILCE-7M5) (** see note) verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Sony FX2 (ILMEFX2) (** see note) verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Sony ILCE - 6400A verified corpus: crates/raw/tests/it/sony_leica_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus SP-550 UZ verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus E-10 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash) +Olympus Camedia C-5060 Zoom verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus SP-565 UZ verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus OM-D E-M5 Mark II verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus EVOLT E-300 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus Stylus SH-2 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus Camedia C-7070 Wide Zoom verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus Stylus XZ-10 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus EVOLT E-330 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus SP-570 UZ verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus SP-510 UZ verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus PEN-F verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Olympus EVOLT E-500 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Samsung TL350 (WB2000) verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Samsung NX1000 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Samsung NX1100 verified corpus: crates/raw/tests/it/olympus_samsung_corpus.rs (pinned sensor hash, camera-JPEG check) +Sony A100 (DSLR-A100) unsupported issue #702: regression, refused as not a readable photo +Sony DSC-F828 preview 2026-10-12 release CLI: raw.pixls.us DSC06227.SRF imports as preview-only through recursive and direct import (docs/raw-parity.md) +Epson R-D1 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Epson R-D1s verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Epson R-D1x verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Mamiya ZD verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Apple iPhone 6s Plus verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Apple iPhone 7 Plus verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Apple iPhone XS verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +DJI DJI Mavic 2 Pro (Hasselblad L1D-20c) verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +DJI DJI Mavic 3 (Hasselblad L2D-20c) verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +DJI DJI Mavic 4 Pro (DJI FC9287) verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +DJI DJI Mini 2 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Google Pixel 4 XL verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Google Pixel 4a Rear Wide Camera verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Google Pixel 6 Pro Rear Main Camera verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Google Pixel 8 Pro Wide Camera* verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Hasselblad CFV-50c verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); decode verified, not colour calibration +Hasselblad H5D-40 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); decode verified, not colour calibration +Hasselblad H5D-50c verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); decode verified, not colour calibration +Hasselblad X1D verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); decode verified, not colour calibration +Hasselblad X1D II 50C verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); decode verified, not colour calibration +Hasselblad X2D 100C verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); decode verified, not colour calibration +Nikon D6 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); checked compressed NEF only; uncompressed remains preview-only (#671) +Nikon Z 6 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); checked compressed NEF only; uncompressed remains preview-only (#671) +Nikon Z 7 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); checked compressed NEF only; uncompressed remains preview-only (#671) +Nikon Z 8 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check); HE / HE* remain preview-only +Parrot Anafi verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Samsung Galaxy S21 Ultra Rear Main Camera verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Samsung Galaxy S22 Ultra Rear Wide Camera verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Samsung Galaxy S23 Rear Wide Camera verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Samsung Galaxy S7 Edge verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Samsung Galaxy S8 verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Samsung Galaxy S8+ verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Sigma fp verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Sigma fp L verified corpus: crates/raw/tests/it/coverage_sweep_corpus.rs (pinned sensor hashes); bulk: docs/raw/coverage-sweep.tsv (CC0, SHA-256, camera-image check) +Konica Minolta DiMAGE 7Hi verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Konica Minolta DiMAGE A1 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Konica Minolta DiMAGE A200 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Konica Minolta Maxxum 7D Dynax 7D verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Konica Minolta DiMAGE 7 verified raw.pixls.us check: CC0 sample decoded and its WB / CFA matched to the embedded preview (vendor/mrw.rs) +Konica Minolta DiMAGE A2 verified raw.pixls.us check: CC0 sample decoded and its WB / CFA matched to the embedded preview (vendor/mrw.rs) +Canon EOS D30 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon EOS D60 verified corpus: crates/raw/tests/it/corpus.rs; 18 maintainer files exact (craft-libs crates/crw/clean-room/03-verification.md) +Canon EOS 10D verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES; 3 files) +Canon EOS 300D (EOS Digital Rebel, EOS Kiss Digital) verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES; 3 files) +Canon PowerShot G1 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon PowerShot G6 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES) +Canon PowerShot G5 preview CRW entropy anomaly (craft-libs crates/crw/clean-room/02-crw-format.md section 4); embedded JPEG +Olympus OM-D E-M1 preview compressed ORF support withdrawn pending provenance review; embedded JPEG +Olympus OM-D E-M10 Mark III preview compressed ORF support withdrawn pending provenance review; embedded JPEG diff --git a/crates/raw/docs/coverage/usage-weights.tsv b/crates/raw/docs/coverage/usage-weights.tsv new file mode 100644 index 000000000..f3f57c5e0 --- /dev/null +++ b/crates/raw/docs/coverage/usage-weights.tsv @@ -0,0 +1,114 @@ +# Estimated share of Lightroom raw photographers by camera maker, and the popular bodies of the last ~8 years inside each. +# Sources (2026-10-10): Nikkei 2025 mirrorless production via PhotoRumors (Canon 2.3 M, Sony 1.8 M, Nikon 0.86 M, Fujifilm 0.75 M +# units, i.e. ~36 / 29 / 14 / 12 % of CIPA's 6.31 M); BCN 2025 Japanese retail via PetaPixel (Sony 29.9 %, Canon 27.4 %, Nikon 15.1 %); +# camera makers named in this repo's user issues (325 issues: Sony 33, Nikon 31, Canon 26, phones 23, Fujifilm 9, Panasonic/Leica 8, +# OM/Olympus 6, Pentax/Ricoh 6, Samsung NX 5). Canon and Nikon get extra weight for their installed base of DSLRs. +# Rows "maker" give the maker bucket's weight (percent of photographers); rows "body" give a popular body inside a bucket. +# Inside a bucket, the listed bodies share 80 % equally and every other Adobe-listed model of the bucket's makers shares 20 %. +kind bucket makers_or_camera weight +maker Canon Canon 33 +maker Sony Sony 27 +maker Nikon Nikon 16 +maker Fujifilm Fujifilm 10 +maker Panasonic Panasonic 4 +maker OM / Olympus Olympus,OM Digital Solutions 3 +maker Phones Apple,Google,Samsung,Xiaomi,Huawei,Oppo,OnePlus,LG,Motorola,Nokia 3 +maker Pentax / Ricoh Pentax,Ricoh 1.5 +maker Leica Leica 1 +maker DJI DJI 1 +maker Medium format and others Hasselblad,Phase One,Leaf,Mamiya,Sigma,Kodak,Konica Minolta,Epson,Casio,GoPro,Insta360,Zeiss,Pixii,Light,Skydio,Parrot,Yuneec,Contax,DxO 0.5 +body Canon EOS R6 Mark II 1 +body Canon EOS R5 1 +body Canon EOS R6 1 +body Canon EOS R7 1 +body Canon EOS R8 1 +body Canon EOS R10 1 +body Canon EOS R50 1 +body Canon EOS RP 1 +body Canon EOS R (** see note) 1 +body Canon EOS R5 Mark II 1 +body Canon EOS R6 Mark III 1 +body Canon EOS R3 1 +body Canon EOS R100 1 +body Canon EOS 90D 1 +body Canon EOS M50 (EOS Kiss M) 1 +body Canon EOS M50 Mark II 1 +body Canon EOS M6 Mark II 1 +body Canon EOS-5D Mark IV (** see note) 1 +body Canon EOS 6D Mark II 1 +body Canon EOS 80D 1 +body Canon EOS 250D (EOS Rebel SL3, EOS 200D II, EOS Kiss X10) 1 +body Canon EOS 1500D (EOS Rebel T7, EOS Kiss X90, EOS 2000D) 1 +body Sony A7 IV (ILCE-7M4) 1 +body Sony A7 III (ILCE-7M3) 1 +body Sony A7R V (ILCE-7RM5) 1 +body Sony A7R IV (ILCE-7RM4) 1 +body Sony A7C (ILCE-7C) 1 +body Sony A7C II (ILCE-7CM2) 1 +body Sony A7CR (ILCE-7CR) 1 +body Sony A6400 (ILCE-6400) 1 +body Sony A6700 (ILCE-6700) 1 +body Sony A6600 (ILCE-6600) 1 +body Sony A6100 (ILCE-6100) 1 +body Sony Alpha 1 (ILCE-1)* 1 +body Sony A9 III (ILCE-9M3) 1 +body Sony ZV-E10 1 +body Sony A7s III (ILCE-7SM3) 1 +body Sony RX100 VII (DSC-RX100M7) 1 +body Nikon Z 6 II 1 +body Nikon Z 6 III 1 +body Nikon Z 8 1 +body Nikon Z 9 (*See note) 1 +body Nikon Z f 1 +body Nikon Z 5 1 +body Nikon Z 50 1 +body Nikon Z 50 II 1 +body Nikon Z fc 1 +body Nikon Z 7 II 1 +body Nikon D850 1 +body Nikon D750 1 +body Nikon D7500 1 +body Nikon D780 1 +body Fujifilm X-T5 1 +body Fujifilm X-T4** 1 +body Fujifilm X-S20 1 +body Fujifilm X-S10 1 +body Fujifilm X-H2S 1 +body Fujifilm X-H2 1 +body Fujifilm X100V** 1 +body Fujifilm X100VI 1 +body Fujifilm X-T30 II 1 +body Fujifilm X-E4 1 +body Fujifilm X-T50 1 +body Fujifilm GFX 100S 1 +body Fujifilm X-M5 1 +body Panasonic LUMIX DC-S5 1 +body Panasonic LUMIX DC-S5M2 1 +body Panasonic LUMIX DC-GH6 1 +body Panasonic LUMIX DC-GH5M2 1 +body Panasonic LUMIX DCG9M2 1 +body Panasonic LUMIX DC-G9 1 +body Panasonic LUMIX DC-S1 1 +body Panasonic LUMIX DC-GX9 (DC-GX7MK3) 1 +body Panasonic LUMIX DMC-G8 (DMC-G80, DMC-G81, DMC-G85) 1 +body Panasonic LUMIX DC-LX100 II 1 +body OM / Olympus OM-D E-M1 Mark III 1 +body OM / Olympus OM-D E-M1 Mark II 1 +body OM / Olympus OM-D E-M10 Mark IV 1 +body OM / Olympus OM-D E-M5 Mark III 1 +body OM / Olympus PEN E-P7 1 +body OM / Olympus Stylus Tough TG-6 1 +body OM / Olympus OM System OM-1 1 +body OM / Olympus OM System OM-1 MARK II 1 +body OM / Olympus OM System OM-5 1 +body Pentax / Ricoh K-1 II (* see note) 1 +body Pentax / Ricoh K-3 Mark III 1 +body Pentax / Ricoh KF 1 +body Pentax / Ricoh K-70 1 +body Pentax / Ricoh GR III 1 +body Pentax / Ricoh GR IIIx 1 +body Leica Q3 1 +body Leica Q2 1 +body Leica M11 1 +body Leica SL2 1 +body Leica M10 1 diff --git a/crates/raw/docs/cr3.md b/crates/raw/docs/cr3.md new file mode 100644 index 000000000..40e375b54 --- /dev/null +++ b/crates/raw/docs/cr3.md @@ -0,0 +1,243 @@ +# Canon CR3 decoding + +> Moved from LightCraft (`docs/cr3.md`) with the decoder (`src/cr3.rs`, `src/crx.rs`, `src/crx_wavelet.rs`; the container parser is craft-metadata's `cr3` module). Mentions of LightCraft's app describe that app. + +The CR3 adapter reads the CRX Bayer track, `CMP1` compression descriptor, +`CDI1`/`IAD1` sensor geometry and timed `CTMD` maker notes. White balance, +black/white levels and the default crop come from the file. A Dual Pixel delta +track is never substituted for the primary Bayer track. + +The EOS R6 Mark III writes `ColorData` version 66, which keeps version 64's as-shot white balance (word 105) and +black level (word 383) offsets. Measured on 33 files from 12 shoots (ISO 100–12800, 1/1250 s–25 s): the as-shot +R, G, G, B quadruple is followed by its colour temperature (3271–7424 K across the shoots) and then the fixed +preset table (Daylight 5200 K, Shade 7000 K, Cloudy 6000 K, Tungsten 3200 K, ...), and the black levels equal the +masked-column levels in every file (512 at low ISO, 2047–2048 above). Word 661 holds 14351 or 13995, but it is not +the clipping point: highlights pile up at 16383 and run smoothly through the range above 14351, so version 66 +takes its white level from the data. + +The in-camera aspect ratio (3:2, 4:3, 16:9, 1:1) is not applied to the sensor data. The maker note's +`AspectInfo` (tag `0x009a`: aspect code, cropped width, height, left, top) gives the shot's rectangle inside the +recommended crop, and the decoder uses it as the default crop, so the developed image has the shape the photographer +framed (PowerShot SX70 HS, EOS 250D, PowerShot G5 X Mark II, EOS M6 Mark II and EOS M50 samples). A rectangle that +is missing or does not fit inside the crop is ignored. The embedded JPEG still shows the whole frame. + +The EOS R5 Mark II's APS-C crop-mode files (5376 x 3574) state an active area that starts in dark columns +(132..~256 sit at the black level) and stops ~130 columns short of the image, while their recommended crop +(5088 x 3392, the camera JPEG's size) lies on the image. When the recommended crop doesn't fit inside the stated +active area, it is used as the valid area: on raw.pixls.us 7883/7884 the render matches the camera JPEG +(high-pass correlation 0.99, was -0.13 with a black band on the left). No other of the 110 CC0 CR3s changes. + +The decoder is independent Rust code shared by desktop, CLI and WebAssembly; +it has no operating-system imaging dependency or external decoder. Its sources +are the public prose descriptions and patents cited in +`src/crx.rs`. Unsupported coding variants retain the existing +embedded-JPEG fallback and its visible warning when a JPEG is present. So does +any other CR3 decode error (a corrupt file, or a body whose CRX stream the +decoder misreads): every CR3 opened from its embedded JPEG before this decoder, +and not every body's variants are known, so a CR3 never fails to import or load +where a preview exists. HEVC-only preview tracks are recognized but are not +decoded. + +## Variant status + +The supported and rejected CRX coding variants. Every rejection is an explicit `RawError::Unsupported` whose +text is part of the contract: `crates/raw/tests/it/cr3_variants.rs` rebuilds each variant as a synthetic CR3 +fixture and asserts the exact string, and that header probing (`probe_info`, the import path) and full +decoding fail with the same error, because the engine shows that reason when it falls back to the embedded +preview. The same tests decode a control fixture at both edges of each accepted range, so a rejection can +only come from the gate named here. + +| Variant | Status | Gate | Exact rejection | +|---|---|---|---| +| Version `0x100`, four Bayer planes, encoding 0, wavelet levels 0 (lossless) or 3 (C-RAW, horizontal tiles) | Supported | `supports()` in `vendor/crx.rs` | none | +| Version `0x200`: single-tile 14-bit three-level C-RAW with adaptive quantization, QP map of any height (an odd height leaves the last QP row without a partner and it is used on its own), QP values 131–175 (169–175: EOS R6 Mark II) | Supported | `supports()`, `qp_map()` | none | +| Version other than `0x100`/`0x200`, or wavelet levels other than 0 and 3 | Rejected | `supports()` | `Canon CRX version {version:#x}, encoding {encoding}, {planes} planes, {levels} wavelet levels` | +| Bit depth outside 8–16 (the `CMP1` parser only accepts 1–16, so 1–7 is what can arrive) | Rejected | `supports()` | `Canon CRX {bits}-bit samples` | +| C-RAW with vertical tiles (tile height below the image height, three levels) | Rejected | `supports()` | `Canon CRX C-RAW with vertical tiles` | +| Version `0x200` that is not a single-tile 14-bit C-RAW | Rejected | `supports()` | `Canon CRX version 0x200 requires a single-tile 14-bit Bayer C-RAW image` | +| Extended `CMP1` header with a median bit depth (used by type-3 burst coding) | Rejected | `supports()` | `Canon CRX extended median-bit-depth header` | +| Rounded-plane coding or unexpected plane flags | Rejected | `validate_planes()` | `Canon CRX plane coding flags` | +| Partial-subband coding | Rejected | `validate_planes()` | `Canon CRX partial subband coding` | +| Adaptive quantization gain on a low-frequency band (bands 0–3) | Rejected | `validate_planes()` | `Canon CRX adaptive quantization subband parameters` | +| Uniform quantization value outside 4–44 | Rejected | `quant_step()`, called from `validate_planes()` | `Canon CRX quantization value {value}` | +| Adaptive QP value outside 131–175 | Rejected | `qp_map()` | `Canon CRX adaptive QP value {value}` | +| `ff11`/`ff12`/`ff13` markers in a `0x100` stream | Rejected | `layout()` | `Canon CRX ff11/ff12/ff13 marker family` | +| Marker header of the wrong length (8 bytes expected for `ff01`, `ff02`, `ff03` and `ff12`, 16 for `ff11` and `ff13`) | Rejected | `layout()` | `Canon CRX marker {marker:#x} header length {size}` | +| `0x200` tile header with a wrong magic, a nonzero reserved field, an empty QP map or QP padding above 7 | Rejected | `layout()` | `Canon CRX adaptive quantization tile header` | +| `0x200` band header with a nonzero reserved field | Rejected | `layout()` | `Canon CRX adaptive subband reserved field` | +| Container adapter: a track that is not four planes with encoding 0 | Rejected | `decode()` in `vendor/cr3.rs` | `Canon CR3 encoding {encoding} with {planes} planes` | + +`supports()` also rejects encoding other than 0, a plane count other than 4 and a CFA pattern above 3, but +through a CR3 container the adapter rejects the first two before the CRX layer is reached, and the `CMP1` +parser rejects the third for four-plane tracks. A descriptor the parser cannot accept at all (a zero +dimension, a tile larger than the image, a bit depth outside 1–16, a plane count outside 1–4 or a zero +header size) is reported as `Corrupt` (`CR3 raw track has an invalid CMP1 descriptor`), not as unsupported. +A gain on a low-frequency band cannot reach `adaptive_step()`, which has its own rejection +(`Canon CRX adaptive low-frequency quantization`): `validate_planes()` refuses it first. That gate is defence +in depth and is not tested separately. + +## Verified sensor decoding + +CRX version `0x100`, four Bayer planes and `ff01`/`ff02`/`ff03` marker headers +support lossless RAW and three-level C-RAW with horizontal tiles. Uniform +quantization values 4–43 are independently verified; 44 continues the sequence (step 102, checked against a measured step on an EOS R50 C-RAW file). Version `0x200` C-RAW +supports the `ff11`/`ff12`/`ff13` marker family, a single 14-bit Bayer tile and +its adaptive quantization map. C-RAW is quantized, so next to clipped highlights +its reconstruction can overshoot the sensor range by a few quantization steps +(up to 36 codes above 16383 in 17 CC0 files); those samples are clamped to the +CMP1 bit depth. A lossless sample outside the bit depth is still corrupt data. +Other unverified coding variants return explicit unsupported errors (the table above lists them all). Header probing validates the same coding variants and +tile/plane layout as full decoding. + +`crates/raw/tests/it/cr3_corpus.rs` checks every full-sensor sample against a +black-box reference, before cropping or colour processing. Public CC0 files +are downloaded by `cargo xtask corpus --download` and kept outside version +control. The exact inputs and reference hashes are: + +| File | Input SHA-256 | Sensor FNV-1a (u16 little-endian) | +|---|---|---| +| Canon M50 RAW, raw.pixls.us 4657 | `1ba9ad6b51b315b88820eb0fe5fd7f52bc136ba37b1a5ca76825ebfe25b18058` | `62261f0ba81cfcd2` | +| Canon M50 C-RAW, raw.pixls.us 2663 | `15384b775867ec4c42b11882837f1e368cedc0561832ffab271221e6bb80be4c` | `9aacbfe66f505e1b` | +| Canon R100 RAW, raw.pixls.us 7896 | `6d83217d58a5e6d2dabcf470430e91a16676b60531c025b81fa7453fc74b0521` | `cbe5299ab9c52630` | +| Canon R100 C-RAW, raw.pixls.us 7897 | `0b66842b2fe00329ebc05fe8ab9357ddbbe1a0a2d2e69a2893102de7fdf5c942` | `341c706b37c38bbf` | +| Canon R8 RAW, raw.pixls.us 6585 | `7d5c6dbb11ff7e6ee58715d90a20f5d801e8b103f4711ff6ea329ceafcef254c` | `63c1c6d8d1eb2312` | +| Canon R8 C-RAW, raw.pixls.us 6587 | `df33cf394573645ce03dea1b2e9f0b5cc2b7734e9e3391ee7114151414cf2812` | `f836a663a795a375` | +| Canon RP RAW, raw.pixls.us 4674 | `dcbd0f6f9929de2e2331b0fd10a9d01e7cfd04c5a5e4038456de1647d714cdf2` | `5cb0f46404b8b663` | +| Canon RP C-RAW, raw.pixls.us 4672 | `bd3270610593d37ff8a399908bb9406a4e32f9151d55bf19ac1cf8f10d787b46` | `f48df06ca1d67382` | +| Canon RP RAW, 1.6× crop, raw.pixls.us 4673 | `7d4ba45dc9264d598336133593c677b09774bcdf13b602ee770e72415a652a16` | `46808a9de3b3dcc7` | +| Canon RP C-RAW, 1.6× crop, raw.pixls.us 4671 | `4a859ab137f792aedaf3416b0aad1497ebaab02052a49453902e226699154aa7` | `39a60c4efed4e1f4` | + +The EOS R5 Mark II files (raw.pixls.us 7881–7884, CC0) are pinned to LightCraft's own decode; no external +decoder was run on them: + +| File | Input SHA-256 | Sensor FNV-1a (u16 little-endian) | +|---|---|---| +| Canon R5 Mark II RAW, raw.pixls.us 7881 | `d6e6aa3b493ea8936dff0efb0cc17c87471437e7317bce37c8c82ba08b9cab7b` | `ff716fb30248496f` | +| Canon R5 Mark II C-RAW, raw.pixls.us 7882 | `b3c08a5c7a97299dfd58896a12b939e7b43ec78682761812524825e6e5d087be` | `af32b45357b534e5` | +| Canon R5 Mark II crop-mode RAW, raw.pixls.us 7883 | `9f88c0ff012924193b7f4497559d133dfe626766d600b5d67689156721739ead` | `31cf630afe868958` | +| Canon R5 Mark II crop-mode C-RAW, raw.pixls.us 7884 | `6a597497776a6abbfab8fcf12d0086ac7a3f8afea2879d61accf6bbf48d357d7` | `4721605b20339332` | + +Reference pixels were observed from the DNGLab 0.8.0 macOS arm64 release +binary; no decoder source was read or copied, and it is not shipped or linked. +The release ZIP SHA-256 was +`ee70805cb60f18d5ed62548c4e595f6cee0a33407e15358eb1766a46afc1c16e`. +The RP references came from the same release's Windows x64 binary (ZIP SHA-256 +`d3add6b71568bf5f7938468b0b40a0ee1eaec01e0057fe78dcc453188010be9a`), after it +reproduced the M50 RAW hash above. The RP's 1.6× crop mode writes a single +4032×2656 tile, where full frame (6384×4224) uses two 3192-wide horizontal +tiles. All ten sensor comparisons pass. R8 HEVC preview tracks are distinguished from +the primary Bayer RAW; the HEVC preview itself is not decoded. Missing corpus +files are reported as skipped by the tests. + +In a tile with a neighbour on its right, the vertical high-pass bands of the level-1 and level-2 +low-pass columns (bands 8 and 5) are entropy coded at an odd width: an even band carries one extra +column that synthesis does not use. No other band is extended, including the last level's (band 2). +The decoder takes each band's coded width from this rule. It used to try the synthesis width and up to +two more columns and keep the first that decoded to the exact end of the stream, but two widths can +both do that and give different coefficients (#899). The rule was measured on every C-RAW plane in +`cr3_corpus.rs`: two-tile files at tile widths 2816, 3144, 3192, 3444 and 3564 (G5 X Mark II, M50, +R100, 250D, RP, R, 90D, M6 Mark II), whose left tiles extend bands 5 and 8 exactly where the rule says, +and single-tile `0x100` and `0x200` files, which never extend a band. In every plane, each band decodes +at its coded width and at no other width up to two columns wider, so all sensor hashes are unchanged. +No public file has three or more C-RAW tiles; a middle tile follows the same rule, which is unverified. + +The version `0x200` QP predictor and entropy framing were checked independently. +Controlled input mutations establish floor averaging of paired QP rows before +the nonlinear integer table, then multiplication, division by eight, addition +of the subband base and a minimum step of one. QP values 131–167 are verified; 168 (quantization +value 44, step 102, measured at about 100 on EOS R50 C-RAW files) continues the sequence and is +accepted. Other values are rejected. + +The QP map is coded as follows (established on the EOS R5 Mark II). Its first symbol is a 32-bit +big-endian literal, mapped like every other symbol and added to a prediction of zero. Every later +symbol is Rice coded with a limited-length escape: a quotient of 23 or more is written as 23 zeros, +the terminating 1 and the symbol in 8 bits. This is the escape layout of ITU-T T.87 for a 32-bit +code-length limit and 8-bit symbols, without that standard's minus-one offset. In the R8 maps only +the first symbol needs more than 15 leading zeros, and its bits read the same whether taken as a +32-bit literal or as an escape after 16 zeros with a 16-bit literal (the rule LightCraft used before). +The R7 maps decoded to their exact ends under that rule, which a later long prefix would have broken; +no R7 file was at hand to recheck them. R5 Mark II maps (QP 150–162) also jump by up to 12 where the Rice parameter is 0. That +gives 18- to 22-zero prefixes, which the 16-zero rule misread as escapes ("Canon CRX adaptive QP value +−21087" and other out-of-range values; the file then opened as a preview). It also gives real escapes, +symbols 23 and 24. Escapes were seen only where the Rice parameter is 0 (in 100 of 612 maps; all 351 inspected, up to 10 per map); at a larger +parameter they would use the same layout, which is unobserved. With this coding the QP maps of all +33,665 R5 Mark II C-RAW files in a private library (version `0x200`, 8480 × 5650) decode to the exact +end of their entropy streams, with values 150–162. So do the reduced-size tracks of 306 of those +files, the two CC0 R5 Mark II C-RAW files above and the R8 C-RAW fixture; all six fixture hashes are +unchanged. Under the 16-zero rule, 174 of those 306 files (57%) failed and opened as previews, all +with out-of-range values (none at QP 168). The two CC0 C-RAW files are low-ISO shots whose maps need no +long prefix: they decoded under the old rule too, so the escape is covered by synthetic unit tests in +`crx.rs` rather than by a public file. Render cache version 43 redraws thumbnails of files whose decode +changed; photos imported as preview-only switch over with Photo ▸ Reload from Disk. + +The R5 Mark II sensor has 5650 rows (3574 in crop mode), so its Bayer planes have odd heights (2825, +1787). Lossless planes decode as before. In C-RAW every wavelet level has an odd height (2825 → 1413 → +707), and each subband's entropy stream ends exactly where the subband geometry says. The synthesis +uses the same boundary rules that are verified on the R8's odd lengths. Decoded rows show no step +down to the last sensor row, and the developed images match the camera's embedded JPEGs at 100 %. + +An odd QP-map height (EOS R7: 4732 sensor rows give 1183 QP rows) leaves the +last row of the second-level high-frequency bands without a partner row; that +row's QP is used on its own. The odd-height map decodes to the exact end of its +entropy stream. This rule is not yet checked against a black-box reference: +it was checked on paired R7 RAW and C-RAW captures of the same scene (ISO 100 +and 6400), whose developed images match visually and whose sensor means agree +within 0.1% at ISO 6400. Files with even-height maps never reach this rule, so +the six verified fixtures are unchanged. Partial-subband and +rounded-plane coding, vertical C-RAW tiling and adaptive low-frequency gains +remain unsupported (see the variant table above; `cr3_variants.rs` pins each rejection). The decoder bounds image dimensions, sample count, +tile count, marker headers and temporary allocations. The full sensor buffer is +allocated only after the first plane decodes, and plane buffers grow row by row, +so a small crafted file fails before reserving memory for the image it claims. + +## Further bodies: one CC0 file each + +All 110 CR3 files on raw.pixls.us (2026-10-12, every one CC0, 31 folders) decode from sensor data; none +falls back to the preview, so none of them uses a CRX variant the decoder refuses (vertical C-RAW tiles, +partial-subband or rounded-plane coding, multi-tile 0x200, adaptive low-frequency gains) or a CR3 +sRAW / mRAW. HEIF stills are separate `.HIF` files, not CR3. One file per body is pinned in +`canon_cr3_bodies_decode_like_the_camera_jpeg` (`crates/raw/tests/it/cr3_corpus.rs`; URLs and SHA-256 in +`RAW_SAMPLES`, `xtask/src/main.rs`): EOS-1D X Mark III, 90D, 250D, 850D, M200, M50 Mark II, M6 Mark II, +R, R3, R5, R6, R6 Mark II, R6 Mark III, R7, R10, RP, R50, R50 V and PowerShot G5 X Mark II, G7 X Mark III, +SX70 HS and V1, C-RAW where the body has a CC0 C-RAW sample. No external decoder was run: the sensor +hashes pin LightCraft's own decode, and the check is black-box against the camera: + +- the green sites of the active area, on a 24 × 16 grid, follow the camera's own embedded JPEG + (gamma-encoded Pearson r 0.93–0.99; 0.83 for the M6 Mark II and 0.71 for the G7 X Mark III, whose + JPEGs are strongly distortion-corrected; a misread stream gives r near 0); +- each render was inspected at 100 % for tile seams, band or quantiser artefacts (none); +- where raw.pixls.us has RAW and C-RAW shots of one scene, their black-subtracted sensor means correlate + on 16 × 16 blocks at 0.99–0.995 (R, R6, 250D, R50, G5 X Mark II, R5 crop mode) and 0.97 (RP); the + two R5 full-frame shots correlate at only 0.83, though each follows its own camera JPEG at r 0.98, so + the shots differ, not the decodes. The shots differ in exposure, so the means are not compared. + +The EOS R6 Mark III (`ColorData` version 66) reads its as-shot white balance and black level as described at +the top of this page; its white level comes from the data. + +Not every C-RAW file of a verified body decodes: the decoder accepts adaptive QP values 131–175 only. The 3 of +40 EOS R6 Mark II C-RAW files with QP 172 or 175 a user reported (they opened as their preview with "Canon CRX +adaptive QP value 172") now decode (#1694). EOS R7 C-RAW (the odd-height QP map above) has only been checked on six paired RAW / +C-RAW captures; the pinned R7 file is lossless, so R7 C-RAW still has no black-box reference. + +## White level + +The white level is Canon's specular white from `ColorData` (the word after the +normal white level), not the sensor's saturation point. On the RP at ISO 100 +it is 14558 while samples clip at 16383. At the −⅓-stop ISOs, which pull a +higher analog ISO down digitally, it drops further: 11062 at ISO 5000, where +red saturates near 13668. Samples above the white level render as clipped. +Whether Lightroom recovers them has not been measured. + +## Colour limits + +CR3 carries as-shot white balance but no measured camera matrix is imported +here. The existing guarded embedded-JPEG fitting path now includes CR3, with +both proxies oriented identically before gathering training pairs. A bounded +crop/translation registration of the JPEG is confirmed on held-out edge +directions before fitting colour; the existing held-out colour acceptance gates +are unchanged. No lens distortion correction is applied (LightCraft has no lens +profiles of its own yet), so lenses whose camera JPEG is strongly +distortion-corrected may fail the gates and keep the neutral matrix. Render +cache version 13 invalidates earlier thumbnails. All developed pixels still come +from the RAW sensor; a rejected colour fit retains the documented neutral +matrix fallback. See `camera-preview-colour.md` for the limits of this estimate. diff --git a/crates/raw/src/cr3.rs b/crates/raw/src/cr3.rs new file mode 100644 index 000000000..3a7baa488 --- /dev/null +++ b/crates/raw/src/cr3.rs @@ -0,0 +1,778 @@ +//! Canon CR3 container, camera metadata and sensor geometry. +//! +//! Sources: Laurent Clévy's public CR3 format description (`CMP1`, `CDI1/IAD1`, `CMT3`), +//! , and ExifTool's published Canon +//! tag-name tables (`SensorInfo`, `ColorData9`…`ColorData12`, `LensModel`), +//! . No third-party decoder source was consulted. +//! +//! `IAD1` stores the valid sensor area separately from the recommended image crop. Both use inclusive +//! sensor offsets. The CFA is anchored at the full sensor's origin; cropping shifts its phase later. +//! +//! The in-camera aspect ratio (3:2, 4:3, 16:9, 1:1 ...) is not applied to the sensor data: the raw always +//! holds the whole image. The maker note's `AspectInfo` (tag `0x009a`; ExifTool names `AspectRatio`, +//! `CroppedImageWidth`, `CroppedImageHeight`, `CroppedImageLeft`, `CroppedImageTop`) holds the shot's rectangle +//! as five 32-bit values, measured from the top-left of the recommended image crop (the "Canon image", whose +//! size is ExifTool's `CanonImageWidth` x `CanonImageHeight`). That rectangle replaces the crop. Established +//! on the CC0 PowerShot SX70 HS, EOS 250D, PowerShot G5 X Mark II and EOS M6 Mark II samples shot at 3:2, 4:3, +//! 16:9 and 1:1: the rectangle is centred, has the shot's aspect, and files shot at the sensor's own aspect +//! store the whole image. +//! Unknown camera metadata versions are ignored rather than assigned a guessed white balance. + +use crate::crx; +use crate::shared::{black_from_columns, white_from_data}; +use crate::{BlackLevel, Camera, Cfa, ColorData, Limits, Mode, OpcodeLists, Orientation, RawData, RawError, RawFormat, RawImage, Rect, Result}; +use craft_metadata::cr3::{Cr3ImageArea, Cr3Track, Cr3TrackKind, parse_cr3}; +use craft_tiff::{Ifd, Tiff}; + +const SENSOR_INFO: u16 = 0x00e0; +const COLOR_DATA: u16 = 0x4001; +const ASPECT_INFO: u16 = 0x009a; + +/// Pick the first largest raw track. Equal-sized later tracks can be the Dual Pixel delta, which +/// must never replace the main sensor image. An invalid full-size descriptor is reported, not +/// silently replaced with the reduced raw preview. +pub(crate) fn main_track(tracks: &[Cr3Track]) -> Option<&Cr3Track> { + let mut best = None; + let mut best_area = 0u64; + for track in tracks { + if let Cr3TrackKind::Raw { width, height, .. } = track.kind { + let area = u64::from(width) * u64::from(height); + if area > best_area { + best = Some(track); + best_area = area; + } + } + } + best +} + +fn cfa(pattern: u8) -> Option { + let name = match pattern { + 0 => "RGGB", + 1 => "GRBG", + 2 => "GBRG", + 3 => "BGGR", + _ => return None, + }; + Some(Cfa::bayer_static(name)) +} + +/// Inclusive offsets, clipped to the sample dimensions (some Canon crop-mode files record right +/// and bottom borders a few pixels beyond the encoded image). +fn inclusive_rect(bounds: [u16; 4], width: usize, height: usize) -> Option { + let [left, top, right, bottom] = bounds.map(usize::from); + if left >= width || top >= height || right < left || bottom < top { + return None; + } + let end_x = right.checked_add(1)?.min(width); + let end_y = bottom.checked_add(1)?.min(height); + Some(Rect::new(left, top, end_x.checked_sub(left)?, end_y.checked_sub(top)?)) +} + +fn relative_crop(crop: Rect, active: Rect) -> Option { + let x = crop.x.checked_sub(active.x)?; + let y = crop.y.checked_sub(active.y)?; + if crop.width < 2 || crop.height < 2 || x.checked_add(crop.width)? > active.width || y.checked_add(crop.height)? > active.height { + return None; + } + Some(Rect::new(x, y, crop.width, crop.height)) +} + +fn sensor_crop(maker: Option<&Ifd>, width: usize, height: usize) -> Option { + let data = maker?.u64s(SENSOR_INFO)?; + let bounds = data.get(5..9)?; + inclusive_rect( + [ + u16::try_from(*bounds.first()?).ok()?, + u16::try_from(*bounds.get(1)?).ok()?, + u16::try_from(*bounds.get(2)?).ok()?, + u16::try_from(*bounds.get(3)?).ok()?, + ], + width, + height, + ) +} + +/// The shot's aspect-ratio rectangle from `AspectInfo`, placed inside `image` (the recommended crop). `None` +/// when the tag is absent or malformed, or its rectangle doesn't fit inside `image`. +fn aspect_crop(maker: Option<&Ifd>, image: Rect) -> Option { + let data = maker?.u64s(ASPECT_INFO)?; + let [width, height, left, top] = [1usize, 2, 3, 4].map(|i| data.get(i).and_then(|&v| usize::try_from(v).ok())); + let (width, height, left, top) = (width?, height?, left?, top?); + if width < 2 || height < 2 || left.checked_add(width)? > image.width || top.checked_add(height)? > image.height { + return None; + } + Some(Rect::new(image.x.checked_add(left)?, image.y.checked_add(top)?, width, height)) +} + +fn geometry(area: Option<&Cr3ImageArea>, maker: Option<&Ifd>, width: usize, height: usize) -> (Rect, Rect) { + let area = area.filter(|a| usize::from(a.width) == width && usize::from(a.height) == height); + let sensor = sensor_crop(maker, width, height); + let mut active = area.and_then(|a| a.active).and_then(|b| inclusive_rect(b, width, height)).or(sensor).unwrap_or(Rect::new(0, 0, width, height)); + // The recommended crop. `AspectInfo` is measured from its origin, so it is applied on top of it. + let stated = area.and_then(|a| inclusive_rect(a.crop, width, height)).or(sensor); + let mut recommended = stated.and_then(|c| relative_crop(c, active)); + // Some crop-mode files (EOS R5 Mark II APS-C, 5376 x 3574) state an active area that starts in the dark + // columns and stops short of the image, while their recommended crop lies on the image (measured: columns + // 132..~256 sit at the black level, the image runs to the right edge). The recommended crop is then the + // valid area. + if recommended.is_none() + && let Some(c) = stated.filter(|c| c.width >= 2 && c.height >= 2) + { + active = c; + recommended = Some(Rect::new(0, 0, c.width, c.height)); + } + let crop = recommended.map(|c| aspect_crop(maker, c).unwrap_or(c)).unwrap_or(Rect::new(0, 0, active.width, active.height)); + (active, crop) +} + +#[derive(Default)] +struct CameraLevels { + wb: Option<[f32; 3]>, + black: Option<[f32; 4]>, + white: Option, +} + +/// Canon's versioned word arrays. Offsets are word indices from the published tag tables, not +/// byte offsets. The four as-shot WB values are R, G, G, B, independent of the CFA layout. +/// A version without a white-level offset takes its white level from the data (see `docs/cr3.md`). +fn camera_levels(maker: Option<&Ifd>, bits: u8) -> CameraLevels { + let Some(values) = maker.and_then(|m| m.u64s(COLOR_DATA)) else { return CameraLevels::default() }; + let offsets = match values.first().copied() { + Some(16..=19) => (71, 329, Some(797)), + Some(32 | 33) => (85, 343, Some(811)), + Some(34 | 48) => (105, 363, Some(641)), + Some(64 | 65) => (105, 383, Some(661)), + Some(66) => (105, 383, None), + _ => return CameraLevels::default(), + }; + let full = ((1u32 << u32::from(bits.clamp(1, 16))) - 1) as f32; + let quadruple = |at: usize| -> Option<[f32; 4]> { + let v = values.get(at..at + 4)?; + Some([*v.first()? as f32, *v.get(1)? as f32, *v.get(2)? as f32, *v.get(3)? as f32]) + }; + let wb = quadruple(offsets.0).and_then(|[r, g1, g2, b]| { + let green = (g1 + g2) * 0.5; + if !(64.0..=32767.0).contains(&green) || (g1 - g2).abs() > green * 0.1 { + return None; + } + let red = r / green; + let blue = b / green; + ((0.1..8.0).contains(&red) && (0.1..8.0).contains(&blue)).then_some([red, 1.0, blue]) + }); + let black = quadruple(offsets.1).filter(|v| v.iter().any(|&b| b > 0.0) && v.iter().all(|&b| b >= 0.0 && b < full * 0.5)); + let max_black = black.map(|v| v.into_iter().fold(0.0f32, f32::max)).unwrap_or(0.0); + let white = offsets.2.and_then(|at| values.get(at)).map(|&v| v as f32).filter(|&v| v > max_black && v >= full * 0.5 && v <= full); + CameraLevels { wb, black, white } +} + +fn black_at_active(levels: [f32; 4], pattern: u8, active: Rect) -> BlackLevel { + // Convert Canon's channel order to full-sensor site order, then shift to the active origin. + let sites = match pattern { + 1 => [1, 0, 3, 2], + 2 => [2, 3, 0, 1], + 3 => [3, 2, 1, 0], + _ => [0, 1, 2, 3], + }; + let values = (0..4) + .map(|i| { + let x = (active.x + i % 2) % 2; + let y = (active.y + i / 2) % 2; + levels.get(*sites.get(y * 2 + x).unwrap_or(&0)).copied().unwrap_or(0.0) + }) + .collect(); + BlackLevel { repeat_rows: 2, repeat_cols: 2, values, delta_h: vec![], delta_v: vec![] } +} + +pub(crate) fn decode(bytes: &[u8], mode: Mode, limits: &Limits) -> Result { + let container = parse_cr3(bytes).ok_or(RawError::NotRaw)?; + let track = main_track(&container.tracks).ok_or_else(|| RawError::Unsupported("CR3 without a Bayer raw track".into()))?; + let coding = track.compression(bytes).ok_or_else(|| RawError::Corrupt("CR3 raw track has an invalid CMP1 descriptor".into()))?; + if coding.planes != 4 || coding.encoding != 0 { + return Err(RawError::Unsupported(format!("Canon CR3 encoding {} with {} planes", coding.encoding, coding.planes))); + } + let (width, height) = (coding.width as usize, coding.height as usize); + let samples = width.checked_mul(height).filter(|&n| n > 0 && n <= limits.samples()).ok_or(RawError::Limit("CR3 sensor dimensions"))?; + let cfa = cfa(coding.cfa_pattern).ok_or_else(|| RawError::Unsupported(format!("Canon CR3 CFA {}", coding.cfa_pattern)))?; + let (offset, len) = track.data.ok_or_else(|| RawError::Corrupt("CR3 raw sample outside file".into()))?; + let sample = + offset.checked_add(len).and_then(|end| bytes.get(offset..end)).ok_or_else(|| RawError::Corrupt("CR3 raw sample outside file".into()))?; + if coding.header_size as usize > sample.len() { + return Err(RawError::Corrupt("CR3 coding header outside sample".into())); + } + crx::validate(sample, &coding)?; + let data = match mode { + Mode::Full => { + let data = crx::decode(sample, &coding)?; + if data.len() != samples { + return Err(RawError::Corrupt("CR3 decoded sample count does not match CMP1".into())); + } + data + } + Mode::Header => vec![], + }; + let static_maker = container.cmt.get(2).and_then(|b| *b).and_then(|b| Tiff::parse(b).ok()); + let maker = static_maker.as_ref().and_then(|m| m.ifds.first()); + // Retain only the first useful timed maker note: repeated CTMD records must not accumulate + // independent copies of every bounded TIFF value in memory. + // Records aliasing the same bytes are parsed once, and all parses share a budget of twice the file (at least 1 MiB); + // a record past what is left is skipped, a later smaller one still parsed. + let mut seen = std::collections::HashSet::new(); + let mut left = bytes.len().saturating_mul(2).max(1 << 20); + let timed_maker = container + .timed_exif(bytes) + .into_iter() + .filter(|b| b.tag == 0x927c) + .filter(|b| seen.insert((b.data.as_ptr() as usize, b.data.len()))) + .filter(|b| match left.checked_sub(b.data.len()) { + Some(rest) => { + left = rest; + true + } + None => false, + }) + .filter_map(|b| Tiff::parse(b.data).ok()) + .find(|t| t.ifds.first().is_some_and(|m| m.contains(COLOR_DATA))); + let color_maker = timed_maker.as_ref().and_then(|t| t.ifds.first()).or(maker); + let area = track.image_area(bytes); + let (active, crop) = geometry(area.as_ref(), maker, width, height); + let levels = camera_levels(color_maker, coding.bit_depth); + let black = match levels.black { + Some(levels) => black_at_active(levels, coding.cfa_pattern, active), + None if active.x >= 8 => black_from_columns(&data, width, 2..active.x - 2, active.y..active.y + active.height, active), + None => BlackLevel::uniform(0.0), + }; + let white = levels.white.unwrap_or_else(|| white_from_data(&data, u32::from(coding.bit_depth))); + // CMT1 is the file's IFD0: orientation, make and model + let cmt1 = container.cmt.first().and_then(|b| *b).and_then(|b| Tiff::parse(b).ok()); + let ifd0 = cmt1.as_ref().and_then(|t| t.ifds.first()); + let mut image = RawImage { + format: RawFormat::Cr3, + width, + height, + cpp: 1, + data: RawData::U16(data), + cfa: Some(cfa), + bits: u32::from(coding.bit_depth), + black, + white: vec![white], + active_area: active, + crop, + orientation: Orientation::from_exif(ifd0.and_then(|i| i.u16(craft_tiff::tags::ORIENTATION)).unwrap_or(1)), + color: ColorData::default(), + wb_multipliers: levels.wb, + linearized: false, + pixel_aspect: 1.0, + opcodes: OpcodeLists::default(), + camera: ifd0.map(Camera::from_ifd0).unwrap_or_default(), + cinema: Default::default(), + lens_metadata: Default::default(), + }; + // Canon's stated white level can sit below the sensor's true saturation (EOS RP: 14558 + // stated, 16383 real). Extend it to a saturation pile-up above it, so highlight data + // between the stated level and the real clip point is not treated as clipped. No-op when + // no such pile-up exists, and for header-only decodes (no samples). + extend_white(&mut image); + image.validate_for(mode)?; + Ok(image) +} + +// The white extension below is LightCraft's `saturation.rs` (CR3 part). +/// A saturation plateau must be at least this many times as populated as the average value just below it. +const MIN_SPIKE: f64 = 16.0; +/// At most this share of the plateau may lie above it (a real clip point is the top of the data). +const MAX_ABOVE: f64 = 0.02; +// CR3 gain-normalized saturation can occupy several nearby codes, rather than one exact value. +const CR3_BINS: usize = 256; +const CR3_PLATEAU_BINS: usize = 8; +const CR3_MIN_SAMPLES: u64 = 64; + +/// The lower edge of a terminal pile-up, ignoring a few brighter outliers. A continuous +/// highlight distribution has comparable density below it and therefore has no plateau. +fn cr3_plateau(hist: &[[u64; 3]; CR3_BINS], first: &[[u16; 3]; CR3_BINS], colour: usize, from: usize, white: f32) -> Option { + let (start, end, count) = (from + 1..=CR3_BINS) + .map(|end| { + let start = end.saturating_sub(CR3_PLATEAU_BINS).max(from); + let count: u64 = hist[start..end].iter().map(|bin| bin[colour]).sum(); + (start, end, count) + }) + .max_by_key(|&(_, end, count)| (count, end))?; + let below_start = start.saturating_sub(CR3_PLATEAU_BINS); + let below: u64 = hist[below_start..start].iter().map(|bin| bin[colour]).sum(); + let above: u64 = hist[end..].iter().map(|bin| bin[colour]).sum(); + let density = count as f64 / (end - start) as f64; + let below_density = below as f64 / (start - below_start).max(1) as f64; + if count < CR3_MIN_SAMPLES || (below > 0 && density < MIN_SPIKE * below_density) || above as f64 > MAX_ABOVE * count as f64 { + return None; + } + let threshold = (density / MIN_SPIKE).max(MIN_SPIKE * below_density).max(1.0); + (start..end).find(|&bin| hist[bin][colour] as f64 >= threshold).map(|bin| first[bin][colour] as f32).filter(|&point| point > white) +} + +/// Recover real CR3 samples beyond a conservatively stated common white level. Search +/// coarse bins so gain-spread saturation (e.g. R100) is found too. With enough above-white +/// data but no clear pile-up, the bit-depth ceiling keeps all that unclipped headroom. +/// One shared range and its compensating baseline EV preserve lower tones and colour ratios. +fn extend_white(image: &mut RawImage) -> bool { + if image.format != crate::RawFormat::Cr3 || !image.color.baseline_exposure.is_finite() || !(1..=16).contains(&image.bits) { + return false; + } + let (RawData::U16(data), Some(cfa)) = (&image.data, &image.cfa) else { return false }; + let (old, black) = (image.white_at(0), image.black.mean()); + let ceiling = ((1u32 << image.bits) - 1) as f32; + if image.cpp != 1 + || !cfa.valid() + || image.width.checked_mul(image.height) != Some(data.len()) + || !black.is_finite() + || !old.is_finite() + || black < 0.0 + || old <= black + || old >= ceiling + || image.white.is_empty() + || image.white.iter().any(|w| *w != old) + { + return false; + } + let a = image.active_area; + let fits = |start: usize, len: usize, max: usize| start.checked_add(len).is_some_and(|end| end <= max); + if a.width == 0 || a.height == 0 || !fits(a.x, a.width, image.width) || !fits(a.y, a.height, image.height) { + return false; + } + let step = ((ceiling - black) / CR3_BINS as f32).ceil().max(1.0); + let mut hist = [[0u64; 3]; CR3_BINS]; + let mut first = [[u16::MAX; 3]; CR3_BINS]; + let mut over = [0u64; 3]; + for y in a.y..a.y + a.height { + for (x, &v) in data[y * image.width + a.x..][..a.width].iter().enumerate() { + if v as f32 > ceiling { + return false; + } + let c = cfa.color_at(a.x + x, y).min(2) as usize; + let bin = (((v as f32 - black).max(0.0) / step) as usize).min(CR3_BINS - 1); + hist[bin][c] += 1; + first[bin][c] = first[bin][c].min(v); + over[c] += u64::from(v as f32 > old); + } + } + let above: u64 = over.iter().sum(); + let pixels = a.width * a.height; + if above < CR3_MIN_SAMPLES || (above as f64) < pixels as f64 * 1e-5 { + return false; + } + let from = (((old - black) / step) as usize).min(CR3_BINS - 1); + let new = (0..3) + .map(|c| if over[c] < CR3_MIN_SAMPLES { ceiling } else { cr3_plateau(&hist, &first, c, from, old).unwrap_or(ceiling) }) + .fold(ceiling, f32::min); + let ev = ((new - black) as f64 / (old - black) as f64).log2(); + if new <= old || !ev.is_finite() { + return false; + } + image.white.fill(new); + image.color.baseline_exposure += ev; + true +} + +#[cfg(test)] +mod tests { + use super::*; + use craft_tiff::{IfdBuilder, Value}; + + fn maker(version: u16, wb_at: usize, black_at: usize, white_at: usize) -> Ifd { + let mut words = vec![0u16; white_at + 1]; + words[0] = version; + words[wb_at..wb_at + 4].copy_from_slice(&[2048, 1024, 1024, 1536]); + words[black_at..black_at + 4].copy_from_slice(&[100, 101, 102, 103]); + words[white_at] = 15000; + let b = IfdBuilder::new().with(COLOR_DATA, Value::Short(words)); + let bytes = craft_tiff::TiffWriter::new(craft_tiff::ByteOrder::Little, false).write(&[b]).unwrap(); + Tiff::parse(&bytes).unwrap().ifds.remove(0) + } + + fn box_bytes(kind: &[u8; 4], body: &[u8]) -> Vec { + let mut b = ((body.len() + 8) as u32).to_be_bytes().to_vec(); + b.extend_from_slice(kind); + b.extend_from_slice(body); + b + } + + fn track(entry: &[u8], offset: u64, size: u32) -> Vec { + let mut stsd = vec![0u8; 4]; + stsd.extend(1u32.to_be_bytes()); + stsd.extend(entry); + let mut stsz = vec![0u8; 4]; + stsz.extend(size.to_be_bytes()); + stsz.extend(1u32.to_be_bytes()); + let mut co64 = vec![0u8; 4]; + co64.extend(1u32.to_be_bytes()); + co64.extend(offset.to_be_bytes()); + let tables = [box_bytes(b"stsd", &stsd), box_bytes(b"stsz", &stsz), box_bytes(b"co64", &co64)].concat(); + box_bytes(b"trak", &box_bytes(b"mdia", &box_bytes(b"minf", &box_bytes(b"stbl", &tables)))) + } + + fn synthetic_header() -> Vec { + synthetic_header_with(None) + } + + fn synthetic_header_with(aspect: Option<[u32; 5]>) -> Vec { + use craft_tiff::{ByteOrder, TiffWriter, tags as t}; + let tiff = |b: IfdBuilder| TiffWriter::new(ByteOrder::Little, false).write(&[b]).unwrap(); + let cmt1 = tiff( + IfdBuilder::new() + .with(t::MAKE, Value::Ascii("Canon".into())) + .with(t::MODEL, Value::Ascii("EOS Test".into())) + .with(t::ORIENTATION, Value::Short(vec![6])), + ); + let cmt2 = tiff(IfdBuilder::new().with(t::ISO_SPEED, Value::Short(vec![800]))); + let mut cmt3 = IfdBuilder::new() + .with(SENSOR_INFO, Value::Short(vec![34, 64, 48, 1, 1, 6, 4, 61, 45])) + .with(0x0095, Value::Ascii("RF-S18-45mm F4.5-6.3 IS STM".into())); + if let Some(a) = aspect { + cmt3 = cmt3.with(ASPECT_INFO, Value::Long(a.to_vec())); + } + let cmt3 = tiff(cmt3); + let mut canon = vec![0x85, 0xc0, 0xb6, 0x87, 0x82, 0x0f, 0x11, 0xe0, 0x81, 0x11, 0xf4, 0xce, 0x46, 0x2b, 0x6a, 0x48]; + canon.extend([box_bytes(b"CMT1", &cmt1), box_bytes(b"CMT2", &cmt2), box_bytes(b"CMT3", &cmt3)].concat()); + let mut coding = vec![0u8; 52]; + coding[0..2].copy_from_slice(&0xff00u16.to_be_bytes()); + coding[2..4].copy_from_slice(&48u16.to_be_bytes()); + coding[4..6].copy_from_slice(&0x100u16.to_be_bytes()); + for (at, value) in [(8, 64u32), (12, 48), (16, 32), (20, 48), (28, 216)] { + coding[at..at + 4].copy_from_slice(&value.to_be_bytes()); + } + coding[24] = 14; + coding[25] = 0x40; + let mut area = vec![0u8; 48]; + for (at, value) in [ + (4, 64u16), + (6, 48), + (10, 2), + (16, 6), + (18, 4), + (20, 61), + (22, 45), + (28, 3), + (30, 47), + (32, 4), + (36, 63), + (38, 1), + (40, 4), + (42, 2), + (44, 63), + (46, 47), + ] { + area[at..at + 2].copy_from_slice(&value.to_be_bytes()); + } + let mut craw = vec![0u8; 82]; + craw[24..26].copy_from_slice(&64u16.to_be_bytes()); + craw[26..28].copy_from_slice(&48u16.to_be_bytes()); + craw.extend(box_bytes(b"CMP1", &coding)); + craw.extend(box_bytes(b"CDI1", &[vec![0u8; 4], box_bytes(b"IAD1", &area)].concat())); + let levels = maker(48, 105, 363, 641).value(COLOR_DATA).unwrap().clone(); + let dynamic_maker = tiff(IfdBuilder::new().with(COLOR_DATA, levels)); + let mut item = ((dynamic_maker.len() + 8) as u32).to_le_bytes().to_vec(); + item.extend(0x927cu32.to_le_bytes()); + item.extend(dynamic_maker); + let mut timed = vec![0u8; 12]; + timed[0..4].copy_from_slice(&((12 + item.len()) as u32).to_le_bytes()); + timed[4..6].copy_from_slice(&8u16.to_le_bytes()); + timed.extend(item); + // Two tiles, each with four one-byte lossless plane streams. Header + // probing checks the complete marker layout without decoding entropy. + let mut sample = Vec::new(); + let marker = |out: &mut Vec, code: u16, size: u32, flags: u32| { + out.extend(code.to_be_bytes()); + out.extend(8u16.to_be_bytes()); + out.extend(size.to_be_bytes()); + out.extend(flags.to_be_bytes()); + }; + for tile in 0..2 { + marker(&mut sample, 0xff01, 4, tile << 16); + for plane in 0..4 { + marker(&mut sample, 0xff02, 1, (plane << 28) | 0x0800_0000); + marker(&mut sample, 0xff03, 1, 0x0020_0000); + } + } + assert_eq!(sample.len(), 216); + sample.extend([0x1e; 8]); + let moov = + [box_bytes(b"uuid", &canon), track(&box_bytes(b"CRAW", &craw), 4096, 224), track(&box_bytes(b"CTMD", &[]), 4320, timed.len() as u32)] + .concat(); + let mut file = box_bytes(b"ftyp", b"crx \0\0\0\x01crx isom"); + file.extend(box_bytes(b"moov", &moov)); + assert!(file.len() < 4096); + file.resize(4096, 0); + file.extend(sample); + file.extend(timed); + file + } + + #[test] + fn header_reads_timed_white_balance_and_distinct_geometry() { + let bytes = synthetic_header(); + let image = decode(&bytes, Mode::Header, &Limits::default()).unwrap(); + assert_eq!((image.width, image.height, image.bits), (64, 48, 14)); + assert_eq!(image.active_area, Rect::new(4, 2, 60, 46)); + assert_eq!(image.crop, Rect::new(2, 2, 56, 42)); + assert_eq!(image.wb_multipliers, Some([2.0, 1.0, 1.5])); + assert_eq!(image.black.values, vec![100.0, 101.0, 102.0, 103.0]); + assert_eq!(image.white, vec![15000.0]); + assert_eq!(image.info().developed_size(), (56, 42)); + assert_eq!((image.camera.make.as_deref(), image.camera.model.as_deref()), (Some("Canon"), Some("EOS Test"))); + assert_eq!(image.orientation, crate::Orientation::Rotate90); + assert!(image.data.is_empty()); + assert!(decode(&bytes[..4200], Mode::Header, &Limits::default()).is_err()); + } + + #[test] + fn aspect_info_replaces_the_crop() { + // 16:9 inside the 56 x 42 recommended crop at (2, 2): 56 x 30, 6 rows down. + let bytes = synthetic_header_with(Some([7, 56, 30, 0, 6])); + let image = decode(&bytes, Mode::Header, &Limits::default()).unwrap(); + assert_eq!(image.active_area, Rect::new(4, 2, 60, 46)); + assert_eq!(image.crop, Rect::new(2, 8, 56, 30)); + assert_eq!(image.info().developed_size(), (56, 30)); + // 1:1 with a left offset. + let image = decode(&synthetic_header_with(Some([1, 42, 42, 7, 0])), Mode::Header, &Limits::default()).unwrap(); + assert_eq!(image.crop, Rect::new(9, 2, 42, 42)); + // The whole image, as stored for a shot at the sensor's own aspect, changes nothing. + let image = decode(&synthetic_header_with(Some([0, 56, 42, 0, 0])), Mode::Header, &Limits::default()).unwrap(); + assert_eq!(image.crop, Rect::new(2, 2, 56, 42)); + } + + #[test] + fn aspect_info_that_does_not_fit_is_ignored() { + for bad in [[7, 57, 30, 0, 6], [7, 56, 30, 0, 13], [7, 56, 30, 1, 6], [7, 1, 30, 0, 0], [7, 56, 0, 0, 0], [7, u32::MAX, 30, 0, 0]] { + let image = decode(&synthetic_header_with(Some(bad)), Mode::Header, &Limits::default()).unwrap(); + assert_eq!(image.crop, Rect::new(2, 2, 56, 42), "{bad:?}"); + } + } + + /// Some crop-mode files (EOS R5 Mark II 7883) record a recommended crop that reaches past their stated active + /// area, which starts in dark columns. The recommended crop is then the valid area (no black band on the left), + /// and `AspectInfo` applies from its origin. + #[test] + fn aspect_info_needs_a_consistent_recommended_crop() { + let aspect = craft_tiff::IfdBuilder::new().with(ASPECT_INFO, Value::Long(vec![13, 5088, 3392, 0, 0])); + let bytes = craft_tiff::TiffWriter::new(craft_tiff::ByteOrder::Little, false).write(&[aspect]).unwrap(); + let maker = Tiff::parse(&bytes).unwrap().ifds.remove(0); + let area = + |crop| Cr3ImageArea { width: 5376, height: 3574, crop, active: Some([132, 160, 5243, 3567]), masked_left: [0; 4], masked_top: None }; + // crop right edge 5359 > active right edge 5243: the recommended crop becomes the valid area + assert_eq!( + geometry(Some(&area([272, 172, 5359, 3563])), Some(&maker), 5376, 3574), + (Rect::new(272, 172, 5088, 3392), Rect::new(0, 0, 5088, 3392)) + ); + // consistent crop: AspectInfo applies from its origin + assert_eq!(geometry(Some(&area([140, 172, 5227, 3563])), Some(&maker), 5376, 3574).1, Rect::new(8, 12, 5088, 3392)); + } + + /// `AspectInfo` as the SX70 HS 16:9 sample stores it, byte for byte (little-endian LONG x 5), read through + /// the TIFF parser: AspectRatio 7, 5184 x 2912 at (0, 488). + #[test] + fn aspect_info_known_bytes() { + let mut tiff = b"II*".to_vec(); + tiff.extend_from_slice(&[0x9a, 0x00, 0x04, 0x00, 0x05, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00]); + tiff.extend_from_slice(&[0, 0, 0, 0]); + tiff.extend_from_slice(&[0x07, 0, 0, 0, 0x40, 0x14, 0, 0, 0x60, 0x0b, 0, 0, 0, 0, 0, 0, 0xe8, 0x01, 0, 0]); + let parsed = Tiff::parse(&tiff).unwrap(); + let maker = parsed.ifds.first(); + assert_eq!(aspect_crop(maker, Rect::new(132, 40, 5184, 3888)), Some(Rect::new(132, 528, 5184, 2912))); + assert_eq!(aspect_crop(maker, Rect::new(0, 0, 5184, 2000)), None); + assert_eq!(aspect_crop(None, Rect::new(0, 0, 5184, 3888)), None); + } + + fn prepend_tracks(bytes: &[u8], tracks: &[u8]) -> Vec { + let ftyp_end = u32::from_be_bytes(bytes[..4].try_into().unwrap()) as usize; + let moov_len = u32::from_be_bytes(bytes[ftyp_end..ftyp_end + 4].try_into().unwrap()) as usize; + assert_eq!(&bytes[ftyp_end + 4..ftyp_end + 8], b"moov"); + let moov = [tracks, &bytes[ftyp_end + 8..ftyp_end + moov_len]].concat(); + let mut out = [bytes[..ftyp_end].to_vec(), box_bytes(b"moov", &moov)].concat(); + assert!(out.len() < 4096); + out.resize(4096, 0); + out.extend_from_slice(&bytes[4096..]); + out + } + + #[test] + fn hevc_preview_with_equal_dimensions_does_not_replace_the_sensor_track() { + let bytes = synthetic_header(); + let mut preview = vec![0u8; 82]; + preview[24..26].copy_from_slice(&64u16.to_be_bytes()); + preview[26..28].copy_from_slice(&48u16.to_be_bytes()); + preview.extend(box_bytes(b"HEVC", &[0; 4])); + let bytes = prepend_tracks(&bytes, &track(&box_bytes(b"CRAW", &preview), 4096, 224)); + let container = parse_cr3(&bytes).unwrap(); + assert_eq!(container.tracks[0].kind, Cr3TrackKind::Other(*b"HEVC")); + let image = decode(&bytes, Mode::Header, &Limits::default()).unwrap(); + assert_eq!((image.width, image.height, image.bits), (64, 48, 14)); + assert_eq!(image.active_area, Rect::new(4, 2, 60, 46)); + } + + #[test] + fn corrupt_main_descriptor_is_not_replaced_by_the_reduced_raw() { + let bytes = synthetic_header(); + let container = parse_cr3(&bytes).unwrap(); + let Cr3TrackKind::Raw { cmp1: Some((at, len)), .. } = main_track(&container.tracks).unwrap().kind else { panic!("raw fixture") }; + let mut reduced = vec![0u8; 82]; + reduced[24..26].copy_from_slice(&32u16.to_be_bytes()); + reduced[26..28].copy_from_slice(&24u16.to_be_bytes()); + reduced.extend(box_bytes(b"CMP1", &bytes[at..at + len])); + let bytes = prepend_tracks(&bytes, &track(&box_bytes(b"CRAW", &reduced), 4096, 224)); + let container = parse_cr3(&bytes).unwrap(); + let Cr3TrackKind::Raw { cmp1: Some((at, len)), .. } = main_track(&container.tracks).unwrap().kind else { panic!("main raw fixture") }; + let mut malformed = bytes.clone(); + malformed[at..at + len].fill(0); + assert!(matches!(decode(&malformed, Mode::Header, &Limits::default()), Err(RawError::Corrupt(_)))); + let mut missing = bytes; + missing[at - 4..at].copy_from_slice(b"free"); + assert!(matches!(decode(&missing, Mode::Header, &Limits::default()), Err(RawError::Corrupt(_)))); + } + + #[test] + fn color_data_uses_versioned_as_shot_offsets() { + for (version, wb_at, black_at, white_at) in + [(16, 71, 329, 797), (19, 71, 329, 797), (33, 85, 343, 811), (48, 105, 363, 641), (64, 105, 383, 661)] + { + let m = maker(version, wb_at, black_at, white_at); + let levels = camera_levels(Some(&m), 14); + assert_eq!(levels.wb, Some([2.0, 1.0, 1.5])); + assert_eq!(levels.black, Some([100.0, 101.0, 102.0, 103.0])); + assert_eq!(levels.white, Some(15000.0)); + } + // A version without a white-level offset leaves the white level to the data. + let levels = camera_levels(Some(&maker(66, 105, 383, 661)), 14); + assert_eq!(levels.wb, Some([2.0, 1.0, 1.5])); + assert_eq!(levels.black, Some([100.0, 101.0, 102.0, 103.0])); + assert_eq!(levels.white, None); + assert!(camera_levels(Some(&maker(255, 71, 329, 797)), 14).wb.is_none()); + assert!(camera_levels(None, 14).black.is_none()); + } + + #[test] + fn crop_is_relative_to_the_valid_sensor_area() { + let area = Cr3ImageArea { + width: 6888, + height: 4546, + crop: [156, 158, 6875, 4537], + active: Some([144, 46, 6887, 4545]), + masked_left: [0, 0, 143, 4545], + masked_top: None, + }; + assert_eq!(geometry(Some(&area), None, 6888, 4546), (Rect::new(144, 46, 6744, 4500), Rect::new(12, 112, 6720, 4380))); + assert_eq!(inclusive_rect([152, 46, 4353, 2851], 4352, 2850), Some(Rect::new(152, 46, 4200, 2804))); + assert_eq!(inclusive_rect([100, 100, 50, 50], 200, 200), None); + assert_eq!(relative_crop(Rect::new(0, 0, 100, 100), Rect::new(4, 4, 92, 92)), None); + } + + #[test] + fn first_full_size_track_wins_over_dual_pixel_delta() { + let raw = |w, h, offset| Cr3Track { kind: Cr3TrackKind::Raw { width: w, height: h, cmp1: None, iad1: None }, data: Some((offset, 100)) }; + let tracks = [raw(1624, 1080, 1), raw(6888, 4546, 2), raw(6888, 4546, 3)]; + assert_eq!(main_track(&tracks).unwrap().data, Some((2, 100))); + } + + #[test] + fn cfa_and_black_follow_the_full_sensor_origin() { + for (code, name) in [(0, "RGGB"), (1, "GRBG"), (2, "GBRG"), (3, "BGGR")] { + assert_eq!(cfa(code).unwrap().name(), name); + } + let b = black_at_active([100.0, 101.0, 102.0, 103.0], 0, Rect::new(3, 1, 32, 20)); + assert_eq!(b.values, vec![103.0, 102.0, 101.0, 100.0]); + } + + /// A 64 x 64 RGGB CR3 image with a gradient (black 528, 14 bits) stated to clip at `white`. + fn levels_image(white: f32) -> RawImage { + let data = (0..64 * 64).map(|i| 528 + ((i % 64) * 60 + (i / 64) * 7) as u16 % 4000).collect(); + RawImage { + format: RawFormat::Cr3, + width: 64, + height: 64, + cpp: 1, + data: RawData::U16(data), + cfa: Cfa::bayer("RGGB"), + bits: 14, + black: BlackLevel::uniform(528.0), + white: vec![white], + active_area: Rect::new(0, 0, 64, 64), + crop: Rect::new(0, 0, 64, 64), + orientation: Orientation::Normal, + color: ColorData::default(), + wb_multipliers: None, + linearized: false, + pixel_aspect: 1.0, + opcodes: OpcodeLists::default(), + camera: Camera::default(), + cinema: Default::default(), + lens_metadata: Default::default(), + } + } + + /// Canon's stated white (EOS RP: 14558) below a pile-up at 16383 is extended to it, the samples unchanged and + /// the BaselineExposure raised by the stops that keep unclipped tones where they were; once only. + #[test] + fn white_extends_to_a_saturation_plateau_above_the_stated_level() { + let mut r = levels_image(14558.0); + if let RawData::U16(d) = &mut r.data { + d[..2048].fill(16383); + } + r.color.baseline_exposure = 0.5; + let samples = r.data.clone(); + assert!(extend_white(&mut r)); + assert_eq!(r.white_at(0), 16383.0); + assert!((r.color.baseline_exposure - (0.5 + ((16383.0f64 - 528.0) / (14558.0 - 528.0)).log2())).abs() < 1e-6); + assert_eq!(r.data, samples); + let ev = r.color.baseline_exposure; + assert!(!extend_white(&mut r), "idempotent"); + assert_eq!(r.color.baseline_exposure, ev); + } + + #[test] + fn white_extension_leaves_other_formats_and_unequal_levels_alone() { + let mut r = levels_image(14558.0); + if let RawData::U16(d) = &mut r.data { + d[..2048].fill(16383); + } + r.format = RawFormat::Dng; + assert!(!extend_white(&mut r)); + assert_eq!(r.white_at(0), 14558.0); + r.format = RawFormat::Cr3; + r.white = vec![14558.0, 15000.0, 14558.0]; + assert!(!extend_white(&mut r)); + assert_eq!(r.color.baseline_exposure, 0.0); + } + + #[test] + fn white_extension_finds_a_spread_plateau() { + let mut r = levels_image(10439.0); + if let RawData::U16(d) = &mut r.data { + for (i, v) in d.iter_mut().take(3072).enumerate() { + *v = 13300 + (i * 37 % 401) as u16; + } + } + assert!(extend_white(&mut r), "the R100-style plateau spans hundreds of codes"); + assert!((13300.0..=13364.0).contains(&r.white_at(0)), "{}", r.white_at(0)); + } + + #[test] + fn white_extension_keeps_unclipped_data_past_the_stated_level() { + let mut r = levels_image(14558.0); + if let RawData::U16(d) = &mut r.data { + for (i, v) in d.iter_mut().enumerate() { + *v = 528 + (i * 37 % 15855) as u16; + } + } + assert!(extend_white(&mut r), "unclipped data past the stated white is still real light"); + assert_eq!(r.white_at(0), 16383.0); + let mut r = levels_image(14558.0); + let cfa = r.cfa.clone().unwrap(); + if let RawData::U16(d) = &mut r.data { + for (i, v) in d.iter_mut().enumerate() { + if cfa.color_at(i % 64, i / 64) == 0 { + *v = 16383; + } + } + } + assert!(extend_white(&mut r), "a coloured highlight need not clip all three channels"); + assert_eq!(r.white_at(0), 16383.0); + } +} diff --git a/crates/raw/src/crx.rs b/crates/raw/src/crx.rs new file mode 100644 index 000000000..a8382f757 --- /dev/null +++ b/crates/raw/src/crx.rs @@ -0,0 +1,1229 @@ +//! Canon CRX sensor sample decompression. +//! +//! Clean-room sources: Laurent Clévy's public prose CR3 format description +//! (CMP1 and marker layouts), +//! Canon patent US20160323602A1 (MED prediction, signed mapping, adaptive Rice coding), +//! Canon patent JP2017192077A (zero-run flag and MELCODE of run length minus one), and +//! ITU-T T.87 (MELCODE run index table). No third-party decoder source was consulted. +//! CRX-specific boundaries and adaptive parameters were established by comparing +//! CC0 raw.pixls.us files against an external decoder used only as a black-box oracle. +//! +//! Current support: version 0x100 lossless Bayer RAW and horizontal-tile C-RAW, +//! plus version 0x200 single-tile 14-bit C-RAW with adaptive QP quantization +//! (including odd-height QP maps, as on the EOS R7, and odd-height Bayer planes, as on the EOS R5 Mark II). +//! Both ff01/ff02/ff03 and ff11/ff12/ff13 marker families are supported. +//! Six full M50/R100/R8 sensor fixtures match an external decoder sample for sample. +//! The QP map's first-symbol literal and its escape (the T.87 limited-length layout) were found +//! on EOS R5 Mark II maps, whose larger QP steps need them; every map of 33,665 R5 Mark II files +//! decodes to the exact end of its entropy stream with them. +//! Unverified coding variants return explicit unsupported errors. + +use crate::crx_wavelet::{self, Band as WaveletBand, TileEdges}; +use crate::{MAX_SAMPLES, RawError, Result}; +use craft_metadata::cr3::Cr3Compression; +use rayon::prelude::*; +use std::ops::Range; + +const MAX_TILES: usize = 1024; +const MAX_HEADER_BYTES: usize = 1 << 20; +const MAX_CRX_SAMPLES: usize = 200_000_000; +const MAX_CRX_EDGE: u32 = 65_536; +/// Unary QP-map prefix length that announces an 8-bit escaped symbol. +const QP_ESCAPE_ZEROS: u32 = 23; +/// Highest uniform (per-subband) quantization value with a measured step. +const MAX_UNIFORM_QUANT: u8 = 44; +/// Highest adaptive quantization value (QP − 124) seen in real files: QP 175 on the EOS R6 Mark II. +/// See [`quant_step`]. +const MAX_ADAPTIVE_QUANT: u8 = 51; +const MAX_QP: i32 = 124 + MAX_ADAPTIVE_QUANT as i32; +const RUN_EXPONENT: [u8; 32] = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + +fn corrupt(reason: &str) -> RawError { + RawError::Corrupt(format!("Canon CRX: {reason}")) +} + +#[derive(Debug)] +struct Band { + data: Range, + padding: usize, + quant: Quantization, + partial: bool, +} + +#[derive(Debug)] +enum Quantization { + Uniform(u8), + Adaptive { base: u32, gain: u32 }, +} + +struct QpMap { + width: usize, + height: usize, + data: Vec, +} + +#[derive(Debug)] +struct Plane { + partial: bool, + rounded_bits: u8, + bands: Vec, +} + +#[derive(Debug)] +struct Tile { + x: usize, + y: usize, + width: usize, + height: usize, + planes: Vec, + qp: Option>, +} + +/// The byte ranges of a CRX sample (relative to its start) that [`validate`] reads: the tile, plane +/// and band headers, and each tile's quantization map. `sample` must hold its first +/// `config.header_size` bytes; the rest is only measured. +pub(crate) fn header_ranges(sample: &[u8], config: &Cr3Compression) -> Result>> { + let tiles = layout(sample, config)?; + let header = 0..config.header_size as usize; + Ok(std::iter::once(header).chain(tiles.into_iter().filter_map(|t| t.qp)).collect()) +} + +/// Decode to full sensor-order CFA samples, including masked borders. +pub(crate) fn decode(sample: &[u8], config: &Cr3Compression) -> Result> { + supports(config)?; + let width = usize::try_from(config.width).map_err(|_| RawError::Limit("CRX width"))?; + let count = image_size(config)?; + let tiles = layout(sample, config)?; + validate_planes(&tiles, config)?; + // Allocated once the first plane decodes: a tiny crafted file can claim a huge image. + let mut output = Vec::new(); + let midpoint = 1i32 << (config.bit_depth - 1); + let maximum = (1i32 << config.bit_depth) - 1; + for tile in tiles { + let qp = tile_qp(sample, &tile)?; + // Each plane is its own entropy stream: decode a tile's planes in parallel. + let planes: Vec>> = tile + .planes + .par_iter() + .enumerate() + .map(|(p, plane)| { + if config.levels == 0 { + let band = plane.bands.first().ok_or_else(|| corrupt("missing lossless band"))?; + lossless_plane(band_bytes(sample, band)?, tile.width / 2, tile.height / 2) + } else { + let edges = TileEdges { left: tile.x > 0, right: tile.x + tile.width < width, top: false, bottom: false }; + wavelet_plane(sample, plane, tile.width / 2, tile.height / 2, edges, qp.as_ref()) + } + .map_err(|error| RawError::Corrupt(format!("Canon CRX tile ({}, {}), plane {p}: {error}", tile.x, tile.y))) + }) + .collect(); + for (p, decoded) in planes.into_iter().enumerate() { + let decoded = decoded?; + if output.is_empty() { + output.try_reserve_exact(count).map_err(|_| RawError::Limit("CRX sample allocation"))?; + output.resize(count, 0); + } + for (i, &value) in decoded.iter().enumerate() { + let sensor_value = sensor_sample(value, midpoint, maximum, config.levels != 0)?; + let (x, y) = (tile.x + 2 * (i % (tile.width / 2)) + p % 2, tile.y + 2 * (i / (tile.width / 2)) + p / 2); + let slot = y + .checked_mul(width) + .and_then(|n| n.checked_add(x)) + .and_then(|n| output.get_mut(n)) + .ok_or_else(|| corrupt("tile writes outside image"))?; + *slot = sensor_value; + } + } + } + Ok(output) +} + +/// A decoded plane value as a sensor sample of the CMP1 bit depth. +/// +/// Lossless planes reproduce the sensor exactly, so a value outside the bit depth is corrupt data. +/// Wavelet (C-RAW) planes are quantized: next to clipped highlights the reconstruction overshoots +/// the sensor's range by up to a few quantization steps (measured on 17 CC0 files: at most 36 codes +/// above 16383 at 14 bits, never below 0), while lossless files of the same bodies clip at exactly +/// 16383. Those samples are clamped to the range the sensor can record. +fn sensor_sample(value: i32, midpoint: i32, maximum: i32, quantized: bool) -> Result { + let sample = value.checked_add(midpoint).ok_or_else(|| corrupt("decoded sample outside its bit depth"))?; + let sample = if quantized { sample.clamp(0, maximum) } else { sample }; + u16::try_from(sample).ok().filter(|_| sample <= maximum).ok_or_else(|| corrupt("decoded sample outside its bit depth")) +} + +/// Header-only probing must reject the same unsupported coding variants as full decoding. +pub(crate) fn validate(sample: &[u8], config: &Cr3Compression) -> Result<()> { + supports(config)?; + image_size(config)?; + let tiles = layout(sample, config)?; + validate_planes(&tiles, config)?; + for tile in &tiles { + tile_qp(sample, tile)?; + } + Ok(()) +} + +fn image_size(config: &Cr3Compression) -> Result { + if config.width > MAX_CRX_EDGE || config.height > MAX_CRX_EDGE { + return Err(RawError::Limit("CRX image edge")); + } + (config.width as usize) + .checked_mul(config.height as usize) + .filter(|&n| n > 0 && n <= MAX_SAMPLES.min(MAX_CRX_SAMPLES)) + .ok_or(RawError::Limit("CRX image size")) +} + +fn supports(config: &Cr3Compression) -> Result<()> { + if ![0x100, 0x200].contains(&config.version) + || config.encoding != 0 + || config.planes != 4 + || config.cfa_pattern > 3 + || ![0, 3].contains(&config.levels) + { + return Err(RawError::Unsupported(format!( + "Canon CRX version {:#x}, encoding {}, {} planes, {} wavelet levels", + config.version, config.encoding, config.planes, config.levels + ))); + } + if !(8..=16).contains(&config.bit_depth) { + return Err(RawError::Unsupported(format!("Canon CRX {}-bit samples", config.bit_depth))); + } + if config.levels == 3 && config.tile_height != config.height { + return Err(RawError::Unsupported("Canon CRX C-RAW with vertical tiles".into())); + } + if config.version == 0x200 && (config.levels != 3 || config.bit_depth != 14 || config.tile_width != config.width) { + return Err(RawError::Unsupported("Canon CRX version 0x200 requires a single-tile 14-bit Bayer C-RAW image".into())); + } + if config.median_bit_depth.is_some() { + return Err(RawError::Unsupported("Canon CRX extended median-bit-depth header".into())); + } + Ok(()) +} + +fn validate_planes(tiles: &[Tile], config: &Cr3Compression) -> Result<()> { + for tile in tiles { + for plane in &tile.planes { + if !plane.partial || plane.rounded_bits != 0 || plane.bands.len() != 1 + usize::from(config.levels) * 3 { + return Err(RawError::Unsupported("Canon CRX plane coding flags".into())); + } + for (index, band) in plane.bands.iter().enumerate() { + if band.partial { + return Err(RawError::Unsupported("Canon CRX partial subband coding".into())); + } + if config.levels != 0 { + match band.quant { + Quantization::Uniform(value) => { + quant_step(value, MAX_UNIFORM_QUANT)?; + } + Quantization::Adaptive { gain, .. } => { + if index < 4 && gain != 0 { + return Err(RawError::Unsupported("Canon CRX adaptive quantization subband parameters".into())); + } + } + } + } + } + } + } + Ok(()) +} + +fn layout(sample: &[u8], config: &Cr3Compression) -> Result> { + let (width, height, tw, th) = (config.width as usize, config.height as usize, config.tile_width as usize, config.tile_height as usize); + if width == 0 + || height == 0 + || tw == 0 + || th == 0 + || tw > width + || th > height + || !width.is_multiple_of(2) + || !height.is_multiple_of(2) + || !tw.is_multiple_of(2) + || !th.is_multiple_of(2) + { + return Err(corrupt("invalid Bayer tile dimensions")); + } + let columns = width.div_ceil(tw); + let rows = height.div_ceil(th); + let tile_count = columns.checked_mul(rows).filter(|&n| n <= MAX_TILES).ok_or(RawError::Limit("CRX tile count"))?; + let header_size = config.header_size as usize; + if header_size > MAX_HEADER_BYTES { + return Err(RawError::Limit("CRX header size")); + } + let header = sample.get(..header_size).ok_or_else(|| corrupt("truncated tile headers"))?; + let mut reader = Header { src: header, at: 0 }; + let mut data_at = header_size; + let mut tiles = Vec::with_capacity(tile_count); + for index in 0..tile_count { + let modern = config.version == 0x200; + let body = reader.marker(if modern { 0xff11 } else { 0xff01 }, if modern { 16 } else { 8 })?; + let tile_size = usize::try_from(be32(body, 0)?).map_err(|_| RawError::Limit("CRX tile size"))?; + if usize::from(be16(body, 4)?) != index { + return Err(corrupt("tile index out of sequence")); + } + let tile_end = data_at.checked_add(tile_size).filter(|&end| end <= sample.len()).ok_or_else(|| corrupt("tile data outside sample"))?; + let qp = if modern { + let length = be32(body, 8)? as usize; + let padding = usize::from(be16(body, 12)?); + if be16(body, 6)? != 0x4000 || be16(body, 14)? != 0 || length == 0 || padding > 7 { + return Err(RawError::Unsupported("Canon CRX adaptive quantization tile header".into())); + } + let end = data_at.checked_add(length).filter(|&end| end <= tile_end).ok_or_else(|| corrupt("QP data outside tile"))?; + let data = data_at..end; + data_at = end.checked_add(padding).filter(|&end| end <= tile_end).ok_or_else(|| corrupt("QP padding outside tile"))?; + Some(data) + } else { + None + }; + let mut planes = Vec::with_capacity(usize::from(config.planes)); + for p in 0..config.planes { + let body = reader.marker(if modern { 0xff12 } else { 0xff02 }, 8)?; + let size = be32(body, 0)? as usize; + let flags = be32(body, 4)?; + if flags >> 28 != u32::from(p) { + return Err(corrupt("plane index out of sequence")); + } + let plane_end = data_at.checked_add(size).filter(|&end| end <= tile_end).ok_or_else(|| corrupt("plane size exceeds tile"))?; + let mut bands = Vec::with_capacity(usize::from(config.levels) * 3 + 1); + for b in 0..u32::from(config.levels) * 3 + 1 { + let body = reader.marker(if modern { 0xff13 } else { 0xff03 }, if modern { 16 } else { 8 })?; + let band_size = be32(body, 0)? as usize; + let band_flags = be32(body, 4)?; + if band_flags >> 28 != b { + return Err(corrupt("band index out of sequence")); + } + let band_end = data_at.checked_add(band_size).filter(|&end| end <= plane_end).ok_or_else(|| corrupt("band size exceeds plane"))?; + let padding = if modern { usize::from(be16(body, 12)?) } else { (band_flags & 0x7ffff) as usize }; + if padding > band_size || (modern && padding > 7) { + return Err(corrupt("invalid band padding")); + } + bands.push(Band { + data: data_at..band_end, + padding, + quant: if modern { + if be16(body, 14)? != 0 { + return Err(RawError::Unsupported("Canon CRX adaptive subband reserved field".into())); + } + Quantization::Adaptive { base: be32(body, 8)?, gain: band_flags & 0x07ff_ffff } + } else { + Quantization::Uniform(((band_flags >> 19) & 0xff) as u8) + }, + partial: band_flags & 0x0800_0000 != 0, + }); + data_at = band_end; + } + if data_at != plane_end { + return Err(corrupt("band sizes do not sum to plane size")); + } + planes.push(Plane { partial: flags & 0x0800_0000 != 0, rounded_bits: ((flags >> 25) & 3) as u8, bands }); + } + if data_at != tile_end { + return Err(corrupt("plane sizes do not sum to tile size")); + } + let (x, y) = ((index % columns) * tw, (index / columns) * th); + tiles.push(Tile { x, y, width: tw.min(width - x), height: th.min(height - y), planes, qp }); + } + if header.get(reader.at..).is_none_or(|rest| rest.iter().any(|&byte| byte != 0)) { + return Err(corrupt("unexpected bytes after tile headers")); + } + if data_at != sample.len() { + return Err(corrupt("tile sizes do not cover the sample")); + } + Ok(tiles) +} + +fn band_bytes<'a>(sample: &'a [u8], band: &Band) -> Result<&'a [u8]> { + let end = + band.data.end.checked_sub(band.padding).filter(|&end| end >= band.data.start).ok_or_else(|| corrupt("band padding exceeds its size"))?; + sample.get(band.data.start..end).ok_or_else(|| corrupt("band data outside sample")) +} + +fn tile_qp(sample: &[u8], tile: &Tile) -> Result> { + tile.qp + .as_ref() + .map(|range| { + let bytes = sample.get(range.clone()).ok_or_else(|| corrupt("QP map outside sample"))?; + qp_map(bytes, tile.width.div_ceil(16), tile.height.div_ceil(4)) + }) + .transpose() +} + +fn quant_step(value: u8, max: u8) -> Result { + // Measured independently with an external decoder after modifying only the + // quantValue field: all forty values 4..43 follow this integer sequence. Value 44 (QP 168) + // continues it (step 102); it was checked against a measured step of about 100 on an EOS R50 + // C-RAW file. Uniform subband values stop there (`MAX_UNIFORM_QUANT`). Adaptive QP maps reach + // QP 175 (value 51) on the EOS R6 Mark II; those steps (114..228) continue the same sequence and + // were checked against the camera's own JPEGs (docs/cr3.md). Anything higher is still rejected. + if !(4..=max).contains(&value) { + return Err(RawError::Unsupported(format!("Canon CRX quantization value {value}"))); + } + let scale = [40i32, 45, 51, 57, 64, 72].get(usize::from(value % 6)).copied().ok_or_else(|| corrupt("quantization table index"))?; + Ok((scale << (value / 6)) / 64) +} + +fn split_dimension(length: usize, before: bool, after: bool) -> (usize, usize) { + (length.div_ceil(2) + usize::from(after && length.is_multiple_of(2)), length / 2 + usize::from(before) + usize::from(after)) +} + +/// The width at which high-frequency band `index` (1–9, coarsest level first) is entropy coded, +/// given its synthesis width `width`. +/// +/// A tile with a neighbour on its right codes the vertical high-pass band of the level-1 and level-2 +/// low-pass columns (bands 8 and 5, whose low-pass columns the next level transforms again) at an odd +/// width: an even synthesis width gains one column, which synthesis does not use. Every other band, +/// including band 2 of the last level, is coded at its synthesis width. Measured on every C-RAW plane +/// that `tests/it/cr3_corpus.rs` decodes (CC0 raw.pixls.us files): two-tile files with tile widths 2816, +/// 3144, 3192, 3444 and 3564 (G5 X Mark II, M50, R100, 250D, RP, R, 90D, M6 Mark II), where bands 5 +/// and 8 gain a column in left tiles exactly when the rule says so, and single-tile `0x100` and +/// `0x200` files, which never extend a band. In every one of those planes, each band's entropy stream +/// ends exactly at that width and at no other width from `width` to `width + 2`. The width used to be +/// probed by trying `width`, `width + 1` and `width + 2` in turn, but two widths can both consume a +/// stream exactly and give different coefficients (storytold/lightcraft#899), so it is now taken from the geometry. +/// No public file has a middle tile (three or more C-RAW tiles); it is assumed to follow the left tile. +fn coded_band_width(index: usize, width: usize, edges: TileEdges) -> usize { + if edges.right && matches!(index, 5 | 8) { width | 1 } else { width } +} + +/// A high-frequency band entropy coded `coded_width` columns wide, cropped to its `width` synthesis columns. +fn coded_high_frequency_band(src: &[u8], width: usize, coded_width: usize, height: usize) -> Result> { + let values = high_frequency_band(src, coded_width, height)?; + if coded_width == width { + return Ok(values); + } + let mut cropped = Vec::new(); + cropped.try_reserve_exact(width.saturating_mul(height)).map_err(|_| RawError::Limit("CRX cropped band allocation"))?; + for row in values.chunks_exact(coded_width) { + cropped.extend_from_slice(row.get(..width).ok_or_else(|| corrupt("band crop outside row"))?); + } + Ok(cropped) +} + +fn wavelet_plane(sample: &[u8], plane: &Plane, width: usize, height: usize, edges: TileEdges, qp: Option<&QpMap>) -> Result> { + let mut dimensions = vec![(width, height)]; + let mut high_dimensions = Vec::new(); + for _ in 0..3 { + let &(w, h) = dimensions.last().ok_or_else(|| corrupt("missing wavelet dimensions"))?; + let (lw, hw) = split_dimension(w, edges.left, edges.right); + let (lh, hh) = split_dimension(h, edges.top, edges.bottom); + dimensions.push((lw, lh)); + high_dimensions.push([(hw, lh), (lw, hh), (hw, hh)]); + } + let mut shapes = vec![*dimensions.last().ok_or_else(|| corrupt("missing LL dimensions"))?]; + for high in high_dimensions.iter().rev() { + shapes.extend_from_slice(high); + } + let mut bands = Vec::with_capacity(10); + for (i, (&(w, h), band)) in shapes.iter().zip(&plane.bands).enumerate() { + let bytes = band_bytes(sample, band)?; + let mut data = if i == 0 { lossless_plane(bytes, w, h)? } else { coded_high_frequency_band(bytes, w, coded_band_width(i, w, edges), h)? }; + for (position, value) in data.iter_mut().enumerate() { + let step = match band.quant { + Quantization::Uniform(value) => quant_step(value, MAX_UNIFORM_QUANT)?, + Quantization::Adaptive { base, gain } => { + adaptive_step(qp.ok_or_else(|| corrupt("missing QP map"))?, i, position % w, position / w, base, gain)? + } + }; + *value = value.checked_mul(step).ok_or_else(|| corrupt("dequantized coefficient overflow"))?; + } + bands.push(WaveletBand { width: w, height: h, data }); + } + crx_wavelet::reconstruct(&bands, width, height, 3, edges) +} + +fn adaptive_step(map: &QpMap, band: usize, x: usize, y: usize, base: u32, gain: u32) -> Result { + let step = if gain == 0 { + u64::from(base) + } else { + let (row, column) = if (4..=6).contains(&band) { + (y * 2, x / 2) + } else if (7..=9).contains(&band) { + (y, x / 4) + } else { + return Err(RawError::Unsupported("Canon CRX adaptive low-frequency quantization".into())); + }; + if row >= map.height || column >= map.width { + return Err(corrupt("QP coordinate outside map")); + } + let index = row.checked_mul(map.width).and_then(|n| n.checked_add(column)).ok_or(RawError::Limit("CRX QP map index"))?; + let first = *map.data.get(index).ok_or_else(|| corrupt("QP coordinate outside map"))?; + let value = if band < 7 { + // An odd-height map (R7: 4732 rows give 1183 QP rows) leaves the + // last row of the coarser bands without a partner; it is used alone. + let second_row = (row + 1).min(map.height - 1); + let second = *map.data.get(second_row * map.width + column).ok_or_else(|| corrupt("QP average outside map"))?; + (first + second) / 2 + } else { + first + }; + // QP rows are averaged before looking up the integer step. R8 field + // mutations distinguish this order from averaging dequantization steps. + let quant = u8::try_from(value - 124).map_err(|_| corrupt("QP value outside quantization table"))?; + u64::from(base) + u64::from(gain) * quant_step(quant, MAX_ADAPTIVE_QUANT)? as u64 / 8 + }; + i32::try_from(step.max(1)).map_err(|_| corrupt("adaptive quantization step outside precision")) +} + +struct Header<'a> { + src: &'a [u8], + at: usize, +} + +impl<'a> Header<'a> { + fn marker(&mut self, wanted: u16, wanted_size: usize) -> Result<&'a [u8]> { + let marker = be16(self.src, self.at)?; + if marker == wanted + 0x10 { + return Err(RawError::Unsupported("Canon CRX ff11/ff12/ff13 marker family".into())); + } + if marker != wanted { + return Err(corrupt("unexpected tile/plane/band marker")); + } + let size = usize::from(be16(self.src, self.at + 2)?); + if size != wanted_size { + return Err(RawError::Unsupported(format!("Canon CRX marker {marker:#x} header length {size}"))); + } + let start = self.at.checked_add(4).ok_or(RawError::Limit("CRX header offset"))?; + let end = start.checked_add(size).ok_or(RawError::Limit("CRX header offset"))?; + let body = self.src.get(start..end).ok_or_else(|| corrupt("truncated marker header"))?; + self.at = end; + Ok(body) + } +} + +fn be16(src: &[u8], at: usize) -> Result { + let end = at.checked_add(2).ok_or(RawError::Limit("CRX header offset"))?; + let bytes = src.get(at..end).and_then(|s| <[u8; 2]>::try_from(s).ok()).ok_or_else(|| corrupt("truncated header field"))?; + Ok(u16::from_be_bytes(bytes)) +} + +fn be32(src: &[u8], at: usize) -> Result { + let end = at.checked_add(4).ok_or(RawError::Limit("CRX header offset"))?; + let bytes = src.get(at..end).and_then(|s| <[u8; 4]>::try_from(s).ok()).ok_or_else(|| corrupt("truncated header field"))?; + Ok(u32::from_be_bytes(bytes)) +} + +struct Bits<'a> { + src: &'a [u8], + at: usize, +} + +impl Bits<'_> { + /// The next 32 bits, MSB first, when at least that many remain before the end of the stream. + fn peek32(&self) -> Option { + let bytes = self.src.get(self.at / 8..self.at / 8 + 5)?; + let word = u64::from_be_bytes([0, 0, 0, bytes[0], bytes[1], bytes[2], bytes[3], bytes[4]]); + Some((word >> (8 - self.at % 8)) as u32) + } + + fn take(&mut self, count: u32) -> Result { + if count > 24 { + return Err(corrupt("invalid Rice parameter")); + } + if count == 0 { + return Ok(0); + } + if let Some(window) = self.peek32() { + self.at += count as usize; + return Ok(window >> (32 - count)); + } + // Near the end of the stream: bit by bit, so truncation is reported exactly. + let mut value = 0; + for _ in 0..count { + let byte = self.src.get(self.at / 8).ok_or_else(|| corrupt("truncated entropy stream"))?; + value = (value << 1) | u32::from((byte >> (7 - self.at % 8)) & 1); + self.at += 1; + } + Ok(value) + } + + fn rice(&mut self, k: u32) -> Result { + // Common case: the unary prefix and its terminating 1 lie within the next 32 bits. + if let Some(window) = self.peek32().filter(|&w| w != 0) { + let zeros = window.leading_zeros(); + self.at += zeros as usize + 1; + return if k <= 20 { Ok((zeros << k) | self.take(k)?) } else { Err(corrupt("Rice parameter exceeds sample precision")) }; + } + let mut zeros = 0; + while self.take(1)? == 0 { + zeros += 1; + if zeros > 41 { + return Err(corrupt("Rice escape prefix exceeds 41 bits")); + } + } + if zeros == 41 { + self.take(21) + } else if k <= 20 { + Ok((zeros << k) | self.take(k)?) + } else { + Err(corrupt("Rice parameter exceeds sample precision")) + } + } + + fn qp_rice(&mut self, k: u32) -> Result { + // Limited-length code (the ITU-T T.87 layout for a 32-bit limit and 8-bit symbols): + // a quotient of 23 or more is sent as 23 zeros, the terminating 1 and the symbol in 8 bits. + let mut zeros = 0; + while self.take(1)? == 0 { + zeros += 1; + if zeros > QP_ESCAPE_ZEROS { + return Err(corrupt("QP Rice escape prefix exceeds 23 bits")); + } + } + if zeros == QP_ESCAPE_ZEROS { + self.take(8) + } else if k <= 20 { + Ok((zeros << k) | self.take(k)?) + } else { + Err(corrupt("QP Rice parameter exceeds precision")) + } + } + + fn finish(&self) -> Result<()> { + let bits = self.src.len().checked_mul(8).ok_or(RawError::Limit("CRX entropy length"))?; + if bits.saturating_sub(self.at) > 7 { + return Err(corrupt("unconsumed entropy data")); + } + if let Some(&last) = self.src.last() { + let remaining = (bits - self.at) as u32; + if u32::from(last) & ((1 << remaining) - 1) != 0 { + return Err(corrupt("nonzero entropy alignment bits")); + } + } + Ok(()) + } +} + +fn signed(symbol: u32) -> i32 { + (symbol >> 1) as i32 ^ -((symbol & 1) as i32) +} + +fn update_rice(mut k: u32, symbol: u32) -> u32 { + if symbol >= 3 << k { + k += 1; + if symbol >= 3 << k { + k += 1; + } + } else if k > 0 && symbol < 1 << (k - 1) { + k -= 1; + } + k.min(20) +} + +fn median(left: i32, above: i32, corner: i32) -> i32 { + if corner >= left.max(above) { + left.min(above) + } else if corner <= left.min(above) { + left.max(above) + } else { + left + above - corner + } +} + +fn run(bits: &mut Bits<'_>, index: &mut usize, remaining: usize) -> Result { + let mut length = 1usize; + while length < remaining { + let exponent = u32::from(*RUN_EXPONENT.get(*index).ok_or_else(|| corrupt("run index outside table"))?); + if bits.take(1)? == 0 { + length += bits.take(exponent)? as usize; + *index = index.saturating_sub(1); + if length > remaining { + return Err(corrupt("run exceeds row")); + } + break; + } + length += 1usize << exponent; + if length <= remaining { + *index = (*index + 1).min(RUN_EXPONENT.len() - 1); + } + } + Ok(length.min(remaining)) +} + +fn lossless_plane(src: &[u8], width: usize, height: usize) -> Result> { + let count = width + .checked_mul(height) + .filter(|&n| width > 0 && n > 0 && n <= MAX_SAMPLES.min(MAX_CRX_SAMPLES) / 4) + .ok_or(RawError::Limit("CRX plane size"))?; + // Grows per decoded row: a truncated stream fails before the whole plane is reserved. + let mut output = Vec::new(); + let mut previous = Vec::new(); + previous.try_reserve_exact(width).map_err(|_| RawError::Limit("CRX row allocation"))?; + previous.resize(width, 0i32); + let mut current = Vec::new(); + current.try_reserve_exact(width).map_err(|_| RawError::Limit("CRX row allocation"))?; + current.resize(width, 0i32); + let (mut k, mut run_index) = (0u32, 0usize); + let mut bits = Bits { src, at: 0 }; + for y in 0..height { + let mut x = 0; + let mut left = previous.first().copied().ok_or_else(|| corrupt("empty plane row"))?; + while x < width { + let mut interrupted = false; + let above = *previous.get(x).ok_or_else(|| corrupt("above sample outside row"))?; + let right = previous.get(x + 1).copied().unwrap_or(above); + if x + 1 < width && left == above && left == right && bits.take(1)? != 0 { + let length = run(&mut bits, &mut run_index, width - x)?; + let end = x.checked_add(length).ok_or(RawError::Limit("CRX run length"))?; + current.get_mut(x..end).ok_or_else(|| corrupt("run outside row"))?.fill(left); + x = end; + if x == width { + continue; + } + // A run interruption always uses regular prediction; no second run flag. + interrupted = true; + } + let above = *previous.get(x).ok_or_else(|| corrupt("above sample outside row"))?; + let corner = if x == 0 { above } else { *previous.get(x - 1).ok_or_else(|| corrupt("corner sample outside row"))? }; + let right = previous.get(x + 1).copied().unwrap_or(above); + let symbol = bits.rice(k)?; + let prediction = if interrupted { above } else { median(left, above, corner) }; + let value = prediction + .checked_add(signed(symbol)) + .filter(|v| (-1_048_576..=1_048_575).contains(v)) + .ok_or_else(|| RawError::Corrupt(format!("Canon CRX prediction outside precision at row {y}, column {x}, k {k}, symbol {symbol}")))?; + *current.get_mut(x).ok_or_else(|| corrupt("decoded sample outside row"))? = value; + left = value; + let estimate = if y > 0 && x + 1 < width { + symbol.checked_add(2 * right.abs_diff(above)).ok_or_else(|| corrupt("Rice estimate overflow"))? / 2 + } else { + symbol + }; + k = update_rice(k, estimate); + x += 1; + } + output.try_reserve(width).map_err(|_| RawError::Limit("CRX plane allocation"))?; + output.extend_from_slice(¤t); + std::mem::swap(&mut current, &mut previous); + } + bits.finish()?; + debug_assert_eq!(output.len(), count); + Ok(output) +} + +fn high_frequency_band(src: &[u8], width: usize, height: usize) -> Result> { + let count = width + .checked_mul(height) + .filter(|&n| width > 0 && n > 0 && n <= MAX_SAMPLES.min(MAX_CRX_SAMPLES) / 4) + .ok_or(RawError::Limit("CRX high-frequency band size"))?; + // Grows per decoded row, as in `lossless_plane`. + let mut output = Vec::new(); + let row = || -> Result> { + let mut values = Vec::new(); + values.try_reserve_exact(width).map_err(|_| RawError::Limit("CRX row allocation"))?; + values.resize(width, 0); + Ok(values) + }; + let (mut previous, mut current) = (row()?, row()?); + let (mut previous_k, mut current_k) = (row()?, row()?); + let (mut k, mut run_index) = (0u32, 0usize); + let mut bits = Bits { src, at: 0 }; + for y in 0..height { + let (mut x, mut left) = (0, 0); + while x < width { + let above = *previous.get(x).ok_or_else(|| corrupt("above coefficient outside row"))?; + let right = previous.get(x + 1).copied().unwrap_or(above); + let mut nonzero = false; + if x + 1 < width && left == 0 && above == 0 && right == 0 { + if bits.take(1)? != 0 { + let length = run(&mut bits, &mut run_index, width - x)?; + let end = x.checked_add(length).ok_or(RawError::Limit("CRX run length"))?; + current.get_mut(x..end).ok_or_else(|| corrupt("coefficient run outside row"))?.fill(0); + // The upper-row context for a zero run is zero, although the + // current Rice parameter remains unchanged across the run. + current_k.get_mut(x..end).ok_or_else(|| corrupt("Rice context run outside row"))?.fill(0); + left = 0; + x = end; + if x == width { + continue; + } + } + nonzero = true; + } + let coded = bits.rice(k)?; + // A zero-run flag 0 or its interruption proves that the coefficient + // is nonzero. That context omits zero from the signed mapping. + let symbol = coded.checked_add(u32::from(nonzero)).ok_or_else(|| corrupt("coefficient symbol overflow"))?; + let value = signed(symbol); + if !(-1_048_576..=1_048_575).contains(&value) { + return Err(corrupt("high-frequency coefficient outside precision")); + } + *current.get_mut(x).ok_or_else(|| corrupt("coefficient outside row"))? = value; + left = value; + k = update_rice(k, coded); + if y > 0 && x + 1 < width { + let upper_right_k = *previous_k.get(x + 1).ok_or_else(|| corrupt("Rice context outside row"))? as u32; + if upper_right_k > k + 1 { + k += 1; + } + } + *current_k.get_mut(x).ok_or_else(|| corrupt("Rice context outside row"))? = k as i32; + x += 1; + } + output.try_reserve(width).map_err(|_| RawError::Limit("CRX coefficient allocation"))?; + output.extend_from_slice(¤t); + std::mem::swap(&mut current, &mut previous); + std::mem::swap(&mut current_k, &mut previous_k); + } + bits.finish()?; + debug_assert_eq!(output.len(), count); + Ok(output) +} + +fn qp_map(src: &[u8], width: usize, height: usize) -> Result { + let count = width.checked_mul(height).filter(|&n| width > 0 && n > 0 && n <= MAX_CRX_SAMPLES / 32).ok_or(RawError::Limit("CRX QP map size"))?; + let mut data = Vec::new(); + data.try_reserve_exact(count).map_err(|_| RawError::Limit("CRX QP allocation"))?; + let row = || -> Result> { + let mut values = Vec::new(); + values.try_reserve_exact(width).map_err(|_| RawError::Limit("CRX QP row allocation"))?; + values.resize(width, 0); + Ok(values) + }; + let (mut previous, mut current) = (row()?, row()?); + let mut bits = Bits { src, at: 0 }; + let mut k = 0; + for y in 0..height { + let mut left = *previous.first().ok_or_else(|| corrupt("empty QP row"))?; + for x in 0..width { + let above = *previous.get(x).ok_or_else(|| corrupt("above QP outside row"))?; + let corner = if x == 0 { above } else { *previous.get(x - 1).ok_or_else(|| corrupt("corner QP outside row"))? }; + let right = previous.get(x + 1).copied().unwrap_or(above); + // The map opens with its first symbol as a 32-bit literal; every later one is Rice coded. + let symbol = if x == 0 && y == 0 { (bits.take(16)? << 16) | bits.take(16)? } else { bits.qp_rice(k)? }; + let value = median(left, above, corner).checked_add(signed(symbol)).ok_or_else(|| corrupt("QP prediction overflow"))?; + if !(131..=MAX_QP).contains(&value) { + return Err(RawError::Unsupported(format!("Canon CRX adaptive QP value {value}"))); + } + *current.get_mut(x).ok_or_else(|| corrupt("QP outside row"))? = value; + left = value; + let estimate = if y > 0 && x + 1 < width { + symbol.checked_add(2 * right.abs_diff(above)).ok_or_else(|| corrupt("QP Rice estimate overflow"))? / 2 + } else { + symbol + }; + k = update_rice(k, estimate); + } + data.extend_from_slice(¤t); + std::mem::swap(&mut current, &mut previous); + } + bits.finish()?; + Ok(QpMap { width, height, data }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn coding() -> Cr3Compression { + Cr3Compression { + version: 0x100, + width: 4, + height: 4, + tile_width: 4, + tile_height: 4, + bit_depth: 14, + planes: 4, + cfa_pattern: 0, + encoding: 0, + levels: 0, + tile_flags: 0, + header_size: 108, + median_bit_depth: None, + } + } + + fn marker(out: &mut Vec, code: u16, size: u32, flags: u32) { + out.extend(code.to_be_bytes()); + out.extend(8u16.to_be_bytes()); + out.extend(size.to_be_bytes()); + out.extend(flags.to_be_bytes()); + } + + fn flat_sample() -> Vec { + let mut out = Vec::new(); + marker(&mut out, 0xff01, 4, 0); + for p in 0..4 { + marker(&mut out, 0xff02, 1, (p << 28) | 0x0800_0000); + marker(&mut out, 0xff03, 1, 0x0020_0000); + } + // Four 2×2 planes at coefficient 1. Row 0: zero-run flag 0, Rice(2,k0)=001, + // Rice(0,k0)=1. Row 1: nonzero-run flag 1, MELCODE(RL−1=1)=1. Zero alignment. + out.extend([0x1e; 4]); + out + } + + #[test] + fn sensor_mosaic_from_four_lossless_planes() { + let result = decode(&flat_sample(), &coding()).unwrap(); + assert_eq!(result, vec![8193; 16]); + } + + #[test] + fn med_signed_mapping_and_adaptation() { + assert_eq!([signed(0), signed(1), signed(2), signed(3)], [0, -1, 1, -2]); + assert_eq!(median(-7, -8, -9), -7); + assert_eq!(median(-7, -8, -6), -8); + assert_eq!(median(10, 20, 14), 16); + assert_eq!(update_rice(0, 12_289), 2); + assert_eq!(update_rice(2, 58), 4); + assert_eq!(update_rice(4, 7), 3); + assert_eq!(update_rice(4, 8), 4); + } + + #[test] + fn interrupted_run_predicts_from_above() { + // First row [5, 5, 8, 9]. The next row runs three 5s, so interruption at + // column 3 must predict the above sample 9, then add −2 to obtain 7. + assert_eq!(lossless_plane(&[0x00, 0x18, 0x2d, 0xdc], 4, 2).unwrap(), [5, 5, 8, 9, 5, 5, 5, 7]); + } + + #[test] + fn partial_final_run_segment_keeps_its_index() { + let mut bits = Bits { src: &[0xc0], at: 0 }; + let mut index = 11; + assert_eq!(run(&mut bits, &mut index, 6).unwrap(), 6); + assert_eq!(index, 12); + assert_eq!(bits.at, 2); + } + + #[test] + fn every_truncated_header_is_an_error() { + let sample = flat_sample(); + for n in 0..sample.len() { + assert!(decode(&sample[..n], &coding()).is_err(), "prefix {n}"); + } + } + + #[test] + fn inconsistent_parent_sizes_and_indices_are_errors() { + let original = flat_sample(); + for (at, mask) in [(7, 1), (9, 1), (19, 1), (20, 0x10), (31, 1), (32, 0x10)] { + let mut broken = original.clone(); + broken[at] ^= mask; + assert!(decode(&broken, &coding()).is_err(), "offset {at}"); + } + } + + #[test] + fn entropy_truncation_and_invalid_alignment_are_errors() { + assert!(lossless_plane(&[], 2, 2).is_err()); + assert!(lossless_plane(&[0x1f], 2, 2).is_err()); + assert!(lossless_plane(&[0x1e, 0], 2, 2).is_err()); + assert!(lossless_plane(&[0; 8], 2, 2).is_err()); + } + + #[test] + fn high_frequency_zero_runs_and_nonzero_mapping() { + assert_eq!(high_frequency_band(&[0xf0], 2, 2).unwrap(), [0, 0, 0, 0]); + assert_eq!(high_frequency_band(&[0x34, 0x80], 2, 2).unwrap(), [1, 0, -1, 1]); + assert!(high_frequency_band(&[0x34], 2, 2).is_err()); + assert!(high_frequency_band(&[0xf1], 2, 2).is_err()); + } + + #[test] + fn unsupported_wavelet_variant_is_explicit() { + let mut config = coding(); + config.levels = 2; + assert!(matches!(decode(&flat_sample(), &config), Err(RawError::Unsupported(_)))); + assert!(matches!(validate(&flat_sample(), &config), Err(RawError::Unsupported(_)))); + } + + #[test] + fn three_level_craw_reconstructs_sensor_samples() { + let mut config = coding(); + config.width = 16; + config.height = 16; + config.tile_width = 16; + config.tile_height = 16; + config.header_size = 540; + config.levels = 3; + let mut sample = Vec::new(); + marker(&mut sample, 0xff01, 52, 0); + for plane in 0..4 { + marker(&mut sample, 0xff02, 13, (plane << 28) | 0x0800_0000); + for band in 0..10 { + marker(&mut sample, 0xff03, if band >= 7 { 2 } else { 1 }, (band << 28) | 0x0020_0000); + } + } + for _ in 0..4 { + sample.extend([0x80; 4]); // LL3 and the three 1×1 HF3 bands. + sample.extend([0xf0; 3]); // Three 2×2 all-zero HF2 bands. + for _ in 0..3 { + sample.extend([0xff, 0xf8]); // Three 4×4 all-zero HF1 bands. + } + } + validate(&sample, &config).unwrap(); + assert_eq!(decode(&sample, &config).unwrap(), vec![8192; 256]); + for end in 0..sample.len() { + assert!(decode(&sample[..end], &config).is_err()); + } + } + + #[test] + fn coded_band_width_follows_the_measured_tile_geometry() { + let left_tile = TileEdges { right: true, ..TileEdges::default() }; + let right_tile = TileEdges { left: true, ..TileEdges::default() }; + let single = TileEdges::default(); + // Bands 5 and 8 of a left tile: an even width gains a column (RP 400 -> 401, R 862 -> 863, + // 90D / M6 Mark II 892 -> 893), an odd one does not (90D / M6 Mark II band 5 at 447). + for (band, width, coded) in [(5, 394, 395), (5, 400, 401), (5, 432, 433), (5, 447, 447), (8, 862, 863), (8, 892, 893), (8, 705, 705)] { + assert_eq!(coded_band_width(band, width, left_tile), coded, "band {band} at {width}"); + } + // The last level's low-pass columns are not transformed again: band 2 keeps an even width. + assert_eq!(coded_band_width(2, 198, left_tile), 198); + for band in [1, 3, 4, 6, 7, 9] { + assert_eq!(coded_band_width(band, 394, left_tile), 394, "band {band}"); + } + for band in 1..=9 { + assert_eq!(coded_band_width(band, 446, right_tile), 446, "band {band}"); + assert_eq!(coded_band_width(band, 446, single), 446, "band {band}"); + } + } + + #[test] + fn a_band_that_decodes_at_two_widths_uses_the_coded_width() { + // storytold/lightcraft#899: entropy alignment alone does not identify the coded width. This 2-row stream is + // consumed exactly both 2 and 3 columns wide, with different retained coefficients. + let src = [0xc0, 0x0f]; + assert_eq!(high_frequency_band(&src, 2, 2).unwrap(), [0, 0, 5, -2]); + assert_eq!(high_frequency_band(&src, 3, 2).unwrap(), [0, 0, 5, 0, 0, 0]); + // The width probe took the first width that decoded (2). Band 8 of a left tile at an even + // width is coded one column wider, so the geometry selects 3 and crops it. + let coded = coded_band_width(8, 2, TileEdges { right: true, ..TileEdges::default() }); + assert_eq!(coded, 3); + assert_eq!(coded_high_frequency_band(&src, 2, coded, 2).unwrap(), [0, 0, 0, 0]); + // Elsewhere the same stream is read at its synthesis width. + assert_eq!(coded_high_frequency_band(&src, 2, coded_band_width(7, 2, TileEdges::default()), 2).unwrap(), [0, 0, 5, -2]); + // The storytold/lightcraft#899 reproducer: an odd width is never extended, so the narrow reading is kept. + let src = [0x00, 0x33]; + assert_eq!(coded_band_width(5, 3, TileEdges { right: true, ..TileEdges::default() }), 3); + assert_eq!(coded_high_frequency_band(&src, 3, 3, 1).unwrap(), [5, 0, -1]); + assert_eq!(high_frequency_band(&src, 4, 1).unwrap(), [5, 0, 0, 0]); + } + + #[test] + fn a_band_that_fails_at_its_coded_width_is_an_error() { + // No fallback to another width: a stream that only fits 2 columns is corrupt where 3 are coded. + let src = [0xc0, 0x0f]; + assert!(high_frequency_band(&src, 4, 2).is_err()); + assert!(coded_high_frequency_band(&src, 3, 4, 2).is_err()); + } + + #[test] + fn craw_overshoot_at_clipped_highlights_is_clamped_to_the_bit_depth() { + // As `three_level_craw_reconstructs_sensor_samples`, but each LL3 coefficient is +8200 + // (Rice escape: 41 zeros, 1, 21-bit literal 16400 = signed +8200). Flat synthesis gives + // 8192 + 8200 = 16392, 9 codes above the 14-bit maximum, as in clipped C-RAW highlights. + let mut config = coding(); + config.width = 16; + config.height = 16; + config.tile_width = 16; + config.tile_height = 16; + config.header_size = 540; + config.levels = 3; + let mut sample = Vec::new(); + marker(&mut sample, 0xff01, 80, 0); + for plane in 0..4 { + marker(&mut sample, 0xff02, 20, (plane << 28) | 0x0800_0000); + for band in 0..10 { + marker(&mut sample, 0xff03, [8, 1, 1, 1, 1, 1, 1, 2, 2, 2][band as usize], (band << 28) | 0x0020_0000); + } + } + for _ in 0..4 { + sample.extend([0, 0, 0, 0, 0, 0x40, 0x80, 0x20]); // LL3 = +8200. + sample.extend([0x80; 3]); // The three 1×1 HF3 bands. + sample.extend([0xf0; 3]); // Three 2×2 all-zero HF2 bands. + for _ in 0..3 { + sample.extend([0xff, 0xf8]); // Three 4×4 all-zero HF1 bands. + } + } + validate(&sample, &config).unwrap(); + assert_eq!(decode(&sample, &config).unwrap(), vec![16383; 256]); + } + + #[test] + fn only_quantized_planes_clamp_out_of_range_samples() { + assert_eq!(sensor_sample(8200, 8192, 16383, true).unwrap(), 16383); + assert_eq!(sensor_sample(-8300, 8192, 16383, true).unwrap(), 0); + assert_eq!(sensor_sample(8191, 8192, 16383, false).unwrap(), 16383); + assert_eq!(sensor_sample(-8192, 8192, 16383, false).unwrap(), 0); + // Lossless planes reproduce the sensor exactly: outside the bit depth is corrupt data. + assert!(matches!(sensor_sample(8192, 8192, 16383, false), Err(RawError::Corrupt(_)))); + assert!(matches!(sensor_sample(-8193, 8192, 16383, false), Err(RawError::Corrupt(_)))); + assert!(matches!(sensor_sample(i32::MAX, 8192, 16383, true), Err(RawError::Corrupt(_)))); + } + + #[test] + fn measured_quantization_and_allocation_limits() { + for (quant, expected) in [(4, 1), (9, 1), (10, 2), (16, 4), (22, 8), (26, 12), (32, 25), (36, 40), (41, 72), (42, 80), (43, 90), (44, 102)] { + assert_eq!(quant_step(quant, MAX_UNIFORM_QUANT).unwrap(), expected); + } + assert!(matches!(quant_step(3, MAX_UNIFORM_QUANT), Err(RawError::Unsupported(_)))); + assert!(matches!(quant_step(45, MAX_UNIFORM_QUANT), Err(RawError::Unsupported(_)))); + // Adaptive QP 169..=175 (EOS R6 Mark II C-RAW) continue the sequence; QP 176 does not decode. + for (quant, expected) in [(45, 114), (46, 128), (47, 144), (48, 160), (49, 180), (50, 204), (51, 228)] { + assert_eq!(quant_step(quant, MAX_ADAPTIVE_QUANT).unwrap(), expected); + } + assert!(matches!(quant_step(52, MAX_ADAPTIVE_QUANT), Err(RawError::Unsupported(_)))); + let high = QpMap { width: 1, height: 2, data: vec![172, 175] }; + // floor((172+175)/2) = 173 → value 49, step 180; gain 8/8 plus base 1. + assert_eq!(adaptive_step(&high, 4, 0, 0, 1, 8).unwrap(), 181); + assert_eq!(adaptive_step(&high, 7, 0, 1, 0, 8).unwrap(), 228); + for (width, height) in [(20_000, 20_000), (65_538, 2), (2, 65_538)] { + let mut config = coding(); + config.width = width; + config.height = height; + assert!(matches!(decode(&[], &config), Err(RawError::Limit(_)))); + assert!(matches!(validate(&[], &config), Err(RawError::Limit(_)))); + } + } + + #[test] + fn qp_escape_and_average_precede_the_quantization_table() { + // 23 zeros, the terminating 1, then the symbol in 8 bits. + let mut bits = Bits { src: &[0, 0, 1, 0x18], at: 0 }; + assert_eq!(bits.qp_rice(0).unwrap(), 24); + assert_eq!(bits.at, 32); + // 22 zeros are an ordinary Rice prefix. + let mut bits = Bits { src: &[0, 0, 2, 0], at: 0 }; + assert_eq!(bits.qp_rice(0).unwrap(), 22); + assert_eq!(bits.at, 23); + assert!(Bits { src: &[0, 0, 0, 0x80, 0], at: 0 }.qp_rice(0).is_err()); + // The map's first symbol is a 32-bit literal (300 = QP 150 from prediction 0). + let map = qp_map(&[0, 0, 1, 0x2c, 0x94], 1, 4).unwrap(); + assert_eq!(map.data, [150; 4]); + assert!(qp_map(&[0, 0, 1, 0x2c, 0x95], 1, 4).is_err()); + let map = QpMap { width: 1, height: 2, data: vec![150, 153] }; + // floor((150+153)/2)=151; integer step 14, gain4/8 gives7. Averaging + // the nonlinear steps, or rounding QP upward, incorrectly gives8. + assert_eq!(adaptive_step(&map, 4, 0, 0, 0, 4).unwrap(), 7); + assert_eq!(adaptive_step(&map, 7, 0, 0, 1, 4).unwrap(), 7); + assert_eq!(adaptive_step(&map, 7, 0, 1, 0, 16).unwrap(), 36); + let low = QpMap { width: 1, height: 2, data: vec![131; 2] }; + assert_eq!(adaptive_step(&low, 4, 0, 0, 1, 1).unwrap(), 1); + assert_eq!(adaptive_step(&low, 4, 0, 0, 0, 1).unwrap(), 1); + assert_eq!(adaptive_step(&low, 0, 0, 0, 0, 0).unwrap(), 1); + assert!(matches!(qp_map(&[0, 0, 1, 4, 0x94], 1, 4), Err(RawError::Unsupported(_)))); + } + + #[test] + fn qp_limit_is_accepted_and_one_above_is_not() { + // The first symbol is a 32-bit literal: signed(2·QP) = QP from a prediction of 0. QP 172 and 175 + // occur in EOS R6 Mark II maps (3 of 40 sample files failed with them before). + let map = |qp: u32| { + let symbol = 2 * qp; + let mut src = vec![0, 0, (symbol >> 8) as u8, symbol as u8]; + src.push(0x94); + qp_map(&src, 1, 4) + }; + assert_eq!(map(168).unwrap().data, [168; 4]); + assert_eq!(map(MAX_QP as u32).unwrap().data, [MAX_QP; 4]); + assert!(matches!(map(MAX_QP as u32 + 1), Err(RawError::Unsupported(_)))); + } + + #[test] + fn long_qp_prefixes_and_escapes_as_on_the_eos_r5_mark_ii() { + // EOS R5 Mark II maps (QP 150–162) jump by up to 12 where k is 0: an 18-zero prefix that an + // escape after 16 zeros misread, then a real escape (23 zeros, 1, symbol 23 = −12). + let src = [0x00, 0x00, 0x01, 0x2c, 0x90, 0x00, 0x01, 0x90, 0x00, 0x00, 0x08, 0xb8]; + assert_eq!(qp_map(&src, 7, 1).unwrap().data, [150, 150, 150, 159, 159, 159, 147]); + for end in 0..src.len() { + assert!(qp_map(&src[..end], 7, 1).is_err()); + } + } + + #[test] + fn odd_height_qp_map_uses_its_last_row_alone() { + // EOS R7 C-RAW: 4732 sensor rows give a 1183-row QP map, so the last + // row of the coarser high-frequency bands has no partner row. + let map = QpMap { width: 1, height: 3, data: vec![150, 153, 151] }; + assert_eq!(adaptive_step(&map, 4, 0, 0, 0, 4).unwrap(), 7); + // Row 2 alone is QP 151; averaging with a missing row would leave the table. + assert_eq!(adaptive_step(&map, 4, 0, 1, 0, 4).unwrap(), adaptive_step(&map, 7, 0, 2, 0, 4).unwrap()); + assert!(adaptive_step(&map, 4, 0, 2, 0, 4).is_err()); + } + + #[test] + fn adaptive_v200_craw_reconstructs_sensor_samples() { + let mut config = coding(); + config.version = 0x200; + config.width = 16; + config.height = 16; + config.tile_width = 16; + config.tile_height = 16; + config.header_size = 872; + config.levels = 3; + let mut sample = Vec::new(); + sample.extend(0xff11u16.to_be_bytes()); + sample.extend(16u16.to_be_bytes()); + sample.extend(60u32.to_be_bytes()); + sample.extend(0x4000u32.to_be_bytes()); + sample.extend(5u32.to_be_bytes()); + sample.extend(3u16.to_be_bytes()); + sample.extend(0u16.to_be_bytes()); + let gains = [0, 0, 0, 0, 4, 4, 8, 8, 8, 16]; + for plane in 0..4 { + marker(&mut sample, 0xff12, 13, (plane << 28) | 0x0800_0000); + for (band, &gain) in gains.iter().enumerate() { + sample.extend(0xff13u16.to_be_bytes()); + sample.extend(16u16.to_be_bytes()); + sample.extend(if band >= 7 { 2u32 } else { 1 }.to_be_bytes()); + sample.extend(((band as u32) << 28 | gain).to_be_bytes()); + sample.extend(u32::from(band < 4).to_be_bytes()); + sample.extend([0u8; 4]); + } + } + sample.extend([0u8; 4]); + sample.extend([0, 0, 1, 0x2c, 0x94, 0, 0, 0]); + for _ in 0..4 { + sample.extend([0x80; 4]); + sample.extend([0xf0; 3]); + for _ in 0..3 { + sample.extend([0xff, 0xf8]); + } + } + validate(&sample, &config).unwrap(); + assert_eq!(decode(&sample, &config).unwrap(), vec![8192; 256]); + for end in 0..sample.len() { + assert!(decode(&sample[..end], &config).is_err()); + } + } + + #[test] + fn optional_cc0_samples_match_external_oracle() { + let Some(directory) = std::env::var_os("LIGHTCRAFT_CR3_CORPUS") else { return }; + for (name, expected) in [ + ("cr3-canon-r100-raw.cr3", 0xcbe5299ab9c52630u64), + ("cr3-canon-m50-raw.cr3", 0x62261f0ba81cfcd2u64), + ("cr3-canon-r100-craw.cr3", 0x341c706b37c38bbfu64), + ("cr3-canon-m50-craw.cr3", 0x9aacbfe66f505e1bu64), + ("cr3-canon-r8-raw.cr3", 0x63c1c6d8d1eb2312u64), + ("cr3-canon-r8-craw.cr3", 0xf836a663a795a375u64), + ("cr3-canon-rp-raw.cr3", 0x5cb0f46404b8b663u64), + ("cr3-canon-rp-craw.cr3", 0xf48df06ca1d67382u64), + ("cr3-canon-rp-raw-crop.cr3", 0x46808a9de3b3dcc7u64), + ("cr3-canon-rp-craw-crop.cr3", 0x39a60c4efed4e1f4u64), + ] { + let path = std::path::Path::new(&directory).join(name); + let bytes = std::fs::read(path).unwrap(); + let container = craft_metadata::cr3::parse_cr3(&bytes).unwrap(); + let mut candidates = container + .tracks + .iter() + .filter_map(|track| { + let craft_metadata::cr3::Cr3TrackKind::Raw { cmp1: Some((at, length)), .. } = track.kind else { return None }; + let coding = Cr3Compression::parse(bytes.get(at..at + length)?)?; + Some((u64::from(coding.width) * u64::from(coding.height), coding, track.data?)) + }) + .collect::>(); + candidates.sort_by_key(|entry| entry.0); + let (_, coding, (at, length)) = candidates.last().unwrap(); + let sensor = decode(&bytes[*at..at + length], coding).unwrap(); + let hash = sensor + .iter() + .flat_map(|value| value.to_le_bytes()) + .fold(0xcbf29ce484222325u64, |hash, byte| (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3)); + assert_eq!(hash, expected, "{name}"); + } + } +} diff --git a/crates/raw/src/crx_wavelet.rs b/crates/raw/src/crx_wavelet.rs new file mode 100644 index 000000000..ac5e51d1d --- /dev/null +++ b/crates/raw/src/crx_wavelet.rs @@ -0,0 +1,338 @@ +//! Reversible integer Le Gall 5/3 synthesis for Canon CRX subbands. +//! +//! Independent implementation of the lifting equations in ITU-T T.800, Annex F; +//! the equations are also printed in US20030031370A1, paragraph 0047. +//! Each tile level has even local origin. Internal boundaries retain the +//! neighbouring low/high coefficients; only outer image edges are symmetric. + +use crate::{RawError, Result}; + +#[derive(Clone, Debug)] +pub(crate) struct Band { + pub width: usize, + pub height: usize, + pub data: Vec, +} + +fn corrupt() -> RawError { + RawError::Corrupt("invalid CRX wavelet subband geometry".into()) +} + +fn checked_sample(v: i64) -> Result { + i32::try_from(v).map_err(|_| RawError::Corrupt("CRX wavelet coefficient overflow".into())) +} + +#[derive(Clone, Copy, Debug, Default)] +pub(crate) struct TileEdges { + pub left: bool, + pub right: bool, + pub top: bool, + pub bottom: bool, +} + +fn sizes(length: usize, before: bool, after: bool) -> (usize, usize) { + (length.div_ceil(2) + usize::from(after && length.is_multiple_of(2)), length / 2 + usize::from(before) + usize::from(after)) +} + +fn inverse_line(low: &[i32], high: &[i32], out: &mut [i32], before: bool, after: bool) -> Result<()> { + let (low_count, high_count) = sizes(out.len(), before, after); + if out.is_empty() || low.len() != low_count || high.len() != high_count { + return Err(corrupt()); + } + if high.is_empty() { + let value = low.first().copied().ok_or_else(corrupt)?; + *out.first_mut().ok_or_else(corrupt)? = value; + return Ok(()); + } + // The high-pass storage starts at -1 when there is a preceding tile. + // Only absent external neighbours are reflected at the image boundary. + let high_at = |index: usize| -> Result { high.get(index.min(high.len().saturating_sub(1))).copied().ok_or_else(corrupt) }; + let even = |i: usize| -> Result { + let left = high_at((i + usize::from(before)).saturating_sub(1))?; + let right = high_at(i + usize::from(before))?; + let update = (i64::from(left) + i64::from(right) + 2).div_euclid(4); + checked_sample(i64::from(*low.get(i).ok_or_else(corrupt)?) - update) + }; + for i in 0..out.len().div_ceil(2) { + *out.get_mut(i * 2).ok_or_else(corrupt)? = even(i)?; + } + for i in 0..out.len() / 2 { + let left = out.get(i * 2).copied().ok_or_else(corrupt)?; + let right = match out.get(i * 2 + 2).copied() { + Some(value) => value, + None if after => even(i + 1)?, + None => left, + }; + let prediction = (i64::from(left) + i64::from(right)).div_euclid(2); + let value = high_at(i + usize::from(before))?; + *out.get_mut(i * 2 + 1).ok_or_else(corrupt)? = checked_sample(i64::from(value) + prediction)?; + } + Ok(()) +} + +fn valid_band(band: &Band, width: usize, height: usize) -> bool { + band.width == width && band.height == height && width.checked_mul(height) == Some(band.data.len()) +} + +/// A zeroed buffer, or a limit error instead of an abort when the allocation fails. +fn zeroed(count: usize) -> Result> { + let mut v = Vec::new(); + v.try_reserve_exact(count).map_err(|_| RawError::Limit("CRX wavelet allocation"))?; + v.resize(count, 0); + Ok(v) +} + +/// One inverse level. `HL` is horizontally high-pass, `LH` vertically high-pass. +pub(crate) fn inverse_level(ll: &Band, hl: &Band, lh: &Band, hh: &Band, width: usize, height: usize, edges: TileEdges) -> Result { + let count = width.checked_mul(height).filter(|n| *n > 0).ok_or_else(corrupt)?; + if count > crate::MAX_SAMPLES { + return Err(RawError::Limit("CRX wavelet image size")); + } + let (lw, hw) = sizes(width, edges.left, edges.right); + let (lhgt, hhgt) = sizes(height, edges.top, edges.bottom); + if !valid_band(ll, lw, lhgt) || !valid_band(hl, hw, lhgt) || !valid_band(lh, lw, hhgt) || !valid_band(hh, hw, hhgt) { + return Err(corrupt()); + } + let intermediate_count = width.checked_mul(lhgt + hhgt).filter(|n| *n <= crate::MAX_SAMPLES).ok_or(RawError::Limit("CRX wavelet image size"))?; + let mut temporary = zeroed(intermediate_count)?; + // CRX analysis is vertical then horizontal. Integer rounding means that + // synthesis must reverse that order, including for quantized subbands. + for y in 0..lhgt + hhgt { + let (lo, hi, by) = if y < lhgt { (ll, hl, y) } else { (lh, hh, y - lhgt) }; + inverse_line( + lo.data.get(by * lw..(by + 1) * lw).ok_or_else(corrupt)?, + hi.data.get(by * hw..(by + 1) * hw).ok_or_else(corrupt)?, + temporary.get_mut(y * width..(y + 1) * width).ok_or_else(corrupt)?, + edges.left, + edges.right, + )?; + } + let mut data = zeroed(count)?; + let mut low = zeroed(lhgt)?; + let mut high = zeroed(hhgt)?; + let mut column = zeroed(height)?; + for x in 0..width { + for (y, value) in low.iter_mut().enumerate() { + *value = temporary.get(y * width + x).copied().ok_or_else(corrupt)?; + } + for (y, value) in high.iter_mut().enumerate() { + *value = temporary.get((y + lhgt) * width + x).copied().ok_or_else(corrupt)?; + } + inverse_line(&low, &high, &mut column, edges.top, edges.bottom)?; + for (y, &value) in column.iter().enumerate() { + *data.get_mut(y * width + x).ok_or_else(corrupt)? = value; + } + } + Ok(Band { width, height, data }) +} + +/// Subbands in CRX order: LLn, HLn, LHn, HHn, ... HL1, LH1, HH1. +pub(crate) fn reconstruct(bands: &[Band], width: usize, height: usize, levels: u8, edges: TileEdges) -> Result> { + if levels > 3 || bands.len() != 1 + usize::from(levels) * 3 || width == 0 || height == 0 { + return Err(corrupt()); + } + let count = width.checked_mul(height).ok_or_else(corrupt)?; + if count > crate::MAX_SAMPLES { + return Err(RawError::Limit("CRX wavelet image size")); + } + let mut dimensions = vec![(width, height)]; + for _ in 0..levels { + let &(w, h) = dimensions.last().ok_or_else(corrupt)?; + dimensions.push((sizes(w, edges.left, edges.right).0, sizes(h, edges.top, edges.bottom).0)); + } + let &(low_width, low_height) = dimensions.last().ok_or_else(corrupt)?; + if !bands.first().is_some_and(|b| valid_band(b, low_width, low_height)) { + return Err(corrupt()); + } + let mut ll = bands.first().cloned().ok_or_else(corrupt)?; + for step in 0..usize::from(levels) { + let &(w, h) = dimensions.get(usize::from(levels) - step - 1).ok_or_else(corrupt)?; + let index = 1 + step * 3; + ll = inverse_level( + &ll, + bands.get(index).ok_or_else(corrupt)?, + bands.get(index + 1).ok_or_else(corrupt)?, + bands.get(index + 2).ok_or_else(corrupt)?, + w, + h, + edges, + )?; + } + if !valid_band(&ll, width, height) { + return Err(corrupt()); + } + Ok(ll.data) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn integer_lifting_preserves_signed_rounding_and_symmetric_ends() { + let mut out = [0; 9]; + inverse_line(&[6, 11, 8, 6, 3], &[2, 2, -1, -2], &mut out, false, false).unwrap(); + assert_eq!(out, [5, 9, 10, 11, 8, 6, 7, 3, 4]); + let mut out = [0; 2]; + inverse_line(&[25], &[20], &mut out, false, false).unwrap(); + assert_eq!(out, [15, 35]); + let mut out = [0]; + inverse_line(&[-27], &[], &mut out, false, false).unwrap(); + assert_eq!(out, [-27]); + } + + #[test] + fn two_dimensional_subband_order() { + let band = |value| Band { width: 1, height: 1, data: vec![value] }; + let out = reconstruct(&[band(25), band(10), band(20), band(0)], 2, 2, 1, TileEdges::default()).unwrap(); + assert_eq!(out, [10, 20, 30, 40]); + } + + #[test] + fn invalid_geometry_and_overflow_are_errors() { + let mut out = [0; 2]; + assert!(inverse_line(&[], &[0], &mut out, false, false).is_err()); + assert!(inverse_line(&[i32::MAX], &[i32::MIN], &mut out, false, false).is_err()); + let b = Band { width: 1, height: 1, data: vec![0] }; + assert!(inverse_level(&b, &b, &b, &b, usize::MAX, 2, TileEdges::default()).is_err()); + assert!(reconstruct(&[b], 2, 2, 0, TileEdges::default()).is_err()); + } + + // Independent forward lifting in tests checks all odd/even boundary + // combinations; hand-computed vectors above also establish sign/order. + fn forward_line(input: &[i32]) -> (Vec, Vec) { + let high: Vec<_> = (0..input.len() / 2) + .map(|i| input[i * 2 + 1] - (input[i * 2] + input.get(i * 2 + 2).copied().unwrap_or(input[i * 2])).div_euclid(2)) + .collect(); + let low = input + .iter() + .step_by(2) + .enumerate() + .map(|(i, &v)| { + if high.is_empty() { + v + } else { + v + (high[i.saturating_sub(1)] + high.get(i).copied().unwrap_or(high[high.len() - 1]) + 2).div_euclid(4) + } + }) + .collect(); + (low, high) + } + + #[test] + fn every_boundary_parity_round_trips_signed_coefficients() { + for width in 1..=17usize { + for height in 1..=17usize { + let input: Vec = (0..width * height).map(|i| ((i * 193 + i * i * 17) % 1001) as i32 - 500).collect(); + let (lw, hw, lhgt, hhgt) = (width.div_ceil(2), width / 2, height.div_ceil(2), height / 2); + let mut vertical = vec![0; width * height]; + for x in 0..width { + let column: Vec<_> = (0..height).map(|y| input[y * width + x]).collect(); + let (lo, hi) = forward_line(&column); + for (y, value) in lo.into_iter().chain(hi).enumerate() { + vertical[y * width + x] = value; + } + } + let mut bands = [ + Band { width: lw, height: lhgt, data: vec![0; lw * lhgt] }, + Band { width: hw, height: lhgt, data: vec![0; hw * lhgt] }, + Band { width: lw, height: hhgt, data: vec![0; lw * hhgt] }, + Band { width: hw, height: hhgt, data: vec![0; hw * hhgt] }, + ]; + for y in 0..height { + let (lo, hi) = forward_line(&vertical[y * width..(y + 1) * width]); + let (b, by) = if y < lhgt { (0, y) } else { (2, y - lhgt) }; + bands[b].data[by * lw..(by + 1) * lw].copy_from_slice(&lo); + bands[b + 1].data[by * hw..(by + 1) * hw].copy_from_slice(&hi); + } + assert_eq!(reconstruct(&bands, width, height, 1, TileEdges::default()).unwrap(), input, "{width}x{height}"); + } + } + } + // Independently generated from ITU-T T.800 integer 5/3 forward equations. + // Original40x9 samples: ((x*23+y*37+x*y*7)%101)-50. + // Tile width20 creates the same level3phase4 mismatch as R1001572. + fn left_halo_fixture() -> Vec { + vec![ + Band { width: 4, height: 2, data: vec![1, 1, -4, 1, 20, -2, -1, 2] }, + Band { width: 4, height: 2, data: vec![35, 21, -2, 19, -24, -10, -7, -2] }, + Band { width: 4, height: 1, data: vec![31, -16, -19, -7] }, + Band { width: 4, height: 1, data: vec![22, 9, 20, 18] }, + Band { width: 6, height: 3, data: vec![24, -34, -55, -21, 2, 7, 25, 5, -3, 14, 31, 14, 24, 37, 55, 19, -42, -41] }, + Band { width: 6, height: 2, data: vec![9, -21, 20, -4, 31, -6, -32, -3, 7, 31, 14, -11] }, + Band { width: 6, height: 2, data: vec![3, 62, 40, 50, 50, 51, -3, -62, -38, -45, 50, 35] }, + Band { + width: 11, + height: 5, + data: vec![ + 13, -38, -12, 13, -50, -13, 13, 39, 25, -38, 38, -44, 6, 0, -44, -50, 57, 0, -6, 38, 19, -13, 26, 26, 39, -37, -62, -37, -37, + -25, -25, -44, -44, 6, -44, -12, 57, 39, -44, 13, 32, -18, 0, 57, -38, 13, 13, -13, 51, 13, -12, -38, 13, 13, -13, + ], + }, + Band { + width: 11, + height: 4, + data: vec![ + 13, 39, 0, -25, 6, -6, 26, 0, -43, 20, -25, -50, 26, 7, -19, 0, 13, 32, -6, -25, 38, 32, 1, 26, -44, 19, 0, -12, -6, 45, -25, -6, + 32, -37, 39, -12, 26, -6, 6, -25, 0, 39, -12, 26, + ], + }, + Band { + width: 11, + height: 4, + data: vec![ + 26, -75, 76, 25, 0, -25, -75, 76, -50, -75, -25, 0, -101, -75, 0, 0, 51, 76, 101, 0, -51, -25, -101, 0, 25, 51, -50, 0, -25, 0, + 101, 76, 51, -75, 26, -75, -25, 0, 25, 76, -75, 26, -75, -25, + ], + }, + ] + } + fn right_halo_fixture() -> Vec { + vec![ + Band { width: 3, height: 2, data: vec![-12, 8, -5, 0, 4, -2] }, + Band { width: 3, height: 2, data: vec![-10, -5, -15, 8, 6, 5] }, + Band { width: 3, height: 1, data: vec![-10, -14, -10] }, + Band { width: 3, height: 1, data: vec![-17, -14, -25] }, + Band { width: 6, height: 3, data: vec![2, 7, -4, 7, 26, 38, 31, 14, 6, -12, -2, -22, -42, -41, -47, -7, 10, 44] }, + Band { width: 5, height: 2, data: vec![-6, 15, 31, -7, 12, -11, 6, 9, 5, -5] }, + Band { width: 6, height: 2, data: vec![50, 51, -46, -36, -58, -61, 50, 35, 69, 18, -36, -21] }, + Band { + width: 11, + height: 5, + data: vec![ + -38, 38, -13, 13, -38, 39, -25, -38, 63, 13, 49, 19, -13, -57, 70, 25, -6, -12, 32, 0, -44, -51, -44, -44, -50, -37, -37, -25, 1, + -31, -44, -50, -50, 0, 57, -37, -44, 13, 32, -12, 6, 57, -37, -36, 13, -13, 77, 13, -12, -38, -25, -25, -13, 77, -22, + ], + }, + Band { + width: 10, + height: 4, + data: vec![ + -25, 39, 0, -37, 25, -18, 45, -12, -37, 19, 32, -6, 13, 32, -6, 1, -25, 45, -6, 0, 32, 13, 0, -6, 45, -25, -6, 32, 13, 0, 26, 7, + 19, -25, 0, 45, -50, 32, 7, 13, + ], + }, + Band { + width: 11, + height: 4, + data: vec![ + -75, -25, -25, 25, 25, 76, 51, -75, 25, 25, 51, -51, -25, 0, 51, 76, 101, 101, 0, -25, 0, 0, 76, 51, 0, 0, -25, 0, 101, 76, 51, + 0, 0, -75, -25, 51, 25, 76, -75, 51, -50, -25, 51, 0, + ], + }, + ] + } + + #[test] + fn three_levels_keep_internal_tile_halos_and_local_phase() { + for (tile, bands, edges) in [ + (0, left_halo_fixture(), TileEdges { right: true, ..Default::default() }), + (1, right_halo_fixture(), TileEdges { left: true, ..Default::default() }), + ] { + let expected: Vec = + (0..9).flat_map(|y| (tile * 20..tile * 20 + 20).map(move |x| ((x * 23 + y * 37 + x * y * 7) % 101) - 50)).collect(); + assert_eq!(reconstruct(&bands, 20, 9, 3, edges).unwrap(), expected); + assert!(reconstruct(&bands, 20, 9, 3, TileEdges::default()).is_err()); + } + } +} diff --git a/crates/raw/src/lib.rs b/crates/raw/src/lib.rs index 444241c59..b3f318241 100644 --- a/crates/raw/src/lib.rs +++ b/crates/raw/src/lib.rs @@ -35,6 +35,7 @@ //! | `raf` | Fujifilm RAF: uncompressed Bayer and X-Trans, Fujifilm's lossless and lossy compression, lens corrections | //! | `orf` | Olympus / OM System ORF: uncompressed 16-bit, 12-bit packed and two-field layouts, 16-byte blocks; compressed refused | //! | `arw` | Sony ARW (and SR2 / SRF containers): uncompressed, ARW2, lossless JPEG tiles, ARW6, the encrypted SR2SubIFD, lens corrections | +//! | `cr3` | Canon CR3: CRX lossless Bayer and C-RAW (lossy), through craft-metadata's CR3 container | //! | `pef` | Pentax PEF: uncompressed (packed or 16-bit words) and Pentax's Huffman coding (compression 65535) | //! | `srw` | Samsung SRW: the uncompressed variants (compression 32769 words, 32770 at 12 bits packed) | //! | `hasselblad` | Hasselblad 3FR and FFF (and other CFA TIFFs in the DNG layout without a DNG version): uncompressed and Hasselblad's lossless JPEG; implies `dng` | @@ -57,8 +58,14 @@ #[cfg(feature = "cr2")] mod cr2; +#[cfg(feature = "cr3")] +mod cr3; #[cfg(feature = "crw")] mod crw; +#[cfg(feature = "cr3")] +mod crx; +#[cfg(feature = "cr3")] +mod crx_wavelet; #[cfg(feature = "dng")] mod dng; #[cfg(feature = "dng")] @@ -119,6 +126,7 @@ mod rafc; mod rw2; #[cfg(feature = "dng")] pub mod semantic; +pub mod sparse; #[cfg(feature = "arw")] mod sr2; // (the TIFF pixel plumbing every TIFF-based maker shares; DNG is its only user so far) @@ -377,6 +385,7 @@ impl RawFormat { || (self == RawFormat::Raf && cfg!(feature = "raf")) || (self == RawFormat::Orf && cfg!(feature = "orf")) || (self == RawFormat::Arw && cfg!(feature = "arw")) + || (self == RawFormat::Cr3 && cfg!(feature = "cr3")) || (self == RawFormat::Pef && cfg!(feature = "pef")) || (self == RawFormat::Srw && cfg!(feature = "srw")) } @@ -671,6 +680,8 @@ fn decode_in(bytes: &[u8], mode: Mode, limits: &Limits) -> Result { RawFormat::Orf => orf::decode(bytes, mode, limits), #[cfg(feature = "arw")] RawFormat::Arw => arw::decode(bytes, mode, limits), + #[cfg(feature = "cr3")] + RawFormat::Cr3 => cr3::decode(bytes, mode, limits), #[cfg(feature = "pef")] RawFormat::Pef => pef::decode(bytes, mode, limits), #[cfg(feature = "srw")] diff --git a/crates/raw/src/preview.rs b/crates/raw/src/preview.rs index 094308fd4..6917d2588 100644 --- a/crates/raw/src/preview.rs +++ b/crates/raw/src/preview.rs @@ -22,7 +22,7 @@ pub enum PreviewColorSpace { /// the Exif and DCF specifications), not for a DNG. This is a fallback only: a JPEG's own ICC/EXIF colour /// declaration takes precedence. pub fn embedded_preview_color_space(bytes: &[u8]) -> Option { - let tiff = Tiff::parse(bytes).ok()?; + let Ok(tiff) = Tiff::parse(bytes) else { return cr3_color_space(bytes) }; let make = tiff.ifds.first()?.string(t::MAKE).unwrap_or_default(); let exif = tiff.exif()?; if make.to_ascii_uppercase().starts_with("NIKON") @@ -48,6 +48,24 @@ pub fn embedded_preview_color_space(bytes: &[u8]) -> Option { } } +/// A CR3 keeps its EXIF and maker note in `CMT2`/`CMT3` TIFF blocks and its JPEGs carry no EXIF; a body shot in +/// Adobe RGB writes ColorSpace Uncalibrated (0xFFFF) with no Interoperability IFD (storytold/lightcraft#2654), so the Canon +/// maker note says which, else an EXIF ColorSpace of sRGB is taken. +#[cfg(feature = "cr3")] +fn cr3_color_space(bytes: &[u8]) -> Option { + let cr3 = craft_metadata::cr3::parse_cr3(bytes)?; + let first = |i: usize| cr3.cmt[i].and_then(|b| Tiff::parse(b).ok()).and_then(|t| t.ifds.into_iter().next()); + if let Some(space) = first(2).and_then(|mn| canon_color_space(&mn)) { + return Some(space); + } + (first(1)?.u16(0xA001)? == 1).then_some(PreviewColorSpace::Srgb) +} + +#[cfg(not(feature = "cr3"))] +fn cr3_color_space(_: &[u8]) -> Option { + None +} + fn canon_color_space(note: &Ifd) -> Option { match note.u16(0x00b4)? { 1 => Some(PreviewColorSpace::Srgb), @@ -260,6 +278,24 @@ fn preview_covering(bytes: &[u8], min_edge: Option) -> Option> { return raf_jpeg(bytes).filter(|j| is_dct_jpeg(j)).map(|j| trim_eoi(j).to_vec()); } let format = crate::probe(bytes); + #[cfg(feature = "cr3")] + if format == Some(crate::RawFormat::Cr3) { + // a Canon CR3 keeps its full-size JPEG beside a 1620 px `PRVW` (and a 160 px `THMB`): a + // thumbnail decodes the small copy, not 45 MP of JPEG (about 150 ms a photo, the grid's + // "popcorn" on a folder of R5 / R5 C raws) + // (without a JPEG track, the largest box is the picture's shape: the boxes alone may hold several) + if let Some(min) = min_edge { + let mut found = cr3_preview_box_list(bytes); + let full = cr3_full_jpeg(bytes); + found.extend(full); + if let Some(largest) = full.or_else(|| largest_by_pixels(&found)) + && let Some(j) = smallest_covering(&found, largest, min) + { + return Some(trim_eoi(j).to_vec()); + } + } + return cr3_preview(bytes).map(|j| trim_eoi(j).to_vec()); + } if matches!(format, Some(crate::RawFormat::Crw | crate::RawFormat::Mrw | crate::RawFormat::X3f)) { return scan_for_jpeg(bytes); } @@ -521,6 +557,135 @@ fn scan_with_budget(data: &[u8], budget: usize) -> (Option>, u64) { (jpeg, walked) } +/// Canon CR3: the full-size JPEG track (see `craft_metadata::cr3`), else the `PRVW` / `THMB` boxes. +#[cfg(feature = "cr3")] +fn cr3_preview(bytes: &[u8]) -> Option<&[u8]> { + cr3_full_jpeg(bytes).or_else(|| cr3_preview_boxes(bytes)) +} + +/// A CR3's full-size JPEG track, when it holds a DCT JPEG. +#[cfg(feature = "cr3")] +fn cr3_full_jpeg(bytes: &[u8]) -> Option<&[u8]> { + let full = craft_metadata::cr3::parse_cr3(bytes) + .and_then(|c| c.tracks.iter().find(|t| t.kind == craft_metadata::cr3::Cr3TrackKind::Jpeg).and_then(|t| t.data)); + full.and_then(|(at, len)| bytes.get(at..at.checked_add(len)?)).filter(|j| is_dct_jpeg(j)) +} + +/// Marker-walk budget of the CR3 fallback: candidates can overlap (each claiming the rest of the file) and +/// [`is_dct_jpeg`] walks each marker by marker, so walking every candidate would be quadratic in file size. +/// The `THMB`/`PRVW` previews this finds are far below the cap together; once it is spent the scan stops. +#[cfg(feature = "cr3")] +const CR3_MAX_WALK: usize = 8 << 20; + +/// Canon CR3 (ISO base media file): the `PRVW` box (Laurent Clévy's CR3 notes; layout confirmed on a CC0 +/// sample) is `u32 size, "PRVW", u32 0, u16 ?, u16 width, u16 height, u16 ?, u32 jpeg length, JPEG`; the smaller +/// `THMB` box has the same shape. Returns the larger valid one. +#[cfg(feature = "cr3")] +fn cr3_preview_boxes(bytes: &[u8]) -> Option<&[u8]> { + largest_by_pixels(&cr3_preview_box_list(bytes)) +} + +/// Of `found`, the JPEG with the longest edge in pixels (a smaller picture can take more bytes); the +/// first of equals. CodeRabbit on #3077. +#[cfg(feature = "cr3")] +fn largest_by_pixels<'a>(found: &[&'a [u8]]) -> Option<&'a [u8]> { + let long = |j: &[u8]| jpeg_frame(j).map_or(0, |(w, h, _)| w.max(h)); + found.iter().copied().rev().max_by_key(|j| long(j)) +} + +/// The valid JPEGs of a CR3's `PRVW` and `THMB` boxes, every one: a file may carry more than one +/// `PRVW`, and the smallest that covers a size is picked from them all. +#[cfg(feature = "cr3")] +fn cr3_preview_box_list(bytes: &[u8]) -> Vec<&[u8]> { + let mut out: Vec<&[u8]> = Vec::new(); + let mut walked = 0usize; + // the boxes sit outside the media data: only the rest of the file is searched (the media data + // is most of the file, and a thumbnail is asked for every photo in view) + let file = bytes; + // (every top-level `mdat` is left out, not only the first: a PRVW-shaped JPEG in media data is not a + // preview; CodeRabbit on #3583) + let media = cr3_media_data(file); + let whole_file = media.is_empty(); + let mut regions: Vec<&[u8]> = Vec::new(); + let mut from = 0usize; + for (start, end) in media { + if start >= from { + regions.extend(file.get(from..start)); + } + from = from.max(end); + } + regions.extend(file.get(from..)); + for tag in [b"PRVW", b"THMB"] { + // the whole file searched (its media data not found): the first valid box of a kind only, as + // the declared one comes before the media data and a later look-alike must not win + let mut taken = false; + for &bytes in ®ions { + if whole_file && taken { + break; + } + let mut from = 0; + while let Some(i) = bytes.get(from..).and_then(|s| s.windows(4).position(|w| w == tag)).map(|p| p + from) { + from = i + 4; + let Some(start) = i.checked_sub(4) else { continue }; + // (the sizes are the file's: checked, a 32-bit build must not wrap them) + let be32 = |at: usize| bytes.get(at..at.checked_add(4)?).map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]]) as usize); + let (Some(size), Some(len)) = (be32(start), start.checked_add(20).and_then(be32)) else { continue }; + let (Some(j), Some(box_end)) = (start.checked_add(24), start.checked_add(size.max(24))) else { continue }; + let Some(end) = j.checked_add(len).filter(|e| len >= 4 && *e <= box_end && *e <= bytes.len()) else { continue }; + let Some(jpeg) = bytes.get(j..end) else { continue }; + // One that doesn't fit the budget left is skipped; a smaller one later may still fit. + if walked.saturating_add(jpeg.len()) > CR3_MAX_WALK { + continue; + } + walked = walked.saturating_add(jpeg.len()); + if is_dct_jpeg(jpeg) { + out.push(jpeg); + taken = true; + if whole_file { + break; + } + // (past it: a tag inside its JPEG is not another box) + from = end; + } + } + } + } + out +} + +/// Where a CR3's top-level `mdat` boxes lie (`start..end` each, `end` clamped to the file, in file +/// order): the media data, most of the file. Empty when there is none, or the boxes stop making sense +/// before the first. The metadata and the small previews are in the other top-level boxes. +#[cfg(feature = "cr3")] +fn cr3_media_data(bytes: &[u8]) -> Vec<(usize, usize)> { + let mut out = Vec::new(); + let _ = cr3_media_walk(bytes, &mut out); + out +} + +/// [`cr3_media_data`]'s walk of the top-level boxes; `None` where they stop making sense. +#[cfg(feature = "cr3")] +fn cr3_media_walk(bytes: &[u8], out: &mut Vec<(usize, usize)>) -> Option<()> { + let be32 = |at: usize| bytes.get(at..at.checked_add(4)?).map(|s| u64::from(u32::from_be_bytes([s[0], s[1], s[2], s[3]]))); + let be64 = |at: usize| bytes.get(at..at.checked_add(8)?).map(|s| u64::from_be_bytes([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]])); + let mut at = 0usize; + for _ in 0..64 { + let size = be32(at)?; + let len = match size { + // (a box running to the end of the file) + 0 => u64::try_from(bytes.len().saturating_sub(at)).unwrap_or(u64::MAX), + 1 => be64(at.checked_add(8)?)?, + n => n, + }; + let next = usize::try_from(len).ok().filter(|l| *l >= 8).and_then(|l| at.checked_add(l)); + if bytes.get(at.checked_add(4)?..at.checked_add(8)?) == Some(b"mdat".as_slice()) { + out.push((at, next.unwrap_or(bytes.len()).min(bytes.len()))); + } + at = next.filter(|n| *n < bytes.len())?; + } + None +} + fn trim_eoi(s: &[u8]) -> &[u8] { let end = s.windows(2).rposition(|w| w == [0xff, 0xd9]).map(|p| p + 2).unwrap_or(s.len()); &s[..end] diff --git a/crates/raw/src/preview/tests.rs b/crates/raw/src/preview/tests.rs index a55bcfd76..19018d9b1 100644 --- a/crates/raw/src/preview/tests.rs +++ b/crates/raw/src/preview/tests.rs @@ -504,3 +504,361 @@ fn bounded_preview_declines_what_the_header_cannot_answer() { tiff.extend_from_slice(&[0u8; 64]); assert_eq!(bounded_preview(&tiff), None); } + +#[cfg(feature = "cr3")] +/// CR3 candidates claim overlapping extents: each block's first bytes double as a 40-byte jump marker so +/// a walk jogs past the next header and reaches the end of the file, making every one of the 9000 +/// candidates walk its whole suffix before the marker-walk budget — quadratic without it. Nothing here +/// is a JPEG (no frame header), so the answer is `None` either way. +#[test] +fn cr3_preview_boxes_stop_walking_when_the_budget_is_spent() { + // one 256-byte block per candidate, all marker-aligned so walks chain through later blocks: + // 0..4, size (itself a jump marker of 40 bytes); 4..8, "PRVW"; 20..24, the claimed length + // (to the file end); 24..28, an SOI plus a marker jumping to the fill at 40; 64..256, a fill of + // 4-byte markers (`ff 01 00 02`). + let blocks = 9000usize; + let total = blocks * 256; + let mut f = Vec::with_capacity(total); + for k in 0..blocks { + let base = k * 256; + f.extend_from_slice(&[0xff, 0x01, 0x00, 0x26, b'P', b'R', b'V', b'W', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + f.extend_from_slice(&u32::try_from(total - base - 24).unwrap().to_be_bytes()); + f.extend_from_slice(&[0xff, 0xd8, 0x00, 0x24, 0xff, 0x01, 0x00, 0x26, 0, 0, 0, 0, 0, 0, 0, 0]); + f.resize(base + 64, 0); // 40..64 filler: walks jump over it, so its bytes are free + for _ in 0..48 { + f.extend_from_slice(&[0xff, 0x01, 0x00, 0x02]); + } + } + assert_eq!(f.len(), total); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || tx.send(cr3_preview_boxes(&f).map(|s| s.len()))); + assert_eq!(rx.recv_timeout(std::time::Duration::from_secs(2)), Ok(None), "no PRVW candidate is a JPEG"); +} + +#[cfg(feature = "raf")] +/// A range that does not hold a JPEG yields nothing, rather than a truncated image handed on. +#[test] +fn a_range_that_does_not_hold_a_jpeg_yields_nothing() { + let jpeg = fake_jpeg(200); + let range = PreviewRange { offset: 0, window_len: jpeg.len() as u64 }; + assert_eq!(embedded_preview_from_range(&jpeg, range).as_deref(), Some(&jpeg[..])); + assert_eq!(embedded_preview_from_range(&jpeg[..10], range), None, "window shorter than the range claims"); + let not_jpeg = vec![0u8; jpeg.len()]; + assert_eq!(embedded_preview_from_range(¬_jpeg, range), None, "not a JPEG"); + assert_eq!(embedded_preview_from_range(&[], PreviewRange { offset: 0, window_len: 0 }), None, "empty window"); +} + +#[cfg(feature = "cr3")] +/// #2569: `PRVW` box and JPEG lengths near `u32::MAX` (which wrap `usize` on wasm32) find no preview, never panic. +#[test] +fn cr3_preview_box_huge_lengths_are_rejected() { + let j = fake_jpeg(300); + for (size, len) in [(u32::MAX, u32::MAX), (u32::MAX, u32::MAX - 23), (24 + j.len() as u32, u32::MAX - 3)] { + let mut cr3 = b"\x00\x00\x00\x18ftypcrx \x00\x00\x00\x01crx isom".to_vec(); + cr3.extend_from_slice(&size.to_be_bytes()); + cr3.extend_from_slice(b"PRVW\x00\x00\x00\x00\x00\x01\x06T\x048\x00\x01"); + cr3.extend_from_slice(&len.to_be_bytes()); + cr3.extend_from_slice(&j); + assert_eq!(cr3_preview_boxes(&cr3), None, "size {size:#x}, len {len:#x}"); + } +} + +#[cfg(feature = "cr3")] +/// CR3 with a full-size JPEG track: that JPEG wins over the smaller `PRVW` box. +#[test] +fn cr3_prefers_the_full_size_jpeg_track() { + let bx = |kind: &[u8; 4], body: &[u8]| -> Vec { [&((body.len() + 8) as u32).to_be_bytes()[..], kind, body].concat() }; + let full = |kind: &[u8; 4], body: &[u8]| bx(kind, &[&[0u8; 4][..], body].concat()); + let (small, big) = (fake_jpeg(300), fake_jpeg(3000)); + let mut file = bx(b"ftyp", b"crx \0\0\0\x01crx isom"); + let mut prvw = b"\0\0\0\0\0\x01\x06\x54\x04\x38\0\x01".to_vec(); + prvw.extend_from_slice(&(small.len() as u32).to_be_bytes()); + prvw.extend_from_slice(&small); + file.extend(bx(b"PRVW", &prvw)); + // CRAW sample entry: 82 bytes, then a JPEG child box; one sample at `at` + let mut craw = vec![0u8; 82]; + craw.extend(bx(b"JPEG", &[0; 4])); + let at = 2048u64; + let stsd = full(b"stsd", &[&1u32.to_be_bytes()[..], &bx(b"CRAW", &craw)].concat()); + let stsz = full(b"stsz", &[0u32.to_be_bytes(), 1u32.to_be_bytes(), (big.len() as u32).to_be_bytes()].concat()); + let co64 = full(b"co64", &[&1u32.to_be_bytes()[..], &at.to_be_bytes()].concat()); + let trak = bx(b"trak", &bx(b"mdia", &bx(b"minf", &bx(b"stbl", &[stsd, stsz, co64].concat())))); + file.extend(bx(b"moov", &trak)); + file.resize(at as usize, 0); + file.extend_from_slice(&big); + assert_eq!(embedded_preview(&file).unwrap(), big); + // a track pointing past the end falls back to PRVW + file.truncate(at as usize + 100); + assert_eq!(embedded_preview(&file).unwrap(), small); +} + +#[cfg(feature = "cr3")] +/// A CR3 whose `PRVW` box holds `prvw` and whose JPEG track holds `big`. +fn cr3_with(prvw_jpeg: &[u8], big: &[u8]) -> Vec { + let bx = |kind: &[u8; 4], body: &[u8]| -> Vec { [&((body.len() + 8) as u32).to_be_bytes()[..], kind, body].concat() }; + let full = |kind: &[u8; 4], body: &[u8]| bx(kind, &[&[0u8; 4][..], body].concat()); + let mut file = bx(b"ftyp", b"crx \0\0\0\x01crx isom"); + let mut prvw = b"\0\0\0\0\0\x01\x06T\x048\0\x01".to_vec(); + prvw.extend_from_slice(&(prvw_jpeg.len() as u32).to_be_bytes()); + prvw.extend_from_slice(prvw_jpeg); + file.extend(bx(b"PRVW", &prvw)); + let mut craw = vec![0u8; 82]; + craw.extend(bx(b"JPEG", &[0; 4])); + let at = 4096u64; + let stsd = full(b"stsd", &[&1u32.to_be_bytes()[..], &bx(b"CRAW", &craw)].concat()); + let stsz = full(b"stsz", &[0u32.to_be_bytes(), 1u32.to_be_bytes(), (big.len() as u32).to_be_bytes()].concat()); + let co64 = full(b"co64", &[&1u32.to_be_bytes()[..], &at.to_be_bytes()].concat()); + let trak = bx(b"trak", &bx(b"mdia", &bx(b"minf", &bx(b"stbl", &[stsd, stsz, co64].concat())))); + file.extend(bx(b"moov", &trak)); + file.resize(at as usize, 0); + file.extend_from_slice(big); + file +} + +#[cfg(feature = "cr3")] +/// A CR3 thumbnail decodes the 1620 px `PRVW` copy, not the full-size JPEG (PERF-THUMB-FIRST: +/// 150 ms a photo for the full one); a size the copy can't cover, and the largest preview, are +/// still the full JPEG; a `PRVW` of another shape is never taken for the picture. +#[test] +fn cr3_thumbnails_take_the_smallest_copy_that_covers_them() { + let (prvw, big) = (sized_jpeg(1620, 1080, 3, 300), sized_jpeg(6000, 4000, 3, 3000)); + let file = cr3_with(&prvw, &big); + assert_eq!(embedded_preview_covering(&file, 512).unwrap(), prvw); + assert_eq!(embedded_preview_covering(&file, 1620).unwrap(), prvw); + assert_eq!(embedded_preview_covering(&file, 2000).unwrap(), big); + assert_eq!(embedded_preview(&file).unwrap(), big); + let letterboxed = cr3_with(&sized_jpeg(1620, 1215, 3, 300), &big); + assert_eq!(embedded_preview_covering(&letterboxed, 512).unwrap(), big, "another shape is not a copy"); + for n in (0..file.len()).step_by(13) { + let _ = embedded_preview_covering(&file[..n], 512); + } +} + +#[cfg(feature = "cr3")] +/// the largest when none covers it. A CR3's 8192 px track, 1620 px `PRVW` and 160 px `THMB`. +#[test] +fn the_smallest_preview_covering_the_size_is_chosen() { + let bx = |kind: &[u8; 4], body: &[u8]| -> Vec { [&((body.len() + 8) as u32).to_be_bytes()[..], kind, body].concat() }; + let full = |kind: &[u8; 4], body: &[u8]| bx(kind, &[&[0u8; 4][..], body].concat()); + let boxed = |j: &[u8]| { + let mut b = b"\0\0\0\0\0\x01\x06\x54\x04\x38\0\x01".to_vec(); + b.extend_from_slice(&(j.len() as u32).to_be_bytes()); + b.extend_from_slice(j); + b + }; + let (thmb, prvw, big) = (sized_jpeg(160, 107, 1, 50), sized_jpeg(1620, 1080, 1, 300), sized_jpeg(8192, 5464, 1, 3000)); + let mut file = bx(b"ftyp", b"crx \0\0\0\x01crx isom"); + file.extend(bx(b"THMB", &boxed(&thmb))); + file.extend(bx(b"PRVW", &boxed(&prvw))); + let mut craw = vec![0u8; 82]; + craw.extend(bx(b"JPEG", &[0; 4])); + let at = 4096u64; + let stsd = full(b"stsd", &[&1u32.to_be_bytes()[..], &bx(b"CRAW", &craw)].concat()); + let stsz = full(b"stsz", &[0u32.to_be_bytes(), 1u32.to_be_bytes(), (big.len() as u32).to_be_bytes()].concat()); + let co64 = full(b"co64", &[&1u32.to_be_bytes()[..], &at.to_be_bytes()].concat()); + let trak = bx(b"trak", &bx(b"mdia", &bx(b"minf", &bx(b"stbl", &[stsd, stsz, co64].concat())))); + file.extend(bx(b"moov", &trak)); + file.resize(at as usize, 0); + file.extend_from_slice(&big); + assert_eq!(embedded_preview(&file).unwrap(), big); + assert_eq!(embedded_preview_covering(&file, 512).unwrap(), prvw, "a grid thumbnail"); + assert_eq!(embedded_preview_covering(&file, 1620).unwrap(), prvw); + assert_eq!(embedded_preview_covering(&file, 100).unwrap(), thmb); + assert_eq!(embedded_preview_covering(&file, 2560).unwrap(), big, "a large loupe"); + assert_eq!(embedded_preview_covering(&file, u32::MAX).unwrap(), big); + // a smaller preview of another shape (letterboxed 16:9) is never a stand-in for the 3:2 one + let mut wide = file.clone(); + let p = wide.windows(4).position(|w| w == b"PRVW").unwrap(); + // the JPEG starts 20 bytes after the tag; its frame height 7 bytes into it + let height = p + 20 + 7; + wide[height..height + 2].copy_from_slice(&911u16.to_be_bytes()); + assert_eq!(embedded_preview_covering(&wide, 512).unwrap(), big); + // without the JPEG track the boxes are chosen from too: the smallest that covers, else the largest + // (CodeRabbit on #3577: a box-only CR3 always gave its largest box) + let boxes_only = &file[..file.windows(4).position(|w| w == b"moov").unwrap() - 4]; + assert_eq!(embedded_preview_covering(boxes_only, 100).unwrap(), thmb); + assert_eq!(embedded_preview_covering(boxes_only, 512).unwrap(), prvw); + assert_eq!(embedded_preview_covering(boxes_only, 2560).unwrap(), prvw, "none covers: the largest"); + assert_eq!(embedded_preview(boxes_only).unwrap(), prvw); + // damaged files never panic + for n in (0..file.len()).step_by(7) { + let _ = embedded_preview_covering(&file[..n], 512); + } +} + +#[cfg(feature = "cr3")] +/// A minimal CR3 whose Canon box holds `CMT2` (EXIF ColorSpace `exif`) and `CMT3` (maker-note ColorSpace `note`). +fn cr3_with_color_space(exif: Option, note: Option) -> Vec { + let bx = |kind: &[u8; 4], body: &[u8]| -> Vec { [&((body.len() + 8) as u32).to_be_bytes()[..], kind, body].concat() }; + let block = |tag: u16, v: Option| { + let ifd = + v.map_or_else(|| IfdBuilder::new().with(t::ISO_SPEED, Value::Short(vec![100])), |v| IfdBuilder::new().with(tag, Value::Short(vec![v]))); + TiffWriter::new(craft_tiff::ByteOrder::Little, false).write(&[ifd]).unwrap() + }; + let cmt1 = TiffWriter::new(craft_tiff::ByteOrder::Little, false).write(&[IfdBuilder::new().with(t::MAKE, Value::Ascii("Canon".into()))]).unwrap(); + let canon_uuid = [0x85, 0xc0, 0xb6, 0x87, 0x82, 0x0f, 0x11, 0xe0, 0x81, 0x11, 0xf4, 0xce, 0x46, 0x2b, 0x6a, 0x48]; + let boxes = [bx(b"CMT1", &cmt1), bx(b"CMT2", &block(0xA001, exif)), bx(b"CMT3", &block(0x00b4, note))].concat(); + let mut file = bx(b"ftyp", b"crx \0\0\0\x01crx isom"); + file.extend(bx(b"moov", &bx(b"uuid", &[&canon_uuid[..], &boxes].concat()))); + file +} + +#[cfg(feature = "cr3")] +/// A CR3 (not TIFF-based) says its preview's colour space in the Canon maker note (`CMT3` 0x00b4), else an +/// EXIF ColorSpace of sRGB in `CMT2`: an EOS R shot in Adobe RGB writes Uncalibrated with no interop (issue #2654). +#[test] +fn cr3_preview_colour_space_comes_from_the_canon_maker_note() { + use PreviewColorSpace::{AdobeRgb, Srgb}; + let space = |exif, note| embedded_preview_color_space(&cr3_with_color_space(exif, note)); + assert_eq!(space(Some(0xFFFF), Some(2)), Some(AdobeRgb)); + assert_eq!(space(Some(1), Some(1)), Some(Srgb)); + assert_eq!(space(Some(1), Some(2)), Some(AdobeRgb), "the maker note wins"); + assert_eq!(space(Some(1), None), Some(Srgb), "no maker-note value: the EXIF sRGB"); + assert_eq!(space(Some(0xFFFF), Some(0xFFFF)), None, "neither says"); + assert_eq!(space(Some(0xFFFF), None), None); + assert_eq!(space(None, None), None); + let f = cr3_with_color_space(Some(0xFFFF), Some(2)); + for n in 0..f.len() { + let _ = embedded_preview_color_space(&f[..n]); + } +} + +#[cfg(feature = "cr3")] +/// A CR3's `PRVW` / `THMB` boxes are searched outside the media data only, but before and after +/// it: a `moov` after the `mdat` (its Canon `uuid` holding the `THMB`) still gives its preview, and +/// bytes inside the media data that look like a preview box are never taken for one. +#[test] +fn cr3_preview_boxes_after_the_media_data_are_found() { + let bx = |kind: &[u8; 4], body: &[u8]| -> Vec { [&((body.len() + 8) as u32).to_be_bytes()[..], kind, body].concat() }; + let boxed = |j: &[u8]| { + let mut b = b"\0\0\0\0\0\x01\x06\x54\x04\x38\0\x01".to_vec(); + b.extend_from_slice(&(j.len() as u32).to_be_bytes()); + b.extend_from_slice(j); + b + }; + let (thmb, in_media) = (sized_jpeg(160, 107, 1, 50), sized_jpeg(1620, 1080, 1, 300)); + let mut file = bx(b"ftyp", b"crx \0\0\0\x01crx isom"); + file.extend(bx(b"mdat", &bx(b"PRVW", &boxed(&in_media)))); + file.extend(bx(b"moov", &bx(b"uuid", &[&[0x85u8; 16][..], &bx(b"THMB", &boxed(&thmb))].concat()))); + assert_eq!(embedded_preview(&file).unwrap(), thmb); + for n in (0..file.len()).step_by(5) { + let _ = embedded_preview(&file[..n]); + } +} + +#[cfg(feature = "cr3")] +/// Every valid `PRVW` of a CR3 is a candidate (CodeRabbit on #3055): with a small one first and +/// one that covers the size after it, a thumbnail takes the second, not the full-size track. +#[test] +fn a_cr3_with_two_prvw_boxes_offers_both() { + let bx = |kind: &[u8; 4], body: &[u8]| -> Vec { [&((body.len() + 8) as u32).to_be_bytes()[..], kind, body].concat() }; + let full = |kind: &[u8; 4], body: &[u8]| bx(kind, &[&[0u8; 4][..], body].concat()); + let boxed = |j: &[u8]| { + let mut b = b"\0\0\0\0\0\x01\x06\x54\x04\x38\0\x01".to_vec(); + b.extend_from_slice(&(j.len() as u32).to_be_bytes()); + b.extend_from_slice(j); + b + }; + let (small, covering, big) = (sized_jpeg(300, 200, 1, 50), sized_jpeg(1620, 1080, 1, 300), sized_jpeg(8192, 5464, 1, 3000)); + let mut file = bx(b"ftyp", b"crx \0\0\0\x01crx isom"); + file.extend(bx(b"PRVW", &boxed(&small))); + file.extend(bx(b"PRVW", &boxed(&covering))); + let mut craw = vec![0u8; 82]; + craw.extend(bx(b"JPEG", &[0; 4])); + let at = 4096u64; + let stsd = full(b"stsd", &[&1u32.to_be_bytes()[..], &bx(b"CRAW", &craw)].concat()); + let stsz = full(b"stsz", &[0u32.to_be_bytes(), 1u32.to_be_bytes(), (big.len() as u32).to_be_bytes()].concat()); + let co64 = full(b"co64", &[&1u32.to_be_bytes()[..], &at.to_be_bytes()].concat()); + let trak = bx(b"trak", &bx(b"mdia", &bx(b"minf", &bx(b"stbl", &[stsd, stsz, co64].concat())))); + file.extend(bx(b"moov", &trak)); + // (the track in the media data, as a camera writes it, after a `free` box up to it) + let pad = at as usize - 16 - file.len(); + file.extend(bx(b"free", &vec![0u8; pad])); + file.extend(bx(b"mdat", &big)); + assert_eq!(embedded_preview_covering(&file, 1024).unwrap(), covering); + assert_eq!(embedded_preview_covering(&file, 200).unwrap(), small); +} + +#[cfg(feature = "cr3")] +/// A CR3 preview box claiming the largest sizes a file can give is skipped, never wrapped or +/// panicked on (CodeRabbit on #3080: on a 32-bit build the plain sums could overflow). +#[test] +fn a_cr3_preview_box_with_hostile_sizes_is_skipped() { + let mut file = b"\0\0\0\x18ftypcrx \0\0\0\x01crx isom".to_vec(); + file.extend_from_slice(&u32::MAX.to_be_bytes()); + file.extend_from_slice(b"PRVW\0\0\0\0\0\x01\x06\x54\x04\x38\0\x01"); + file.extend_from_slice(&u32::MAX.to_be_bytes()); + file.extend_from_slice(&fake_jpeg(64)); + assert!(embedded_preview(&file).is_none()); + assert!(embedded_preview_covering(&file, 64).is_none()); +} + +#[cfg(feature = "cr3")] +/// A CR3 without a JPEG track whose smaller `PRVW` (another shape) takes more bytes: the picture is +/// the box with the most pixels, not the most bytes (CodeRabbit on #3077). +#[test] +fn a_box_only_cr3_is_judged_by_pixels_not_bytes() { + let bx = |kind: &[u8; 4], body: &[u8]| -> Vec { [&((body.len() + 8) as u32).to_be_bytes()[..], kind, body].concat() }; + let boxed = |j: &[u8]| { + let mut b = b"\0\0\0\0\0\x01\x06\x54\x04\x38\0\x01".to_vec(); + b.extend_from_slice(&(j.len() as u32).to_be_bytes()); + b.extend_from_slice(j); + b + }; + let (big, square) = (sized_jpeg(1620, 1080, 3, 300), sized_jpeg(800, 800, 3, 5000)); + let mut file = bx(b"ftyp", b"crx \0\0\0\x01crx isom"); + file.extend(bx(b"PRVW", &boxed(&square))); + file.extend(bx(b"PRVW", &boxed(&big))); + // no media data found: the whole file is searched, and only its first box counts (a later look-alike could be + // in media data; CodeRabbit on #3583) + assert_eq!(embedded_preview(&file).unwrap(), square); + file.extend(bx(b"mdat", &[0u8; 16])); + assert_eq!(embedded_preview(&file).unwrap(), big); + assert_eq!(embedded_preview_covering(&file, 100).unwrap(), big, "the square is another picture's shape"); +} + +#[cfg(feature = "cr3")] +/// Every top-level `mdat` is media data, not only the first: a PRVW-shaped JPEG in a second one is +/// never the preview; and a `PRVW` cut short before its end marker isn't chosen to cover a size +/// (CodeRabbit on #3583). +#[test] +fn cr3_boxes_in_any_media_data_or_cut_short_are_not_previews() { + let bx = |kind: &[u8; 4], body: &[u8]| -> Vec { [&((body.len() + 8) as u32).to_be_bytes()[..], kind, body].concat() }; + let boxed = |j: &[u8]| { + let mut b = b"\0\0\0\0\0\x01\x06\x54\x04\x38\0\x01".to_vec(); + b.extend_from_slice(&(j.len() as u32).to_be_bytes()); + b.extend_from_slice(j); + b + }; + let (real, fake) = (sized_jpeg(1620, 1080, 3, 300), sized_jpeg(3000, 2000, 3, 300)); + let mut file = bx(b"ftyp", b"crx \0\0\0\x01crx isom"); + file.extend(bx(b"PRVW", &boxed(&real))); + file.extend(bx(b"mdat", &[0u8; 64])); + file.extend(bx(b"mdat", &bx(b"PRVW", &boxed(&fake)))); + assert_eq!(embedded_preview(&file).unwrap(), real, "the look-alike in the second mdat is media data"); + // a PRVW cut short (no end marker): the full-size track covers a thumbnail instead + let mut short = real.clone(); + short.truncate(real.len() - 2); + let big = sized_jpeg(6000, 4000, 3, 3000); + assert_eq!(embedded_preview_covering(&cr3_with(&short, &big), 512).unwrap(), big); + assert_eq!(embedded_preview_covering(&cr3_with(&real, &big), 512).unwrap(), real, "a whole one is still taken"); +} + +#[cfg(all(feature = "cr3", feature = "dng"))] +/// The copy a CR3 thumbnail is given decodes, whichever is chosen (CodeRabbit on #3583: the other CR3 fixtures have +/// no scan): real encoded JPEGs, the `PRVW` for a small size, the full-size track past it. +#[test] +fn the_cr3_copy_chosen_for_a_thumbnail_decodes() { + let encode = |w: u16, h: u16| { + let px: Vec = (0..usize::from(w) * usize::from(h)).flat_map(|i| [(i % 251) as u8, 90, 160]).collect(); + let mut j = Vec::new(); + jpeg_encoder::Encoder::new(&mut j, 85).encode(&px, w, h, jpeg_encoder::ColorType::Rgb).unwrap(); + j + }; + let file = cr3_with(&encode(96, 64), &encode(384, 256)); + for (min, want) in [(64u32, (96, 64)), (200, (384, 256))] { + let j = embedded_preview_covering(&file, min).unwrap(); + let (_, w, h, c) = crate::tiffraw::lossy_jpeg(&j, 3, 1 << 20).unwrap(); + assert_eq!(((w, h), c), (want, 3), "covering {min} px"); + } +} diff --git a/crates/raw/src/sparse.rs b/crates/raw/src/sparse.rs new file mode 100644 index 000000000..08720bbdb --- /dev/null +++ b/crates/raw/src/sparse.rs @@ -0,0 +1,773 @@ +//! Describing a raw file without reading its pixel data. +//! +//! [`probe_info`](crate::probe_info) and a metadata reader read only a raw file's +//! headers, but those are a few hundred kilobytes of a file of tens of megabytes: an import from a +//! slow drive spends nearly all its time reading samples nothing looks at. [`header_plan`] says +//! which byte ranges of a file those readers need. It is asked about a buffer of the file's full +//! length in which only the ranges in `loaded` hold the file's bytes (the rest is zeros); the caller +//! loads the ranges it names and asks again. Once it says [`HeaderPlan::Ready`], probing the buffer +//! gives what probing the whole file gives. +//! +//! Only bulk sample data is left out: the strips and tiles of each large TIFF image (keeping the +//! start of the first one, where the coding headers a header probe validates sit), a CR3's image +//! tracks (keeping the raw track's coding headers and quantization maps, and the timed-metadata +//! track where Canon puts the as-shot white balance), a RAF's CFA strip (keeping its compression +//! header) and a Panasonic RW2's raw data. Formats whose header probe reads samples (NEF: masked +//! columns; ORF: bit depth and CFA phase; PEF: dark borders; ARW6, for its header plan only: the +//! start of every tile) and anything else get [`HeaderPlan::Whole`], as does a structure that is +//! incomplete, inconsistent or keeps metadata inside its sample data: the plan never guesses. + +use std::collections::HashSet; + +#[cfg(feature = "cr3")] +use craft_metadata::cr3::{Cr3TrackKind, XMP_UUID, is_cr3, parse_cr3}; +use craft_tiff::{ByteOrder, FieldType, Tiff, tags as t}; + +use crate::RawFormat; + +/// Bytes kept at the start of a skipped block: the coding headers a header probe validates there +/// (lossless-JPEG frame headers, a RAF's compression header and stripe table) fit with room to spare. +const HEAD: u64 = 64 * 1024; +/// An image whose chunks hold fewer bytes than this is read (thumbnails, small previews). +const BULK: u64 = 256 * 1024; +/// The same bounds as `craft_tiff`'s reader. +const MAX_IFDS: usize = 1024; +const MAX_ENTRIES: u64 = 8192; +#[cfg(feature = "cr3")] +const MAX_BOXES: usize = 4096; +/// Child IFDs listed by one `SubIFDs` entry. +const MAX_CHILDREN: u64 = 4096; +/// Pending child-IFD offsets the walker may queue at once: no more than it can ever visit +/// (the same [`MAX_IFDS`] the seen set is held to, deduplicated as they are popped), so an alley +/// of hostile `SubIFDs` entries can't grow the queue into the tens of millions of pointers. +/// The plan never guesses: the walk refuses and the plan reports the whole file when it would go +/// past the cap. +const MAX_PENDING: usize = MAX_IFDS; +/// The RAF raw container's pointer to its raw IFD. +#[cfg(feature = "raf")] +const RAF_RAW_IFD: u16 = 0xf000; +/// Panasonic RW2 IFD0 `RawDataOffset` (`0x0118`): its raw data runs from there to the end of the file. +const RW2_RAW_DATA_OFFSET: u16 = 0x0118; + +/// Byte ranges `[start, end)` of a file: sorted, disjoint and not touching. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Ranges(Vec<(u64, u64)>); + +impl Ranges { + pub fn new() -> Self { + Self::default() + } + + /// Add `[start, end)` (nothing when it is empty). + pub fn insert(&mut self, start: u64, end: u64) { + if start >= end { + return; + } + let (mut start, mut end) = (start, end); + let mut out = Vec::with_capacity(self.0.len() + 1); + let mut placed = false; + for &(a, b) in &self.0 { + if b < start { + out.push((a, b)); + } else if a > end { + if !placed { + out.push((start, end)); + placed = true; + } + out.push((a, b)); + } else { + start = start.min(a); + end = end.max(b); + } + } + if !placed { + out.push((start, end)); + } + self.0 = out; + } + + /// Add every `[start, end)` of `spans` in one sort and merge (empty ones add nothing). + pub fn extend(&mut self, spans: impl IntoIterator) { + let mut all: Vec<(u64, u64)> = std::mem::take(&mut self.0); + all.extend(spans.into_iter().filter(|&(a, b)| a < b)); + all.sort_unstable(); + let mut out: Vec<(u64, u64)> = Vec::with_capacity(all.len()); + for (a, b) in all { + match out.last_mut() { + Some(last) if a <= last.1 => last.1 = last.1.max(b), + _ => out.push((a, b)), + } + } + self.0 = out; + } + + /// Whether all of `[start, end)` is covered (an empty range always is). + pub fn contains(&self, start: u64, end: u64) -> bool { + start >= end || self.0.iter().any(|&(a, b)| a <= start && end <= b) + } + + /// Whether some of `[start, end)` is covered. + pub fn overlaps(&self, start: u64, end: u64) -> bool { + start < end && self.0.iter().any(|&(a, b)| a < end && start < b) + } + + pub fn spans(&self) -> &[(u64, u64)] { + &self.0 + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Bytes covered. + pub fn total(&self) -> u64 { + self.0.iter().fold(0u64, |n, &(a, b)| n.saturating_add(b - a)) + } + + /// The parts of `[start, end)` not covered. + pub fn gaps(&self, start: u64, end: u64) -> Ranges { + // the gaps come in order, apart from each other: pushed, not merged in one by one + let mut out = Vec::new(); + let mut at = start; + for &(a, b) in &self.0 { + if b <= at { + continue; + } + if a >= end { + break; + } + if a > at { + out.push((at, a)); + } + at = at.max(b); + } + if at < end { + out.push((at, end)); + } + Ranges(out) + } + + /// These ranges without the ones in `other`. + pub fn minus(&self, other: &Ranges) -> Ranges { + let mut out = Ranges::new(); + out.extend(self.0.iter().flat_map(|&(a, b)| other.gaps(a, b).0)); + out + } +} + +/// What [`header_plan`] says about a partly loaded file. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum HeaderPlan { + /// Load these ranges (none of them loaded yet) and ask again. + Need(Ranges), + /// Everything the header readers look at is loaded: probing the buffer gives the whole file's + /// result. + Ready, + /// The file can't be described from part of it (why): read it whole. + Whole(&'static str), +} + +/// What of the raw file in `bytes` a header probe reads. `bytes` has the file's full length; +/// only the ranges in `loaded` hold its bytes (see the module docs). +pub fn header_plan(bytes: &[u8], loaded: &Ranges) -> HeaderPlan { + plan(bytes, loaded, false) +} + +/// [`header_plan`] for a raw shown from its embedded preview (a coding not decoded yet): also what +/// [`crate::embedded_preview`] reads — a CR3's JPEG track and preview boxes, the strips of a TIFF +/// raw's preview images. +pub fn preview_plan(bytes: &[u8], loaded: &Ranges) -> HeaderPlan { + plan(bytes, loaded, true) +} + +fn plan(bytes: &[u8], loaded: &Ranges, preview: bool) -> HeaderPlan { + let mut w = Want::new(bytes, loaded); + if !w.want(0, 16) { + return w.into_plan().unwrap_or(HeaderPlan::Whole("file too short")); + } + if crate::probe(bytes) == Some(RawFormat::Cr3) { + #[cfg(feature = "cr3")] + return if is_cr3(bytes) { cr3_plan(bytes, loaded, preview) } else { HeaderPlan::Whole("CR3 structure") }; + #[cfg(not(feature = "cr3"))] + return HeaderPlan::Whole("CR3 support not built"); + } + if bytes.starts_with(b"FUJIFILMCCD-RAW") { + // (its preview, the embedded JPEG, is read anyway: its metadata are there) + #[cfg(feature = "raf")] + return raf_plan(bytes, loaded); + #[cfg(not(feature = "raf"))] + return HeaderPlan::Whole("RAF support not built"); + } + if bytes.get(..4).is_some_and(|m| matches!(m, b"IIRO" | b"IIRS" | b"MMOR")) { + HeaderPlan::Whole("an ORF header probe reads samples") + } else if Tiff::sniff(bytes).is_some() { + tiff_plan(bytes, loaded, preview) + } else { + HeaderPlan::Whole("not a raw layout read in part") + } +} + +/// Collects the ranges a plan still needs. +struct Want<'a> { + len: u64, + loaded: &'a Ranges, + /// What is missing, as found; merged once into the plan (merging each find into the ranges so + /// far was a copy of them per find: quadratic over a file of many out-of-line values). + need: Vec<(u64, u64)>, + /// `need` is merged when it reaches this long, so repeated finds of one gap stay bounded by + /// the distinct gaps (merging when it doubles keeps the cost linear). + compact_at: usize, +} + +impl<'a> Want<'a> { + fn new(bytes: &[u8], loaded: &'a Ranges) -> Self { + Self { len: bytes.len() as u64, loaded, need: Vec::new(), compact_at: 64 } + } + + /// Whether `[start, end)` (clipped to the file) is loaded; asks for what isn't. + fn want(&mut self, start: u64, end: u64) -> bool { + let end = end.min(self.len); + if self.loaded.contains(start, end) { + return true; + } + self.need.extend(self.loaded.gaps(start, end).0); + if self.need.len() >= self.compact_at { + let mut merged = Ranges::new(); + merged.extend(std::mem::take(&mut self.need)); + self.need = merged.spans().to_vec(); + self.compact_at = (self.need.len() * 2).max(64); + } + false + } + + /// `Need` when something is missing. + fn into_plan(self) -> Option { + let mut need = Ranges::new(); + need.extend(self.need); + (!need.is_empty()).then_some(HeaderPlan::Need(need)) + } +} + +/// Everything but `holes` is needed. `meta` (IFD tables and out-of-line values) must not lie in a +/// hole; once all is loaded, `ready` decides. +fn finish(len: u64, loaded: &Ranges, holes: &Ranges, meta: &Ranges, ready: impl FnOnce() -> HeaderPlan) -> HeaderPlan { + if holes.spans().iter().any(|&(a, b)| meta.overlaps(a, b)) { + return HeaderPlan::Whole("metadata inside sample data"); + } + let missing = holes.gaps(0, len).minus(loaded); + if missing.is_empty() { ready() } else { HeaderPlan::Need(missing) } +} + +fn tiff_plan(bytes: &[u8], loaded: &Ranges, preview: bool) -> HeaderPlan { + let len = bytes.len() as u64; + let mut w = Want::new(bytes, loaded); + let meta = match walk_tiff(&mut w, 0, bytes, &[]) { + Ok(meta) => meta, + Err(why) => return HeaderPlan::Whole(why), + }; + if let Some(plan) = w.into_plan() { + return plan; + } + // (the Make is usually in the first kilobytes: a NEF, PEF or ORF is known before the rest is read) + if matches!(crate::probe(bytes), Some(RawFormat::Nef | RawFormat::Nrw | RawFormat::Orf | RawFormat::Pef | RawFormat::Erf | RawFormat::Mef)) { + return HeaderPlan::Whole("its header probe reads samples"); + } + let Ok(tiff) = Tiff::parse(bytes) else { return HeaderPlan::Whole("TIFF structure") }; + let mut holes = Ranges::new(); + let mut spans = Vec::new(); + for ifd in tiff.all_ifds() { + let chunks: Vec<(u64, u64)> = match ifd.image() { + // a preview's strips are read when the preview is wanted: only the raw image is skipped + Ok(info) if preview && !matches!(info.photometric, t::photometric::CFA | t::photometric::LINEAR_RAW) => continue, + // (Sony Compressed RAW 2: the header probe decodes the start of every tile; a preview reads none) + Ok(info) if !preview && info.compression == 32766 => return HeaderPlan::Whole("an ARW6 header probe reads its tiles"), + Ok(info) => info.chunks(len).iter().map(|c| (c.offset, c.len)).collect(), + // a raw IFD without the tags a TIFF image needs (an older CR2's) still lists its strips + Err(_) => match (ifd.u64s(t::STRIP_OFFSETS), ifd.u64s(t::STRIP_BYTE_COUNTS)) { + (Some(offsets), Some(counts)) if offsets.len() == counts.len() => offsets.into_iter().zip(counts).collect(), + _ => continue, + }, + }; + let total = chunks.iter().fold(0u64, |n, c| n.saturating_add(c.1)); + if total < BULK { + continue; + } + spans.extend(chunks.iter().enumerate().map(|(i, &(offset, size))| { + let start = if i == 0 { offset.saturating_add(HEAD) } else { offset }; + (start, offset.saturating_add(size).min(len)) + })); + } + holes.extend(spans); + // A Panasonic RW2 keeps its raw data from `RawDataOffset` to the end, outside any TIFF image. + if holes.is_empty() + && bytes.starts_with(b"IIU\0") + && let Some(off) = tiff.ifds.first().and_then(|i| i.u64(RW2_RAW_DATA_OFFSET)) + { + holes.insert(off.saturating_add(HEAD), len); + } + finish(len, loaded, &holes, &meta, || match crate::probe(bytes) { + Some(RawFormat::Nef | RawFormat::Nrw | RawFormat::Orf | RawFormat::Pef | RawFormat::Erf | RawFormat::Mef) => { + HeaderPlan::Whole("its header probe reads samples") + } + Some(f) if f.is_supported() => HeaderPlan::Ready, + _ => HeaderPlan::Whole("not a supported raw"), + }) +} + +/// Walk the IFDs of the TIFF in `data` (which starts at `origin` in the file) as `craft_tiff`'s +/// reader does — the chain, `SubIFDs`, Exif, GPS and Interop, plus the `extra` pointer tags — +/// asking for every IFD table and for the values that locate IFDs and image chunks. Returns the IFD +/// tables and out-of-line values found (absolute ranges); incomplete while `w` needs more. +fn walk_tiff(w: &mut Want, origin: u64, data: &[u8], extra: &[u16]) -> Result { + let len = data.len() as u64; + let at = |rel: u64| origin.saturating_add(rel); + let mut meta = Ranges::new(); + if !w.want(origin, at(len.min(16))) { + return Ok(meta); + } + let (order, big) = Tiff::sniff(data).ok_or("not a TIFF")?; + let first = if big { order.read_u64(data, 8) } else { order.read_u32(data, 4).map(u64::from) }.ok_or("truncated TIFF header")?; + meta.insert(origin, at(len.min(if big { 16 } else { 8 }))); + let (head, entry, inline) = if big { (8u64, 20u64, 8u64) } else { (2, 12, 4) }; + let offset_at = |pos: u64| -> Option { + let pos = usize::try_from(pos).ok()?; + if big { order.read_u64(data, pos) } else { order.read_u32(data, pos).map(u64::from) } + }; + let mut stack = vec![first]; + let mut seen = HashSet::new(); + // Every offset ever queued: the pending cap counts distinct IFDs, so a thousand references to + // one child cost one visit, not a thousand. + let mut queued = HashSet::from([first]); + // Every IFD table and out-of-line value, collected and merged once at the end: an insert + // per entry would scan the ranges of every entry before it — quadratic in the metadata + // this walker collects. + let mut spans: Vec<(u64, u64)> = Vec::new(); + while let Some(off) = stack.pop() { + if off == 0 || off >= len || seen.len() >= MAX_IFDS || !seen.insert(off) { + continue; + } + if !w.want(at(off), at(off.saturating_add(head))) { + continue; + } + let Ok(pos) = usize::try_from(off) else { continue }; + let n = if big { order.read_u64(data, pos) } else { order.read_u16(data, pos).map(u64::from) }.unwrap_or(0); + let table = off.saturating_add(head); + let n_read = n.min(len.saturating_sub(table) / entry).min(MAX_ENTRIES); + if n_read == 0 { + continue; + } + let table_end = table.saturating_add(n_read * entry); + // the reader reads the next-IFD pointer only when every entry fits + let complete = n_read == n; + let end = if complete { table_end.saturating_add(inline).min(len) } else { table_end }; + if !w.want(at(off), at(end)) { + continue; + } + spans.push((at(off), at(end))); + for i in 0..n_read { + let p = table + i * entry; + let Ok(pu) = usize::try_from(p) else { continue }; + let (Some(tag), Some(typ)) = (order.read_u16(data, pu), order.read_u16(data, pu + 2).and_then(FieldType::from_u16)) else { continue }; + let count = if big { order.read_u64(data, pu + 4) } else { order.read_u32(data, pu + 4).map(u64::from) }; + let Some(count) = count else { continue }; + let field = p + if big { 12 } else { 8 }; + let Some(total) = count.checked_mul(typ.size() as u64) else { continue }; + let start = if total <= inline { + field + } else { + match offset_at(field) { + Some(v) => v, + None => continue, + } + }; + let Some(value_end) = start.checked_add(total).filter(|&e| e <= len) else { continue }; + if total > inline { + spans.push((at(start), at(value_end))); + } + let child = matches!(tag, t::SUB_IFDS | t::EXIF_IFD | t::GPS_IFD | t::INTEROP_IFD) || extra.contains(&tag); + let locates = child || matches!(tag, t::STRIP_OFFSETS | t::STRIP_BYTE_COUNTS | t::TILE_OFFSETS | t::TILE_BYTE_COUNTS); + if !locates || !w.want(at(start), at(value_end)) || !child { + continue; + } + // the reader follows every `SubIFDs` value and the first value of the other pointers + let take = if tag == t::SUB_IFDS { count.min(MAX_CHILDREN) } else { count.min(1) }; + for k in 0..take { + if let Some(v) = unsigned(data, order, typ, start + k * typ.size() as u64) { + if v == 0 || v >= len || !queued.insert(v) { + continue; + } + if stack.len() >= MAX_PENDING { + return Err("too many child IFDs queued to visit"); + } + stack.push(v); + } + } + } + if complete && let Some(next) = offset_at(table_end).filter(|&next| next != 0 && next < len && queued.insert(next)) { + if stack.len() >= MAX_PENDING { + return Err("too many child IFDs queued to visit"); + } + stack.push(next); + } + } + meta.extend(spans); + Ok(meta) +} + +/// One unsigned value of type `typ` at `pos`. +fn unsigned(data: &[u8], order: ByteOrder, typ: FieldType, pos: u64) -> Option { + let pos = usize::try_from(pos).ok()?; + match typ.size() { + 1 => data.get(pos).map(|&b| u64::from(b)), + 2 => order.read_u16(data, pos).map(u64::from), + 4 => order.read_u32(data, pos).map(u64::from), + _ => order.read_u64(data, pos), + } +} + +#[cfg(any(feature = "cr3", feature = "raf"))] +fn be32(bytes: &[u8], at: u64) -> Option { + let at = usize::try_from(at).ok()?; + bytes.get(at..at.checked_add(4)?).map(|s| u64::from(u32::from_be_bytes([s[0], s[1], s[2], s[3]]))) +} + +#[cfg(feature = "cr3")] +fn be64(bytes: &[u8], at: u64) -> Option { + let at = usize::try_from(at).ok()?; + bytes.get(at..at.checked_add(8)?).map(|s| u64::from_be_bytes([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]])) +} + +#[cfg(feature = "cr3")] +/// A CR3 is read where `craft_metadata::cr3::parse_cr3` and the CR3 decoder look: the file type, +/// `moov` and the XMP `uuid` box, the first sample of each timed-metadata (`CTMD`) track, and the +/// main raw track's coding headers and quantization maps. Previews and image tracks are skipped, +/// unless `preview`: then every top-level box but `mdat` and the JPEG tracks are read too. +fn cr3_plan(bytes: &[u8], loaded: &Ranges, preview: bool) -> HeaderPlan { + let len = bytes.len() as u64; + let mut w = Want::new(bytes, loaded); + // top-level boxes, walked as `parse_cr3` walks them (it stops at the first malformed header) + let mut at = 0u64; + let mut boxes = 0; + while at.saturating_add(8) <= len && boxes < MAX_BOXES { + boxes += 1; + // size, type, a 64-bit size and a uuid + if !w.want(at, at.saturating_add(32)) { + break; + } + let Some(size) = be32(bytes, at) else { break }; + let (box_len, hdr) = match size { + 0 => (len - at, 8), + 1 => match be64(bytes, at + 8) { + Some(l) => (l, 16), + None => break, + }, + n => (n, 8), + }; + let Some(end) = at.checked_add(box_len).filter(|&e| e <= len && box_len >= hdr) else { break }; + let kind = bytes.get(at as usize + 4..at as usize + 8); + let needed = match kind { + Some(b"mdat") => false, + Some(b"moov") => true, + Some(b"uuid") => preview || bytes.get((at + hdr) as usize..(at + hdr + 16) as usize) == Some(XMP_UUID.as_slice()), + _ => preview, + }; + if needed { + w.want(at, end); + } + at = end; + } + if let Some(plan) = w.into_plan() { + return plan; + } + let mut w = Want::new(bytes, loaded); + let Some(cr3) = parse_cr3(bytes) else { return HeaderPlan::Whole("CR3 structure") }; + for track in &cr3.tracks { + if (track.kind == Cr3TrackKind::Other(*b"CTMD") || preview && track.kind == Cr3TrackKind::Jpeg) + && let Some((offset, size)) = track.data + { + w.want(offset as u64, offset.saturating_add(size) as u64); + } + } + if let Some(track) = crate::cr3::main_track(&cr3.tracks) + && let Some((offset, size)) = track.data + && let Some(coding) = track.compression(bytes) + { + let o = offset as u64; + if w.want(o, o.saturating_add(u64::from(coding.header_size))) + && let Some(sample) = bytes.get(offset..offset.saturating_add(size)) + && let Ok(ranges) = crate::crx::header_ranges(sample, &coding) + { + for r in ranges { + w.want(o.saturating_add(r.start as u64), o.saturating_add(r.end as u64)); + } + } + } + w.into_plan().unwrap_or(HeaderPlan::Ready) +} + +#[cfg(feature = "raf")] +/// A RAF is read whole but for its CFA strip: the header, the embedded JPEG (its metadata), the +/// record directory and the raw container's IFDs and compression header. +fn raf_plan(bytes: &[u8], loaded: &Ranges) -> HeaderPlan { + let len = bytes.len() as u64; + let mut w = Want::new(bytes, loaded); + if !w.want(0, 160) { + return w.into_plan().unwrap_or(HeaderPlan::Whole("RAF header")); + } + let (Some(start), Some(size)) = (be32(bytes, 100), be32(bytes, 104)) else { return HeaderPlan::Whole("RAF header") }; + let Some(end) = start.checked_add(size).filter(|&e| start > 0 && size > 0 && e <= len) else { return HeaderPlan::Whole("RAF raw container") }; + let Some(container) = bytes.get(start as usize..end as usize) else { return HeaderPlan::Whole("RAF raw container") }; + let meta = match walk_tiff(&mut w, start, container, &[RAF_RAW_IFD]) { + Ok(meta) => meta, + Err(why) => return HeaderPlan::Whole(why), + }; + if let Some(plan) = w.into_plan() { + return plan; + } + let Ok(ifd) = crate::raf::raw_ifd(container) else { return HeaderPlan::Whole("RAF raw IFD") }; + let (Some(off), Some(strip)) = (ifd.u64(0xf007), ifd.u64(0xf008)) else { return HeaderPlan::Whole("RAF raw strip") }; + let mut holes = Ranges::new(); + if strip >= BULK { + let s = start.saturating_add(off); + holes.insert(s.saturating_add(HEAD), s.saturating_add(strip).min(end)); + } + finish(len, loaded, &holes, &meta, || HeaderPlan::Ready) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Run a plan to its end, loading from `file` what it asks for; the buffer's unloaded bytes are `fill`. + pub(crate) fn run(file: &[u8], prefix: usize, fill: u8) -> (HeaderPlan, Vec, Ranges) { + let mut buf = vec![fill; file.len()]; + let mut loaded = Ranges::new(); + let load = |buf: &mut Vec, loaded: &mut Ranges, a: u64, b: u64| { + buf[a as usize..b as usize].copy_from_slice(&file[a as usize..b as usize]); + loaded.insert(a, b); + }; + load(&mut buf, &mut loaded, 0, prefix.min(file.len()) as u64); + for _ in 0..32 { + match header_plan(&buf, &loaded) { + HeaderPlan::Need(r) => { + for &(a, b) in r.spans() { + assert!(!loaded.overlaps(a, b), "asked again for {a}..{b}"); + load(&mut buf, &mut loaded, a, b); + } + } + done => return (done, buf, loaded), + } + } + panic!("no answer after 32 rounds"); + } + + #[test] + fn ranges_merge_and_subtract() { + let mut r = Ranges::new(); + r.insert(10, 20); + r.insert(30, 40); + r.insert(20, 25); // touching: merged + r.insert(5, 5); // empty + assert_eq!(r.spans(), &[(10, 25), (30, 40)]); + r.insert(0, 100); + assert_eq!(r.spans(), &[(0, 100)]); + let mut r = Ranges::new(); + r.insert(10, 20); + r.insert(30, 40); + assert!(r.contains(12, 18) && r.contains(5, 5) && !r.contains(15, 35)); + assert!(r.overlaps(15, 35) && !r.overlaps(20, 30)); + assert_eq!(r.gaps(0, 50).spans(), &[(0, 10), (20, 30), (40, 50)]); + assert_eq!(r.gaps(12, 18).spans(), &[] as &[(u64, u64)]); + let mut cut = Ranges::new(); + cut.insert(15, 32); + assert_eq!(r.minus(&cut).spans(), &[(10, 15), (32, 40)]); + assert_eq!(r.total(), 20); + } + + #[test] + fn a_plan_of_many_missing_values_is_gathered_in_one_merge() { + // 200 000 loaded spans with a byte missing between each: every gap wanted once + let mut loaded = Ranges::new(); + loaded.extend((0..20_000u64).map(|i| (i * 4, i * 4 + 3))); + let bytes = vec![0u8; 80_000]; + let mut w = Want::new(&bytes, &loaded); + for i in 0..20_000u64 { + assert!(!w.want(i * 4 + 2, i * 4 + 4)); + } + let Some(HeaderPlan::Need(need)) = w.into_plan() else { panic!("a plan") }; + assert_eq!(need.spans().len(), 20_000); + assert_eq!(need.total(), 20_000); + assert_eq!(loaded.gaps(0, 12).spans(), &[(3, 4), (7, 8), (11, 12)]); + } + + #[test] + fn repeated_wants_of_one_gap_keep_the_buffer_bounded() { + let loaded = Ranges::new(); + let bytes = vec![0u8; 1000]; + let mut w = Want::new(&bytes, &loaded); + for _ in 0..100_000 { + assert!(!w.want(10, 20)); + assert!(w.need.len() <= 64); + } + let Some(HeaderPlan::Need(need)) = w.into_plan() else { panic!("a plan") }; + assert_eq!(need.spans(), &[(10, 20)]); + } + + #[test] + fn hostile_input_never_panics_and_ends() { + let mut loaded = Ranges::new(); + for len in [0usize, 1, 8, 15, 16, 200] { + let bytes = vec![0u8; len]; + loaded.insert(0, len as u64); + assert!(matches!(header_plan(&bytes, &loaded), HeaderPlan::Whole(_))); + } + // an IFD that points at itself, an IFD count past the end, a huge SubIFDs count + let mut tiff = b"II*\0\x08\0\0\0".to_vec(); + tiff.extend_from_slice(&2u16.to_le_bytes()); + tiff.extend_from_slice(&[0x4a, 0x01, 4, 0, 0xff, 0xff, 0xff, 0x0f, 8, 0, 0, 0]); // SubIFDs, 0x0fffffff values at 8 + tiff.extend_from_slice(&[0x69, 0x87, 4, 0, 1, 0, 0, 0, 8, 0, 0, 0]); // Exif → itself + tiff.extend_from_slice(&8u32.to_le_bytes()); // next → itself + let (plan, _, _) = run(&tiff, 4, 0); + assert!(matches!(plan, HeaderPlan::Whole(_)), "{plan:?}"); + // a CR3 whose `moov` claims more than the file holds: walked as the parser walks it (it + // stops there), so the plan ends with the little there is, and the probe then fails as + // it does on the whole file + #[cfg(feature = "cr3")] + { + let mut cr3 = 24u32.to_be_bytes().to_vec(); + cr3.extend_from_slice(b"ftypcrx \0\0\0\x01crx isom"); + cr3.extend_from_slice(&u32::MAX.to_be_bytes()); + cr3.extend_from_slice(b"moov"); + cr3.resize(4096, 0); + let (plan, buf, loaded) = run(&cr3, 16, 0); + assert_eq!(plan, HeaderPlan::Ready); + assert_eq!(loaded.total(), 24 + 32, "only the two box headers are read"); + assert_eq!(format!("{:?}", crate::probe_info(&buf)), format!("{:?}", crate::probe_info(&cr3))); + } + // a RAF whose container lies outside the file + #[cfg(feature = "raf")] + { + let mut raf = b"FUJIFILMCCD-RAW 0201FF383501".to_vec(); + raf.resize(160, 0); + raf[100..104].copy_from_slice(&1000u32.to_be_bytes()); + raf[104..108].copy_from_slice(&1000u32.to_be_bytes()); + assert_eq!(run(&raf, 160, 0).0, HeaderPlan::Whole("RAF raw container")); + } + } + + /// An ARW6 header probe reads every tile's headers and the first lines of its data, which lie in the strip a + /// plan would skip: here the second tile, past a first one of bulk size. + #[cfg(feature = "arw")] + #[test] + fn arw6_is_described_from_the_whole_file() { + use crate::arw6_testenc::{Qis, arw6_file, one_tile_quantised}; + let (tile, _) = one_tile_quantised(64, 48, 0, Qis::REAL); + let mut first = tile.clone(); + first.resize(BULK as usize, 0); + let file = arw6_file(&[(0, 0, 64, 48, first), (64, 0, 64, 48, tile)], 128, 48); + assert!(crate::probe_info(&file).is_ok()); + assert_eq!(run(&file, 16, 0).0, HeaderPlan::Whole("an ARW6 header probe reads its tiles")); + } + + /// An IFD whose `SubIFDs` entries would queue 33 million child pointers (8192 entries of + /// 4096 values apiece, all pointing at the same array) is refused at the walker's pending + /// cap instead of growing the queue — and the plan reports the whole file, never guessing. + #[test] + fn a_subifds_alley_is_refused_at_the_pending_cap() { + // First read everything, so the walk reaches the directory table itself. + let n = 8192u32; + let len = 8 + 2 + (n * 12 + 4 + 4 * 4096) as usize; + let mut tiff = b"II*\0\x08\0\0\0".to_vec(); + tiff.extend_from_slice(&(n as u16).to_le_bytes()); + let mut entry = |tag: u16, typ: u16, count: u32, value: u32| { + tiff.extend_from_slice(&tag.to_le_bytes()); + tiff.extend_from_slice(&typ.to_le_bytes()); + tiff.extend_from_slice(&count.to_le_bytes()); + tiff.extend_from_slice(&value.to_le_bytes()); + }; + // header (8) + count field (2) + the {n} numbers and pointer (4), then the value array + let values_at = (8 + 2u64 + u64::from(n) * 12 + 4) as u32; + for _ in 0..n { + entry(t::SUB_IFDS, 4, MAX_CHILDREN as u32, values_at); + } + tiff.extend_from_slice(&0u32.to_le_bytes()); + tiff.extend((1..=4096u32).flat_map(u32::to_le_bytes)); + let mut loaded = Ranges::new(); + loaded.insert(0, len as u64); + let mut w = Want::new(&tiff, &loaded); + assert_eq!(walk_tiff(&mut w, 0, &tiff, &[]), Err("too many child IFDs queued to visit")); + } + + /// A TIFF with many out-of-line values across 512 directories is planned in one sort and + /// merge, not one pass over all accumulated ranges per value (CodeRabbit storytold/lightcraft#52: before, each + /// `Ranges::insert` copied every range found before it — the ~million disjoint values of the + /// fixture below ask for more copying than the timeout allows). + #[test] + fn many_out_of_line_values_plan_fast() { + let (ifds, values) = (512usize, 2048usize); + let ifd_len = 2 + values * 12 + 4; + let values_at = 8 + ifds * ifd_len; + let len = values_at + values * 8; + let mut tiff = vec![0u8; len]; + tiff[..8].copy_from_slice(b"II*\0\x08\0\0\0"); + for i in 0..ifds { + let at = 8 + i * ifd_len; + // {values} out-of-line strings (count 8 > the 4 inline bytes), then the next-IFD + // pointer; every value is a disjoint 8-byte range for the merge to handle. + tiff[at..at + 2].copy_from_slice(&(values as u16).to_le_bytes()); + for k in 0..values { + let e = at + 2 + k * 12; + tiff[e..e + 2].copy_from_slice(&t::MAKE.to_le_bytes()); + tiff[e + 2..e + 4].copy_from_slice(&2u16.to_le_bytes()); // ASCII: 8 bytes > inline — a span per entry + tiff[e + 4..e + 8].copy_from_slice(&8u32.to_le_bytes()); + tiff[e + 8..e + 12].copy_from_slice(&(values_at as u32 + (k * 8) as u32).to_le_bytes()); + } + let next = if i == ifds - 1 { 0u32 } else { (at + ifd_len) as u32 }; + tiff[at + ifd_len - 4..at + ifd_len].copy_from_slice(&next.to_le_bytes()); + } + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let mut loaded = Ranges::new(); + loaded.insert(0, len as u64); + let _ = tx.send(header_plan(&tiff, &loaded)); + }); + rx.recv_timeout(std::time::Duration::from_secs(20)).expect("header_plan took longer than 20 s"); + } + + /// A TIFF with a million one-byte strips listed in descending order and all byte counts 0: + /// the plan is built in one sort and merge, not one pass over all strips (or all holes) per + /// strip. + #[test] + fn many_strips_without_byte_counts_plan_fast() { + let n = 1_000_000u32; + let (offsets_at, counts_at) = (8 + 2 + 5 * 12 + 4, 8 + 2 + 5 * 12 + 4 + 4 * n); + let data = counts_at + 4 * n; + let mut tiff = b"II*\0\x08\0\0\0".to_vec(); + tiff.extend_from_slice(&5u16.to_le_bytes()); + let entry = |tiff: &mut Vec, tag: u16, typ: u16, count: u32, value: u32| { + tiff.extend_from_slice(&tag.to_le_bytes()); + tiff.extend_from_slice(&typ.to_le_bytes()); + tiff.extend_from_slice(&count.to_le_bytes()); + tiff.extend_from_slice(&value.to_le_bytes()); + }; + entry(&mut tiff, 256, 4, 1, 1); + entry(&mut tiff, 257, 4, 1, n); + entry(&mut tiff, 273, 4, n, offsets_at); + entry(&mut tiff, 278, 4, 1, 1); + entry(&mut tiff, 279, 4, n, counts_at); + tiff.extend_from_slice(&0u32.to_le_bytes()); + for i in (0..n).rev() { + tiff.extend_from_slice(&(data + i).to_le_bytes()); + } + tiff.resize(data as usize + n as usize, 0); + let len = tiff.len() as u64; + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let mut loaded = Ranges::new(); + loaded.insert(0, len); + let _ = tx.send(header_plan(&tiff, &loaded)); + }); + let plan = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("header_plan took longer than 20 s"); + assert!(!matches!(plan, HeaderPlan::Need(_)), "{plan:?}"); + } +}