Merge pull request 'feat(image): add a 1024px size=medium preview tier' (#174) from feat/medium-preview-tier into master
Reviewed-on: #174
This commit was merged in pull request #174.
This commit is contained in:
commit
f1779358b5
5 files changed
+92
-157
No files matched your search
@@ -511,7 +511,7 @@ All protected endpoints extract `Claims` via `FromRequest` trait implementation.
|
|||||||
**Key Endpoint Patterns:**
|
**Key Endpoint Patterns:**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Image serving & upload
|
GET /image?path=...&size=...&format=... // size: thumb (200px) | medium (1024) | large (2048) | xlarge (4096) | full
|
||||||
GET /image?path=...&size=...&format=...
|
GET /image?path=...&size=...&format=...
|
||||||
POST /image (multipart file upload)
|
POST /image (multipart file upload)
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,15 @@ pub fn thumbnail_path(thumbs_dir: &Path, hash: &str) -> PathBuf {
|
|||||||
thumbs_dir.join(shard).join(format!("{}.jpg", hash))
|
thumbs_dir.join(shard).join(format!("{}.jpg", hash))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hash-keyed medium-preview path: `<thumbs_dir>/_medium/<hash[..2]>/<hash>.jpg`.
|
||||||
|
pub fn medium_preview_path(thumbs_dir: &Path, hash: &str) -> PathBuf {
|
||||||
|
let shard = shard_prefix(hash);
|
||||||
|
thumbs_dir
|
||||||
|
.join("_medium")
|
||||||
|
.join(shard)
|
||||||
|
.join(format!("{}.jpg", hash))
|
||||||
|
}
|
||||||
|
|
||||||
/// Hash-keyed large-preview path: `<thumbs_dir>/_large/<hash[..2]>/<hash>.jpg`.
|
/// Hash-keyed large-preview path: `<thumbs_dir>/_large/<hash[..2]>/<hash>.jpg`.
|
||||||
/// Kept under the same root as 200px thumbs so deployments don't need a
|
/// Kept under the same root as 200px thumbs so deployments don't need a
|
||||||
/// second env var, but namespaced under `_large/` so the existing 200px
|
/// second env var, but namespaced under `_large/` so the existing 200px
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ pub enum PhotoSize {
|
|||||||
Full,
|
Full,
|
||||||
XLarge,
|
XLarge,
|
||||||
Large,
|
Large,
|
||||||
|
Medium,
|
||||||
Thumb,
|
Thumb,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+48
-37
@@ -79,27 +79,25 @@ pub async fn get_image(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Some((library, path)) = resolved {
|
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
|
// useful "resized video preview" tier. Videos fall back to the
|
||||||
// existing thumb pipeline (which already handles gif/static
|
// existing thumb pipeline (which already handles gif/static
|
||||||
// selection). `mut` so preview branches can downgrade to `Full`
|
// selection). `mut` so preview branches can downgrade to `Full`
|
||||||
// after a generation failure.
|
// after a generation failure.
|
||||||
let mut image_size = if (image_size == PhotoSize::Large || image_size == PhotoSize::XLarge)
|
if tier.is_some() && file_types::is_video_file(&path) {
|
||||||
&& file_types::is_video_file(&path)
|
tier = None;
|
||||||
{
|
image_size = PhotoSize::Thumb;
|
||||||
PhotoSize::Thumb
|
}
|
||||||
} else {
|
|
||||||
image_size
|
|
||||||
};
|
|
||||||
|
|
||||||
if image_size == PhotoSize::Large || image_size == PhotoSize::XLarge {
|
if let Some(tier) = tier {
|
||||||
let tier = if image_size == PhotoSize::Large {
|
|
||||||
PreviewTier::Large
|
|
||||||
} else {
|
|
||||||
PreviewTier::XLarge
|
|
||||||
};
|
|
||||||
if let Some(resp) = serve_preview(
|
if let Some(resp) = serve_preview(
|
||||||
tier,
|
tier,
|
||||||
library,
|
library,
|
||||||
@@ -277,6 +275,7 @@ pub async fn get_image(
|
|||||||
/// On-demand resized tiers served by [`serve_preview`].
|
/// On-demand resized tiers served by [`serve_preview`].
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum PreviewTier {
|
enum PreviewTier {
|
||||||
|
Medium,
|
||||||
Large,
|
Large,
|
||||||
XLarge,
|
XLarge,
|
||||||
}
|
}
|
||||||
@@ -284,6 +283,7 @@ enum PreviewTier {
|
|||||||
impl PreviewTier {
|
impl PreviewTier {
|
||||||
fn name(self) -> &'static str {
|
fn name(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
PreviewTier::Medium => "Medium",
|
||||||
PreviewTier::Large => "Large",
|
PreviewTier::Large => "Large",
|
||||||
PreviewTier::XLarge => "XLarge",
|
PreviewTier::XLarge => "XLarge",
|
||||||
}
|
}
|
||||||
@@ -291,6 +291,7 @@ impl PreviewTier {
|
|||||||
|
|
||||||
fn legacy_dir(self) -> &'static str {
|
fn legacy_dir(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
PreviewTier::Medium => "_medium",
|
||||||
PreviewTier::Large => "_large",
|
PreviewTier::Large => "_large",
|
||||||
PreviewTier::XLarge => "_xlarge",
|
PreviewTier::XLarge => "_xlarge",
|
||||||
}
|
}
|
||||||
@@ -298,17 +299,24 @@ impl PreviewTier {
|
|||||||
|
|
||||||
fn hash_path(self, thumbs: &Path, hash: &str) -> PathBuf {
|
fn hash_path(self, thumbs: &Path, hash: &str) -> PathBuf {
|
||||||
match self {
|
match self {
|
||||||
|
PreviewTier::Medium => content_hash::medium_preview_path(thumbs, hash),
|
||||||
PreviewTier::Large => content_hash::large_preview_path(thumbs, hash),
|
PreviewTier::Large => content_hash::large_preview_path(thumbs, hash),
|
||||||
PreviewTier::XLarge => content_hash::xlarge_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 {
|
match self {
|
||||||
PreviewTier::Large => crate::thumbnails::generate_large_preview(src, dest),
|
PreviewTier::Medium => MEDIUM_PREVIEW_MAX_DIM,
|
||||||
PreviewTier::XLarge => crate::thumbnails::generate_xlarge_preview(src, dest),
|
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.
|
/// Serve the cached `tier` preview of `path`, generating it on a miss.
|
||||||
@@ -1384,31 +1392,34 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[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 root = tempfile::tempdir().unwrap();
|
||||||
let thumbs = tempfile::tempdir().unwrap();
|
let thumbs = tempfile::tempdir().unwrap();
|
||||||
let lib = library(root.path());
|
let lib = library(root.path());
|
||||||
let src = write_jpeg(root.path(), "pano.jpg", 6000, 1600);
|
let src = write_jpeg(root.path(), "pano.jpg", 6000, 1600);
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
|
|
||||||
serve_preview(
|
for (tier, dir, dims) in [
|
||||||
PreviewTier::Large,
|
(PreviewTier::Medium, "_medium", (1024, 273)),
|
||||||
&lib,
|
(PreviewTier::Large, "_large", (2048, 546)),
|
||||||
&src,
|
] {
|
||||||
thumbs.path(),
|
serve_preview(tier, &lib, &src, thumbs.path(), &exif_dao(), &req)
|
||||||
&exif_dao(),
|
.await
|
||||||
&req,
|
.expect("preview served");
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("preview served");
|
|
||||||
|
|
||||||
// Unhashed row → library-scoped legacy path under `_large/`.
|
// Unhashed row → library-scoped legacy path under the tier's dir.
|
||||||
let dest = content_hash::library_scoped_legacy_path(
|
let dest = content_hash::library_scoped_legacy_path(
|
||||||
&thumbs.path().join("_large"),
|
&thumbs.path().join(dir),
|
||||||
lib.id,
|
lib.id,
|
||||||
Path::new("pano.jpg"),
|
Path::new("pano.jpg"),
|
||||||
);
|
);
|
||||||
let (w, h) = image::open(dest).unwrap().dimensions();
|
assert_eq!(image::open(dest).unwrap().dimensions(), dims, "{dir}");
|
||||||
assert_eq!((w, h), (2048, 546));
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+33
-119
@@ -25,6 +25,13 @@ use crate::file_types;
|
|||||||
use crate::libraries;
|
use crate::libraries;
|
||||||
use crate::video::actors::{generate_image_thumbnail_ffmpeg, generate_video_thumbnail};
|
use crate::video::actors::{generate_image_thumbnail_ffmpeg, generate_video_thumbnail};
|
||||||
|
|
||||||
|
/// Maximum long-edge size (px) for the medium preview tier: grid and
|
||||||
|
/// memories tiles. A full-width single-column tile on a 3× phone is
|
||||||
|
/// ≈1180px and a two-column one ≈590px, so 1024 is crisp for both at a
|
||||||
|
/// quarter of `large`'s pixels. Tiles used to pull `large`, downloading
|
||||||
|
/// 10–15× what they displayed.
|
||||||
|
pub const MEDIUM_PREVIEW_MAX_DIM: u32 = 1024;
|
||||||
|
|
||||||
/// Maximum long-edge size (px) for the large preview tier. Tuned to look
|
/// Maximum long-edge size (px) for the large preview tier. Tuned to look
|
||||||
/// crisp full-screen on a 3× phone (≈1290×2796 native) and to hold up
|
/// crisp full-screen on a 3× phone (≈1290×2796 native) and to hold up
|
||||||
/// through a few stops of pinch-zoom before the original streams in.
|
/// through a few stops of pinch-zoom before the original streams in.
|
||||||
@@ -32,11 +39,11 @@ use crate::video::actors::{generate_image_thumbnail_ffmpeg, generate_video_thumb
|
|||||||
/// `size=full` and the handler streams the original bytes.
|
/// `size=full` and the handler streams the original bytes.
|
||||||
pub const LARGE_PREVIEW_MAX_DIM: u32 = 2048;
|
pub const LARGE_PREVIEW_MAX_DIM: u32 = 2048;
|
||||||
|
|
||||||
/// JPEG quality for the large and xlarge preview tiers. 85 is the
|
/// JPEG quality for the preview tiers. 85 is the
|
||||||
/// conventional "indistinguishable from source at viewing size" point —
|
/// conventional "indistinguishable from source at viewing size" point —
|
||||||
/// well above the `image` crate's default ~75, but well below quality-90+
|
/// well above the `image` crate's default ~75, but well below quality-90+
|
||||||
/// territory where file size doubles for no perceptible win.
|
/// territory where file size doubles for no perceptible win.
|
||||||
const LARGE_PREVIEW_JPEG_QUALITY: u8 = 85;
|
const PREVIEW_JPEG_QUALITY: u8 = 85;
|
||||||
|
|
||||||
/// Maximum long-edge size (px) for the xlarge preview tier. Bridges the
|
/// Maximum long-edge size (px) for the xlarge preview tier. Bridges the
|
||||||
/// gap between `large` (2048px, ~16MB decoded) and the original bytes
|
/// gap between `large` (2048px, ~16MB decoded) and the original bytes
|
||||||
@@ -108,18 +115,20 @@ pub fn generate_image_thumbnail(src: &Path, thumb_path: &Path) -> std::io::Resul
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the on-demand large-preview tier (≈2048 long edge JPEG).
|
/// Generate an on-demand preview tier: a JPEG whose long edge is capped
|
||||||
|
/// at `max_dim` ([`MEDIUM_PREVIEW_MAX_DIM`], [`LARGE_PREVIEW_MAX_DIM`] or
|
||||||
|
/// [`XLARGE_PREVIEW_MAX_DIM`]).
|
||||||
///
|
///
|
||||||
/// Mirrors [`generate_image_thumbnail`]'s decode waterfall — embedded RAW
|
/// Mirrors [`generate_image_thumbnail`]'s decode waterfall — embedded RAW
|
||||||
/// preview, then ffmpeg for HEIC/HEIF, then the `image` crate — but
|
/// preview, then ffmpeg for HEIC/HEIF, then the `image` crate — but
|
||||||
/// resizes to [`LARGE_PREVIEW_MAX_DIM`] instead of 200 and encodes at
|
/// resizes to `max_dim` instead of 200 and encodes at quality 85 rather
|
||||||
/// quality 85 rather than the crate default. Caller is expected to have
|
/// than the crate default. Caller is expected to have already created the
|
||||||
/// already created the destination's parent dir.
|
/// destination's parent dir.
|
||||||
///
|
///
|
||||||
/// Does not upscale: if the source's long edge is already below the cap,
|
/// Does not upscale: if the source's long edge is already below the cap,
|
||||||
/// the file is encoded at its native size (still re-saved as JPEG so the
|
/// the file is encoded at its native size (still re-saved as JPEG so the
|
||||||
/// served bytes match for callers that key off `Content-Length`).
|
/// served bytes match for callers that key off `Content-Length`).
|
||||||
pub fn generate_large_preview(src: &Path, dest: &Path) -> std::io::Result<()> {
|
pub fn generate_preview(src: &Path, dest: &Path, max_dim: u32) -> std::io::Result<()> {
|
||||||
let orientation = exif::read_orientation(src).unwrap_or(1);
|
let orientation = exif::read_orientation(src).unwrap_or(1);
|
||||||
|
|
||||||
// RAW: prefer the in-file embedded JPEG preview over raw-sensor decode.
|
// RAW: prefer the in-file embedded JPEG preview over raw-sensor decode.
|
||||||
@@ -132,36 +141,35 @@ pub fn generate_large_preview(src: &Path, dest: &Path) -> std::io::Result<()> {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let img = exif::apply_orientation(img, orientation);
|
let img = exif::apply_orientation(img, orientation);
|
||||||
return encode_large_jpeg(img, dest);
|
return encode_preview_jpeg(img, dest, max_dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
if file_types::needs_ffmpeg_thumbnail(src) {
|
if file_types::needs_ffmpeg_thumbnail(src) {
|
||||||
return generate_large_preview_ffmpeg(src, dest, orientation);
|
return generate_preview_ffmpeg(src, dest, orientation, max_dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
let img = image::open(src).map_err(|e| {
|
let img = image::open(src).map_err(|e| {
|
||||||
std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{:?}: {}", src, e))
|
std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{:?}: {}", src, e))
|
||||||
})?;
|
})?;
|
||||||
let img = exif::apply_orientation(img, orientation);
|
let img = exif::apply_orientation(img, orientation);
|
||||||
encode_large_jpeg(img, dest)
|
encode_preview_jpeg(img, dest, max_dim)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resize-if-needed + JPEG-encode at q85. Used by both the embedded-preview
|
/// Resize-if-needed + JPEG-encode at q85. Used by both the embedded-preview
|
||||||
/// and image-crate-decode branches of `generate_large_preview`.
|
/// and image-crate-decode branches of [`generate_preview`].
|
||||||
fn encode_large_jpeg(img: image::DynamicImage, dest: &Path) -> std::io::Result<()> {
|
fn encode_preview_jpeg(img: image::DynamicImage, dest: &Path, max_dim: u32) -> std::io::Result<()> {
|
||||||
let (w, h) = img.dimensions();
|
let (w, h) = img.dimensions();
|
||||||
let max_dim = w.max(h);
|
// Avoid upscaling small sources — pointless work and adds nothing for
|
||||||
// Avoid upscaling tiny sources — pointless work and adds nothing for
|
|
||||||
// the viewer. `thumbnail` would scale up freely; explicit guard.
|
// the viewer. `thumbnail` would scale up freely; explicit guard.
|
||||||
let scaled = if max_dim > LARGE_PREVIEW_MAX_DIM {
|
let scaled = if w.max(h) > max_dim {
|
||||||
img.thumbnail(LARGE_PREVIEW_MAX_DIM, LARGE_PREVIEW_MAX_DIM)
|
img.thumbnail(max_dim, max_dim)
|
||||||
} else {
|
} else {
|
||||||
img
|
img
|
||||||
};
|
};
|
||||||
let file = std::fs::File::create(dest)
|
let file = std::fs::File::create(dest)
|
||||||
.map_err(|e| std::io::Error::other(format!("create {:?}: {}", dest, e)))?;
|
.map_err(|e| std::io::Error::other(format!("create {:?}: {}", dest, e)))?;
|
||||||
let mut writer = std::io::BufWriter::new(file);
|
let mut writer = std::io::BufWriter::new(file);
|
||||||
let mut encoder = JpegEncoder::new_with_quality(&mut writer, LARGE_PREVIEW_JPEG_QUALITY);
|
let mut encoder = JpegEncoder::new_with_quality(&mut writer, PREVIEW_JPEG_QUALITY);
|
||||||
encoder
|
encoder
|
||||||
.encode_image(&scaled)
|
.encode_image(&scaled)
|
||||||
.map_err(|e| std::io::Error::other(format!("encode {:?}: {}", dest, e)))?;
|
.map_err(|e| std::io::Error::other(format!("encode {:?}: {}", dest, e)))?;
|
||||||
@@ -170,8 +178,13 @@ fn encode_large_jpeg(img: image::DynamicImage, dest: &Path) -> std::io::Result<(
|
|||||||
|
|
||||||
/// ffmpeg path for HEIC/HEIF (image crate can't decode these). Mirrors
|
/// ffmpeg path for HEIC/HEIF (image crate can't decode these). Mirrors
|
||||||
/// [`crate::video::actors::generate_image_thumbnail_ffmpeg`] but scales
|
/// [`crate::video::actors::generate_image_thumbnail_ffmpeg`] but scales
|
||||||
/// to the large-preview cap instead of 200.
|
/// to `max_dim` instead of 200.
|
||||||
fn generate_large_preview_ffmpeg(src: &Path, dest: &Path, orientation: i32) -> std::io::Result<()> {
|
fn generate_preview_ffmpeg(
|
||||||
|
src: &Path,
|
||||||
|
dest: &Path,
|
||||||
|
orientation: i32,
|
||||||
|
max_dim: u32,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
// Rotation + scale + colorspace. HEIC sources use Display P3; without
|
// Rotation + scale + colorspace. HEIC sources use Display P3; without
|
||||||
// colorspace=bt709 the mjpeg encoder treats P3 values as sRGB, producing
|
// colorspace=bt709 the mjpeg encoder treats P3 values as sRGB, producing
|
||||||
// warm/oversaturated output. The min(iw,cap) trick caps the long edge
|
// warm/oversaturated output. The min(iw,cap) trick caps the long edge
|
||||||
@@ -188,7 +201,7 @@ fn generate_large_preview_ffmpeg(src: &Path, dest: &Path, orientation: i32) -> s
|
|||||||
};
|
};
|
||||||
let scale_expr = format!(
|
let scale_expr = format!(
|
||||||
"scale='if(gt(iw,ih),min(iw,{cap}),-1)':'if(gt(iw,ih),-1,min(ih,{cap}))'",
|
"scale='if(gt(iw,ih),min(iw,{cap}),-1)':'if(gt(iw,ih),-1,min(ih,{cap}))'",
|
||||||
cap = LARGE_PREVIEW_MAX_DIM
|
cap = max_dim
|
||||||
);
|
);
|
||||||
let vf = if rotation.is_empty() {
|
let vf = if rotation.is_empty() {
|
||||||
format!("{},colorspace=bt709", scale_expr)
|
format!("{},colorspace=bt709", scale_expr)
|
||||||
@@ -224,105 +237,6 @@ fn generate_large_preview_ffmpeg(src: &Path, dest: &Path, orientation: i32) -> s
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the on-demand xlarge-preview tier (≈4096 long edge JPEG).
|
|
||||||
///
|
|
||||||
/// Same waterfall as [`generate_large_preview`] but targeting
|
|
||||||
/// [`XLARGE_PREVIEW_MAX_DIM`]. Sources whose long edge is already below
|
|
||||||
/// the cap are encoded at native size (no upscale).
|
|
||||||
pub fn generate_xlarge_preview(src: &Path, dest: &Path) -> std::io::Result<()> {
|
|
||||||
let orientation = exif::read_orientation(src).unwrap_or(1);
|
|
||||||
|
|
||||||
if let Some(preview) = exif::extract_embedded_jpeg_preview(src) {
|
|
||||||
let img = image::load_from_memory(&preview).map_err(|e| {
|
|
||||||
std::io::Error::new(
|
|
||||||
std::io::ErrorKind::InvalidData,
|
|
||||||
format!("decode embedded preview {:?}: {}", src, e),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let img = exif::apply_orientation(img, orientation);
|
|
||||||
return encode_xlarge_jpeg(img, dest);
|
|
||||||
}
|
|
||||||
|
|
||||||
if file_types::needs_ffmpeg_thumbnail(src) {
|
|
||||||
return generate_xlarge_preview_ffmpeg(src, dest, orientation);
|
|
||||||
}
|
|
||||||
|
|
||||||
let img = image::open(src).map_err(|e| {
|
|
||||||
std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{:?}: {}", src, e))
|
|
||||||
})?;
|
|
||||||
let img = exif::apply_orientation(img, orientation);
|
|
||||||
encode_xlarge_jpeg(img, dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encode_xlarge_jpeg(img: image::DynamicImage, dest: &Path) -> std::io::Result<()> {
|
|
||||||
let (w, h) = img.dimensions();
|
|
||||||
let max_dim = w.max(h);
|
|
||||||
let scaled = if max_dim > XLARGE_PREVIEW_MAX_DIM {
|
|
||||||
img.thumbnail(XLARGE_PREVIEW_MAX_DIM, XLARGE_PREVIEW_MAX_DIM)
|
|
||||||
} else {
|
|
||||||
img
|
|
||||||
};
|
|
||||||
let file = std::fs::File::create(dest)
|
|
||||||
.map_err(|e| std::io::Error::other(format!("create {:?}: {}", dest, e)))?;
|
|
||||||
let mut writer = std::io::BufWriter::new(file);
|
|
||||||
let mut encoder = JpegEncoder::new_with_quality(&mut writer, LARGE_PREVIEW_JPEG_QUALITY);
|
|
||||||
encoder
|
|
||||||
.encode_image(&scaled)
|
|
||||||
.map_err(|e| std::io::Error::other(format!("encode {:?}: {}", dest, e)))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_xlarge_preview_ffmpeg(
|
|
||||||
src: &Path,
|
|
||||||
dest: &Path,
|
|
||||||
orientation: i32,
|
|
||||||
) -> std::io::Result<()> {
|
|
||||||
let rotation = match orientation {
|
|
||||||
2 => "hflip",
|
|
||||||
3 => "transpose=2",
|
|
||||||
4 => "vflip",
|
|
||||||
5 => "transpose=0,hflip",
|
|
||||||
6 => "transpose=0",
|
|
||||||
7 => "transpose=1,hflip",
|
|
||||||
8 => "transpose=1",
|
|
||||||
_ => "",
|
|
||||||
};
|
|
||||||
let scale_expr = format!(
|
|
||||||
"scale='if(gt(iw,ih),min(iw,{cap}),-1)':'if(gt(iw,ih),-1,min(ih,{cap}))'",
|
|
||||||
cap = XLARGE_PREVIEW_MAX_DIM
|
|
||||||
);
|
|
||||||
let vf = if rotation.is_empty() {
|
|
||||||
format!("{},colorspace=bt709", scale_expr)
|
|
||||||
} else {
|
|
||||||
format!("{},{},colorspace=bt709", rotation, scale_expr)
|
|
||||||
};
|
|
||||||
let output = Command::new("ffmpeg")
|
|
||||||
.arg("-y")
|
|
||||||
.arg("-i")
|
|
||||||
.arg(src)
|
|
||||||
.arg("-vframes")
|
|
||||||
.arg("1")
|
|
||||||
.arg("-vf")
|
|
||||||
.arg(&vf)
|
|
||||||
.arg("-q:v")
|
|
||||||
.arg("5")
|
|
||||||
.arg("-f")
|
|
||||||
.arg("image2")
|
|
||||||
.arg("-c:v")
|
|
||||||
.arg("mjpeg")
|
|
||||||
.arg(dest)
|
|
||||||
.output()?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
return Err(std::io::Error::other(format!(
|
|
||||||
"ffmpeg failed ({}): {}",
|
|
||||||
output.status,
|
|
||||||
String::from_utf8_lossy(&output.stderr).trim()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(name = "creating thumbnails", skip_all)]
|
#[tracing::instrument(name = "creating thumbnails", skip_all)]
|
||||||
pub fn create_thumbnails(libs: &[libraries::Library], excluded_dirs: &[String]) {
|
pub fn create_thumbnails(libs: &[libraries::Library], excluded_dirs: &[String]) {
|
||||||
let span = tracing::Span::current();
|
let span = tracing::Span::current();
|
||||||
|
|||||||
Reference in new issue
Block a user