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 f541ed457e.

* Revert "fix: decode Linux AVIF previews without video fallback"

This reverts commit d8805a36e6.

* fix: preserve aspect ratio for FFmpeg-extracted thumbnails (fixes Linux AVIF cropping)
This commit is contained in:
julyX10
2026-04-24 13:25:36 +08:00
committed by GitHub
parent 993fbd4283
commit b40ae4490a
10 changed files with 386 additions and 23 deletions
+3
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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()
+15
View File
@@ -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
View File
@@ -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)
}
+20 -1
View File
@@ -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,
+38 -1
View File
@@ -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"
}
}
}
+9
View File
@@ -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
+2 -2
View File
@@ -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
+3
View File
@@ -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);
}