Thumbnail a composite during the merge, as develop first shows it
The DNG a merge writes has no embedded preview, and an embedded preview is all the grid's thumbnail path reads, so a composite stood in the grid as a blank cell until it was opened. Reading an 800 MB file back to make one would cost what the merge already has in hand. The bands are box-reduced as they are written, after the border fill, into a copy 4096 pixels long. That copy is written as a linear DNG in memory with the composite's own profile, header and crop, and opened through open_session, the function develop opens every file with: the same decode, the default graph and view transform, the as-shot white balance and the conversion to the display's space. The grid and large thumbnails are rendered from that session, staged beside the payload before the rename releases it to a drain, and put in the store under the file id once the upload has learned it. Until then the grid draws them from memory, so the cell is not blank while the file is uploading. A test develops a synthetic composite both ways, the whole file as develop opens it and the merge's reduced copy, and holds the thumbnail's mean, 95th and 99.5th luma percentiles to within 3-4 levels of develop's render; the naive balanced-and-gamma picture the merge's preview draws misses by 13.
This commit is contained in:
@@ -113,6 +113,16 @@ pub struct LibraryController {
|
||||
/// two states apart — without it a carried thumbnail either blocks the
|
||||
/// sharper fetch forever or is re-fetched on every scroll.
|
||||
pub(super) thumb_class: RefCell<Vec<Option<dr_thumbs::ThumbSize>>>,
|
||||
/// TRACES: FR-MRG-6
|
||||
/// Thumbnails a merge made for a composite it has just catalogued, by
|
||||
/// image, decoded and at each class it made.
|
||||
///
|
||||
/// The grid's own path keys the store on the server's file id, and a
|
||||
/// composite has none until its upload lands — minutes, on Nextcloud. The
|
||||
/// merge's pixels are drawn from here until then, so the cell is not
|
||||
/// blank in the meantime; after, the store holds the same ones.
|
||||
pub(super) merged_thumbs:
|
||||
RefCell<std::collections::HashMap<i64, Vec<(dr_thumbs::ThumbSize, slint::Image)>>>,
|
||||
/// Where in the catalog the current window starts. Scrubbing moves this.
|
||||
pub(super) offset: RefCell<usize>,
|
||||
/// The first visible ordinal, kept so returning from the develop view lands
|
||||
@@ -449,6 +459,7 @@ impl LibraryController {
|
||||
needs_metadata: RefCell::new(Vec::new()),
|
||||
captured_at: RefCell::new(Vec::new()),
|
||||
thumb_class: RefCell::new(Vec::new()),
|
||||
merged_thumbs: RefCell::new(Default::default()),
|
||||
offset: RefCell::new(0),
|
||||
resume_at: std::cell::Cell::new(0),
|
||||
roll_open: std::cell::Cell::new(None),
|
||||
|
||||
@@ -81,6 +81,20 @@ pub fn catalogue_composite(
|
||||
match written {
|
||||
Ok(image) => {
|
||||
log::info!("merge: {} catalogued as image {image}", row.source_ref);
|
||||
let decoded: Vec<(dr_thumbs::ThumbSize, slint::Image)> = c
|
||||
.thumbnails
|
||||
.iter()
|
||||
.filter_map(|(class, t)| {
|
||||
let (w, h, rgba) = dr_thumbs::decode_rgba(&t.bytes).ok()?;
|
||||
Some((*class, super::window::to_slint_image(w, h, &rgba)))
|
||||
})
|
||||
.collect();
|
||||
if !decoded.is_empty() {
|
||||
ctl.merged_thumbs.borrow_mut().insert(image, decoded);
|
||||
}
|
||||
// Asked for afresh: a row under this name may have been drawn,
|
||||
// and answered, before this merge wrote over it.
|
||||
ctl.requested.borrow_mut().retain(|(id, _)| *id != image);
|
||||
reload(window, ctl);
|
||||
}
|
||||
Err(e) => log::warn!("merge: cataloguing {}: {e}", row.source_ref),
|
||||
@@ -114,6 +128,9 @@ pub fn open(
|
||||
|
||||
let filter = account.format_filter();
|
||||
*ctl.session.borrow_mut() = Some((conn.clone(), filter.clone()));
|
||||
// Image ids are one catalog's; another library's would name other
|
||||
// photographs.
|
||||
ctl.merged_thumbs.borrow_mut().clear();
|
||||
|
||||
// TRACES: FR-UI-8
|
||||
// Where this library's position is kept, and what this device signs it
|
||||
|
||||
@@ -467,6 +467,43 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
hold_thumbnails(&previous, &ids, &classes)
|
||||
};
|
||||
|
||||
// A composite a merge has just catalogued is drawn from the merge's own
|
||||
// thumbnails until the store can serve it (FR-MRG-6), and counts as
|
||||
// served at that class so no fetch is spent on a file still uploading.
|
||||
let wanted = dr_thumbs::ThumbSize::for_cell(
|
||||
window.global::<Library>().get_library_cell_size().max(1.0) as u32,
|
||||
);
|
||||
let mut held = held;
|
||||
{
|
||||
let merged = ctl.merged_thumbs.borrow();
|
||||
for c in &cells {
|
||||
let Some(made) = merged.get(&c.image_id) else {
|
||||
continue;
|
||||
};
|
||||
if held
|
||||
.get(&c.image_id)
|
||||
.is_some_and(|h| h.class == Some(wanted))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let pick = made
|
||||
.iter()
|
||||
.find(|(class, _)| *class == wanted)
|
||||
.or_else(|| made.iter().max_by_key(|(class, _)| class.edge()));
|
||||
if let Some((class, image)) = pick {
|
||||
held.insert(
|
||||
c.image_id,
|
||||
Held {
|
||||
thumbnail: image.clone(),
|
||||
has_thumb: true,
|
||||
unavailable: false,
|
||||
class: Some(*class),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rows: Vec<LibraryCell> = cells
|
||||
.iter()
|
||||
.zip(headings)
|
||||
@@ -989,7 +1026,7 @@ fn drain_thumbnails(
|
||||
/// This is a CPU copy, which is acceptable here and not in the develop path:
|
||||
/// a 256px thumbnail is 256 KB and happens once per image, where the canvas
|
||||
/// would pay per frame (ARCH §6.1).
|
||||
fn to_slint_image(width: u32, height: u32, rgba: &[u8]) -> slint::Image {
|
||||
pub(super) fn to_slint_image(width: u32, height: u32, rgba: &[u8]) -> slint::Image {
|
||||
let mut buf = slint::SharedPixelBuffer::<slint::Rgba8Pixel>::new(width, height);
|
||||
let expected = (width as usize) * (height as usize) * 4;
|
||||
let src = &rgba[..expected.min(rgba.len())];
|
||||
|
||||
Reference in New Issue
Block a user