Say which camera profile a photograph renders through, and offer to copy it

The info panel gains a line under the lens: "Adobe Standard · in the
file", the .dcp it came from, "· off" when the photographer switched it
off, or "No camera profile · matrix only" — the ordinary case for a
CR2, worded as a fact rather than a failure. A DNG whose embedded
profile may be copied, for a body with no installed profile, also gets
"Use this profile for every Canon EOS 6D →", which saves it into the
profiles directory; the body's CR2s render through it from their next
decode.

The profiles directory is <data>/profiles, read at start-up on desktop
and Android before anything decodes. The library open path never set
the lens line; it now sets both. Labels: Camera Profile, Use Profile,
Look Amount.
This commit is contained in:
2026-10-02 22:38:07 -04:00
parent c02b401a9a
commit eae720ce75
7 changed files with 358 additions and 53 deletions
+70
View File
@@ -324,6 +324,13 @@ pub(crate) fn open_session(
// the capture date because of this line; before it, the same photograph
// exported from the grid kept them and exported from develop did not.
session.set_source_metadata(meta.clone());
// TRACES: FR-DEV-3e
// The embedded camera profile, read again from the header, for the info
// panel's offer to copy it. Only a DNG carries one; the read is the
// header's IFDs, not the photosites.
if dr_decode::probe(bytes) != Some(dr_types::Format::Jpeg) {
session.set_embedded_profile(dr_decode::dcp::embedded_in(bytes));
}
Ok(session)
}
@@ -1194,6 +1201,19 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
log::info!("gpu opened in {} ms", launch_began.elapsed().as_millis());
let window = init_window(&gpu)?;
// TRACES: FR-DEV-3e
// The installed camera profiles, before anything decodes (D20). A
// directory of a few `.dcp` files, each a couple of hundred kilobytes,
// so read here rather than deferred: a photograph opened before a
// background load finished would render without its profile once and
// with it the next time.
let profiles_began = std::time::Instant::now();
dr_decode::dcp::set_profiles_directory(library::data_root().join("profiles"));
log::info!(
"camera profiles read in {} ms",
profiles_began.elapsed().as_millis()
);
wire_inference_status(&window);
wire_diagnostics(&window, &gpu);
@@ -1246,6 +1266,7 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
let index = Rc::new(RefCell::new(0usize));
// The current develop session, if the file yielded sensor data.
let session: Rc<RefCell<Option<DevelopSession>>> = Rc::new(RefCell::new(None));
wire_profile_offer(&window, &session);
// TRACES: FR-PLAT-AND-5
// The GPU tier — the first thing given back under memory pressure, and on
@@ -1479,6 +1500,43 @@ fn init_window(gpu: &Option<dr_gpu::GpuContext>) -> Result<AppWindow> {
/// What the models run on. Re-read every two seconds because the answer
/// changes twice after launch — when the probe reports and as each
/// engine lands — and the page is open for longer than either takes.
/// TRACES: FR-DEV-3e
/// The info panel's camera profile line and its offer, for the session just
/// opened — or cleared, for none.
fn show_camera_profile(window: &AppWindow, session: Option<&DevelopSession>) {
window.global::<Capture>().set_profile(
session
.map(DevelopSession::profile_summary)
.unwrap_or_default()
.into(),
);
window.global::<ProfileOffer>().set_offer(
session
.and_then(DevelopSession::profile_offer)
.unwrap_or_default()
.into(),
);
}
/// TRACES: FR-DEV-3e
/// Saving the open DNG's embedded profile for every photograph from its
/// body (D20). The photograph on screen already renders through it; the
/// others pick it up when they are next decoded.
fn wire_profile_offer(window: &AppWindow, session: &Rc<RefCell<Option<DevelopSession>>>) {
let weak = window.as_weak();
let session = session.clone();
window.global::<ProfileOffer>().on_adopt(move || {
let Some(w) = weak.upgrade() else { return };
let mut guard = session.borrow_mut();
let Some(s) = guard.as_mut() else { return };
match s.adopt_profile() {
Ok(path) => log::info!("camera profile saved to {}", path.display()),
Err(e) => log::warn!("camera profile not saved: {e}"),
}
show_camera_profile(&w, Some(s));
});
}
fn wire_inference_status(window: &AppWindow) {
let set = |w: &AppWindow| {
let (line, detail) = inference::about_lines();
@@ -2547,6 +2605,7 @@ fn build_show(
.unwrap_or_default()
.into(),
);
show_camera_profile(window, l.session.as_ref());
// The panel is built from what the pipeline reports, so
// this code names no operation (FR-DEV-3a).
@@ -2604,6 +2663,7 @@ fn build_show(
capture.set_exposure("".into());
capture.set_dimensions("".into());
capture.set_lens("".into());
show_camera_profile(window, None);
}
}
})
@@ -2691,6 +2751,8 @@ fn wire_remote_open(
capture.set_camera("".into());
capture.set_exposure("".into());
capture.set_dimensions("".into());
capture.set_lens("".into());
show_camera_profile(&w, None);
// The grid is one image at a time, so next/previous have nothing
// to walk. Shown as 1 of 1 rather than left reading 0.
w.set_index(0);
@@ -2868,6 +2930,14 @@ fn wire_remote_open(
capture.set_camera(describe_camera(&l.meta).into());
capture.set_exposure(describe_exposure(&l.meta).into());
capture.set_dimensions(format!("{} × {}", l.width, l.height).into());
capture.set_lens(
l.session
.as_ref()
.map(|s| s.lens_summary())
.unwrap_or_default()
.into(),
);
show_camera_profile(&w, l.session.as_ref());
match l.session {
Some(mut s) => {
w.global::<Develop>().set_enabled(true);