mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
commit
992e67188b
4 files changed
+77
-3
No files matched your search
@@ -251,12 +251,14 @@ pub(crate) fn decode_as(bytes: &[u8], mode: Mode, format: RawFormat) -> Result<R
|
||||
[r, c] if (1..=16).contains(r) && (1..=16).contains(c) => (*r as usize, *c as usize),
|
||||
_ => (1, 1),
|
||||
};
|
||||
let mut values: Vec<f32> = raw.f64s(t::BLACK_LEVEL).unwrap_or_default().into_iter().map(|v| v as f32).collect();
|
||||
// (a FLOAT / DOUBLE tag can hold NaN or infinity: such a level reads as 0, in its place)
|
||||
let level = |v: f64| Some(v as f32).filter(|v| v.is_finite()).unwrap_or(0.0);
|
||||
let mut values: Vec<f32> = raw.f64s(t::BLACK_LEVEL).unwrap_or_default().into_iter().map(level).collect();
|
||||
if values.len() != br * bc * cpp {
|
||||
values = vec![values.first().copied().unwrap_or(0.0)];
|
||||
}
|
||||
let (br, bc) = if values.len() == 1 { (1, 1) } else { (br, bc) };
|
||||
let floats = |tag| raw.f64s(tag).unwrap_or_default().into_iter().map(|v| v as f32).collect::<Vec<f32>>();
|
||||
let floats = |tag| raw.f64s(tag).unwrap_or_default().into_iter().map(level).collect::<Vec<f32>>();
|
||||
let black =
|
||||
BlackLevel { repeat_rows: br, repeat_cols: bc, values, delta_h: floats(t::BLACK_LEVEL_DELTA_H), delta_v: floats(t::BLACK_LEVEL_DELTA_V) };
|
||||
|
||||
|
||||
@@ -768,7 +768,10 @@ impl RawImage {
|
||||
for s in 0..cpp {
|
||||
let v = src(base + x * cpp + s);
|
||||
let b = self.black.at(x, y, s, cpp);
|
||||
row[x * cpp + s] = (v - b) * scale[s];
|
||||
// a NaN or infinite sample (a float DNG's, or a black level a file got wrong)
|
||||
// reads as black, so the demosaic and every stage after it see numbers
|
||||
let n = (v - b) * scale[s];
|
||||
row[x * cpp + s] = if n.is_finite() { n } else { 0.0 };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -102,3 +102,68 @@ fn hostile_dimensions_are_rejected_quickly() {
|
||||
assert!(lightcraft_raw::decode(&bytes).is_err());
|
||||
assert!(t0.elapsed().as_secs_f64() < 2.0);
|
||||
}
|
||||
|
||||
/// A Bayer DNG whose `BlackLevel` is a FLOAT holding NaN (or infinity), and a float CFA DNG with
|
||||
/// NaN samples: every normalised sample came out NaN, and AHD's green estimate clamped between two
|
||||
/// NaN neighbours, which panics (the 1:1 view and every export of such a file). Non-finite levels
|
||||
/// read as 0 and non-finite samples as black: the photo develops to finite numbers.
|
||||
#[test]
|
||||
fn non_finite_black_levels_and_samples_develop() {
|
||||
use lightcraft_tiff::tags as t;
|
||||
use lightcraft_tiff::{IfdBuilder, ImageData, TiffWriter, Value};
|
||||
let (w, h) = (32u32, 16u32);
|
||||
for black in [Value::Float(vec![f32::NAN]), Value::Double(vec![f64::INFINITY]), Value::Float(vec![64.0, f32::NAN, 64.0, 64.0])] {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(t::DNG_VERSION, Value::Byte(vec![1, 4, 0, 0]));
|
||||
ifd.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![0]));
|
||||
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![w]));
|
||||
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![h]));
|
||||
ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![16]));
|
||||
ifd.set(t::COMPRESSION, Value::Short(vec![1]));
|
||||
ifd.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA]));
|
||||
ifd.set(t::CFA_REPEAT_PATTERN_DIM, Value::Short(vec![2, 2]));
|
||||
ifd.set(t::CFA_PATTERN_EP, Value::Byte(vec![0, 1, 1, 2]));
|
||||
ifd.set(t::BLACK_LEVEL_REPEAT_DIM, Value::Short(vec![2, 2]));
|
||||
ifd.set(t::BLACK_LEVEL, black.clone());
|
||||
ifd.set(t::WHITE_LEVEL, Value::Long(vec![4095]));
|
||||
let strip: Vec<u8> = (0..w * h).flat_map(|i| (100 + (i * 37 % 3000) as u16).to_le_bytes()).collect();
|
||||
ifd.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![strip] });
|
||||
let bytes = TiffWriter::default().write(&[ifd]).unwrap();
|
||||
let img = lightcraft_raw::decode(&bytes).unwrap_or_else(|e| panic!("{black:?}: {e}"));
|
||||
let rgb = img.develop(Method::Ahd).unwrap();
|
||||
assert!(rgb.data.iter().flatten().all(|v| v.is_finite()), "{black:?}: finite output");
|
||||
let _ = img.develop(Method::Ppg);
|
||||
}
|
||||
// float samples: a block of NaN and infinities in the middle of the mosaic
|
||||
let (w, h) = (24usize, 16usize);
|
||||
let data: Vec<f32> = (0..w * h)
|
||||
.map(|i| match (i % w, i / w) {
|
||||
(8..=13, 5..=10) => f32::NAN,
|
||||
(2, _) => f32::INFINITY,
|
||||
_ => (i * 97 % 1000) as f32 / 1000.0,
|
||||
})
|
||||
.collect();
|
||||
let raw = RawImage {
|
||||
format: RawFormat::Dng,
|
||||
width: w,
|
||||
height: h,
|
||||
cpp: 1,
|
||||
data: RawData::F32(data),
|
||||
cfa: Some(Cfa::bayer("RGGB").unwrap()),
|
||||
bits: 32,
|
||||
black: BlackLevel::uniform(0.0),
|
||||
white: vec![1.0],
|
||||
active_area: Rect::new(0, 0, w, h),
|
||||
crop: Rect::new(0, 0, w, h),
|
||||
orientation: Orientation::Normal,
|
||||
color: ColorData::default(),
|
||||
wb_multipliers: None,
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata: Default::default(),
|
||||
};
|
||||
for img in [raw.clone(), lightcraft_raw::decode(&lightcraft_raw::write_dng(&raw, &DngWriteOptions::default()).unwrap()).unwrap()] {
|
||||
let rgb = img.develop(Method::Ahd).unwrap();
|
||||
assert!(rgb.data.iter().flatten().all(|v| v.is_finite()), "float NaN samples: finite output");
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,10 @@
|
||||
- Faces are kept in the thumbnail cache at a few fixed sizes, so moving the thumbnail slider no longer renders them
|
||||
all again.
|
||||
|
||||
### Reliability
|
||||
- A raw file whose black level is stored as "not a number" (or infinity), or a floating-point DNG with such
|
||||
samples, no longer crashes LightCraft at 1:1 or on export: the bad values read as black.
|
||||
|
||||
### Slider drags on large raws
|
||||
- Dragging a slider no longer re-renders the edited photo's filmstrip thumbnail on every frame; it follows once you
|
||||
let go. On a 64 MP raw the first of those renders decoded the whole file again, which held up the loupe's
|
||||
|
||||
Reference in new issue
Block a user