use crate::error::{Error, UploadError}; use std::path::PathBuf; pub(crate) trait Processor { const NAME: &'static str; fn parse(k: &str, v: &str) -> Option where Self: Sized; fn path(&self, path: PathBuf) -> PathBuf; fn command(&self, args: Vec) -> Vec; } pub(crate) struct Identity; pub(crate) struct Thumbnail(usize); pub(crate) struct Resize { filter: Option, kind: ResizeKind, } #[derive(Copy, Clone)] pub(crate) enum ResizeFilter { Lanczos, LanczosSharp, Lanczos2, Lanczos2Sharp, Mitchell, RobidouxSharp, } #[derive(Copy, Clone)] pub(crate) enum ResizeKind { Bounds(usize), Area(usize), } pub(crate) struct Crop(usize, usize); pub(crate) struct Blur(f64); impl ResizeFilter { fn from_str(s: &str) -> Option { match s.to_lowercase().as_str() { "lanczos" => Some(Self::Lanczos), "lanczossharp" => Some(Self::LanczosSharp), "lanczos2" => Some(Self::Lanczos2), "lanczos2sharp" => Some(Self::Lanczos2Sharp), "mitchell" => Some(Self::Mitchell), "robidouxsharp" => Some(Self::RobidouxSharp), _ => None, } } fn to_magick_str(self) -> &'static str { match self { Self::Lanczos => "Lanczos", Self::LanczosSharp => "LanczosSharp", Self::Lanczos2 => "Lanczos2", Self::Lanczos2Sharp => "Lanczos2Sharp", Self::Mitchell => "Mitchell", Self::RobidouxSharp => "RobidouxSharp", } } } impl Default for ResizeFilter { fn default() -> Self { Self::Lanczos2 } } impl ResizeKind { fn from_str(s: &str) -> Option { let kind = if s.starts_with('a') { let size = s.trim_start_matches('a').parse().ok()?; ResizeKind::Area(size) } else { let size = s.parse().ok()?; ResizeKind::Bounds(size) }; Some(kind) } fn to_magick_string(self) -> String { match self { Self::Area(size) => format!("{size}@>"), Self::Bounds(size) => format!("{size}x{size}>"), } } } #[tracing::instrument(level = "debug")] pub(crate) fn build_chain( args: &[(String, String)], ext: &str, ) -> Result<(String, Vec), Error> { fn parse(key: &str, value: &str) -> Result, Error> { if key == P::NAME { return Ok(Some(P::parse(key, value).ok_or(UploadError::ParsePath)?)); } Ok(None) } macro_rules! parse { ($inner:expr, $x:ident, $k:expr, $v:expr) => {{ if let Some(processor) = parse::<$x>($k, $v)? { return Ok((processor.path($inner.0), processor.command($inner.1))); }; }}; } let (mut path, args) = args.iter() .try_fold((PathBuf::default(), vec![]), |inner, (name, value)| { parse!(inner, Identity, name, value); parse!(inner, Thumbnail, name, value); parse!(inner, Resize, name, value); parse!(inner, Crop, name, value); parse!(inner, Blur, name, value); Err(Error::from(UploadError::ParsePath)) })?; path.push(ext); Ok((path.to_string_lossy().to_string(), args)) } impl Processor for Identity { const NAME: &'static str = "identity"; fn parse(_: &str, _: &str) -> Option where Self: Sized, { Some(Identity) } fn path(&self, path: PathBuf) -> PathBuf { path } fn command(&self, args: Vec) -> Vec { args } } impl Processor for Thumbnail { const NAME: &'static str = "thumbnail"; fn parse(_: &str, v: &str) -> Option where Self: Sized, { let size = v.parse().ok()?; Some(Thumbnail(size)) } fn path(&self, mut path: PathBuf) -> PathBuf { path.push(Self::NAME); path.push(self.0.to_string()); path } fn command(&self, mut args: Vec) -> Vec { args.extend(["-sample".to_string(), format!("{}x{}>", self.0, self.0)]); args } } impl Processor for Resize { const NAME: &'static str = "resize"; fn parse(_: &str, v: &str) -> Option where Self: Sized, { if let Some((first, second)) = v.split_once('.') { let filter = ResizeFilter::from_str(first); let kind = ResizeKind::from_str(second)?; Some(Resize { filter, kind }) } else { let size = v.parse().ok()?; Some(Resize { filter: None, kind: ResizeKind::Bounds(size), }) } } fn path(&self, mut path: PathBuf) -> PathBuf { path.push(Self::NAME); match self { Resize { filter: None, kind: ResizeKind::Bounds(size), } => { path.push(size.to_string()); } Resize { filter: None, kind: ResizeKind::Area(size), } => { let node = format!(".a{size}"); path.push(node); } Resize { filter: Some(filter), kind: ResizeKind::Bounds(size), } => { let node = format!("{}.{size}", filter.to_magick_str()); path.push(node); } Resize { filter: Some(filter), kind: ResizeKind::Area(size), } => { let node = format!("{}.a{size}", filter.to_magick_str()); path.push(node); } } path } fn command(&self, mut args: Vec) -> Vec { args.extend([ "-filter".to_string(), self.filter.unwrap_or_default().to_magick_str().to_string(), "-resize".to_string(), self.kind.to_magick_string(), ]); args } } impl Processor for Crop { const NAME: &'static str = "crop"; fn parse(_: &str, v: &str) -> Option { let mut iter = v.split('x'); let first = iter.next()?; let second = iter.next()?; let width = first.parse::().ok()?; let height = second.parse::().ok()?; if width == 0 || height == 0 { return None; } if width > 20 || height > 20 { return None; } Some(Crop(width, height)) } fn path(&self, mut path: PathBuf) -> PathBuf { path.push(Self::NAME); path.push(format!("{}x{}", self.0, self.1)); path } fn command(&self, mut args: Vec) -> Vec { args.extend([ "-gravity".to_string(), "center".to_string(), "-crop".to_string(), format!("{}:{}+0+0", self.0, self.1), ]); args } } impl Processor for Blur { const NAME: &'static str = "blur"; fn parse(_: &str, v: &str) -> Option { let sigma = v.parse().ok()?; Some(Blur(sigma)) } fn path(&self, mut path: PathBuf) -> PathBuf { path.push(Self::NAME); path.push(self.0.to_string()); path } fn command(&self, mut args: Vec) -> Vec { args.extend(["-gaussian-blur".to_string(), format!("0x{}", self.0)]); args } }