From b40ae4490a7f7edcac4bfdc8734f16cddc62203e Mon Sep 17 00:00:00 2001 From: julyX10 Date: Fri, 24 Apr 2026 13:25:36 +0800 Subject: [PATCH] Lap heif support (#84) * wip: wire Windows libheif and libde265 support * chore: add libde265 submodule * fix windows build error * update build.rs * fix: error C1083: Cannot open include file: 'libde265/de265-version.h' * refactor: remove redundant orientation parameter in HEIF processing as libheif handles transformations automatically * Update Readme * fix: cannot find libde265 build when compile linux version * Update build.rs * fix: detect image mime types for protocol previews * fix: generate Linux AVIF previews and thumbnails as JPEG * fix: decode Linux AVIF previews without video fallback * update t_protocal.rs * Revert "update t_protocal.rs" This reverts commit f541ed457e5411e1a4844abba4dadd4ccf3f8a16. * Revert "fix: decode Linux AVIF previews without video fallback" This reverts commit d8805a36e6d700504a03200a1ed69f4a5853270a. * fix: preserve aspect ratio for FFmpeg-extracted thumbnails (fixes Linux AVIF cropping) --- .gitmodules | 3 + README.md | 1 + src-tauri/build.rs | 212 ++++++++++++++++++++++++++++++++++- src-tauri/src/main.rs | 15 +++ src-tauri/src/t_heif.rs | 102 ++++++++++++++--- src-tauri/src/t_image.rs | 21 +++- src-tauri/src/t_protocol.rs | 39 ++++++- src-tauri/src/t_sqlite.rs | 9 ++ src-tauri/src/t_video.rs | 4 +- src-vite/src/common/utils.ts | 3 + 10 files changed, 386 insertions(+), 23 deletions(-) diff --git a/.gitmodules b/.gitmodules index 790f2be5..077f94a6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ path = src-tauri/third_party/libheif url = https://github.com/strukturag/libheif.git branch = v1.21.x-releases +[submodule "src-tauri/third_party/libde265"] + path = src-tauri/third_party/libde265 + url = https://github.com/strukturag/libde265.git diff --git a/README.md b/README.md index c3a9fef2..aada80d0 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ cargo tauri dev | Library | Purpose | | :-- | :-- | | [LibRaw](https://github.com/LibRaw/LibRaw) | RAW image decoding and thumbnail extraction | +| [libheif](https://github.com/strukturag/libheif) | HEIC/HEIF image decoding and preview generation | | [FFmpeg](https://ffmpeg.org/) | Video processing and thumbnail generation | | [ONNX Runtime](https://onnxruntime.ai/) | Local AI model inference engine | | [CLIP](https://github.com/openai/CLIP) | Image-text similarity search | diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 8b357823..e7abb1dc 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -27,6 +27,7 @@ fn main() { fn build_libheif() { println!("cargo:rerun-if-changed=third_party/libheif"); + println!("cargo:rerun-if-changed=third_party/libde265"); let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); let source_dir = manifest_dir.join("third_party").join("libheif"); @@ -46,6 +47,15 @@ fn build_libheif() { fs::create_dir_all(&binary_dir).unwrap(); let is_windows = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows"; + let libde265 = match build_libde265(&manifest_dir, &out_dir, is_windows) { + Some(build) => build, + None => { + println!( + "cargo:warning=libde265 is unavailable, so libheif will be built without a working HEIC decoder backend." + ); + return; + } + }; // Configure let mut configure = Command::new("cmake"); @@ -55,15 +65,48 @@ fn build_libheif() { configure .arg("-DCMAKE_BUILD_TYPE=Release") .arg("-DBUILD_SHARED_LIBS=OFF") + .arg("-DBUILD_DOCUMENTATION=OFF") + .arg("-DBUILD_TESTING=OFF") + .arg("-DENABLE_PLUGIN_LOADING=OFF") + .arg("-DENABLE_EXPERIMENTAL_FEATURES=OFF") + .arg("-DENABLE_EXPERIMENTAL_MINI_FORMAT=OFF") .arg("-DWITH_EXAMPLES=OFF") .arg("-DWITH_GDK_PIXBUF=OFF") + .arg("-DWITH_UNCOMPRESSED_CODEC=OFF") + .arg("-DWITH_WEBCODECS=OFF") + .arg("-DWITH_KVAZAAR=OFF") + .arg("-DWITH_OpenJPEG_DECODER=OFF") + .arg("-DWITH_OpenJPEG_ENCODER=OFF") + .arg("-DWITH_OPENJPH_ENCODER=OFF") + .arg("-DWITH_FFMPEG_DECODER=OFF") + .arg("-DWITH_UVG266=OFF") + .arg("-DWITH_VVDEC=OFF") + .arg("-DWITH_VVENC=OFF") .arg("-DWITH_RAV1E=OFF") + .arg("-DWITH_X265=OFF") + .arg("-DWITH_X264=OFF") + .arg("-DWITH_OpenH264_DECODER=OFF") .arg("-DWITH_AOM_DECODER=OFF") .arg("-DWITH_AOM_ENCODER=OFF") - .arg("-DWITH_DAV1D=ON") + .arg("-DWITH_DAV1D=OFF") .arg("-DWITH_LIBDE265=ON") + .arg("-DWITH_JPEG_DECODER=OFF") + .arg("-DWITH_JPEG_ENCODER=OFF") + .arg("-DWITH_LIBSHARPYUV=OFF") + .arg(format!( + "-DLIBDE265_INCLUDE_DIR={}", + libde265.include_dir.display() + )) + .arg(format!( + "-DLIBDE265_LIBRARY={}", + libde265.lib_path.display() + )) .arg(source_dir.as_os_str()) .current_dir(&binary_dir); + if is_windows { + configure.arg("-DCMAKE_C_FLAGS=/DLIBDE265_STATIC_BUILD"); + configure.arg("-DCMAKE_CXX_FLAGS=/DLIBDE265_STATIC_BUILD"); + } run_command(&mut configure, "configure libheif"); @@ -72,6 +115,8 @@ fn build_libheif() { Command::new("cmake") .arg("--build") .arg(".") + .arg("--target") + .arg("heif") .arg("--config") .arg("Release") .arg("--parallel") @@ -82,18 +127,27 @@ fn build_libheif() { // Link - locate the static library output. // libheif's output name differs across platforms/build systems; keep it permissive. - let candidates: [(&str, PathBuf); 6] = [ + let candidates: [(&str, PathBuf); 14] = [ ("heif", binary_dir.join("libheif.a")), ("heif", binary_dir.join("Release").join("libheif.a")), + ("heif", binary_dir.join("libheif").join("libheif.a")), + ("heif", binary_dir.join("libheif").join("Release").join("libheif.a")), ("heif", binary_dir.join("heif.lib")), ("heif", binary_dir.join("Release").join("heif.lib")), + ("heif", binary_dir.join("Debug").join("heif.lib")), + ("heif", binary_dir.join("libheif").join("Release").join("heif.lib")), + ("heif", binary_dir.join("libheif").join("Debug").join("heif.lib")), ("libheif", binary_dir.join("libheif.lib")), ("libheif", binary_dir.join("Release").join("libheif.lib")), + ("libheif", binary_dir.join("Debug").join("libheif.lib")), + ("libheif", binary_dir.join("libheif").join("Release").join("libheif.lib")), + ("libheif", binary_dir.join("libheif").join("Debug").join("libheif.lib")), ]; let (lib_name, lib_path) = match candidates.iter().find(|(_, p)| p.exists()) { Some((name, path)) => (name.to_string(), path.clone()), None => { + log_library_search_failure(&binary_dir, "libheif"); println!( "cargo:warning=libheif build completed but static library was not found under {}", binary_dir.display() @@ -105,6 +159,8 @@ fn build_libheif() { let lib_dir = lib_path.parent().unwrap_or(&binary_dir); println!("cargo:rustc-link-search=native={}", lib_dir.display()); println!("cargo:rustc-link-lib=static={}", lib_name); + println!("cargo:rustc-link-search=native={}", libde265.lib_dir.display()); + println!("cargo:rustc-link-lib=static={}", libde265.lib_name); if !is_windows { // Some libheif builds depend on stdc++. println!("cargo:rustc-link-lib=stdc++"); @@ -243,6 +299,13 @@ struct JpegBuild { lib_name: String, } +struct LibDe265Build { + include_dir: PathBuf, + lib_dir: PathBuf, + lib_name: String, + lib_path: PathBuf, +} + fn build_libjpeg(manifest_dir: &Path, out_dir: &Path, is_windows: bool) -> Option { let source_dir = manifest_dir.join("third_party/libjpeg-turbo"); if !source_dir.exists() { @@ -309,6 +372,118 @@ fn build_libjpeg(manifest_dir: &Path, out_dir: &Path, is_windows: bool) -> Optio }) } +fn build_libde265(manifest_dir: &Path, out_dir: &Path, is_windows: bool) -> Option { + let source_dir = manifest_dir.join("third_party/libde265"); + if !source_dir.exists() { + println!( + "cargo:warning=libde265 submodule not found at {}. Run: git submodule update --init --recursive", + source_dir.display() + ); + return None; + } + + let build_root = out_dir.join("libde265-build"); + let binary_dir = build_root.join("build"); + fs::create_dir_all(&binary_dir).unwrap(); + + let release_candidates: [PathBuf; 9] = [ + binary_dir.join("libde265.a"), + binary_dir.join("Release").join("libde265.a"), + binary_dir.join("libde265").join("libde265.a"), + binary_dir.join("de265.lib"), + binary_dir.join("Release").join("de265.lib"), + binary_dir.join("libde265.lib"), + binary_dir.join("Release").join("libde265.lib"), + binary_dir.join("libde265").join("Release").join("de265.lib"), + binary_dir.join("libde265").join("Release").join("libde265.lib"), + ]; + + let candidates: [(&str, PathBuf); 13] = [ + ("de265", binary_dir.join("libde265.a")), + ("de265", binary_dir.join("Release").join("libde265.a")), + ("de265", binary_dir.join("libde265").join("libde265.a")), + ("de265", binary_dir.join("de265.lib")), + ("de265", binary_dir.join("Release").join("de265.lib")), + ("de265", binary_dir.join("Debug").join("de265.lib")), + ("de265", binary_dir.join("libde265").join("Release").join("de265.lib")), + ("de265", binary_dir.join("libde265").join("Debug").join("de265.lib")), + ("libde265", binary_dir.join("libde265.lib")), + ("libde265", binary_dir.join("Release").join("libde265.lib")), + ("libde265", binary_dir.join("Debug").join("libde265.lib")), + ("libde265", binary_dir.join("libde265").join("Release").join("libde265.lib")), + ("libde265", binary_dir.join("libde265").join("Debug").join("libde265.lib")), + ]; + + let have_existing = release_candidates.iter().any(|path| path.exists()); + if !have_existing { + let mut configure = Command::new("cmake"); + if !is_windows { + configure.arg("-G").arg("Unix Makefiles"); + } + configure + .arg("-DBUILD_SHARED_LIBS=OFF") + .arg("-DENABLE_SDL=OFF") + .arg("-DENABLE_DECODER=ON") + .arg("-DENABLE_ENCODER=OFF") + .arg("-DENABLE_SHERLOCK265=OFF") + .arg("-DENABLE_INTERNAL_DEVELOPMENT_TOOLS=OFF") + .arg("-DWITH_FUZZERS=OFF") + .arg(source_dir.as_os_str()) + .current_dir(&binary_dir); + + run_command(&mut configure, "configure libde265"); + + let jobs = env::var("NUM_JOBS").unwrap_or_else(|_| "1".to_string()); + run_command( + Command::new("cmake") + .arg("--build") + .arg(".") + .arg("--target") + .arg("de265") + .arg("--config") + .arg("Release") + .arg("--parallel") + .arg(jobs) + .current_dir(&binary_dir), + "build libde265", + ); + } + + let (lib_name, lib_path) = match candidates.iter().find(|(_, path)| path.exists()) { + Some((name, path)) => (name.to_string(), path.clone()), + None => { + log_library_search_failure(&binary_dir, "libde265"); + println!( + "cargo:warning=libde265 build completed but static library was not found under {}", + binary_dir.display() + ); + return None; + } + }; + + let include_dir = out_dir.join("libde265-include"); + let include_libde265_dir = include_dir.join("libde265"); + fs::create_dir_all(&include_libde265_dir).unwrap(); + fs::copy( + source_dir.join("libde265").join("de265.h"), + include_libde265_dir.join("de265.h"), + ) + .unwrap(); + fs::copy( + binary_dir.join("libde265").join("de265-version.h"), + include_libde265_dir.join("de265-version.h"), + ) + .unwrap(); + let lib_dir = lib_path.parent().unwrap_or(&binary_dir).to_path_buf(); + + Some(LibDe265Build { + include_dir, + lib_dir, + lib_name, + lib_path, + }) +} + /// Recursively collect all .cpp files under a directory fn collect_cpp_sources(dir: &Path) -> Vec { let mut sources = Vec::new(); @@ -332,6 +507,39 @@ fn out_dir_path() -> PathBuf { PathBuf::from(env::var("OUT_DIR").unwrap()) } +fn log_library_search_failure(binary_dir: &Path, library_name: &str) { + fn walk(dir: &Path, depth: usize, max_depth: usize) { + if depth > max_depth { + return; + } + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + walk(&path, depth + 1, max_depth); + continue; + } + if path.extension().is_some_and(|ext| { + matches!( + ext.to_string_lossy().to_ascii_lowercase().as_str(), + "a" | "lib" | "dll" | "pdb" + ) + }) { + println!("cargo:warning= found: {}", path.display()); + } + } + } + + println!( + "cargo:warning=--- searching {} artifacts under {}", + library_name, + binary_dir.display() + ); + walk(binary_dir, 0, 3); +} + fn run_command(command: &mut Command, description: &str) { let status = command .status() diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index fc3c9be5..53ff5a32 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -42,6 +42,21 @@ async fn main() { eprintln!("Unhandled panic: {}", panic_info); })); + #[cfg(lap_has_libheif)] + { + let _ = std::fs::write( + std::env::temp_dir().join("lap-has-libheif.txt"), + "lap_has_libheif enabled", + ); + } + #[cfg(not(lap_has_libheif))] + { + let _ = std::fs::write( + std::env::temp_dir().join("lap-no-libheif.txt"), + "lap_has_libheif disabled", + ); + } + let builder = tauri::Builder::default(); let builder = t_protocol::register_protocols(builder); diff --git a/src-tauri/src/t_heif.rs b/src-tauri/src/t_heif.rs index aa0ecd4c..2eec6ee9 100644 --- a/src-tauri/src/t_heif.rs +++ b/src-tauri/src/t_heif.rs @@ -1,4 +1,5 @@ use std::ffi::CString; +use std::fs; use std::os::raw::{c_char, c_int, c_void}; use std::ptr; use std::slice; @@ -30,15 +31,27 @@ struct HeifImage(c_void); const HEIF_COLORSPACE_RGB: c_int = 1; // heif_chroma_interleaved_RGB const HEIF_CHROMA_INTERLEAVED_RGB: c_int = 10; +// heif_chroma_interleaved_RGBA +const HEIF_CHROMA_INTERLEAVED_RGBA: c_int = 11; // heif_channel_interleaved const HEIF_CHANNEL_INTERLEAVED: c_int = 10; -extern "C" { +unsafe extern "C" { fn heif_context_alloc() -> *mut HeifContext; fn heif_context_free(ctx: *mut HeifContext); - fn heif_context_read_from_file(ctx: *mut HeifContext, filename: *const c_char, options: *const c_void) -> HeifError; - fn heif_context_get_primary_image_handle(ctx: *mut HeifContext, handle: *mut *mut HeifImageHandle) -> HeifError; + fn heif_context_read_from_file( + ctx: *mut HeifContext, + filename: *const c_char, + options: *const c_void, + ) -> HeifError; + fn heif_context_get_primary_image_handle( + ctx: *mut HeifContext, + handle: *mut *mut HeifImageHandle, + ) -> HeifError; fn heif_image_handle_release(handle: *mut HeifImageHandle); + fn heif_image_handle_get_width(handle: *const HeifImageHandle) -> c_int; + fn heif_image_handle_get_height(handle: *const HeifImageHandle) -> c_int; + fn heif_image_handle_has_alpha_channel(handle: *const HeifImageHandle) -> c_int; fn heif_decode_image( handle: *const HeifImageHandle, @@ -51,7 +64,11 @@ extern "C" { fn heif_image_get_width(img: *const HeifImage, channel: c_int) -> c_int; fn heif_image_get_height(img: *const HeifImage, channel: c_int) -> c_int; - fn heif_image_get_plane_readonly(img: *const HeifImage, channel: c_int, out_stride: *mut c_int) -> *const u8; + fn heif_image_get_plane_readonly( + img: *const HeifImage, + channel: c_int, + out_stride: *mut c_int, + ) -> *const u8; } fn fmt_heif_error(err: HeifError) -> String { @@ -73,6 +90,11 @@ fn fmt_heif_error(err: HeifError) -> String { } } +fn mark_heif_hit(kind: &str, file_path: &str) { + let path = std::env::temp_dir().join(format!("lap-heif-{}-hit.txt", kind)); + let _ = fs::write(path, file_path); +} + fn decode_primary_rgb(file_path: &str) -> Result<(Vec, u32, u32, u32), String> { let c_path = CString::new(file_path).map_err(|_| "Invalid file path".to_string())?; unsafe { @@ -106,8 +128,23 @@ fn decode_primary_rgb(file_path: &str) -> Result<(Vec, u32, u32, u32), Strin } let _handle_guard = HandleGuard(handle); + let handle_w = heif_image_handle_get_width(handle); + let handle_h = heif_image_handle_get_height(handle); + let has_alpha = heif_image_handle_has_alpha_channel(handle) != 0; + eprintln!("[heif] file: {}", file_path); + eprintln!( + "[heif] handle: {}x{}, has_alpha: {}", + handle_w, handle_h, has_alpha + ); + let mut img: *mut HeifImage = ptr::null_mut(); - let err = heif_decode_image(handle, &mut img, HEIF_COLORSPACE_RGB, HEIF_CHROMA_INTERLEAVED_RGB, ptr::null()); + let chroma = if has_alpha { + HEIF_CHROMA_INTERLEAVED_RGBA + } else { + HEIF_CHROMA_INTERLEAVED_RGB + }; + let err = heif_decode_image(handle, &mut img, HEIF_COLORSPACE_RGB, chroma, ptr::null()); + eprintln!("[heif] decode err.code: {}", err.code); if err.code != 0 || img.is_null() { return Err(fmt_heif_error(err)); } @@ -130,10 +167,23 @@ fn decode_primary_rgb(file_path: &str) -> Result<(Vec, u32, u32, u32), Strin if ptr_plane.is_null() || stride <= 0 { return Err("libheif returned empty plane".to_string()); } + eprintln!( + "[heif] decoded: {}x{}, stride: {}, bpp: {}", + width, + height, + stride, + if has_alpha { 4 } else { 3 } + ); + if handle_w != width as c_int || handle_h != height as c_int { + eprintln!( + "[heif] size mismatch: handle={}x{} decoded={}x{}", + handle_w, handle_h, width, height + ); + } let stride_u = stride as u32; - let row_bytes = width.saturating_mul(3); - if stride_u < row_bytes { + let decoded_row_bytes = width.saturating_mul(if has_alpha { 4 } else { 3 }); + if stride_u < decoded_row_bytes { return Err("libheif returned invalid stride".to_string()); } @@ -141,31 +191,49 @@ fn decode_primary_rgb(file_path: &str) -> Result<(Vec, u32, u32, u32), Strin let mut out = vec![0u8; (width * height * 3) as usize]; for y in 0..height { let src_off = (y * stride_u) as usize; - let dst_off = (y * row_bytes) as usize; - out[dst_off..dst_off + row_bytes as usize] - .copy_from_slice(&src[src_off..src_off + row_bytes as usize]); + let dst_off = (y * width * 3) as usize; + if has_alpha { + let src_row = &src[src_off..src_off + decoded_row_bytes as usize]; + let dst_row = &mut out[dst_off..dst_off + (width * 3) as usize]; + for (i, pixel) in src_row.chunks_exact(4).enumerate() { + dst_row[i * 3..i * 3 + 3].copy_from_slice(&pixel[0..3]); + } + } else { + out[dst_off..dst_off + (width * 3) as usize] + .copy_from_slice(&src[src_off..src_off + (width * 3) as usize]); + } } - Ok((out, width, height, row_bytes)) + Ok((out, width, height, width * 3)) } } -pub fn get_heif_thumbnail(file_path: &str, orientation: i32, thumbnail_size: u32) -> Result>, String> { +pub fn get_heif_thumbnail( + file_path: &str, + _orientation: i32, + thumbnail_size: u32, +) -> Result>, String> { + mark_heif_hit("thumbnail", file_path); let (rgb, width, height, _row_bytes) = decode_primary_rgb(file_path)?; // Build a DynamicImage to reuse existing orientation + alpha handling logic. // libheif decode gives us RGB, no alpha here. let img = image::RgbImage::from_raw(width, height, rgb) .ok_or_else(|| "Failed to build RGB image from libheif buffer".to_string())?; let dyn_img = DynamicImage::ImageRgb8(img); - // Reuse existing pipeline for consistent orientation behavior. - resize_dynamic_image_to_jpeg(dyn_img, orientation, thumbnail_size).map(Some) + // libheif already applies HEIF geometric transformations (rotation/mirroring/crop). + resize_dynamic_image_to_jpeg(dyn_img, 1, thumbnail_size).map(Some) } -pub fn get_heif_preview(file_path: &str, orientation: i32, max_size: u32) -> Result>, String> { +pub fn get_heif_preview( + file_path: &str, + _orientation: i32, + max_size: u32, +) -> Result>, String> { + mark_heif_hit("preview", file_path); let (rgb, width, height, _row_bytes) = decode_primary_rgb(file_path)?; let img = image::RgbImage::from_raw(width, height, rgb) .ok_or_else(|| "Failed to build RGB image from libheif buffer".to_string())?; // Preview path: keep it JPEG encoded at up to max_size (same as thumbnail sizing semantics). - // This uses the same resizing logic as thumbnails. - resize_dynamic_image_to_jpeg(DynamicImage::ImageRgb8(img), orientation, max_size).map(Some) + // libheif already applies HEIF geometric transformations (rotation/mirroring/crop). + resize_dynamic_image_to_jpeg(DynamicImage::ImageRgb8(img), 1, max_size).map(Some) } diff --git a/src-tauri/src/t_image.rs b/src-tauri/src/t_image.rs index 8afd7154..2f3f6da8 100644 --- a/src-tauri/src/t_image.rs +++ b/src-tauri/src/t_image.rs @@ -1075,11 +1075,22 @@ fn is_heic_path(file_path: &str) -> bool { ) } +fn is_avif_path(file_path: &str) -> bool { + matches!( + t_utils::get_file_extension(file_path) + .unwrap_or_default() + .to_lowercase() + .as_str(), + "avif" + ) +} + fn should_generate_preview_for_file(file_path: &str, file_type: i64) -> bool { file_type == 3 || crate::t_libraw::is_tiff_path(file_path) || t_jxl::is_jxl_path(file_path) || is_heic_path(file_path) + || cfg!(target_os = "linux") && is_avif_path(file_path) } async fn get_generated_preview_bytes(file_path: &str) -> Result>, String> { @@ -1124,6 +1135,11 @@ async fn get_generated_preview_bytes(file_path: &str) -> Result>, } } + #[cfg(target_os = "linux")] + if is_avif_path(file_path) { + return get_image_thumbnail(file_path, get_image_orientation(file_path), 4096); + } + Ok(None) } @@ -1493,7 +1509,7 @@ pub async fn get_file_image_bytes_cached(file_path: &str) -> Result, Str } #[cfg(all(not(target_os = "macos"), lap_has_libheif))] { - crate::t_heif::get_heif_preview(file_path, orientation, 4096)? + crate::t_heif::get_heif_preview(file_path, get_image_orientation(file_path), 4096)? .ok_or_else(|| format!("Failed to resolve HEIC preview image: {}", file_path))? } #[cfg(all(not(target_os = "macos"), not(lap_has_libheif)))] @@ -1501,6 +1517,9 @@ pub async fn get_file_image_bytes_cached(file_path: &str) -> Result, Str t_video::get_video_thumbnail(file_path, 4096, None).await? .ok_or_else(|| format!("Failed to resolve HEIC preview image: {}", file_path))? } + } else if cfg!(target_os = "linux") && is_avif_path(file_path) { + get_image_thumbnail(file_path, get_image_orientation(file_path), 4096)? + .ok_or_else(|| format!("Failed to resolve AVIF preview image: {}", file_path))? } else if crate::t_libraw::is_tiff_path(file_path) { match get_raw_preview_image(file_path) { Ok(Some(data)) => data, diff --git a/src-tauri/src/t_protocol.rs b/src-tauri/src/t_protocol.rs index 2ee62da3..05ad1897 100644 --- a/src-tauri/src/t_protocol.rs +++ b/src-tauri/src/t_protocol.rs @@ -18,14 +18,51 @@ fn detect_image_mime(data: &[u8]) -> &'static str { "image/png" } else if data.starts_with(&[0xFF, 0xD8, 0xFF]) { "image/jpeg" + } else if data.starts_with(&[0x47, 0x49, 0x46, 0x38]) { + "image/gif" } else if data.starts_with(&[0x49, 0x49, 0x2A, 0x00]) || data.starts_with(&[0x4D, 0x4D, 0x00, 0x2A]) || data.starts_with(&[0x4D, 0x4D, 0x00, 0x2B]) || data.starts_with(&[0x49, 0x49, 0x2B, 0x00]) { "image/tiff" + } else if data.starts_with(b"RIFF") && data.get(8..12) == Some(b"WEBP") { + "image/webp" } else { - "application/octet-stream" + detect_isobmff_mime(data) + } +} + +fn detect_isobmff_mime(data: &[u8]) -> &'static str { + if data.len() < 16 || &data[4..8] != b"ftyp" { + return "application/octet-stream"; + } + + let box_size = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize; + if box_size < 16 || box_size > data.len() { + return "application/octet-stream"; + } + + match &data[8..12] { + b"avif" | b"avis" => "image/avif", + b"heic" | b"heix" | b"hevc" | b"hevx" => "image/heic", + b"mif1" | b"heif" => "image/heif", + _ => { + for brand in data[16..box_size].chunks(4) { + if brand.len() < 4 { + break; + } + match brand { + b"avif" | b"avis" => return "image/avif", + b"heic" | b"heix" | b"hevc" | b"hevx" => { + return "image/heic"; + } + b"mif1" | b"heif" => return "image/heif", + _ => {} + } + } + "application/octet-stream" + } } } diff --git a/src-tauri/src/t_sqlite.rs b/src-tauri/src/t_sqlite.rs index 93281f5a..5f801512 100644 --- a/src-tauri/src/t_sqlite.rs +++ b/src-tauri/src/t_sqlite.rs @@ -2317,6 +2317,15 @@ impl AThumb { .ok() .flatten() .map(|file| { + #[cfg(target_os = "linux")] + if file + .file_path + .as_deref() + .is_some_and(|path| path.to_ascii_lowercase().ends_with(".avif")) + { + return false; + } + let width = file.width.unwrap_or(0).max(0) as u32; let height = file.height.unwrap_or(0).max(0) as u32; width > 0 && height > 0 && width <= thumbnail_size && height <= thumbnail_size diff --git a/src-tauri/src/t_video.rs b/src-tauri/src/t_video.rs index f4f16340..2a274154 100644 --- a/src-tauri/src/t_video.rs +++ b/src-tauri/src/t_video.rs @@ -417,8 +417,8 @@ pub async fn get_video_thumbnail( let seek_time = if duration > 10 { duration / 10 } else { 0 }; let ffmpeg_threads = thumbnail_ffmpeg_threads().to_string(); let filter = format!( - "scale=w={}:h={}:force_original_aspect_ratio=increase,crop={}:{}", - thumbnail_size, thumbnail_size, thumbnail_size, thumbnail_size + "scale=w={}:h={}:force_original_aspect_ratio=decrease", + thumbnail_size, thumbnail_size ); let primary_strategy = probe diff --git a/src-vite/src/common/utils.ts b/src-vite/src/common/utils.ts index b4fcf09a..3a699832 100644 --- a/src-vite/src/common/utils.ts +++ b/src-vite/src/common/utils.ts @@ -365,6 +365,9 @@ export function shouldUseBackendPreview(filePath = '', fileType = 0): boolean { if (Number(fileType) === 3) return true; const extension = getFileExtension(filePath).toLowerCase(); + if (isLinux && extension === 'avif') { + return true; + } return ['tif', 'tiff', 'jxl', 'heic', 'heif'].includes(extension); }