LR-IMP-FORMATS: decode EOS R6 Mark II C-RAW with adaptive QP up to 175

EOS R6 Mark II C-RAW QP maps reach 172 and 175; crx.rs accepted 131-168,
so those files opened as their preview ("Canon CRX adaptive QP value 172").
Adaptive QP now goes to 175 (quantization value 51, step 228), the highest
seen in any file; uniform subband values still stop at 44.

Checked black-box against the camera JPEGs on private files: 144 R6 II /
R7 C-RAW files decode to identical hashes where they decoded before, the 3
failing ones now decode (green follows the JPEG at r 0.98), fine detail per
QP shows no jump at 172/175, 100 % crops are clean, and 1,492 files sampled
from a 35,796-file library decode with max QP 175. No third-party decoder
source consulted. Bumps RENDER_CACHE_VERSION to 63; cr3.md and What's New.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSGFEf87ba1zKJZb9uzddK
This commit is contained in:
Claude
2026-10-14 08:41:52 +00:00
parent a4b53b28cc
commit 3ae306ecb3
4 changed files with 62 additions and 15 deletions
+1 -1
View File
@@ -62,7 +62,7 @@ impl SettingsHashes {
}
/// Bump when the pipeline's output changes, to invalidate cached thumbnails.
pub const RENDER_CACHE_VERSION: u64 = 62;
pub const RENDER_CACHE_VERSION: u64 = 63;
/// Thumbnails render at one of these long edges (so window/cell size changes reuse the cache).
pub const THUMB_SIZES: [usize; 4] = [128, 256, 384, 512];
+39 -13
View File
@@ -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<Option<QpMap>> {
.transpose()
}
fn quant_step(value: u8) -> Result<i32> {
fn quant_step(value: u8, max: u8) -> Result<i32> {
// 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<QpMap> {
// 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]
+17 -1
View File
@@ -93,7 +93,23 @@ 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 are rejected.
168–175 continue the same integer sequence (below); higher values are rejected.
EOS R6 Mark II C-RAW maps reach QP 172 and 175, which the decoder used to reject ("Canon CRX adaptive QP
value 172"; the file opened as its preview). Quantization values 45–51 (QP 169–175) give steps 114, 128,
144, 160, 180, 204 and 228, continuing the 2^(1/6) ladder of the measured values. No external decoder was
run; the check is black-box against the camera, on a user's private files:
- 144 C-RAW files (EOS R6 Mark II and R7): every file that decoded before decodes to an identical sensor
hash; the 3 that failed (QP maps 168–175) now decode, and their green mosaic follows the camera's
embedded JPEG at Pearson r 0.980–0.984 (24 × 16 cells, as in the corpus test), within the set's range;
- per QP value, the ratio of fine detail in the decode to fine detail in the camera JPEG over the same
16 × 4-pixel QP cells (pooled over the files) is 6.2 at QP 172 and 12.2 at QP 175, inside the
2.8–15.5 spread of the already-accepted QPs 150–165; a wrong step at those QPs would show as a jump;
- 100 % crops round the highest-QP cells of six files show no blocking, banding, ringing or seams, and
no change in noise between high-QP areas and their surroundings;
- 1,492 files sampled from a 35,796-file library (1,094 R7, 298 R6 Mark II, 100 R100) decode with no
errors; the highest QP is 175 (3 of the 298 R6 Mark II files go above 167; the R7 stays at or below 164).
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
+5
View File
@@ -2,6 +2,11 @@
## 2026-10-14
### 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.