LR-IMP-FORMATS: read DNG BaselineSharpness and keep it in DNG exports

ColorData::baseline_sharpness holds the file's BaselineSharpness (raw IFD
or IFD 0; zero, negative, non-finite or above 16 is ignored) and the DNG
writer writes it back, so a conversion or smart preview keeps it. Merge
outputs leave it unset. Rendering doesn't use it yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Exalted5373andClaude Opus 5.5 committed 2026-10-08 18:18:54 -07:00
1 parent ed59d78e83
commit ce7b8f511e
5 files changed
+28

No files matched your search

+1
View File
@@ -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(),
}
}
+2
View File
@@ -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),
}
}
+3
View File
@@ -182,6 +182,9 @@ pub fn write_dng(raw: &RawImage, opts: &DngWriteOptions) -> Result<Vec<u8>> {
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| {
+4
View File
@@ -516,6 +516,10 @@ pub struct ColorData {
pub as_shot_white_xy: Option<Xy>,
/// 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<f64>,
/// 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.
+18
View File
@@ -39,6 +39,7 @@ fn synthetic(w: usize, h: usize, cfa: Option<Cfa>, 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<String> = 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);
}