2020-06-16 23:13:22 +00:00
|
|
|
use crate::{
|
2020-11-01 20:23:56 +00:00
|
|
|
config::Format,
|
2020-06-16 23:13:22 +00:00
|
|
|
error::UploadError,
|
|
|
|
validate::{ptos, Op},
|
|
|
|
};
|
2020-06-07 17:51:45 +00:00
|
|
|
use actix_web::web;
|
2020-06-16 23:13:22 +00:00
|
|
|
use bytes::Bytes;
|
|
|
|
use magick_rust::MagickWand;
|
2020-06-24 16:58:46 +00:00
|
|
|
use std::path::PathBuf;
|
2020-11-01 20:23:56 +00:00
|
|
|
use tracing::{debug, error, instrument, Span};
|
2020-06-07 17:51:45 +00:00
|
|
|
|
|
|
|
pub(crate) trait Processor {
|
2020-06-07 19:12:19 +00:00
|
|
|
fn name() -> &'static str
|
|
|
|
where
|
|
|
|
Self: Sized;
|
|
|
|
|
|
|
|
fn is_processor(s: &str) -> bool
|
|
|
|
where
|
|
|
|
Self: Sized;
|
|
|
|
|
2020-06-24 16:58:46 +00:00
|
|
|
fn parse(k: &str, v: &str) -> Option<Box<dyn Processor + Send>>
|
2020-06-07 19:12:19 +00:00
|
|
|
where
|
|
|
|
Self: Sized;
|
|
|
|
|
2020-06-07 17:51:45 +00:00
|
|
|
fn path(&self, path: PathBuf) -> PathBuf;
|
2020-06-24 16:58:46 +00:00
|
|
|
fn process(&self, wand: &mut MagickWand) -> Result<(), UploadError>;
|
2020-06-07 17:51:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) struct Identity;
|
|
|
|
|
|
|
|
impl Processor for Identity {
|
2020-06-07 19:12:19 +00:00
|
|
|
fn name() -> &'static str
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
|
|
|
"identity"
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_processor(s: &str) -> bool
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
|
|
|
s == Self::name()
|
|
|
|
}
|
|
|
|
|
2020-06-24 16:58:46 +00:00
|
|
|
fn parse(_: &str, _: &str) -> Option<Box<dyn Processor + Send>>
|
2020-06-07 19:12:19 +00:00
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
2020-06-14 15:07:31 +00:00
|
|
|
debug!("Identity");
|
2020-06-07 19:12:19 +00:00
|
|
|
Some(Box::new(Identity))
|
|
|
|
}
|
|
|
|
|
2020-06-07 18:03:36 +00:00
|
|
|
fn path(&self, path: PathBuf) -> PathBuf {
|
2020-06-07 17:51:45 +00:00
|
|
|
path
|
|
|
|
}
|
|
|
|
|
2020-06-24 16:58:46 +00:00
|
|
|
fn process(&self, _: &mut MagickWand) -> Result<(), UploadError> {
|
|
|
|
Ok(())
|
2020-06-07 17:51:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-16 23:13:22 +00:00
|
|
|
pub(crate) struct Thumbnail(usize);
|
2020-06-07 17:51:45 +00:00
|
|
|
|
|
|
|
impl Processor for Thumbnail {
|
2020-06-07 19:12:19 +00:00
|
|
|
fn name() -> &'static str
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
|
|
|
"thumbnail"
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_processor(s: &str) -> bool
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
2020-06-24 16:58:46 +00:00
|
|
|
s == Self::name()
|
2020-06-07 19:12:19 +00:00
|
|
|
}
|
|
|
|
|
2020-06-24 16:58:46 +00:00
|
|
|
fn parse(_: &str, v: &str) -> Option<Box<dyn Processor + Send>>
|
2020-06-07 19:12:19 +00:00
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
2020-06-24 16:58:46 +00:00
|
|
|
let size = v.parse().ok()?;
|
2020-06-07 19:12:19 +00:00
|
|
|
Some(Box::new(Thumbnail(size)))
|
|
|
|
}
|
|
|
|
|
2020-06-07 17:51:45 +00:00
|
|
|
fn path(&self, mut path: PathBuf) -> PathBuf {
|
2020-06-07 19:12:19 +00:00
|
|
|
path.push(Self::name());
|
2020-06-07 17:51:45 +00:00
|
|
|
path.push(self.0.to_string());
|
|
|
|
path
|
|
|
|
}
|
|
|
|
|
2020-06-24 16:58:46 +00:00
|
|
|
fn process(&self, wand: &mut MagickWand) -> Result<(), UploadError> {
|
2020-06-14 15:07:31 +00:00
|
|
|
debug!("Thumbnail");
|
2020-06-16 23:13:22 +00:00
|
|
|
let width = wand.get_image_width();
|
|
|
|
let height = wand.get_image_height();
|
|
|
|
|
|
|
|
if width > self.0 || height > self.0 {
|
2020-06-17 02:05:06 +00:00
|
|
|
let width_ratio = width as f64 / self.0 as f64;
|
|
|
|
let height_ratio = height as f64 / self.0 as f64;
|
|
|
|
|
|
|
|
let (new_width, new_height) = if width_ratio < height_ratio {
|
|
|
|
(width as f64 / height_ratio, self.0 as f64)
|
|
|
|
} else {
|
|
|
|
(self.0 as f64, height as f64 / width_ratio)
|
|
|
|
};
|
|
|
|
|
|
|
|
wand.op(|w| w.sample_image(new_width as usize, new_height as usize))?;
|
2020-06-07 17:51:45 +00:00
|
|
|
}
|
2020-06-24 16:58:46 +00:00
|
|
|
|
|
|
|
Ok(())
|
2020-06-07 17:51:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-10 05:05:04 +00:00
|
|
|
pub(crate) struct Resize(usize);
|
|
|
|
|
|
|
|
impl Processor for Resize {
|
|
|
|
fn name() -> &'static str
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
|
|
|
"resize"
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_processor(s: &str) -> bool
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
|
|
|
s == Self::name()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse(_: &str, v: &str) -> Option<Box<dyn Processor + Send>>
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
|
|
|
let size = v.parse().ok()?;
|
|
|
|
Some(Box::new(Resize(size)))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn path(&self, mut path: PathBuf) -> PathBuf {
|
|
|
|
path.push(Self::name());
|
|
|
|
path.push(self.0.to_string());
|
|
|
|
path
|
|
|
|
}
|
|
|
|
|
|
|
|
fn process(&self, wand: &mut MagickWand) -> Result<(), UploadError> {
|
|
|
|
debug!("Resize");
|
|
|
|
let width = wand.get_image_width();
|
|
|
|
let height = wand.get_image_height();
|
|
|
|
|
|
|
|
if width > self.0 || height > self.0 {
|
|
|
|
let width_ratio = width as f64 / self.0 as f64;
|
|
|
|
let height_ratio = height as f64 / self.0 as f64;
|
|
|
|
|
|
|
|
let (new_width, new_height) = if width_ratio < height_ratio {
|
|
|
|
(width as f64 / height_ratio, self.0 as f64)
|
|
|
|
} else {
|
|
|
|
(self.0 as f64, height as f64 / width_ratio)
|
|
|
|
};
|
|
|
|
|
|
|
|
wand.resize_image(
|
|
|
|
new_width as usize,
|
|
|
|
new_height as usize,
|
|
|
|
magick_rust::bindings::FilterType_Lanczos2Filter,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-17 00:20:06 +00:00
|
|
|
pub(crate) struct Crop(usize, usize);
|
|
|
|
|
|
|
|
impl Processor for Crop {
|
|
|
|
fn name() -> &'static str
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
|
|
|
"crop"
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_processor(s: &str) -> bool {
|
|
|
|
s == Self::name()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse(_: &str, v: &str) -> Option<Box<dyn Processor + Send>> {
|
|
|
|
let mut iter = v.split('x');
|
|
|
|
let first = iter.next()?;
|
|
|
|
let second = iter.next()?;
|
|
|
|
|
|
|
|
let width = first.parse::<usize>().ok()?;
|
|
|
|
let height = second.parse::<usize>().ok()?;
|
|
|
|
|
|
|
|
if width == 0 || height == 0 {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
if width > 20 || height > 20 {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
Some(Box::new(Crop(width, height)))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn path(&self, mut path: PathBuf) -> PathBuf {
|
|
|
|
path.push(Self::name());
|
|
|
|
path.push(format!("{}x{}", self.0, self.1));
|
|
|
|
path
|
|
|
|
}
|
|
|
|
|
|
|
|
fn process(&self, wand: &mut MagickWand) -> Result<(), UploadError> {
|
|
|
|
let width = wand.get_image_width();
|
|
|
|
let height = wand.get_image_height();
|
|
|
|
|
|
|
|
// 16x9 becomes 16/9, which is bigger than 16/10. a bigger number means a wider image
|
|
|
|
//
|
|
|
|
// Crop ratios bigger than Image ratios mean cropping the image's height and leaving the
|
|
|
|
// width alone.
|
|
|
|
let img_ratio = width as f64 / height as f64;
|
|
|
|
let crop_ratio = self.0 as f64 / self.1 as f64;
|
|
|
|
|
|
|
|
let final_width;
|
|
|
|
let final_height;
|
|
|
|
|
|
|
|
let x_offset;
|
|
|
|
let y_offset;
|
|
|
|
|
|
|
|
if crop_ratio > img_ratio {
|
|
|
|
final_height = (width as f64 / self.0 as f64 * self.1 as f64) as usize;
|
|
|
|
final_width = width;
|
|
|
|
|
|
|
|
x_offset = 0;
|
|
|
|
y_offset = ((height - final_height) as f64 / 2.0) as isize;
|
|
|
|
} else {
|
|
|
|
final_height = height;
|
|
|
|
final_width = (height as f64 / self.1 as f64 * self.0 as f64) as usize;
|
|
|
|
|
|
|
|
x_offset = ((width - final_width) as f64 / 2.0) as isize;
|
|
|
|
y_offset = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
wand.op(|w| w.crop_image(final_width, final_height, x_offset, y_offset))?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-16 23:13:22 +00:00
|
|
|
pub(crate) struct Blur(f64);
|
2020-06-07 17:51:45 +00:00
|
|
|
|
|
|
|
impl Processor for Blur {
|
2020-06-07 19:12:19 +00:00
|
|
|
fn name() -> &'static str
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
|
|
|
"blur"
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_processor(s: &str) -> bool {
|
2020-06-24 16:58:46 +00:00
|
|
|
s == Self::name()
|
2020-06-07 19:12:19 +00:00
|
|
|
}
|
|
|
|
|
2020-06-24 16:58:46 +00:00
|
|
|
fn parse(_: &str, v: &str) -> Option<Box<dyn Processor + Send>> {
|
|
|
|
let sigma = v.parse().ok()?;
|
2020-06-07 19:12:19 +00:00
|
|
|
Some(Box::new(Blur(sigma)))
|
|
|
|
}
|
|
|
|
|
2020-06-07 17:51:45 +00:00
|
|
|
fn path(&self, mut path: PathBuf) -> PathBuf {
|
2020-06-07 19:12:19 +00:00
|
|
|
path.push(Self::name());
|
2020-06-07 17:51:45 +00:00
|
|
|
path.push(self.0.to_string());
|
|
|
|
path
|
|
|
|
}
|
|
|
|
|
2020-06-24 16:58:46 +00:00
|
|
|
fn process(&self, wand: &mut MagickWand) -> Result<(), UploadError> {
|
2020-06-14 15:07:31 +00:00
|
|
|
debug!("Blur");
|
2020-06-15 04:10:30 +00:00
|
|
|
if self.0 > 0.0 {
|
2020-06-16 23:13:22 +00:00
|
|
|
wand.op(|w| w.gaussian_blur_image(0.0, self.0))?;
|
2020-06-15 04:10:30 +00:00
|
|
|
}
|
2020-06-24 16:58:46 +00:00
|
|
|
|
|
|
|
Ok(())
|
2020-06-07 17:51:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-07 19:12:19 +00:00
|
|
|
macro_rules! parse {
|
2020-06-24 16:58:46 +00:00
|
|
|
($x:ident, $k:expr, $v:expr) => {{
|
|
|
|
if $x::is_processor($k) {
|
|
|
|
return $x::parse($k, $v);
|
2020-06-07 19:12:19 +00:00
|
|
|
}
|
|
|
|
}};
|
|
|
|
}
|
|
|
|
|
2020-06-14 02:28:06 +00:00
|
|
|
pub(crate) struct ProcessChain {
|
|
|
|
inner: Vec<Box<dyn Processor + Send>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl std::fmt::Debug for ProcessChain {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
|
|
f.debug_struct("ProcessChain")
|
2020-06-14 15:07:31 +00:00
|
|
|
.field("steps", &self.inner.len())
|
2020-06-14 02:28:06 +00:00
|
|
|
.finish()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[instrument]
|
2020-06-24 16:58:46 +00:00
|
|
|
pub(crate) fn build_chain(args: &[(String, String)]) -> ProcessChain {
|
2020-06-14 02:28:06 +00:00
|
|
|
let inner = args
|
2020-12-26 02:39:19 +00:00
|
|
|
.iter()
|
2020-06-24 16:58:46 +00:00
|
|
|
.filter_map(|(k, v)| {
|
|
|
|
let k = k.as_str();
|
|
|
|
let v = v.as_str();
|
|
|
|
|
|
|
|
parse!(Identity, k, v);
|
|
|
|
parse!(Thumbnail, k, v);
|
2020-12-10 05:05:04 +00:00
|
|
|
parse!(Resize, k, v);
|
2020-12-17 00:20:06 +00:00
|
|
|
parse!(Crop, k, v);
|
2020-06-24 16:58:46 +00:00
|
|
|
parse!(Blur, k, v);
|
2020-06-07 19:12:19 +00:00
|
|
|
|
2020-06-24 16:58:46 +00:00
|
|
|
debug!("Skipping {}: {}, invalid", k, v);
|
2020-06-07 19:12:19 +00:00
|
|
|
|
|
|
|
None
|
|
|
|
})
|
2020-06-14 02:28:06 +00:00
|
|
|
.collect();
|
|
|
|
|
|
|
|
ProcessChain { inner }
|
2020-06-07 17:51:45 +00:00
|
|
|
}
|
|
|
|
|
2020-06-14 02:28:06 +00:00
|
|
|
pub(crate) fn build_path(base: PathBuf, chain: &ProcessChain, filename: String) -> PathBuf {
|
|
|
|
let mut path = chain
|
|
|
|
.inner
|
|
|
|
.iter()
|
|
|
|
.fold(base, |acc, processor| processor.path(acc));
|
2020-06-07 17:51:45 +00:00
|
|
|
|
|
|
|
path.push(filename);
|
|
|
|
path
|
|
|
|
}
|
|
|
|
|
2020-06-24 16:58:46 +00:00
|
|
|
fn is_motion(s: &str) -> bool {
|
|
|
|
s.ends_with(".gif") || s.ends_with(".mp4")
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) enum Exists {
|
|
|
|
Exists,
|
|
|
|
New,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Exists {
|
|
|
|
pub(crate) fn is_new(&self) -> bool {
|
2020-12-26 02:39:19 +00:00
|
|
|
matches!(self, Exists::New)
|
2020-06-24 16:58:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) async fn prepare_image(
|
|
|
|
original_file: PathBuf,
|
|
|
|
) -> Result<Option<(PathBuf, Exists)>, UploadError> {
|
|
|
|
let original_path_str = ptos(&original_file)?;
|
|
|
|
let jpg_path = format!("{}.jpg", original_path_str);
|
|
|
|
let jpg_path = PathBuf::from(jpg_path);
|
|
|
|
|
2021-01-18 23:11:32 +00:00
|
|
|
if async_fs::metadata(jpg_path.clone()).await.is_ok() {
|
2020-06-24 16:58:46 +00:00
|
|
|
return Ok(Some((jpg_path, Exists::Exists)));
|
|
|
|
}
|
|
|
|
|
|
|
|
if is_motion(&original_path_str) {
|
|
|
|
let orig_path = original_path_str.clone();
|
|
|
|
|
|
|
|
let tmpfile = crate::tmp_file();
|
2020-10-16 22:48:42 +00:00
|
|
|
crate::safe_create_parent(tmpfile.clone()).await?;
|
2020-06-24 16:58:46 +00:00
|
|
|
let tmpfile2 = tmpfile.clone();
|
|
|
|
|
|
|
|
let res = web::block(move || {
|
|
|
|
use crate::validate::transcode::{transcode, Target};
|
|
|
|
|
|
|
|
transcode(orig_path, tmpfile, Target::Jpeg).map_err(UploadError::Transcode)
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
|
|
|
if let Err(e) = res {
|
2020-10-16 22:48:42 +00:00
|
|
|
error!("transcode error: {:?}", e);
|
2021-01-18 23:11:32 +00:00
|
|
|
async_fs::remove_file(tmpfile2).await?;
|
2020-06-24 16:58:46 +00:00
|
|
|
return Err(e.into());
|
|
|
|
}
|
|
|
|
|
|
|
|
return match crate::safe_move_file(tmpfile2, jpg_path.clone()).await {
|
|
|
|
Err(UploadError::FileExists) => Ok(Some((jpg_path, Exists::Exists))),
|
|
|
|
Err(e) => Err(e),
|
|
|
|
_ => Ok(Some((jpg_path, Exists::New))),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
|
2020-06-16 23:13:22 +00:00
|
|
|
#[instrument]
|
2020-06-07 17:51:45 +00:00
|
|
|
pub(crate) async fn process_image(
|
2020-06-16 23:13:22 +00:00
|
|
|
original_file: PathBuf,
|
2020-06-14 02:28:06 +00:00
|
|
|
chain: ProcessChain,
|
2020-11-01 20:23:56 +00:00
|
|
|
format: Format,
|
2020-06-24 16:58:46 +00:00
|
|
|
) -> Result<Bytes, UploadError> {
|
2020-06-16 23:13:22 +00:00
|
|
|
let original_path_str = ptos(&original_file)?;
|
2020-06-15 04:10:30 +00:00
|
|
|
|
2020-06-24 16:58:46 +00:00
|
|
|
let span = Span::current();
|
|
|
|
let bytes = web::block(move || {
|
2020-06-16 23:13:22 +00:00
|
|
|
let entered = span.enter();
|
|
|
|
|
|
|
|
let mut wand = MagickWand::new();
|
|
|
|
debug!("Reading image");
|
|
|
|
wand.op(|w| w.read_image(&original_path_str))?;
|
|
|
|
|
|
|
|
debug!("Processing image");
|
|
|
|
for processor in chain.inner.into_iter() {
|
|
|
|
debug!("Step");
|
2020-06-24 16:58:46 +00:00
|
|
|
processor.process(&mut wand)?;
|
2020-06-16 23:13:22 +00:00
|
|
|
}
|
|
|
|
|
2020-11-01 20:23:56 +00:00
|
|
|
let vec = wand.op(|w| w.write_image_blob(format.to_magick_format()))?;
|
2020-06-16 23:13:22 +00:00
|
|
|
drop(entered);
|
2020-12-26 02:39:19 +00:00
|
|
|
Ok(Bytes::from(vec)) as Result<Bytes, UploadError>
|
2020-06-16 23:13:22 +00:00
|
|
|
})
|
|
|
|
.await?;
|
2020-06-07 17:51:45 +00:00
|
|
|
|
2020-06-24 16:58:46 +00:00
|
|
|
Ok(bytes)
|
2020-06-07 17:51:45 +00:00
|
|
|
}
|