feat(image): add a 1024px size=medium preview tier

Grid and memories tiles requested `size=large` (up to 2048px) for tiles
that display at ~590px (two columns) to ~1180px (one full-width column)
on a 3× phone, downloading 10–15× the pixels they show. `medium` caps
the long edge at 1024: crisp at both, a quarter of `large`'s pixels.
Cached under `_medium/` like the other tiers.

The large and xlarge generators were copies differing only in their
cap; fold them into one `generate_preview(src, dest, max_dim)` so the
third tier doesn't make a third copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 committed 2026-10-16 16:01:58 -04:00
1 parent fca9a28cee
commit 94a566b220
5 files changed
+92 -157

No files matched your search

+48 -37
View File
@@ -79,27 +79,25 @@ pub async fn get_image(
});
if let Some((library, path)) = resolved {
let image_size = req.size.unwrap_or(PhotoSize::Full);
let mut image_size = req.size.unwrap_or(PhotoSize::Full);
let mut tier = match image_size {
PhotoSize::Medium => Some(PreviewTier::Medium),
PhotoSize::Large => Some(PreviewTier::Large),
PhotoSize::XLarge => Some(PreviewTier::XLarge),
PhotoSize::Full | PhotoSize::Thumb => None,
};
// `size=large|xlarge` is only meaningful for stills — there's no
// The preview tiers are 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
};
if tier.is_some() && file_types::is_video_file(&path) {
tier = None;
image_size = PhotoSize::Thumb;
}
if image_size == PhotoSize::Large || image_size == PhotoSize::XLarge {
let tier = if image_size == PhotoSize::Large {
PreviewTier::Large
} else {
PreviewTier::XLarge
};
if let Some(tier) = tier {
if let Some(resp) = serve_preview(
tier,
library,
@@ -277,6 +275,7 @@ pub async fn get_image(
/// On-demand resized tiers served by [`serve_preview`].
#[derive(Clone, Copy)]
enum PreviewTier {
Medium,
Large,
XLarge,
}
@@ -284,6 +283,7 @@ enum PreviewTier {
impl PreviewTier {
fn name(self) -> &'static str {
match self {
PreviewTier::Medium => "Medium",
PreviewTier::Large => "Large",
PreviewTier::XLarge => "XLarge",
}
@@ -291,6 +291,7 @@ impl PreviewTier {
fn legacy_dir(self) -> &'static str {
match self {
PreviewTier::Medium => "_medium",
PreviewTier::Large => "_large",
PreviewTier::XLarge => "_xlarge",
}
@@ -298,17 +299,24 @@ impl PreviewTier {
fn hash_path(self, thumbs: &Path, hash: &str) -> PathBuf {
match self {
PreviewTier::Medium => content_hash::medium_preview_path(thumbs, hash),
PreviewTier::Large => content_hash::large_preview_path(thumbs, hash),
PreviewTier::XLarge => content_hash::xlarge_preview_path(thumbs, hash),
}
}
fn generate(self, src: &Path, dest: &Path) -> std::io::Result<()> {
fn max_dim(self) -> u32 {
use crate::thumbnails::*;
match self {
PreviewTier::Large => crate::thumbnails::generate_large_preview(src, dest),
PreviewTier::XLarge => crate::thumbnails::generate_xlarge_preview(src, dest),
PreviewTier::Medium => MEDIUM_PREVIEW_MAX_DIM,
PreviewTier::Large => LARGE_PREVIEW_MAX_DIM,
PreviewTier::XLarge => XLARGE_PREVIEW_MAX_DIM,
}
}
fn generate(self, src: &Path, dest: &Path) -> std::io::Result<()> {
crate::thumbnails::generate_preview(src, dest, self.max_dim())
}
}
/// Serve the cached `tier` preview of `path`, generating it on a miss.
@@ -1384,31 +1392,34 @@ mod tests {
}
#[actix_rt::test]
async fn large_preview_caps_a_panorama_at_2048() {
async fn preview_tiers_cap_a_panorama_at_their_size() {
let root = tempfile::tempdir().unwrap();
let thumbs = tempfile::tempdir().unwrap();
let lib = library(root.path());
let src = write_jpeg(root.path(), "pano.jpg", 6000, 1600);
let req = TestRequest::default().to_http_request();
serve_preview(
PreviewTier::Large,
&lib,
&src,
thumbs.path(),
&exif_dao(),
&req,
)
.await
.expect("preview served");
for (tier, dir, dims) in [
(PreviewTier::Medium, "_medium", (1024, 273)),
(PreviewTier::Large, "_large", (2048, 546)),
] {
serve_preview(tier, &lib, &src, thumbs.path(), &exif_dao(), &req)
.await
.expect("preview served");
// Unhashed row → library-scoped legacy path under `_large/`.
let dest = content_hash::library_scoped_legacy_path(
&thumbs.path().join("_large"),
lib.id,
Path::new("pano.jpg"),
);
let (w, h) = image::open(dest).unwrap().dimensions();
assert_eq!((w, h), (2048, 546));
// Unhashed row → library-scoped legacy path under the tier's dir.
let dest = content_hash::library_scoped_legacy_path(
&thumbs.path().join(dir),
lib.id,
Path::new("pano.jpg"),
);
assert_eq!(image::open(dest).unwrap().dimensions(), dims, "{dir}");
}
}
#[test]
fn size_param_accepts_medium() {
let q = web::Query::<ThumbnailRequest>::from_query("path=a.jpg&size=medium").unwrap();
assert_eq!(q.size, Some(PhotoSize::Medium));
}
}