feat: apply EXIF orientation and colorspace correction to ffmpeg thumbnails
HEIC/HEIF sources use Display P3 color primaries. Without colorspace=bt709 the mjpeg encoder treated P3 values as sRGB, producing warm/oversaturated output. Also bake EXIF Orientation tag into pixels so saved JPEGs are canonically oriented. - Extract orientation from exif-reader and pass through all ffmpeg thumbnail paths (small, large, xlarge previews) - Add shared build_image_thumb_filter() for the 200px path - Add rotation + colorspace=bt709 to large/xlarge ffmpeg paths
This commit is contained in:
1 parent
5e6bd69405
commit
4720d5653a
2 files changed
+83
-15
No files matched your search
+49
-10
@@ -98,7 +98,7 @@ pub fn generate_image_thumbnail(src: &Path, thumb_path: &Path) -> std::io::Resul
|
||||
}
|
||||
|
||||
if file_types::needs_ffmpeg_thumbnail(src) {
|
||||
return generate_image_thumbnail_ffmpeg(src, thumb_path);
|
||||
return generate_image_thumbnail_ffmpeg(src, thumb_path, orientation);
|
||||
}
|
||||
|
||||
let img = image::open(src).map_err(|e| {
|
||||
@@ -140,7 +140,7 @@ pub fn generate_large_preview(src: &Path, dest: &Path) -> std::io::Result<()> {
|
||||
}
|
||||
|
||||
if file_types::needs_ffmpeg_thumbnail(src) {
|
||||
return generate_large_preview_ffmpeg(src, dest);
|
||||
return generate_large_preview_ffmpeg(src, dest, orientation);
|
||||
}
|
||||
|
||||
let img = image::open(src).map_err(|e| {
|
||||
@@ -175,14 +175,34 @@ fn encode_large_jpeg(img: image::DynamicImage, dest: &Path) -> std::io::Result<(
|
||||
/// ffmpeg path for HEIC/HEIF (image crate can't decode these). Mirrors
|
||||
/// [`crate::video::actors::generate_image_thumbnail_ffmpeg`] but scales
|
||||
/// to the large-preview cap instead of 200.
|
||||
fn generate_large_preview_ffmpeg(src: &Path, dest: &Path) -> std::io::Result<()> {
|
||||
// scale=W:-1 with force_original_aspect_ratio=decrease + the min(iw,W)
|
||||
// trick caps the long edge regardless of orientation, mirroring what
|
||||
// image::thumbnail does for the non-ffmpeg branch.
|
||||
let vf = format!(
|
||||
fn generate_large_preview_ffmpeg(
|
||||
src: &Path,
|
||||
dest: &Path,
|
||||
orientation: i32,
|
||||
) -> std::io::Result<()> {
|
||||
// Rotation + scale + colorspace. HEIC sources use Display P3; without
|
||||
// colorspace=bt709 the mjpeg encoder treats P3 values as sRGB, producing
|
||||
// warm/oversaturated output. The min(iw,cap) trick caps the long edge
|
||||
// regardless of orientation, mirroring image::thumbnail.
|
||||
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 = LARGE_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")
|
||||
@@ -232,7 +252,7 @@ pub fn generate_xlarge_preview(src: &Path, dest: &Path) -> std::io::Result<()> {
|
||||
}
|
||||
|
||||
if file_types::needs_ffmpeg_thumbnail(src) {
|
||||
return generate_xlarge_preview_ffmpeg(src, dest);
|
||||
return generate_xlarge_preview_ffmpeg(src, dest, orientation);
|
||||
}
|
||||
|
||||
let img = image::open(src).map_err(|e| {
|
||||
@@ -260,11 +280,30 @@ fn encode_xlarge_jpeg(img: image::DynamicImage, dest: &Path) -> std::io::Result<
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_xlarge_preview_ffmpeg(src: &Path, dest: &Path) -> std::io::Result<()> {
|
||||
let vf = format!(
|
||||
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")
|
||||
|
||||
Reference in new issue
Block a user