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:
@@ -2556,6 +2556,113 @@ fn request_thumbnails(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
drain_thumbnails(window.as_weak(), ctl.clone(), rx, requested, class);
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-9 | FR-DEV-6
|
||||
/// Replace a photograph's cached thumbnail with one rendered from its edit.
|
||||
///
|
||||
/// # Why the grid cannot be left alone
|
||||
///
|
||||
/// 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 — the one view of a
|
||||
/// library where an edit is least visible is the one the photographer spends
|
||||
/// most of their time in.
|
||||
///
|
||||
/// # Both classes, and why
|
||||
///
|
||||
/// The store keys on the size class, so replacing only the one the grid
|
||||
/// happens to be drawing at leaves the other holding the unedited preview —
|
||||
/// and a zoom past the class boundary would show the edit undoing itself.
|
||||
/// Each class is rendered separately because they are different sizes; a
|
||||
/// downscale of the large one would be a second, worse resampler than the GPU
|
||||
/// already applied.
|
||||
///
|
||||
/// # Silent on failure
|
||||
///
|
||||
/// The edit is saved to the sidecar by the caller before this runs, so nothing
|
||||
/// here can lose work. A thumbnail that could not be re-rendered is a stale
|
||||
/// cell, which the next scroll past it corrects from the store — worth a log
|
||||
/// line and not worth an error in front of a photographer who has just
|
||||
/// finished an image.
|
||||
pub fn refresh_thumbnail(
|
||||
window: &AppWindow,
|
||||
ctl: &Rc<LibraryController>,
|
||||
remote_path: &str,
|
||||
mut render: impl FnMut(u32) -> Result<(u32, u32, Vec<u8>), String>,
|
||||
) {
|
||||
// Where this photograph sits in the loaded window. It is always in it: the
|
||||
// develop view is reached from a cell, and the guards on the scroll and
|
||||
// geometry handlers stop the window moving while it is open.
|
||||
let Some(row) = ctl.paths.borrow().iter().position(|p| p == remote_path) else {
|
||||
return;
|
||||
};
|
||||
let Some(file_id) = ctl.file_ids.borrow().get(row).copied().flatten() else {
|
||||
// Nothing to key the store on. A photograph the scan recorded without
|
||||
// a server file id cannot have a cached thumbnail either, so there is
|
||||
// nothing here to correct.
|
||||
return;
|
||||
};
|
||||
let Some((_, session, _)) = ctl.session.borrow().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut store = match dr_thumbs::ThumbStore::open(&library::thumbs_dir(
|
||||
&session.server,
|
||||
&session.user_id,
|
||||
)) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::warn!("re-thumbnailing {remote_path}: opening the store: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// What the grid is drawing at, so the cell can be corrected on screen
|
||||
// rather than only on disk.
|
||||
let drawn = dr_thumbs::ThumbSize::for_cell(window.get_library_cell_size().max(1.0) as u32);
|
||||
|
||||
for class in [dr_thumbs::ThumbSize::Grid, dr_thumbs::ThumbSize::Large] {
|
||||
let (w, h, rgba) = match render(class.edge()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
log::warn!("re-thumbnailing {remote_path} at {class:?}: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match dr_thumbs::codec::encode_rgba(w, h, &rgba) {
|
||||
Ok(bytes) => {
|
||||
let thumb = dr_thumbs::Thumbnail {
|
||||
width: w,
|
||||
height: h,
|
||||
bytes,
|
||||
};
|
||||
if let Err(e) = store.put(file_id, class, &thumb) {
|
||||
log::warn!("re-thumbnailing {remote_path} at {class:?}: {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("re-thumbnailing {remote_path} at {class:?}: encoding: {e}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if class == drawn {
|
||||
// Straight into the model, so the edit is on the cell the moment
|
||||
// the grid comes back rather than after a scroll evicts and
|
||||
// refetches it.
|
||||
let model = window.get_library_cells();
|
||||
if let Some(mut cell) = model.row_data(row) {
|
||||
cell.thumbnail = to_slint_image(w, h, &rgba);
|
||||
cell.has_thumb = true;
|
||||
cell.unavailable = false;
|
||||
model.set_row_data(row, cell);
|
||||
}
|
||||
record_class(ctl, row, class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Note which size class a row's pixels came from.
|
||||
///
|
||||
/// Silent about a row past the end: the model and this vector are rebuilt
|
||||
|
||||
Reference in New Issue
Block a user