2020-06-16 23:13:22 +00:00
|
|
|
use crate::{
|
|
|
|
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-07 19:12:19 +00:00
|
|
|
use std::{collections::HashSet, path::PathBuf};
|
2020-06-14 15:07:31 +00:00
|
|
|
use tracing::{debug, 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;
|
|
|
|
|
|
|
|
fn parse(s: &str) -> Option<Box<dyn Processor + Send>>
|
|
|
|
where
|
|
|
|
Self: Sized;
|
|
|
|
|
2020-06-07 17:51:45 +00:00
|
|
|
fn path(&self, path: PathBuf) -> PathBuf;
|
2020-06-16 23:13:22 +00:00
|
|
|
fn process(&self, wand: &mut MagickWand) -> Result<bool, UploadError>;
|
2020-06-07 19:12:19 +00:00
|
|
|
|
|
|
|
fn is_whitelisted(whitelist: Option<&HashSet<String>>) -> bool
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
|
|
|
whitelist
|
|
|
|
.map(|wl| wl.contains(Self::name()))
|
|
|
|
.unwrap_or(true)
|
|
|
|
}
|
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()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse(_: &str) -> Option<Box<dyn Processor + Send>>
|
|
|
|
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-16 23:13:22 +00:00
|
|
|
fn process(&self, _: &mut MagickWand) -> Result<bool, UploadError> {
|
|
|
|
Ok(false)
|
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,
|
|
|
|
{
|
|
|
|
s.starts_with(Self::name())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse(s: &str) -> Option<Box<dyn Processor + Send>>
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
|
|
|
let size = s.trim_start_matches(Self::name()).parse().ok()?;
|
|
|
|
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-16 23:13:22 +00:00
|
|
|
fn process(&self, wand: &mut MagickWand) -> Result<bool, 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 {
|
|
|
|
wand.fit(self.0, self.0);
|
|
|
|
Ok(true)
|
|
|
|
} else if wand.op(|w| w.get_image_format())? == "GIF" {
|
|
|
|
Ok(true)
|
2020-06-07 17:51:45 +00:00
|
|
|
} else {
|
2020-06-16 23:13:22 +00:00
|
|
|
Ok(false)
|
2020-06-07 17:51:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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 {
|
|
|
|
s.starts_with(Self::name())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse(s: &str) -> Option<Box<dyn Processor + Send>> {
|
|
|
|
let sigma = s.trim_start_matches(Self::name()).parse().ok()?;
|
|
|
|
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-16 23:13:22 +00:00
|
|
|
fn process(&self, wand: &mut MagickWand) -> Result<bool, 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))?;
|
|
|
|
Ok(true)
|
2020-06-15 04:10:30 +00:00
|
|
|
} else {
|
2020-06-16 23:13:22 +00:00
|
|
|
Ok(false)
|
2020-06-15 04:10:30 +00:00
|
|
|
}
|
2020-06-07 17:51:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-07 19:12:19 +00:00
|
|
|
macro_rules! parse {
|
|
|
|
($x:ident, $y:expr, $z:expr) => {{
|
|
|
|
if $x::is_processor($y) && $x::is_whitelisted($z) {
|
|
|
|
return $x::parse($y);
|
|
|
|
}
|
|
|
|
}};
|
|
|
|
}
|
|
|
|
|
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]
|
|
|
|
pub(crate) fn build_chain(args: &[String], whitelist: Option<&HashSet<String>>) -> ProcessChain {
|
|
|
|
let inner = args
|
|
|
|
.into_iter()
|
2020-06-07 19:12:19 +00:00
|
|
|
.filter_map(|arg| {
|
|
|
|
parse!(Identity, arg.as_str(), whitelist);
|
|
|
|
parse!(Thumbnail, arg.as_str(), whitelist);
|
|
|
|
parse!(Blur, arg.as_str(), whitelist);
|
|
|
|
|
|
|
|
debug!("Skipping {}, invalid or whitelisted", arg);
|
|
|
|
|
|
|
|
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-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-06-16 23:13:22 +00:00
|
|
|
) -> Result<Option<Bytes>, UploadError> {
|
|
|
|
let original_path_str = ptos(&original_file)?;
|
|
|
|
let span = Span::current();
|
2020-06-15 04:10:30 +00:00
|
|
|
|
2020-06-16 23:13:22 +00:00
|
|
|
let opt = web::block(move || {
|
|
|
|
let entered = span.enter();
|
|
|
|
|
|
|
|
let mut wand = MagickWand::new();
|
|
|
|
debug!("Reading image");
|
|
|
|
wand.op(|w| w.read_image(&original_path_str))?;
|
|
|
|
|
|
|
|
let format = wand.op(|w| w.get_image_format())?;
|
|
|
|
|
|
|
|
debug!("Processing image");
|
|
|
|
let mut changed = false;
|
|
|
|
|
|
|
|
for processor in chain.inner.into_iter() {
|
|
|
|
debug!("Step");
|
|
|
|
changed |= processor.process(&mut wand)?;
|
|
|
|
debug!("Step complete");
|
|
|
|
}
|
|
|
|
|
|
|
|
if changed {
|
|
|
|
let vec = wand.op(|w| w.write_image_blob(&format))?;
|
|
|
|
return Ok(Some(Bytes::from(vec)));
|
|
|
|
}
|
|
|
|
|
|
|
|
drop(entered);
|
|
|
|
Ok(None) as Result<Option<Bytes>, UploadError>
|
|
|
|
})
|
|
|
|
.await?;
|
2020-06-07 17:51:45 +00:00
|
|
|
|
2020-06-16 23:13:22 +00:00
|
|
|
Ok(opt)
|
2020-06-07 17:51:45 +00:00
|
|
|
}
|