diff --git a/crates/raw/src/vendor/crx.rs b/crates/raw/src/vendor/crx.rs index 0321ac042..853efdffe 100644 --- a/crates/raw/src/vendor/crx.rs +++ b/crates/raw/src/vendor/crx.rs @@ -30,6 +30,12 @@ const MAX_CRX_SAMPLES: usize = 200_000_000; const MAX_CRX_EDGE: u32 = 65_536; /// Unary QP-map prefix length that announces an 8-bit escaped symbol. const QP_ESCAPE_ZEROS: u32 = 23; +/// Highest uniform (per-subband) quantization value with a measured step. +const MAX_UNIFORM_QUANT: u8 = 44; +/// Highest adaptive quantization value (QP − 124) seen in real files: QP 175 on the EOS R6 Mark II. +/// See [`quant_step`]. +const MAX_ADAPTIVE_QUANT: u8 = 51; +const MAX_QP: i32 = 124 + MAX_ADAPTIVE_QUANT as i32; const RUN_EXPONENT: [u8; 32] = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 9, 10, 11, 12, 13, 14, 15]; fn corrupt(reason: &str) -> RawError { @@ -207,7 +213,7 @@ fn validate_planes(tiles: &[Tile], config: &Cr3Compression) -> Result<()> { if config.levels != 0 { match band.quant { Quantization::Uniform(value) => { - quant_step(value)?; + quant_step(value, MAX_UNIFORM_QUANT)?; } Quantization::Adaptive { gain, .. } => { if index < 4 && gain != 0 { @@ -342,12 +348,14 @@ fn tile_qp(sample: &[u8], tile: &Tile) -> Result> { .transpose() } -fn quant_step(value: u8) -> Result { +fn quant_step(value: u8, max: u8) -> Result { // Measured independently with an external decoder after modifying only the // quantValue field: all forty values 4..43 follow this integer sequence. Value 44 (QP 168) // continues it (step 102); it was checked against a measured step of about 100 on an EOS R50 - // C-RAW file. Nothing above 44 has been seen, so it stays rejected. - if !(4..=44).contains(&value) { + // C-RAW file. Uniform subband values stop there (`MAX_UNIFORM_QUANT`). Adaptive QP maps reach + // QP 175 (value 51) on the EOS R6 Mark II; those steps (114..228) continue the same sequence and + // were checked against the camera's own JPEGs (docs/cr3.md). Anything higher is still rejected. + if !(4..=max).contains(&value) { return Err(RawError::Unsupported(format!("Canon CRX quantization value {value}"))); } let scale = [40i32, 45, 51, 57, 64, 72].get(usize::from(value % 6)).copied().ok_or_else(|| corrupt("quantization table index"))?; @@ -403,7 +411,7 @@ fn wavelet_plane(sample: &[u8], plane: &Plane, width: usize, height: usize, edge }; for (position, value) in data.iter_mut().enumerate() { let step = match band.quant { - Quantization::Uniform(value) => quant_step(value)?, + Quantization::Uniform(value) => quant_step(value, MAX_UNIFORM_QUANT)?, Quantization::Adaptive { base, gain } => { adaptive_step(qp.ok_or_else(|| corrupt("missing QP map"))?, i, position % w, position / w, base, gain)? } @@ -443,7 +451,7 @@ fn adaptive_step(map: &QpMap, band: usize, x: usize, y: usize, base: u32, gain: // QP rows are averaged before looking up the integer step. R8 field // mutations distinguish this order from averaging dequantization steps. let quant = u8::try_from(value - 124).map_err(|_| corrupt("QP value outside quantization table"))?; - u64::from(base) + u64::from(gain) * quant_step(quant)? as u64 / 8 + u64::from(base) + u64::from(gain) * quant_step(quant, MAX_ADAPTIVE_QUANT)? as u64 / 8 }; i32::try_from(step.max(1)).map_err(|_| corrupt("adaptive quantization step outside precision")) } @@ -775,7 +783,7 @@ fn qp_map(src: &[u8], width: usize, height: usize) -> Result { // The map opens with its first symbol as a 32-bit literal; every later one is Rice coded. let symbol = if x == 0 && y == 0 { (bits.take(16)? << 16) | bits.take(16)? } else { bits.qp_rice(k)? }; let value = median(left, above, corner).checked_add(signed(symbol)).ok_or_else(|| corrupt("QP prediction overflow"))?; - if !(131..=168).contains(&value) { + if !(131..=MAX_QP).contains(&value) { return Err(RawError::Unsupported(format!("Canon CRX adaptive QP value {value}"))); } *current.get_mut(x).ok_or_else(|| corrupt("QP outside row"))? = value; @@ -990,10 +998,19 @@ mod tests { #[test] fn measured_quantization_and_allocation_limits() { for (quant, expected) in [(4, 1), (9, 1), (10, 2), (16, 4), (22, 8), (26, 12), (32, 25), (36, 40), (41, 72), (42, 80), (43, 90), (44, 102)] { - assert_eq!(quant_step(quant).unwrap(), expected); + assert_eq!(quant_step(quant, MAX_UNIFORM_QUANT).unwrap(), expected); } - assert!(matches!(quant_step(3), Err(RawError::Unsupported(_)))); - assert!(matches!(quant_step(45), Err(RawError::Unsupported(_)))); + assert!(matches!(quant_step(3, MAX_UNIFORM_QUANT), Err(RawError::Unsupported(_)))); + assert!(matches!(quant_step(45, MAX_UNIFORM_QUANT), Err(RawError::Unsupported(_)))); + // Adaptive QP 169..=175 (EOS R6 Mark II C-RAW) continue the sequence; QP 176 does not decode. + for (quant, expected) in [(45, 114), (46, 128), (47, 144), (48, 160), (49, 180), (50, 204), (51, 228)] { + assert_eq!(quant_step(quant, MAX_ADAPTIVE_QUANT).unwrap(), expected); + } + assert!(matches!(quant_step(52, MAX_ADAPTIVE_QUANT), Err(RawError::Unsupported(_)))); + let high = QpMap { width: 1, height: 2, data: vec![172, 175] }; + // floor((172+175)/2) = 173 → value 49, step 180; gain 8/8 plus base 1. + assert_eq!(adaptive_step(&high, 4, 0, 0, 1, 8).unwrap(), 181); + assert_eq!(adaptive_step(&high, 7, 0, 1, 0, 8).unwrap(), 228); for (width, height) in [(20_000, 20_000), (65_538, 2), (2, 65_538)] { let mut config = coding(); config.width = width; @@ -1032,9 +1049,18 @@ mod tests { } #[test] - fn qp_168_is_accepted_and_169_is_not() { - assert_eq!(qp_map(&[0, 0, 1, 0x50, 0x94], 1, 4).unwrap().data, [168; 4]); - assert!(matches!(qp_map(&[0, 0, 1, 0x52, 0x94], 1, 4), Err(RawError::Unsupported(_)))); + fn qp_limit_is_accepted_and_one_above_is_not() { + // The first symbol is a 32-bit literal: signed(2·QP) = QP from a prediction of 0. QP 172 and 175 + // occur in EOS R6 Mark II maps (3 of 40 sample files failed with them before). + let map = |qp: u32| { + let symbol = 2 * qp; + let mut src = vec![0, 0, (symbol >> 8) as u8, symbol as u8]; + src.push(0x94); + qp_map(&src, 1, 4) + }; + assert_eq!(map(168).unwrap().data, [168; 4]); + assert_eq!(map(MAX_QP as u32).unwrap().data, [MAX_QP; 4]); + assert!(matches!(map(MAX_QP as u32 + 1), Err(RawError::Unsupported(_)))); } #[test] diff --git a/docs/whats-new.md b/docs/whats-new.md index 64e71172a..89a3b9672 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -112,6 +112,11 @@ did). On the D600 and D610 the white level is also measured inside the picture, so the filler can't pass for clipped highlights. +### More Canon EOS R6 Mark II C-RAW files decode +- Some EOS R6 Mark II C-RAW files (about 1 in 50 in one photographer's library) opened as their preview with + "Canon CRX adaptive QP value 172" or "175". They now decode from the sensor data. Photos imported before this + switch over with Photo ▸ Reload from Disk. + ### Hard-edged Heal and Clone spots - A spot with Feather at 0 no longer draws black pixels exactly on its radius. This also fixes spots at 0% Opacity changing pixels along their edge. The hard edge now leaves pixels on the radius unchanged.