[ENG-603] Support HEIC images (#834)

* working HEIC thumbnails

* better error handling

* better error handling and some cleanup

* fix type of maximum size, and clippy

* better extension support, WIP quick preview, better image resolution

* remove unnecessary x86_64 macos rustflags

* add correct rustflags to setup script

* add fedora libheif deps

* debian libheif deps

* arch libheif too

* add sd-heif as a dep and feature gate it

* enable aforementioned feature in tauri only

* add URI support for heif/heic (quick preview still won't work)

* correct feature gating on everything

* dedicated sd-heif crate

---------

Co-authored-by: brxken128 <77554505+brxken128@users.noreply.github.com>
This commit is contained in:
Jamie Pine 2023-05-19 22:40:37 -08:00 committed by GitHub
parent a42bc63f5d
commit df70781af3
10 changed files with 276 additions and 36 deletions

View File

@ -1,9 +1,3 @@
[alias]
prisma = "run -p prisma-cli --bin prisma --"
prisma-sync = "run -p prisma-cli --bin sync --"
[target.x86_64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]

View File

@ -146,6 +146,8 @@ if [ "$SYSNAME" = "Linux" ]; then
# FFmpeg dependencies
DEBIAN_FFMPEG_DEPS="libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev ffmpeg"
DEBIAN_LIBHEIF_DEPS="libheif1 libheif-dev"
# Webkit2gtk requires gstreamer plugins for video playback to work
DEBIAN_VIDEO_DEPS="gstreamer1.0-libav gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly"
@ -156,7 +158,7 @@ if [ "$SYSNAME" = "Linux" ]; then
DEBIAN_LIBP2P_DEPS="protobuf-compiler"
sudo apt-get -y update
sudo apt-get -y install ${SPACEDRIVE_CUSTOM_APT_FLAGS:-} $DEBIAN_TAURI_DEPS $DEBIAN_FFMPEG_DEPS $DEBIAN_BINDGEN_DEPS $DEBIAN_LIBP2P_DEPS $DEBIAN_VIDEO_DEPS
sudo apt-get -y install ${SPACEDRIVE_CUSTOM_APT_FLAGS:-} $DEBIAN_TAURI_DEPS $DEBIAN_FFMPEG_DEPS $DEBIAN_LIBHEIF_DEPS $DEBIAN_BINDGEN_DEPS $DEBIAN_LIBP2P_DEPS $DEBIAN_VIDEO_DEPS
elif has pacman; then
echo "Detected pacman!"
echo "Installing dependencies with pacman..."
@ -170,13 +172,15 @@ if [ "$SYSNAME" = "Linux" ]; then
# FFmpeg dependencies
ARCH_FFMPEG_DEPS="ffmpeg"
ARCH_LIBHEIF_DEPS="libheif"
# Bindgen dependencies - it's used by a dependency of Spacedrive
ARCH_BINDGEN_DEPS="clang"
# Protobuf compiler - https://github.com/archlinux/svntogit-packages/blob/packages/protobuf/trunk/PKGBUILD provides `libprotoc`
ARCH_LIBP2P_DEPS="protobuf"
sudo pacman -Sy --needed $ARCH_TAURI_DEPS $ARCH_FFMPEG_DEPS $ARCH_BINDGEN_DEPS $ARCH_LIBP2P_DEPS $ARCH_VIDEO_DEPS
sudo pacman -Sy --needed $ARCH_TAURI_DEPS $ARCH_FFMPEG_DEPS $ARCH_LIBHEIF_DEPS $ARCH_BINDGEN_DEPS $ARCH_LIBP2P_DEPS $ARCH_VIDEO_DEPS
elif has dnf; then
echo "Detected dnf!"
echo "Installing dependencies with dnf..."
@ -198,6 +202,9 @@ if [ "$SYSNAME" = "Linux" ]; then
# FFmpeg dependencies
FEDORA_FFMPEG_DEPS="ffmpeg ffmpeg-devel"
# libheif dependencies
FEDORA_LIBHEIF_DEPS="libheif libheif-devel"
# Webkit2gtk requires gstreamer plugins for video playback to work
FEDORA_VIDEO_DEPS="gstreamer1-plugin-libav gstreamer1-plugins-base gstreamer1-plugins-good gstreamer1-plugins-good-extras gstreamer1-plugins-bad-free gstreamer1-plugins-bad-free-extras gstreamer1-plugins-ugly-free"
@ -219,7 +226,7 @@ if [ "$SYSNAME" = "Linux" ]; then
'https://docs.fedoraproject.org/en-US/quick-docs/setup_rpmfusion'
fi
sudo dnf install $FEDORA_TAURI_DEPS $FEDORA_BINDGEN_DEPS $FEDORA_LIBP2P_DEPS $FEDORA_VIDEO_DEPS
sudo dnf install $FEDORA_TAURI_DEPS $FEDORA_BINDGEN_DEPS $FEDORA_LIBP2P_DEPS $FEDORA_VIDEO_DEPS $FEDORA_LIBHEIF_DEPS
sudo dnf group install "C Development Tools and Libraries"
else
err "Your Linux distro '$(lsb_release -s -d)' is not supported by this script." \
@ -386,6 +393,12 @@ elif [ "$SYSNAME" = "Darwin" ]; then
PROTOC = "${_frameworks_dir}/bin/protoc"
FFMPEG_DIR = "${_frameworks_dir}"
[target.aarch64-apple-darwin]
rustflags = ["-L ${_frameworks_dir}/lib"]
[target.x86_64-apple-darwin]
rustflags = ["-L ${_frameworks_dir}/lib"]
$(cat "${_cargo_config}/config.toml")
EOF
else

114
Cargo.lock generated
View File

@ -2128,6 +2128,17 @@ dependencies = [
"syn 1.0.107",
]
[[package]]
name = "enumn"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48016319042fb7c87b78d2993084a831793a897a5cd1a2a67cab9d1eeb4b7d76"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
]
[[package]]
name = "errno"
version = "0.2.8"
@ -2200,6 +2211,15 @@ dependencies = [
"instant",
]
[[package]]
name = "fdeflate"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10"
dependencies = [
"simd-adler32",
]
[[package]]
name = "ff"
version = "0.12.1"
@ -2317,6 +2337,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "four-cc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d73a076bdabd78c2f9045dba1b90664a655fa8372581c238596e1eb3a5e1b7"
[[package]]
name = "fsevent-sys"
version = "4.1.0"
@ -2625,9 +2651,9 @@ dependencies = [
[[package]]
name = "gif"
version = "0.11.4"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06"
checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
dependencies = [
"color_quant",
"weezl",
@ -3232,9 +3258,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.24.4"
version = "0.24.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd8e4fb07cf672b1642304e731ef8a6a4c7891d67bb4fd4f5ce58cd6ed86803c"
checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a"
dependencies = [
"bytemuck",
"byteorder",
@ -3245,7 +3271,7 @@ dependencies = [
"num-rational",
"num-traits",
"png",
"scoped_threadpool",
"qoi",
"tiff",
]
@ -3487,9 +3513,9 @@ dependencies = [
[[package]]
name = "jpeg-decoder"
version = "0.2.6"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b"
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
dependencies = [
"rayon",
]
@ -3597,6 +3623,27 @@ version = "0.2.142"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
[[package]]
name = "libheif-rs"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "749fcebc2069f334599304546cfa891c30be08cdf4f358ed984a2c71c5e0031f"
dependencies = [
"enumn",
"four-cc",
"libc",
"libheif-sys",
]
[[package]]
name = "libheif-sys"
version = "1.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af8b7a4151ae10f6d2e8684f7172c43f09c0258c84190fd9704422588ceec63"
dependencies = [
"libc",
]
[[package]]
name = "libloading"
version = "0.7.3"
@ -4287,6 +4334,16 @@ dependencies = [
"adler",
]
[[package]]
name = "miniz_oxide"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
"adler",
"simd-adler32",
]
[[package]]
name = "mio"
version = "0.8.4"
@ -5454,14 +5511,15 @@ dependencies = [
[[package]]
name = "png"
version = "0.17.6"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f0e7f4c94ec26ff209cee506314212639d6c91b80afb82984819fafce9df01c"
checksum = "aaeebc51f9e7d2c150d3f3bfeb667f2aa985db5ef1e3d212847bdedb488beeaa"
dependencies = [
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide 0.5.4",
"miniz_oxide 0.7.1",
]
[[package]]
@ -5861,6 +5919,15 @@ dependencies = [
"unicase",
]
[[package]]
name = "qoi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
dependencies = [
"bytemuck",
]
[[package]]
name = "quaint"
version = "0.2.0-alpha.13"
@ -6682,12 +6749,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
[[package]]
name = "scoped_threadpool"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -6751,6 +6812,7 @@ dependencies = [
"sd-crypto",
"sd-ffmpeg",
"sd-file-ext",
"sd-heif",
"sd-p2p",
"sd-sync",
"serde",
@ -6832,6 +6894,16 @@ dependencies = [
"tokio",
]
[[package]]
name = "sd-heif"
version = "0.1.0"
dependencies = [
"image",
"libheif-rs",
"png",
"thiserror",
]
[[package]]
name = "sd-macos"
version = "0.1.0"
@ -7328,6 +7400,12 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "simd-adler32"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f"
[[package]]
name = "siphasher"
version = "0.3.10"
@ -8245,9 +8323,9 @@ dependencies = [
[[package]]
name = "tiff"
version = "0.7.3"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7259662e32d1e219321eb309d5f9d898b779769d81b76e762c07c8e5d38fcb65"
checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471"
dependencies = [
"flate2",
"jpeg-decoder",

View File

@ -9,13 +9,21 @@ repository.workspace = true
edition.workspace = true
[dependencies]
tauri = { version = "1.3.0", features = ["api-all", "linux-protocol-headers", "macos-private-api"] }
tauri = { version = "1.3.0", features = [
"api-all",
"linux-protocol-headers",
"macos-private-api",
] }
rspc = { workspace = true, features = ["tauri"] }
httpz = { workspace = true, features = [
"axum",
"tauri",
] } # TODO: The `axum` feature should be only enabled on Linux but this currently can't be done: https://github.com/rust-lang/cargo/issues/1197
sd-core = { path = "../../../core", features = ["ffmpeg", "location-watcher"] }
sd-core = { path = "../../../core", features = [
"ffmpeg",
"location-watcher",
"heif",
] }
tokio = { workspace = true, features = ["sync"] }
window-shadows = "0.2.0"
tracing = "0.1.36"

View File

@ -17,6 +17,7 @@ ffmpeg = [
] # This feature controls whether the Spacedrive Core contains functionality which requires FFmpeg.
location-watcher = ["dep:notify"]
sync-messages = []
heif = ["dep:sd-heif"]
[dependencies]
sd-ffmpeg = { path = "../crates/ffmpeg", optional = true }
@ -26,6 +27,7 @@ sd-crypto = { path = "../crates/crypto", features = [
"serde",
"keymanager",
] }
sd-heif = { path = "../crates/heif", optional = true }
sd-file-ext = { path = "../crates/file-ext" }
sd-sync = { path = "../crates/sync" }
sd-p2p = { path = "../crates/p2p", features = ["specta", "serde"] }
@ -62,7 +64,7 @@ sysinfo = "0.28.3"
thiserror = "1.0.37"
include_dir = { version = "0.7.2", features = ["glob"] }
async-trait = "^0.1.57"
image = "0.24.4"
image = "0.24.6"
webp = "0.2.2"
tracing = "0.1.36"
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
@ -86,6 +88,7 @@ normpath = { version = "1.1.1", features = ["localization"] }
strum = { version = "0.24", features = ["derive"] }
strum_macros = "0.24"
[target.'cfg(windows)'.dependencies.winapi-util]
version = "0.1.5"

View File

@ -244,10 +244,8 @@ async fn handle_file(
"3gp" => "video/3gpp",
// 3GPP2 audio/video container (TODO: audio/3gpp2 if it doesn't contain video)
"3g2" => "video/3gpp2",
// Quicktime movies
// Quicktime movies
"mov" => "video/quicktime",
// AVIF image
"avif" => "image/avif",
// Windows OS/2 Bitmap Graphics
"bmp" => "image/bmp",
// Graphics Interchange Format (GIF)
@ -266,6 +264,12 @@ async fn handle_file(
"webp" => "image/webp",
// PDF document
"pdf" => "application/pdf",
// HEIF/HEIC images
"heif" | "heifs" => "image/heif,image/heif-sequence",
"heic" | "heics" => "image/heic,image/heic-sequence",
// AVIF images
"avif" | "avci" | "avcs" => "image/avif",
_ => {
return Err(HandleCustomUriError::BadRequest(
"TODO: This filetype is not supported because of the missing mime type!",

View File

@ -27,7 +27,11 @@ use image::{self, imageops, DynamicImage, GenericImageView};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::{fs, io, task::block_in_place};
use tokio::{
fs::{self},
io::{self},
task::block_in_place,
};
use tracing::{error, info, trace, warn};
use webp::Encoder;
@ -108,14 +112,32 @@ pub struct ThumbnailerJobStep {
kind: ThumbnailerJobStepKind,
}
// TOOD(brxken128): validate avci and avcs
#[cfg(all(feature = "heif", any(target_os = "macos", target_os = "linux")))]
const HEIF_EXTENSIONS: [&str; 7] = ["heif", "heifs", "heic", "heics", "avif", "avci", "avcs"];
pub async fn generate_image_thumbnail<P: AsRef<Path>>(
file_path: P,
output_path: P,
) -> Result<(), Box<dyn Error>> {
// Webp creation has blocking code
let webp = block_in_place(|| -> Result<Vec<u8>, Box<dyn Error>> {
// Using `image` crate, open the included .jpg file
#[cfg(all(feature = "heif", any(target_os = "macos", target_os = "linux")))]
let img = {
let ext = file_path.as_ref().extension().unwrap().to_ascii_lowercase();
if HEIF_EXTENSIONS
.iter()
.any(|e| ext == std::ffi::OsStr::new(e))
{
sd_heif::heif_to_dynamic_image(file_path.as_ref())?
} else {
image::open(file_path)?
}
};
#[cfg(not(all(feature = "heif", any(target_os = "macos", target_os = "linux"))))]
let img = image::open(file_path)?;
let (w, h) = img.dimensions();
// Optionally, resize the existing photo and convert back into DynamicImage
let img = DynamicImage::ImageRgba8(imageops::resize(
@ -160,7 +182,17 @@ pub const fn can_generate_thumbnail_for_video(video_extension: &VideoExtension)
pub const fn can_generate_thumbnail_for_image(image_extension: &ImageExtension) -> bool {
use ImageExtension::*;
matches!(image_extension, Jpg | Jpeg | Png | Webp | Gif)
#[cfg(all(feature = "heif", any(target_os = "macos", target_os = "linux")))]
let res = matches!(
image_extension,
Jpg | Jpeg | Png | Webp | Gif | Heic | Heics | Heif | Heifs | Avif
);
#[cfg(not(all(feature = "heif", any(target_os = "macos", target_os = "linux"))))]
let res = matches!(image_extension, Jpg | Jpeg | Png | Webp | Gif);
res
}
fn finalize_thumbnailer(data: &ThumbnailerJobState, ctx: WorkerContext) -> JobResult {

View File

@ -76,6 +76,12 @@ extension_category_enum! {
Svg = [0x3C, 0x73, 0x76, 0x67],
Ico = [0x00, 0x00, 0x01, 0x00],
Heic = [0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63],
Heics = [0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63],
Heif = [],
Heifs = [],
Avif = [],
Avci = [],
Avcs = [],
Raw = [],
Akw = [0x41, 0x4B, 0x57, 0x42],
Dng = [0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, 0x44, 0x4E, 0x47, 0x00],

13
crates/heif/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "sd-heif"
version = "0.1.0"
authors = ["Jake Robinson <jake@spacedrive.com>"]
license.workspace = true
repository.workspace = true
edition.workspace = true
[dependencies]
libheif-rs = "0.19.2"
png = "0.17.8"
thiserror = "1.0.40"
image = "0.24.6"

89
crates/heif/src/lib.rs Normal file
View File

@ -0,0 +1,89 @@
use std::{
fs,
io::{Cursor, Read, Seek, SeekFrom},
path::Path,
};
use image::DynamicImage;
use libheif_rs::{ColorSpace, HeifContext, LibHeif, RgbChroma};
use png::{BitDepth, ColorType};
use thiserror::Error;
type HeifResult<T> = Result<T, HeifError>;
/// The maximum file size that an image can be in order to have a thumbnail generated.
///
/// This value is in MiB.
const HEIF_MAXIMUM_FILE_SIZE: u64 = 1048576 * 20;
#[derive(Error, Debug)]
pub enum HeifError {
#[error("error with libheif: {0}")]
LibHeif(#[from] libheif_rs::HeifError),
#[error("error while encoding to png: {0}")]
PngEncode(#[from] png::EncodingError),
#[error("error while loading the image (via the `image` crate): {0}")]
Image(#[from] image::ImageError),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("the image provided is unsupported")]
Unsupported,
#[error("the image provided is too large (over 20MiB)")]
TooLarge,
#[error("the provided bit depth is invalid")]
InvalidBitDepth,
#[error("invalid path provided (non UTF-8)")]
InvalidPath,
}
pub fn heif_to_dynamic_image(path: &Path) -> HeifResult<DynamicImage> {
if fs::metadata(path)?.len() > HEIF_MAXIMUM_FILE_SIZE {
return Err(HeifError::TooLarge);
}
let img = {
// do this in a separate block so we drop the raw (potentially huge) image handle
let ctx = HeifContext::read_from_file(path.to_str().ok_or(HeifError::InvalidPath)?)?;
let heif = LibHeif::new();
let handle = ctx.primary_image_handle()?;
heif.decode(&handle, ColorSpace::Rgb(RgbChroma::Rgb), None)?
};
// TODO(brxken128): add support for images with individual r/g/b channels
// i'm unable to find a sample to test with, but it should follow the same principles as this one
if let Some(i) = img.planes().interleaved {
let data = i.data.to_vec();
let mut reader = Cursor::new(data);
let mut sequence = vec![];
let mut buffer = [0u8; 3]; // [r, g, b]
// this is the interpolation stuff, it essentially just makes the image correct
// in regards to stretching/resolution, etc
for y in 0..img.height() {
reader.seek(SeekFrom::Start((i.stride * y as usize) as u64))?;
for _ in 0..img.width() {
reader.read_exact(&mut buffer)?;
sequence.extend_from_slice(&buffer);
}
}
let mut writer = Cursor::new(vec![]);
let mut png_encoder = png::Encoder::new(&mut writer, i.width, i.height);
png_encoder.set_color(ColorType::Rgb);
png_encoder
.set_depth(BitDepth::from_u8(i.bits_per_pixel).ok_or(HeifError::InvalidBitDepth)?);
let mut png_writer = png_encoder.write_header()?;
png_writer.write_image_data(&sequence)?;
png_writer.finish()?;
image::load_from_memory_with_format(&writer.into_inner(), image::ImageFormat::Png)
.map_err(HeifError::Image)
} else {
Err(HeifError::Unsupported)
}
}