mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-13 17:31:09 -06:00
Cut edited thumbnails from the decoded source, not the file (#3810)
* Cut edited thumbnails from the decoded source, not the file In Detail view the decoded source of the photo on screen is held at full size, but the thumbnail branch of `source_ref_plain` only looks for a preview-level source, so each edit re-queued a thumbnail that decoded the original again —1.4 GB of gate usage for one 489 MB file, matching the reported stall on the SMB share. When building a thumbnail job, reuse the photo's decoded source at whatever level it is cached (preview or full), fitting it to the thumbnail's edge, so an edited photo's thumbnail no longer re-reads the original file while its source is already in memory. Test: a_thumbnail_is_cut_from_the_decoded_full_source failed before the change and passes after. * Reuse decoded sources for thumbnail rendering Assisted-by: OpenAI Codex * Web worker: box the fitted source, as SourceRef::Fit holds it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <FredApps@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
eliets2
FredApps
FredApps
parent
1b6e722292
commit
e50db33453
@@ -33,6 +33,9 @@ pub struct WireJob {
|
||||
pub origin: Source,
|
||||
/// Decode the source at most this long.
|
||||
pub max_edge: usize,
|
||||
/// Resize an already decoded source exactly as the engine's cached-source path does.
|
||||
#[serde(default)]
|
||||
pub fit_edge: Option<usize>,
|
||||
pub raw: bool,
|
||||
#[serde(default)]
|
||||
pub relative_wb: bool,
|
||||
@@ -67,7 +70,8 @@ impl WireJob {
|
||||
/// `view`: the slot name, for jobs that keep a stage cache.
|
||||
pub fn from_job(job: &RenderJob, thumb_cached: bool, view: &str) -> WireJob {
|
||||
let max_edge = match &job.source {
|
||||
SourceRef::Demo { max_edge, .. } | SourceRef::File { max_edge, .. } | SourceRef::Fit { max_edge, .. } => *max_edge,
|
||||
SourceRef::Demo { max_edge, .. } | SourceRef::File { max_edge, .. } => *max_edge,
|
||||
SourceRef::Fit { from, .. } => from.image.width.max(from.image.height),
|
||||
SourceRef::Loaded(img) => img.image.width.max(img.image.height),
|
||||
SourceRef::Smart { .. } => 2560,
|
||||
};
|
||||
@@ -77,6 +81,10 @@ impl WireJob {
|
||||
level: job.level,
|
||||
origin: job.origin.clone(),
|
||||
max_edge,
|
||||
fit_edge: match &job.source {
|
||||
SourceRef::Fit { max_edge, .. } => Some(*max_edge),
|
||||
_ => None,
|
||||
},
|
||||
raw: job.info.raw,
|
||||
relative_wb: job.info.relative_wb,
|
||||
legacy_relative_wb: job.info.legacy_relative_wb,
|
||||
@@ -196,6 +204,10 @@ impl WorkerCore {
|
||||
src
|
||||
}
|
||||
};
|
||||
let src = match job.fit_edge {
|
||||
Some(max_edge) => SourceRef::Fit { from: Box::new(src), max_edge }.load_source()?,
|
||||
None => src,
|
||||
};
|
||||
let info = src.info_or(job.info());
|
||||
let mut rendered = match &job.stages {
|
||||
Some(view) => {
|
||||
@@ -472,6 +484,7 @@ mod tests {
|
||||
level: SourceLevel::Thumb,
|
||||
origin,
|
||||
max_edge: 512,
|
||||
fit_edge: None,
|
||||
raw: false,
|
||||
relative_wb: false,
|
||||
legacy_relative_wb: false,
|
||||
|
||||
@@ -344,13 +344,17 @@ impl MediaCache {
|
||||
{
|
||||
return SourceRef::Loaded(Box::new(a));
|
||||
}
|
||||
// a thumbnail of a photo whose preview is decoded (the one in the loupe) is cut from it; not
|
||||
// a thumbnail of a photo whose preview or full source is decoded (the one in the loupe) is cut from it; not
|
||||
// with a denoised picture, which the thumbnail would lose, nor from one decoded with another camera profile
|
||||
if cached
|
||||
&& level == SourceLevel::Thumb
|
||||
&& self.denoise.spec(p.id).is_none()
|
||||
&& let Some(big) = self.get_source(p.id, SourceLevel::Preview).filter(|b| b.fits_outliers(outliers))
|
||||
&& (big.camera_profile == wanted.as_ref().map(|(tag, _)| tag.clone()) || (big.from_proxy && self.original_offline(p)))
|
||||
&& let Some(big) = [SourceLevel::Preview, SourceLevel::Full].into_iter().find_map(|level| {
|
||||
self.get_source(p.id, level).filter(|b| {
|
||||
b.fits_outliers(outliers)
|
||||
&& (b.camera_profile.as_deref() == wanted.as_ref().map(|(tag, _)| tag.as_str()) || (b.from_proxy && self.original_offline(p)))
|
||||
})
|
||||
})
|
||||
{
|
||||
return SourceRef::Fit { from: Box::new(big), max_edge: level.max_edge() };
|
||||
}
|
||||
|
||||
@@ -644,3 +644,19 @@ fn an_export_develops_from_a_source_twice_its_size() {
|
||||
}
|
||||
assert_eq!(s.render_job(id, 1600, 1600, false, true).unwrap().level, SourceLevel::Preview, "a view keeps the preview");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_thumbnail_is_cut_from_the_decoded_full_source() {
|
||||
let mut s = crate::Session::new();
|
||||
let p = Photo::new(PhotoId(1), Source::File { path: "/nowhere/a.ARW".into() }, "a.ARW", "ARW", 2400, 1600, "");
|
||||
s.catalog.apply(lightcraft_catalog::Op::AddPhoto { photo: Box::new(p.clone()) }).unwrap();
|
||||
s.media.insert(p.id, SourceLevel::Full, Arc::new(Rgb32f::from_fn(2400, 1600, |x, _| [x as f32 / 2400.0, 0.5, 0.25])));
|
||||
let job = s.thumb_job(p.id, 512).unwrap();
|
||||
assert!(matches!(job.source, SourceRef::Fit { .. }));
|
||||
let r = job.run();
|
||||
let thumb = r.rendered.as_ref().unwrap();
|
||||
assert_eq!((thumb.image.width, thumb.image.height), (512, 341));
|
||||
s.accept(&r);
|
||||
let cached = s.media.get(p.id, SourceLevel::Thumb).unwrap();
|
||||
assert_eq!((cached.width, cached.height), (512, 341), "a thumbnail-sized source is kept, not the full source");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user