From b03f1dd3969b8b1fb4c8526d37fa75020ca9da86 Mon Sep 17 00:00:00 2001 From: Andrew Frahn Date: Fri, 16 Oct 2026 21:13:35 +1100 Subject: [PATCH 1/3] LR-EDIT-COLOR-TEMP: Kelvin white balance for the Nikon Z5II from built-in presets The Z5II has no colour matrices and its NEFs carry no preset table, so its Temp slider showed the relative -100..+100 scale. A small per-model table (crates/raw/src/kelvin_builtin.rs) gives five (Kelvin, multipliers) points that feed the existing KelvinPresets curve: the NEF decoder attaches them to ColorData::kelvin_presets and kelvin_presets::covers accepts the model. Points: as-shot WB_RBLevels of the body's own shots at Incandescent, Direct sunlight, Kelvin 5500 K, Cloudy and Shade, placed at the temperature the camera itself writes for each preset (maker note ColorTemperatureAuto: 3000, 5210, 5500, 5990, 8000 K). Fluorescent and flash are left out. Natural Auto (camera: 5030 K) reads back at 5036 K. Sources: own design on the existing kelvin_presets model; the camera's own stated temperatures; ExifTool Nikon tag-name documentation (prose), exiftool used as a black-box tool. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/raw/src/kelvin_builtin.rs | 92 ++++++++++++++++++++++++++++++++ crates/raw/src/kelvin_presets.rs | 6 ++- crates/raw/src/lib.rs | 1 + crates/raw/src/vendor/nef.rs | 2 +- docs/parity-checklist.md | 2 +- 5 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 crates/raw/src/kelvin_builtin.rs diff --git a/crates/raw/src/kelvin_builtin.rs b/crates/raw/src/kelvin_builtin.rs new file mode 100644 index 000000000..96a503d81 --- /dev/null +++ b/crates/raw/src/kelvin_builtin.rs @@ -0,0 +1,92 @@ +//! Built-in white-balance presets for cameras whose raws carry neither colour matrices nor a preset table, feeding +//! the Kelvin scale of [`crate::kelvin_presets`]. +//! +//! Nikon NEFs store only the as-shot multipliers (`WB_RBLevels`) and, in the maker note, the colour temperature the +//! camera states for the white-balance setting it shot with (`ColorTemperatureAuto`, `0x0045`: tag name from the +//! ExifTool Nikon tag documentation, read with exiftool run as a black-box tool on the files). A body's points are +//! the as-shot multipliers of its own photos shot at one preset each, placed at the temperature the camera itself +//! wrote for that preset. Presets off the daylight locus (fluorescent, flash) and shots with a fine-tune shift are +//! left out. +//! +//! Sources: own photos of each body listed (a reporter's samples, never committed), the camera's own stated +//! temperatures, and the ExifTool Nikon tag-name documentation (prose). No matrices or third-party data. +use crate::kelvin_presets::KelvinPresets; + +/// One body: its make's first word, its Exif model without the make, and five (Kelvin, R/G, B/G) points. +struct Builtin { + make: &'static str, + model: &'static str, + points: [(f32, f32, f32); 5], +} + +const BUILTINS: &[Builtin] = &[ + // Nikon Z5II (issue emmertex/lightcraft-classic#157): Incandescent 3000 K, Direct sunlight 5210 K, the Kelvin + // setting at 5500 K, Cloudy 5990 K, Shade 8000 K (the temperatures the camera writes in `ColorTemperatureAuto`). + // Natural Auto (the camera wrote 5030 K) reads back at 5036 K; Flash (5410 K nominal) at about 6500 K. + Builtin { + make: "NIKON", + model: "Z5_2", + points: [ + (3000.0, 1.138_671_9, 2.083_984_4), + (5210.0, 1.726_562_5, 1.335_937_5), + (5500.0, 1.771_484_4, 1.281_25), + (5990.0, 1.839_843_8, 1.212_890_6), + (8000.0, 2.097_656_3, 1.089_843_8), + ], + }, +]; + +fn find_builtin(make: Option<&str>, model: &str) -> Option<&'static Builtin> { + let model = model.trim(); + BUILTINS.iter().find(|b| { + let n = b.make.len(); + let bare = match (model.get(..n), model.get(n..)) { + (Some(prefix), Some(rest)) if prefix.eq_ignore_ascii_case(b.make) => rest.trim_start(), + _ => model, + }; + make.is_none_or(|m| m.split_whitespace().next().is_some_and(|w| w.eq_ignore_ascii_case(b.make))) && bare.eq_ignore_ascii_case(b.model) + }) +} + +/// Whether this Exif make and model has built-in presets. +pub fn covers(make: Option<&str>, model: &str) -> bool { + find_builtin(make, model).is_some() +} + +/// The built-in presets of this Exif make and model, if any. +pub fn find(make: Option<&str>, model: &str) -> Option { + let b = find_builtin(make, model)?; + KelvinPresets::new(b.points.map(|(k, r, b)| (k, [r, 1.0, b]))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_builtin_makes_a_curve() { + for b in BUILTINS { + assert!(find(Some(b.make), b.model).is_some(), "{} {}", b.make, b.model); + } + } + + #[test] + fn z5ii_reads_its_own_shots_near_the_camera_temperatures() { + let p = find(Some("NIKON CORPORATION"), "NIKON Z5_2").unwrap(); + // Natural Auto, not on the curve: the camera wrote 5030 K for it + let (t, tint) = p.temp_tint([1.695_312_5, 1.0, 1.373_046_9]).unwrap(); + assert!((1e6 / t - 1e6 / 5030.0).abs() < 10.0 && tint.abs() < 5.0, "{t} {tint}"); + // Flash (nominally 5410 K) sits near the curve on the daylight side + let (t, tint) = p.temp_tint([1.916_015_6, 1.0, 1.164_062_5]).unwrap(); + assert!((5000.0..7500.0).contains(&t) && tint.abs() < 10.0, "{t} {tint}"); + // the 5990 K Kelvin shot with a green-magenta fine-tune shift: daylight, clearly tinted + let (t, tint) = p.temp_tint([1.531_25, 1.0, 1.074_218_8]).unwrap(); + assert!((4000.0..7500.0).contains(&t) && tint.abs() > 20.0, "{t} {tint}"); + } + + #[test] + fn matching() { + assert!(covers(Some("NIKON CORPORATION"), "NIKON Z5_2") && covers(None, "Z5_2") && covers(Some("Nikon"), "nikon z5_2")); + assert!(!covers(Some("NIKON CORPORATION"), "NIKON Z5") && !covers(Some("SONY"), "Z5_2") && !covers(None, "") && !covers(None, "NIK")); + } +} diff --git a/crates/raw/src/kelvin_presets.rs b/crates/raw/src/kelvin_presets.rs index d4e6f33f3..975e578c9 100644 --- a/crates/raw/src/kelvin_presets.rs +++ b/crates/raw/src/kelvin_presets.rs @@ -41,8 +41,12 @@ const MAX_TINT: f64 = 150.0; const MODELS: &[&str] = &["DSC-RX10", "ILCA-77M2", "ILCA-99M2", "ILCE-3500", "ILCE-7S", "SLT-A33"]; /// Whether a raw of this Exif make and model is edited in Kelvin through its presets (when it has no colour -/// matrices of its own or measured: see [`crate::spectral::find`], which comes first). +/// matrices of its own or measured: see [`crate::spectral::find`], which comes first), from the file's presets or +/// built-in ones ([`crate::kelvin_builtin`]). pub fn covers(make: Option<&str>, model: &str) -> bool { + if crate::kelvin_builtin::covers(make, model) { + return true; + } let model = model.trim(); let model = model.strip_prefix("SONY ").or_else(|| model.strip_prefix("Sony ")).unwrap_or(model); make.is_none_or(|m| m.split_whitespace().next().is_some_and(|b| b.eq_ignore_ascii_case("SONY"))) diff --git a/crates/raw/src/lib.rs b/crates/raw/src/lib.rs index 202983331..fe62e87a5 100644 --- a/crates/raw/src/lib.rs +++ b/crates/raw/src/lib.rs @@ -34,6 +34,7 @@ pub mod gaintable; pub mod highlight; #[cfg(feature = "jxl")] mod jxl; +pub mod kelvin_builtin; pub mod kelvin_presets; pub mod lens_metadata; pub mod ljpeg; diff --git a/crates/raw/src/vendor/nef.rs b/crates/raw/src/vendor/nef.rs index 5076c6df9..47f866447 100644 --- a/crates/raw/src/vendor/nef.rs +++ b/crates/raw/src/vendor/nef.rs @@ -298,7 +298,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { active_area: Rect::new(0, 0, active_w, h), crop, orientation: Orientation::from_exif(ifd0.u16(t::ORIENTATION).unwrap_or(1)), - color: ColorData::default(), + color: ColorData { kelvin_presets: crate::kelvin_builtin::find(Some(&make), &model), ..Default::default() }, wb_multipliers: wb, linearized: false, pixel_aspect, diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 95dcd2aef..e2a096d3b 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -193,7 +193,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | LR-EDIT-LIGHT-CURVE-TAT | Drag-on-image curve adjust | P1 | ✅ | `cmd:develop.targeted` (`target: curve`) | | | LR-EDIT-COLOR-WB-PRESET | White-balance presets | P0 | ✅ | `cmd:develop.wb` | | | LR-EDIT-COLOR-WB-PICKER | White-balance eyedropper | P0 | ✅ | `cmd:tool.wbPicker`, `cmd:develop.wbPick`, `crates/ui-egui/src/tests_wb_selector.rs` | the Edit panel's selector button does what W does (from Compare, Survey or the grid it opens the photo in Detail); no magnified loupe while picking | -| LR-EDIT-COLOR-TEMP | Temperature | P0 | ✅ | `ctl:wb.temp`, `crates/pipeline/src/local.rs` (`wb_matrix_for`, `legacy_white`), `crates/raw/src/color.rs` (`rebalance`), `crates/raw/src/spectral.rs` (`find_camera`), `crates/catalog/src/model.rs` (`relative_wb_camera`), `crates/develop/src/controls.rs` (`ControlSpec::position`), `crates/raw/src/kelvin_presets.rs` | Kelvin (2000–50000 K, As Shot read from the file) on raws with colour matrices: in the file (DNG: re-developed for the chosen white through the camera's own matrices, camera-space white balance as Lightroom; the eyedropper / Auto invert the same model) or measured for the camera model (`lightcraft_raw::spectral`, 52 bodies, issue #730: the matrices turn the as-shot multipliers into Kelvin and the slider back, the look stays the file's fit, and the picture is adapted from the as-shot white, Bradford), or read from the camera's own white-balance presets (Sony ARW `SR2SubIFD`, Sony ILCA-99M2, ILCA-77M2, SLT-A33, DSC-RX10, ILCE-3500, ILCE-7S: Sony's Kelvin dial reads back within 0.3 % on 5 settings of 4 bodies); raws without matrices anywhere and rendered files use the relative −100..+100 scale (6500 K / 0 = as shot). The Kelvin slider's track is Lightroom's: 7000 K in the middle, 2000–7000 K evenly on the left half, logarithmic up to 50000 K on the right (within 2 % of positions measured in Lightroom Classic), for drawing, clicks and drags; wheel / arrow-key nudges and Quick Develop still step a fixed 50 / 100 / 500 K; the relative slider stays linear. Settings written on the relative scale before a camera's matrices reached it keep their look (`WbScale`). Not verified against Lightroom's Kelvin: on the ILCE-7M3 the matrices read 7–10 % below Sony's dial (issue #730), and whether the matrices or the dial are off is open; raws without matrices or presets anywhere and rendered files use the relative −100..+100 scale (6500 K / 0 = as shot). Settings written on the relative scale before a camera's matrices reached it keep their look (`WbScale`). Not verified against Lightroom's Kelvin: on the ILCE-7M3 the matrices read 7–10 % below Sony's dial (issue #730), and whether the matrices or the dial are off is open | +| LR-EDIT-COLOR-TEMP | Temperature | P0 | ✅ | `ctl:wb.temp`, `crates/pipeline/src/local.rs` (`wb_matrix_for`, `legacy_white`), `crates/raw/src/color.rs` (`rebalance`), `crates/raw/src/spectral.rs` (`find_camera`), `crates/catalog/src/model.rs` (`relative_wb_camera`), `crates/develop/src/controls.rs` (`ControlSpec::position`), `crates/raw/src/kelvin_presets.rs` | Kelvin (2000–50000 K, As Shot read from the file) on raws with colour matrices: in the file (DNG: re-developed for the chosen white through the camera's own matrices, camera-space white balance as Lightroom; the eyedropper / Auto invert the same model) or measured for the camera model (`lightcraft_raw::spectral`, 52 bodies, issue #730: the matrices turn the as-shot multipliers into Kelvin and the slider back, the look stays the file's fit, and the picture is adapted from the as-shot white, Bradford), or read from the camera's own white-balance presets (Sony ARW `SR2SubIFD`, Sony ILCA-99M2, ILCA-77M2, SLT-A33, DSC-RX10, ILCE-3500, ILCE-7S: Sony's Kelvin dial reads back within 0.3 % on 5 settings of 4 bodies; or built-in presets from the camera's own stated temperatures, `crates/raw/src/kelvin_builtin.rs`: Nikon Z5II, whose Natural Auto shot reads back at 5036 K against the 5030 K the camera wrote); raws without matrices anywhere and rendered files use the relative −100..+100 scale (6500 K / 0 = as shot). The Kelvin slider's track is Lightroom's: 7000 K in the middle, 2000–7000 K evenly on the left half, logarithmic up to 50000 K on the right (within 2 % of positions measured in Lightroom Classic), for drawing, clicks and drags; wheel / arrow-key nudges and Quick Develop still step a fixed 50 / 100 / 500 K; the relative slider stays linear. Settings written on the relative scale before a camera's matrices reached it keep their look (`WbScale`). Not verified against Lightroom's Kelvin: on the ILCE-7M3 the matrices read 7–10 % below Sony's dial (issue #730), and whether the matrices or the dial are off is open; raws without matrices or presets anywhere and rendered files use the relative −100..+100 scale (6500 K / 0 = as shot). Settings written on the relative scale before a camera's matrices reached it keep their look (`WbScale`). Not verified against Lightroom's Kelvin: on the ILCE-7M3 the matrices read 7–10 % below Sony's dial (issue #730), and whether the matrices or the dial are off is open | | LR-EDIT-COLOR-TINT | Tint | P0 | ✅ | `ctl:wb.tint`, `docs/tint-direction.md` | DNG / Lightroom sign: negative (left) adds green, positive (right) magenta; XMP `crs:Tint` values render as in Lightroom (issue #188); CPU/GPU regression tests | | LR-EDIT-COLOR-VIBRANCE | Vibrance | P0 | ✅ | `ctl:color.vibrance` | | | LR-EDIT-COLOR-SATURATION | Saturation | P0 | ✅ | `ctl:color.saturation` | | From d8f1b30bcc1af4f216b2a4d01c307f6a2adc1cff Mon Sep 17 00:00:00 2001 From: Andrew Frahn Date: Tue, 20 Oct 2026 10:04:25 +1100 Subject: [PATCH 2/3] Kelvin presets: match multiword makes in the catalog's joined camera lookup covers_camera tried only the first-space split, so NIKON CORPORATION NIKON Z5_2 stayed on the relative scale in the catalog and editor. Try every split, as spectral::find_camera does. Co-Authored-By: Claude Sonnet 5.5 --- crates/catalog/src/model.rs | 1 + crates/raw/src/kelvin_presets.rs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/catalog/src/model.rs b/crates/catalog/src/model.rs index 6ac3544ee..a3fe36aba 100644 --- a/crates/catalog/src/model.rs +++ b/crates/catalog/src/model.rs @@ -653,6 +653,7 @@ mod edited_tests { ("SONY ILCE-1", "ARW", true), ("", "ARW", true), ("SONY ILCE-7M3", "DNG", false), + ("NIKON CORPORATION NIKON Z5_2", "NEF", false), ] { let mut p = Photo::new(PhotoId(1), Source::Demo { scene: 0 }, "a.raw", format, 10, 10, "2026-10-01T00:00:00"); p.kind = MediaKind::Raw; diff --git a/crates/raw/src/kelvin_presets.rs b/crates/raw/src/kelvin_presets.rs index 975e578c9..f9465a94c 100644 --- a/crates/raw/src/kelvin_presets.rs +++ b/crates/raw/src/kelvin_presets.rs @@ -55,10 +55,9 @@ pub fn covers(make: Option<&str>, model: &str) -> bool { /// [`covers`] for make and model joined by a space, as the catalog keeps them (`"SONY ILCA-99M2"`). pub fn covers_camera(camera: &str) -> bool { - match camera.split_once(char::is_whitespace) { - Some((make, model)) => covers(Some(make), model), - None => covers(None, camera), - } + let words: Vec<&str> = camera.split_whitespace().collect(); + // the make can be several words ("NIKON CORPORATION"), so try every split, as `spectral::find_camera` does + (1..words.len()).any(|i| covers(Some(&words[..i].join(" ")), &words[i..].join(" "))) || covers(None, camera) } /// A camera's multipliers at five stated temperatures, ascending, green = 1. @@ -220,6 +219,7 @@ mod tests { #[test] fn covered_models() { + assert!(covers_camera("NIKON CORPORATION NIKON Z5_2") && covers_camera("NIKON Z5_2") && !covers_camera("NIKON CORPORATION NIKON Z6")); for c in ["SONY ILCA-99M2", "SONY DSC-RX10", "SONY ILCA-77M2", "SONY SLT-A33", "ILCA-99M2", "Sony ilca-99m2"] { assert!(covers_camera(c), "{c}"); } From 376ad8177d9bc1bc8973b4bd38c456e280f1076c Mon Sep 17 00:00:00 2001 From: Andrew Frahn Date: Tue, 20 Oct 2026 12:03:36 +1100 Subject: [PATCH 3/3] Review: correct ColorTemperatureAuto tag id, dedupe relative-scale text Co-Authored-By: Claude Sonnet 5.5 --- crates/raw/src/kelvin_builtin.rs | 2 +- docs/parity-checklist.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/raw/src/kelvin_builtin.rs b/crates/raw/src/kelvin_builtin.rs index 96a503d81..1ce4ddfc4 100644 --- a/crates/raw/src/kelvin_builtin.rs +++ b/crates/raw/src/kelvin_builtin.rs @@ -2,7 +2,7 @@ //! the Kelvin scale of [`crate::kelvin_presets`]. //! //! Nikon NEFs store only the as-shot multipliers (`WB_RBLevels`) and, in the maker note, the colour temperature the -//! camera states for the white-balance setting it shot with (`ColorTemperatureAuto`, `0x0045`: tag name from the +//! camera states for the white-balance setting it shot with (`ColorTemperatureAuto`, `0x004f`: tag name from the //! ExifTool Nikon tag documentation, read with exiftool run as a black-box tool on the files). A body's points are //! the as-shot multipliers of its own photos shot at one preset each, placed at the temperature the camera itself //! wrote for that preset. Presets off the daylight locus (fluorescent, flash) and shots with a fine-tune shift are diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 0184af93c..8fd05399c 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -193,7 +193,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | LR-EDIT-LIGHT-CURVE-TAT | Drag-on-image curve adjust | P1 | ✅ | `cmd:develop.targeted` (`target: curve`) | | | LR-EDIT-COLOR-WB-PRESET | White-balance presets | P0 | ✅ | `cmd:develop.wb` | | | LR-EDIT-COLOR-WB-PICKER | White-balance eyedropper | P0 | ✅ | `cmd:tool.wbPicker`, `cmd:develop.wbPick`, `crates/ui-egui/src/tests_wb_selector.rs` | the Edit panel's selector button does what W does (from Compare, Survey or the grid it opens the photo in Detail); no magnified loupe while picking | -| LR-EDIT-COLOR-TEMP | Temperature | P0 | ✅ | `ctl:wb.temp`, `crates/pipeline/src/local.rs` (`wb_matrix_for`, `legacy_white`), `crates/raw/src/color.rs` (`rebalance`), `crates/raw/src/spectral.rs` (`find_camera`), `crates/catalog/src/model.rs` (`relative_wb_camera`), `crates/develop/src/controls.rs` (`ControlSpec::position`), `crates/raw/src/kelvin_presets.rs` | Kelvin (2000–50000 K, As Shot read from the file) on raws with colour matrices: in the file (DNG: re-developed for the chosen white through the camera's own matrices, camera-space white balance as Lightroom; the eyedropper / Auto invert the same model) or measured for the camera model (`lightcraft_raw::spectral`, 52 bodies, issue #730: the matrices turn the as-shot multipliers into Kelvin and the slider back, the look stays the file's fit, and the picture is adapted from the as-shot white, Bradford), or read from the camera's own white-balance presets (Sony ARW `SR2SubIFD`, Sony ILCA-99M2, ILCA-77M2, SLT-A33, DSC-RX10, ILCE-3500, ILCE-7S: Sony's Kelvin dial reads back within 0.3 % on 5 settings of 4 bodies; or built-in presets from the camera's own stated temperatures, `crates/raw/src/kelvin_builtin.rs`: Nikon Z5II, whose Natural Auto shot reads back at 5036 K against the 5030 K the camera wrote); raws without matrices anywhere and rendered files use the relative −100..+100 scale (6500 K / 0 = as shot). The Kelvin slider's track is Lightroom's: 7000 K in the middle, 2000–7000 K evenly on the left half, logarithmic up to 50000 K on the right (within 2 % of positions measured in Lightroom Classic), for drawing, clicks and drags; wheel / arrow-key nudges and Quick Develop still step a fixed 50 / 100 / 500 K; the relative slider stays linear. Settings written on the relative scale before a camera's matrices reached it keep their look (`WbScale`). Not verified against Lightroom's Kelvin: on the ILCE-7M3 the matrices read 7–10 % below Sony's dial (issue #730), and whether the matrices or the dial are off is open; raws without matrices or presets anywhere and rendered files use the relative −100..+100 scale (6500 K / 0 = as shot). Settings written on the relative scale before a camera's matrices reached it keep their look (`WbScale`). Not verified against Lightroom's Kelvin: on the ILCE-7M3 the matrices read 7–10 % below Sony's dial (issue #730), and whether the matrices or the dial are off is open | +| LR-EDIT-COLOR-TEMP | Temperature | P0 | ✅ | `ctl:wb.temp`, `crates/pipeline/src/local.rs` (`wb_matrix_for`, `legacy_white`), `crates/raw/src/color.rs` (`rebalance`), `crates/raw/src/spectral.rs` (`find_camera`), `crates/catalog/src/model.rs` (`relative_wb_camera`), `crates/develop/src/controls.rs` (`ControlSpec::position`), `crates/raw/src/kelvin_presets.rs` | Kelvin (2000–50000 K, As Shot read from the file) on raws with colour matrices: in the file (DNG: re-developed for the chosen white through the camera's own matrices, camera-space white balance as Lightroom; the eyedropper / Auto invert the same model) or measured for the camera model (`lightcraft_raw::spectral`, 52 bodies, issue #730: the matrices turn the as-shot multipliers into Kelvin and the slider back, the look stays the file's fit, and the picture is adapted from the as-shot white, Bradford), or read from the camera's own white-balance presets (Sony ARW `SR2SubIFD`, Sony ILCA-99M2, ILCA-77M2, SLT-A33, DSC-RX10, ILCE-3500, ILCE-7S: Sony's Kelvin dial reads back within 0.3 % on 5 settings of 4 bodies; or built-in presets from the camera's own stated temperatures, `crates/raw/src/kelvin_builtin.rs`: Nikon Z5II, whose Natural Auto shot reads back at 5036 K against the 5030 K the camera wrote). The Kelvin slider's track is Lightroom's: 7000 K in the middle, 2000–7000 K evenly on the left half, logarithmic up to 50000 K on the right (within 2 % of positions measured in Lightroom Classic), for drawing, clicks and drags; wheel / arrow-key nudges and Quick Develop still step a fixed 50 / 100 / 500 K; the relative slider stays linear. Settings written on the relative scale before a camera's matrices reached it keep their look (`WbScale`). Not verified against Lightroom's Kelvin: on the ILCE-7M3 the matrices read 7–10 % below Sony's dial (issue #730), and whether the matrices or the dial are off is open; raws without matrices or presets anywhere and rendered files use the relative −100..+100 scale (6500 K / 0 = as shot). | | LR-EDIT-COLOR-TINT | Tint | P0 | ✅ | `ctl:wb.tint`, `docs/tint-direction.md` | DNG / Lightroom sign: negative (left) adds green, positive (right) magenta; XMP `crs:Tint` values render as in Lightroom (issue #188); CPU/GPU regression tests | | LR-EDIT-COLOR-VIBRANCE | Vibrance | P0 | ✅ | `ctl:color.vibrance` | | | LR-EDIT-COLOR-SATURATION | Saturation | P0 | ✅ | `ctl:color.saturation` | |