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:
1 parent
9dba659d1e
commit
b9175e2718
4 files changed
+196
-10
No files matched your search
+95
-6
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user