Re-thumbnail a photograph from its own edit

A thumbnail comes from the file's embedded preview, which is the camera's
idea of the photograph and knows nothing about what has been done to it
since. So a frame could be cropped, turned upright and pulled two stops
back, and the grid would go on showing the original — making the library,
where a photographer spends most of their time, the one view in which an
edit is invisible.

The render is the framed output, not the sensor: `output_size` is what a
crop, a quarter turn, a flip and a straighten all act on, so a thumbnail
taken from the raw frame would be the right pixels in the wrong shape and
still the wrong way up. It is the same path an export takes, at a size
the store wants rather than at full resolution, and always sRGB — this is
a JPEG in a shard that syncs between devices and is drawn as a cell, not
a file anyone is finishing.

Both size classes are replaced. The store keys on the class, so
refreshing only the one the grid happens to be drawing leaves the other
holding the unedited preview, and a zoom across the boundary would show
the edit undoing itself. Each is rendered rather than downscaled from the
larger, which would be a second and worse resampler than the GPU has
already applied.

It runs on the way out of develop, after the sidecar write is queued and
never instead of it — the edit is what must not be lost, and a render
that failed must not take the save down with it. Two cases are worth the
work: an edit made in this sitting, which `can-undo` records even when it
ends back at neutral, and an image opened with an edit already in its
sidecar and left untouched, whose cached thumbnail has never shown that
edit at all. A neutral image nobody touched fails both and costs nothing.

Not covered: a batch paste onto a selection, which deliberately never
opens a session — there is no rendered frame to take a thumbnail from,
and downloading forty RAWs to make forty is exactly what that path exists
to avoid.
This commit is contained in:
2026-08-21 22:48:10 +02:00
parent 3085ec4d2e
commit caf61d41a5
3 changed files with 186 additions and 0 deletions
+29
View File
@@ -694,6 +694,35 @@ impl DevelopSession {
dr_export::Frame::in_space(rw, rh, pixels, space).map_err(|e| e.to_string())
}
/// TRACES: FR-CAT-9
/// Render this edit small, for the grid's thumbnail.
///
/// **The framed output, not the sensor.** `output_size` is what a crop, a
/// quarter turn, a flip and a straighten all act on, so a thumbnail taken
/// from the raw frame would show the grid a photograph the user no longer
/// has — the right pixels in the wrong shape, still the wrong way up. This
/// is the same path [`Self::render_for_export`] takes, at a size the store
/// wants instead of at full resolution.
///
/// Always sRGB: this is going into a JPEG in a thumbnail shard that syncs
/// between devices and is drawn as a cell, not a file the user is
/// finishing. The wider spaces exist for export and mean nothing here.
///
/// Returns width, height and RGBA8.
pub fn render_thumbnail(&mut self, edge: u32) -> Result<(u32, u32, Vec<u8>), String> {
let (sw, sh) = self.demosaiced.size();
let (fw, fh) = self.graph.output_size(sw, sh);
let (w, h) = fit(fw, fh, edge.max(1), edge.max(1));
let shader = self.graph.compose_for(dr_types::ColourSpace::Srgb);
self.adjust
.render(&self.demosaiced, &shader, w, h)
.map_err(|e| e.to_string())?;
let (pixels, rw, rh) = self.adjust.export_pixels().map_err(|e| e.to_string())?;
Ok((rw, rh, pixels))
}
/// The sensor's own dimensions, before framing.
///
/// What a crop overlay needs: its handles are placed against the full