diff --git a/crates/merge/src/output.rs b/crates/merge/src/output.rs index fc12bb3a..e020ac45 100644 --- a/crates/merge/src/output.rs +++ b/crates/merge/src/output.rs @@ -51,6 +51,7 @@ pub fn color_data(color: &FrameColor) -> ColorData { as_shot_neutral: Some([1.0, 1.0, 1.0]), as_shot_white_xy: None, baseline_exposure: 0.0, + baseline_sharpness: None, profile: Default::default(), } } diff --git a/crates/raw/src/dng.rs b/crates/raw/src/dng.rs index 05b34900..63ddf91a 100644 --- a/crates/raw/src/dng.rs +++ b/crates/raw/src/dng.rs @@ -48,6 +48,8 @@ pub(crate) fn color_data(ifd0: &Ifd, raw: &Ifd, order: ByteOrder) -> ColorData { as_shot_white_xy: get(t::AS_SHOT_WHITE_XY).filter(|v| v.len() == 2 && v[0] > 0.0 && v[1] > 0.0).map(|v| Xy::new(v[0], v[1])), baseline_exposure: get(t::BASELINE_EXPOSURE).and_then(|v| v.first().copied()).filter(|v| v.is_finite()).unwrap_or(0.0) + get(t::BASELINE_EXPOSURE_OFFSET).and_then(|v| v.first().copied()).filter(|v| v.is_finite()).unwrap_or(0.0), + // DNG 1.7 BaselineSharpness (IFD 0 or the enhanced IFD); implausible values are ignored + baseline_sharpness: get(t::BASELINE_SHARPNESS).and_then(|v| v.first().copied()).filter(|v| v.is_finite() && *v > 0.0 && *v <= 16.0), profile: profile_look(ifd0, raw, order), } } diff --git a/crates/raw/src/dngwrite.rs b/crates/raw/src/dngwrite.rs index bf5ccc95..2f04f016 100644 --- a/crates/raw/src/dngwrite.rs +++ b/crates/raw/src/dngwrite.rs @@ -182,6 +182,9 @@ pub fn write_dng(raw: &RawImage, opts: &DngWriteOptions) -> Result> { ifd.set(t::AS_SHOT_NEUTRAL, rat_vec(&m.map(|v| 1.0 / v as f64))); } ifd.set(t::BASELINE_EXPOSURE, Value::SRational(vec![srational(c.baseline_exposure)])); + if let Some(v) = c.baseline_sharpness { + ifd.set(t::BASELINE_SHARPNESS, rat_vec(&[v])); + } // the source's own profile look travels with its data (conversions, smart previews, merges) let p = &c.profile; let table_tags = |ifd: &mut IfdBuilder, table: &crate::profile::HsvTable, dims: u16, enc: u16| { diff --git a/crates/raw/src/lib.rs b/crates/raw/src/lib.rs index 82f1bdf8..6732ad92 100644 --- a/crates/raw/src/lib.rs +++ b/crates/raw/src/lib.rs @@ -516,6 +516,10 @@ pub struct ColorData { pub as_shot_white_xy: Option, /// EV to add for a "normal" rendering (`BaselineExposure` + `BaselineExposureOffset`). pub baseline_exposure: f64, + /// `BaselineSharpness`: sharpening relative to a reference camera (`None`: the DNG default, + /// 1). Read and written back by DNG export; rendering doesn't use it yet. + #[serde(default)] + pub baseline_sharpness: Option, /// The file's own camera-profile look (`ProfileHueSatMap*`, `ProfileLookTable*`, /// `ProfileToneCurve`), applied by [`color`]'s users at render time, and its /// `ProfileGainTableMap*`, kept (a DNG export writes it back) but not rendered. diff --git a/crates/raw/tests/dng.rs b/crates/raw/tests/dng.rs index 02d4f82a..bba5ac81 100644 --- a/crates/raw/tests/dng.rs +++ b/crates/raw/tests/dng.rs @@ -39,6 +39,7 @@ fn synthetic(w: usize, h: usize, cfa: Option, cpp: usize) -> RawImage { ], as_shot_neutral: Some([0.5, 1.0, 0.7]), baseline_exposure: 0.35, + baseline_sharpness: Some(1.5), ..Default::default() }; let metadata = lightcraft_meta::Metadata { make: Some("Synth".into()), model: Some("Cam 1".into()), rating: Some(3), ..Default::default() }; @@ -84,6 +85,7 @@ fn assert_same(a: &RawImage, b: &RawImage) { let (n1, n2) = (a.color.as_shot_neutral.unwrap(), b.color.as_shot_neutral.unwrap()); assert!((0..3).all(|i| (n1[i] - n2[i]).abs() < 1e-6)); assert!((a.color.baseline_exposure - b.color.baseline_exposure).abs() < 1e-6); + assert!(b.color.baseline_sharpness.is_some_and(|v| (v - 1.5).abs() < 1e-6), "{:?}", b.color.baseline_sharpness); assert_eq!(b.metadata.make.as_deref(), Some("Synth")); assert_eq!(b.metadata.rating, Some(3)); } @@ -951,3 +953,19 @@ fn semantic_mask_ifds_past_the_cap_are_not_decoded() { let names: Vec = lightcraft_raw::semantic_masks(&with(MAX_MASKS - 1, 3 * MAX_MASKS)).into_iter().map(|m| m.name).collect(); assert_eq!(names, ["valid 0"]); } + +/// `BaselineSharpness` is read from IFD 0; a zero, negative or absurd value is ignored. +#[test] +fn baseline_sharpness_is_read_when_plausible() { + for (value, want) in [((3, 2), Some(1.5)), ((0, 1), None), ((1, 0), None), ((100, 1), None)] { + let bytes = profile_dng(ByteOrder::Big, |ifd0| { + ifd0.set(t::BASELINE_SHARPNESS, Value::Rational(vec![value])); + }); + assert_eq!(decode(&bytes).unwrap().color.baseline_sharpness, want, "{value:?}"); + } + let bytes = profile_dng(ByteOrder::Big, |ifd0| { + ifd0.set(t::BASELINE_SHARPNESS, Value::SRational(vec![(-1, 1)])); + }); + assert_eq!(decode(&bytes).unwrap().color.baseline_sharpness, None); + assert_eq!(decode(&profile_dng(ByteOrder::Big, |_| {})).unwrap().color.baseline_sharpness, None); +}