Carry the photograph's header into an export made from develop
The same file exported from the library grid kept its camera, its lens,
its capture date and its rights statement. Exported from the develop
button it kept none of them, and `{date}` in a filename template
resolved to nothing at all. Two buttons, one photograph, two different
files -- and the develop one was the version the photographer had just
finished working on.
A session now remembers the header it was opened from, and
`open_session` takes that header rather than the orientation read out of
it, so a photograph cannot be opened for editing without saying which
file it came from. `render_open_frame` clones it onto
`Source::Rendered`; both arms of `export_one` -- the worker's own decode
and the frame handed over already rendered -- turn a header into a
`{date}` and a `SourceMetadata` through the same function, so the two
paths cannot come to different readings of one file. What of it actually
reaches the exported bytes is still decided inside `dr-export` from the
settings, which is what keeps the location-stripping option working here
rather than giving it a second implementation to disagree with.
The alternative was to hang the metadata on `Source::Rendered` alone and
keep it beside the session in the interface. That touches less, but it
makes the header and the pixels two cells to hold in step across the six
places an image is opened, replaced or fails to open, and the failure
mode of getting that pairing wrong is not a missing tag: it is one
photograph exported under another's byline and coordinates, silently.
Kept on the session, the two travel together or not at all.
The header is stored decoded rather than transcribed at open time,
deliberately. `dr-export` argues that source metadata is a parameter and
not a field on `Frame`, because two exports of one frame may legitimately
disclose different amounts; by the same reasoning a session may remember
where its pixels came from without that being a decision about what to
publish, and the allowlist that decides remains the single function in
`export.rs`.
A file with no header is left with none -- an empty `{date}` and nothing
for the encoder to copy -- rather than today's date standing in for a
capture time nobody recorded.
This commit is contained in:
+48
-5
@@ -183,12 +183,13 @@ fn load(ctx: Option<&dr_gpu::GpuContext>, path: &Path) -> Result<Loaded, String>
|
||||
fn load_bytes(ctx: Option<&dr_gpu::GpuContext>, bytes: &[u8]) -> Result<Loaded, String> {
|
||||
let meta = dr_decode::metadata(bytes).unwrap_or_default();
|
||||
|
||||
// How the file stored its pixels. A file that says nothing is taken as
|
||||
// upright — see `Orientation::from_exif`.
|
||||
// How the file stored its pixels, for the preview path below — the develop
|
||||
// path takes it from the same header inside `open_session`. A file that
|
||||
// says nothing is taken as upright — see `Orientation::from_exif`.
|
||||
let orientation = meta.orientation.unwrap_or_default();
|
||||
|
||||
if let Some(ctx) = ctx {
|
||||
match open_session(ctx, bytes, orientation) {
|
||||
match open_session(ctx, bytes, &meta) {
|
||||
Ok(session) => {
|
||||
let (width, height) = session.source_size();
|
||||
return Ok(Loaded {
|
||||
@@ -228,7 +229,7 @@ fn load_bytes(ctx: Option<&dr_gpu::GpuContext>, bytes: &[u8]) -> Result<Loaded,
|
||||
})
|
||||
}
|
||||
|
||||
/// TRACES: FR-EXP-7 | FR-RAW-4
|
||||
/// TRACES: FR-EXP-7 | FR-EXP-8 | FR-RAW-4
|
||||
/// Open bytes for editing, with no interface types involved.
|
||||
///
|
||||
/// Split out of [`load_bytes`] so the batch exporter can share it. That runs on
|
||||
@@ -236,7 +237,38 @@ fn load_bytes(ctx: Option<&dr_gpu::GpuContext>, bytes: &[u8]) -> Result<Loaded,
|
||||
/// and a second copy of the JPEG-versus-RAW routing is a second copy that would
|
||||
/// drift, which is exactly how a batch comes to export something the viewer
|
||||
/// would have shown differently.
|
||||
///
|
||||
/// Takes the decoded header rather than just the orientation out of it. Both
|
||||
/// callers had already read it, and the session keeps it: this is the one
|
||||
/// place a photograph becomes editable, so making the header an argument here
|
||||
/// is what makes "a session knows which file it came from" true by
|
||||
/// construction rather than by everybody remembering to say so.
|
||||
pub(crate) fn open_session(
|
||||
ctx: &dr_gpu::GpuContext,
|
||||
bytes: &[u8],
|
||||
meta: &Metadata,
|
||||
) -> Result<DevelopSession, String> {
|
||||
// How the file stored its pixels. A file that says nothing is taken as
|
||||
// upright — see `Orientation::from_exif`.
|
||||
let mut session = open_pixels(ctx, bytes, meta.orientation.unwrap_or_default())?;
|
||||
|
||||
// TRACES: FR-EXP-8
|
||||
// The header goes with the session rather than being read again later,
|
||||
// and this function takes it rather than an orientation so that there is
|
||||
// no way to open a photograph for editing without saying what file it came
|
||||
// from. An export from the develop button carries the camera, the lens and
|
||||
// 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());
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Decode bytes into a session, with no header attached.
|
||||
///
|
||||
/// Split from [`open_session`] only so the routing below stays one expression
|
||||
/// with two early returns; every caller wants the version that remembers where
|
||||
/// the pixels came from.
|
||||
fn open_pixels(
|
||||
ctx: &dr_gpu::GpuContext,
|
||||
bytes: &[u8],
|
||||
orientation: dr_types::Orientation,
|
||||
@@ -481,7 +513,18 @@ fn render_open_frame(
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "export".into());
|
||||
|
||||
Ok(export::Source::Rendered { stem, frame })
|
||||
// TRACES: FR-EXP-8
|
||||
// The header the session was opened from, cloned because the frame is
|
||||
// about to leave this thread. `None` where the file had none to read,
|
||||
// which the worker takes as "say nothing" rather than as a reason to
|
||||
// invent something.
|
||||
let header = session.source_metadata().cloned();
|
||||
|
||||
Ok(export::Source::Rendered {
|
||||
stem,
|
||||
header,
|
||||
frame,
|
||||
})
|
||||
}
|
||||
|
||||
/// TRACES: FR-EXP-7
|
||||
|
||||
Reference in New Issue
Block a user