From ca8f046142ad8d0db6f2680f2794861a61b90933 Mon Sep 17 00:00:00 2001 From: Antoine Gersant Date: Wed, 23 Sep 2020 22:20:27 -0700 Subject: [PATCH] Thumbnails code cleanup --- src/main.rs | 6 ++ src/service/rocket/api.rs | 14 ++- src/service/rocket/server.rs | 5 + src/service/rocket/test.rs | 8 ++ src/service/test.rs | 21 +++++ src/thumbnails.rs | 178 ++++++++++++++++++++++------------- 6 files changed, 164 insertions(+), 68 deletions(-) diff --git a/src/main.rs b/src/main.rs index c68dad5..558f2f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -202,6 +202,11 @@ fn main() -> Result<()> { let swagger_url = format!("/{}swagger", &prefix_url); info!("Mounting swagger files on {}", swagger_url); + // Thumbnails manager + let mut thumbnails_path = utils::get_data_root()?; + thumbnails_path.push("thumbnails"); + let thumbnails_manager = thumbnails::ThumbnailsManager::new(thumbnails_path.as_path()); + // Start server info!("Starting up server"); let port: u16 = matches @@ -221,6 +226,7 @@ fn main() -> Result<()> { swagger_dir_path, db_server, index, + thumbnails_manager, ); }); diff --git a/src/service/rocket/api.rs b/src/service/rocket/api.rs index 47c9d8d..64f616c 100644 --- a/src/service/rocket/api.rs +++ b/src/service/rocket/api.rs @@ -22,7 +22,7 @@ use crate::playlist; use crate::service::constants::*; use crate::service::dto; use crate::service::error::APIError; -use crate::thumbnails; +use crate::thumbnails::{ThumbnailOptions, ThumbnailsManager}; use crate::user; use crate::vfs::VFSSource; @@ -329,12 +329,18 @@ fn audio(db: State<'_, DB>, _auth: Auth, path: VFSPathBuf) -> Result?")] -fn thumbnail(db: State<'_, DB>, _auth: Auth, path: VFSPathBuf, pad: Option) -> Result { +fn thumbnail( + db: State<'_, DB>, + thumbnails_manager: State<'_, ThumbnailsManager>, + _auth: Auth, + path: VFSPathBuf, + pad: Option, +) -> Result { let vfs = db.get_vfs()?; let image_path = vfs.virtual_to_real(&path.into() as &PathBuf)?; - let mut options = thumbnails::Options::default(); + let mut options = ThumbnailOptions::default(); options.pad_to_square = pad.unwrap_or(options.pad_to_square); - let thumbnail_path = thumbnails::get_thumbnail(&image_path, &options)?; + let thumbnail_path = thumbnails_manager.get_thumbnail(&image_path, &options)?; let file = File::open(thumbnail_path)?; Ok(file) } diff --git a/src/service/rocket/server.rs b/src/service/rocket/server.rs index 527e3d4..594566f 100644 --- a/src/service/rocket/server.rs +++ b/src/service/rocket/server.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; use super::api; use crate::db::DB; use crate::index::Index; +use crate::thumbnails::ThumbnailsManager; pub fn get_server( port: u16, @@ -18,6 +19,7 @@ pub fn get_server( swagger_dir_path: &PathBuf, db: DB, command_sender: Index, + thumbnails_manager: ThumbnailsManager, ) -> Result { let mut config = rocket::Config::build(Environment::Production) .log_level(LoggingLevel::Normal) @@ -35,6 +37,7 @@ pub fn get_server( Ok(rocket::custom(config) .manage(db) .manage(command_sender) + .manage(thumbnails_manager) .mount(&api_url, api::get_routes()) .mount( &swagger_url, @@ -56,6 +59,7 @@ pub fn run( swagger_dir_path: PathBuf, db: DB, command_sender: Index, + thumbnails_manager: ThumbnailsManager, ) -> Result<()> { let server = get_server( port, @@ -67,6 +71,7 @@ pub fn run( &swagger_dir_path, db, command_sender, + thumbnails_manager, )?; server.launch(); Ok(()) diff --git a/src/service/rocket/test.rs b/src/service/rocket/test.rs index 6895c9f..9dd452b 100644 --- a/src/service/rocket/test.rs +++ b/src/service/rocket/test.rs @@ -12,6 +12,7 @@ use super::server; use crate::db::DB; use crate::index; use crate::service::test::TestService; +use crate::thumbnails::ThumbnailsManager; pub struct RocketResponse<'r, 's> { response: &'s mut rocket::Response<'r>, @@ -70,6 +71,12 @@ impl TestService for RocketTestService { swagger_dir_path.push("swagger"); let index = index::builder(db.clone()).periodic_updates(false).build(); + let mut thumbnails_path = PathBuf::new(); + thumbnails_path.push("test-output"); + thumbnails_path.push("thumbnails"); + thumbnails_path.push(db_name); + let thumbnails_manager = ThumbnailsManager::new(thumbnails_path.as_path()); + let auth_secret: [u8; 32] = [0; 32]; let server = server::get_server( @@ -82,6 +89,7 @@ impl TestService for RocketTestService { &swagger_dir_path, db, index, + thumbnails_manager, ) .unwrap(); let client = Client::new(server).unwrap(); diff --git a/src/service/test.rs b/src/service/test.rs index 65e8a15..d01370e 100644 --- a/src/service/test.rs +++ b/src/service/test.rs @@ -434,3 +434,24 @@ fn test_service_playlists() { let playlists = response.body(); assert_eq!(playlists.len(), 0); } + +#[test] +fn test_service_thumbnail() { + let mut service = ServiceType::new(&format!("{}{}", TEST_DB_PREFIX, line!())); + service.complete_initial_setup(); + service.login(); + service.index(); + + let mut path = PathBuf::new(); + path.push("collection"); + path.push("Khemmis"); + path.push("Hunted"); + path.push("Folder.jpg"); + let uri = format!( + "/api/thumbnail/{}", + percent_encode(path.to_string_lossy().as_ref().as_bytes(), NON_ALPHANUMERIC) + ); + + let response = service.get(&uri); + assert_eq!(response.status(), StatusCode::OK); +} diff --git a/src/thumbnails.rs b/src/thumbnails.rs index bb95ad3..612aff6 100644 --- a/src/thumbnails.rs +++ b/src/thumbnails.rs @@ -8,81 +8,131 @@ use std::fs::{DirBuilder, File}; use std::hash::{Hash, Hasher}; use std::path::*; -use crate::utils; +pub struct ThumbnailsManager { + thumbnails_path: PathBuf, +} -const THUMBNAILS_PATH: &str = "thumbnails"; +impl ThumbnailsManager { + pub fn new(thumbnails_path: &Path) -> ThumbnailsManager { + ThumbnailsManager { + thumbnails_path: thumbnails_path.to_owned(), + } + } + + pub fn get_thumbnail( + &self, + image_path: &Path, + thumbnailoptions: &ThumbnailOptions, + ) -> Result { + match self.retrieve_thumbnail(image_path, thumbnailoptions) { + Some(path) => Ok(path), + None => self.create_thumbnail(image_path, thumbnailoptions), + } + } + + fn create_thumbnails_directory(&self) -> Result<()> { + let mut dir_builder = DirBuilder::new(); + dir_builder.recursive(true); + dir_builder.create(self.thumbnails_path.as_path())?; + Ok(()) + } + + fn get_thumbnail_path( + &self, + image_path: &Path, + thumbnailoptions: &ThumbnailOptions, + ) -> PathBuf { + let hash = hash(image_path, thumbnailoptions); + let mut thumbnail_path = self.thumbnails_path.clone(); + thumbnail_path.push(format!("{}.jpg", hash.to_string())); + thumbnail_path + } + + fn retrieve_thumbnail( + &self, + image_path: &Path, + thumbnailoptions: &ThumbnailOptions, + ) -> Option { + let path = self.get_thumbnail_path(image_path, thumbnailoptions); + if path.exists() { + Some(path) + } else { + None + } + } + + fn create_thumbnail( + &self, + image_path: &Path, + thumbnailoptions: &ThumbnailOptions, + ) -> Result { + let thumbnail = generate_thumbnail(image_path, thumbnailoptions)?; + let quality = 80; + + self.create_thumbnails_directory()?; + let path = self.get_thumbnail_path(image_path, thumbnailoptions); + let mut out_file = File::create(&path)?; + thumbnail.write_to(&mut out_file, ImageOutputFormat::Jpeg(quality))?; + Ok(path) + } +} + +fn hash(path: &Path, thumbnailoptions: &ThumbnailOptions) -> u64 { + let mut hasher = DefaultHasher::new(); + path.hash(&mut hasher); + thumbnailoptions.hash(&mut hasher); + hasher.finish() +} + +fn generate_thumbnail( + image_path: &Path, + thumbnailoptions: &ThumbnailOptions, +) -> Result { + let source_image = image::open(image_path)?; + let (source_width, source_height) = source_image.dimensions(); + let largest_dimension = cmp::max(source_width, source_height); + let out_dimension = cmp::min(thumbnailoptions.max_dimension, largest_dimension); + + let source_aspect_ratio: f32 = source_width as f32 / source_height as f32; + let is_almost_square = source_aspect_ratio > 0.8 && source_aspect_ratio < 1.2; + + let mut final_image; + if is_almost_square && thumbnailoptions.resize_if_almost_square { + final_image = source_image.resize_exact(out_dimension, out_dimension, FilterType::Lanczos3); + } else if thumbnailoptions.pad_to_square { + let scaled_image = source_image.resize(out_dimension, out_dimension, FilterType::Lanczos3); + let (scaled_width, scaled_height) = scaled_image.dimensions(); + let background = image::Rgb([255, 255 as u8, 255 as u8]); + final_image = DynamicImage::ImageRgb8(ImageBuffer::from_pixel( + out_dimension, + out_dimension, + background, + )); + final_image.copy_from( + &scaled_image, + (out_dimension - scaled_width) / 2, + (out_dimension - scaled_height) / 2, + )?; + } else { + final_image = source_image.resize(out_dimension, out_dimension, FilterType::Lanczos3); + } + + Ok(final_image) +} #[derive(Debug, Hash)] -pub struct Options { +pub struct ThumbnailOptions { pub max_dimension: u32, pub resize_if_almost_square: bool, pub pad_to_square: bool, } -impl Default for Options { - fn default() -> Options { - Options { +impl Default for ThumbnailOptions { + fn default() -> ThumbnailOptions { + ThumbnailOptions { max_dimension: 400, resize_if_almost_square: true, pad_to_square: true, } } } - -fn hash(path: &Path, options: &Options) -> u64 { - let mut hasher = DefaultHasher::new(); - path.hash(&mut hasher); - options.hash(&mut hasher); - hasher.finish() -} - -pub fn get_thumbnail(real_path: &Path, options: &Options) -> Result { - let mut out_path = utils::get_data_root()?; - out_path.push(THUMBNAILS_PATH); - - let mut dir_builder = DirBuilder::new(); - dir_builder.recursive(true); - dir_builder.create(out_path.as_path())?; - - let source_image = image::open(real_path)?; - let (source_width, source_height) = source_image.dimensions(); - let largest_dimension = cmp::max(source_width, source_height); - let out_dimension = cmp::min(options.max_dimension, largest_dimension); - - let hash = hash(real_path, options); - out_path.push(format!("{}.jpg", hash.to_string())); - - if !out_path.exists() { - let quality = 80; - let source_aspect_ratio: f32 = source_width as f32 / source_height as f32; - let is_almost_square = source_aspect_ratio > 0.8 && source_aspect_ratio < 1.2; - - let mut final_image; - if is_almost_square && options.resize_if_almost_square { - final_image = - source_image.resize_exact(out_dimension, out_dimension, FilterType::Lanczos3); - } else if options.pad_to_square { - let scaled_image = - source_image.resize(out_dimension, out_dimension, FilterType::Lanczos3); - let (scaled_width, scaled_height) = scaled_image.dimensions(); - let background = image::Rgb([255, 255 as u8, 255 as u8]); - final_image = DynamicImage::ImageRgb8(ImageBuffer::from_pixel( - out_dimension, - out_dimension, - background, - )); - final_image.copy_from( - &scaled_image, - (out_dimension - scaled_width) / 2, - (out_dimension - scaled_height) / 2, - )?; - } else { - final_image = source_image.resize(out_dimension, out_dimension, FilterType::Lanczos3); - } - - let mut out_file = File::create(&out_path)?; - final_image.write_to(&mut out_file, ImageOutputFormat::Jpeg(quality))?; - } - - Ok(out_path) -}