Made thumbnail padding optional

This commit is contained in:
Antoine Gersant 2020-02-02 15:11:43 -08:00
parent 86d61dd964
commit 6e3f439d8a
6 changed files with 97 additions and 57 deletions

View file

@ -2,7 +2,7 @@
"openapi": "3.0.0",
"info": {
"description": "",
"version": "4.0",
"version": "5.0",
"title": "Polaris",
"termsOfService": ""
},
@ -444,13 +444,13 @@
]
}
},
"/serve/{file}": {
"/audio/{file}": {
"get": {
"tags": [
"Collection"
],
"summary": "Access a media file in the collection",
"operationId": "getServe",
"operationId": "getAudio",
"parameters": [
{
"name": "file",
@ -465,12 +465,53 @@
"200": {
"description": "Successful operation",
"content": {
"image/*": {
"audio/*": {
"schema": {
"format": "binary"
}
},
"audio/*": {
}
}
}
},
"security": [
{
"auth_http_header": [],
"auth_cookie": []
}
]
}
},
"/thumbnail/{file}": {
"get": {
"tags": [
"Collection"
],
"summary": "Generate an image thumbnail for a media file in the collection",
"operationId": "getServe",
"parameters": [
{
"name": "file",
"in": "path",
"description": "Path to the desired file",
"schema": {
"type": "string"
}
},
{
"name": "pad",
"in": "query",
"description": "Indicates whether the thumbnail should be padded to a square aspect-ratio",
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Successful operation",
"content": {
"image/*": {
"schema": {
"format": "binary"
}

View file

@ -1,4 +1,4 @@
pub const API_MAJOR_VERSION: i32 = 4;
pub const API_MAJOR_VERSION: i32 = 5;
pub const API_MINOR_VERSION: i32 = 0;
pub const COOKIE_SESSION: &str = "session";
pub const COOKIE_USERNAME: &str = "username";

View file

@ -4,6 +4,7 @@ use rocket::request::{self, FromParam, FromRequest, Request};
use rocket::response::content::Html;
use rocket::{delete, get, post, put, routes, Outcome, State};
use rocket_contrib::json::Json;
use std::default::Default;
use std::fs::File;
use std::ops::Deref;
use std::path::PathBuf;
@ -23,7 +24,6 @@ use crate::service::dto;
use crate::service::error::APIError;
use crate::thumbnails;
use crate::user;
use crate::utils;
use crate::vfs::VFSSource;
pub fn get_routes() -> Vec<rocket::Route> {
@ -44,7 +44,8 @@ pub fn get_routes() -> Vec<rocket::Route> {
recent,
search_root,
search,
serve,
audio,
thumbnail,
list_playlists,
save_playlist,
read_playlist,
@ -305,21 +306,25 @@ fn search(
Ok(Json(result))
}
#[get("/serve/<path>")]
fn serve(db: State<'_, DB>, _auth: Auth, path: VFSPathBuf) -> Result<serve::RangeResponder<File>> {
#[get("/audio/<path>")]
fn audio(db: State<'_, DB>, _auth: Auth, path: VFSPathBuf) -> Result<serve::RangeResponder<File>> {
let vfs = db.get_vfs()?;
let real_path = vfs.virtual_to_real(&path.into() as &PathBuf)?;
let serve_path = if utils::is_image(&real_path) {
thumbnails::get_thumbnail(&real_path, 400)?
} else {
real_path
};
let file = File::open(serve_path)?;
let file = File::open(&real_path)?;
Ok(serve::RangeResponder::new(file))
}
#[get("/thumbnail/<path>?<pad>")]
fn thumbnail(db: State<'_, DB>, _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();
options.pad_to_square = pad.unwrap_or(options.pad_to_square);
let thumbnail_path = thumbnails::get_thumbnail(&image_path, &options)?;
let file = File::open(thumbnail_path)?;
Ok(file)
}
#[get("/playlists")]
fn list_playlists(db: State<'_, DB>, auth: Auth) -> Result<Json<Vec<dto::ListPlaylistsEntry>>> {
let playlist_names = playlist::list_playlists(&auth.username, db.deref().deref())?;

View file

@ -107,7 +107,7 @@ fn test_service_version() {
let mut service = ServiceType::new(function_name!());
let response = service.get_json::<dto::Version>("/api/version");
let version = response.body();
assert_eq!(version, &dto::Version { major: 4, minor: 0 });
assert_eq!(version, &dto::Version { major: 5, minor: 0 });
}
#[named]
@ -389,7 +389,7 @@ fn test_service_serve() {
path.push("Hunted");
path.push("02 - Candlelight.mp3");
let uri = format!(
"/api/serve/{}",
"/api/audio/{}",
percent_encode(path.to_string_lossy().as_ref().as_bytes(), NON_ALPHANUMERIC)
);

View file

@ -16,15 +16,31 @@ use crate::utils;
const THUMBNAILS_PATH: &str = "thumbnails";
fn hash(path: &Path, dimension: u32) -> u64 {
let path_string = path.to_string_lossy();
let hash_input = format!("{}:{}", path_string, dimension.to_string());
#[derive(Debug, Hash)]
pub struct Options {
pub max_dimension: u32,
pub resize_if_almost_square: bool,
pub pad_to_square: bool,
}
impl Default for Options {
fn default() -> Options {
Options {
max_dimension: 400,
resize_if_almost_square: true,
pad_to_square: true,
}
}
}
fn hash(path: &Path, options: &Options) -> u64 {
let mut hasher = DefaultHasher::new();
hash_input.hash(&mut hasher);
path.hash(&mut hasher);
options.hash(&mut hasher);
hasher.finish()
}
pub fn get_thumbnail(real_path: &Path, max_dimension: u32) -> Result<PathBuf> {
pub fn get_thumbnail(real_path: &Path, options: &Options) -> Result<PathBuf> {
let mut out_path = utils::get_data_root()?;
out_path.push(THUMBNAILS_PATH);
@ -35,17 +51,21 @@ pub fn get_thumbnail(real_path: &Path, max_dimension: u32) -> Result<PathBuf> {
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(max_dimension, largest_dimension);
let out_dimension = cmp::min(options.max_dimension, largest_dimension);
let hash = hash(real_path, out_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 source_aspect_ratio < 0.8 || source_aspect_ratio > 1.2 {
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();
@ -61,9 +81,8 @@ pub fn get_thumbnail(real_path: &Path, max_dimension: u32) -> Result<PathBuf> {
(out_dimension - scaled_height) / 2,
);
} else {
final_image =
source_image.resize_exact(out_dimension, out_dimension, FilterType::Lanczos3);
};
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))?;

View file

@ -60,28 +60,3 @@ fn test_get_audio_format() {
Some(AudioFormat::FLAC)
);
}
pub fn is_image(path: &Path) -> bool {
let extension = match path.extension() {
Some(e) => e,
_ => return false,
};
let extension = match extension.to_str() {
Some(e) => e,
_ => return false,
};
match extension.to_lowercase().as_str() {
"png" => true,
"gif" => true,
"jpg" => true,
"jpeg" => true,
"bmp" => true,
_ => false,
}
}
#[test]
fn test_is_image() {
assert!(!is_image(Path::new("animals/🐷/my🐖file.mp3")));
assert!(is_image(Path::new("animals/🐷/my🐖file.jpg")));
}