From e9e1da3a58c1943efd003b9f557d4e6acd970b19 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 22:01:07 -0500 Subject: [PATCH] craft-raw: Canon CR3 (CRX), header plans and the remaining preview branches (#1070) * craft-raw: the file-format layer of camera raw files, with DNG and CRW A new crate for #587 and #778: bytes in, the sensor data out as a neutral RawImage (samples, CFA, levels, active area, crop, orientation, colour tags, opcode lists, profile tables), and back for DNG. One Cargo feature per format: dng (read and write), jxl (JPEG XL DNG), crw (through craft-crw). The base is LightCraft's crates/raw at 5ff567a4: dng, dngwrite, tiffraw, unpack, ljpeg, jxl, semantic, preview, the parsing half of opcodes, profile and gaintable, the model and probe. Demosaicing and everything after it stay with the develop pipeline. Ported from PhotoCraft's and FilmCraft's DNG readers, each with a test: the default crop's per-tag defaults, an unusable crop falling back to the active area, non-finite white levels, the white default of a linearized image, the AsShotWhiteXY range, 8-bit-only lossy JPEG, lossy tiles that must cover their part of the image, and the CinemaDNG frame tags. * craft-raw: Hasselblad 3FR and FFF (feature hasselblad) RawFormat::CfaTiff decodes: the DNG-layout reader plus what these files add. Uncompressed data or Hasselblad's lossless JPEG (selection value 8), RGGB when no CFAPattern is stated, an FFF's colour matrix from the 3FR header it embeds, and the 8-bit RGB preview handed on as a baseline TIFF. Moved from LightCraft's vendor/hasselblad.rs at 5ff567a4 with its tests. Ported from PhotoCraft's reader: four BlackLevel values without a BlackLevelRepeatDim are one per CFA site in a file that is not a DNG. * craft-raw: Minolta MRW, Epson ERF, Mamiya MEF and Kodak DCS520C Four formats only LightCraft decodes, one feature each (mrw, erf, mef, kodak), moved from its vendor modules at 5ff567a4 with their tests; the MRW block container comes from lightcraft-meta. src/shared.rs holds the level helpers the maker decoders share. ERF and MEF state no white balance: the estimate LightCraft made from the preview is develop-side work, so wb_multipliers is None for them here. * craft-raw: shared scan budget for tagged previews, tile-sized lossy JPEG budget, bound on Deflate tile padding Ported from storytold/lightcraft#3695. Candidate preview ranges are deduplicated and charged to one byte budget and one header-walk budget; a lossy JPEG decoded for a tile may hold no more samples than the tile rounded up to whole MCUs (and never more than Limits::max_lossy_tile_samples); a Deflate chunk whose nominal size is mostly padding outside the image is refused before it is inflated. * craft-raw: lossless JPEG sample bound per referenced byte; previews larger than the file are passed over Ported from storytold/lightcraft#3910 (the parts whose code is in craft-raw today). check_image refuses a lossless JPEG image that declares more than 32 samples per referenced byte, counted once per chunk that points at the bytes so identical tiles stored once still decode. The Hasselblad RGB preview search passes over an image whose pixel bytes exceed the file and still takes a smaller complete one. * craft-raw: test for the Deflate padding bound * craft-raw: Pentax PEF and Samsung SRW Moved from LightCraft crates/raw (vendor/pef.rs, vendor/srw.rs) at 5ff567a4. The Huffman difference decoder and bit reader that PEF shares with the Nikon, Panasonic and Sony codings become the huffman module. * craft-raw: Canon CR2, with sRAW and mRAW Moved from LightCraft crates/raw (vendor/cr2.rs, vendor/cr2/sraw.rs) at 5ff567a4. The white-balance presets a CR2 lists come back as data in ColorData::kelvin_presets (the ordering from LightCraft's kelvin_presets.rs; the temperature and tint arithmetic stays with the developer). * craft-raw: an sRAW preview larger than the file is refused before its buffer is reserved Ported from storytold/lightcraft#3910 (cr2::sraw::read_preview). * WIP craft-raw: Nikon NEF/NRW (does not compile yet: limits threading, develop() calls in tests) * craft-raw: Nikon NEF/NRW, with PhotoCraft's maker-note compression routing and 12-bit D4 white level * craft-raw: Panasonic RW2 / RWL / RAW, every raw format * craft-raw: RW2 format 8 refuses strips stating fewer bits than samples Ported from storytold/lightcraft#3910. * craft-raw: RW2 format 8 refuses samples above the declared bit depth Ported from storytold/photocraft#11838. * craft-raw: the RW2 format 8 robustness seed carries a valid stream Its random payload decoded only while out-of-depth samples were clamped; mutations now start from a file that decodes. * craft-raw: Fujifilm RAF, uncompressed and compressed, with lens corrections * craft-raw: compressed RAF stripes cap their capacity hint at the stripe's bits Ported from storytold/lightcraft#3910. * craft-raw: test RAF orientation and camera from the embedded JPEG's Exif * WIP craft-raw: Olympus ORF * craft-raw: RAF embedded preview, and its range from the header alone (bounded_preview) * craft-raw: Olympus ORF, the uncompressed layouts, with its maker-note preview and WB_GLevel * WIP craft-raw: Sony ARW * craft-raw: tiled lossless ARW decodes hold one group of tiles at a time Ported from storytold/lightcraft#3910. * craft-raw: dark 12-bit uncompressed ARWs keep Sony's 14-bit levels, and the DNG writer's depth holds the white level Ported from storytold/lightcraft#3429. * craft-raw: Sony ARW family, docs, seeds and the derivation records * WIP craft-raw: Canon CR3 * craft-raw: Canon CR3 (CRX) through craft-metadata's container, header plans, CR3 previews and the coverage records --------- Co-authored-by: radialmonster Co-authored-by: wnanbei Co-authored-by: lownlyrider --- ATTRIBUTION.md | 3 + Cargo.lock | 1 + NOTICE | 2 +- README.md | 5 +- crates/raw/Cargo.toml | 8 +- crates/raw/README.md | 7 + crates/raw/docs/coverage/adobe-cameras.tsv | 1450 +++++++++++++++++++ crates/raw/docs/coverage/coverage-sweep.tsv | 45 + crates/raw/docs/coverage/coverage.py | 192 +++ crates/raw/docs/coverage/coverage.tsv | 1448 ++++++++++++++++++ crates/raw/docs/coverage/evidence.tsv | 580 ++++++++ crates/raw/docs/coverage/usage-weights.tsv | 114 ++ crates/raw/docs/cr3.md | 243 ++++ crates/raw/src/cr3.rs | 778 ++++++++++ crates/raw/src/crx.rs | 1229 ++++++++++++++++ crates/raw/src/crx_wavelet.rs | 338 +++++ crates/raw/src/lib.rs | 11 + crates/raw/src/preview.rs | 167 ++- crates/raw/src/preview/tests.rs | 358 +++++ crates/raw/src/sparse.rs | 773 ++++++++++ 20 files changed, 7744 insertions(+), 8 deletions(-) create mode 100644 crates/raw/docs/coverage/adobe-cameras.tsv create mode 100644 crates/raw/docs/coverage/coverage-sweep.tsv create mode 100755 crates/raw/docs/coverage/coverage.py create mode 100644 crates/raw/docs/coverage/coverage.tsv create mode 100644 crates/raw/docs/coverage/evidence.tsv create mode 100644 crates/raw/docs/coverage/usage-weights.tsv create mode 100644 crates/raw/docs/cr3.md create mode 100644 crates/raw/src/cr3.rs create mode 100644 crates/raw/src/crx.rs create mode 100644 crates/raw/src/crx_wavelet.rs create mode 100644 crates/raw/src/sparse.rs 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:?}"); + } +}