Browse / github.com/storytold/lightcraft / patch
Issue #3191: encode DNG WhiteLevel tag as LONG instead of RATIONAL
[Bug] WhiteLevel DNG tag is written with an incompatible data type HDR DNG files created by LightCraft contain a `WhiteLevel` tag using a data type that does not conform to the DNG specification. Observed tag: - Tag: `WhiteLevel` (`50717`) - Type written by LightCraft: `RATIONAL` - Values: `1/1, 1/1, 1/1` - Image format: three-channel, 16-bit IEEE floating-point LinearRaw According to the DNG specification, the permitted data types for `WhiteLevel` are `SHORT` or `LONG`, with one value per sample. For floating-point image data, the default white level is `1.0`. When inspecting the file with `libtiff`, the following warning is reported: ```text TIFFFetchN
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- kk376 (imported from pull request #3197, open)
- Base
37b2e523ad4084991211e99b0c501dd951a98dbf- Size
- +19 −7 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
002495d4832d67be83fe7966
How it went
No other operator has reported using it yet.
Diff
raw · crates/raw/src/dngwrite.rs crates/raw/tests/it/dng.rs crates/raw/tests/it/rendered_dng.rs
@@ -241,12 +241,11 @@ pub fn write_dng(raw: &RawImage, opts: &DngWriteOptions) -> Result<Vec<u8>> { if !b.delta_v.is_empty() { ifd.set(t::BLACK_LEVEL_DELTA_V, Value::SRational(b.delta_v.iter().map(|&v| srational(v as f64)).collect())); }- let white: Vec<u32> = (0..cpp).map(|s| raw.white_at(s).round().clamp(0.0, u32::MAX as f32) as u32).collect();- if matches!(raw.data, RawData::F32(_)) {- ifd.set(t::WHITE_LEVEL, rat_vec(&(0..cpp).map(|s| raw.white_at(s) as f64).collect::<Vec<_>>()));- } else {- ifd.set(t::WHITE_LEVEL, Value::Long(white));+ if !(0..cpp).all(|s| (raw.white_at(s) - raw.white_at(s).round()).abs() < 1e-4) {+ return Err(RawError::Unsupported("fractional white level".into())); }+ let white: Vec<u32> = (0..cpp).map(|s| raw.white_at(s).round().clamp(1.0, u32::MAX as f32) as u32).collect();+ ifd.set(t::WHITE_LEVEL, Value::Long(white)); let a = raw.active_area; ifd.set(t::ACTIVE_AREA, Value::Long(vec![a.y as u32, a.x as u32, (a.y + a.height) as u32, (a.x + a.width) as u32])); ifd.set(t::DEFAULT_CROP_ORIGIN, Value::Long(vec![raw.crop.x as u32, raw.crop.y as u32]));@@ -172,6 +172,8 @@ fn deflate_roundtrips_integer_and_float() { raw.white = vec![1.0]; for half in [false, true] { let bytes = write_dng(&raw, &DngWriteOptions { compression: DngCompression::Deflate { tile: 32, half }, ..Default::default() }).unwrap();+ let tiff = lightcraft_tiff::Tiff::parse(&bytes).unwrap();+ assert_eq!(tiff.ifds[0].value(t::WHITE_LEVEL), Some(&Value::Long(vec![1, 1, 1]))); let back = decode(&bytes).unwrap(); let RawData::F32(b) = &back.data else { panic!("float data expected") }; for (a, b) in vals.iter().zip(b) {@@ -435,7 +437,7 @@ fn non_finite_float_samples_decode_and_develop() { } raw.data = RawData::F32(vals.clone()); raw.black = BlackLevel::uniform(0.0);- raw.white = vec![0.75];+ raw.white = vec![1.0]; for compression in [DngCompression::Uncompressed, DngCompression::Deflate { tile: 16, half: false }] { let bytes = write_dng(&raw, &DngWriteOptions { compression, ..Default::default() }).unwrap(); let img = decode(&bytes).unwrap();@@ -443,7 +445,7 @@ fn non_finite_float_samples_decode_and_develop() { let RawData::F32(got) = &img.data else { panic!("float data expected") }; for (a, b) in vals.iter().zip(got) { let want = match *a {- f32::INFINITY => 0.75,+ f32::INFINITY => 1.0, v if v.is_finite() => v, _ => 0.0, };@@ -456,6 +458,15 @@ fn non_finite_float_samples_decode_and_develop() { } } +#[test]+fn fractional_white_level_is_rejected() {+ let mut raw = synthetic(16, 12, None, 1);+ raw.data = RawData::F32(vec![0.5; 16 * 12]);+ raw.white = vec![0.75];+ let err = write_dng(&raw, &DngWriteOptions::default()).unwrap_err();+ assert!(matches!(err, lightcraft_raw::RawError::Unsupported(ref msg) if msg.contains("fractional white level")));+}+ #[test] fn lj92_strips_and_levels_and_linearization() { let (w, h) = (20, 12);@@ -18,6 +18,8 @@ fn rendered_dng_round_trips_pixels_and_colour() { for space in [SRGB, PROPHOTO] { let meta = lightcraft_meta::Metadata { make: Some("Maker".into()), model: Some("Model".into()), ..Default::default() }; let bytes = write_rendered_dng(w, h, &rgb, &space, &meta, None).unwrap();+ let tiff = lightcraft_tiff::Tiff::parse(&bytes).unwrap();+ assert_eq!(tiff.ifds[0].value(lightcraft_tiff::tags::WHITE_LEVEL), Some(&lightcraft_tiff::Value::Long(vec![1, 1, 1]))); let raw = lightcraft_raw::decode(&bytes).unwrap(); assert_eq!((raw.width, raw.height, raw.cpp, raw.cfa.is_none()), (w, h, 3, true)); assert_eq!(raw.metadata.model.as_deref(), Some("Model"));