Feature/insight chat branching #109
+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) {
|
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| {
|
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) {
|
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| {
|
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
|
/// 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 the large-preview cap instead of 200.
|
||||||
fn generate_large_preview_ffmpeg(src: &Path, dest: &Path) -> std::io::Result<()> {
|
fn generate_large_preview_ffmpeg(
|
||||||
// scale=W:-1 with force_original_aspect_ratio=decrease + the min(iw,W)
|
src: &Path,
|
||||||
// trick caps the long edge regardless of orientation, mirroring what
|
dest: &Path,
|
||||||
// image::thumbnail does for the non-ffmpeg branch.
|
orientation: i32,
|
||||||
let vf = format!(
|
) -> 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}))'",
|
"scale='if(gt(iw,ih),min(iw,{cap}),-1)':'if(gt(iw,ih),-1,min(ih,{cap}))'",
|
||||||
cap = LARGE_PREVIEW_MAX_DIM
|
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")
|
let output = Command::new("ffmpeg")
|
||||||
.arg("-y")
|
.arg("-y")
|
||||||
.arg("-i")
|
.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) {
|
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| {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_xlarge_preview_ffmpeg(src: &Path, dest: &Path) -> std::io::Result<()> {
|
fn generate_xlarge_preview_ffmpeg(
|
||||||
let vf = format!(
|
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}))'",
|
"scale='if(gt(iw,ih),min(iw,{cap}),-1)':'if(gt(iw,ih),-1,min(ih,{cap}))'",
|
||||||
cap = XLARGE_PREVIEW_MAX_DIM
|
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")
|
let output = Command::new("ffmpeg")
|
||||||
.arg("-y")
|
.arg("-y")
|
||||||
.arg("-i")
|
.arg("-i")
|
||||||
|
|||||||
+34
-5
@@ -90,10 +90,39 @@ pub fn generate_video_thumbnail(path: &Path, destination: &Path) -> std::io::Res
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use ffmpeg to extract a 200px-wide thumbnail from formats the `image` crate
|
/// Build the ffmpeg filter chain for image thumbnails: rotation (from EXIF
|
||||||
/// can't decode (RAW: NEF/ARW, HEIC/HEIF). Writes JPEG bytes to `destination`
|
/// orientation) + scale + color-space conversion. HEIC sources use Display P3
|
||||||
/// regardless of its extension.
|
/// primaries; without `colorspace=bt709` the mjpeg encoder treats P3 values
|
||||||
pub fn generate_image_thumbnail_ffmpeg(path: &Path, destination: &Path) -> std::io::Result<()> {
|
/// as sRGB, producing warm/oversaturated output.
|
||||||
|
fn build_image_thumb_filter(orientation: i32, scale_w: u32) -> String {
|
||||||
|
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",
|
||||||
|
_ => "", // orientation 1 or unknown — no rotation needed
|
||||||
|
};
|
||||||
|
if rotation.is_empty() {
|
||||||
|
format!("scale={}:{{-1}},colorspace=bt709", scale_w)
|
||||||
|
} else {
|
||||||
|
format!("{},scale={}:{{-1}},colorspace=bt709", rotation, scale_w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use ffmpeg to extract a thumbnail from formats the `image` crate can't
|
||||||
|
/// decode (HEIC/HEIF, RAW: NEF/ARW). `orientation` is the EXIF Orientation
|
||||||
|
/// tag value (1..=8) — baked into the pixels so the saved JPEG is
|
||||||
|
/// canonically oriented. Writes JPEG bytes to `destination` regardless of
|
||||||
|
/// its extension.
|
||||||
|
pub fn generate_image_thumbnail_ffmpeg(
|
||||||
|
path: &Path,
|
||||||
|
destination: &Path,
|
||||||
|
orientation: i32,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
let vf = build_image_thumb_filter(orientation, 200);
|
||||||
let output = Command::new("ffmpeg")
|
let output = Command::new("ffmpeg")
|
||||||
.arg("-y")
|
.arg("-y")
|
||||||
.arg("-i")
|
.arg("-i")
|
||||||
@@ -101,7 +130,7 @@ pub fn generate_image_thumbnail_ffmpeg(path: &Path, destination: &Path) -> std::
|
|||||||
.arg("-vframes")
|
.arg("-vframes")
|
||||||
.arg("1")
|
.arg("1")
|
||||||
.arg("-vf")
|
.arg("-vf")
|
||||||
.arg("scale=200:-1")
|
.arg(&vf)
|
||||||
.arg("-f")
|
.arg("-f")
|
||||||
.arg("image2")
|
.arg("image2")
|
||||||
.arg("-c:v")
|
.arg("-c:v")
|
||||||
|
|||||||
Reference in New Issue
Block a user