M11.1: decode EOS R7 C-RAW (odd-height CRX QP map)

An odd-height adaptive QP map (EOS R7: 4732 sensor rows, 1183 QP rows)
was rejected as unsupported, so R7 C-RAW files opened from their embedded
JPEG. The map decodes to the exact end of its entropy stream; the last
row of the second-level high-frequency bands has no partner row and its
QP is used alone. Even-height maps never reach this rule, so the six
verified M50/R100/R8 fixtures are unchanged.

Checked on paired R7 RAW and C-RAW captures (ISO 100 and 6400): all
six C-RAW files decode, developed images match their RAW counterparts
visually, and ISO 6400 sensor means agree within 0.1%.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Gs6roPby7THvteijZwptQt
This commit is contained in:
Claude committed 2026-10-09 07:38:24 +00:00
1 parent c435d143de
commit a45c2b661b
3 files changed
+28 -9

No files matched your search

+16 -8
View File
@@ -9,7 +9,8 @@
//! CC0 raw.pixls.us files against an external decoder used only as a black-box oracle.
//!
//! Current support: version 0x100 lossless Bayer RAW and horizontal-tile C-RAW,
//! plus version 0x200 single-tile 14-bit C-RAW with adaptive QP quantization.
//! plus version 0x200 single-tile 14-bit C-RAW with adaptive QP quantization
//! (including odd-height QP maps, as on the EOS R7).
//! Both ff01/ff02/ff03 and ff11/ff12/ff13 marker families are supported.
//! Six full M50/R100/R8 sensor fixtures match an external decoder sample for sample.
//! Unverified coding variants return explicit unsupported errors.
@@ -173,9 +174,6 @@ fn supports(config: &Cr3Compression) -> Result<()> {
if config.version == 0x200 && (config.levels != 3 || config.bit_depth != 14 || config.tile_width != config.width) {
return Err(RawError::Unsupported("Canon CRX version 0x200 requires a single-tile 14-bit Bayer C-RAW image".into()));
}
if config.version == 0x200 && !config.height.div_ceil(4).is_multiple_of(2) {
return Err(RawError::Unsupported("Canon CRX adaptive QP map with an odd height".into()));
}
if config.median_bit_depth.is_some() {
return Err(RawError::Unsupported("Canon CRX extended median-bit-depth header".into()));
}
@@ -418,7 +416,9 @@ fn adaptive_step(map: &QpMap, band: usize, x: usize, y: usize, base: u32, gain:
let index = row.checked_mul(map.width).and_then(|n| n.checked_add(column)).ok_or(RawError::Limit("CRX QP map index"))?;
let first = *map.data.get(index).ok_or_else(|| corrupt("QP coordinate outside map"))?;
let value = if band < 7 {
let second_row = row + 1;
// An odd-height map (R7: 4732 rows give 1183 QP rows) leaves the
// last row of the coarser bands without a partner; it is used alone.
let second_row = (row + 1).min(map.height - 1);
let second = *map.data.get(second_row * map.width + column).ok_or_else(|| corrupt("QP average outside map"))?;
(first + second) / 2
} else {
@@ -734,9 +734,6 @@ fn high_frequency_band(src: &[u8], width: usize, height: usize) -> Result<Vec<i3
}
fn qp_map(src: &[u8], width: usize, height: usize) -> Result<QpMap> {
if !height.is_multiple_of(2) {
return Err(RawError::Unsupported("Canon CRX adaptive QP map with an odd height".into()));
}
let count = width.checked_mul(height).filter(|&n| width > 0 && n > 0 && n <= MAX_CRX_SAMPLES / 32).ok_or(RawError::Limit("CRX QP map size"))?;
let mut data = Vec::new();
data.try_reserve_exact(count).map_err(|_| RawError::Limit("CRX QP allocation"))?;
@@ -1006,6 +1003,17 @@ mod tests {
assert!(matches!(qp_map(&[0, 0, 1, 4, 0x94], 1, 4), Err(RawError::Unsupported(_))));
}
#[test]
fn odd_height_qp_map_uses_its_last_row_alone() {
// EOS R7 C-RAW: 4732 sensor rows give a 1183-row QP map, so the last
// row of the coarser high-frequency bands has no partner row.
let map = QpMap { width: 1, height: 3, data: vec![150, 153, 151] };
assert_eq!(adaptive_step(&map, 4, 0, 0, 0, 4).unwrap(), 7);
// Row 2 alone is QP 151; averaging with a missing row would leave the table.
assert_eq!(adaptive_step(&map, 4, 0, 1, 0, 4).unwrap(), adaptive_step(&map, 7, 0, 2, 0, 4).unwrap());
assert!(adaptive_step(&map, 4, 0, 2, 0, 4).is_err());
}
#[test]
fn adaptive_v200_craw_reconstructs_sensor_samples() {
let mut config = coding();
+10 -1
View File
@@ -55,7 +55,16 @@ 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;
unmeasured values and an odd QP-map height are rejected. Partial-subband and
unmeasured values are rejected.
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. The decoder bounds image dimensions, sample count,
tile count, marker headers and temporary allocations. The full sensor buffer is
+2
View File
@@ -49,6 +49,8 @@
are unchanged; photos imported before this change get it when imported again.
- Canon CR3 raws now develop from their sensor data: lossless RAW and C-RAW, checked sample for sample on the EOS
M50, R100 and R8. CR3 files the decoder can't read yet still open from their embedded JPEG, as before.
- Canon EOS R7 C-RAW files develop from their sensor data too, instead of opening from their embedded JPEG. For R7
C-RAWs imported earlier, Photo ▸ Reload from Disk picks up the raw data.
- Canon CRW, Minolta MRW, Sigma X3F, Kodak KDC, Leaf MOS and Epson ERF files that LightCraft can't decode yet
now import as "preview only" with their embedded JPEG instead of failing. A raw whose data is damaged but whose
preview is intact does the same.