image: add xlarge (4096px) on-demand preview tier

New `PhotoSize::XLarge` variant sits between `Large` (2048px) and
`Full` (original). On-demand generated and disk-cached at
`_xlarge/<hash>.jpg`, same waterfall as `Large` (embedded RAW preview
→ ffmpeg → image crate). Sources below 4096px serve at native size.

Reduces decoded bitmap memory from ~192MB (48MP full) to ~64MB for
the mobile viewer's zoom tier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
cameronandClaude Opus 4.7 committed 2026-05-25 15:33:03 -04:00
1 parent 9dba659d1e
commit b9175e2718
4 files changed
+196 -10

No files matched your search

+95 -6
View File
@@ -83,12 +83,14 @@ pub async fn get_image(
if let Some((library, path)) = resolved {
let image_size = req.size.unwrap_or(PhotoSize::Full);
// `size=large` is only meaningful for stills — there's no useful
// "2048px video preview" tier. Videos fall back to the existing
// thumb pipeline (which already handles gif/static selection).
// `mut` so the Large branch can downgrade itself to `Full` after a
// generation failure (RAW-preview branch below keys off `Full`).
let mut image_size = if image_size == PhotoSize::Large && file_types::is_video_file(&path) {
// `size=large|xlarge` is only meaningful for stills — there's no
// useful "resized video preview" tier. Videos fall back to the
// existing thumb pipeline (which already handles gif/static
// selection). `mut` so preview branches can downgrade to `Full`
// after a generation failure.
let mut image_size = if (image_size == PhotoSize::Large || image_size == PhotoSize::XLarge)
&& file_types::is_video_file(&path)
{
PhotoSize::Thumb
} else {
image_size
@@ -196,6 +198,93 @@ pub async fn get_image(
image_size = PhotoSize::Full;
}
if image_size == PhotoSize::XLarge {
let relative_path = path
.strip_prefix(&library.root_path)
.expect("Error stripping library root prefix from xlarge preview");
let relative_path_str = relative_path.to_string_lossy().replace('\\', "/");
let thumbs = Path::new(&app_state.thumbnail_path);
let xlarge_dir = thumbs.join("_xlarge");
let hash_xlarge_path: Option<PathBuf> = {
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
match dao.get_exif(&context, &relative_path_str) {
Ok(Some(row)) => row
.content_hash
.as_deref()
.map(|h| content_hash::xlarge_preview_path(thumbs, h)),
_ => None,
}
};
let scoped_legacy_xlarge_path =
content_hash::library_scoped_legacy_path(&xlarge_dir, library.id, relative_path);
let existing = hash_xlarge_path
.as_ref()
.filter(|p| p.exists())
.cloned()
.or_else(|| {
if scoped_legacy_xlarge_path.exists() {
Some(scoped_legacy_xlarge_path.clone())
} else {
None
}
});
if let Some(found) = existing {
if let Ok(file) = NamedFile::open(&found) {
span.set_status(Status::Ok);
return file
.use_etag(true)
.use_last_modified(true)
.prefer_utf8(true)
.into_response(&request);
}
}
let dest = hash_xlarge_path
.clone()
.unwrap_or_else(|| scoped_legacy_xlarge_path.clone());
let src = path.clone();
let dest_for_block = dest.clone();
let generated = web::block(move || {
if let Some(parent) = dest_for_block.parent() {
std::fs::create_dir_all(parent)?;
}
let tmp = dest_for_block.with_extension("jpg.tmp");
crate::thumbnails::generate_xlarge_preview(&src, &tmp)?;
std::fs::rename(&tmp, &dest_for_block)?;
Ok::<(), std::io::Error>(())
})
.await;
match generated {
Ok(Ok(())) => {
if let Ok(file) = NamedFile::open(&dest) {
span.set_status(Status::Ok);
return file
.use_etag(true)
.use_last_modified(true)
.prefer_utf8(true)
.into_response(&request);
}
}
Ok(Err(e)) => {
warn!(
"XLarge preview generation failed for {:?}: {} — falling back to original",
path, e
);
}
Err(e) => {
warn!(
"XLarge preview blocking-pool error for {:?}: {} — falling back to original",
path, e
);
}
}
image_size = PhotoSize::Full;
}
if image_size == PhotoSize::Thumb {
let relative_path = path
.strip_prefix(&library.root_path)