mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
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:
1 parent
ed59d78e83
commit
ce7b8f511e
5 files changed
+28
No files matched your search
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user