Translate Lightroom's HSL panel, and read the edit inside a DNG
The library's DNGs carry a Lightroom house look in their embedded XMP — Blue +58, Aqua +50, Yellow and Purple +23, Highlights -40, Blacks -20 on most — and that, not the camera profile, is why the same files look richer in Lightroom. The importer skipped exactly that part: the HSL panel was on its list of structures it did not translate. Lightroom's eight HSL bands now map onto the colour mixer, hue, saturation and luminance each one for one: Aqua to our cyan and Purple to our violet, the nearest of our twelve bands by hue; chartreuse, spring, azure and rose are left alone. The figures are a first translation that lr-fit's measurement against Lightroom's output may yet scale. read_embedded finds the XMP packet in a photograph's bytes by its delimiters and translates it, or answers None for a file whose XMP has no Camera Raw settings — darktable's sidecars, a camera's own packet. A test reads the library's _MG_9080.dng when it is present.
This commit is contained in:
@@ -35,7 +35,7 @@
|
|||||||
//! wrong on most images and invisibly so, which is worse than an honest gap,
|
//! wrong on most images and invisibly so, which is worse than an honest gap,
|
||||||
//! so these keys are counted as skipped and reported.
|
//! so these keys are counted as skipped and reported.
|
||||||
//!
|
//!
|
||||||
//! **Tone curves, colour mixing, masks, lens profiles and grain.** Each is a
|
//! **Tone curves, masks, lens profiles and grain.** Each is a
|
||||||
//! structure rather than a number, and each would need its own argument about
|
//! structure rather than a number, and each would need its own argument about
|
||||||
//! whether the two applications mean the same thing. They are skipped by
|
//! whether the two applications mean the same thing. They are skipped by
|
||||||
//! omission — a key not in the table is simply not understood — and the
|
//! omission — a key not in the table is simply not understood — and the
|
||||||
@@ -137,6 +137,158 @@ const MAPPINGS: &[Mapping] = &[
|
|||||||
param: "saturation",
|
param: "saturation",
|
||||||
convert: Convert::Direct,
|
convert: Convert::Direct,
|
||||||
},
|
},
|
||||||
|
// TRACES: FR-DEV-6
|
||||||
|
// Lightroom's HSL panel: eight bands, each ±100 for hue, saturation and
|
||||||
|
// luminance, onto the colour mixer's twelve. Lightroom's bands sit where
|
||||||
|
// ours do except two, matched to the nearest of ours by hue: Aqua (180°)
|
||||||
|
// is our cyan, Purple (270°) our violet; chartreuse, spring, azure and
|
||||||
|
// rose have no Lightroom counterpart and are left alone. One for one, as
|
||||||
|
// a first translation — the band widths differ, and `lr-fit`'s
|
||||||
|
// measurement against Lightroom's own output may yet scale these.
|
||||||
|
Mapping {
|
||||||
|
crs: "HueAdjustmentRed",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "red_hue",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "SaturationAdjustmentRed",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "red_sat",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "LuminanceAdjustmentRed",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "red_lum",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "HueAdjustmentOrange",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "orange_hue",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "SaturationAdjustmentOrange",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "orange_sat",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "LuminanceAdjustmentOrange",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "orange_lum",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "HueAdjustmentYellow",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "yellow_hue",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "SaturationAdjustmentYellow",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "yellow_sat",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "LuminanceAdjustmentYellow",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "yellow_lum",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "HueAdjustmentGreen",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "green_hue",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "SaturationAdjustmentGreen",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "green_sat",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "LuminanceAdjustmentGreen",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "green_lum",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "HueAdjustmentAqua",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "cyan_hue",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "SaturationAdjustmentAqua",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "cyan_sat",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "LuminanceAdjustmentAqua",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "cyan_lum",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "HueAdjustmentBlue",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "blue_hue",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "SaturationAdjustmentBlue",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "blue_sat",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "LuminanceAdjustmentBlue",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "blue_lum",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "HueAdjustmentPurple",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "violet_hue",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "SaturationAdjustmentPurple",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "violet_sat",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "LuminanceAdjustmentPurple",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "violet_lum",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "HueAdjustmentMagenta",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "magenta_hue",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "SaturationAdjustmentMagenta",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "magenta_sat",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
|
Mapping {
|
||||||
|
crs: "LuminanceAdjustmentMagenta",
|
||||||
|
op: "colour_mixer",
|
||||||
|
param: "magenta_lum",
|
||||||
|
convert: Convert::Direct,
|
||||||
|
},
|
||||||
// Adobe's sharpening runs 0…150 where ours runs 0…100, so a preset asking
|
// Adobe's sharpening runs 0…150 where ours runs 0…100, so a preset asking
|
||||||
// for its maximum gets ours rather than being clamped there silently.
|
// for its maximum gets ours rather than being clamped there silently.
|
||||||
Mapping {
|
Mapping {
|
||||||
@@ -192,6 +344,27 @@ pub enum ImportError {
|
|||||||
NoSettings,
|
NoSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-DEV-6
|
||||||
|
/// The Lightroom edit stored inside a photograph — the XMP packet Lightroom
|
||||||
|
/// writes into a DNG — translated as a preset is.
|
||||||
|
///
|
||||||
|
/// `None` where the file carries no packet, or one with no Camera Raw
|
||||||
|
/// settings in it (darktable's sidecars, a camera's own XMP). The packet is
|
||||||
|
/// found by its delimiters rather than by walking the TIFF structure: it is
|
||||||
|
/// plain text by specification, and the same search serves any container.
|
||||||
|
pub fn read_embedded(bytes: &[u8]) -> Option<Import> {
|
||||||
|
const OPEN: &[u8] = b"<x:xmpmeta";
|
||||||
|
const CLOSE: &[u8] = b"</x:xmpmeta>";
|
||||||
|
let start = find(bytes, OPEN)?;
|
||||||
|
let end = start + find(&bytes[start..], CLOSE)? + CLOSE.len();
|
||||||
|
let text = std::str::from_utf8(&bytes[start..end]).ok()?;
|
||||||
|
read_xmp(text).ok().filter(|i| !i.preset.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
||||||
|
haystack.windows(needle.len()).position(|w| w == needle)
|
||||||
|
}
|
||||||
|
|
||||||
/// Read one Lightroom `.xmp` preset.
|
/// Read one Lightroom `.xmp` preset.
|
||||||
///
|
///
|
||||||
/// Tolerant in the same direction the sidecar parser is: a value that will not
|
/// Tolerant in the same direction the sidecar parser is: a value that will not
|
||||||
@@ -375,6 +548,56 @@ mod tests {
|
|||||||
.copied()
|
.copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_dngs_embedded_lightroom_edit_comes_across_with_its_hsl() {
|
||||||
|
// TRACES: FR-DEV-6
|
||||||
|
// The shape Lightroom 6 writes into a DNG, trimmed: the library's
|
||||||
|
// house look, as camera-profiles.md's D21 note records it.
|
||||||
|
let mut file = b"II*\0 binary header bytes ".to_vec();
|
||||||
|
file.extend_from_slice(
|
||||||
|
br#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?><x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:crs="http://ns.adobe.com/camera-raw-settings/1.0/" crs:ProcessVersion="6.7" crs:Exposure2012="0.00" crs:Highlights2012="-40" crs:Blacks2012="-20" crs:SaturationAdjustmentBlue="+58" crs:SaturationAdjustmentAqua="+50" crs:SaturationAdjustmentPurple="+23" crs:HueAdjustmentRed="-5" crs:LuminanceAdjustmentGreen="+7"/></rdf:RDF></x:xmpmeta><?xpacket end="w"?>"#,
|
||||||
|
);
|
||||||
|
file.extend_from_slice(b"\0 more binary");
|
||||||
|
let import = read_embedded(&file).expect("an edit");
|
||||||
|
let p = import.preset.params();
|
||||||
|
let get = |op: &str, param: &str| p.get(&(op.to_string(), param.to_string())).copied();
|
||||||
|
assert_eq!(get("colour_mixer", "blue_sat"), Some(58.0));
|
||||||
|
assert_eq!(get("colour_mixer", "cyan_sat"), Some(50.0));
|
||||||
|
assert_eq!(get("colour_mixer", "violet_sat"), Some(23.0));
|
||||||
|
assert_eq!(get("colour_mixer", "red_hue"), Some(-5.0));
|
||||||
|
assert_eq!(get("colour_mixer", "green_lum"), Some(7.0));
|
||||||
|
assert_eq!(get("highlights_shadows", "highlights"), Some(-40.0));
|
||||||
|
assert_eq!(get("blacks_whites", "blacks"), Some(-20.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_libraries_dngs_carry_the_house_look() {
|
||||||
|
// TRACES: FR-DEV-6
|
||||||
|
// The library's own Lightroom 6 DNG, where it is on this machine.
|
||||||
|
let Some(home) = std::env::var_os("HOME") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let path =
|
||||||
|
std::path::Path::new(&home).join("Nextcloud/PhotosRaw/2017/2017-08-12/_MG_9080.dng");
|
||||||
|
let Ok(bytes) = std::fs::read(&path) else {
|
||||||
|
eprintln!("skipped: no sample DNG at {}", path.display());
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let import = read_embedded(&bytes).expect("Lightroom's edit");
|
||||||
|
let p = import.preset.params();
|
||||||
|
let get = |op: &str, param: &str| p.get(&(op.to_string(), param.to_string())).copied();
|
||||||
|
assert_eq!(get("colour_mixer", "blue_sat"), Some(58.0));
|
||||||
|
assert_eq!(get("colour_mixer", "cyan_sat"), Some(50.0));
|
||||||
|
assert_eq!(get("highlights_shadows", "highlights"), Some(-40.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_file_with_no_camera_raw_settings_has_no_edit() {
|
||||||
|
assert!(read_embedded(b"no packet at all").is_none());
|
||||||
|
let darktable = br#"<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmp:Rating="3"/></rdf:RDF></x:xmpmeta>"#;
|
||||||
|
assert!(read_embedded(darktable).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_mapping_names_a_parameter_this_build_actually_has() {
|
fn every_mapping_names_a_parameter_this_build_actually_has() {
|
||||||
// The test that keeps the table honest. Adobe's half cannot be checked
|
// The test that keeps the table honest. Adobe's half cannot be checked
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user