From a9812d0cb41593ff4da22155a7ed1171dab7ca2d Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Mon, 12 Oct 2026 17:34:31 +0200 Subject: [PATCH] PEF: the dark-border tolerance is relative to the black level, not the interior, so dark real content stays (#1271) --- crates/raw/src/vendor/pef.rs | 39 ++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/crates/raw/src/vendor/pef.rs b/crates/raw/src/vendor/pef.rs index 868d99771..60b34d95b 100644 --- a/crates/raw/src/vendor/pef.rs +++ b/crates/raw/src/vendor/pef.rs @@ -167,9 +167,11 @@ pub(crate) fn decode_huffman(src: &[u8], h: &Huffman, w: usize, hgt: usize, bits Ok(out) } -/// The image area without all-dark leading/trailing columns and rows (masked or empty), kept at even offsets. A -/// line is dark when its mean is less than 5% of the interior's above `black`: masked lines sit at the black level. -fn dark_trimmed(d: &[u16], w: usize, h: usize, black: f64) -> Rect { +/// The image area without all-dark leading/trailing columns and rows (masked or empty), kept at even offsets. With +/// the maker-note `black` level, a line is dark when its mean is at most 1/256 of the remaining range (at least 16) +/// above it: masked lines sit at the black level plus noise, while dark real content (a night sky) is well above. +/// The tolerance is the one `cr2::masked_columns` uses. Without it, below 5% of the interior's mean. +fn dark_trimmed(d: &[u16], w: usize, h: usize, black: Option, white: f32) -> Rect { if w < 256 || h < 256 { return Rect::new(0, 0, w, h); } @@ -177,7 +179,10 @@ fn dark_trimmed(d: &[u16], w: usize, h: usize, black: f64) -> Rect { let col_mean = |x: usize| (0..h).step_by(ystep).map(|y| d[y * w + x] as f64).sum::() / h.div_ceil(ystep) as f64; let row_mean = |y: usize| (0..w).step_by(xstep).map(|x| d[y * w + x] as f64).sum::() / w.div_ceil(xstep) as f64; let interior = (w / 4..w * 3 / 4).step_by(w / 64).map(col_mean).sum::() / 32.0; - let dark = black + (interior - black) * 0.05; + let dark = match black { + Some(b) => b + ((f64::from(white) - b) / 256.0).max(16.0), + None => interior * 0.05, + }; let edges = |n: usize, mean: &dyn Fn(usize) -> f64| { let mut lo = 0; while lo < 64 && mean(lo) < dark { @@ -256,10 +261,11 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { }; let RawData::U16(ref samples) = data else { return Err(RawError::Unsupported("float PEF".into())) }; + let white = white_from_data(samples, bits); let black_point = mn.as_ref().and_then(|m| m.ifd.f64s(BLACK_POINT)).filter(|v| v.len() == 4); let active = match tagged { Some(r) => r, - None => dark_trimmed(samples, w, hgt, black_point.as_deref().map_or(0.0, |v| v.iter().sum::() / 4.0)), + None => dark_trimmed(samples, w, hgt, black_point.as_deref().map(|v| v.iter().sum::() / 4.0), white), }; let cfa = match (ifd0.u64s(t::CFA_REPEAT_PATTERN_DIM).as_deref(), ifd0.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() }, @@ -283,7 +289,6 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { let g = (v[1] + v[2]) / 2.0; [(v[0] / g) as f32, 1.0, (v[3] / g) as f32] }); - let white = white_from_data(samples, bits); let mut metadata = lightcraft_meta::from_tiff(&tiff); metadata.width = Some(active.width as u32); metadata.height = Some(active.height as u32); @@ -547,4 +552,26 @@ mod tests { assert_eq!(img.black.values, vec![256.0; 4]); assert_eq!(img.active_area, Rect::new(24, 28, w - 40, h - 42)); } + + /// A night frame (bright centre, dark sky): 20 lines of real content 100 DN above the black level inside the + /// masked border stay in the picture, whatever the interior's brightness. + #[test] + fn dark_real_content_next_to_the_masked_border_is_kept() { + let (w, h) = (512, 384); + let file = pef_with_black_point(256, |x, y| { + if x >= w - 8 { + 0 + } else if y >= h - 14 { + 15 + } else if x < 24 || x >= w - 16 || y < 28 { + 256 + ((x + y) % 3) as u16 + } else if x < 44 || x >= w - 36 || y < 48 || y >= h - 34 { + 356 + ((x * 7 + y * 13) % 16) as u16 + } else { + 3256 + ((x * 7 + y * 13) % 64) as u16 + } + }); + let img = decode(&file, Mode::Full).unwrap(); + assert_eq!(img.active_area, Rect::new(24, 28, w - 40, h - 42)); + } }