diff --git a/Cargo.lock b/Cargo.lock index 308db09..cd6f957 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1533,6 +1533,16 @@ dependencies = [ "x11rb", ] +[[package]] +name = "dr-preset-xmp" +version = "0.9.0" +dependencies = [ + "dr-pipeline", + "log", + "quick-xml", + "thiserror 2.0.20", +] + [[package]] name = "dr-segment" version = "0.9.0" @@ -1630,6 +1640,7 @@ dependencies = [ "dr-ingest", "dr-pipeline", "dr-plat", + "dr-preset-xmp", "dr-segment", "dr-sync", "dr-sync-folder", diff --git a/Cargo.toml b/Cargo.toml index 6f5bea1..1a57353 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "core/dr-gpu", "core/dr-lens", "core/dr-pipeline", + "core/dr-preset-xmp", "core/dr-segment", "core/dr-sync", "core/dr-sync-folder", @@ -46,6 +47,7 @@ dr-ingest = { path = "core/dr-ingest" } dr-gpu = { path = "core/dr-gpu" } dr-lens = { path = "core/dr-lens" } dr-pipeline = { path = "core/dr-pipeline" } +dr-preset-xmp = { path = "core/dr-preset-xmp" } # `default-features = false` belongs *here*, not on each dependant: a member # inheriting a workspace dependency cannot turn its default features off, so # writing it below would silently do nothing and every crate touching diff --git a/core/dr-pipeline/src/lib.rs b/core/dr-pipeline/src/lib.rs index c8ffbf8..6222417 100644 --- a/core/dr-pipeline/src/lib.rs +++ b/core/dr-pipeline/src/lib.rs @@ -44,6 +44,7 @@ pub mod ops; pub mod preset; pub mod sidecar; pub mod spot; +pub mod starter; pub mod state; pub use declared::{Declaration, DeclaredOp}; diff --git a/core/dr-pipeline/src/starter.rs b/core/dr-pipeline/src/starter.rs new file mode 100644 index 0000000..d5de100 --- /dev/null +++ b/core/dr-pipeline/src/starter.rs @@ -0,0 +1,220 @@ +//! TRACES: FR-DEV-6 +//! The presets a first run starts with. +//! +//! # Why any at all +//! +//! A preset sheet that opens on "No presets yet" teaches the photographer that +//! the feature is homework. These exist so the first thing the sheet does is +//! demonstrate what a preset *is* — and so that applying one to a selection, +//! which is the action worth discovering, is available before anybody has +//! saved anything. +//! +//! # Why these are ours and not Adobe's +//! +//! Lightroom ships a large bundled set, and importing one of *those* files is +//! what [`crate::preset_import`] is for — a photographer's own library, +//! carried across. Redistributing Adobe's inside this application would be +//! shipping their creative work under a licence that does not permit it, which +//! is a reason on its own; and their numbers are calibrated against their tone +//! curve rather than ours, so the look would not survive the trip even if the +//! licence allowed it. +//! +//! So these are written against this pipeline, in its units, and they are +//! deliberately mild. A starter preset is a starting point — a photographer +//! who wanted the full effect can push the sliders, where one who is handed a +//! caricature learns to distrust the list. +//! +//! # Why they live in the core rather than in the interface +//! +//! Because they name operations, and nothing in `ui/` may +//! (`ui_names_no_operation.rs`, ARCH §4.3a). That test is right to object: a +//! preset called "Punch" *is* a statement about contrast, clarity and +//! vibrance, which makes it a statement in the pipeline's vocabulary rather +//! than a fact about any interface. The frontend asks for the set and stores +//! it; it never learns what is in it. +//! +//! # Why they are seeded rather than merged +//! +//! Written once, on the first run that finds no library at all, and never +//! again. Re-adding them on every start would resurrect one the photographer +//! deleted on purpose, and updating them in place would silently rewrite an +//! edit they had adjusted and kept under the same name. After the first run +//! these are ordinary presets: renameable, editable, deletable, and gone for +//! good when deleted. + +use std::collections::BTreeMap; + +use crate::{Preset, PresetLibrary}; + +/// One starter preset: a name and the parameters that differ from default. +struct Starter { + name: &'static str, + params: &'static [(&'static str, &'static str, f32)], +} + +/// The set. Small on purpose — six a photographer might actually reach for +/// beats forty they have to scroll past. +const STARTERS: &[Starter] = &[ + Starter { + name: "Punch", + params: &[ + ("contrast", "contrast", 18.0), + ("clarity", "amount", 12.0), + ("vibrance", "vibrance", 18.0), + ("blacks_whites", "blacks", -8.0), + ], + }, + Starter { + name: "Soft portrait", + params: &[ + ("contrast", "contrast", -8.0), + ("highlights_shadows", "highlights", -20.0), + ("highlights_shadows", "shadows", 15.0), + ("clarity", "amount", -10.0), + ("vibrance", "vibrance", 10.0), + ("saturation", "saturation", -5.0), + ], + }, + Starter { + name: "Recover the sky", + params: &[ + // The most common single fix in landscape work: a bright sky and a + // dark foreground, both pulled back toward the middle. + ("highlights_shadows", "highlights", -55.0), + ("highlights_shadows", "shadows", 35.0), + ("blacks_whites", "whites", -10.0), + ], + }, + Starter { + name: "Lift the shadows", + params: &[ + ("highlights_shadows", "shadows", 40.0), + ("blacks_whites", "blacks", 12.0), + ("contrast", "contrast", -5.0), + ], + }, + Starter { + name: "Crisp detail", + params: &[ + ("texture", "amount", 20.0), + ("clarity", "amount", 10.0), + ("capture_sharpen", "amount", 35.0), + ], + }, + Starter { + name: "Muted", + params: &[ + ("saturation", "saturation", -30.0), + ("vibrance", "vibrance", 10.0), + ("contrast", "contrast", -10.0), + ("highlights_shadows", "shadows", 12.0), + ], + }, +]; + +/// The starter library, for a device that has never had one. +pub fn library() -> PresetLibrary { + let mut library = PresetLibrary::default(); + for starter in STARTERS { + let params: BTreeMap<(String, String), f32> = starter + .params + .iter() + .map(|(op, param, value)| ((op.to_string(), param.to_string()), *value)) + .collect(); + // The name is a literal in this file, so a refusal would be a bug here + // rather than bad input — but it still must not take the whole set + // down, since the alternative to five presets is not six, it is none. + if let Err(e) = library.insert(starter.name, Preset::from_params(params)) { + log::warn!( + "starter preset {:?} is unusable ({e:?}); skipping", + starter.name + ); + } + } + library +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{EditGraph, Scope}; + + #[test] + fn every_starter_names_parameters_this_build_actually_has() { + // The same guard the importer's table has, for the same reason: a + // renamed parameter must break the build rather than ship a preset + // that quietly does nothing. + let graph = EditGraph::default_chain(); + let capabilities = graph.capabilities(); + for starter in STARTERS { + for (op, param, _) in starter.params { + let capability = capabilities + .iter() + .find(|c| c.id.0 == *op) + .unwrap_or_else(|| panic!("{:?}: no operation {op:?}", starter.name)); + assert!( + capability.params.iter().any(|p| p.id.0 == *param), + "{:?}: operation {op:?} has no parameter {param:?}", + starter.name + ); + } + } + } + + #[test] + fn every_starter_actually_changes_something() { + // A preset that applies to nothing is worse than one fewer preset: it + // teaches the photographer that the list does not work. + for (name, preset) in library().iter() { + assert!(!preset.is_empty(), "{name} carries nothing"); + + let mut graph = EditGraph::default_chain(); + preset.apply(&mut graph, Scope::adjustments()); + assert_ne!( + Preset::capture(&graph), + Preset::default(), + "{name} left the graph at its defaults" + ); + } + } + + #[test] + fn no_starter_carries_a_crop() { + // These are looks, not compositions. One that re-framed every image it + // was applied to would be the exact accident `Scope`'s default exists + // to prevent. + for (name, preset) in library().iter() { + assert!(!preset.touches_framing(), "{name} carries framing"); + } + } + + #[test] + fn the_names_are_distinct() { + assert_eq!(library().len(), STARTERS.len()); + } + + #[test] + fn the_values_stay_inside_what_the_controls_accept() { + // Clamping happens on apply, so an out-of-range literal here would be + // silently trimmed and the preset would not be the one written. + let graph = EditGraph::default_chain(); + for starter in STARTERS { + for (op, param, value) in starter.params { + let mut applied = EditGraph::default_chain(); + let capability = graph + .capabilities() + .into_iter() + .find(|c| c.id.0 == *op) + .unwrap(); + let descriptor = capability.params.iter().find(|p| p.id.0 == *param).unwrap(); + applied.set_param(capability.id, descriptor.id, *value); + assert_eq!( + applied.param(capability.id, descriptor.id), + Some(*value), + "{}: {op}.{param} = {value} was clamped", + starter.name + ); + } + } + } +} diff --git a/core/dr-preset-xmp/Cargo.toml b/core/dr-preset-xmp/Cargo.toml new file mode 100644 index 0000000..37eb61e --- /dev/null +++ b/core/dr-preset-xmp/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "dr-preset-xmp" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +dr-pipeline.workspace = true +# XMP is real XML with namespaces and is not worth hand-rolling. Already in the +# tree for WebDAV, so this is a use rather than a new dependency. +quick-xml.workspace = true +log.workspace = true +thiserror.workspace = true diff --git a/core/dr-preset-xmp/src/lib.rs b/core/dr-preset-xmp/src/lib.rs new file mode 100644 index 0000000..33e7b6d --- /dev/null +++ b/core/dr-preset-xmp/src/lib.rs @@ -0,0 +1,693 @@ +//! TRACES: FR-DEV-6 +//! Reading a Lightroom `.xmp` preset into one of ours. +//! +//! # Why this is possible at all +//! +//! Adobe's Camera Raw settings and ours agree on more than they had any +//! obligation to. Exposure is in stops in both; contrast, the four recovery +//! controls, clarity, texture, vibrance and saturation are all ±100 in both. +//! That is not imitation — it is the convention raw developers converged on, +//! and `highlights_shadows.yaml` cites it in as many words ("matching the +//! convention every other developer uses"). So most of the table below is a +//! rename rather than a conversion. +//! +//! # Why this is its own crate +//! +//! Two walls, and it sits between them. It cannot live in `dr-pipeline`, +//! which depends on nothing deliberately — that is what lets the descriptor +//! and codegen logic be tested without a device (ARCH §6.5a) — and XMP is real +//! XML with namespaces, not worth hand-rolling. And it cannot live in `dr-ui`, +//! because the table below *names operations* and nothing in the interface may +//! (`ui_names_no_operation.rs`, ARCH §4.3a). That test is right to object: a +//! mapping from Adobe's vocabulary to ours is a statement about the pipeline, +//! not about any interface. +//! +//! So: a small crate below the interface and beside the pipeline, which is how +//! the rest of `core/` is already organised. +//! +//! # What is deliberately not imported +//! +//! **White balance.** Lightroom writes `crs:Temperature` as absolute Kelvin +//! for a raw file; ours is a relative ±100 nudge away from what the camera +//! recorded. Converting between them needs the *target image's* as-shot white +//! balance, which is exactly what a preset does not carry — the same preset +//! lands on a frame shot at 3200K and one shot at 7000K. A guess here would be +//! wrong on most images and invisibly so, which is worse than an honest gap, +//! so these keys are counted as skipped and reported. +//! +//! **Tone curves, colour mixing, masks, lens profiles and grain.** Each is a +//! 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 +//! omission — a key not in the table is simply not understood — and the +//! [`Import::skipped`] count is what tells the user their preset arrived +//! partial. + +use std::collections::BTreeMap; + +use dr_pipeline::Preset; +use quick_xml::events::Event; +use quick_xml::XmlVersion; + +/// How a Lightroom value becomes one of ours. +#[derive(Debug, Clone, Copy)] +enum Convert { + /// Same units, same range. Most of the table. + Direct, + /// Same zero, different full deflection — multiply. + /// + /// Carries the factor rather than the two ranges because that is what the + /// arithmetic needs, and a range pair would invite the reader to check it + /// against a descriptor that is not the authority for Adobe's half. + Scale(f32), +} + +/// One key we understand. +struct Mapping { + /// The attribute name inside the `crs` namespace. + crs: &'static str, + op: &'static str, + param: &'static str, + convert: Convert, +} + +/// Every Camera Raw key this build can translate. +/// +/// The op and param names are checked against the pipeline's own descriptors +/// by a test, so a renamed parameter breaks the build rather than silently +/// dropping a setting on import. +const MAPPINGS: &[Mapping] = &[ + // Stops in both. The one mapping that needed no thought at all. + Mapping { + crs: "Exposure2012", + op: "exposure", + param: "exposure", + convert: Convert::Direct, + }, + Mapping { + crs: "Contrast2012", + op: "contrast", + param: "contrast", + convert: Convert::Direct, + }, + Mapping { + crs: "Highlights2012", + op: "highlights_shadows", + param: "highlights", + convert: Convert::Direct, + }, + Mapping { + crs: "Shadows2012", + op: "highlights_shadows", + param: "shadows", + convert: Convert::Direct, + }, + Mapping { + crs: "Whites2012", + op: "blacks_whites", + param: "whites", + convert: Convert::Direct, + }, + Mapping { + crs: "Blacks2012", + op: "blacks_whites", + param: "blacks", + convert: Convert::Direct, + }, + Mapping { + crs: "Clarity2012", + op: "clarity", + param: "amount", + convert: Convert::Direct, + }, + Mapping { + crs: "Texture", + op: "texture", + param: "amount", + convert: Convert::Direct, + }, + Mapping { + crs: "Vibrance", + op: "vibrance", + param: "vibrance", + convert: Convert::Direct, + }, + Mapping { + crs: "Saturation", + op: "saturation", + param: "saturation", + convert: Convert::Direct, + }, + // 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. + Mapping { + crs: "Sharpness", + op: "capture_sharpen", + param: "amount", + convert: Convert::Scale(100.0 / 150.0), + }, + Mapping { + crs: "LuminanceSmoothing", + op: "noise_reduction", + param: "luminance", + convert: Convert::Direct, + }, + Mapping { + crs: "ColorNoiseReduction", + op: "noise_reduction", + param: "chroma", + convert: Convert::Direct, + }, +]; + +/// Keys that are understood well enough to know we are *not* importing them. +/// +/// Separate from silence so the report can say "the white balance did not come +/// across" rather than leaving the photographer to notice. See the module note. +const KNOWN_UNSUPPORTED: &[&str] = &[ + "Temperature", + "Tint", + "ToneCurvePV2012", + "ToneCurvePV2012Red", + "ToneCurvePV2012Green", + "ToneCurvePV2012Blue", +]; + +/// What came of reading one file. +#[derive(Debug, Clone, PartialEq)] +pub struct Import { + /// The preset's name, from `crs:Name` where the file carries one. + pub name: Option, + /// The settings that translated. + pub preset: Preset, + /// Keys recognised as deliberately unsupported — the white balance, the + /// tone curve. Named, so the report can be specific. + pub skipped: Vec, +} + +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum ImportError { + #[error("not readable as XML: {0}")] + NotXml(String), + #[error("no Camera Raw settings in this file")] + NoSettings, +} + +/// Read one Lightroom `.xmp` preset. +/// +/// Tolerant in the same direction the sidecar parser is: a value that will not +/// parse as a number costs that setting and not the file, because a preset +/// that arrives missing its contrast is still worth having and an error +/// message is not. +/// +/// Camera Raw writes its settings either as attributes on an +/// `rdf:Description` or as child elements of one, depending on version and on +/// whether the value is a structure. Both are read, because a photographer's +/// preset folder spans a decade of Lightroom versions and the file does not +/// say which shape it used. +pub fn read_xmp(text: &str) -> Result { + let mut reader = quick_xml::Reader::from_str(text); + reader.config_mut().trim_text(true); + + let mut found: BTreeMap = BTreeMap::new(); + let mut name: Option = None; + let mut pending_element: Option = None; + // Inside `` the text is the preset's name. + let mut in_name = false; + + loop { + match reader.read_event() { + Err(e) => return Err(ImportError::NotXml(e.to_string())), + Ok(Event::Eof) => break, + Ok(Event::Start(e)) | Ok(Event::Empty(e)) => { + let local = local_name(e.name().as_ref()); + if local == "Name" { + in_name = true; + } + pending_element = Some(local.clone()); + + for attribute in e.attributes().flatten() { + let key = local_name(attribute.key.as_ref()); + // `normalized_value` rather than the deprecated + // `unescape_value`. `Implicit1_0` is the right version + // here: an XMP packet opens with `` rather than + // an XML declaration, so the version is unstated and the + // specification says to assume 1.0. + let Ok(value) = attribute.normalized_value(XmlVersion::Implicit1_0) else { + continue; + }; + found.entry(key).or_insert_with(|| value.to_string()); + } + } + Ok(Event::End(e)) => { + if local_name(e.name().as_ref()) == "Name" { + in_name = false; + } + pending_element = None; + } + Ok(Event::Text(e)) => { + let Ok(text) = e.xml10_content() else { + continue; + }; + let text = text.trim().to_string(); + if text.is_empty() { + continue; + } + if in_name && name.is_none() { + name = Some(text.clone()); + } + if let Some(key) = pending_element.clone() { + found.entry(key).or_insert(text); + } + } + _ => {} + } + } + + if found.is_empty() { + return Err(ImportError::NoSettings); + } + + let mut params: BTreeMap<(String, String), f32> = BTreeMap::new(); + for mapping in MAPPINGS { + let Some(raw) = found.get(mapping.crs) else { + continue; + }; + // Adobe writes a leading `+` on positive numbers, which Rust's float + // parser accepts, and occasionally a trailing `%`, which it does not. + let Ok(value) = raw.trim().trim_end_matches('%').parse::() else { + log::debug!("xmp: {} is not a number: {raw:?}", mapping.crs); + continue; + }; + let value = match mapping.convert { + Convert::Direct => value, + Convert::Scale(by) => value * by, + }; + // Out-of-range values are left as they are: `EditGraph::set_param` + // clamps when the preset is applied, and clamping here as well would + // mean two places to be wrong about a range. + params.insert((mapping.op.to_string(), mapping.param.to_string()), value); + } + + let skipped = KNOWN_UNSUPPORTED + .iter() + .filter(|k| found.contains_key(**k)) + .map(|k| k.to_string()) + .collect(); + + if params.is_empty() && name.is_none() { + return Err(ImportError::NoSettings); + } + + Ok(Import { + name, + preset: Preset::from_params(params), + skipped, + }) +} + +/// The part of `ns:Local` after the colon. +/// +/// Namespace prefixes are declared per document and Camera Raw is not the only +/// thing that writes XMP, so matching on the prefix would be matching on a +/// convention rather than on the standard. Comparing local names is what makes +/// a file written by a different tool with a different prefix still readable. +fn local_name(raw: &[u8]) -> String { + let name = String::from_utf8_lossy(raw); + match name.split_once(':') { + Some((_, local)) => local.to_string(), + None => name.to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use dr_pipeline::{EditGraph, Scope}; + + /// The attribute form: settings as attributes on `rdf:Description`. + const ATTRIBUTE_FORM: &str = r#" + + + + + + Warm Portrait + + + + +"#; + + /// The element form: the same settings as child elements. + const ELEMENT_FORM: &str = r#" + + + +0.75 + +25 + + +"#; + + fn value(import: &Import, op: &str, param: &str) -> Option { + import + .preset + .params() + .get(&(op.to_string(), param.to_string())) + .copied() + } + + #[test] + fn every_mapping_names_a_parameter_this_build_actually_has() { + // The test that keeps the table honest. Adobe's half cannot be checked + // from here, but ours can: a renamed operation or parameter must break + // this rather than silently drop a setting on every import. + let graph = EditGraph::default_chain(); + let capabilities = graph.capabilities(); + for mapping in MAPPINGS { + let op = capabilities + .iter() + .find(|c| c.id.0 == mapping.op) + .unwrap_or_else(|| panic!("no operation {:?}", mapping.op)); + assert!( + op.params.iter().any(|p| p.id.0 == mapping.param), + "operation {:?} has no parameter {:?}", + mapping.op, + mapping.param + ); + } + } + + #[test] + fn no_two_mappings_claim_the_same_key_or_the_same_target() { + let mut keys: Vec<&str> = MAPPINGS.iter().map(|m| m.crs).collect(); + keys.sort_unstable(); + let before = keys.len(); + keys.dedup(); + assert_eq!(before, keys.len(), "two mappings read the same crs key"); + + let mut targets: Vec<(&str, &str)> = MAPPINGS.iter().map(|m| (m.op, m.param)).collect(); + targets.sort_unstable(); + let before = targets.len(); + targets.dedup(); + assert_eq!( + before, + targets.len(), + "two mappings write the same parameter" + ); + } + + #[test] + fn the_settings_that_share_a_convention_come_across_unchanged() { + let import = read_xmp(ATTRIBUTE_FORM).unwrap(); + assert_eq!(value(&import, "exposure", "exposure"), Some(0.75)); + assert_eq!(value(&import, "contrast", "contrast"), Some(25.0)); + assert_eq!( + value(&import, "highlights_shadows", "highlights"), + Some(-40.0) + ); + assert_eq!(value(&import, "highlights_shadows", "shadows"), Some(30.0)); + assert_eq!(value(&import, "blacks_whites", "whites"), Some(10.0)); + assert_eq!(value(&import, "blacks_whites", "blacks"), Some(-15.0)); + assert_eq!(value(&import, "clarity", "amount"), Some(12.0)); + assert_eq!(value(&import, "texture", "amount"), Some(8.0)); + assert_eq!(value(&import, "vibrance", "vibrance"), Some(20.0)); + assert_eq!(value(&import, "saturation", "saturation"), Some(-5.0)); + } + + #[test] + fn a_range_that_differs_is_rescaled_rather_than_clamped() { + // Adobe's sharpening runs to 150. A preset asking for half of its + // range should ask for half of ours, not for two thirds of it. + let import = read_xmp(ATTRIBUTE_FORM).unwrap(); + assert_eq!(value(&import, "capture_sharpen", "amount"), Some(50.0)); + } + + #[test] + fn the_white_balance_is_reported_as_skipped_rather_than_guessed_at() { + // The honest gap. Adobe's Kelvin cannot become our relative nudge + // without the target image's as-shot white balance, which a preset + // does not carry. + let import = read_xmp(ATTRIBUTE_FORM).unwrap(); + assert!(value(&import, "white_balance", "temperature").is_none()); + assert!(value(&import, "white_balance", "tint").is_none()); + assert!(import.skipped.contains(&"Temperature".to_string())); + assert!(import.skipped.contains(&"Tint".to_string())); + } + + #[test] + fn the_preset_carries_its_name() { + assert_eq!( + read_xmp(ATTRIBUTE_FORM).unwrap().name.as_deref(), + Some("Warm Portrait") + ); + } + + #[test] + fn settings_written_as_elements_read_the_same_as_settings_written_as_attributes() { + // A preset folder spans a decade of Lightroom versions and the file + // does not say which shape it used. + let import = read_xmp(ELEMENT_FORM).unwrap(); + assert_eq!(value(&import, "exposure", "exposure"), Some(0.75)); + assert_eq!(value(&import, "contrast", "contrast"), Some(25.0)); + } + + #[test] + fn an_imported_preset_applies_like_any_other() { + // The point of the exercise: what comes out is a `Preset`, not a + // second kind of thing with a second apply path. + let import = read_xmp(ATTRIBUTE_FORM).unwrap(); + let mut graph = EditGraph::default_chain(); + import.preset.apply(&mut graph, Scope::adjustments()); + assert_eq!( + graph.param( + dr_pipeline::ops::exposure::ID, + dr_pipeline::ops::exposure::EXPOSURE + ), + Some(0.75) + ); + } + + #[test] + fn a_value_that_is_not_a_number_costs_that_setting_and_not_the_file() { + let text = + ATTRIBUTE_FORM.replace(r#"crs:Contrast2012="+25""#, r#"crs:Contrast2012="lots""#); + let import = read_xmp(&text).unwrap(); + assert_eq!(value(&import, "contrast", "contrast"), None); + assert_eq!(value(&import, "exposure", "exposure"), Some(0.75)); + } + + #[test] + fn a_file_that_is_not_xml_is_refused() { + assert!(matches!( + read_xmp(""), + Err(ImportError::NotXml(_)) | Err(ImportError::NoSettings) + )); + } + + #[test] + fn a_file_with_no_camera_raw_settings_is_refused() { + let text = r#""#; + assert_eq!(read_xmp(text), Err(ImportError::NoSettings)); + } + + #[test] + fn a_prefix_other_than_crs_is_still_read() { + // Namespace prefixes are declared per document. Matching on `crs:` + // would be matching on a convention rather than on the standard. + let text = ELEMENT_FORM.replace("crs:", "cameraraw:"); + let import = read_xmp(&text).unwrap(); + assert_eq!(value(&import, "exposure", "exposure"), Some(0.75)); + } + + // ----------------------------------------------------------------------- + // Reading a folder + // ----------------------------------------------------------------------- + + fn tempdir(name: &str) -> std::path::PathBuf { + let dir = std::env::temp_dir().join(format!( + "dr-preset-import-{name}-{}-{:?}", + std::process::id(), + std::thread::current().id() + )); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + dir + } + + #[test] + fn a_folder_of_presets_is_read_including_its_subfolders() { + // The shape a photographer's presets are actually in: an exported + // Lightroom folder, nested one level per group. + let dir = tempdir("folder"); + std::fs::write(dir.join("one.xmp"), ATTRIBUTE_FORM).unwrap(); + let group = dir.join("Portraits"); + std::fs::create_dir_all(&group).unwrap(); + std::fs::write(group.join("two.xmp"), ELEMENT_FORM).unwrap(); + // Not a preset, and not an error either. + std::fs::write(dir.join("notes.txt"), "ignore me").unwrap(); + + let report = read_path(&dir); + assert_eq!(report.presets.len(), 2, "{report:?}"); + assert_eq!(report.failed, 0); + } + + #[test] + fn a_preset_without_a_name_is_called_after_its_file() { + // Lightroom writes the name it displays, which is not always the file + // name — but a file with no `crs:Name` still has to arrive callable. + let dir = tempdir("unnamed"); + std::fs::write(dir.join("Golden Hour.xmp"), ELEMENT_FORM).unwrap(); + let report = read_path(&dir); + assert_eq!(report.presets[0].0, "Golden Hour"); + } + + #[test] + fn the_displayed_name_wins_over_the_file_name() { + let dir = tempdir("named"); + std::fs::write(dir.join("preset_04b.xmp"), ATTRIBUTE_FORM).unwrap(); + let report = read_path(&dir); + assert_eq!(report.presets[0].0, "Warm Portrait"); + } + + #[test] + fn an_unreadable_file_is_counted_rather_than_stopping_the_import() { + // Ninety presets and one bad file should import eighty-nine. + let dir = tempdir("partial"); + std::fs::write(dir.join("good.xmp"), ATTRIBUTE_FORM).unwrap(); + std::fs::write(dir.join("bad.xmp"), "not xml at all <<<").unwrap(); + let report = read_path(&dir); + assert_eq!(report.presets.len(), 1); + assert_eq!(report.failed, 1); + } + + #[test] + fn the_unsupported_keys_are_gathered_once_and_not_once_per_file() { + // After forty presets, "the white balance did not come across" is the + // useful sentence; forty repetitions of it is not. + let dir = tempdir("gathered"); + std::fs::write(dir.join("a.xmp"), ATTRIBUTE_FORM).unwrap(); + std::fs::write(dir.join("b.xmp"), ATTRIBUTE_FORM).unwrap(); + let report = read_path(&dir); + assert_eq!(report.presets.len(), 2); + assert!(report.unsupported.contains("Temperature")); + assert_eq!( + report + .unsupported + .iter() + .filter(|k| *k == "Temperature") + .count(), + 1 + ); + } + + #[test] + fn a_single_file_can_be_imported_on_its_own() { + let dir = tempdir("single"); + let file = dir.join("one.xmp"); + std::fs::write(&file, ATTRIBUTE_FORM).unwrap(); + assert_eq!(read_path(&file).presets.len(), 1); + } + + #[test] + fn a_path_that_is_not_there_reports_nothing_rather_than_failing() { + let report = read_path(std::path::Path::new("/definitely/not/here")); + assert_eq!(report, Report::default()); + } +} + +// --------------------------------------------------------------------------- +// Reading a folder of them +// --------------------------------------------------------------------------- + +/// What came of reading a path. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct Report { + /// Presets read and named, ready to be stored. + pub presets: Vec<(String, Preset)>, + /// Files that were `.xmp` but did not yield a preset. + pub failed: usize, + /// Every deliberately-unsupported key seen across the whole run. + /// + /// Gathered rather than counted: after importing forty presets, "the white + /// balance did not come across" is the useful sentence, and forty + /// repetitions of it is not. + pub unsupported: std::collections::BTreeSet, +} + +/// Read one `.xmp` file, or every `.xmp` under a folder. +/// +/// A folder because that is the shape a photographer's presets are in — an +/// exported Lightroom preset folder, nested one level per group — and asking +/// them to import ninety files one at a time would be asking them not to +/// bother. Nested folders are walked, which is what makes the group structure +/// available to whatever wants it later. +/// +/// The *name* comes from `crs:Name` where the file carries one and from the +/// file stem where it does not. Lightroom writes the name it displays, which +/// is not always the file name, and the displayed name is the one the +/// photographer will look for. +pub fn read_path(path: &std::path::Path) -> Report { + let mut report = Report::default(); + read_into(path, &mut report); + report.presets.sort_by(|a, b| a.0.cmp(&b.0)); + report +} + +fn read_into(path: &std::path::Path, report: &mut Report) { + if path.is_dir() { + let Ok(entries) = std::fs::read_dir(path) else { + log::warn!("preset import: cannot read {}", path.display()); + return; + }; + // Sorted, so importing the same folder twice reports the same order + // and a name collision resolves the same way both times. + let mut paths: Vec = entries.flatten().map(|e| e.path()).collect(); + paths.sort(); + for path in paths { + read_into(&path, report); + } + return; + } + + let is_xmp = path + .extension() + .is_some_and(|e| e.eq_ignore_ascii_case("xmp")); + if !is_xmp { + return; + } + + let Ok(text) = std::fs::read_to_string(path) else { + report.failed += 1; + return; + }; + match read_xmp(&text) { + Ok(import) => { + let name = import.name.unwrap_or_else(|| { + path.file_stem() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default() + }); + report.unsupported.extend(import.skipped); + report.presets.push((name, import.preset)); + } + Err(e) => { + log::debug!("preset import: {}: {e}", path.display()); + report.failed += 1; + } + } +} diff --git a/docs/traceability.md b/docs/traceability.md index 140843f..bb09ff3 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -9,8 +9,8 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| -| Source files scanned | 292 | -| TRACES tags found | 902 | +| Source files scanned | 294 | +| TRACES tags found | 908 | | Requirements defined | 179 | | Requirements covered | 107 | | **Coverage** | **59.8%** (107/179) | @@ -44,7 +44,7 @@ _None._ | FR-CAT-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/walk.rs:66`](../core/dr-catalog/src/walk.rs#L66), [`core/dr-sync/src/scan.rs:69`](../core/dr-sync/src/scan.rs#L69), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/import.rs:463`](../ui/dr-ui/src/import.rs#L463), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488), [`ui/dr-ui/src/library.rs:2848`](../ui/dr-ui/src/library.rs#L2848), [`ui/dr-ui/src/library.rs:3356`](../ui/dr-ui/src/library.rs#L3356), [`ui/dr-ui/src/library_ui.rs:172`](../ui/dr-ui/src/library_ui.rs#L172), [`ui/dr-ui/src/library_ui.rs:3627`](../ui/dr-ui/src/library_ui.rs#L3627), [`ui/dr-ui/src/library_ui.rs:4592`](../ui/dr-ui/src/library_ui.rs#L4592), [`ui/dr-ui/ui/app.slint:348`](../ui/dr-ui/ui/app.slint#L348), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) | | FR-CAT-4 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/schema.rs:318`](../core/dr-catalog/src/schema.rs#L318), [`ui/dr-ui/src/library.rs:189`](../ui/dr-ui/src/library.rs#L189), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1) | | FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:1059`](../core/dr-catalog/src/schema.rs#L1059), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`ui/dr-ui/src/collections_ui.rs:1558`](../ui/dr-ui/src/collections_ui.rs#L1558), [`ui/dr-ui/src/collections_ui.rs:1577`](../ui/dr-ui/src/collections_ui.rs#L1577), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3917`](../ui/dr-ui/src/library.rs#L3917), [`ui/dr-ui/src/library_ui.rs:631`](../ui/dr-ui/src/library_ui.rs#L631), [`ui/dr-ui/src/library_ui.rs:6389`](../ui/dr-ui/src/library_ui.rs#L6389), [`ui/dr-ui/src/library_ui.rs:6400`](../ui/dr-ui/src/library_ui.rs#L6400), [`ui/dr-ui/src/library_ui.rs:6413`](../ui/dr-ui/src/library_ui.rs#L6413), [`ui/dr-ui/src/library_ui.rs:6428`](../ui/dr-ui/src/library_ui.rs#L6428), [`ui/dr-ui/src/library_ui.rs:6437`](../ui/dr-ui/src/library_ui.rs#L6437), [`ui/dr-ui/ui/app.slint:293`](../ui/dr-ui/ui/app.slint#L293), [`ui/dr-ui/ui/app.slint:474`](../ui/dr-ui/ui/app.slint#L474), [`ui/dr-ui/ui/library.slint:1242`](../ui/dr-ui/ui/library.slint#L1242), [`ui/dr-ui/ui/library.slint:1245`](../ui/dr-ui/ui/library.slint#L1245), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:859`](../ui/dr-ui/ui/library.slint#L859), [`ui/dr-ui/ui/library.slint:911`](../ui/dr-ui/ui/library.slint#L911) | -| FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:213`](../ui/dr-ui/src/library.rs#L213), [`ui/dr-ui/src/library.rs:4163`](../ui/dr-ui/src/library.rs#L4163), [`ui/dr-ui/src/library_ui.rs:321`](../ui/dr-ui/src/library_ui.rs#L321), [`ui/dr-ui/src/library_ui.rs:393`](../ui/dr-ui/src/library_ui.rs#L393), [`ui/dr-ui/src/library_ui.rs:5203`](../ui/dr-ui/src/library_ui.rs#L5203), [`ui/dr-ui/src/library_ui.rs:5256`](../ui/dr-ui/src/library_ui.rs#L5256), [`ui/dr-ui/src/library_ui.rs:6529`](../ui/dr-ui/src/library_ui.rs#L6529), [`ui/dr-ui/ui/app.slint:296`](../ui/dr-ui/ui/app.slint#L296), [`ui/dr-ui/ui/app.slint:474`](../ui/dr-ui/ui/app.slint#L474), [`ui/dr-ui/ui/app.slint:757`](../ui/dr-ui/ui/app.slint#L757), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1318`](../ui/dr-ui/ui/library.slint#L1318), [`ui/dr-ui/ui/library.slint:911`](../ui/dr-ui/ui/library.slint#L911), [`ui/dr-ui/ui/settings.slint:150`](../ui/dr-ui/ui/settings.slint#L150) | +| FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:213`](../ui/dr-ui/src/library.rs#L213), [`ui/dr-ui/src/library.rs:4163`](../ui/dr-ui/src/library.rs#L4163), [`ui/dr-ui/src/library_ui.rs:321`](../ui/dr-ui/src/library_ui.rs#L321), [`ui/dr-ui/src/library_ui.rs:393`](../ui/dr-ui/src/library_ui.rs#L393), [`ui/dr-ui/src/library_ui.rs:5203`](../ui/dr-ui/src/library_ui.rs#L5203), [`ui/dr-ui/src/library_ui.rs:5256`](../ui/dr-ui/src/library_ui.rs#L5256), [`ui/dr-ui/src/library_ui.rs:6529`](../ui/dr-ui/src/library_ui.rs#L6529), [`ui/dr-ui/ui/app.slint:296`](../ui/dr-ui/ui/app.slint#L296), [`ui/dr-ui/ui/app.slint:474`](../ui/dr-ui/ui/app.slint#L474), [`ui/dr-ui/ui/app.slint:762`](../ui/dr-ui/ui/app.slint#L762), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1318`](../ui/dr-ui/ui/library.slint#L1318), [`ui/dr-ui/ui/library.slint:911`](../ui/dr-ui/ui/library.slint#L911), [`ui/dr-ui/ui/settings.slint:150`](../ui/dr-ui/ui/settings.slint#L150) | | FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1673`](../ui/dr-ui/src/collections_ui.rs#L1673), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library_ui.rs:3489`](../ui/dr-ui/src/library_ui.rs#L3489), [`ui/dr-ui/src/library_ui.rs:4182`](../ui/dr-ui/src/library_ui.rs#L4182), [`ui/dr-ui/ui/app.slint:469`](../ui/dr-ui/ui/app.slint#L469), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2585`](../ui/dr-ui/ui/library.slint#L2585), [`ui/dr-ui/ui/library.slint:882`](../ui/dr-ui/ui/library.slint#L882), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901) | | FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`ui/dr-ui/src/develop.rs:3570`](../ui/dr-ui/src/develop.rs#L3570), [`ui/dr-ui/src/develop.rs:3599`](../ui/dr-ui/src/develop.rs#L3599), [`ui/dr-ui/src/export.rs:750`](../ui/dr-ui/src/export.rs#L750), [`ui/dr-ui/src/lib.rs:1429`](../ui/dr-ui/src/lib.rs#L1429), [`ui/dr-ui/src/lib.rs:1830`](../ui/dr-ui/src/lib.rs#L1830), [`ui/dr-ui/src/lib.rs:1965`](../ui/dr-ui/src/lib.rs#L1965), [`ui/dr-ui/src/lib.rs:554`](../ui/dr-ui/src/lib.rs#L554), [`ui/dr-ui/src/lib.rs:979`](../ui/dr-ui/src/lib.rs#L979), [`ui/dr-ui/src/library.rs:1787`](../ui/dr-ui/src/library.rs#L1787), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459), [`ui/dr-ui/src/library.rs:506`](../ui/dr-ui/src/library.rs#L506), [`ui/dr-ui/src/library.rs:543`](../ui/dr-ui/src/library.rs#L543), [`ui/dr-ui/src/library.rs:790`](../ui/dr-ui/src/library.rs#L790), [`ui/dr-ui/src/library_ui.rs:4884`](../ui/dr-ui/src/library_ui.rs#L4884), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | | FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs:120`](../core/dr-types/src/lib.rs#L120), [`ui/dr-ui/src/develop.rs:2997`](../ui/dr-ui/src/develop.rs#L2997), [`ui/dr-ui/src/library.rs:148`](../ui/dr-ui/src/library.rs#L148), [`ui/dr-ui/src/library.rs:1747`](../ui/dr-ui/src/library.rs#L1747), [`ui/dr-ui/src/library.rs:1823`](../ui/dr-ui/src/library.rs#L1823), [`ui/dr-ui/src/library.rs:233`](../ui/dr-ui/src/library.rs#L233), [`ui/dr-ui/src/library.rs:4270`](../ui/dr-ui/src/library.rs#L4270), [`ui/dr-ui/src/library.rs:543`](../ui/dr-ui/src/library.rs#L543), [`ui/dr-ui/src/library.rs:774`](../ui/dr-ui/src/library.rs#L774), [`ui/dr-ui/src/library.rs:790`](../ui/dr-ui/src/library.rs#L790), [`ui/dr-ui/src/library.rs:844`](../ui/dr-ui/src/library.rs#L844), [`ui/dr-ui/src/library_ui.rs:1580`](../ui/dr-ui/src/library_ui.rs#L1580), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:1622`](../ui/dr-ui/src/library_ui.rs#L1622), [`ui/dr-ui/src/library_ui.rs:1716`](../ui/dr-ui/src/library_ui.rs#L1716), [`ui/dr-ui/src/library_ui.rs:232`](../ui/dr-ui/src/library_ui.rs#L232), [`ui/dr-ui/src/library_ui.rs:2332`](../ui/dr-ui/src/library_ui.rs#L2332), [`ui/dr-ui/src/library_ui.rs:265`](../ui/dr-ui/src/library_ui.rs#L265), [`ui/dr-ui/src/library_ui.rs:2770`](../ui/dr-ui/src/library_ui.rs#L2770), [`ui/dr-ui/src/library_ui.rs:2992`](../ui/dr-ui/src/library_ui.rs#L2992), [`ui/dr-ui/src/library_ui.rs:3270`](../ui/dr-ui/src/library_ui.rs#L3270), [`ui/dr-ui/src/library_ui.rs:3358`](../ui/dr-ui/src/library_ui.rs#L3358), [`ui/dr-ui/src/library_ui.rs:3540`](../ui/dr-ui/src/library_ui.rs#L3540), [`ui/dr-ui/src/library_ui.rs:3654`](../ui/dr-ui/src/library_ui.rs#L3654), [`ui/dr-ui/src/library_ui.rs:446`](../ui/dr-ui/src/library_ui.rs#L446), [`ui/dr-ui/src/library_ui.rs:504`](../ui/dr-ui/src/library_ui.rs#L504), [`ui/dr-ui/src/library_ui.rs:5301`](../ui/dr-ui/src/library_ui.rs#L5301), [`ui/dr-ui/src/library_ui.rs:5416`](../ui/dr-ui/src/library_ui.rs#L5416), [`ui/dr-ui/src/presets.rs:446`](../ui/dr-ui/src/presets.rs#L446), [`ui/dr-ui/src/presets.rs:458`](../ui/dr-ui/src/presets.rs#L458), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | @@ -57,32 +57,32 @@ _None._ | FR-CULL-8 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:401`](../core/dr-catalog/src/schema.rs#L401), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/library.rs:2849`](../ui/dr-ui/src/library.rs#L2849), [`ui/dr-ui/src/library.rs:2953`](../ui/dr-ui/src/library.rs#L2953), [`ui/dr-ui/ui/settings.slint:419`](../ui/dr-ui/ui/settings.slint#L419), [`ui/dr-ui/ui/settings.slint:82`](../ui/dr-ui/ui/settings.slint#L82) | | FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`core/dr-face/src/assign.rs:1`](../core/dr-face/src/assign.rs#L1), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1) | | FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:389`](../core/dr-pipeline/src/operation.rs#L389) | -| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:434`](../core/dr-pipeline/src/detail.rs#L434), [`core/dr-pipeline/src/detail.rs:524`](../core/dr-pipeline/src/detail.rs#L524), [`core/dr-pipeline/src/framing.rs:177`](../core/dr-pipeline/src/framing.rs#L177), [`core/dr-pipeline/src/framing.rs:234`](../core/dr-pipeline/src/framing.rs#L234), [`core/dr-pipeline/src/framing.rs:314`](../core/dr-pipeline/src/framing.rs#L314), [`core/dr-pipeline/src/framing.rs:488`](../core/dr-pipeline/src/framing.rs#L488), [`core/dr-pipeline/src/framing.rs:743`](../core/dr-pipeline/src/framing.rs#L743), [`core/dr-pipeline/src/graph.rs:169`](../core/dr-pipeline/src/graph.rs#L169), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/mask.rs:121`](../core/dr-pipeline/src/mask.rs#L121), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:516`](../core/dr-pipeline/src/operation.rs#L516), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:210`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L210), [`core/dr-pipeline/src/ops/curve.rs:100`](../core/dr-pipeline/src/ops/curve.rs#L100), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:219`](../core/dr-pipeline/src/ops/curve.rs#L219), [`core/dr-pipeline/src/ops/curve.rs:635`](../core/dr-pipeline/src/ops/curve.rs#L635), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:273`](../core/dr-pipeline/src/ops/noise_reduction.rs#L273), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:1696`](../core/dr-pipeline/src/sidecar.rs#L1696), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1424`](../ui/dr-ui/src/develop.rs#L1424), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1902`](../ui/dr-ui/src/develop.rs#L1902), [`ui/dr-ui/src/develop.rs:1920`](../ui/dr-ui/src/develop.rs#L1920), [`ui/dr-ui/src/develop.rs:1934`](../ui/dr-ui/src/develop.rs#L1934), [`ui/dr-ui/src/develop.rs:1956`](../ui/dr-ui/src/develop.rs#L1956), [`ui/dr-ui/src/develop.rs:2102`](../ui/dr-ui/src/develop.rs#L2102), [`ui/dr-ui/src/develop.rs:2200`](../ui/dr-ui/src/develop.rs#L2200), [`ui/dr-ui/src/develop.rs:3268`](../ui/dr-ui/src/develop.rs#L3268), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3298`](../ui/dr-ui/src/develop.rs#L3298), [`ui/dr-ui/src/develop.rs:3364`](../ui/dr-ui/src/develop.rs#L3364), [`ui/dr-ui/src/develop.rs:3378`](../ui/dr-ui/src/develop.rs#L3378), [`ui/dr-ui/src/develop.rs:3570`](../ui/dr-ui/src/develop.rs#L3570), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:4230`](../ui/dr-ui/src/develop.rs#L4230), [`ui/dr-ui/src/develop.rs:4284`](../ui/dr-ui/src/develop.rs#L4284), [`ui/dr-ui/src/develop.rs:4328`](../ui/dr-ui/src/develop.rs#L4328), [`ui/dr-ui/src/develop.rs:4378`](../ui/dr-ui/src/develop.rs#L4378), [`ui/dr-ui/src/develop.rs:586`](../ui/dr-ui/src/develop.rs#L586), [`ui/dr-ui/src/develop.rs:647`](../ui/dr-ui/src/develop.rs#L647), [`ui/dr-ui/src/develop.rs:726`](../ui/dr-ui/src/develop.rs#L726), [`ui/dr-ui/src/develop.rs:773`](../ui/dr-ui/src/develop.rs#L773), [`ui/dr-ui/src/develop.rs:802`](../ui/dr-ui/src/develop.rs#L802), [`ui/dr-ui/src/lib.rs:1514`](../ui/dr-ui/src/lib.rs#L1514), [`ui/dr-ui/src/lib.rs:2240`](../ui/dr-ui/src/lib.rs#L2240), [`ui/dr-ui/src/lib.rs:2436`](../ui/dr-ui/src/lib.rs#L2436), [`ui/dr-ui/src/lib.rs:2608`](../ui/dr-ui/src/lib.rs#L2608), [`ui/dr-ui/src/lib.rs:2672`](../ui/dr-ui/src/lib.rs#L2672), [`ui/dr-ui/src/lib.rs:2726`](../ui/dr-ui/src/lib.rs#L2726), [`ui/dr-ui/src/lib.rs:320`](../ui/dr-ui/src/lib.rs#L320), [`ui/dr-ui/src/lib.rs:363`](../ui/dr-ui/src/lib.rs#L363), [`ui/dr-ui/src/lib.rs:386`](../ui/dr-ui/src/lib.rs#L386), [`ui/dr-ui/src/library.rs:506`](../ui/dr-ui/src/library.rs#L506), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/adjust.slint:517`](../ui/dr-ui/ui/adjust.slint#L517), [`ui/dr-ui/ui/adjust.slint:635`](../ui/dr-ui/ui/adjust.slint#L635), [`ui/dr-ui/ui/app.slint:119`](../ui/dr-ui/ui/app.slint#L119), [`ui/dr-ui/ui/app.slint:165`](../ui/dr-ui/ui/app.slint#L165), [`ui/dr-ui/ui/app.slint:1857`](../ui/dr-ui/ui/app.slint#L1857), [`ui/dr-ui/ui/app.slint:867`](../ui/dr-ui/ui/app.slint#L867), [`ui/dr-ui/ui/masks.slint:490`](../ui/dr-ui/ui/masks.slint#L490) | +| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:434`](../core/dr-pipeline/src/detail.rs#L434), [`core/dr-pipeline/src/detail.rs:524`](../core/dr-pipeline/src/detail.rs#L524), [`core/dr-pipeline/src/framing.rs:177`](../core/dr-pipeline/src/framing.rs#L177), [`core/dr-pipeline/src/framing.rs:234`](../core/dr-pipeline/src/framing.rs#L234), [`core/dr-pipeline/src/framing.rs:314`](../core/dr-pipeline/src/framing.rs#L314), [`core/dr-pipeline/src/framing.rs:488`](../core/dr-pipeline/src/framing.rs#L488), [`core/dr-pipeline/src/framing.rs:743`](../core/dr-pipeline/src/framing.rs#L743), [`core/dr-pipeline/src/graph.rs:169`](../core/dr-pipeline/src/graph.rs#L169), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/mask.rs:121`](../core/dr-pipeline/src/mask.rs#L121), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:516`](../core/dr-pipeline/src/operation.rs#L516), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:210`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L210), [`core/dr-pipeline/src/ops/curve.rs:100`](../core/dr-pipeline/src/ops/curve.rs#L100), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:219`](../core/dr-pipeline/src/ops/curve.rs#L219), [`core/dr-pipeline/src/ops/curve.rs:635`](../core/dr-pipeline/src/ops/curve.rs#L635), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:273`](../core/dr-pipeline/src/ops/noise_reduction.rs#L273), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:1696`](../core/dr-pipeline/src/sidecar.rs#L1696), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1424`](../ui/dr-ui/src/develop.rs#L1424), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1902`](../ui/dr-ui/src/develop.rs#L1902), [`ui/dr-ui/src/develop.rs:1920`](../ui/dr-ui/src/develop.rs#L1920), [`ui/dr-ui/src/develop.rs:1934`](../ui/dr-ui/src/develop.rs#L1934), [`ui/dr-ui/src/develop.rs:1956`](../ui/dr-ui/src/develop.rs#L1956), [`ui/dr-ui/src/develop.rs:2102`](../ui/dr-ui/src/develop.rs#L2102), [`ui/dr-ui/src/develop.rs:2200`](../ui/dr-ui/src/develop.rs#L2200), [`ui/dr-ui/src/develop.rs:3268`](../ui/dr-ui/src/develop.rs#L3268), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3298`](../ui/dr-ui/src/develop.rs#L3298), [`ui/dr-ui/src/develop.rs:3364`](../ui/dr-ui/src/develop.rs#L3364), [`ui/dr-ui/src/develop.rs:3378`](../ui/dr-ui/src/develop.rs#L3378), [`ui/dr-ui/src/develop.rs:3570`](../ui/dr-ui/src/develop.rs#L3570), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:4230`](../ui/dr-ui/src/develop.rs#L4230), [`ui/dr-ui/src/develop.rs:4284`](../ui/dr-ui/src/develop.rs#L4284), [`ui/dr-ui/src/develop.rs:4328`](../ui/dr-ui/src/develop.rs#L4328), [`ui/dr-ui/src/develop.rs:4378`](../ui/dr-ui/src/develop.rs#L4378), [`ui/dr-ui/src/develop.rs:586`](../ui/dr-ui/src/develop.rs#L586), [`ui/dr-ui/src/develop.rs:647`](../ui/dr-ui/src/develop.rs#L647), [`ui/dr-ui/src/develop.rs:726`](../ui/dr-ui/src/develop.rs#L726), [`ui/dr-ui/src/develop.rs:773`](../ui/dr-ui/src/develop.rs#L773), [`ui/dr-ui/src/develop.rs:802`](../ui/dr-ui/src/develop.rs#L802), [`ui/dr-ui/src/lib.rs:1514`](../ui/dr-ui/src/lib.rs#L1514), [`ui/dr-ui/src/lib.rs:2240`](../ui/dr-ui/src/lib.rs#L2240), [`ui/dr-ui/src/lib.rs:2436`](../ui/dr-ui/src/lib.rs#L2436), [`ui/dr-ui/src/lib.rs:2608`](../ui/dr-ui/src/lib.rs#L2608), [`ui/dr-ui/src/lib.rs:2672`](../ui/dr-ui/src/lib.rs#L2672), [`ui/dr-ui/src/lib.rs:2726`](../ui/dr-ui/src/lib.rs#L2726), [`ui/dr-ui/src/lib.rs:320`](../ui/dr-ui/src/lib.rs#L320), [`ui/dr-ui/src/lib.rs:363`](../ui/dr-ui/src/lib.rs#L363), [`ui/dr-ui/src/lib.rs:386`](../ui/dr-ui/src/lib.rs#L386), [`ui/dr-ui/src/library.rs:506`](../ui/dr-ui/src/library.rs#L506), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/adjust.slint:517`](../ui/dr-ui/ui/adjust.slint#L517), [`ui/dr-ui/ui/adjust.slint:635`](../ui/dr-ui/ui/adjust.slint#L635), [`ui/dr-ui/ui/app.slint:119`](../ui/dr-ui/ui/app.slint#L119), [`ui/dr-ui/ui/app.slint:165`](../ui/dr-ui/ui/app.slint#L165), [`ui/dr-ui/ui/app.slint:1862`](../ui/dr-ui/ui/app.slint#L1862), [`ui/dr-ui/ui/app.slint:872`](../ui/dr-ui/ui/app.slint#L872), [`ui/dr-ui/ui/masks.slint:490`](../ui/dr-ui/ui/masks.slint#L490) | | FR-DEV-3a | [`core/dr-pipeline/build.rs:756`](../core/dr-pipeline/build.rs#L756), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:194`](../core/dr-pipeline/src/descriptor.rs#L194), [`core/dr-pipeline/src/descriptor.rs:234`](../core/dr-pipeline/src/descriptor.rs#L234), [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`core/dr-pipeline/src/descriptor.rs:313`](../core/dr-pipeline/src/descriptor.rs#L313), [`core/dr-pipeline/src/framing.rs:385`](../core/dr-pipeline/src/framing.rs#L385), [`core/dr-pipeline/src/graph.rs:23`](../core/dr-pipeline/src/graph.rs#L23), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/mask.rs:955`](../core/dr-pipeline/src/mask.rs#L955), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/src/operation.rs:365`](../core/dr-pipeline/src/operation.rs#L365), [`core/dr-pipeline/src/ops/curve.rs:319`](../core/dr-pipeline/src/ops/curve.rs#L319), [`ui/dr-ui/src/develop.rs:1321`](../ui/dr-ui/src/develop.rs#L1321), [`ui/dr-ui/src/lib.rs:669`](../ui/dr-ui/src/lib.rs#L669), [`ui/dr-ui/tests/ui_names_no_operation.rs:1`](../ui/dr-ui/tests/ui_names_no_operation.rs#L1) | | FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`core/dr-pipeline/src/framing.rs:385`](../core/dr-pipeline/src/framing.rs#L385), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/operation.rs:365`](../core/dr-pipeline/src/operation.rs#L365) | | FR-DEV-3c | [`core/dr-pipeline/build.rs:756`](../core/dr-pipeline/build.rs#L756), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/mask.rs:955`](../core/dr-pipeline/src/mask.rs#L955), [`ui/dr-ui/src/develop.rs:4957`](../ui/dr-ui/src/develop.rs#L4957) | | FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:1041`](../core/dr-gpu/src/adjust.rs#L1041), [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/src/adjust.rs:986`](../core/dr-gpu/src/adjust.rs#L986), [`core/dr-gpu/tests/capture_sharpen.rs:434`](../core/dr-gpu/tests/capture_sharpen.rs#L434), [`core/dr-gpu/tests/detail_stage.rs:242`](../core/dr-gpu/tests/detail_stage.rs#L242), [`core/dr-gpu/tests/local_contrast.rs:558`](../core/dr-gpu/tests/local_contrast.rs#L558), [`core/dr-gpu/tests/noise_reduction.rs:556`](../core/dr-gpu/tests/noise_reduction.rs#L556), [`core/dr-pipeline/src/framing.rs:314`](../core/dr-pipeline/src/framing.rs#L314), [`core/dr-pipeline/src/graph.rs:616`](../core/dr-pipeline/src/graph.rs#L616), [`core/dr-pipeline/src/operation.rs:32`](../core/dr-pipeline/src/operation.rs#L32), [`core/dr-pipeline/src/operation.rs:389`](../core/dr-pipeline/src/operation.rs#L389), [`core/dr-pipeline/src/operation.rs:53`](../core/dr-pipeline/src/operation.rs#L53), [`core/dr-pipeline/src/operation.rs:71`](../core/dr-pipeline/src/operation.rs#L71) | | FR-DEV-3e | [`core/dr-decode/src/base_curve.rs:145`](../core/dr-decode/src/base_curve.rs#L145), [`core/dr-decode/src/base_curve.rs:158`](../core/dr-decode/src/base_curve.rs#L158), [`core/dr-decode/src/base_curve.rs:1`](../core/dr-decode/src/base_curve.rs#L1), [`core/dr-decode/src/base_curve.rs:267`](../core/dr-decode/src/base_curve.rs#L267), [`core/dr-decode/src/base_curve.rs:347`](../core/dr-decode/src/base_curve.rs#L347), [`core/dr-decode/src/base_curve.rs:55`](../core/dr-decode/src/base_curve.rs#L55), [`core/dr-decode/src/lib.rs:121`](../core/dr-decode/src/lib.rs#L121), [`core/dr-decode/src/lib.rs:708`](../core/dr-decode/src/lib.rs#L708), [`core/dr-decode/src/lib.rs:748`](../core/dr-decode/src/lib.rs#L748), [`core/dr-decode/src/profile.rs:102`](../core/dr-decode/src/profile.rs#L102), [`core/dr-decode/src/profile.rs:151`](../core/dr-decode/src/profile.rs#L151), [`core/dr-decode/src/profile.rs:1`](../core/dr-decode/src/profile.rs#L1), [`core/dr-decode/src/profile.rs:235`](../core/dr-decode/src/profile.rs#L235), [`core/dr-decode/src/profile.rs:286`](../core/dr-decode/src/profile.rs#L286), [`core/dr-decode/src/profile.rs:343`](../core/dr-decode/src/profile.rs#L343), [`core/dr-decode/src/profile.rs:458`](../core/dr-decode/src/profile.rs#L458), [`core/dr-decode/src/profile.rs:492`](../core/dr-decode/src/profile.rs#L492), [`core/dr-decode/src/profile.rs:630`](../core/dr-decode/src/profile.rs#L630), [`core/dr-gpu/src/adjust.rs:37`](../core/dr-gpu/src/adjust.rs#L37), [`core/dr-gpu/src/adjust.rs:967`](../core/dr-gpu/src/adjust.rs#L967), [`core/dr-gpu/src/demosaic.rs:121`](../core/dr-gpu/src/demosaic.rs#L121), [`core/dr-gpu/src/demosaic.rs:86`](../core/dr-gpu/src/demosaic.rs#L86), [`core/dr-gpu/tests/base_curve.rs:1`](../core/dr-gpu/tests/base_curve.rs#L1), [`core/dr-pipeline/src/operation.rs:1495`](../core/dr-pipeline/src/operation.rs#L1495), [`core/dr-pipeline/src/operation.rs:1576`](../core/dr-pipeline/src/operation.rs#L1576), [`core/dr-pipeline/src/operation.rs:1601`](../core/dr-pipeline/src/operation.rs#L1601), [`core/dr-pipeline/src/operation.rs:1616`](../core/dr-pipeline/src/operation.rs#L1616), [`core/dr-pipeline/src/operation.rs:1640`](../core/dr-pipeline/src/operation.rs#L1640), [`core/dr-pipeline/src/operation.rs:310`](../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/operation.rs:440`](../core/dr-pipeline/src/operation.rs#L440), [`core/dr-pipeline/src/operation.rs:450`](../core/dr-pipeline/src/operation.rs#L450), [`core/dr-pipeline/src/operation.rs:600`](../core/dr-pipeline/src/operation.rs#L600) | -| FR-DEV-3f | [`core/dr-film/src/bake.rs:271`](../core/dr-film/src/bake.rs#L271), [`core/dr-film/src/bake.rs:62`](../core/dr-film/src/bake.rs#L62), [`core/dr-film/src/boolean_grain.rs:1`](../core/dr-film/src/boolean_grain.rs#L1), [`core/dr-film/src/boolean_grain.rs:78`](../core/dr-film/src/boolean_grain.rs#L78), [`core/dr-film/src/grain.rs:140`](../core/dr-film/src/grain.rs#L140), [`core/dr-film/src/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:302`](../core/dr-film/src/grain.rs#L302), [`core/dr-film/src/grain.rs:79`](../core/dr-film/src/grain.rs#L79), [`core/dr-film/src/lib.rs:160`](../core/dr-film/src/lib.rs#L160), [`core/dr-film/src/lib.rs:1`](../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:142`](../core/dr-film/src/profile.rs#L142), [`core/dr-film/src/profile.rs:182`](../core/dr-film/src/profile.rs#L182), [`core/dr-film/src/profile.rs:259`](../core/dr-film/src/profile.rs#L259), [`core/dr-film/src/profile.rs:502`](../core/dr-film/src/profile.rs#L502), [`core/dr-film/src/profile.rs:73`](../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`core/dr-gpu/tests/film_sim.rs:1`](../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:101`](../core/dr-pipeline/src/graph.rs#L101), [`core/dr-pipeline/src/graph.rs:124`](../core/dr-pipeline/src/graph.rs#L124), [`core/dr-pipeline/src/graph.rs:324`](../core/dr-pipeline/src/graph.rs#L324), [`core/dr-pipeline/src/operation.rs:1065`](../core/dr-pipeline/src/operation.rs#L1065), [`core/dr-pipeline/src/operation.rs:1094`](../core/dr-pipeline/src/operation.rs#L1094), [`core/dr-pipeline/src/operation.rs:1495`](../core/dr-pipeline/src/operation.rs#L1495), [`core/dr-pipeline/src/operation.rs:296`](../core/dr-pipeline/src/operation.rs#L296), [`core/dr-pipeline/src/operation.rs:310`](../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/ops/film_sim.rs:129`](../core/dr-pipeline/src/ops/film_sim.rs#L129), [`core/dr-pipeline/src/ops/film_sim.rs:153`](../core/dr-pipeline/src/ops/film_sim.rs#L153), [`core/dr-pipeline/src/ops/film_sim.rs:1`](../core/dr-pipeline/src/ops/film_sim.rs#L1), [`core/dr-pipeline/src/ops/film_sim.rs:331`](../core/dr-pipeline/src/ops/film_sim.rs#L331), [`core/dr-pipeline/src/ops/film_sim.rs:43`](../core/dr-pipeline/src/ops/film_sim.rs#L43), [`core/dr-pipeline/src/ops/film_sim.rs:87`](../core/dr-pipeline/src/ops/film_sim.rs#L87), [`core/dr-pipeline/src/ops/film_sim.rs:92`](../core/dr-pipeline/src/ops/film_sim.rs#L92), [`core/dr-pipeline/src/sidecar.rs:111`](../core/dr-pipeline/src/sidecar.rs#L111), [`core/dr-pipeline/src/sidecar.rs:167`](../core/dr-pipeline/src/sidecar.rs#L167), [`core/dr-pipeline/src/sidecar.rs:1957`](../core/dr-pipeline/src/sidecar.rs#L1957), [`core/dr-pipeline/src/sidecar.rs:2033`](../core/dr-pipeline/src/sidecar.rs#L2033), [`core/dr-pipeline/src/sidecar.rs:533`](../core/dr-pipeline/src/sidecar.rs#L533), [`core/dr-pipeline/src/sidecar.rs:660`](../core/dr-pipeline/src/sidecar.rs#L660), [`core/dr-pipeline/src/sidecar.rs:792`](../core/dr-pipeline/src/sidecar.rs#L792), [`core/dr-pipeline/src/state.rs:100`](../core/dr-pipeline/src/state.rs#L100), [`core/dr-pipeline/src/state.rs:115`](../core/dr-pipeline/src/state.rs#L115), [`core/dr-pipeline/src/state.rs:60`](../core/dr-pipeline/src/state.rs#L60), [`ui/dr-ui/src/develop.rs:3012`](../ui/dr-ui/src/develop.rs#L3012), [`ui/dr-ui/src/develop.rs:3029`](../ui/dr-ui/src/develop.rs#L3029), [`ui/dr-ui/src/develop.rs:3041`](../ui/dr-ui/src/develop.rs#L3041), [`ui/dr-ui/src/develop.rs:3079`](../ui/dr-ui/src/develop.rs#L3079), [`ui/dr-ui/src/develop.rs:3088`](../ui/dr-ui/src/develop.rs#L3088), [`ui/dr-ui/src/develop.rs:3191`](../ui/dr-ui/src/develop.rs#L3191), [`ui/dr-ui/src/develop.rs:3607`](../ui/dr-ui/src/develop.rs#L3607), [`ui/dr-ui/src/develop.rs:3622`](../ui/dr-ui/src/develop.rs#L3622), [`ui/dr-ui/src/lib.rs:2214`](../ui/dr-ui/src/lib.rs#L2214), [`ui/dr-ui/src/lib.rs:602`](../ui/dr-ui/src/lib.rs#L602), [`ui/dr-ui/src/lib.rs:660`](../ui/dr-ui/src/lib.rs#L660), [`ui/dr-ui/src/library.rs:497`](../ui/dr-ui/src/library.rs#L497), [`ui/dr-ui/src/library.rs:745`](../ui/dr-ui/src/library.rs#L745), [`ui/dr-ui/src/presets.rs:395`](../ui/dr-ui/src/presets.rs#L395), [`ui/dr-ui/ui/adjust.slint:1034`](../ui/dr-ui/ui/adjust.slint#L1034), [`ui/dr-ui/ui/adjust.slint:1116`](../ui/dr-ui/ui/adjust.slint#L1116), [`ui/dr-ui/ui/app.slint:2372`](../ui/dr-ui/ui/app.slint#L2372), [`ui/dr-ui/ui/app.slint:600`](../ui/dr-ui/ui/app.slint#L600) | +| FR-DEV-3f | [`core/dr-film/src/bake.rs:271`](../core/dr-film/src/bake.rs#L271), [`core/dr-film/src/bake.rs:62`](../core/dr-film/src/bake.rs#L62), [`core/dr-film/src/boolean_grain.rs:1`](../core/dr-film/src/boolean_grain.rs#L1), [`core/dr-film/src/boolean_grain.rs:78`](../core/dr-film/src/boolean_grain.rs#L78), [`core/dr-film/src/grain.rs:140`](../core/dr-film/src/grain.rs#L140), [`core/dr-film/src/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:302`](../core/dr-film/src/grain.rs#L302), [`core/dr-film/src/grain.rs:79`](../core/dr-film/src/grain.rs#L79), [`core/dr-film/src/lib.rs:160`](../core/dr-film/src/lib.rs#L160), [`core/dr-film/src/lib.rs:1`](../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:142`](../core/dr-film/src/profile.rs#L142), [`core/dr-film/src/profile.rs:182`](../core/dr-film/src/profile.rs#L182), [`core/dr-film/src/profile.rs:259`](../core/dr-film/src/profile.rs#L259), [`core/dr-film/src/profile.rs:502`](../core/dr-film/src/profile.rs#L502), [`core/dr-film/src/profile.rs:73`](../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`core/dr-gpu/tests/film_sim.rs:1`](../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:101`](../core/dr-pipeline/src/graph.rs#L101), [`core/dr-pipeline/src/graph.rs:124`](../core/dr-pipeline/src/graph.rs#L124), [`core/dr-pipeline/src/graph.rs:324`](../core/dr-pipeline/src/graph.rs#L324), [`core/dr-pipeline/src/operation.rs:1065`](../core/dr-pipeline/src/operation.rs#L1065), [`core/dr-pipeline/src/operation.rs:1094`](../core/dr-pipeline/src/operation.rs#L1094), [`core/dr-pipeline/src/operation.rs:1495`](../core/dr-pipeline/src/operation.rs#L1495), [`core/dr-pipeline/src/operation.rs:296`](../core/dr-pipeline/src/operation.rs#L296), [`core/dr-pipeline/src/operation.rs:310`](../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/ops/film_sim.rs:129`](../core/dr-pipeline/src/ops/film_sim.rs#L129), [`core/dr-pipeline/src/ops/film_sim.rs:153`](../core/dr-pipeline/src/ops/film_sim.rs#L153), [`core/dr-pipeline/src/ops/film_sim.rs:1`](../core/dr-pipeline/src/ops/film_sim.rs#L1), [`core/dr-pipeline/src/ops/film_sim.rs:331`](../core/dr-pipeline/src/ops/film_sim.rs#L331), [`core/dr-pipeline/src/ops/film_sim.rs:43`](../core/dr-pipeline/src/ops/film_sim.rs#L43), [`core/dr-pipeline/src/ops/film_sim.rs:87`](../core/dr-pipeline/src/ops/film_sim.rs#L87), [`core/dr-pipeline/src/ops/film_sim.rs:92`](../core/dr-pipeline/src/ops/film_sim.rs#L92), [`core/dr-pipeline/src/sidecar.rs:111`](../core/dr-pipeline/src/sidecar.rs#L111), [`core/dr-pipeline/src/sidecar.rs:167`](../core/dr-pipeline/src/sidecar.rs#L167), [`core/dr-pipeline/src/sidecar.rs:1957`](../core/dr-pipeline/src/sidecar.rs#L1957), [`core/dr-pipeline/src/sidecar.rs:2033`](../core/dr-pipeline/src/sidecar.rs#L2033), [`core/dr-pipeline/src/sidecar.rs:533`](../core/dr-pipeline/src/sidecar.rs#L533), [`core/dr-pipeline/src/sidecar.rs:660`](../core/dr-pipeline/src/sidecar.rs#L660), [`core/dr-pipeline/src/sidecar.rs:792`](../core/dr-pipeline/src/sidecar.rs#L792), [`core/dr-pipeline/src/state.rs:100`](../core/dr-pipeline/src/state.rs#L100), [`core/dr-pipeline/src/state.rs:115`](../core/dr-pipeline/src/state.rs#L115), [`core/dr-pipeline/src/state.rs:60`](../core/dr-pipeline/src/state.rs#L60), [`ui/dr-ui/src/develop.rs:3012`](../ui/dr-ui/src/develop.rs#L3012), [`ui/dr-ui/src/develop.rs:3029`](../ui/dr-ui/src/develop.rs#L3029), [`ui/dr-ui/src/develop.rs:3041`](../ui/dr-ui/src/develop.rs#L3041), [`ui/dr-ui/src/develop.rs:3079`](../ui/dr-ui/src/develop.rs#L3079), [`ui/dr-ui/src/develop.rs:3088`](../ui/dr-ui/src/develop.rs#L3088), [`ui/dr-ui/src/develop.rs:3191`](../ui/dr-ui/src/develop.rs#L3191), [`ui/dr-ui/src/develop.rs:3607`](../ui/dr-ui/src/develop.rs#L3607), [`ui/dr-ui/src/develop.rs:3622`](../ui/dr-ui/src/develop.rs#L3622), [`ui/dr-ui/src/lib.rs:2214`](../ui/dr-ui/src/lib.rs#L2214), [`ui/dr-ui/src/lib.rs:602`](../ui/dr-ui/src/lib.rs#L602), [`ui/dr-ui/src/lib.rs:660`](../ui/dr-ui/src/lib.rs#L660), [`ui/dr-ui/src/library.rs:497`](../ui/dr-ui/src/library.rs#L497), [`ui/dr-ui/src/library.rs:745`](../ui/dr-ui/src/library.rs#L745), [`ui/dr-ui/src/presets.rs:395`](../ui/dr-ui/src/presets.rs#L395), [`ui/dr-ui/ui/adjust.slint:1034`](../ui/dr-ui/ui/adjust.slint#L1034), [`ui/dr-ui/ui/adjust.slint:1116`](../ui/dr-ui/ui/adjust.slint#L1116), [`ui/dr-ui/ui/app.slint:2377`](../ui/dr-ui/ui/app.slint#L2377), [`ui/dr-ui/ui/app.slint:600`](../ui/dr-ui/ui/app.slint#L600) | | FR-DEV-3h | [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-decode/src/preview.rs:29`](../core/dr-decode/src/preview.rs#L29), [`core/dr-pipeline/src/framing.rs:1050`](../core/dr-pipeline/src/framing.rs#L1050), [`core/dr-pipeline/src/framing.rs:328`](../core/dr-pipeline/src/framing.rs#L328), [`core/dr-pipeline/src/framing.rs:488`](../core/dr-pipeline/src/framing.rs#L488), [`core/dr-types/src/lib.rs:337`](../core/dr-types/src/lib.rs#L337), [`core/dr-types/src/lib.rs:445`](../core/dr-types/src/lib.rs#L445), [`core/dr-types/src/lib.rs:457`](../core/dr-types/src/lib.rs#L457), [`core/dr-types/src/lib.rs:473`](../core/dr-types/src/lib.rs#L473), [`ui/dr-ui/src/develop.rs:138`](../ui/dr-ui/src/develop.rs#L138), [`ui/dr-ui/src/develop.rs:2119`](../ui/dr-ui/src/develop.rs#L2119), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322) | | FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:335`](../core/dr-gpu/src/lib.rs#L335), [`ui/dr-ui/ui/crop.slint:1`](../ui/dr-ui/ui/crop.slint#L1) | | FR-DEV-5 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/history.rs:102`](../core/dr-pipeline/src/history.rs#L102), [`core/dr-pipeline/src/history.rs:110`](../core/dr-pipeline/src/history.rs#L110), [`core/dr-pipeline/src/history.rs:127`](../core/dr-pipeline/src/history.rs#L127), [`core/dr-pipeline/src/history.rs:184`](../core/dr-pipeline/src/history.rs#L184), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:234`](../core/dr-pipeline/src/history.rs#L234), [`core/dr-pipeline/src/history.rs:293`](../core/dr-pipeline/src/history.rs#L293), [`core/dr-pipeline/src/history.rs:479`](../core/dr-pipeline/src/history.rs#L479), [`core/dr-pipeline/src/history.rs:489`](../core/dr-pipeline/src/history.rs#L489), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`ui/dr-ui/src/develop.rs:3041`](../ui/dr-ui/src/develop.rs#L3041), [`ui/dr-ui/src/develop.rs:3622`](../ui/dr-ui/src/develop.rs#L3622), [`ui/dr-ui/src/develop.rs:3652`](../ui/dr-ui/src/develop.rs#L3652), [`ui/dr-ui/src/develop.rs:3665`](../ui/dr-ui/src/develop.rs#L3665), [`ui/dr-ui/src/develop.rs:3677`](../ui/dr-ui/src/develop.rs#L3677), [`ui/dr-ui/src/develop.rs:3693`](../ui/dr-ui/src/develop.rs#L3693), [`ui/dr-ui/src/develop.rs:3725`](../ui/dr-ui/src/develop.rs#L3725), [`ui/dr-ui/src/develop.rs:3729`](../ui/dr-ui/src/develop.rs#L3729), [`ui/dr-ui/src/develop.rs:3748`](../ui/dr-ui/src/develop.rs#L3748), [`ui/dr-ui/src/develop.rs:3764`](../ui/dr-ui/src/develop.rs#L3764), [`ui/dr-ui/src/develop.rs:708`](../ui/dr-ui/src/develop.rs#L708), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:215`](../ui/dr-ui/src/labels.rs#L215), [`ui/dr-ui/src/lib.rs:1462`](../ui/dr-ui/src/lib.rs#L1462), [`ui/dr-ui/src/lib.rs:1492`](../ui/dr-ui/src/lib.rs#L1492), [`ui/dr-ui/src/lib.rs:1500`](../ui/dr-ui/src/lib.rs#L1500), [`ui/dr-ui/src/lib.rs:2410`](../ui/dr-ui/src/lib.rs#L2410), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | -| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-pipeline/src/preset.rs:381`](../core/dr-pipeline/src/preset.rs#L381), [`core/dr-pipeline/src/preset.rs:414`](../core/dr-pipeline/src/preset.rs#L414), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:3564`](../ui/dr-ui/src/develop.rs#L3564), [`ui/dr-ui/src/develop.rs:3585`](../ui/dr-ui/src/develop.rs#L3585), [`ui/dr-ui/src/lib.rs:1429`](../ui/dr-ui/src/lib.rs#L1429), [`ui/dr-ui/src/lib.rs:2025`](../ui/dr-ui/src/lib.rs#L2025), [`ui/dr-ui/src/lib.rs:2054`](../ui/dr-ui/src/lib.rs#L2054), [`ui/dr-ui/src/library.rs:1787`](../ui/dr-ui/src/library.rs#L1787), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459), [`ui/dr-ui/src/library.rs:487`](../ui/dr-ui/src/library.rs#L487), [`ui/dr-ui/src/library_ui.rs:2592`](../ui/dr-ui/src/library_ui.rs#L2592), [`ui/dr-ui/src/library_ui.rs:2992`](../ui/dr-ui/src/library_ui.rs#L2992), [`ui/dr-ui/src/library_ui.rs:472`](../ui/dr-ui/src/library_ui.rs#L472), [`ui/dr-ui/src/preset_store.rs:1`](../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/presets.rs:657`](../ui/dr-ui/src/presets.rs#L657), [`ui/dr-ui/ui/adjust.slint:709`](../ui/dr-ui/ui/adjust.slint#L709), [`ui/dr-ui/ui/adjust.slint:736`](../ui/dr-ui/ui/adjust.slint#L736), [`ui/dr-ui/ui/adjust.slint:779`](../ui/dr-ui/ui/adjust.slint#L779), [`ui/dr-ui/ui/app.slint:1451`](../ui/dr-ui/ui/app.slint#L1451), [`ui/dr-ui/ui/app.slint:2461`](../ui/dr-ui/ui/app.slint#L2461), [`ui/dr-ui/ui/app.slint:672`](../ui/dr-ui/ui/app.slint#L672), [`ui/dr-ui/ui/library.slint:1345`](../ui/dr-ui/ui/library.slint#L1345), [`ui/dr-ui/ui/library.slint:1352`](../ui/dr-ui/ui/library.slint#L1352), [`ui/dr-ui/ui/library.slint:837`](../ui/dr-ui/ui/library.slint#L837), [`ui/dr-ui/ui/library.slint:853`](../ui/dr-ui/ui/library.slint#L853), [`ui/dr-ui/ui/library.slint:922`](../ui/dr-ui/ui/library.slint#L922), [`ui/dr-ui/ui/library.slint:937`](../ui/dr-ui/ui/library.slint#L937), [`ui/dr-ui/ui/presets.slint:105`](../ui/dr-ui/ui/presets.slint#L105), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/presets.slint:268`](../ui/dr-ui/ui/presets.slint#L268), [`ui/dr-ui/ui/presets.slint:5`](../ui/dr-ui/ui/presets.slint#L5), [`ui/dr-ui/ui/presets.slint:60`](../ui/dr-ui/ui/presets.slint#L60), [`ui/dr-ui/ui/settings.slint:101`](../ui/dr-ui/ui/settings.slint#L101), [`ui/dr-ui/ui/settings.slint:557`](../ui/dr-ui/ui/settings.slint#L557) | +| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-pipeline/src/preset.rs:381`](../core/dr-pipeline/src/preset.rs#L381), [`core/dr-pipeline/src/preset.rs:414`](../core/dr-pipeline/src/preset.rs#L414), [`core/dr-pipeline/src/starter.rs:1`](../core/dr-pipeline/src/starter.rs#L1), [`core/dr-preset-xmp/src/lib.rs:1`](../core/dr-preset-xmp/src/lib.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:3564`](../ui/dr-ui/src/develop.rs#L3564), [`ui/dr-ui/src/develop.rs:3585`](../ui/dr-ui/src/develop.rs#L3585), [`ui/dr-ui/src/lib.rs:1429`](../ui/dr-ui/src/lib.rs#L1429), [`ui/dr-ui/src/lib.rs:2025`](../ui/dr-ui/src/lib.rs#L2025), [`ui/dr-ui/src/lib.rs:2054`](../ui/dr-ui/src/lib.rs#L2054), [`ui/dr-ui/src/library.rs:1787`](../ui/dr-ui/src/library.rs#L1787), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459), [`ui/dr-ui/src/library.rs:487`](../ui/dr-ui/src/library.rs#L487), [`ui/dr-ui/src/library_ui.rs:2592`](../ui/dr-ui/src/library_ui.rs#L2592), [`ui/dr-ui/src/library_ui.rs:2992`](../ui/dr-ui/src/library_ui.rs#L2992), [`ui/dr-ui/src/library_ui.rs:472`](../ui/dr-ui/src/library_ui.rs#L472), [`ui/dr-ui/src/preset_store.rs:1`](../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/presets.rs:657`](../ui/dr-ui/src/presets.rs#L657), [`ui/dr-ui/src/presets.rs:691`](../ui/dr-ui/src/presets.rs#L691), [`ui/dr-ui/ui/adjust.slint:709`](../ui/dr-ui/ui/adjust.slint#L709), [`ui/dr-ui/ui/adjust.slint:736`](../ui/dr-ui/ui/adjust.slint#L736), [`ui/dr-ui/ui/adjust.slint:779`](../ui/dr-ui/ui/adjust.slint#L779), [`ui/dr-ui/ui/app.slint:1456`](../ui/dr-ui/ui/app.slint#L1456), [`ui/dr-ui/ui/app.slint:2466`](../ui/dr-ui/ui/app.slint#L2466), [`ui/dr-ui/ui/app.slint:672`](../ui/dr-ui/ui/app.slint#L672), [`ui/dr-ui/ui/app.slint:683`](../ui/dr-ui/ui/app.slint#L683), [`ui/dr-ui/ui/library.slint:1345`](../ui/dr-ui/ui/library.slint#L1345), [`ui/dr-ui/ui/library.slint:1352`](../ui/dr-ui/ui/library.slint#L1352), [`ui/dr-ui/ui/library.slint:837`](../ui/dr-ui/ui/library.slint#L837), [`ui/dr-ui/ui/library.slint:853`](../ui/dr-ui/ui/library.slint#L853), [`ui/dr-ui/ui/library.slint:922`](../ui/dr-ui/ui/library.slint#L922), [`ui/dr-ui/ui/library.slint:937`](../ui/dr-ui/ui/library.slint#L937), [`ui/dr-ui/ui/presets.slint:105`](../ui/dr-ui/ui/presets.slint#L105), [`ui/dr-ui/ui/presets.slint:113`](../ui/dr-ui/ui/presets.slint#L113), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/presets.slint:276`](../ui/dr-ui/ui/presets.slint#L276), [`ui/dr-ui/ui/presets.slint:297`](../ui/dr-ui/ui/presets.slint#L297), [`ui/dr-ui/ui/presets.slint:5`](../ui/dr-ui/ui/presets.slint#L5), [`ui/dr-ui/ui/presets.slint:60`](../ui/dr-ui/ui/presets.slint#L60), [`ui/dr-ui/ui/settings.slint:101`](../ui/dr-ui/ui/settings.slint#L101), [`ui/dr-ui/ui/settings.slint:557`](../ui/dr-ui/ui/settings.slint#L557) | | FR-DEV-7 | [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`ui/dr-ui/src/develop.rs:3693`](../ui/dr-ui/src/develop.rs#L3693), [`ui/dr-ui/src/develop.rs:3725`](../ui/dr-ui/src/develop.rs#L3725), [`ui/dr-ui/src/lib.rs:1500`](../ui/dr-ui/src/lib.rs#L1500), [`ui/dr-ui/src/lib.rs:2410`](../ui/dr-ui/src/lib.rs#L2410), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | -| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:387`](../core/dr-gpu/src/detail.rs#L387), [`core/dr-gpu/src/detail.rs:592`](../core/dr-gpu/src/detail.rs#L592), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:410`](../core/dr-pipeline/src/detail.rs#L410), [`core/dr-pipeline/src/detail.rs:434`](../core/dr-pipeline/src/detail.rs#L434), [`core/dr-pipeline/src/detail.rs:469`](../core/dr-pipeline/src/detail.rs#L469), [`core/dr-pipeline/src/detail.rs:555`](../core/dr-pipeline/src/detail.rs#L555), [`core/dr-pipeline/src/graph.rs:113`](../core/dr-pipeline/src/graph.rs#L113), [`core/dr-pipeline/src/graph.rs:191`](../core/dr-pipeline/src/graph.rs#L191), [`core/dr-pipeline/src/graph.rs:685`](../core/dr-pipeline/src/graph.rs#L685), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:554`](../core/dr-pipeline/src/operation.rs#L554), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:672`](../core/dr-pipeline/src/sidecar.rs#L672), [`core/dr-pipeline/src/sidecar.rs:808`](../core/dr-pipeline/src/sidecar.rs#L808), [`core/dr-pipeline/src/sidecar.rs:862`](../core/dr-pipeline/src/sidecar.rs#L862), [`core/dr-pipeline/src/sidecar.rs:892`](../core/dr-pipeline/src/sidecar.rs#L892), [`core/dr-pipeline/src/spot.rs:115`](../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop.rs:2271`](../ui/dr-ui/src/develop.rs#L2271), [`ui/dr-ui/src/develop.rs:2309`](../ui/dr-ui/src/develop.rs#L2309), [`ui/dr-ui/src/develop.rs:2374`](../ui/dr-ui/src/develop.rs#L2374), [`ui/dr-ui/src/develop.rs:2457`](../ui/dr-ui/src/develop.rs#L2457), [`ui/dr-ui/src/develop.rs:2471`](../ui/dr-ui/src/develop.rs#L2471), [`ui/dr-ui/src/develop.rs:756`](../ui/dr-ui/src/develop.rs#L756), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1521`](../ui/dr-ui/src/lib.rs#L1521), [`ui/dr-ui/src/lib.rs:2528`](../ui/dr-ui/src/lib.rs#L2528), [`ui/dr-ui/src/lib.rs:325`](../ui/dr-ui/src/lib.rs#L325), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/src/spots_ui.rs:1`](../ui/dr-ui/src/spots_ui.rs#L1), [`ui/dr-ui/src/spots_ui.rs:265`](../ui/dr-ui/src/spots_ui.rs#L265), [`ui/dr-ui/ui/adjust.slint:810`](../ui/dr-ui/ui/adjust.slint#L810), [`ui/dr-ui/ui/app.slint:109`](../ui/dr-ui/ui/app.slint#L109), [`ui/dr-ui/ui/app.slint:1760`](../ui/dr-ui/ui/app.slint#L1760), [`ui/dr-ui/ui/app.slint:1935`](../ui/dr-ui/ui/app.slint#L1935), [`ui/dr-ui/ui/app.slint:2278`](../ui/dr-ui/ui/app.slint#L2278), [`ui/dr-ui/ui/spots.slint:180`](../ui/dr-ui/ui/spots.slint#L180), [`ui/dr-ui/ui/spots.slint:48`](../ui/dr-ui/ui/spots.slint#L48), [`ui/dr-ui/ui/spots.slint:5`](../ui/dr-ui/ui/spots.slint#L5) | +| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:387`](../core/dr-gpu/src/detail.rs#L387), [`core/dr-gpu/src/detail.rs:592`](../core/dr-gpu/src/detail.rs#L592), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:410`](../core/dr-pipeline/src/detail.rs#L410), [`core/dr-pipeline/src/detail.rs:434`](../core/dr-pipeline/src/detail.rs#L434), [`core/dr-pipeline/src/detail.rs:469`](../core/dr-pipeline/src/detail.rs#L469), [`core/dr-pipeline/src/detail.rs:555`](../core/dr-pipeline/src/detail.rs#L555), [`core/dr-pipeline/src/graph.rs:113`](../core/dr-pipeline/src/graph.rs#L113), [`core/dr-pipeline/src/graph.rs:191`](../core/dr-pipeline/src/graph.rs#L191), [`core/dr-pipeline/src/graph.rs:685`](../core/dr-pipeline/src/graph.rs#L685), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:554`](../core/dr-pipeline/src/operation.rs#L554), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:672`](../core/dr-pipeline/src/sidecar.rs#L672), [`core/dr-pipeline/src/sidecar.rs:808`](../core/dr-pipeline/src/sidecar.rs#L808), [`core/dr-pipeline/src/sidecar.rs:862`](../core/dr-pipeline/src/sidecar.rs#L862), [`core/dr-pipeline/src/sidecar.rs:892`](../core/dr-pipeline/src/sidecar.rs#L892), [`core/dr-pipeline/src/spot.rs:115`](../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop.rs:2271`](../ui/dr-ui/src/develop.rs#L2271), [`ui/dr-ui/src/develop.rs:2309`](../ui/dr-ui/src/develop.rs#L2309), [`ui/dr-ui/src/develop.rs:2374`](../ui/dr-ui/src/develop.rs#L2374), [`ui/dr-ui/src/develop.rs:2457`](../ui/dr-ui/src/develop.rs#L2457), [`ui/dr-ui/src/develop.rs:2471`](../ui/dr-ui/src/develop.rs#L2471), [`ui/dr-ui/src/develop.rs:756`](../ui/dr-ui/src/develop.rs#L756), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1521`](../ui/dr-ui/src/lib.rs#L1521), [`ui/dr-ui/src/lib.rs:2528`](../ui/dr-ui/src/lib.rs#L2528), [`ui/dr-ui/src/lib.rs:325`](../ui/dr-ui/src/lib.rs#L325), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/src/spots_ui.rs:1`](../ui/dr-ui/src/spots_ui.rs#L1), [`ui/dr-ui/src/spots_ui.rs:265`](../ui/dr-ui/src/spots_ui.rs#L265), [`ui/dr-ui/ui/adjust.slint:810`](../ui/dr-ui/ui/adjust.slint#L810), [`ui/dr-ui/ui/app.slint:109`](../ui/dr-ui/ui/app.slint#L109), [`ui/dr-ui/ui/app.slint:1765`](../ui/dr-ui/ui/app.slint#L1765), [`ui/dr-ui/ui/app.slint:1940`](../ui/dr-ui/ui/app.slint#L1940), [`ui/dr-ui/ui/app.slint:2283`](../ui/dr-ui/ui/app.slint#L2283), [`ui/dr-ui/ui/spots.slint:180`](../ui/dr-ui/ui/spots.slint#L180), [`ui/dr-ui/ui/spots.slint:48`](../ui/dr-ui/ui/spots.slint#L48), [`ui/dr-ui/ui/spots.slint:5`](../ui/dr-ui/ui/spots.slint#L5) | | FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2088`](../core/dr-gpu/src/adjust.rs#L2088), [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:2250`](../core/dr-gpu/src/adjust.rs#L2250), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:156`](../core/dr-gpu/src/lib.rs#L156), [`core/dr-gpu/src/lib.rs:176`](../core/dr-gpu/src/lib.rs#L176), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:77`](../core/dr-gpu/src/lib.rs#L77), [`core/dr-gpu/tests/capture_sharpen.rs:200`](../core/dr-gpu/tests/capture_sharpen.rs#L200), [`core/dr-gpu/tests/detail_stage.rs:328`](../core/dr-gpu/tests/detail_stage.rs#L328), [`core/dr-gpu/tests/local_contrast.rs:264`](../core/dr-gpu/tests/local_contrast.rs#L264), [`core/dr-gpu/tests/noise_reduction.rs:378`](../core/dr-gpu/tests/noise_reduction.rs#L378), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:524`](../core/dr-pipeline/src/detail.rs#L524), [`core/dr-pipeline/src/graph.rs:547`](../core/dr-pipeline/src/graph.rs#L547), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:659`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L659), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:971`](../core/dr-pipeline/src/ops/local_contrast.rs#L971), [`core/dr-pipeline/src/ops/noise_reduction.rs:700`](../core/dr-pipeline/src/ops/noise_reduction.rs#L700), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop.rs:2795`](../ui/dr-ui/src/develop.rs#L2795), [`ui/dr-ui/src/develop.rs:4019`](../ui/dr-ui/src/develop.rs#L4019), [`ui/dr-ui/src/develop.rs:4502`](../ui/dr-ui/src/develop.rs#L4502), [`ui/dr-ui/src/develop.rs:4536`](../ui/dr-ui/src/develop.rs#L4536), [`ui/dr-ui/src/lib.rs:73`](../ui/dr-ui/src/lib.rs#L73), [`ui/dr-ui/src/lib.rs:810`](../ui/dr-ui/src/lib.rs#L810), [`ui/dr-ui/src/lib.rs:869`](../ui/dr-ui/src/lib.rs#L869) | | FR-DSP-3 | [`core/dr-gpu/tests/frame_budget.rs:101`](../core/dr-gpu/tests/frame_budget.rs#L101), [`core/dr-gpu/tests/local_contrast.rs:329`](../core/dr-gpu/tests/local_contrast.rs#L329), [`core/dr-pipeline/src/ops/local_contrast.rs:447`](../core/dr-pipeline/src/ops/local_contrast.rs#L447) | | FR-DSP-5 | [`core/dr-gpu/tests/frame_budget.rs:101`](../core/dr-gpu/tests/frame_budget.rs#L101), [`core/dr-gpu/tests/zoom_resolution.rs:135`](../core/dr-gpu/tests/zoom_resolution.rs#L135), [`core/dr-gpu/tests/zoom_resolution.rs:166`](../core/dr-gpu/tests/zoom_resolution.rs#L166), [`core/dr-gpu/tests/zoom_resolution.rs:1`](../core/dr-gpu/tests/zoom_resolution.rs#L1), [`core/dr-gpu/tests/zoom_resolution.rs:216`](../core/dr-gpu/tests/zoom_resolution.rs#L216) | | FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:483`](../core/dr-pipeline/src/operation.rs#L483), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`ui/dr-ui/src/develop.rs:2825`](../ui/dr-ui/src/develop.rs#L2825), [`ui/dr-ui/src/develop.rs:5794`](../ui/dr-ui/src/develop.rs#L5794), [`ui/dr-ui/src/lib.rs:1477`](../ui/dr-ui/src/lib.rs#L1477), [`ui/dr-ui/src/lib.rs:2847`](../ui/dr-ui/src/lib.rs#L2847) | | FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:2874`](../ui/dr-ui/src/develop.rs#L2874), [`ui/dr-ui/src/develop.rs:5604`](../ui/dr-ui/src/develop.rs#L5604), [`ui/dr-ui/src/develop.rs:5636`](../ui/dr-ui/src/develop.rs#L5636), [`ui/dr-ui/src/develop.rs:719`](../ui/dr-ui/src/develop.rs#L719), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1577`](../ui/dr-ui/src/lib.rs#L1577), [`ui/dr-ui/src/lib.rs:315`](../ui/dr-ui/src/lib.rs#L315), [`ui/dr-ui/ui/app.slint:69`](../ui/dr-ui/ui/app.slint#L69), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) | -| FR-DSP-8 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/icc.rs:1`](../platform/dr-plat/src/display/icc.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1), [`ui/dr-ui/src/develop.rs:2771`](../ui/dr-ui/src/develop.rs#L2771), [`ui/dr-ui/src/develop.rs:2825`](../ui/dr-ui/src/develop.rs#L2825), [`ui/dr-ui/src/develop.rs:5794`](../ui/dr-ui/src/develop.rs#L5794), [`ui/dr-ui/src/develop.rs:5840`](../ui/dr-ui/src/develop.rs#L5840), [`ui/dr-ui/src/develop.rs:5859`](../ui/dr-ui/src/develop.rs#L5859), [`ui/dr-ui/src/develop.rs:787`](../ui/dr-ui/src/develop.rs#L787), [`ui/dr-ui/src/display_ui.rs:192`](../ui/dr-ui/src/display_ui.rs#L192), [`ui/dr-ui/src/display_ui.rs:1`](../ui/dr-ui/src/display_ui.rs#L1), [`ui/dr-ui/src/display_ui.rs:325`](../ui/dr-ui/src/display_ui.rs#L325), [`ui/dr-ui/src/display_ui.rs:346`](../ui/dr-ui/src/display_ui.rs#L346), [`ui/dr-ui/src/display_ui.rs:379`](../ui/dr-ui/src/display_ui.rs#L379), [`ui/dr-ui/src/lib.rs:1451`](../ui/dr-ui/src/lib.rs#L1451), [`ui/dr-ui/src/lib.rs:1477`](../ui/dr-ui/src/lib.rs#L1477), [`ui/dr-ui/src/lib.rs:2824`](../ui/dr-ui/src/lib.rs#L2824), [`ui/dr-ui/src/lib.rs:2847`](../ui/dr-ui/src/lib.rs#L2847), [`ui/dr-ui/ui/app.slint:1620`](../ui/dr-ui/ui/app.slint#L1620), [`ui/dr-ui/ui/app.slint:48`](../ui/dr-ui/ui/app.slint#L48), [`ui/dr-ui/ui/settings.slint:124`](../ui/dr-ui/ui/settings.slint#L124), [`ui/dr-ui/ui/settings.slint:794`](../ui/dr-ui/ui/settings.slint#L794) | +| FR-DSP-8 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/icc.rs:1`](../platform/dr-plat/src/display/icc.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1), [`ui/dr-ui/src/develop.rs:2771`](../ui/dr-ui/src/develop.rs#L2771), [`ui/dr-ui/src/develop.rs:2825`](../ui/dr-ui/src/develop.rs#L2825), [`ui/dr-ui/src/develop.rs:5794`](../ui/dr-ui/src/develop.rs#L5794), [`ui/dr-ui/src/develop.rs:5840`](../ui/dr-ui/src/develop.rs#L5840), [`ui/dr-ui/src/develop.rs:5859`](../ui/dr-ui/src/develop.rs#L5859), [`ui/dr-ui/src/develop.rs:787`](../ui/dr-ui/src/develop.rs#L787), [`ui/dr-ui/src/display_ui.rs:192`](../ui/dr-ui/src/display_ui.rs#L192), [`ui/dr-ui/src/display_ui.rs:1`](../ui/dr-ui/src/display_ui.rs#L1), [`ui/dr-ui/src/display_ui.rs:325`](../ui/dr-ui/src/display_ui.rs#L325), [`ui/dr-ui/src/display_ui.rs:346`](../ui/dr-ui/src/display_ui.rs#L346), [`ui/dr-ui/src/display_ui.rs:379`](../ui/dr-ui/src/display_ui.rs#L379), [`ui/dr-ui/src/lib.rs:1451`](../ui/dr-ui/src/lib.rs#L1451), [`ui/dr-ui/src/lib.rs:1477`](../ui/dr-ui/src/lib.rs#L1477), [`ui/dr-ui/src/lib.rs:2824`](../ui/dr-ui/src/lib.rs#L2824), [`ui/dr-ui/src/lib.rs:2847`](../ui/dr-ui/src/lib.rs#L2847), [`ui/dr-ui/ui/app.slint:1625`](../ui/dr-ui/ui/app.slint#L1625), [`ui/dr-ui/ui/app.slint:48`](../ui/dr-ui/ui/app.slint#L48), [`ui/dr-ui/ui/settings.slint:124`](../ui/dr-ui/ui/settings.slint#L124), [`ui/dr-ui/ui/settings.slint:794`](../ui/dr-ui/ui/settings.slint#L794) | | FR-EXP-1 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2398`](../core/dr-gpu/src/adjust.rs#L2398), [`core/dr-pipeline/src/graph.rs:537`](../core/dr-pipeline/src/graph.rs#L537), [`core/dr-pipeline/src/graph.rs:591`](../core/dr-pipeline/src/graph.rs#L591), [`core/dr-pipeline/src/operation.rs:483`](../core/dr-pipeline/src/operation.rs#L483), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:623`](../core/dr-types/src/settings.rs#L623), [`ui/dr-ui/src/develop.rs:5859`](../ui/dr-ui/src/develop.rs#L5859), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-3 | [`core/dr-export/src/lib.rs:182`](../core/dr-export/src/lib.rs#L182), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/size.rs:136`](../core/dr-export/src/size.rs#L136), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`core/dr-export/src/size.rs:25`](../core/dr-export/src/size.rs#L25), [`core/dr-export/src/size.rs:84`](../core/dr-export/src/size.rs#L84), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:668`](../core/dr-types/src/settings.rs#L668), [`core/dr-types/src/settings.rs:676`](../core/dr-types/src/settings.rs#L676), [`core/dr-types/src/settings.rs:692`](../core/dr-types/src/settings.rs#L692), [`core/dr-types/src/settings.rs:761`](../core/dr-types/src/settings.rs#L761), [`core/dr-types/src/settings.rs:789`](../core/dr-types/src/settings.rs#L789), [`core/dr-types/src/settings.rs:800`](../core/dr-types/src/settings.rs#L800), [`core/dr-types/src/settings.rs:806`](../core/dr-types/src/settings.rs#L806), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:203`](../ui/dr-ui/src/settings_ui.rs#L203), [`ui/dr-ui/src/settings_ui.rs:216`](../ui/dr-ui/src/settings_ui.rs#L216), [`ui/dr-ui/src/settings_ui.rs:231`](../ui/dr-ui/src/settings_ui.rs#L231), [`ui/dr-ui/src/settings_ui.rs:561`](../ui/dr-ui/src/settings_ui.rs#L561), [`ui/dr-ui/ui/settings.slint:697`](../ui/dr-ui/ui/settings.slint#L697), [`ui/dr-ui/ui/settings.slint:707`](../ui/dr-ui/ui/settings.slint#L707) | | FR-EXP-4 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/sharpen.rs:1`](../core/dr-export/src/sharpen.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-5 | [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | FR-EXP-6 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/name.rs:1`](../core/dr-export/src/name.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:415`](../ui/dr-ui/src/lib.rs#L415), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:49`](../ui/dr-ui/src/settings_ui.rs#L49), [`ui/dr-ui/src/settings_ui.rs:675`](../ui/dr-ui/src/settings_ui.rs#L675) | -| FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:942`](../ui/dr-ui/src/export.rs#L942), [`ui/dr-ui/src/lib.rs:205`](../ui/dr-ui/src/lib.rs#L205), [`ui/dr-ui/src/lib.rs:2156`](../ui/dr-ui/src/lib.rs#L2156), [`ui/dr-ui/src/lib.rs:415`](../ui/dr-ui/src/lib.rs#L415), [`ui/dr-ui/src/lib.rs:450`](../ui/dr-ui/src/lib.rs#L450), [`ui/dr-ui/src/lib.rs:477`](../ui/dr-ui/src/lib.rs#L477), [`ui/dr-ui/src/library_ui.rs:3375`](../ui/dr-ui/src/library_ui.rs#L3375), [`ui/dr-ui/src/library_ui.rs:563`](../ui/dr-ui/src/library_ui.rs#L563), [`ui/dr-ui/src/library_ui.rs:6371`](../ui/dr-ui/src/library_ui.rs#L6371), [`ui/dr-ui/src/library_ui.rs:6448`](../ui/dr-ui/src/library_ui.rs#L6448), [`ui/dr-ui/src/library_ui.rs:6460`](../ui/dr-ui/src/library_ui.rs#L6460), [`ui/dr-ui/src/library_ui.rs:663`](../ui/dr-ui/src/library_ui.rs#L663), [`ui/dr-ui/src/library_ui.rs:720`](../ui/dr-ui/src/library_ui.rs#L720), [`ui/dr-ui/ui/app.slint:1003`](../ui/dr-ui/ui/app.slint#L1003), [`ui/dr-ui/ui/app.slint:1461`](../ui/dr-ui/ui/app.slint#L1461), [`ui/dr-ui/ui/library.slint:1355`](../ui/dr-ui/ui/library.slint#L1355), [`ui/dr-ui/ui/library.slint:841`](../ui/dr-ui/ui/library.slint#L841), [`ui/dr-ui/ui/library.slint:951`](../ui/dr-ui/ui/library.slint#L951) | +| FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:942`](../ui/dr-ui/src/export.rs#L942), [`ui/dr-ui/src/lib.rs:205`](../ui/dr-ui/src/lib.rs#L205), [`ui/dr-ui/src/lib.rs:2156`](../ui/dr-ui/src/lib.rs#L2156), [`ui/dr-ui/src/lib.rs:415`](../ui/dr-ui/src/lib.rs#L415), [`ui/dr-ui/src/lib.rs:450`](../ui/dr-ui/src/lib.rs#L450), [`ui/dr-ui/src/lib.rs:477`](../ui/dr-ui/src/lib.rs#L477), [`ui/dr-ui/src/library_ui.rs:3375`](../ui/dr-ui/src/library_ui.rs#L3375), [`ui/dr-ui/src/library_ui.rs:563`](../ui/dr-ui/src/library_ui.rs#L563), [`ui/dr-ui/src/library_ui.rs:6371`](../ui/dr-ui/src/library_ui.rs#L6371), [`ui/dr-ui/src/library_ui.rs:6448`](../ui/dr-ui/src/library_ui.rs#L6448), [`ui/dr-ui/src/library_ui.rs:6460`](../ui/dr-ui/src/library_ui.rs#L6460), [`ui/dr-ui/src/library_ui.rs:663`](../ui/dr-ui/src/library_ui.rs#L663), [`ui/dr-ui/src/library_ui.rs:720`](../ui/dr-ui/src/library_ui.rs#L720), [`ui/dr-ui/ui/app.slint:1008`](../ui/dr-ui/ui/app.slint#L1008), [`ui/dr-ui/ui/app.slint:1466`](../ui/dr-ui/ui/app.slint#L1466), [`ui/dr-ui/ui/library.slint:1355`](../ui/dr-ui/ui/library.slint#L1355), [`ui/dr-ui/ui/library.slint:841`](../ui/dr-ui/ui/library.slint#L841), [`ui/dr-ui/ui/library.slint:951`](../ui/dr-ui/ui/library.slint#L951) | | FR-EXP-8 | [`core/dr-decode/src/lib.rs:326`](../core/dr-decode/src/lib.rs#L326), [`core/dr-decode/src/lib.rs:350`](../core/dr-decode/src/lib.rs#L350), [`core/dr-decode/src/lib.rs:364`](../core/dr-decode/src/lib.rs#L364), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:136`](../core/dr-export/src/lib.rs#L136), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:653`](../core/dr-types/src/lib.rs#L653), [`core/dr-types/src/settings.rs:336`](../core/dr-types/src/settings.rs#L336), [`ui/dr-ui/src/export.rs:619`](../ui/dr-ui/src/export.rs#L619), [`ui/dr-ui/src/export.rs:647`](../ui/dr-ui/src/export.rs#L647), [`ui/dr-ui/src/export.rs:777`](../ui/dr-ui/src/export.rs#L777), [`ui/dr-ui/src/export.rs:794`](../ui/dr-ui/src/export.rs#L794), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-9 | [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1068`](../core/dr-gpu/src/adjust.rs#L1068), [`ui/dr-ui/src/develop.rs:2956`](../ui/dr-ui/src/develop.rs#L2956), [`ui/dr-ui/src/lib.rs:415`](../ui/dr-ui/src/lib.rs#L415) | | FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:355`](../core/dr-sync/src/account.rs#L355), [`ui/dr-ui/src/launch.rs:277`](../ui/dr-ui/src/launch.rs#L277), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61), [`ui/dr-ui/src/launch_ui.rs:417`](../ui/dr-ui/src/launch_ui.rs#L417) | @@ -94,7 +94,7 @@ _None._ | FR-NC-4 | [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-nextcloud/src/propfind.rs:103`](../core/dr-sync-nextcloud/src/propfind.rs#L103), [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`core/dr-sync/src/capability.rs:6`](../core/dr-sync/src/capability.rs#L6), [`core/dr-sync/src/lib.rs:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61) | | FR-NC-5 | [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488) | | FR-NC-6 | [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1) | -| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-catalog/src/schema.rs:1014`](../core/dr-catalog/src/schema.rs#L1014), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3174`](../ui/dr-ui/src/collections_ui.rs#L3174), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/lib.rs:1860`](../ui/dr-ui/src/lib.rs#L1860), [`ui/dr-ui/src/lib.rs:2910`](../ui/dr-ui/src/lib.rs#L2910), [`ui/dr-ui/src/library.rs:1449`](../ui/dr-ui/src/library.rs#L1449), [`ui/dr-ui/src/library.rs:1472`](../ui/dr-ui/src/library.rs#L1472), [`ui/dr-ui/src/library.rs:1747`](../ui/dr-ui/src/library.rs#L1747), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1239`](../ui/dr-ui/src/library_ui.rs#L1239), [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294), [`ui/dr-ui/src/library_ui.rs:1429`](../ui/dr-ui/src/library_ui.rs#L1429), [`ui/dr-ui/src/library_ui.rs:1547`](../ui/dr-ui/src/library_ui.rs#L1547), [`ui/dr-ui/src/library_ui.rs:2074`](../ui/dr-ui/src/library_ui.rs#L2074), [`ui/dr-ui/src/library_ui.rs:273`](../ui/dr-ui/src/library_ui.rs#L273), [`ui/dr-ui/src/library_ui.rs:284`](../ui/dr-ui/src/library_ui.rs#L284), [`ui/dr-ui/src/library_ui.rs:292`](../ui/dr-ui/src/library_ui.rs#L292), [`ui/dr-ui/src/library_ui.rs:304`](../ui/dr-ui/src/library_ui.rs#L304), [`ui/dr-ui/src/library_ui.rs:313`](../ui/dr-ui/src/library_ui.rs#L313), [`ui/dr-ui/src/library_ui.rs:405`](../ui/dr-ui/src/library_ui.rs#L405), [`ui/dr-ui/src/library_ui.rs:415`](../ui/dr-ui/src/library_ui.rs#L415), [`ui/dr-ui/src/library_ui.rs:457`](../ui/dr-ui/src/library_ui.rs#L457), [`ui/dr-ui/src/library_ui.rs:520`](../ui/dr-ui/src/library_ui.rs#L520), [`ui/dr-ui/src/library_ui.rs:5318`](../ui/dr-ui/src/library_ui.rs#L5318), [`ui/dr-ui/src/library_ui.rs:5336`](../ui/dr-ui/src/library_ui.rs#L5336), [`ui/dr-ui/src/library_ui.rs:5348`](../ui/dr-ui/src/library_ui.rs#L5348), [`ui/dr-ui/src/library_ui.rs:551`](../ui/dr-ui/src/library_ui.rs#L551), [`ui/dr-ui/src/library_ui.rs:563`](../ui/dr-ui/src/library_ui.rs#L563), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:2451`](../ui/dr-ui/ui/app.slint#L2451), [`ui/dr-ui/ui/app.slint:463`](../ui/dr-ui/ui/app.slint#L463), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:997`](../ui/dr-ui/ui/library.slint#L997) | +| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-catalog/src/schema.rs:1014`](../core/dr-catalog/src/schema.rs#L1014), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3174`](../ui/dr-ui/src/collections_ui.rs#L3174), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/lib.rs:1860`](../ui/dr-ui/src/lib.rs#L1860), [`ui/dr-ui/src/lib.rs:2910`](../ui/dr-ui/src/lib.rs#L2910), [`ui/dr-ui/src/library.rs:1449`](../ui/dr-ui/src/library.rs#L1449), [`ui/dr-ui/src/library.rs:1472`](../ui/dr-ui/src/library.rs#L1472), [`ui/dr-ui/src/library.rs:1747`](../ui/dr-ui/src/library.rs#L1747), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1239`](../ui/dr-ui/src/library_ui.rs#L1239), [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294), [`ui/dr-ui/src/library_ui.rs:1429`](../ui/dr-ui/src/library_ui.rs#L1429), [`ui/dr-ui/src/library_ui.rs:1547`](../ui/dr-ui/src/library_ui.rs#L1547), [`ui/dr-ui/src/library_ui.rs:2074`](../ui/dr-ui/src/library_ui.rs#L2074), [`ui/dr-ui/src/library_ui.rs:273`](../ui/dr-ui/src/library_ui.rs#L273), [`ui/dr-ui/src/library_ui.rs:284`](../ui/dr-ui/src/library_ui.rs#L284), [`ui/dr-ui/src/library_ui.rs:292`](../ui/dr-ui/src/library_ui.rs#L292), [`ui/dr-ui/src/library_ui.rs:304`](../ui/dr-ui/src/library_ui.rs#L304), [`ui/dr-ui/src/library_ui.rs:313`](../ui/dr-ui/src/library_ui.rs#L313), [`ui/dr-ui/src/library_ui.rs:405`](../ui/dr-ui/src/library_ui.rs#L405), [`ui/dr-ui/src/library_ui.rs:415`](../ui/dr-ui/src/library_ui.rs#L415), [`ui/dr-ui/src/library_ui.rs:457`](../ui/dr-ui/src/library_ui.rs#L457), [`ui/dr-ui/src/library_ui.rs:520`](../ui/dr-ui/src/library_ui.rs#L520), [`ui/dr-ui/src/library_ui.rs:5318`](../ui/dr-ui/src/library_ui.rs#L5318), [`ui/dr-ui/src/library_ui.rs:5336`](../ui/dr-ui/src/library_ui.rs#L5336), [`ui/dr-ui/src/library_ui.rs:5348`](../ui/dr-ui/src/library_ui.rs#L5348), [`ui/dr-ui/src/library_ui.rs:551`](../ui/dr-ui/src/library_ui.rs#L551), [`ui/dr-ui/src/library_ui.rs:563`](../ui/dr-ui/src/library_ui.rs#L563), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:2456`](../ui/dr-ui/ui/app.slint#L2456), [`ui/dr-ui/ui/app.slint:463`](../ui/dr-ui/ui/app.slint#L463), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:997`](../ui/dr-ui/ui/library.slint#L997) | | FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294) | | FR-NC-6c | [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:73`](../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync-folder/src/lib.rs:803`](../core/dr-sync-folder/src/lib.rs#L803), [`core/dr-sync-folder/src/lib.rs:842`](../core/dr-sync-folder/src/lib.rs#L842), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1), [`core/dr-sync-nextcloud/src/desktop_client.rs:167`](../core/dr-sync-nextcloud/src/desktop_client.rs#L167), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-sync/src/error.rs:39`](../core/dr-sync/src/error.rs#L39), [`core/dr-sync/src/lib.rs:158`](../core/dr-sync/src/lib.rs#L158), [`core/dr-sync/src/types.rs:101`](../core/dr-sync/src/types.rs#L101), [`core/dr-types/src/lib.rs:120`](../core/dr-types/src/lib.rs#L120), [`core/dr-types/src/lib.rs:202`](../core/dr-types/src/lib.rs#L202), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3174`](../ui/dr-ui/src/collections_ui.rs#L3174), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/derived_sync.rs:555`](../ui/dr-ui/src/derived_sync.rs#L555), [`ui/dr-ui/src/derived_sync.rs:627`](../ui/dr-ui/src/derived_sync.rs#L627), [`ui/dr-ui/src/library.rs:1569`](../ui/dr-ui/src/library.rs#L1569), [`ui/dr-ui/src/library.rs:1659`](../ui/dr-ui/src/library.rs#L1659), [`ui/dr-ui/src/library.rs:3138`](../ui/dr-ui/src/library.rs#L3138), [`ui/dr-ui/src/library.rs:3476`](../ui/dr-ui/src/library.rs#L3476), [`ui/dr-ui/src/library.rs:948`](../ui/dr-ui/src/library.rs#L948), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1337`](../ui/dr-ui/src/library_ui.rs#L1337), [`ui/dr-ui/src/remote.rs:40`](../ui/dr-ui/src/remote.rs#L40), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260) | | FR-NC-7 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:104`](../core/dr-sync-nextcloud/src/lib.rs#L104), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:3356`](../ui/dr-ui/src/library.rs#L3356), [`ui/dr-ui/src/library_ui.rs:3627`](../ui/dr-ui/src/library_ui.rs#L3627), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387) | @@ -113,13 +113,13 @@ _None._ | FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:205`](../ui/dr-ui/src/lib.rs#L205) | | FR-RAW-5 | [`core/dr-decode/src/lib.rs:167`](../core/dr-decode/src/lib.rs#L167), [`core/dr-gpu/src/demosaic.rs:34`](../core/dr-gpu/src/demosaic.rs#L34), [`core/dr-gpu/src/demosaic.rs:602`](../core/dr-gpu/src/demosaic.rs#L602), [`core/dr-gpu/src/demosaic.rs:681`](../core/dr-gpu/src/demosaic.rs#L681), [`core/dr-gpu/src/demosaic.rs:805`](../core/dr-gpu/src/demosaic.rs#L805) | | FR-UI-1 | [`ui/dr-ui/src/lib.rs:2992`](../ui/dr-ui/src/lib.rs#L2992), [`ui/dr-ui/src/lib.rs:81`](../ui/dr-ui/src/lib.rs#L81), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/identity.slint:164`](../ui/dr-ui/ui/identity.slint#L164), [`ui/dr-ui/ui/library.slint:1063`](../ui/dr-ui/ui/library.slint#L1063) | -| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:1488`](../ui/dr-ui/src/collections_ui.rs#L1488), [`ui/dr-ui/src/collections_ui.rs:1502`](../ui/dr-ui/src/collections_ui.rs#L1502), [`ui/dr-ui/src/collections_ui.rs:1548`](../ui/dr-ui/src/collections_ui.rs#L1548), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1646`](../ui/dr-ui/src/collections_ui.rs#L1646), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/lib.rs:81`](../ui/dr-ui/src/lib.rs#L81), [`ui/dr-ui/src/lib.rs:88`](../ui/dr-ui/src/lib.rs#L88), [`ui/dr-ui/src/library_ui.rs:292`](../ui/dr-ui/src/library_ui.rs#L292), [`ui/dr-ui/src/library_ui.rs:5203`](../ui/dr-ui/src/library_ui.rs#L5203), [`ui/dr-ui/src/library_ui.rs:5348`](../ui/dr-ui/src/library_ui.rs#L5348), [`ui/dr-ui/src/library_ui.rs:6479`](../ui/dr-ui/src/library_ui.rs#L6479), [`ui/dr-ui/ui/adjust.slint:1072`](../ui/dr-ui/ui/adjust.slint#L1072), [`ui/dr-ui/ui/adjust.slint:544`](../ui/dr-ui/ui/adjust.slint#L544), [`ui/dr-ui/ui/adjust.slint:741`](../ui/dr-ui/ui/adjust.slint#L741), [`ui/dr-ui/ui/app.slint:2041`](../ui/dr-ui/ui/app.slint#L2041), [`ui/dr-ui/ui/app.slint:493`](../ui/dr-ui/ui/app.slint#L493), [`ui/dr-ui/ui/app.slint:500`](../ui/dr-ui/ui/app.slint#L500), [`ui/dr-ui/ui/app.slint:55`](../ui/dr-ui/ui/app.slint#L55), [`ui/dr-ui/ui/app.slint:838`](../ui/dr-ui/ui/app.slint#L838), [`ui/dr-ui/ui/controls.slint:474`](../ui/dr-ui/ui/controls.slint#L474), [`ui/dr-ui/ui/develop.slint:223`](../ui/dr-ui/ui/develop.slint#L223), [`ui/dr-ui/ui/histogram.slint:127`](../ui/dr-ui/ui/histogram.slint#L127), [`ui/dr-ui/ui/history.slint:118`](../ui/dr-ui/ui/history.slint#L118), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1226`](../ui/dr-ui/ui/library.slint#L1226), [`ui/dr-ui/ui/library.slint:1232`](../ui/dr-ui/ui/library.slint#L1232), [`ui/dr-ui/ui/library.slint:2335`](../ui/dr-ui/ui/library.slint#L2335), [`ui/dr-ui/ui/library.slint:829`](../ui/dr-ui/ui/library.slint#L829), [`ui/dr-ui/ui/library.slint:882`](../ui/dr-ui/ui/library.slint#L882), [`ui/dr-ui/ui/masks.slint:304`](../ui/dr-ui/ui/masks.slint#L304), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/settings.slint:110`](../ui/dr-ui/ui/settings.slint#L110), [`ui/dr-ui/ui/spots.slint:88`](../ui/dr-ui/ui/spots.slint#L88) | -| FR-UI-3 | [`ui/dr-ui/src/develop.rs:2200`](../ui/dr-ui/src/develop.rs#L2200), [`ui/dr-ui/src/develop.rs:2309`](../ui/dr-ui/src/develop.rs#L2309), [`ui/dr-ui/src/library_ui.rs:4387`](../ui/dr-ui/src/library_ui.rs#L4387), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:908`](../ui/dr-ui/src/masks_ui.rs#L908), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:1857`](../ui/dr-ui/ui/app.slint#L1857), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/masks.slint:490`](../ui/dr-ui/ui/masks.slint#L490) | -| FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:131`](../ui/dr-ui/src/collections_ui.rs#L131), [`ui/dr-ui/src/collections_ui.rs:1488`](../ui/dr-ui/src/collections_ui.rs#L1488), [`ui/dr-ui/src/collections_ui.rs:1502`](../ui/dr-ui/src/collections_ui.rs#L1502), [`ui/dr-ui/src/collections_ui.rs:1548`](../ui/dr-ui/src/collections_ui.rs#L1548), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1646`](../ui/dr-ui/src/collections_ui.rs#L1646), [`ui/dr-ui/src/collections_ui.rs:1673`](../ui/dr-ui/src/collections_ui.rs#L1673), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:582`](../ui/dr-ui/src/collections_ui.rs#L582), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/library_ui.rs:4387`](../ui/dr-ui/src/library_ui.rs#L4387), [`ui/dr-ui/src/library_ui.rs:4432`](../ui/dr-ui/src/library_ui.rs#L4432), [`ui/dr-ui/src/library_ui.rs:4535`](../ui/dr-ui/src/library_ui.rs#L4535), [`ui/dr-ui/src/library_ui.rs:4563`](../ui/dr-ui/src/library_ui.rs#L4563), [`ui/dr-ui/src/library_ui.rs:5336`](../ui/dr-ui/src/library_ui.rs#L5336), [`ui/dr-ui/src/library_ui.rs:5348`](../ui/dr-ui/src/library_ui.rs#L5348), [`ui/dr-ui/ui/app.slint:1697`](../ui/dr-ui/ui/app.slint#L1697), [`ui/dr-ui/ui/app.slint:469`](../ui/dr-ui/ui/app.slint#L469), [`ui/dr-ui/ui/app.slint:500`](../ui/dr-ui/ui/app.slint#L500), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1226`](../ui/dr-ui/ui/library.slint#L1226), [`ui/dr-ui/ui/library.slint:1232`](../ui/dr-ui/ui/library.slint#L1232), [`ui/dr-ui/ui/library.slint:2335`](../ui/dr-ui/ui/library.slint#L2335), [`ui/dr-ui/ui/library.slint:829`](../ui/dr-ui/ui/library.slint#L829), [`ui/dr-ui/ui/library.slint:882`](../ui/dr-ui/ui/library.slint#L882), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901) | +| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:1488`](../ui/dr-ui/src/collections_ui.rs#L1488), [`ui/dr-ui/src/collections_ui.rs:1502`](../ui/dr-ui/src/collections_ui.rs#L1502), [`ui/dr-ui/src/collections_ui.rs:1548`](../ui/dr-ui/src/collections_ui.rs#L1548), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1646`](../ui/dr-ui/src/collections_ui.rs#L1646), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/lib.rs:81`](../ui/dr-ui/src/lib.rs#L81), [`ui/dr-ui/src/lib.rs:88`](../ui/dr-ui/src/lib.rs#L88), [`ui/dr-ui/src/library_ui.rs:292`](../ui/dr-ui/src/library_ui.rs#L292), [`ui/dr-ui/src/library_ui.rs:5203`](../ui/dr-ui/src/library_ui.rs#L5203), [`ui/dr-ui/src/library_ui.rs:5348`](../ui/dr-ui/src/library_ui.rs#L5348), [`ui/dr-ui/src/library_ui.rs:6479`](../ui/dr-ui/src/library_ui.rs#L6479), [`ui/dr-ui/ui/adjust.slint:1072`](../ui/dr-ui/ui/adjust.slint#L1072), [`ui/dr-ui/ui/adjust.slint:544`](../ui/dr-ui/ui/adjust.slint#L544), [`ui/dr-ui/ui/adjust.slint:741`](../ui/dr-ui/ui/adjust.slint#L741), [`ui/dr-ui/ui/app.slint:2046`](../ui/dr-ui/ui/app.slint#L2046), [`ui/dr-ui/ui/app.slint:493`](../ui/dr-ui/ui/app.slint#L493), [`ui/dr-ui/ui/app.slint:500`](../ui/dr-ui/ui/app.slint#L500), [`ui/dr-ui/ui/app.slint:55`](../ui/dr-ui/ui/app.slint#L55), [`ui/dr-ui/ui/app.slint:843`](../ui/dr-ui/ui/app.slint#L843), [`ui/dr-ui/ui/controls.slint:474`](../ui/dr-ui/ui/controls.slint#L474), [`ui/dr-ui/ui/develop.slint:223`](../ui/dr-ui/ui/develop.slint#L223), [`ui/dr-ui/ui/histogram.slint:127`](../ui/dr-ui/ui/histogram.slint#L127), [`ui/dr-ui/ui/history.slint:118`](../ui/dr-ui/ui/history.slint#L118), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1226`](../ui/dr-ui/ui/library.slint#L1226), [`ui/dr-ui/ui/library.slint:1232`](../ui/dr-ui/ui/library.slint#L1232), [`ui/dr-ui/ui/library.slint:2335`](../ui/dr-ui/ui/library.slint#L2335), [`ui/dr-ui/ui/library.slint:829`](../ui/dr-ui/ui/library.slint#L829), [`ui/dr-ui/ui/library.slint:882`](../ui/dr-ui/ui/library.slint#L882), [`ui/dr-ui/ui/masks.slint:304`](../ui/dr-ui/ui/masks.slint#L304), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/settings.slint:110`](../ui/dr-ui/ui/settings.slint#L110), [`ui/dr-ui/ui/spots.slint:88`](../ui/dr-ui/ui/spots.slint#L88) | +| FR-UI-3 | [`ui/dr-ui/src/develop.rs:2200`](../ui/dr-ui/src/develop.rs#L2200), [`ui/dr-ui/src/develop.rs:2309`](../ui/dr-ui/src/develop.rs#L2309), [`ui/dr-ui/src/library_ui.rs:4387`](../ui/dr-ui/src/library_ui.rs#L4387), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:908`](../ui/dr-ui/src/masks_ui.rs#L908), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:1862`](../ui/dr-ui/ui/app.slint#L1862), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/masks.slint:490`](../ui/dr-ui/ui/masks.slint#L490) | +| FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:131`](../ui/dr-ui/src/collections_ui.rs#L131), [`ui/dr-ui/src/collections_ui.rs:1488`](../ui/dr-ui/src/collections_ui.rs#L1488), [`ui/dr-ui/src/collections_ui.rs:1502`](../ui/dr-ui/src/collections_ui.rs#L1502), [`ui/dr-ui/src/collections_ui.rs:1548`](../ui/dr-ui/src/collections_ui.rs#L1548), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1646`](../ui/dr-ui/src/collections_ui.rs#L1646), [`ui/dr-ui/src/collections_ui.rs:1673`](../ui/dr-ui/src/collections_ui.rs#L1673), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:582`](../ui/dr-ui/src/collections_ui.rs#L582), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/library_ui.rs:4387`](../ui/dr-ui/src/library_ui.rs#L4387), [`ui/dr-ui/src/library_ui.rs:4432`](../ui/dr-ui/src/library_ui.rs#L4432), [`ui/dr-ui/src/library_ui.rs:4535`](../ui/dr-ui/src/library_ui.rs#L4535), [`ui/dr-ui/src/library_ui.rs:4563`](../ui/dr-ui/src/library_ui.rs#L4563), [`ui/dr-ui/src/library_ui.rs:5336`](../ui/dr-ui/src/library_ui.rs#L5336), [`ui/dr-ui/src/library_ui.rs:5348`](../ui/dr-ui/src/library_ui.rs#L5348), [`ui/dr-ui/ui/app.slint:1702`](../ui/dr-ui/ui/app.slint#L1702), [`ui/dr-ui/ui/app.slint:469`](../ui/dr-ui/ui/app.slint#L469), [`ui/dr-ui/ui/app.slint:500`](../ui/dr-ui/ui/app.slint#L500), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1226`](../ui/dr-ui/ui/library.slint#L1226), [`ui/dr-ui/ui/library.slint:1232`](../ui/dr-ui/ui/library.slint#L1232), [`ui/dr-ui/ui/library.slint:2335`](../ui/dr-ui/ui/library.slint#L2335), [`ui/dr-ui/ui/library.slint:829`](../ui/dr-ui/ui/library.slint#L829), [`ui/dr-ui/ui/library.slint:882`](../ui/dr-ui/ui/library.slint#L882), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901) | | FR-UI-5 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2493`](../ui/dr-ui/src/lib.rs#L2493), [`ui/dr-ui/src/lib.rs:3031`](../ui/dr-ui/src/lib.rs#L3031), [`ui/dr-ui/src/lib.rs:3216`](../ui/dr-ui/src/lib.rs#L3216), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:90`](../ui/dr-ui/ui/app.slint#L90), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) | | FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:385`](../core/dr-pipeline/src/framing.rs#L385) | | NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/library.rs:2953`](../ui/dr-ui/src/library.rs#L2953) | -| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1553`](../ui/dr-ui/src/export.rs#L1553), [`ui/dr-ui/src/export.rs:1579`](../ui/dr-ui/src/export.rs#L1579), [`ui/dr-ui/src/export.rs:409`](../ui/dr-ui/src/export.rs#L409), [`ui/dr-ui/src/export.rs:435`](../ui/dr-ui/src/export.rs#L435), [`ui/dr-ui/src/lib.rs:2190`](../ui/dr-ui/src/lib.rs#L2190), [`ui/dr-ui/ui/app.slint:1014`](../ui/dr-ui/ui/app.slint#L1014), [`ui/dr-ui/ui/library.slint:951`](../ui/dr-ui/ui/library.slint#L951) | +| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1553`](../ui/dr-ui/src/export.rs#L1553), [`ui/dr-ui/src/export.rs:1579`](../ui/dr-ui/src/export.rs#L1579), [`ui/dr-ui/src/export.rs:409`](../ui/dr-ui/src/export.rs#L409), [`ui/dr-ui/src/export.rs:435`](../ui/dr-ui/src/export.rs#L435), [`ui/dr-ui/src/lib.rs:2190`](../ui/dr-ui/src/lib.rs#L2190), [`ui/dr-ui/ui/app.slint:1019`](../ui/dr-ui/ui/app.slint#L1019), [`ui/dr-ui/ui/library.slint:951`](../ui/dr-ui/ui/library.slint#L951) | | NFR-ARCH-4 | [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-export/src/error.rs:1`](../core/dr-export/src/error.rs#L1), [`core/dr-thumbs/src/error.rs:1`](../core/dr-thumbs/src/error.rs#L1), [`platform/dr-plat/src/storage.rs:148`](../platform/dr-plat/src/storage.rs#L148), [`ui/dr-ui/src/export.rs:499`](../ui/dr-ui/src/export.rs#L499) | | NFR-OPS-1 | [`tools/traceability/src/lib.rs:266`](../tools/traceability/src/lib.rs#L266) | | NFR-P1 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) | diff --git a/ui/dr-ui/Cargo.toml b/ui/dr-ui/Cargo.toml index 77d30f2..03db558 100644 --- a/ui/dr-ui/Cargo.toml +++ b/ui/dr-ui/Cargo.toml @@ -7,6 +7,10 @@ license.workspace = true [dependencies] dr-types.workspace = true +# TRACES: FR-DEV-6 +# Reading Lightroom `.xmp` presets. Its own crate because the translation names +# operations, which the interface may not — see `ui_names_no_operation.rs`. +dr-preset-xmp.workspace = true # No `readback`. S1 wired Slint's texture import, so the develop view hands # the compositor the texture itself and there is no display round-trip left to # gate (ARCH §6.1, AC-8). The export path reads pixels back through diff --git a/ui/dr-ui/src/presets.rs b/ui/dr-ui/src/presets.rs index 395044a..def724f 100644 --- a/ui/dr-ui/src/presets.rs +++ b/ui/dr-ui/src/presets.rs @@ -688,8 +688,74 @@ impl NamedPresets { } fn at(store: PresetStore) -> Rc { - let library = RefCell::new(store.load()); - Rc::new(Self { store, library }) + // TRACES: FR-DEV-6 + // Seed the starter set on a device that has never had a library, and + // only then. See `preset_starter` for why this is not a merge: + // re-adding on every start would resurrect one deleted on purpose. + // + // "Never had one" is the *file* not existing rather than the library + // being empty — a photographer who deleted all six must not be handed + // them again on the next launch. + let seeding = !store.path().exists(); + let library = if seeding { + let starters = dr_pipeline::starter::library(); + if let Err(e) = store.save(&starters) { + // Not fatal. The presets are in memory and usable this + // session; what is lost is their persistence, and refusing to + // start over it would be absurd. + log::warn!("could not write the starter presets: {e}"); + } + starters + } else { + store.load() + }; + Rc::new(Self { + store, + library: RefCell::new(library), + }) + } + + /// Read presets from a file or a folder and store them. + /// + /// Returns what to tell the user. Names already in the library are + /// replaced, the same rule a save follows — importing the same folder + /// twice leaves one copy of each rather than `Warm Portrait 2`. + fn import(&self, path: &Path) -> String { + let report = dr_preset_xmp::read_path(path); + if report.presets.is_empty() { + return if report.failed > 0 { + format!("Found {} file(s), but none could be read.", report.failed) + } else { + "No .xmp presets there.".to_string() + }; + } + + let added = report.presets.len(); + { + let mut library = self.library.borrow_mut(); + for (name, preset) in report.presets { + if let Err(e) = library.insert(&name, preset) { + log::warn!("imported preset {name:?} is unusable ({e:?}); skipping"); + } + } + } + if let Err(e) = self.persist(|_| {}) { + return e.message(); + } + + let mut message = format!( + "Imported {added} preset{}.", + if added == 1 { "" } else { "s" } + ); + if report.failed > 0 { + message.push_str(&format!(" {} could not be read.", report.failed)); + } + // Said once, however many files carried it: after forty presets the + // useful sentence is that the white balance did not come across. + if !report.unsupported.is_empty() { + message.push_str(" White balance and tone curves do not carry across."); + } + message } /// The stored names, in the order they are written. @@ -702,6 +768,12 @@ impl NamedPresets { self.library.borrow().get(name).cloned() } + /// Whether a preset is stored under `name`. + #[cfg(test)] + pub fn contains(&self, name: &str) -> bool { + self.library.borrow().contains(name) + } + /// Store `preset` under `name` and persist. /// /// The in-memory library is updated first and rolled back if the write @@ -726,6 +798,13 @@ impl NamedPresets { } /// Save the library, undoing the in-memory change if the write fails. + /// + /// The rollback is the caller's because only the caller knows what the + /// previous state was. An import passes an empty one deliberately: it may + /// have replaced any number of names, restoring them all would mean + /// snapshotting the whole library, and the honest failure there is to say + /// the write failed and leave what is on screen matching what is in + /// memory until the next successful save. fn persist(&self, rollback: impl FnOnce(&mut PresetLibrary)) -> Result<(), SaveError> { let result = self.store.save(&self.library.borrow()); match result { @@ -745,6 +824,7 @@ impl NamedPresets { } /// Why a preset could not be saved. +#[derive(Debug)] enum SaveError { /// The name itself was refused. Name(NameError), @@ -899,6 +979,25 @@ pub fn wire_named( }); } + // --- import from Lightroom -------------------------------------------- + { + let weak = window.as_weak(); + let named = named.clone(); + window.on_import_presets(move |path| { + let Some(w) = weak.upgrade() else { return }; + let path = path.trim(); + if path.is_empty() { + return; + } + // Synchronous, deliberately. A preset folder is a few hundred + // small text files and the read is milliseconds; a worker and a + // progress bar would be machinery around a wait nobody sees. + let report = named.import(Path::new(path)); + w.set_preset_import_report(report.into()); + render_named(&w, &named); + }); + } + // --- rename ----------------------------------------------------------- { let weak = window.as_weak(); @@ -1275,11 +1374,115 @@ mod tests { // Named presets // ----------------------------------------------------------------------- + /// A library that has *already been started*, so the starter presets are + /// not in the way. + /// + /// Writing an empty library first is what makes the file exist, which is + /// the condition seeding tests — see `NamedPresets::at`. The seeding + /// itself has its own tests below rather than being switched off with a + /// flag that only tests would set. fn named(name: &str) -> (Rc, PathBuf) { + let dir = tempdir(name); + let path = dir.join("presets.drpl"); + PresetStore::open_at(path.clone()) + .save(&PresetLibrary::default()) + .unwrap(); + (NamedPresets::open_at(path), dir) + } + + /// A device that has never had a preset library. + fn fresh(name: &str) -> (Rc, PathBuf) { let dir = tempdir(name); (NamedPresets::open_at(dir.join("presets.drpl")), dir) } + #[test] + fn a_first_run_is_handed_the_starter_presets() { + // A sheet that opens on "No presets yet" teaches the photographer that + // the feature is homework. + let (presets, _dir) = fresh("first-run"); + assert_eq!(presets.names().len(), dr_pipeline::starter::library().len()); + assert!(!presets.names().is_empty()); + } + + #[test] + fn the_starters_are_written_out_so_they_survive_a_restart() { + let (_presets, dir) = fresh("starters-persist"); + let path = dir.join("presets.drpl"); + assert!(path.exists(), "the starter library was never written"); + assert!(!PresetStore::open_at(path).load().is_empty()); + } + + #[test] + fn a_starter_the_photographer_deleted_stays_deleted() { + // The reason seeding keys on the file existing rather than on the + // library being empty: deleting all six must not hand them back on the + // next launch. + let dir = tempdir("deleted-starters"); + let path = dir.join("presets.drpl"); + { + let presets = NamedPresets::open_at(path.clone()); + for name in presets.names() { + let mut library = presets.library.borrow_mut(); + library.remove(&name); + } + presets.persist(|_| {}).unwrap(); + } + + let again = NamedPresets::open_at(path); + assert!( + again.names().is_empty(), + "the starters came back: {:?}", + again.names() + ); + } + + #[test] + fn importing_a_folder_stores_what_it_finds() { + let (presets, dir) = named("import-folder"); + let from = dir.join("from-lightroom"); + std::fs::create_dir_all(&from).unwrap(); + std::fs::write( + from.join("warm.xmp"), + r#" + + + +0.5 + + +"#, + ) + .unwrap(); + + let report = presets.import(&from); + assert!(report.starts_with("Imported 1 preset."), "{report}"); + assert_eq!(presets.names(), vec!["warm".to_string()]); + } + + #[test] + fn importing_the_same_folder_twice_leaves_one_copy() { + let (presets, dir) = named("import-twice"); + let from = dir.join("from-lightroom"); + std::fs::create_dir_all(&from).unwrap(); + std::fs::write( + from.join("warm.xmp"), + r#""#, + ) + .unwrap(); + + presets.import(&from); + presets.import(&from); + assert_eq!(presets.names(), vec!["warm".to_string()]); + } + + #[test] + fn importing_from_nowhere_says_so_rather_than_claiming_success() { + let (presets, _dir) = named("import-nothing"); + let report = presets.import(Path::new("/definitely/not/here")); + assert!(report.contains("No .xmp presets"), "{report}"); + } + #[test] fn a_saved_preset_is_on_disk_before_the_call_returns() { // Not on the way out, and not on a timer: a preset the user named and @@ -1287,7 +1490,6 @@ mod tests { let (presets, dir) = named("saved-immediately"); presets .insert("Warm", Preset::capture(&edited())) - .ok() .expect("saved"); let reloaded = NamedPresets::open_at(dir.join("presets.drpl")); @@ -1331,9 +1533,15 @@ mod tests { let presets = NamedPresets::open_at(blocked.join("presets.drpl")); assert!(presets.insert("Warm", Preset::default()).is_err()); + // Not "the library is empty": the starter set is held in memory even + // on a device whose config directory cannot be written, which is the + // right behaviour — the presets are usable this session and only their + // persistence is lost. What must not survive is the preset whose write + // failed. assert!( - presets.names().is_empty(), - "the failed save left a preset behind" + !presets.contains("Warm"), + "the failed save left a preset behind: {:?}", + presets.names() ); } diff --git a/ui/dr-ui/ui/app.slint b/ui/dr-ui/ui/app.slint index d43e7ea..6c955e8 100644 --- a/ui/dr-ui/ui/app.slint +++ b/ui/dr-ui/ui/app.slint @@ -680,6 +680,11 @@ export component AppWindow inherits Window { in property copy-scope-empty: false; callback copy-scope-toggled(string); + /// TRACES: FR-DEV-6 + /// Reading Lightroom presets in, and what came of the last attempt. + in property preset-import-report; + callback import-presets(string); + // --- settings (FR-EXP-1, FR-EXP-3, FR-NC-6a) --- // // A page rather than an overlay, and the outermost of the view conditions @@ -2473,6 +2478,8 @@ in property panel-visible: true; scope-kinds: root.copy-scope-kinds; scope-empty: root.copy-scope-empty; scope-toggled(name) => { root.copy-scope-toggled(name); } + import-report: root.preset-import-report; + import-presets(path) => { root.import-presets(path); } save(name) => { root.save-preset(name); } apply(name) => { root.apply-preset(name); } rename(from, to) => { root.rename-preset(from, to); } diff --git a/ui/dr-ui/ui/presets.slint b/ui/dr-ui/ui/presets.slint index 255eeb6..e340c8d 100644 --- a/ui/dr-ui/ui/presets.slint +++ b/ui/dr-ui/ui/presets.slint @@ -110,6 +110,14 @@ export component PresetSheet inherits Rectangle { in property scope-empty: false; callback scope-toggled(string); + /// TRACES: FR-DEV-6 + /// Read Lightroom presets from a typed path, and what came of the last + /// attempt. A typed path rather than a file dialogue because that is the + /// idiom this application already has — the launch screen takes a library + /// the same way, and there is no native picker anywhere in it. + in property import-report; + callback import-presets(string); + background: #000000CC; // Swallows the taps that miss the card, and closes. First, so the card's @@ -265,6 +273,27 @@ export component PresetSheet inherits Rectangle { Rectangle { height: 1px; background: Theme.rule; } + // TRACES: FR-DEV-6 + // Bringing a Lightroom library across. A folder rather than a + // file is the expected case — an exported preset folder, nested + // one level per group — so the field says so. + VerticalLayout { + spacing: Theme.gap-sm; + + PanelHeading { text: "IMPORT"; } + + Field { + placeholder: "Path to a .xmp file or folder"; + accepted(path) => { root.import-presets(path); } + } + + if root.import-report != "": Caption { + text: root.import-report; + } + } + + Rectangle { height: 1px; background: Theme.rule; } + // TRACES: FR-DEV-6 // What an apply carries. At the bottom, because it qualifies every // action above it rather than being one of them — and because the