mirror of
https://github.com/julyx10/lap.git
synced 2026-10-02 01:54:57 +08:00
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 commitf541ed457e. * Revert "fix: decode Linux AVIF previews without video fallback" This reverts commitd8805a36e6. * fix: preserve aspect ratio for FFmpeg-extracted thumbnails (fixes Linux AVIF cropping)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
+210
-2
@@ -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<JpegBuild> {
|
||||
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<LibDe265Build> {
|
||||
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<PathBuf> {
|
||||
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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+85
-17
@@ -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<u8>, 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<u8>, 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<u8>, 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<u8>, 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<Option<Vec<u8>>, String> {
|
||||
pub fn get_heif_thumbnail(
|
||||
file_path: &str,
|
||||
_orientation: i32,
|
||||
thumbnail_size: u32,
|
||||
) -> Result<Option<Vec<u8>>, 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<Option<Vec<u8>>, String> {
|
||||
pub fn get_heif_preview(
|
||||
file_path: &str,
|
||||
_orientation: i32,
|
||||
max_size: u32,
|
||||
) -> Result<Option<Vec<u8>>, 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)
|
||||
}
|
||||
|
||||
@@ -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<Option<Vec<u8>>, String> {
|
||||
@@ -1124,6 +1135,11 @@ async fn get_generated_preview_bytes(file_path: &str) -> Result<Option<Vec<u8>>,
|
||||
}
|
||||
}
|
||||
|
||||
#[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<Vec<u8>, 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<Vec<u8>, 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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user