Add pixel-transform
This commit is contained in:
145
crates/lib/pixel-transform/src/chain.rs
Normal file
145
crates/lib/pixel-transform/src/chain.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use image::{DynamicImage, ImageFormat};
|
||||
use serde::{Deserialize, Deserializer, de};
|
||||
use std::{fmt::Display, hash::Hash, io::Cursor, str::FromStr};
|
||||
use thiserror::Error;
|
||||
use toolbox::mime::MimeType;
|
||||
|
||||
use crate::transformers::{ImageTransformer, TransformerEnum};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TransformBytesError {
|
||||
#[error("{0} is not a valid image type")]
|
||||
NotAnImage(String),
|
||||
|
||||
#[error("error while processing image")]
|
||||
ImageError(#[from] image::ImageError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TransformerChain {
|
||||
pub steps: Vec<TransformerEnum>,
|
||||
}
|
||||
|
||||
impl TransformerChain {
|
||||
#[inline]
|
||||
pub fn mime_is_image(mime: &MimeType) -> bool {
|
||||
ImageFormat::from_mime_type(mime.to_string()).is_some()
|
||||
}
|
||||
|
||||
pub fn transform_image(&self, mut image: DynamicImage) -> DynamicImage {
|
||||
for step in &self.steps {
|
||||
match step {
|
||||
TransformerEnum::Format { .. } => {}
|
||||
TransformerEnum::MaxDim(t) => t.transform(&mut image),
|
||||
TransformerEnum::Crop(t) => t.transform(&mut image),
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
pub fn transform_bytes(
|
||||
&self,
|
||||
image_bytes: &[u8],
|
||||
image_format: Option<&MimeType>,
|
||||
) -> Result<(MimeType, Vec<u8>), TransformBytesError> {
|
||||
let format: ImageFormat = match image_format {
|
||||
Some(x) => ImageFormat::from_mime_type(x.to_string())
|
||||
.ok_or(TransformBytesError::NotAnImage(x.to_string()))?,
|
||||
None => image::guess_format(image_bytes)?,
|
||||
};
|
||||
|
||||
let out_format = self
|
||||
.steps
|
||||
.last()
|
||||
.and_then(|x| match x {
|
||||
TransformerEnum::Format { format } => Some(format),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(&format);
|
||||
|
||||
let img = image::load_from_memory_with_format(image_bytes, format)?;
|
||||
let img = self.transform_image(img);
|
||||
|
||||
let out_mime = MimeType::from(out_format.to_mime_type());
|
||||
let mut out_bytes = Cursor::new(Vec::new());
|
||||
img.write_to(&mut out_bytes, *out_format)?;
|
||||
|
||||
return Ok((out_mime, out_bytes.into_inner()));
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for TransformerChain {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let steps_str = s.split(";");
|
||||
|
||||
let mut steps = Vec::new();
|
||||
for s in steps_str {
|
||||
let s = s.trim();
|
||||
if s.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let step = s.parse();
|
||||
match step {
|
||||
Ok(x) => steps.push(x),
|
||||
Err(msg) => return Err(format!("invalid step `{s}`: {msg}")),
|
||||
}
|
||||
}
|
||||
|
||||
let n_format = steps
|
||||
.iter()
|
||||
.filter(|x| matches!(x, TransformerEnum::Format { .. }))
|
||||
.count();
|
||||
if n_format > 2 {
|
||||
return Err("provide at most one format()".to_owned());
|
||||
}
|
||||
|
||||
if n_format == 1 && !matches!(steps.last(), Some(TransformerEnum::Format { .. })) {
|
||||
return Err("format() must be last".to_owned());
|
||||
}
|
||||
|
||||
return Ok(Self { steps });
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TransformerChain {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Self::from_str(&s).map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TransformerChain {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut first = true;
|
||||
for step in &self.steps {
|
||||
if first {
|
||||
write!(f, "{step}")?;
|
||||
first = false
|
||||
} else {
|
||||
write!(f, ";{step}")?;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for TransformerChain {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.to_string() == other.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for TransformerChain {}
|
||||
|
||||
impl Hash for TransformerChain {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.to_string().hash(state);
|
||||
}
|
||||
}
|
||||
6
crates/lib/pixel-transform/src/lib.rs
Normal file
6
crates/lib/pixel-transform/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
mod pixeldim;
|
||||
|
||||
pub mod transformers;
|
||||
|
||||
mod chain;
|
||||
pub use chain::*;
|
||||
68
crates/lib/pixel-transform/src/pixeldim.rs
Normal file
68
crates/lib/pixel-transform/src/pixeldim.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
// TODO: parse -, + (100vw - 10px)
|
||||
// TODO: parse 100vw [min] 10
|
||||
// TODO: parse 100vw [max] 10
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum PixelDim {
|
||||
Pixels(u32),
|
||||
WidthPercent(f32),
|
||||
HeightPercent(f32),
|
||||
}
|
||||
|
||||
impl FromStr for PixelDim {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let numeric_end = s.find(|c: char| !c.is_ascii_digit() && c != '.');
|
||||
|
||||
let (quantity, unit) = numeric_end.map(|x| s.split_at(x)).unwrap_or((s, "px"));
|
||||
let quantity = quantity.trim();
|
||||
let unit = unit.trim();
|
||||
|
||||
match unit {
|
||||
"vw" => Ok(PixelDim::WidthPercent(
|
||||
quantity
|
||||
.parse()
|
||||
.map_err(|_err| format!("invalid quantity {quantity}"))?,
|
||||
)),
|
||||
|
||||
"vh" => Ok(PixelDim::HeightPercent(
|
||||
quantity
|
||||
.parse()
|
||||
.map_err(|_err| format!("invalid quantity {quantity}"))?,
|
||||
)),
|
||||
|
||||
"px" => Ok(PixelDim::Pixels(
|
||||
quantity
|
||||
.parse()
|
||||
.map_err(|_err| format!("invalid quantity {quantity}"))?,
|
||||
)),
|
||||
|
||||
_ => Err(format!("invalid unit {unit}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PixelDim {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
FromStr::from_str(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PixelDim {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
PixelDim::Pixels(px) => write!(f, "{px}"),
|
||||
PixelDim::WidthPercent(p) => write!(f, "{p:.2}vw"),
|
||||
PixelDim::HeightPercent(p) => write!(f, "{p:.2}vh"),
|
||||
}
|
||||
}
|
||||
}
|
||||
184
crates/lib/pixel-transform/src/transformers/crop.rs
Normal file
184
crates/lib/pixel-transform/src/transformers/crop.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use image::DynamicImage;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{pixeldim::PixelDim, transformers::ImageTransformer};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, Serialize, Deserialize, Display)]
|
||||
pub enum Direction {
|
||||
#[serde(rename = "n")]
|
||||
#[strum(to_string = "n")]
|
||||
#[strum(serialize = "north")]
|
||||
North,
|
||||
|
||||
#[serde(rename = "e")]
|
||||
#[strum(serialize = "e")]
|
||||
#[strum(serialize = "east")]
|
||||
East,
|
||||
|
||||
#[serde(rename = "s")]
|
||||
#[strum(serialize = "s")]
|
||||
#[strum(serialize = "south")]
|
||||
South,
|
||||
|
||||
#[serde(rename = "w")]
|
||||
#[strum(to_string = "w")]
|
||||
#[strum(serialize = "west")]
|
||||
West,
|
||||
|
||||
#[serde(rename = "c")]
|
||||
#[strum(serialize = "c")]
|
||||
#[strum(serialize = "center")]
|
||||
Center,
|
||||
|
||||
#[serde(rename = "ne")]
|
||||
#[strum(serialize = "ne")]
|
||||
#[strum(serialize = "northeast")]
|
||||
NorthEast,
|
||||
|
||||
#[serde(rename = "se")]
|
||||
#[strum(serialize = "se")]
|
||||
#[strum(serialize = "southeast")]
|
||||
SouthEast,
|
||||
|
||||
#[serde(rename = "nw")]
|
||||
#[strum(serialize = "nw")]
|
||||
#[strum(serialize = "northwest")]
|
||||
NorthWest,
|
||||
|
||||
#[serde(rename = "sw")]
|
||||
#[strum(serialize = "sw")]
|
||||
#[strum(serialize = "southwest")]
|
||||
SouthWest,
|
||||
}
|
||||
|
||||
/// Crop an image to the given size.
|
||||
/// - does not crop width if `w` is greater than image width
|
||||
/// - does not crop height if `h` is greater than image height
|
||||
/// - does nothing if `w` or `h` are less than or equal to zero.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CropTransformer {
|
||||
w: PixelDim,
|
||||
h: PixelDim,
|
||||
float: Direction,
|
||||
}
|
||||
|
||||
impl CropTransformer {
|
||||
pub fn new(w: PixelDim, h: PixelDim, float: Direction) -> Self {
|
||||
Self { w, h, float }
|
||||
}
|
||||
|
||||
fn crop_dim(&self, img_width: u32, img_height: u32) -> (u32, u32) {
|
||||
let crop_width = match self.w {
|
||||
PixelDim::Pixels(w) => w,
|
||||
PixelDim::WidthPercent(pct) => ((img_width as f32) * pct / 100.0) as u32,
|
||||
PixelDim::HeightPercent(pct) => ((img_height as f32) * pct / 100.0) as u32,
|
||||
};
|
||||
|
||||
let crop_height = match self.h {
|
||||
PixelDim::Pixels(h) => h,
|
||||
PixelDim::WidthPercent(pct) => ((img_width as f32) * pct / 100.0) as u32,
|
||||
PixelDim::HeightPercent(pct) => ((img_height as f32) * pct / 100.0) as u32,
|
||||
};
|
||||
|
||||
(crop_width, crop_height)
|
||||
}
|
||||
|
||||
#[expect(clippy::integer_division)]
|
||||
fn crop_pos(
|
||||
&self,
|
||||
img_width: u32,
|
||||
img_height: u32,
|
||||
crop_width: u32,
|
||||
crop_height: u32,
|
||||
) -> (u32, u32) {
|
||||
match self.float {
|
||||
Direction::North => {
|
||||
let x = (img_width - crop_width) / 2;
|
||||
let y = 0;
|
||||
(x, y)
|
||||
}
|
||||
Direction::East => {
|
||||
let x = img_width - crop_width;
|
||||
let y = (img_height - crop_height) / 2;
|
||||
(x, y)
|
||||
}
|
||||
Direction::South => {
|
||||
let x = (img_width - crop_width) / 2;
|
||||
let y = img_height - crop_height;
|
||||
(x, y)
|
||||
}
|
||||
Direction::West => {
|
||||
let x = 0;
|
||||
let y = (img_height - crop_height) / 2;
|
||||
(x, y)
|
||||
}
|
||||
Direction::Center => {
|
||||
let x = (img_width - crop_width) / 2;
|
||||
let y = (img_height - crop_height) / 2;
|
||||
(x, y)
|
||||
}
|
||||
Direction::NorthEast => {
|
||||
let x = img_width - crop_width;
|
||||
let y = 0;
|
||||
(x, y)
|
||||
}
|
||||
Direction::SouthEast => {
|
||||
let x = img_width - crop_width;
|
||||
let y = img_height - crop_height;
|
||||
(x, y)
|
||||
}
|
||||
Direction::NorthWest => {
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
(x, y)
|
||||
}
|
||||
Direction::SouthWest => {
|
||||
let x = 0;
|
||||
let y = img_height - crop_height;
|
||||
(x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CropTransformer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "crop({},{},{})", self.w, self.h, self.float)
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageTransformer for CropTransformer {
|
||||
fn parse_args(args: &str) -> Result<Self, String> {
|
||||
let args: Vec<&str> = args.split(",").collect();
|
||||
if args.len() != 3 {
|
||||
return Err(format!("expected 3 args, got {}", args.len()));
|
||||
}
|
||||
|
||||
let w = args[0].trim().parse::<PixelDim>()?;
|
||||
let h = args[1].trim().parse::<PixelDim>()?;
|
||||
|
||||
let direction = args[2].trim();
|
||||
let direction = Direction::from_str(direction)
|
||||
.map_err(|_err| format!("invalid direction {direction}"))?;
|
||||
|
||||
Ok(Self {
|
||||
w,
|
||||
h,
|
||||
float: direction,
|
||||
})
|
||||
}
|
||||
|
||||
fn transform(&self, input: &mut DynamicImage) {
|
||||
let (img_width, img_height) = (input.width(), input.height());
|
||||
let (crop_width, crop_height) = self.crop_dim(img_width, img_height);
|
||||
|
||||
if (crop_width < img_width || crop_height < img_height) && crop_width > 0 && crop_height > 0
|
||||
{
|
||||
let (x, y) = self.crop_pos(img_width, img_height, crop_width, crop_height);
|
||||
*input = input.crop(x, y, crop_width, crop_height);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
crates/lib/pixel-transform/src/transformers/maxdim.rs
Normal file
82
crates/lib/pixel-transform/src/transformers/maxdim.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use image::{DynamicImage, imageops::FilterType};
|
||||
|
||||
use crate::{pixeldim::PixelDim, transformers::ImageTransformer};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MaxDimTransformer {
|
||||
w: PixelDim,
|
||||
h: PixelDim,
|
||||
}
|
||||
|
||||
impl MaxDimTransformer {
|
||||
pub fn new(w: PixelDim, h: PixelDim) -> Self {
|
||||
Self { w, h }
|
||||
}
|
||||
|
||||
fn target_dim(&self, img_width: u32, img_height: u32) -> (u32, u32) {
|
||||
let max_width = match self.w {
|
||||
PixelDim::Pixels(w) => Some(w),
|
||||
PixelDim::WidthPercent(pct) => Some(((img_width as f32) * pct / 100.0) as u32),
|
||||
PixelDim::HeightPercent(_) => None,
|
||||
};
|
||||
|
||||
let max_height = match self.h {
|
||||
PixelDim::Pixels(h) => Some(h),
|
||||
PixelDim::HeightPercent(pct) => Some(((img_height as f32) * pct / 100.0) as u32),
|
||||
PixelDim::WidthPercent(_) => None,
|
||||
};
|
||||
|
||||
if max_width.map(|x| img_width <= x).unwrap_or(true)
|
||||
&& max_height.map(|x| img_height <= x).unwrap_or(true)
|
||||
{
|
||||
return (img_width, img_height);
|
||||
}
|
||||
|
||||
let width_ratio = max_width
|
||||
.map(|x| x as f32 / img_width as f32)
|
||||
.unwrap_or(1.0);
|
||||
|
||||
let height_ratio = max_height
|
||||
.map(|x| x as f32 / img_height as f32)
|
||||
.unwrap_or(1.0);
|
||||
|
||||
let ratio = width_ratio.min(height_ratio);
|
||||
|
||||
(
|
||||
(img_width as f32 * ratio) as u32,
|
||||
(img_height as f32 * ratio) as u32,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MaxDimTransformer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "maxdim({},{})", self.w, self.h)
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageTransformer for MaxDimTransformer {
|
||||
fn parse_args(args: &str) -> Result<Self, String> {
|
||||
let args: Vec<&str> = args.split(",").collect();
|
||||
if args.len() != 2 {
|
||||
return Err(format!("expected 2 args, got {}", args.len()));
|
||||
}
|
||||
|
||||
let w = args[0].parse::<PixelDim>()?;
|
||||
let h = args[1].parse::<PixelDim>()?;
|
||||
|
||||
Ok(Self { w, h })
|
||||
}
|
||||
|
||||
fn transform(&self, input: &mut DynamicImage) {
|
||||
let (img_width, img_height) = (input.width(), input.height());
|
||||
let (target_width, target_height) = self.target_dim(img_width, img_height);
|
||||
|
||||
// Only resize if needed
|
||||
if target_width != img_width || target_height != img_height {
|
||||
*input = input.resize(target_width, target_height, FilterType::Lanczos3);
|
||||
}
|
||||
}
|
||||
}
|
||||
165
crates/lib/pixel-transform/src/transformers/mod.rs
Normal file
165
crates/lib/pixel-transform/src/transformers/mod.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use image::{DynamicImage, ImageFormat};
|
||||
use std::fmt;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::str::FromStr;
|
||||
|
||||
mod crop;
|
||||
pub use crop::*;
|
||||
|
||||
mod maxdim;
|
||||
pub use maxdim::*;
|
||||
|
||||
pub trait ImageTransformer
|
||||
where
|
||||
Self: PartialEq,
|
||||
Self: Sized + Clone,
|
||||
Self: Display + Debug,
|
||||
{
|
||||
/// Transform the given image in place
|
||||
fn transform(&self, input: &mut DynamicImage);
|
||||
|
||||
/// Parse an arg string.
|
||||
///
|
||||
/// `name({arg_string})`
|
||||
fn parse_args(args: &str) -> Result<Self, String>;
|
||||
}
|
||||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
/// An enum of all [`ImageTransformer`]s
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum TransformerEnum {
|
||||
/// Usage: `maxdim(w, h)`
|
||||
///
|
||||
/// Scale the image so its width is smaller than `w`
|
||||
/// and its height is smaller than `h`. Aspect ratio is preserved.
|
||||
///
|
||||
/// To only limit the size of one dimension, use `vw` or `vh`.
|
||||
/// For example, `maxdim(50,100vh)` will not limit width.
|
||||
MaxDim(MaxDimTransformer),
|
||||
|
||||
/// Usage: `crop(w, h, float)`
|
||||
///
|
||||
/// Crop the image to at most `w` by `h` pixels,
|
||||
/// floating the crop area in the specified direction.
|
||||
///
|
||||
/// Directions are one of:
|
||||
/// - Cardinal: n,e,s,w
|
||||
/// - Diagonal: ne,nw,se,sw,
|
||||
/// - Centered: c
|
||||
///
|
||||
/// Examples:
|
||||
/// - `crop(100vw, 50)` gets the top 50 pixels of the image \
|
||||
/// (or fewer, if the image's height is smaller than 50)
|
||||
///
|
||||
/// To only limit the size of one dimension, use `vw` or `vh`.
|
||||
/// For example, `maxdim(50,100vh)` will not limit width.
|
||||
Crop(CropTransformer),
|
||||
|
||||
/// Usage: `format(format)`
|
||||
///
|
||||
/// Transcode the image to the given format.
|
||||
/// This step must be last, and cannot be provided
|
||||
/// more than once.
|
||||
///
|
||||
/// Valid formats:
|
||||
/// - bmp
|
||||
/// - gif
|
||||
/// - ico
|
||||
/// - jpeg or jpg
|
||||
/// - png
|
||||
/// - qoi
|
||||
/// - webp
|
||||
///
|
||||
/// Example:
|
||||
/// - `format(png)`
|
||||
///
|
||||
/// When transcoding an animated gif, the first frame is taken
|
||||
/// and all others are thrown away. This happens even if we
|
||||
/// transcode from a gif to a gif.
|
||||
Format { format: ImageFormat },
|
||||
}
|
||||
|
||||
impl FromStr for TransformerEnum {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let s = s.trim();
|
||||
|
||||
let (name, args) = {
|
||||
let name_len = match s.find('(') {
|
||||
Some(x) => x + 1,
|
||||
None => {
|
||||
return Err(format!(
|
||||
"invalid transformation {s}. Must look like name(args)."
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let mut balance = 1;
|
||||
let mut end = name_len;
|
||||
for i in s[name_len..].bytes() {
|
||||
match i {
|
||||
b')' => balance -= 1,
|
||||
b'(' => balance += 1,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if balance == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
end += 1;
|
||||
}
|
||||
|
||||
if balance != 0 {
|
||||
return Err(format!("mismatched parenthesis in {s}"));
|
||||
}
|
||||
|
||||
let name = s[0..name_len - 1].trim();
|
||||
let args = s[name_len..end].trim();
|
||||
let trail = s[end + 1..].trim();
|
||||
if !trail.is_empty() {
|
||||
return Err(format!(
|
||||
"invalid transformation {s}. Must look like name(args)."
|
||||
));
|
||||
}
|
||||
|
||||
(name, args)
|
||||
};
|
||||
|
||||
match name {
|
||||
"maxdim" => Ok(Self::MaxDim(MaxDimTransformer::parse_args(args)?)),
|
||||
"crop" => Ok(Self::Crop(CropTransformer::parse_args(args)?)),
|
||||
|
||||
"format" => Ok(TransformerEnum::Format {
|
||||
format: ImageFormat::from_extension(args)
|
||||
.ok_or(format!("invalid image format {args}"))?,
|
||||
}),
|
||||
|
||||
_ => Err(format!("unknown transformation {name}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TransformerEnum {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
s.parse().map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TransformerEnum {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
TransformerEnum::MaxDim(x) => Display::fmt(x, f),
|
||||
TransformerEnum::Crop(x) => Display::fmt(x, f),
|
||||
TransformerEnum::Format { format } => {
|
||||
write!(f, "format({})", format.extensions_str()[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user