mirror of
https://github.com/agersant/polaris
synced 2024-11-10 10:14:12 +00:00
Thumbnails code cleanup
This commit is contained in:
parent
8c32d7351c
commit
ca8f046142
6 changed files with 164 additions and 68 deletions
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue