From c10ca4e958b8ed6b71cd3707533118c7cf922eda Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Tue, 13 Oct 2026 09:01:48 +0200 Subject: [PATCH] LR-IMP-PREVIEW-READ: a raw whose embedded preview can't be read still gets a thumbnail (fixes #1448) The grid queues no render behind an unedited raw's quick thumbnail, since the embedded JPEG is meant to be the thumbnail. But whether that JPEG can be read is only known when the quick job runs, and `quick_thumb_job` had no fallback: when the preview loader returned nothing the job failed with "no quick preview", the failure was remembered for that key, and the cell stayed empty until an edit changed it. The quick job now carries the real thumbnail job as its last resort, as `quick_view_job` already does for the loupe. --- crates/engine/src/files.rs | 21 +++++++++++++++++++++ crates/engine/src/media.rs | 8 +++++--- docs/raw-preview-reads.md | 3 +++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/files.rs b/crates/engine/src/files.rs index dc79b8b31..ea3b2c72c 100644 --- a/crates/engine/src/files.rs +++ b/crates/engine/src/files.rs @@ -2296,4 +2296,25 @@ mod tests { assert_eq!(s.quick_view_job(id, 1600, true).unwrap().run().quick, Some(QuickSource::Small)); let _ = std::fs::remove_dir_all(&dir); } + + /// Issue #1448: the grid renders nothing behind an unedited raw's quick thumbnail, so when the + /// embedded preview can't be read (and nothing is cached) the quick job itself must render the + /// raw, or the cell stays empty for good ("no quick preview"). + #[test] + fn a_quick_thumbnail_without_an_embedded_preview_renders_the_raw() { + use crate::media::QuickSource; + let dir = std::env::temp_dir().join(format!("lc-quick-none-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("a.cr3"); + std::fs::write(&path, cr3_with_preview(96, 64)).unwrap(); + let mut s = crate::Session::new().with_fs(); + let r = s.execute("library.import", &serde_json::json!({"paths": [path.to_string_lossy()]})).unwrap(); + let id = lightcraft_catalog::PhotoId(r["imported"][0].as_u64().unwrap()); + s.media.preview_loader = Some(std::sync::Arc::new(|_: &str, _| None)); + let job = s.thumb_job(id, 128).unwrap(); + let q = s.quick_thumb_job(&job).unwrap().run(); + assert_eq!((q.quick, q.key), (Some(QuickSource::Small), job.key)); + assert!(q.rendered.is_ok(), "{:?}", q.rendered.err()); + let _ = std::fs::remove_dir_all(&dir); + } } diff --git a/crates/engine/src/media.rs b/crates/engine/src/media.rs index cf17c3264..e2417ec65 100644 --- a/crates/engine/src/media.rs +++ b/crates/engine/src/media.rs @@ -1427,8 +1427,10 @@ impl crate::Session { /// A first grid thumbnail for an unedited raw: the cached render for `job` (the real /// thumbnail job, from [`Self::thumb_job`]) if there is one — then the result carries - /// [`QuickSource::Cached`] and `job.key` and is final — else its embedded preview. `None` for - /// other photos (render them directly). + /// [`QuickSource::Cached`] and `job.key` and is final — else its embedded preview, else `job` + /// itself: the grid queues no render behind an unedited raw's quick thumbnail + /// ([`Self::embedded_thumb_is_final`]), so a preview that can't be read must not leave the + /// cell empty (issue #1448). `None` for other photos (render them directly). pub fn quick_thumb_job(&mut self, job: &RenderJob) -> Option { let p = self.catalog.photo(job.photo)?.clone(); let (path, l) = self.embedded_of(&p)?; @@ -1439,7 +1441,7 @@ impl crate::Session { key: job.key, cached: job.cache.clone().into_iter().collect(), embedded: Some((path, l, edge)), - small: None, + small: Some(Box::new(job.clone())), }) } diff --git a/docs/raw-preview-reads.md b/docs/raw-preview-reads.md index fe82f4662..30ebd642f 100644 --- a/docs/raw-preview-reads.md +++ b/docs/raw-preview-reads.md @@ -101,6 +101,9 @@ loupe render of a cold photo finished 174 s after the click, most of it spent be - `grid::skips_the_real_render(embedded_is_final, from_embedded)` is the decision, unit-tested: a **cached render** as the stand-in, an **edited photo** and a **raw with no embedded JPEG** all still render, because their thumbnail must show the pipeline's output rather than the camera's. +- Whether the embedded JPEG can actually be read is only known when the quick job runs, so + `Session::quick_thumb_job` carries the real thumbnail job as its last resort: a raw whose preview + the loader can't read renders the raw instead of leaving the cell empty (issue #1448). - The thumbnail key still changes with the settings hash, so editing a photo re-renders its thumbnail through the ordinary background path.