diff --git a/Cargo.lock b/Cargo.lock index 765f77224..b1b6cf45b 100644 Binary files a/Cargo.lock and b/Cargo.lock differ diff --git a/core/src/job/manager.rs b/core/src/job/manager.rs index 60ce026d4..f26f5ff59 100644 --- a/core/src/job/manager.rs +++ b/core/src/job/manager.rs @@ -337,11 +337,10 @@ impl Jobs { .read() .await .values() - .filter_map(|worker| { - (!worker.is_paused()).then(|| { - let report = worker.report(); - (report.get_meta().0, report) - }) + .filter(|&worker| !worker.is_paused()) + .map(|worker| { + let report = worker.report(); + (report.get_meta().0, report) }) .collect() } diff --git a/core/src/library/library.rs b/core/src/library/library.rs index fdd1d108f..0a3c6209b 100644 --- a/core/src/library/library.rs +++ b/core/src/library/library.rs @@ -192,7 +192,7 @@ impl Library { }, expires .map(|e| vec![notification::expires_at::set(Some(e.fixed_offset()))]) - .unwrap_or_else(Vec::new), + .unwrap_or_default(), ) .exec() .await diff --git a/core/src/location/indexer/rules/mod.rs b/core/src/location/indexer/rules/mod.rs index 449c57a9b..c50131683 100644 --- a/core/src/location/indexer/rules/mod.rs +++ b/core/src/location/indexer/rules/mod.rs @@ -479,9 +479,9 @@ impl IndexerRule { .await .map(|results| { results.into_iter().flatten().fold( - HashMap::with_capacity(RuleKind::variant_count()), + HashMap::<_, Vec<_>>::with_capacity(RuleKind::variant_count()), |mut map, (kind, result)| { - map.entry(kind).or_insert_with(Vec::new).push(result); + map.entry(kind).or_default().push(result); map }, ) diff --git a/core/src/location/mod.rs b/core/src/location/mod.rs index dabfb6535..ec5df2066 100644 --- a/core/src/location/mod.rs +++ b/core/src/location/mod.rs @@ -9,8 +9,8 @@ use crate::{ media::{ media_processor, thumbnail::{ - can_generate_thumbnail_for_image, generate_image_thumbnail, get_thumb_key, - get_thumbnail_path, + can_generate_thumbnail_for_document, can_generate_thumbnail_for_image, + generate_image_thumbnail, get_thumb_key, get_thumbnail_path, }, MediaProcessorJobInit, }, @@ -27,7 +27,7 @@ use std::{ sync::Arc, }; -use sd_file_ext::extensions::ImageExtension; +use sd_file_ext::extensions::{DocumentExtension, ImageExtension}; use chrono::Utc; use futures::future::TryFutureExt; @@ -928,6 +928,12 @@ pub(super) async fn generate_thumbnail( error!("Failed to image thumbnail on location manager: {e:#?}"); } } + } else if let Ok(extension) = DocumentExtension::from_str(extension) { + if can_generate_thumbnail_for_document(&extension) { + if let Err(e) = generate_image_thumbnail(path, &output_path).await { + error!("Failed to document thumbnail on location manager: {e:#?}"); + } + } } #[cfg(feature = "ffmpeg")] diff --git a/core/src/location/non_indexed.rs b/core/src/location/non_indexed.rs index 73f403148..05f77b943 100644 --- a/core/src/location/non_indexed.rs +++ b/core/src/location/non_indexed.rs @@ -157,7 +157,10 @@ pub async fn walk( .map(Into::into) .unwrap_or(ObjectKind::Unknown); - let thumbnail_key = if matches!(kind, ObjectKind::Image | ObjectKind::Video) { + let thumbnail_key = if matches!( + kind, + ObjectKind::Image | ObjectKind::Video | ObjectKind::Document + ) { if let Ok(cas_id) = generate_cas_id(&entry_path, metadata.len()) .await .map_err(|e| errors.push(NonIndexedLocationError::from((path, e)).into())) diff --git a/core/src/object/media/media_processor/job.rs b/core/src/object/media/media_processor/job.rs index fb8c3296a..51f53f6b9 100644 --- a/core/src/object/media/media_processor/job.rs +++ b/core/src/object/media/media_processor/job.rs @@ -229,7 +229,7 @@ async fn get_files_for_thumbnailer( let image_thumb_files = get_all_children_files_by_extensions( db, parent_iso_file_path, - &thumbnail::FILTERED_IMAGE_EXTENSIONS, + &thumbnail::THUMBNAILABLE_EXTENSIONS, ) .await? .into_iter() @@ -241,7 +241,7 @@ async fn get_files_for_thumbnailer( let video_files = get_all_children_files_by_extensions( db, parent_iso_file_path, - &thumbnail::FILTERED_VIDEO_EXTENSIONS, + &thumbnail::THUMBNAILABLE_VIDEO_EXTENSIONS, ) .await?; diff --git a/core/src/object/media/media_processor/shallow.rs b/core/src/object/media/media_processor/shallow.rs index 0085093ab..8463f56c8 100644 --- a/core/src/object/media/media_processor/shallow.rs +++ b/core/src/object/media/media_processor/shallow.rs @@ -156,7 +156,7 @@ async fn get_files_for_thumbnailer( let image_thumb_files = get_files_by_extensions( db, parent_iso_file_path, - &thumbnail::FILTERED_IMAGE_EXTENSIONS, + &thumbnail::THUMBNAILABLE_EXTENSIONS, ) .await? .into_iter() @@ -168,7 +168,7 @@ async fn get_files_for_thumbnailer( let video_files = get_files_by_extensions( db, parent_iso_file_path, - &thumbnail::FILTERED_VIDEO_EXTENSIONS, + &thumbnail::THUMBNAILABLE_VIDEO_EXTENSIONS, ) .await?; diff --git a/core/src/object/media/thumbnail/mod.rs b/core/src/object/media/thumbnail/mod.rs index 11073a9bd..6d88a8168 100644 --- a/core/src/object/media/thumbnail/mod.rs +++ b/core/src/object/media/thumbnail/mod.rs @@ -8,7 +8,9 @@ use crate::{ Node, }; -use sd_file_ext::extensions::{Extension, ImageExtension, ALL_IMAGE_EXTENSIONS}; +use sd_file_ext::extensions::{ + DocumentExtension, Extension, ImageExtension, ALL_DOCUMENT_EXTENSIONS, ALL_IMAGE_EXTENSIONS, +}; use sd_images::format_image; use sd_media_metadata::image::Orientation; @@ -26,7 +28,7 @@ use image::{self, imageops, DynamicImage, GenericImageView}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use thiserror::Error; -use tokio::{fs, io}; +use tokio::{fs, io, task}; use tracing::{error, trace, warn}; use webp::Encoder; @@ -57,7 +59,7 @@ pub fn get_thumb_key(cas_id: &str) -> Vec { } #[cfg(feature = "ffmpeg")] -pub(super) static FILTERED_VIDEO_EXTENSIONS: Lazy> = Lazy::new(|| { +pub(super) static THUMBNAILABLE_VIDEO_EXTENSIONS: Lazy> = Lazy::new(|| { ALL_VIDEO_EXTENSIONS .iter() .cloned() @@ -66,12 +68,19 @@ pub(super) static FILTERED_VIDEO_EXTENSIONS: Lazy> = Lazy::new(|| .collect() }); -pub(super) static FILTERED_IMAGE_EXTENSIONS: Lazy> = Lazy::new(|| { +pub(super) static THUMBNAILABLE_EXTENSIONS: Lazy> = Lazy::new(|| { ALL_IMAGE_EXTENSIONS .iter() .cloned() .filter(can_generate_thumbnail_for_image) .map(Extension::Image) + .chain( + ALL_DOCUMENT_EXTENSIONS + .iter() + .cloned() + .filter(can_generate_thumbnail_for_document) + .map(Extension::Document), + ) .collect() }); @@ -88,6 +97,8 @@ pub enum ThumbnailerError { Encoding, #[error("error while converting the image: {0}")] SdImages(#[from] sd_images::Error), + #[error("failed to execute converting task: {0}")] + Task(#[from] task::JoinError), } /// This is the target pixel count for all thumbnails to be resized to, and it is eventually downscaled @@ -125,7 +136,7 @@ pub async fn generate_image_thumbnail>( ) -> Result<(), ThumbnailerError> { let file_path = file_path.as_ref().to_path_buf(); - let webp = tokio::task::block_in_place(move || -> Result<_, ThumbnailerError> { + let webp = task::spawn_blocking(move || -> Result<_, ThumbnailerError> { let img = format_image(&file_path).map_err(|_| ThumbnailerError::Encoding)?; let (w, h) = img.dimensions(); @@ -154,7 +165,8 @@ pub async fn generate_image_thumbnail>( // this make us `deref` to have a `&[u8]` and then `to_owned` to make a Vec // which implies on a unwanted clone... Ok(encoder.encode(TARGET_QUALITY).deref().to_owned()) - })?; + }) + .await??; let output_path = output_path.as_ref(); @@ -200,6 +212,12 @@ pub const fn can_generate_thumbnail_for_image(image_extension: &ImageExtension) ) } +pub const fn can_generate_thumbnail_for_document(document_extension: &DocumentExtension) -> bool { + use DocumentExtension::*; + + matches!(document_extension, Pdf) +} + pub(super) async fn process( entries: impl IntoIterator, location_id: location::id::Type, diff --git a/crates/crypto/src/crypto/stream.rs b/crates/crypto/src/crypto/stream.rs index ccfb58417..5bb3b7b56 100644 --- a/crates/crypto/src/crypto/stream.rs +++ b/crates/crypto/src/crypto/stream.rs @@ -133,7 +133,7 @@ macro_rules! impl_stream { s .$streams_fn(bytes, &mut writer, aad) .await - .map_or_else(Err, |_| Ok(writer.into_inner().into())) + .map_or_else(Err, |()| Ok(writer.into_inner().into())) } } diff --git a/crates/crypto/src/keys/hashing.rs b/crates/crypto/src/keys/hashing.rs index 790605c4b..a4f909dae 100644 --- a/crates/crypto/src/keys/hashing.rs +++ b/crates/crypto/src/keys/hashing.rs @@ -88,7 +88,7 @@ impl PasswordHasher { argon2 .hash_password_into(password.expose(), &salt, &mut key) - .map_or(Err(Error::PasswordHash), |_| Ok(Key::new(key))) + .map_or(Err(Error::PasswordHash), |()| Ok(Key::new(key))) } #[allow(clippy::needless_pass_by_value)] @@ -110,7 +110,7 @@ impl PasswordHasher { balloon .hash_into(password.expose(), &salt, &mut key) - .map_or(Err(Error::PasswordHash), |_| Ok(Key::new(key))) + .map_or(Err(Error::PasswordHash), |()| Ok(Key::new(key))) } } diff --git a/crates/file-ext/src/extensions.rs b/crates/file-ext/src/extensions.rs index b5377e0d9..6a7d9f764 100644 --- a/crates/file-ext/src/extensions.rs +++ b/crates/file-ext/src/extensions.rs @@ -156,7 +156,7 @@ extension_category_enum! { // document extensions extension_category_enum! { - DocumentExtension _ALL_DOCUMENT_EXTENSIONS { + DocumentExtension ALL_DOCUMENT_EXTENSIONS { Pdf = [0x25, 0x50, 0x44, 0x46, 0x2D], Key = [0x50, 0x4B, 0x03, 0x04], Pages = [0x50, 0x4B, 0x03, 0x04], diff --git a/crates/images/Cargo.toml b/crates/images/Cargo.toml index 68d38f066..049452df5 100644 --- a/crates/images/Cargo.toml +++ b/crates/images/Cargo.toml @@ -15,8 +15,11 @@ heif = ["dep:libheif-rs", "dep:libheif-sys"] [dependencies] image = "0.24.7" thiserror = "1.0.48" +once_cell = "1.18.0" +tracing = { workspace = true } resvg = "0.35.0" # both of these added *default* bindgen features in 0.22.0 and 2.0.0+1.16.2 respectively # this broke builds as we build our own liibheif, so i disabled their default features libheif-rs = { version = "0.22.0", default-features = false, optional = true } libheif-sys = { version = "2.0.0", default-features = false, optional = true } +pdfium-render = { version ="0.8.8", features = ["sync", "image", "thread_safe"] } diff --git a/crates/images/src/consts.rs b/crates/images/src/consts.rs index 24fdc7eb4..6a8e094d5 100644 --- a/crates/images/src/consts.rs +++ b/crates/images/src/consts.rs @@ -17,11 +17,14 @@ pub const SVG_EXTENSIONS: [&str; 2] = ["svg", "svgz"]; /// This value is in MiB. pub const SVG_MAXIMUM_FILE_SIZE: u64 = MIB * 24; -/// The size that SVG images are rendered at, assuming they are square. -// TODO(brxken128): check for non-1:1 SVG images and create a function to resize -// them while maintaining the aspect ratio. +/// The size that SVG images are rendered at. pub const SVG_RENDER_SIZE: u32 = 512; +pub const PDF_EXTENSION: &str = "pdf"; + +/// The size that PDF pages are rendered at. +pub const PDF_RENDER_SIZE: i32 = 1024; + /// The maximum file size that an image can be in order to have a thumbnail generated. /// /// This value is in MiB. diff --git a/crates/images/src/error.rs b/crates/images/src/error.rs index e95a6dbdc..48629657c 100644 --- a/crates/images/src/error.rs +++ b/crates/images/src/error.rs @@ -4,10 +4,13 @@ pub type Result = std::result::Result; #[derive(thiserror::Error, Debug)] pub enum Error { + #[error("error with pdfium: {0}")] + Pdfium(#[from] pdfium_render::prelude::PdfiumError), + #[error("failed to load pdfium library")] + PdfiumBinding, #[cfg(feature = "heif")] #[error("error with libheif: {0}")] LibHeif(#[from] libheif_rs::HeifError), - #[error("error with usvg: {0}")] USvg(#[from] resvg::usvg::Error), #[error("failed to allocate `Pixbuf` while converting an SVG")] diff --git a/crates/images/src/formatter.rs b/crates/images/src/formatter.rs index 04516f8ed..ac439664c 100644 --- a/crates/images/src/formatter.rs +++ b/crates/images/src/formatter.rs @@ -2,6 +2,7 @@ use crate::{ consts, error::{Error, Result}, generic::GenericHandler, + pdf::PdfHandler, svg::SvgHandler, ImageHandler, }; @@ -43,5 +44,9 @@ fn match_to_handler(ext: &OsStr) -> Box { handler = Box::new(SvgHandler {}); } + if ext == consts::PDF_EXTENSION { + handler = Box::new(PdfHandler {}); + } + handler } diff --git a/crates/images/src/heif.rs b/crates/images/src/heif.rs index d7a817b80..d48ca63ac 100644 --- a/crates/images/src/heif.rs +++ b/crates/images/src/heif.rs @@ -4,10 +4,13 @@ pub use crate::error::{Error, Result}; use crate::ImageHandler; use image::DynamicImage; use libheif_rs::{ColorSpace, HeifContext, LibHeif, RgbChroma}; +use once_cell::sync::Lazy; use std::io::{Cursor, SeekFrom}; use std::io::{Read, Seek}; use std::path::Path; +static HEIF: Lazy = Lazy::new(LibHeif::new); + pub struct HeifHandler {} impl ImageHandler for HeifHandler { @@ -29,7 +32,7 @@ impl ImageHandler for HeifHandler { let img = { let data = self.get_data(path)?; let handle = HeifContext::read_from_bytes(&data)?.primary_image_handle()?; - LibHeif::new().decode(&handle, ColorSpace::Rgb(RgbChroma::Rgb), None) + HEIF.decode(&handle, ColorSpace::Rgb(RgbChroma::Rgb), None) }?; let planes = img.planes(); diff --git a/crates/images/src/lib.rs b/crates/images/src/lib.rs index 60149c0c7..2cb06268f 100644 --- a/crates/images/src/lib.rs +++ b/crates/images/src/lib.rs @@ -26,6 +26,7 @@ mod formatter; mod generic; #[cfg(feature = "heif")] mod heif; +mod pdf; mod svg; pub use error::{Error, Result}; diff --git a/crates/images/src/pdf.rs b/crates/images/src/pdf.rs new file mode 100644 index 000000000..0586ab408 --- /dev/null +++ b/crates/images/src/pdf.rs @@ -0,0 +1,88 @@ +use std::{ + borrow::ToOwned, + env::current_exe, + path::{Path, PathBuf}, +}; + +use crate::{consts::PDF_RENDER_SIZE, Error::PdfiumBinding, ImageHandler, Result}; +use image::DynamicImage; +use once_cell::sync::Lazy; +use pdfium_render::prelude::{PdfPageRenderRotation, PdfRenderConfig, Pdfium}; +use tracing::error; + +// This path must be relative to the running binary +#[cfg(windows)] +const BINDING_LOCATION: &str = "."; +#[cfg(unix)] +const BINDING_LOCATION: &str = if cfg!(target_os = "macos") { + "../Frameworks/FFMpeg.framework/Libraries" +} else { + "../lib/spacedrive" +}; + +static PDFIUM: Lazy> = Lazy::new(|| { + let lib_name = Pdfium::pdfium_platform_library_name(); + let lib_path = current_exe() + .ok() + .and_then(|exe_path| { + exe_path.parent().and_then(|parent_path| { + match parent_path + .join(BINDING_LOCATION) + .join(&lib_name) + .canonicalize() + { + Ok(lib_path) => lib_path.to_str().map(ToOwned::to_owned), + Err(err) => { + error!("{err:#?}"); + None + } + } + }) + }) + .unwrap_or_else(|| { + #[allow(clippy::expect_used)] + PathBuf::from(BINDING_LOCATION) + .join(&lib_name) + .to_str() + .expect("We are converting valid strs to PathBuf then back, it should not fail") + .to_owned() + }); + + Pdfium::bind_to_library(lib_path) + .or_else(|err| { + error!("{err:#?}"); + Pdfium::bind_to_system_library() + }) + .map(Pdfium::new) + .map_err(|err| error!("{err:#?}")) + .ok() +}); + +pub struct PdfHandler {} + +impl ImageHandler for PdfHandler { + fn maximum_size(&self) -> u64 { + // Pdfium will only load the portions of the document it actually needs into memory. + u64::MAX + } + + fn validate_image(&self, _bits_per_pixel: u8, _length: usize) -> Result<()> { + Ok(()) + } + + fn handle_image(&self, path: &Path) -> Result { + let pdfium = PDFIUM.as_ref().ok_or(PdfiumBinding)?; + + let render_config = PdfRenderConfig::new() + .set_target_width(PDF_RENDER_SIZE) + .set_maximum_height(PDF_RENDER_SIZE) + .rotate_if_landscape(PdfPageRenderRotation::Degrees90, true); + + Ok(pdfium + .load_pdf_from_file(path, None)? + .pages() + .first()? + .render_with_config(&render_config)? + .as_image()) + } +} diff --git a/interface/app/$libraryId/Explorer/Inspector/index.tsx b/interface/app/$libraryId/Explorer/Inspector/index.tsx index b5194f8d6..bb269269d 100644 --- a/interface/app/$libraryId/Explorer/Inspector/index.tsx +++ b/interface/app/$libraryId/Explorer/Inspector/index.tsx @@ -31,6 +31,7 @@ import { FilePath, FilePathWithObject, getExplorerItemData, + getItemFilePath, NonIndexedPathItem, Object, ObjectKindEnum, @@ -136,7 +137,7 @@ const Thumbnails = ({ items }: { items: ExplorerItem[] }) => {