collective

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

diff --git a/crates/raw/src/dngwrite.rs b/crates/raw/src/dngwrite.rs--- a/crates/raw/src/dngwrite.rs+++ b/crates/raw/src/dngwrite.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]));diff --git a/crates/raw/tests/it/dng.rs b/crates/raw/tests/it/dng.rs--- a/crates/raw/tests/it/dng.rs+++ b/crates/raw/tests/it/dng.rs@@ -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);diff --git a/crates/raw/tests/it/rendered_dng.rs b/crates/raw/tests/it/rendered_dng.rs--- a/crates/raw/tests/it/rendered_dng.rs+++ b/crates/raw/tests/it/rendered_dng.rs@@ -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"));