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 <radial@gmail.com>
Co-authored-by: wnanbei <wnanbei@gmail.com>
Co-authored-by: lownlyrider <lownlyrider@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-11-11 22:01:07 -05:00
1 parent 838add7b8e
commit e9e1da3a58
20 files changed
+7744 -8

No files matched your search

+6 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "craft-raw"
description = "Camera raw files, the file-format layer: container parsing, decompression and the sensor data out as a neutral raw image with its levels, crop, orientation, colour tags and embedded previews. DNG (read and write), Hasselblad 3FR/FFF, MRW, ERF, MEF, Kodak DCS, Pentax PEF, Samsung SRW, Canon CR2, Nikon NEF/NRW, Panasonic RW2, Fujifilm RAF, Olympus ORF (uncompressed), Sony ARW and Canon CRW so far."
description = "Camera raw files, the file-format layer: container parsing, decompression and the sensor data out as a neutral raw image with its levels, crop, orientation, colour tags and embedded previews. DNG (read and write), Hasselblad 3FR/FFF, MRW, ERF, MEF, Kodak DCS, Pentax PEF, Samsung SRW, Canon CR2, Nikon NEF/NRW, Panasonic RW2, Fujifilm RAF, Olympus ORF (uncompressed), Sony ARW, Canon CR3 and Canon CRW so far."
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -11,7 +11,7 @@ repository.workspace = true
workspace = true
[features]
default = ["dng", "jxl", "crw", "hasselblad", "mrw", "erf", "mef", "kodak", "pef", "srw", "cr2", "nef", "rw2", "raf", "orf", "arw"]
default = ["dng", "jxl", "crw", "hasselblad", "mrw", "erf", "mef", "kodak", "pef", "srw", "cr2", "nef", "rw2", "raf", "orf", "arw", "cr3"]
## Adobe DNG, reading and writing: uncompressed, lossless JPEG, lossy JPEG, Deflate.
dng = ["dep:miniz_oxide", "dep:zune-jpeg", "dep:zune-core"]
## JPEG XL compressed DNGs (DNG 1.7, compression 52546) via jxl-oxide (pure Rust, MIT OR Apache-2.0).
@@ -42,12 +42,16 @@ raf = []
orf = []
## Sony ARW, SR2 and SRF: uncompressed, ARW2 (cRAW), lossless JPEG tiles, ARW6 (Compressed RAW 2, RDD 34 LLVC), the encrypted SR2SubIFD, in-file lens corrections.
arw = []
## Canon CR3: CRX lossless Bayer and C-RAW, through craft-metadata's CR3 container parser.
cr3 = ["dep:craft-metadata"]
## Canon CRW (CIFF) through craft-crw, the one CRW decoder.
crw = ["dep:craft-crw", "dep:chrono"]
[dependencies]
craft-tiff = { workspace = true }
craft-crw = { workspace = true, optional = true }
# The CR3 container (ISO base media boxes, CMT1-4, tracks, CMP1 / CDI1): the one CR3 parser.
craft-metadata = { workspace = true, optional = true }
serde = { workspace = true }
thiserror = { workspace = true }
rayon = { workspace = true }
+7
View File
@@ -59,6 +59,7 @@ One module and one Cargo feature per maker or container family. All are on by de
| `raf` | Fujifilm RAF | The RAF header, records and raw IFD; uncompressed Bayer and X-Trans (16-bit words, 12-bit LSB-first, 32-bit word packing); Fujifilm's lossless and lossy striped compression ([derivation](docs/raf-compression.md), [corpus](docs/raf-corpus.sha256)); black, as-shot white balance and, for bodies without colour matrices, the camera's neutral multipliers for Illuminant A and D65 (`ColorData::kelvin_presets`, a neutral pair); orientation, make and model from the embedded JPEG's Exif; the in-file distortion and vignetting corrections as opcodes; the embedded preview, also by range from the header alone (`bounded_preview`, `embedded_preview_from_range`). The pre-2010 FinePix layout without a raw IFD is refused |
| `orf` | Olympus / OM System ORF | Uncompressed: 16-bit words (12-bit values in the top bits shifted down), 12-bit packed in 32-bit words (XZ-2), 12-bit two-field (older compacts), 16-byte blocks of ten 12-bit samples (E-300, E-330, E-500, high-resolution modes); CFA from Exif `CFAPattern`; black, as-shot white balance (relative to `WB_GLevel`) and crop from the maker note's ImageProcessing, or from the main note on bodies without it; the CameraSettings preview. Compressed ORF is refused ([why](docs/orf-compression.md)); its preview still reads |
| `arw` | Sony ARW (and the SR2 / SRF containers) | Uncompressed 16-bit words and packed 12-bit, ARW2 (cRAW, compression 32767), lossless JPEG tiles of 2 × 2 cells (compression 7) and the linear YCbCr of downsized lossless files, ARW6 "Compressed RAW 2" (compression 32766, SMPTE RDD 34 LLVC; [derivation](docs/arw6-compression.md)); the encrypted SR2SubIFD (every key) for black, white balance, crop and the white-balance presets at their temperatures (`ColorData::kelvin_presets`); crop from Sony's crop tags, the DNG-style default crop or the recorded image size; the in-file distortion, shading and lateral CA tables decoded with their provenance (`RawImage::lens_metadata`), and the validated distortion as a `WarpRectilinear` opcode ([7RM4A and others](docs/sony-lens-corrections.md), [7RM6](docs/sony-ilce7rm6-lens-corrections.md)); the Dynamic Range Optimizer flag of the embedded preview (`embedded_preview_dynamic_range_optimized`) |
| `cr3` | Canon CR3 | CRX lossless Bayer and C-RAW (lossy) from the main raw track (`CMP1`), through craft-metadata's CR3 container parser; geometry from `CDI1`/`IAD1` and the maker note's `AspectInfo`; as-shot white balance, black and white from `ColorData` (static or the timed `CTMD` maker note), with the stated white extended to a saturation plateau above it (and BaselineExposure compensated); orientation, make and model from `CMT1`; the full-size JPEG track or the `PRVW` / `THMB` boxes as the embedded preview, and the preview colour space from the Canon maker note ([record](docs/cr3.md)). Unverified marker families are refused |
| `mrw` | Minolta / Konica Minolta MRW | The `\0MRM` block container (`PRD`, `WBG`, `TTW`), 12-bit packed and 16-bit word mosaics, as-shot white balance from `WBG`, white from the saturation plateau |
| `erf` | Epson ERF (R-D1, R-D1s, R-D1x) | 12-bit samples in 16-byte blocks, black levels from the maker note, the measured picture origin. No white balance (the file states none) |
| `mef` | Mamiya MEF (ZD) | Uncompressed 12-bit packed; default crop from the RGB preview's measured window. No white balance (the file states none) |
@@ -172,6 +173,12 @@ Left as LightCraft has it, with the reason:
LightCraft.
- Exif-only inputs a decoder needs are read from the Exif IFD directly: the 7RM6 lens tables are chosen by
`LensModel` and `FocalLength`.
- Canon CR3 is LightCraft's reader. PhotoCraft has no CR3 sensor decoder (it shows the JPEG track or `PRVW`, and
reads `CMT1` orientation), which this crate also does. The CR3 container parser is craft-metadata's `cr3` module,
the one CR3 parser in this repository.
- `sparse::header_plan` / `preview_plan` say which byte ranges of a file a header probe or preview read needs, so a
caller on a slow drive can skip the sample data. `docs/coverage/` holds LightCraft's camera-coverage records
(`coverage.py --check` regenerates `coverage.tsv` from the evidence and Adobe's list).
- Lossy JPEG is accepted for CFA data (one grey component) as in PhotoCraft; FilmCraft refuses it
because DNG 1.4 allows the coding for LinearRaw only.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,45 @@
# CC0 raw.pixls.us camera-coverage check, 2026-10-12; main 25ef65c8. File SHA-256 matches the public listing.
# FNV-1a-64 pins LightCraft's own complete decoded samples, row-major little-endian, including masked borders.
# Default renders at up to 1200 px; Pearson r on Lab L* at 200 px after orientation/aspect alignment, threshold 0.6.
# Hasselblad PreviewTIFF references check image structure only. Own-matrix DNG tone remains subject to issue #1277.
maker camera id file url sha256 width height cpp samples fnv1a64 luma_r reference notes
Apple iPhone 6s Plus 915 coverage-915.dng https://raw.pixls.us/getfile.php/915/nice/Apple%20-%20iPhone%206s%20Plus%20-%2016bit%20%284:3%29.DNG 845fee4f09c832af3728778737f5ae9b99124debb821b6e2066b94aed1daa91c 4032 3024 1 u16 17e4d3c620d593c9 0.9712 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -10.109
Apple iPhone 7 Plus 939 coverage-939.dng https://raw.pixls.us/getfile.php/939/nice/Apple%20-%20iPhone%207%20Plus%20-%2016bit%20%284:3%29.DNG e8c7ebd8f22f7281d165496b91c8fea8a8e79c8113172fb499e1d66d6045332e 4032 3024 1 u16 d0095a8b553f0f8a 0.9904 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -11.913
Apple iPhone XS 3372 coverage-3372.dng https://raw.pixls.us/getfile.php/3372/nice/Apple%20-%20iPhone%20XS%20-%2016bit%20%284:3%29.dng 50bc64ee8cfbbfb6c0531e29ab428ff985506a16fe2d1d2e74656f21b54505f6 4032 3024 1 u16 a8e819ac4e5beb2a 0.9805 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -14.751
DJI DJI Mavic 2 Pro (Hasselblad L1D-20c) 3009 coverage-3009.dng https://raw.pixls.us/getfile.php/3009/nice/Hasselblad%20-%20L1D-20c%20-%2016bit%20%283:2%29.DNG 7afecae0db80d832941eeeb3c0eb2dc03f5696637b6e78b3c592e8c002dce4ec 5568 3648 1 u16 b207313fa7dee601 0.9781 embedded JPEG 16bit (3:2); comparison Normal/crop-render; mean dL -8.41
DJI DJI Mavic 3 (Hasselblad L2D-20c) 5140 coverage-5140.dng https://raw.pixls.us/getfile.php/5140/nice/Hasselblad%20-%20L2D-20c%20-%2016bit%20%284:3%29.DNG dbe98cb202f884ff11e0144899613b26d33cf9cddd9f1a62226f4352397f18e4 5376 3956 1 u16 2c78ceb8cb893ff1 0.8618 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL 0.418
DJI DJI Mavic 4 Pro (DJI FC9287) 8041 coverage-8041.dng https://raw.pixls.us/getfile.php/8041/nice/DJI%20-%20FC9287%20-%2016bit%20%2816:9%29.DNG 901b7970f02f7197c782f111363feef02108f75811342d8cc4a2b66757bb7af0 8192 4608 1 u16 d1babb4646097d6f 0.6801 embedded JPEG 16bit (16:9); reference has a different distortion/crop; comparison Normal/crop-render; mean dL 2.583
DJI DJI Mini 2 4785 coverage-4785.dng https://raw.pixls.us/getfile.php/4785/nice/DJI%20-%20FC7303%20-%2016bit%20%284:3%29.DNG 21a49e63966f82391ca1188c7559c3acc01a35a89a70194fda4bf44cbd64a851 4000 3000 1 u16 d1e66e08f335b55b 0.9328 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -9.016
DJI DJI Mini 2 4898 coverage-4898.dng https://raw.pixls.us/getfile.php/4898/nice/DJI%20-%20FC7303%20-%2016bit%20%2816:9%29.DNG fb4f294e12cab7b507fd0b7c07f24e5d13e7c0dd127b534ce12c48d644c9363d 4000 2250 1 u16 acaab4fe42d3dc58 0.9716 embedded JPEG 16bit (16:9); comparison Normal/crop-render; mean dL -8.153
Google Pixel 4 XL 3502 coverage-3502.dng https://raw.pixls.us/getfile.php/3502/nice/Google%20-%20Pixel%204%20XL%20-%2016bit%20%284:3%29.dng 0c03a6c93b4b8593cd0e4426166e6389710007238b82eb23861ef191322f0c54 3700 2774 1 u16 d121f233fdbaff2c 0.9143 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -0.351
Google Pixel 4a Rear Wide Camera 4176 coverage-4176.dng https://raw.pixls.us/getfile.php/4176/nice/Google%20-%20Pixel%204a%20-%2016bit%20%284:3%29.dng 11b38186ee3455376291bcdb3049728a3f9dcd94b9e82ff76855d9f3430e44be 4032 3024 1 u16 ac056154d440cb2d 0.9515 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -2.361
Google Pixel 6 Pro Rear Main Camera 5961 coverage-5961.dng https://raw.pixls.us/getfile.php/5961/nice/Google%20-%20Pixel%206%20Pro%20-%2016bit%20%284:3%29.dng 3a859e917a91698a3e15ce3b3721a48a1fb1296873e0697d3bf7c2793297200d 4080 3072 1 u16 96631250f12729ad 0.944 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -0.402
Google Pixel 8 Pro Wide Camera* 7751 coverage-7751.dng https://raw.pixls.us/getfile.php/7751/nice/Google%20-%20Pixel%208%20Pro%20-%2016bit%20%284:3%29.dng 45420c595401547cca950ae58d552bc82b32d31ce1d58c082df33004016197f5 8160 6144 1 u16 5e30a4d3bfc914b3 0.9485 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -5.099
Hasselblad CFV-50c 7217 coverage-7217.3fr https://raw.pixls.us/getfile.php/7217/nice/Hasselblad%20-%20CFV-50c%20-%2016bit%20%284:3%29.3FR e27ff177519298fe7bae64506b975190789dc0e6ad70bea58be7c78fd094eae0 8374 6304 1 u16 14e60b95775930f2 0.9984 exiftool:PreviewTIFF 16bit (4:3); comparison Normal/crop-render; mean dL -14.199
Hasselblad CFV-50c 7218 coverage-7218.fff https://raw.pixls.us/getfile.php/7218/nice/Hasselblad%20-%20CFV-50c%20-%2016bit%20%284:3%29.fff c8d78912ae725a59acff42a4b4731350bbddd4a600423f319579261bd41da932 8374 6304 1 u16 db63ffcd176381ee 0.9443 exiftool:PreviewTIFF 16bit (4:3); TIFF preview is not display-ready; neutral-matrix colour remains unverified; comparison Normal/crop-render; mean dL 28.828
Hasselblad H5D-40 1640 coverage-1640.fff https://raw.pixls.us/getfile.php/1640/nice/Hasselblad%20-%20H5D-40%20-%2016bit%20%284:3%29.fff 209d464e13702de5f4fdb830ac26ac140c98a8d0dc3e81c5ba61d93677330832 7410 5586 1 u16 1d34f6f4e45b5eab 0.9574 exiftool:PreviewTIFF 16bit (4:3); TIFF preview is not display-ready; neutral-matrix colour remains unverified; comparison Normal/crop-render; mean dL 29.541
Hasselblad H5D-50c 1639 coverage-1639.fff https://raw.pixls.us/getfile.php/1639/nice/Hasselblad%20-%20H5D-50c%20-%2016bit%20%284:3%29.fff 1aad9319fcbab81b851a74c3eeaa2a07fc5013bf2c4e272fd31d6419b6b9adb0 8374 6304 1 u16 02a189d2a6b78d4e 0.9453 exiftool:PreviewTIFF 16bit (4:3); TIFF preview is not display-ready; neutral-matrix colour remains unverified; comparison Normal/crop-render; mean dL 28.943
Hasselblad X1D 2058 coverage-2058.3fr https://raw.pixls.us/getfile.php/2058/nice/Hasselblad%20-%20X1D%20-%2016bit%20%284:3%29.3FR 395acb2c706dc10270c58ec261e42a5e36f6ac8c39d2a767e86e83648aa318a3 8384 6304 1 u16 0b480efe4d2ed7c2 0.9753 exiftool:PreviewTIFF 16bit (4:3); comparison Normal/crop-render; mean dL -9.059
Hasselblad X1D II 50C 3874 coverage-3874.fff https://raw.pixls.us/getfile.php/3874/nice/Hasselblad%20-%20X1D%20II%2050C%20-%2016bit%20%284:3%29.fff 8c2451b3478fb529b4e9bd51ed3267999ce26eb1fcb5c3b93c3ae7bc56267794 8384 6304 1 u16 c7372897d70da8a8 0.9436 exiftool:PreviewTIFF 16bit (4:3); TIFF preview is not display-ready; neutral-matrix colour remains unverified; comparison Normal/crop-render; mean dL 22.474
Hasselblad X1D II 50C 3941 coverage-3941.3fr https://raw.pixls.us/getfile.php/3941/nice/Hasselblad%20-%20X1D%20II%2050C%20-%2016bit%20%284:3%29.3FR 809d498263d6f07cbaa4b9a5be97933057b4ab21e7243bb7afbe3862f3b48b47 8384 6304 1 u16 3c3a77bf67197196 0.9982 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -11.176
Hasselblad X2D 100C 6568 coverage-6568.fff https://raw.pixls.us/getfile.php/6568/nice/Hasselblad%20-%20X2D%20100C%20-%2016bit%20%284:3%29.fff 44977fdaa3063eacbb2ac416dfb531bb4e3b96f3745d073dc49fb1ebcd78bedc 11904 8842 1 u16 849906d491fba55a 0.9196 exiftool:PreviewTIFF 16bit (4:3); TIFF preview is not display-ready; neutral-matrix colour remains unverified; comparison Normal/crop-render; mean dL 26.781
Hasselblad X2D 100C 6565 coverage-6565.3fr https://raw.pixls.us/getfile.php/6565/nice/Hasselblad%20-%20X2D%20100C%20-%2016bit%20%284:3%29.3FR dcc5a4abe3498e6f25e89bb491995fd12c3b669d9277c71b45b49210d3e56280 11904 8842 1 u16 c07a267c0bc60a5f 0.9962 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -2.632
Nikon D6 4726 coverage-4726.nef https://raw.pixls.us/getfile.php/4726/nice/Nikon%20-%20D6%20-%2012bit%2012bit%20compressed%20%28Lossy%20%28type%202%29%29%20%283:2%29.NEF 3a098e704d0e1af97efc37541b43a37e5b3457d2f4e7828c0d8d9af37332f233 5584 3728 1 u16 272425c271bb0d76 0.9557 embedded JPEG 12bit 12bit compressed (Lossy (type 2)) (3:2); comparison Normal/crop-render; mean dL 0.037
Nikon D6 4720 coverage-4720.nef https://raw.pixls.us/getfile.php/4720/nice/Nikon%20-%20D6%20-%2014bit%2014bit%20compressed%20%28Lossless%29%20%283:2%29.NEF 827bcfff8a3cd86278382c353e4542faab52cf12c365540f30753bb4c6fabc0f 5584 3728 1 u16 1d6a9f10a502fb14 0.9581 embedded JPEG 14bit 14bit compressed (Lossless) (3:2); comparison Normal/crop-render; mean dL 0.048
Nikon D6 4721 coverage-4721.nef https://raw.pixls.us/getfile.php/4721/nice/Nikon%20-%20D6%20-%2014bit%2014bit%20compressed%20%28Lossy%20%28type%202%29%29%20%283:2%29.NEF 081d5dbc1d6c97935020d33ffa486dcaa8d4564cbde280cfadcf26ff61da2838 5584 3728 1 u16 2d6c8af38e4a0ed4 0.9592 embedded JPEG 14bit 14bit compressed (Lossy (type 2)) (3:2); comparison Normal/crop-render; mean dL 0.043
Nikon Z 6 3584 coverage-3584.nef https://raw.pixls.us/getfile.php/3584/nice/Nikon%20-%20Z%206%20-%2012bit%2012bit%20compressed%20%283:2%29.NEF 3cbd9acf9e9bfd7d0c62c46436109556a101f7535c5fd25f86dad6ea61bf3886 6064 4040 1 u16 a63b22e83bb7c82f 0.8749 embedded JPEG 12bit 12bit compressed (3:2); comparison Normal/crop-render; mean dL 0.327
Nikon Z 6 3581 coverage-3581.nef https://raw.pixls.us/getfile.php/3581/nice/Nikon%20-%20Z%206%20-%2014bit%2014bit%20compressed%20%283:2%29.NEF 8ca94584578b35a422744c1a610174ce37df0691588edfdecaec3111b3fbbb02 6064 4040 1 u16 1e23d74a11c01a97 0.8752 embedded JPEG 14bit 14bit compressed (3:2); comparison Normal/crop-render; mean dL 0.353
Nikon Z 6 3582 coverage-3582.nef https://raw.pixls.us/getfile.php/3582/nice/Nikon%20-%20Z%206%20-%2014bit%2014bit%20lossless%20compressed%20%283:2%29.NEF c079345fc93f53a4f0d322f8ddaae505f920c36015f25b58befccaa61db1af31 6064 4040 1 u16 0a0bc73568e125e0 0.8755 embedded JPEG 14bit 14bit lossless compressed (3:2); comparison Normal/crop-render; mean dL 0.464
Nikon Z 7 2792 coverage-2792.nef https://raw.pixls.us/getfile.php/2792/nice/Nikon%20-%20Z%207%20-%2012bit%2012bit%20compressed%20%283:2%29.NEF c861546fbcf5826d5eb26d223e2143fa32968e89f7edbc5bb10c283ea5461ded 8288 5520 1 u16 c989690cb3e19ef9 0.7417 embedded JPEG 12bit 12bit compressed (3:2); comparison Normal/crop-render; mean dL 2.417
Nikon Z 7 2789 coverage-2789.nef https://raw.pixls.us/getfile.php/2789/nice/Nikon%20-%20Z%207%20-%2012bit%2012bit%20compressed%20%283:2%29.NEF e54a6c4264bf685214f538036fd9d39088e510b2bfedf288e568f0db1897880e 8288 5520 1 u16 627867ac6a8dd4ad 0.7412 embedded JPEG 12bit 12bit compressed (3:2); comparison Normal/crop-render; mean dL 2.299
Nikon Z 7 2790 coverage-2790.nef https://raw.pixls.us/getfile.php/2790/nice/Nikon%20-%20Z%207%20-%2012bit%2012bit%20compressed%20%283:2%29.NEF f0e756dbfd00f3b71cb3d8aa5b9e2e1be32f1052a6c4380e09ebc7c5e7748114 8288 5520 1 u16 8a52e6f3d8ff64dd 0.7389 embedded JPEG 12bit 12bit compressed (3:2); comparison Normal/crop-render; mean dL 2.585
Nikon Z 8 6617 coverage-6617.nef https://raw.pixls.us/getfile.php/6617/nice/Nikon%20-%20Z%208%20-%208bit%208bit%20compressed%20%283:2%29.NEF c13a8675294456b9c979e6c1e556951654f35377b2870af2874bfb556f21ce40 8280 5520 1 u16 716e1158bab28b76 0.9563 embedded JPEG 8bit 8bit compressed (3:2); comparison Normal/crop-render; mean dL 2.044
Parrot Anafi 2912 coverage-2912.dng https://raw.pixls.us/getfile.php/2912/nice/Parrot%20-%20Anafi%20-%2016bit%20%284:3%29.DNG a257a296047ff2d396109fd2e6947fa36aa7986695b6815f83979d59e98a84e6 5344 4016 1 u16 07a188c932a25a31 0.9895 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -3.924
Samsung Galaxy S21 Ultra Rear Main Camera 6728 coverage-6728.dng https://raw.pixls.us/getfile.php/6728/nice/Samsung%20-%20Galaxy%20S21%20Ultra%20-%204:3.dng 5baeb5f0c4a125647df03b82c2d38b0f02784393bd39eff1a5b23d8f5308dc5e 4000 3000 3 u16 16c5d6faed83390d 0.9912 embedded JPEG 4:3; comparison Rotate180/crop-render; mean dL -10.83
Samsung Galaxy S22 Ultra Rear Wide Camera 6261 coverage-6261.dng https://raw.pixls.us/getfile.php/6261/nice/Samsung%20-%20Galaxy%20S22%20Ulra%20-%204:3.dng 53e7f83a94c965495ba32d3ea6021ef08ed02244ce64d111d1329bbd01fd5581 4000 3000 3 u16 7cbe612adc5575b4 0.9739 embedded JPEG 4:3; comparison Normal/crop-render; mean dL -20.487
Samsung Galaxy S23 Rear Wide Camera 6697 coverage-6697.dng https://raw.pixls.us/getfile.php/6697/nice/Samsung%20-%20Galaxy%20S23%20-%204:3.dng 1c04574f32b5a139922f075291c52018b89d9981ee155a16958a231c1fdbb067 8160 6120 3 u16 b3da1535e2e31e9f 0.9418 embedded JPEG 4:3; comparison Normal/crop-render; mean dL -6.585
Samsung Galaxy S7 Edge 1297 coverage-1297.dng https://raw.pixls.us/getfile.php/1297/nice/Samsung%20-%20G935F%20-%2016bit%20%2816:9%29.dng 843617c8b572320b7badfb4ac2eda19479f5804a1dabb909c861bde23ed30561 4032 2268 1 u16 7e99d7da67022e5a 0.8677 exiftool:PreviewTIFF 16bit (16:9); comparison Normal/crop-render; mean dL 5.772
Samsung Galaxy S8 2645 coverage-2645.dng https://raw.pixls.us/getfile.php/2645/nice/Samsung%20-%20SM-G950F%20-%2016bit%20%284:3%29.dng 6abdd353094964eaadddf69bba3a0d551ecce1bc0ed013a434691df4f64dcbf2 4032 3024 1 u16 b70ca9a743283754 0.9884 exiftool:PreviewTIFF 16bit (4:3); comparison Normal/crop-render; mean dL -4.112
Samsung Galaxy S8+ 2310 coverage-2310.dng https://raw.pixls.us/getfile.php/2310/nice/Samsung%20-%20SM-G955F%20-%2016bit%20%284:3%29.dng 3cf5da4331d062bc6b31e0e8f5b7ff2cd21107de4262d79a2667f670516e8e8c 4032 3024 1 u16 c3a20523ad3b18cf 0.986 exiftool:PreviewTIFF 16bit (4:3); comparison Normal/crop-render; mean dL 2.437
Sigma fp 7273 coverage-7273.dng https://raw.pixls.us/getfile.php/7273/nice/Sigma%20-%20fp%20-%2014bit%20%283:2%29.DNG 0844bcf23a49e21b5c2f8489de55ce38fb7b594e3ed337d6b9af2dd9341354e9 6064 4042 1 u16 f7b92f1904d6cc31 0.9754 embedded JPEG 14bit (3:2); comparison Normal/crop-render; mean dL 7.1
Sigma fp L 6949 coverage-6949.dng https://raw.pixls.us/getfile.php/6949/nice/Sigma%20-%20fp%20L%20-%2014bit%20%283:2%29.DNG 8e7229f28be30bfa45289940eac04194af2b3474f6db286e4fa4070c145a0a70 9536 6344 1 u16 6896f73ec6b49231 0.9998 embedded JPEG 14bit (3:2); comparison Normal/crop-render; mean dL 1.597
1 # CC0 raw.pixls.us camera-coverage check, 2026-10-12; main 25ef65c8. File SHA-256 matches the public listing.
2 # FNV-1a-64 pins LightCraft's own complete decoded samples, row-major little-endian, including masked borders.
3 # Default renders at up to 1200 px; Pearson r on Lab L* at 200 px after orientation/aspect alignment, threshold 0.6.
4 # Hasselblad PreviewTIFF references check image structure only. Own-matrix DNG tone remains subject to issue #1277.
5 maker camera id file url sha256 width height cpp samples fnv1a64 luma_r reference notes
6 Apple iPhone 6s Plus 915 coverage-915.dng https://raw.pixls.us/getfile.php/915/nice/Apple%20-%20iPhone%206s%20Plus%20-%2016bit%20%284:3%29.DNG 845fee4f09c832af3728778737f5ae9b99124debb821b6e2066b94aed1daa91c 4032 3024 1 u16 17e4d3c620d593c9 0.9712 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -10.109
7 Apple iPhone 7 Plus 939 coverage-939.dng https://raw.pixls.us/getfile.php/939/nice/Apple%20-%20iPhone%207%20Plus%20-%2016bit%20%284:3%29.DNG e8c7ebd8f22f7281d165496b91c8fea8a8e79c8113172fb499e1d66d6045332e 4032 3024 1 u16 d0095a8b553f0f8a 0.9904 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -11.913
8 Apple iPhone XS 3372 coverage-3372.dng https://raw.pixls.us/getfile.php/3372/nice/Apple%20-%20iPhone%20XS%20-%2016bit%20%284:3%29.dng 50bc64ee8cfbbfb6c0531e29ab428ff985506a16fe2d1d2e74656f21b54505f6 4032 3024 1 u16 a8e819ac4e5beb2a 0.9805 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -14.751
9 DJI DJI Mavic 2 Pro (Hasselblad L1D-20c) 3009 coverage-3009.dng https://raw.pixls.us/getfile.php/3009/nice/Hasselblad%20-%20L1D-20c%20-%2016bit%20%283:2%29.DNG 7afecae0db80d832941eeeb3c0eb2dc03f5696637b6e78b3c592e8c002dce4ec 5568 3648 1 u16 b207313fa7dee601 0.9781 embedded JPEG 16bit (3:2); comparison Normal/crop-render; mean dL -8.41
10 DJI DJI Mavic 3 (Hasselblad L2D-20c) 5140 coverage-5140.dng https://raw.pixls.us/getfile.php/5140/nice/Hasselblad%20-%20L2D-20c%20-%2016bit%20%284:3%29.DNG dbe98cb202f884ff11e0144899613b26d33cf9cddd9f1a62226f4352397f18e4 5376 3956 1 u16 2c78ceb8cb893ff1 0.8618 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL 0.418
11 DJI DJI Mavic 4 Pro (DJI FC9287) 8041 coverage-8041.dng https://raw.pixls.us/getfile.php/8041/nice/DJI%20-%20FC9287%20-%2016bit%20%2816:9%29.DNG 901b7970f02f7197c782f111363feef02108f75811342d8cc4a2b66757bb7af0 8192 4608 1 u16 d1babb4646097d6f 0.6801 embedded JPEG 16bit (16:9); reference has a different distortion/crop; comparison Normal/crop-render; mean dL 2.583
12 DJI DJI Mini 2 4785 coverage-4785.dng https://raw.pixls.us/getfile.php/4785/nice/DJI%20-%20FC7303%20-%2016bit%20%284:3%29.DNG 21a49e63966f82391ca1188c7559c3acc01a35a89a70194fda4bf44cbd64a851 4000 3000 1 u16 d1e66e08f335b55b 0.9328 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -9.016
13 DJI DJI Mini 2 4898 coverage-4898.dng https://raw.pixls.us/getfile.php/4898/nice/DJI%20-%20FC7303%20-%2016bit%20%2816:9%29.DNG fb4f294e12cab7b507fd0b7c07f24e5d13e7c0dd127b534ce12c48d644c9363d 4000 2250 1 u16 acaab4fe42d3dc58 0.9716 embedded JPEG 16bit (16:9); comparison Normal/crop-render; mean dL -8.153
14 Google Pixel 4 XL 3502 coverage-3502.dng https://raw.pixls.us/getfile.php/3502/nice/Google%20-%20Pixel%204%20XL%20-%2016bit%20%284:3%29.dng 0c03a6c93b4b8593cd0e4426166e6389710007238b82eb23861ef191322f0c54 3700 2774 1 u16 d121f233fdbaff2c 0.9143 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -0.351
15 Google Pixel 4a Rear Wide Camera 4176 coverage-4176.dng https://raw.pixls.us/getfile.php/4176/nice/Google%20-%20Pixel%204a%20-%2016bit%20%284:3%29.dng 11b38186ee3455376291bcdb3049728a3f9dcd94b9e82ff76855d9f3430e44be 4032 3024 1 u16 ac056154d440cb2d 0.9515 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -2.361
16 Google Pixel 6 Pro Rear Main Camera 5961 coverage-5961.dng https://raw.pixls.us/getfile.php/5961/nice/Google%20-%20Pixel%206%20Pro%20-%2016bit%20%284:3%29.dng 3a859e917a91698a3e15ce3b3721a48a1fb1296873e0697d3bf7c2793297200d 4080 3072 1 u16 96631250f12729ad 0.944 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -0.402
17 Google Pixel 8 Pro Wide Camera* 7751 coverage-7751.dng https://raw.pixls.us/getfile.php/7751/nice/Google%20-%20Pixel%208%20Pro%20-%2016bit%20%284:3%29.dng 45420c595401547cca950ae58d552bc82b32d31ce1d58c082df33004016197f5 8160 6144 1 u16 5e30a4d3bfc914b3 0.9485 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -5.099
18 Hasselblad CFV-50c 7217 coverage-7217.3fr https://raw.pixls.us/getfile.php/7217/nice/Hasselblad%20-%20CFV-50c%20-%2016bit%20%284:3%29.3FR e27ff177519298fe7bae64506b975190789dc0e6ad70bea58be7c78fd094eae0 8374 6304 1 u16 14e60b95775930f2 0.9984 exiftool:PreviewTIFF 16bit (4:3); comparison Normal/crop-render; mean dL -14.199
19 Hasselblad CFV-50c 7218 coverage-7218.fff https://raw.pixls.us/getfile.php/7218/nice/Hasselblad%20-%20CFV-50c%20-%2016bit%20%284:3%29.fff c8d78912ae725a59acff42a4b4731350bbddd4a600423f319579261bd41da932 8374 6304 1 u16 db63ffcd176381ee 0.9443 exiftool:PreviewTIFF 16bit (4:3); TIFF preview is not display-ready; neutral-matrix colour remains unverified; comparison Normal/crop-render; mean dL 28.828
20 Hasselblad H5D-40 1640 coverage-1640.fff https://raw.pixls.us/getfile.php/1640/nice/Hasselblad%20-%20H5D-40%20-%2016bit%20%284:3%29.fff 209d464e13702de5f4fdb830ac26ac140c98a8d0dc3e81c5ba61d93677330832 7410 5586 1 u16 1d34f6f4e45b5eab 0.9574 exiftool:PreviewTIFF 16bit (4:3); TIFF preview is not display-ready; neutral-matrix colour remains unverified; comparison Normal/crop-render; mean dL 29.541
21 Hasselblad H5D-50c 1639 coverage-1639.fff https://raw.pixls.us/getfile.php/1639/nice/Hasselblad%20-%20H5D-50c%20-%2016bit%20%284:3%29.fff 1aad9319fcbab81b851a74c3eeaa2a07fc5013bf2c4e272fd31d6419b6b9adb0 8374 6304 1 u16 02a189d2a6b78d4e 0.9453 exiftool:PreviewTIFF 16bit (4:3); TIFF preview is not display-ready; neutral-matrix colour remains unverified; comparison Normal/crop-render; mean dL 28.943
22 Hasselblad X1D 2058 coverage-2058.3fr https://raw.pixls.us/getfile.php/2058/nice/Hasselblad%20-%20X1D%20-%2016bit%20%284:3%29.3FR 395acb2c706dc10270c58ec261e42a5e36f6ac8c39d2a767e86e83648aa318a3 8384 6304 1 u16 0b480efe4d2ed7c2 0.9753 exiftool:PreviewTIFF 16bit (4:3); comparison Normal/crop-render; mean dL -9.059
23 Hasselblad X1D II 50C 3874 coverage-3874.fff https://raw.pixls.us/getfile.php/3874/nice/Hasselblad%20-%20X1D%20II%2050C%20-%2016bit%20%284:3%29.fff 8c2451b3478fb529b4e9bd51ed3267999ce26eb1fcb5c3b93c3ae7bc56267794 8384 6304 1 u16 c7372897d70da8a8 0.9436 exiftool:PreviewTIFF 16bit (4:3); TIFF preview is not display-ready; neutral-matrix colour remains unverified; comparison Normal/crop-render; mean dL 22.474
24 Hasselblad X1D II 50C 3941 coverage-3941.3fr https://raw.pixls.us/getfile.php/3941/nice/Hasselblad%20-%20X1D%20II%2050C%20-%2016bit%20%284:3%29.3FR 809d498263d6f07cbaa4b9a5be97933057b4ab21e7243bb7afbe3862f3b48b47 8384 6304 1 u16 3c3a77bf67197196 0.9982 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -11.176
25 Hasselblad X2D 100C 6568 coverage-6568.fff https://raw.pixls.us/getfile.php/6568/nice/Hasselblad%20-%20X2D%20100C%20-%2016bit%20%284:3%29.fff 44977fdaa3063eacbb2ac416dfb531bb4e3b96f3745d073dc49fb1ebcd78bedc 11904 8842 1 u16 849906d491fba55a 0.9196 exiftool:PreviewTIFF 16bit (4:3); TIFF preview is not display-ready; neutral-matrix colour remains unverified; comparison Normal/crop-render; mean dL 26.781
26 Hasselblad X2D 100C 6565 coverage-6565.3fr https://raw.pixls.us/getfile.php/6565/nice/Hasselblad%20-%20X2D%20100C%20-%2016bit%20%284:3%29.3FR dcc5a4abe3498e6f25e89bb491995fd12c3b669d9277c71b45b49210d3e56280 11904 8842 1 u16 c07a267c0bc60a5f 0.9962 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -2.632
27 Nikon D6 4726 coverage-4726.nef https://raw.pixls.us/getfile.php/4726/nice/Nikon%20-%20D6%20-%2012bit%2012bit%20compressed%20%28Lossy%20%28type%202%29%29%20%283:2%29.NEF 3a098e704d0e1af97efc37541b43a37e5b3457d2f4e7828c0d8d9af37332f233 5584 3728 1 u16 272425c271bb0d76 0.9557 embedded JPEG 12bit 12bit compressed (Lossy (type 2)) (3:2); comparison Normal/crop-render; mean dL 0.037
28 Nikon D6 4720 coverage-4720.nef https://raw.pixls.us/getfile.php/4720/nice/Nikon%20-%20D6%20-%2014bit%2014bit%20compressed%20%28Lossless%29%20%283:2%29.NEF 827bcfff8a3cd86278382c353e4542faab52cf12c365540f30753bb4c6fabc0f 5584 3728 1 u16 1d6a9f10a502fb14 0.9581 embedded JPEG 14bit 14bit compressed (Lossless) (3:2); comparison Normal/crop-render; mean dL 0.048
29 Nikon D6 4721 coverage-4721.nef https://raw.pixls.us/getfile.php/4721/nice/Nikon%20-%20D6%20-%2014bit%2014bit%20compressed%20%28Lossy%20%28type%202%29%29%20%283:2%29.NEF 081d5dbc1d6c97935020d33ffa486dcaa8d4564cbde280cfadcf26ff61da2838 5584 3728 1 u16 2d6c8af38e4a0ed4 0.9592 embedded JPEG 14bit 14bit compressed (Lossy (type 2)) (3:2); comparison Normal/crop-render; mean dL 0.043
30 Nikon Z 6 3584 coverage-3584.nef https://raw.pixls.us/getfile.php/3584/nice/Nikon%20-%20Z%206%20-%2012bit%2012bit%20compressed%20%283:2%29.NEF 3cbd9acf9e9bfd7d0c62c46436109556a101f7535c5fd25f86dad6ea61bf3886 6064 4040 1 u16 a63b22e83bb7c82f 0.8749 embedded JPEG 12bit 12bit compressed (3:2); comparison Normal/crop-render; mean dL 0.327
31 Nikon Z 6 3581 coverage-3581.nef https://raw.pixls.us/getfile.php/3581/nice/Nikon%20-%20Z%206%20-%2014bit%2014bit%20compressed%20%283:2%29.NEF 8ca94584578b35a422744c1a610174ce37df0691588edfdecaec3111b3fbbb02 6064 4040 1 u16 1e23d74a11c01a97 0.8752 embedded JPEG 14bit 14bit compressed (3:2); comparison Normal/crop-render; mean dL 0.353
32 Nikon Z 6 3582 coverage-3582.nef https://raw.pixls.us/getfile.php/3582/nice/Nikon%20-%20Z%206%20-%2014bit%2014bit%20lossless%20compressed%20%283:2%29.NEF c079345fc93f53a4f0d322f8ddaae505f920c36015f25b58befccaa61db1af31 6064 4040 1 u16 0a0bc73568e125e0 0.8755 embedded JPEG 14bit 14bit lossless compressed (3:2); comparison Normal/crop-render; mean dL 0.464
33 Nikon Z 7 2792 coverage-2792.nef https://raw.pixls.us/getfile.php/2792/nice/Nikon%20-%20Z%207%20-%2012bit%2012bit%20compressed%20%283:2%29.NEF c861546fbcf5826d5eb26d223e2143fa32968e89f7edbc5bb10c283ea5461ded 8288 5520 1 u16 c989690cb3e19ef9 0.7417 embedded JPEG 12bit 12bit compressed (3:2); comparison Normal/crop-render; mean dL 2.417
34 Nikon Z 7 2789 coverage-2789.nef https://raw.pixls.us/getfile.php/2789/nice/Nikon%20-%20Z%207%20-%2012bit%2012bit%20compressed%20%283:2%29.NEF e54a6c4264bf685214f538036fd9d39088e510b2bfedf288e568f0db1897880e 8288 5520 1 u16 627867ac6a8dd4ad 0.7412 embedded JPEG 12bit 12bit compressed (3:2); comparison Normal/crop-render; mean dL 2.299
35 Nikon Z 7 2790 coverage-2790.nef https://raw.pixls.us/getfile.php/2790/nice/Nikon%20-%20Z%207%20-%2012bit%2012bit%20compressed%20%283:2%29.NEF f0e756dbfd00f3b71cb3d8aa5b9e2e1be32f1052a6c4380e09ebc7c5e7748114 8288 5520 1 u16 8a52e6f3d8ff64dd 0.7389 embedded JPEG 12bit 12bit compressed (3:2); comparison Normal/crop-render; mean dL 2.585
36 Nikon Z 8 6617 coverage-6617.nef https://raw.pixls.us/getfile.php/6617/nice/Nikon%20-%20Z%208%20-%208bit%208bit%20compressed%20%283:2%29.NEF c13a8675294456b9c979e6c1e556951654f35377b2870af2874bfb556f21ce40 8280 5520 1 u16 716e1158bab28b76 0.9563 embedded JPEG 8bit 8bit compressed (3:2); comparison Normal/crop-render; mean dL 2.044
37 Parrot Anafi 2912 coverage-2912.dng https://raw.pixls.us/getfile.php/2912/nice/Parrot%20-%20Anafi%20-%2016bit%20%284:3%29.DNG a257a296047ff2d396109fd2e6947fa36aa7986695b6815f83979d59e98a84e6 5344 4016 1 u16 07a188c932a25a31 0.9895 embedded JPEG 16bit (4:3); comparison Normal/crop-render; mean dL -3.924
38 Samsung Galaxy S21 Ultra Rear Main Camera 6728 coverage-6728.dng https://raw.pixls.us/getfile.php/6728/nice/Samsung%20-%20Galaxy%20S21%20Ultra%20-%204:3.dng 5baeb5f0c4a125647df03b82c2d38b0f02784393bd39eff1a5b23d8f5308dc5e 4000 3000 3 u16 16c5d6faed83390d 0.9912 embedded JPEG 4:3; comparison Rotate180/crop-render; mean dL -10.83
39 Samsung Galaxy S22 Ultra Rear Wide Camera 6261 coverage-6261.dng https://raw.pixls.us/getfile.php/6261/nice/Samsung%20-%20Galaxy%20S22%20Ulra%20-%204:3.dng 53e7f83a94c965495ba32d3ea6021ef08ed02244ce64d111d1329bbd01fd5581 4000 3000 3 u16 7cbe612adc5575b4 0.9739 embedded JPEG 4:3; comparison Normal/crop-render; mean dL -20.487
40 Samsung Galaxy S23 Rear Wide Camera 6697 coverage-6697.dng https://raw.pixls.us/getfile.php/6697/nice/Samsung%20-%20Galaxy%20S23%20-%204:3.dng 1c04574f32b5a139922f075291c52018b89d9981ee155a16958a231c1fdbb067 8160 6120 3 u16 b3da1535e2e31e9f 0.9418 embedded JPEG 4:3; comparison Normal/crop-render; mean dL -6.585
41 Samsung Galaxy S7 Edge 1297 coverage-1297.dng https://raw.pixls.us/getfile.php/1297/nice/Samsung%20-%20G935F%20-%2016bit%20%2816:9%29.dng 843617c8b572320b7badfb4ac2eda19479f5804a1dabb909c861bde23ed30561 4032 2268 1 u16 7e99d7da67022e5a 0.8677 exiftool:PreviewTIFF 16bit (16:9); comparison Normal/crop-render; mean dL 5.772
42 Samsung Galaxy S8 2645 coverage-2645.dng https://raw.pixls.us/getfile.php/2645/nice/Samsung%20-%20SM-G950F%20-%2016bit%20%284:3%29.dng 6abdd353094964eaadddf69bba3a0d551ecce1bc0ed013a434691df4f64dcbf2 4032 3024 1 u16 b70ca9a743283754 0.9884 exiftool:PreviewTIFF 16bit (4:3); comparison Normal/crop-render; mean dL -4.112
43 Samsung Galaxy S8+ 2310 coverage-2310.dng https://raw.pixls.us/getfile.php/2310/nice/Samsung%20-%20SM-G955F%20-%2016bit%20%284:3%29.dng 3cf5da4331d062bc6b31e0e8f5b7ff2cd21107de4262d79a2667f670516e8e8c 4032 3024 1 u16 c3a20523ad3b18cf 0.986 exiftool:PreviewTIFF 16bit (4:3); comparison Normal/crop-render; mean dL 2.437
44 Sigma fp 7273 coverage-7273.dng https://raw.pixls.us/getfile.php/7273/nice/Sigma%20-%20fp%20-%2014bit%20%283:2%29.DNG 0844bcf23a49e21b5c2f8489de55ce38fb7b594e3ed337d6b9af2dd9341354e9 6064 4042 1 u16 f7b92f1904d6cc31 0.9754 embedded JPEG 14bit (3:2); comparison Normal/crop-render; mean dL 7.1
45 Sigma fp L 6949 coverage-6949.dng https://raw.pixls.us/getfile.php/6949/nice/Sigma%20-%20fp%20L%20-%2014bit%20%283:2%29.DNG 8e7229f28be30bfa45289940eac04194af2b3474f6db286e4fa4070c145a0a70 9536 6344 1 u16 6896f73ec6b49231 0.9998 embedded JPEG 14bit (3:2); comparison Normal/crop-render; mean dL 1.597
+192
View File
@@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""Measure LightCraft's raw camera coverage (moved from LightCraft's docs/raw/ with the decoders; its paths
and "LightCraft" mean this crate now) against Adobe's supported-camera list.
python3 crates/raw/docs/coverage/coverage.py # print the summary, rewrite docs/raw/coverage.tsv
python3 crates/raw/docs/coverage/coverage.py --check # exit 1 if coverage.tsv is stale
Inputs (all in this directory):
adobe-cameras.tsv Adobe's published list (maker, camera, extensions, ...).
evidence.tsv per-model evidence: verified decodes, known preview-only or refused bodies.
usage-weights.tsv estimated share of photographers per maker bucket and popular bodies.
Every Adobe-listed model gets one class:
verified decoded from sensor data, and a real file from this body was checked (evidence.tsv)
unverified the decoder path for its format and coding should handle it; no sample checked
preview opens as the embedded JPEG (format or coding not decoded)
unsupported not imported, refused, or decodes wrongly
The format rules below mirror the decoders in crates/raw/src (see docs/raw-parity.md). When a decoder
changes, update the rules and evidence, rerun, and commit coverage.tsv with the docs.
Standard library only; no third-party packages.
"""
import csv
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
# Olympus ORF bodies that write uncompressed / packed data (crates/raw/src/vendor/orf.rs). Everything else
# writes compressed ORF, which is preview-only (the E-M5 II / PEN-F high-res mode alone is decoded).
ORF_DECODED = re.compile(r"^(E-1|E-10|E-20|EVOLT E-300|EVOLT E-330|EVOLT E-400|EVOLT E-500|E-300|E-330|E-500|Camedia C-\S.*|Stylus XZ-2 iHS)$")
# Samsung SRW bodies with compressed raw data (crates/raw/src/vendor/srw.rs refuses 32770 / 32772 / 32773).
SRW_COMPRESSED = re.compile(r"(^|\W)(NX1|NX30|NX300|NX300M|NX500|NX2000|NX3000|NX3300|NX mini|EK-GN\d+|EK-KN\d+)(\W|$)")
# CIFF-container CRW bodies (crates/raw/src/vendor/crw.rs): every CRW EOS and the CIFF PowerShots
# (the G and S series and the Pro 1).
CRW_DECODED = re.compile(r"^EOS |^PowerShot (G[1-6]$|Pro ?1$|S[34567]0$|S45$)")
# Fujifilm bodies before the X series whose RAF has no raw IFD (crates/raw/src/vendor/raf.rs).
RAF_OLD = re.compile(r"^(FinePix (E|F|S\d|S\d{3,4}|IS)|IS Pro|S5Pro|S3Pro|S2Pro)", re.I)
# Probability that an unverified body of a format decodes correctly, used for the use-weighted share only.
CONFIDENCE = {"DNG": 0.95, "CR2": 0.95, "CR3": 0.6, "NEF": 0.9, "NRW": 0.9, "ARW": 0.9, "SR2": 0.8, "RAF": 0.95,
"RW2": 0.95, "RWL": 0.95, "RAW": 0.95, "PEF": 0.9, "ORF": 0.8, "SRW": 0.8}
def read_tsv(name):
with open(os.path.join(HERE, name), newline="", encoding="utf-8") as f:
lines = [line for line in f if not line.startswith("#")]
return list(csv.DictReader(lines, delimiter="\t"))
def exts(field):
return [e.strip().upper() for e in re.split(r"[,/+]", re.sub(r"\(.*?\)", "", field)) if e.strip()]
def classify(maker, camera, ext_field):
"""(class, format, reason) from the format rules alone."""
es = exts(ext_field)
for e in es:
if e == "DNG":
return "unverified", "DNG", "DNG decoder (uncompressed, LJ92, lossy, Deflate, JPEG XL, LinearRaw)"
if e == "CR2":
return "unverified", "CR2", "CR2 lossless JPEG decoder"
if e == "CR3":
return "unverified", "CR3", "CRX decoder: lossless and C-RAW 0x100 / 0x200 single-tile; other variants fall back to the preview"
if e in ("NEF", "NRW"):
return "unverified", e, "NEF decoder (uncompressed, lossless, lossy, lossy after split); HE / HE* setting preview-only"
if e == "ARW":
return "unverified", "ARW", "ARW decoder (uncompressed, cRAW, packed 12-bit, lossless, downsized YCbCr)"
if e == "SR2":
return "unverified", "SR2", "SR2 decoder"
if e == "SRF":
return "unsupported", "SRF", "SRF is discovered; sensor decoding unsupported, usable preview not verified on a real file"
if e == "RAF":
if RAF_OLD.search(camera):
return "preview", "RAF", "pre-X FinePix RAF without a raw IFD"
return "unverified", "RAF", "RAF decoder (uncompressed, lossless / lossy compressed, Bayer and X-Trans)"
if e in ("RW2", "RWL") or (e == "RAW" and maker in ("Panasonic", "Leica")):
return "unverified", e, "Panasonic decoder, every raw format"
if e == "PEF":
return "unverified", "PEF", "PEF decoder (uncompressed, Huffman, packed 12-bit)"
if e == "ORF":
if ORF_DECODED.search(camera):
return "unverified", "ORF", "uncompressed / packed ORF"
return "preview", "ORF", "compressed ORF is unsupported; embedded preview when available"
if e == "SRW":
if SRW_COMPRESSED.search(camera):
return "preview", "SRW", "compressed SRW is not decoded"
return "unverified", "SRW", "uncompressed / packed SRW"
e = es[0] if es else ""
if e == "MEF":
return "unverified", "MEF", "MEF decoder (uncompressed 12-bit)"
if e == "ERF":
return "unverified", "ERF", "ERF decoder (12-bit 16-byte blocks)"
if e == "CRW":
# CIFF-era bodies decode (craft-libs crates/crw/clean-room/02-crw-format.md): EOS D30/D60/10D/300D and
# the CIFF PowerShots; the pre-CIFF compacts (PowerShot 600, A5, A50, Pro70) and the
# unprobed Pro90 IS stay preview. The G5 is preview by evidence (entropy anomaly).
if CRW_DECODED.search(camera):
return "unverified", "CRW", "CRW decoder (CIFF; selector 0-2 tables)"
return "preview", "CRW", "pre-CIFF CRW is not decoded"
if e == "MRW":
return "unverified", "MRW", "MRW decoder (12-bit packed, 16-bit words)"
if e in ("3FR", "FFF", "IIQ", "MOS", "KDC", "X3F", "RAW", "ORI"):
return "preview", e, f"{e} is opened from its embedded JPEG only"
return "unsupported", e, f"{e or 'unknown'} is not imported"
def main():
adobe = read_tsv("adobe-cameras.tsv")
evidence = {(r["maker"], r["camera"]): r for r in read_tsv("evidence.tsv")}
rows = []
for a in adobe:
cls, fmt, reason = classify(a["maker"], a["camera"], a["extensions"])
ev = evidence.get((a["maker"], a["camera"]))
if ev:
cls, reason = ev["status"], ev["source"]
rows.append({"maker": a["maker"], "camera": a["camera"], "format": fmt, "year_added": a["year_added"], "class": cls, "reason": reason})
out = os.path.join(HERE, "coverage.tsv")
header = ["maker", "camera", "format", "year_added", "class", "reason"]
text = "# Generated by docs/raw/coverage.py; do not edit.\n" + "\t".join(header) + "\n"
text += "".join("\t".join(r[k] for k in header) + "\n" for r in rows)
if "--check" in sys.argv:
with open(out, encoding="utf-8") as f:
if f.read() != text:
print("docs/raw/coverage.tsv is stale: run python3 crates/raw/docs/coverage/coverage.py")
return 1
else:
with open(out, "w", encoding="utf-8") as f:
f.write(text)
classes = ["verified", "unverified", "preview", "unsupported"]
by_maker = {}
for r in rows:
by_maker.setdefault(r["maker"], dict.fromkeys(classes, 0))[r["class"]] += 1
print("| Maker | Adobe models | verified | unverified | preview | unsupported |")
print("|---|---:|---:|---:|---:|---:|")
for m, c in sorted(by_maker.items(), key=lambda kv: -sum(kv[1].values())):
print(f"| {m} | {sum(c.values())} | " + " | ".join(str(c[k]) for k in classes) + " |")
tot = {k: sum(c[k] for c in by_maker.values()) for k in classes}
n = sum(tot.values())
print(f"| **All** | **{n}** | " + " | ".join(f"**{tot[k]}** ({100 * tot[k] / n:.0f}%)" for k in classes) + " |")
recent = [r for r in rows if r["year_added"] and int(r["year_added"]) >= 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())
File diff suppressed because it is too large. Load diff
+580
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 3.
+114
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 8.
+243
View File
@@ -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.
+778
View File
@@ -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`),
//! <https://github.com/lclevy/canon_cr3/blob/master/readme.md>, and ExifTool's published Canon
//! tag-name tables (`SensorInfo`, `ColorData9`…`ColorData12`, `LensModel`),
//! <https://exiftool.org/TagNames/Canon.html>. 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<Cfa> {
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<Rect> {
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<Rect> {
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<Rect> {
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<Rect> {
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<f32>,
}
/// 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<RawImage> {
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<f32> {
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<u8> {
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<u8> {
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<u8> {
synthetic_header_with(None)
}
fn synthetic_header_with(aspect: Option<[u32; 5]>) -> Vec<u8> {
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<u8>, 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<u8> {
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);
}
}
File diff suppressed because it is too large. Load diff
+338
View File
@@ -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<i32>,
}
fn corrupt() -> RawError {
RawError::Corrupt("invalid CRX wavelet subband geometry".into())
}
fn checked_sample(v: i64) -> Result<i32> {
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<i32> { high.get(index.min(high.len().saturating_sub(1))).copied().ok_or_else(corrupt) };
let even = |i: usize| -> Result<i32> {
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<Vec<i32>> {
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<Band> {
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<Vec<i32>> {
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<i32>, Vec<i32>) {
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<i32> = (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<Band> {
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<Band> {
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<i32> =
(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());
}
}
}
+11
View File
@@ -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<RawImage> {
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")]
+166 -1
View File
@@ -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<PreviewColorSpace> {
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<PreviewColorSpace> {
}
}
/// 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<PreviewColorSpace> {
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<PreviewColorSpace> {
None
}
fn canon_color_space(note: &Ifd) -> Option<PreviewColorSpace> {
match note.u16(0x00b4)? {
1 => Some(PreviewColorSpace::Srgb),
@@ -260,6 +278,24 @@ fn preview_covering(bytes: &[u8], min_edge: Option<u32>) -> Option<Vec<u8>> {
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<Vec<u8>>, 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 &regions {
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]
+358
View File
@@ -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(&not_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<u8> { [&((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<u8> {
let bx = |kind: &[u8; 4], body: &[u8]| -> Vec<u8> { [&((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<u8> { [&((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<u16>, note: Option<u16>) -> Vec<u8> {
let bx = |kind: &[u8; 4], body: &[u8]| -> Vec<u8> { [&((body.len() + 8) as u32).to_be_bytes()[..], kind, body].concat() };
let block = |tag: u16, v: Option<u16>| {
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<u8> { [&((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<u8> { [&((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<u8> { [&((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<u8> { [&((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<u8> = (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");
}
}
+773
View File
@@ -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<Item = (u64, u64)>) {
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<HeaderPlan> {
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<Ranges, &'static str> {
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<u64> {
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<u64> {
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<u64> {
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<u64> {
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<u8>, Ranges) {
let mut buf = vec![fill; file.len()];
let mut loaded = Ranges::new();
let load = |buf: &mut Vec<u8>, 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<u8>, 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:?}");
}
}