Thumbnails code cleanup

This commit is contained in:
Antoine Gersant 2020-09-23 22:20:27 -07:00
parent 8c32d7351c
commit ca8f046142
6 changed files with 164 additions and 68 deletions

View file

@ -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,
);
});

View file

@ -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<serve::Rang
}
#[get("/thumbnail/<path>?<pad>")]
fn thumbnail(db: State<'_, DB>, _auth: Auth, path: VFSPathBuf, pad: Option<bool>) -> Result<File> {
fn thumbnail(
db: State<'_, DB>,
thumbnails_manager: State<'_, ThumbnailsManager>,
_auth: Auth,
path: VFSPathBuf,
pad: Option<bool>,
) -> Result<File> {
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)
}

View file

@ -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<rocket::Rocket> {
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(())

View file

@ -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();

View file

@ -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);
}

View file

@ -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<PathBuf> {
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<PathBuf> {
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<PathBuf> {
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<DynamicImage> {
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<PathBuf> {
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)
}