From fe9e6e3433f6e56f860ccfd64bfc5622f807fd49 Mon Sep 17 00:00:00 2001 From: radialmonster Date: Tue, 13 Oct 2026 02:44:22 -0400 Subject: [PATCH 1/3] LR-RAW-NEF: size the NRW word packing by the row, not by the padded strip (P7000 stripes) --- crates/raw/src/vendor/nef.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/raw/src/vendor/nef.rs b/crates/raw/src/vendor/nef.rs index 695d55712..ad792b2e4 100644 --- a/crates/raw/src/vendor/nef.rs +++ b/crates/raw/src/vendor/nef.rs @@ -232,7 +232,8 @@ fn uncompressed(bytes: &[u8], info: &ImageInfo, order: ByteOrder, mn: Option<&ma let packing = if bytes_per_row >= row_samples * 2 { Packing::Word16 } else if bits == 12 && bytes_per_row * 8 >= row_samples * 12 && bytes_per_row * 8 < row_samples * 13 { - if nrw_data(mn).is_some() && bytes_per_row.is_multiple_of(4) { Packing::Words32Msb } else { Packing::Msb } + // the row's own size (a strip may be padded past the last row, which skews `bytes_per_row`) + if nrw_data(mn).is_some() && (row_samples * 12).div_ceil(8).is_multiple_of(4) { Packing::Words32Msb } else { Packing::Msb } } else if info.compression == 1 && bits != 8 && bits != 16 { return Err(RawError::Unsupported(format!("NEF uncompressed packing ({bytes_per_row} bytes per {row_samples}-sample row)"))); } else { @@ -575,6 +576,17 @@ mod tests { assert_eq!(decode(Some(both)).black.values, vec![100.0, 101.0, 102.0, 103.0]); } + #[test] + fn nrw_padded_strip_keeps_32_bit_word_packing() { + // 16 x 6 12-bit samples are 24 bytes a row; a strip padded past the last row must not change the packing + let (w, h) = (16usize, 6usize); + let px: Vec = (0..w * h).map(|i| (200 + i * 131 % 3800) as u16).collect(); + let mut packed = words32(&px, w, ByteOrder::Little); + packed.extend_from_slice(&[0u8; 6]); + let r = crate::decode(&nef_in(ByteOrder::Little, 1, 12, vec![packed], w as u32, h as u32, h as u32, Some(nrw_note(0)))).unwrap(); + assert_eq!(r.data, RawData::U16(px)); + } + #[test] fn compressed_without_table_is_unsupported() { let bytes = nef(34713, 14, vec![vec![0; 64]], 8, 8, 8); From 967e57fa50661139fd76fedacff9ed8982b004d1 Mon Sep 17 00:00:00 2001 From: radialmonster Date: Tue, 13 Oct 2026 02:44:22 -0400 Subject: [PATCH 2/3] LR-RAW-NEF: read the as-shot white balance from the NRW data block when the maker note has no WB tag (P7000, P7100) --- crates/raw/src/vendor/nef.rs | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/raw/src/vendor/nef.rs b/crates/raw/src/vendor/nef.rs index ad792b2e4..b9d6c9cc4 100644 --- a/crates/raw/src/vendor/nef.rs +++ b/crates/raw/src/vendor/nef.rs @@ -45,6 +45,17 @@ fn nrw_black(mn: Option<&makernote::MakerNote>, bits: u32) -> Option { (u64::from(v) < (1u64 << bits.min(16)) / 4).then_some(v as f32) } +/// As-shot white balance from the NRW data block, for bodies whose maker note has no `0x000c`: the first entry of the +/// block's colour-temperature table (byte `0x34`) is five `u32`s, kelvin, R, G, G, B, in the same 1/256 units as +/// `0x000c` (the same R and B values in the P950, B700, P1000 and P1100 files, which carry both). +fn nrw_wb(mn: Option<&makernote::MakerNote>) -> Option<[f32; 3]> { + let (b, order) = nrw_data(mn)?; + let at = |i: usize| -> Option { Some(order.u32(b.get(0x34 + 4 * i..0x38 + 4 * i)?.try_into().ok()?)) }; + let (kelvin, r, g1, g2, bl) = (at(0)?, at(1)?, at(2)?, at(3)?, at(4)?); + let sane = |v: u32| (64..=2560).contains(&v); + ((2000..=12000).contains(&kelvin) && g1 == 512 && g2.abs_diff(512) <= 8 && sane(r) && sane(bl)).then(|| [r as f32 / 256.0, 1.0, bl as f32 / 256.0]) +} + /// Nikon CropArea is `[left, top, width, height]` in sensor pixels (maker-note tag documentation). /// Keep the active area/CFA origin unchanged: the default crop is applied after demosaicing. fn default_crop(mn: Option<&makernote::MakerNote>, w: usize, h: usize) -> Rect { @@ -172,7 +183,8 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { .as_ref() .and_then(|m| m.ifd.f64s(WB_RB_LEVELS)) .filter(|v| v.len() >= 2 && v[0] > 0.1 && v[1] > 0.1 && v[0] < 10.0 && v[1] < 10.0) - .map(|v| [v[0] as f32, 1.0, v[1] as f32]); + .map(|v| [v[0] as f32, 1.0, v[1] as f32]) + .or_else(|| nrw_wb(mn.as_ref())); let cfa = match (raw.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), raw.bytes(t::CFA_PATTERN_EP)) { (Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() }, _ => Cfa::bayer_static("RGGB"), @@ -587,6 +599,26 @@ mod tests { assert_eq!(r.data, RawData::U16(px)); } + #[test] + fn nrw_block_supplies_the_as_shot_white_balance_when_the_maker_note_has_none() { + let note = |kelvin: u32, r: u32, g1: u32, g2: u32, b: u32| { + let mut block = b"NRW 0101".to_vec(); + block.resize(0x34, 0); + for v in [kelvin, r, g1, g2, b] { + block.extend_from_slice(&v.to_le_bytes()); + } + block.resize(0x80, 0); + let mut mn = IfdBuilder::new(); + mn.set(NRW_DATA, Value::Undefined(block)); + mn + }; + let wb = |mn| crate::decode(&nef_in(ByteOrder::Little, 1, 12, vec![vec![0; 144]], 16, 6, 6, Some(mn))).unwrap().wb_multipliers; + assert_eq!(wb(note(5236, 430, 512, 510, 567)), Some([430.0 / 256.0, 1.0, 567.0 / 256.0])); + // an implausible entry is not a white balance + assert_eq!(wb(note(0, 430, 512, 510, 567)), None); + assert_eq!(wb(note(5236, 430, 300, 510, 567)), None); + } + #[test] fn compressed_without_table_is_unsupported() { let bytes = nef(34713, 14, vec![vec![0; 64]], 8, 8, 8); From 4f51f051e7b3f8c1303b28470d33e7b0d4b93963 Mon Sep 17 00:00:00 2001 From: radialmonster Date: Tue, 13 Oct 2026 04:03:59 -0400 Subject: [PATCH 3/3] LR-RAW-NEF: only take the NRW block's white balance where the as-shot kelvin is mirrored (P7000); P7100 gets none --- crates/raw/src/vendor/nef.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/crates/raw/src/vendor/nef.rs b/crates/raw/src/vendor/nef.rs index b9d6c9cc4..39dfe6fd5 100644 --- a/crates/raw/src/vendor/nef.rs +++ b/crates/raw/src/vendor/nef.rs @@ -47,13 +47,16 @@ fn nrw_black(mn: Option<&makernote::MakerNote>, bits: u32) -> Option { /// As-shot white balance from the NRW data block, for bodies whose maker note has no `0x000c`: the first entry of the /// block's colour-temperature table (byte `0x34`) is five `u32`s, kelvin, R, G, G, B, in the same 1/256 units as -/// `0x000c` (the same R and B values in the P950, B700, P1000 and P1100 files, which carry both). +/// `0x000c` (the same R and B values in the P950, B700, P1000 and P1100 files, which carry both). Only used when the +/// table's fourth entry, a kelvin-only slot, repeats the first entry's kelvin (the P7000); the P7100 keeps its +/// as-shot kelvin there (3195) while entry 0 is the 3000 K preset, so that body gets no white balance from here. fn nrw_wb(mn: Option<&makernote::MakerNote>) -> Option<[f32; 3]> { let (b, order) = nrw_data(mn)?; let at = |i: usize| -> Option { Some(order.u32(b.get(0x34 + 4 * i..0x38 + 4 * i)?.try_into().ok()?)) }; let (kelvin, r, g1, g2, bl) = (at(0)?, at(1)?, at(2)?, at(3)?, at(4)?); let sane = |v: u32| (64..=2560).contains(&v); - ((2000..=12000).contains(&kelvin) && g1 == 512 && g2.abs_diff(512) <= 8 && sane(r) && sane(bl)).then(|| [r as f32 / 256.0, 1.0, bl as f32 / 256.0]) + let mirrored = at(15)? == kelvin; + (mirrored && (2000..=12000).contains(&kelvin) && g1 == 512 && g2.abs_diff(512) <= 8 && sane(r) && sane(bl)).then(|| [r as f32 / 256.0, 1.0, bl as f32 / 256.0]) } /// Nikon CropArea is `[left, top, width, height]` in sensor pixels (maker-note tag documentation). @@ -601,22 +604,26 @@ mod tests { #[test] fn nrw_block_supplies_the_as_shot_white_balance_when_the_maker_note_has_none() { - let note = |kelvin: u32, r: u32, g1: u32, g2: u32, b: u32| { + let note = |kelvin: u32, r: u32, g1: u32, g2: u32, b: u32, mirror: Option| { let mut block = b"NRW 0101".to_vec(); block.resize(0x34, 0); for v in [kelvin, r, g1, g2, b] { block.extend_from_slice(&v.to_le_bytes()); } + block.resize(0x34 + 60, 0); + block.extend_from_slice(&mirror.unwrap_or(kelvin).to_le_bytes()); block.resize(0x80, 0); let mut mn = IfdBuilder::new(); mn.set(NRW_DATA, Value::Undefined(block)); mn }; let wb = |mn| crate::decode(&nef_in(ByteOrder::Little, 1, 12, vec![vec![0; 144]], 16, 6, 6, Some(mn))).unwrap().wb_multipliers; - assert_eq!(wb(note(5236, 430, 512, 510, 567)), Some([430.0 / 256.0, 1.0, 567.0 / 256.0])); + assert_eq!(wb(note(5236, 430, 512, 510, 567, None)), Some([430.0 / 256.0, 1.0, 567.0 / 256.0])); // an implausible entry is not a white balance - assert_eq!(wb(note(0, 430, 512, 510, 567)), None); - assert_eq!(wb(note(5236, 430, 300, 510, 567)), None); + assert_eq!(wb(note(0, 430, 512, 510, 567, None)), None); + assert_eq!(wb(note(5236, 430, 300, 510, 567, None)), None); + // the P7100 layout: entry 0 is a preset, the as-shot kelvin sits in the fourth entry + assert_eq!(wb(note(3000, 278, 512, 510, 868, Some(3195))), None); } #[test]