Look the lens up and say plainly whether one was found

`dr-lens` has held a complete Lensfun lookup — distortion, TCA and vignetting
coefficients from a lens name, a focal length and an aperture — with no
dependents anywhere in the workspace. The three corrections it feeds now
exist in the graph, so this connects the two and finishes the chain.

The coefficient structs stay duplicated. `dr-pipeline` is organised around
having no dependencies so its codegen is testable without a device or a
database (ARCH §6.5a), and `dr-lens` carries an XML parser and 5.5 MB of
profile data. Neither crate can convert to the other, so the conversion goes
above both, in `develop.rs`, which is the only place that sees them together.

Both traits grow the same defaulted door. The optical corrections do not sit
on the same side of the fetch — distortion and CA rewrite coordinates and are
`Warp`s, vignetting applies a gain to the pixel already there and is an
ordinary node — and fanning a profile out by which trait each happens to
implement would make the caller reason about that distinction. Each correction
takes its own share of the whole profile instead, and `set_lens_profile` walks
both lists identically.

The lookup happens in `set_source_metadata` rather than in its caller, because
that is the one place a session is told which file it came from. Doing it
there makes it unforgettable, in the shape `FilmRebake` already uses for the
other derived thing — and, more to the point, makes *clearing* unforgettable:
a session that opened a second photograph while still holding the first one's
profile would correct it for the wrong optics, invisibly, in a way that looks
exactly like the lens.

It needs the whole shot and not just a name. Distortion is interpolated across
a zoom's focal range and vignetting depends strongly on aperture — a fast
prime can be two stops down in the corners wide open and clean by f/8 — so a
lookup missing either returns coefficients measured for a shot nobody took.
Missing any of the three refuses rather than guesses.

A profile is derived, not persisted: it comes from the file's EXIF and a
database, so it is not a parameter, not in the sidecar and not undoable. What
is an edit is the manual trim beside it, which each correction composes with
the measurement — so a photographer can lean on it, override it, or work
without one.

`InfoPanel` gains a lens line, and it distinguishes three cases rather than
two. `dr-lens` states the rule it exists for: an automatic correction that
silently did nothing is worse than one the user can see is unavailable. A
session with no header draws nothing, a header naming no lens reads "Lens not
recorded", and a lens the database has never heard of reads "· no profile".
Collapsing the last two would send somebody hunting for a profile that was
never missing — which, for third-party and adapted glass, is the ordinary case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-05 15:11:32 +02:00
co-authored by Claude Opus 5
parent a1165ef182
commit c4ddcbe0f7
14 changed files with 525 additions and 42 deletions
+40
View File
@@ -64,6 +64,32 @@ use std::sync::Arc;
use crate::descriptor::{OpDescriptor, ParamId};
use crate::operation::{Helper, Uniform};
/// Lateral chromatic aberration, as a per-channel radial scale.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Tca {
pub red_scale: f32,
pub blue_scale: f32,
}
/// What a lens profile says about one shot, in the pipeline's own types.
///
/// **Deliberately a mirror of `dr_lens::LensProfile` rather than that type
/// itself.** The dependency would have to run the wrong way: `dr-lens` carries
/// an XML parser and 5.5 MB of Lensfun data, and this crate is organised around
/// having no dependencies so that its codegen is testable without a GPU or a
/// database (ARCH §6.5a). Whatever sits above both does the conversion; it is
/// nine fields and a `match`.
///
/// Every field is independently optional because the database is: it commonly
/// carries distortion for a lens and no vignetting, or covers only part of a
/// zoom. A partial profile is useful and must not be discarded wholesale.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct LensProfile {
pub distortion: Option<crate::ops::distortion::PtLens>,
pub tca: Option<Tca>,
pub vignetting: Option<crate::ops::vignetting::Pa>,
}
/// A coordinate-domain operation, applied before the source is sampled.
///
/// Object-safe for the same reason [`crate::operation::Operation`] is: the
@@ -118,6 +144,20 @@ pub trait Warp: Send + Sync {
fn helpers(&self) -> &'static [Helper] {
&[]
}
/// Take whatever this warp needs from a lens profile.
///
/// Handed the *whole* profile rather than its own slice of it, so that the
/// graph fanning one out does not have to know which correction wants
/// which coefficients — the same reason an operation is handed a
/// [`ParamId`] rather than a field. `None` clears any profile in place,
/// which is what opening a photograph from an unrecognised lens must do:
/// leaving the previous one standing would correct this frame for the
/// optics of the last one.
///
/// Defaulted, because a warp need not be profile-driven. Nothing about the
/// coordinate stage requires a database behind it.
fn set_profile(&mut self, _profile: Option<&LensProfile>) {}
}
/// The composed geometry stage: WGSL, uniforms, and what it needs from the