use crate::error::anyhow;
use serde::{Serialize, Serializer};
use serde_repr::*;
use std::fmt;
#[derive(Debug, Clone)]
pub enum ImageOptimizerRegion {
UsEast,
UsCentral,
UsWest,
EuCentral,
EuWest,
Asia,
Australia,
}
impl Serialize for ImageOptimizerRegion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
ImageOptimizerRegion::UsEast => serializer.serialize_str("us_east"),
ImageOptimizerRegion::UsCentral => serializer.serialize_str("us_central"),
ImageOptimizerRegion::UsWest => serializer.serialize_str("us_west"),
ImageOptimizerRegion::EuCentral => serializer.serialize_str("eu_central"),
ImageOptimizerRegion::EuWest => serializer.serialize_str("eu_west"),
ImageOptimizerRegion::Asia => serializer.serialize_str("asia"),
ImageOptimizerRegion::Australia => serializer.serialize_str("australia"),
}
}
}
#[derive(Debug, PartialEq)]
pub enum ImageOptimizerAPIError {
ParameterError(String),
SerializeError,
}
impl ImageOptimizerAPIError {
pub(crate) fn into_anyhow(self) -> anyhow::Error {
match self {
ImageOptimizerAPIError::ParameterError(s) => anyhow!(s),
ImageOptimizerAPIError::SerializeError => anyhow!("failed to serialize api params"),
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum Format {
Auto,
AVIF,
GIF,
JPEG,
JPEGXL,
MP4,
PNG,
WebP,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum Auto {
AVIF,
WebP,
}
#[derive(Debug, Clone)]
pub enum PixelsOrPercentage {
Pixels(u32),
Percentage(f64),
}
impl fmt::Display for PixelsOrPercentage {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PixelsOrPercentage::Pixels(u) => write!(f, "{}", u),
PixelsOrPercentage::Percentage(p) => write!(f, "{}p", fmt_float(*p)),
}
}
}
impl Serialize for PixelsOrPercentage {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
PixelsOrPercentage::Pixels(u) => serializer.serialize_u32(*u),
PixelsOrPercentage::Percentage(p) => {
let val = format!("{}p", fmt_float(*p));
serializer.serialize_str(&val)
}
}
}
}
fn fmt_float(v: f64) -> String {
let nearest_three: f64 = f64::round(1000.0 * v) / 1000.0;
if nearest_three.fract() > 0.0 {
format!("{:?}", nearest_three)
} else {
format!("{:?}", v as i32)
}
}
#[derive(Debug, Clone)]
pub enum CropMode {
Safe = 1,
Smart = 2,
}
#[derive(Debug, Clone)]
pub enum Area {
AspectRatio((u32, u32)),
WidthHeight((PixelsOrPercentage, PixelsOrPercentage)),
}
impl fmt::Display for Area {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Area::AspectRatio((w, h)) => {
write!(fmt, "{}:{}", w, h)
}
Area::WidthHeight((w, h)) => {
write!(fmt, "{},{}", w, h)
}
}
}
}
#[derive(Debug, Clone, Serialize)]
pub enum PointOrOffset {
Point(PixelsOrPercentage),
Offset(u32), }
impl fmt::Display for PointOrOffset {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
PointOrOffset::Point(p) => {
write!(fmt, "{}", p)
}
PointOrOffset::Offset(u) => {
write!(fmt, "{}p", u)
}
}
}
}
#[derive(Debug, Clone)]
pub struct Position {
pub x: Option<PointOrOffset>,
pub y: Option<PointOrOffset>,
}
impl fmt::Display for Position {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fn write_point_or_offset(
fmt: &mut fmt::Formatter,
axis: &str,
p: &PointOrOffset,
) -> fmt::Result {
match p {
PointOrOffset::Point(p) => {
write!(fmt, "{}{}", axis, p)
}
PointOrOffset::Offset(f) => {
write!(fmt, "offset-{}{}", axis, f)
}
}
}
let mut wrote_x = false;
if let Some(x) = &self.x {
wrote_x = true;
write_point_or_offset(fmt, "x", x)?;
}
if let Some(y) = &self.y {
if wrote_x {
write!(fmt, ",")?;
}
write_point_or_offset(fmt, "y", y)?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Crop {
pub size: Area,
pub position: Option<Position>,
pub mode: Option<CropMode>,
}
impl Serialize for Crop {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut crop_str = format!("{}", self.size);
if let Some(pos) = &self.position {
let pos_str = format!(",{}", pos);
crop_str.push_str(&pos_str);
}
if let Some(mode) = &self.mode {
match mode {
CropMode::Safe => crop_str.push_str(",safe"),
CropMode::Smart => crop_str.push_str(",smart"),
};
}
serializer.serialize_str(&crop_str)
}
}
#[derive(Debug, Clone)]
pub struct Sides {
pub top: PixelsOrPercentage,
pub right: PixelsOrPercentage,
pub bottom: PixelsOrPercentage,
pub left: PixelsOrPercentage,
}
impl Serialize for Sides {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let sides_str = format!("{},{},{},{}", self.top, self.right, self.bottom, self.left);
serializer.serialize_str(&sides_str)
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum OptimizeLevel {
Low = 1,
Medium = 2,
High = 3,
}
#[derive(Debug, Clone, Serialize_repr)]
#[repr(u8)]
pub enum Orientation {
Default = 1,
FlipHorizontal = 2,
FlipHorizontalAndVertical = 3,
FlipVertical = 4,
FlipHorizontalOrientLeft = 5,
OrientRight = 6,
FlipHorizontalOrientRight = 7,
OrientLeft = 8,
}
#[derive(Debug, Clone)]
pub struct HexColor {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: f32,
}
impl Serialize for HexColor {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let rgba_str = format!("{},{},{},{:?}", self.r, self.g, self.b, self.a);
serializer.serialize_str(&rgba_str)
}
}
#[derive(Debug, Clone)]
pub struct TrimColor {
pub color: HexColor,
pub threshold: f32,
}
impl Serialize for TrimColor {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let trim_color_str = format!(
"{},{},{},{:?},t{}",
self.color.r,
self.color.g,
self.color.b,
self.color.a,
fmt_float(self.threshold.into())
);
serializer.serialize_str(&trim_color_str)
}
}
#[derive(Debug, Clone)]
pub enum BWMode {
Atkinson,
DefaultThreshold,
Threshold(u32),
}
impl Serialize for BWMode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
BWMode::Atkinson => serializer.serialize_str("atkinson"),
BWMode::DefaultThreshold => serializer.serialize_str("threshold"),
BWMode::Threshold(u) => {
let threshold_str = format!("threshold,{}", u);
serializer.serialize_str(&threshold_str)
}
}
}
}
#[derive(Debug, Clone)]
pub enum BlurMode {
Pixels(f64),
Percentage(f64),
}
impl Serialize for BlurMode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
BlurMode::Pixels(p) => serializer.serialize_f64(*p),
BlurMode::Percentage(p) => {
let percent_str = format!("{}p", p);
serializer.serialize_str(&percent_str)
}
}
}
}
#[derive(Debug, Clone)]
pub struct Canvas {
pub size: Area,
pub position: Option<Position>,
}
impl Serialize for Canvas {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut canvas_str = format!("{}", self.size);
if let Some(p) = &self.position {
let pos_str = format!(",{}", p);
canvas_str.push_str(&pos_str);
}
serializer.serialize_str(&canvas_str)
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum Fit {
Bounds = 1,
Cover = 2,
Crop = 3,
}
#[derive(Debug, Clone)]
pub enum Level {
Level1_0,
Level1_1,
Level1_2,
Level1_3,
Level2_0,
Level2_1,
Level2_2,
Level3_0,
Level3_1,
Level3_2,
Level4_0,
Level4_1,
Level4_2,
Level5_0,
Level5_1,
Level5_2,
Level6_0,
Level6_1,
Level6_2,
}
impl Serialize for Level {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Level::Level1_0 => serializer.serialize_str("1.0"),
Level::Level1_1 => serializer.serialize_str("1.1"),
Level::Level1_2 => serializer.serialize_str("1.2"),
Level::Level1_3 => serializer.serialize_str("1.3"),
Level::Level2_0 => serializer.serialize_str("2.0"),
Level::Level2_1 => serializer.serialize_str("2.1"),
Level::Level2_2 => serializer.serialize_str("2.2"),
Level::Level3_0 => serializer.serialize_str("3.0"),
Level::Level3_1 => serializer.serialize_str("3.1"),
Level::Level3_2 => serializer.serialize_str("3.2"),
Level::Level4_0 => serializer.serialize_str("4.0"),
Level::Level4_1 => serializer.serialize_str("4.1"),
Level::Level4_2 => serializer.serialize_str("4.2"),
Level::Level5_0 => serializer.serialize_str("5.0"),
Level::Level5_1 => serializer.serialize_str("5.1"),
Level::Level5_2 => serializer.serialize_str("5.2"),
Level::Level6_0 => serializer.serialize_str("6.0"),
Level::Level6_1 => serializer.serialize_str("6.1"),
Level::Level6_2 => serializer.serialize_str("6.2"),
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum Metadata {
Copyright,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum Profile {
Baseline,
Main,
High,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum ResizeAlgorithm {
Nearest,
Bilinear,
Bicubic,
Lanczos2,
Lanczos3,
}
#[derive(Debug, Clone)]
pub struct Sharpen {
pub amount: u8,
pub radius: f32,
pub threshold: u8,
}
impl Serialize for Sharpen {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let sharpen_str = format!(
"a{},r{},t{}",
self.amount,
fmt_float(self.radius.into()),
self.threshold
);
serializer.serialize_str(&sharpen_str)
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum EnableOpt {
Upscale,
}
#[derive(Debug, Default, Clone, Serialize)]
#[non_exhaustive]
pub struct ImageOptimizerOptions {
pub region: Option<ImageOptimizerRegion>,
pub preserve_query_string_on_origin_request: Option<bool>,
pub auto: Option<Auto>,
#[serde(rename = "bg-color")]
pub bg_color: Option<HexColor>,
pub blur: Option<BlurMode>,
pub brightness: Option<i32>, pub bw: Option<BWMode>,
pub canvas: Option<Canvas>,
pub contrast: Option<i32>, pub crop: Option<Crop>,
pub dpr: Option<f32>,
pub enable: Option<EnableOpt>,
pub fit: Option<Fit>,
pub format: Option<Format>,
pub frame: Option<u32>,
pub height: Option<PixelsOrPercentage>,
pub level: Option<Level>,
pub metadata: Option<Metadata>,
pub optimize: Option<OptimizeLevel>,
pub orient: Option<Orientation>,
pub pad: Option<Sides>,
pub precrop: Option<Crop>,
pub profile: Option<Profile>,
pub quality: Option<u32>,
#[serde(rename = "resize-filter")]
pub resize_filter: Option<ResizeAlgorithm>,
pub saturation: Option<i32>, pub sharpen: Option<Sharpen>,
pub trim: Option<Sides>,
#[serde(rename = "trim-color")]
pub trim_color: Option<TrimColor>,
pub width: Option<PixelsOrPercentage>,
}
impl ImageOptimizerOptions {
pub fn from_region(region: ImageOptimizerRegion) -> Self {
ImageOptimizerOptions {
region: Some(region),
..Default::default()
}
}
pub(crate) fn to_claims_opts(&self) -> Result<String, ImageOptimizerAPIError> {
self.validate_params()?;
serde_urlencoded::to_string(self).map_err(|_| ImageOptimizerAPIError::SerializeError)
}
fn validate_params(&self) -> Result<(), ImageOptimizerAPIError> {
if self.region.is_none() {
return Err(ImageOptimizerAPIError::ParameterError(
"region parameter on ImageOptimizerOptions must be set".to_string(),
));
}
if let Some(bg) = &self.bg_color {
if bg.a < 0.0 || bg.a > 1.0 {
return Err(ImageOptimizerAPIError::ParameterError(
"alpha must be between 0 and 1, inclusive".to_string(),
));
}
}
if let Some(b) = self.brightness {
if b < -100 || b > 100 {
return Err(ImageOptimizerAPIError::ParameterError(
"brightness must be between -100 and 100, inclusive".to_string(),
));
}
}
if let Some(c) = self.contrast {
if c < -100 || c > 100 {
return Err(ImageOptimizerAPIError::ParameterError(
"contrast must be between -100 and 100, inclusive".to_string(),
));
}
}
if let Some(dpr) = self.dpr {
if dpr < 0.0 || dpr > 10.0 {
return Err(ImageOptimizerAPIError::ParameterError(
"dpr must be between 0 and 10, inclusive".to_string(),
));
}
}
if let Some(q) = self.quality {
if q > 100 {
return Err(ImageOptimizerAPIError::ParameterError(
"quality must be between 0 and 100, inclusive".to_string(),
));
}
}
if let Some(s) = self.saturation {
if s < -100 || s > 100 {
return Err(ImageOptimizerAPIError::ParameterError(
"saturation must be between -100 and 100, inclusive".to_string(),
));
}
}
if let Some(s) = &self.sharpen {
if s.amount > 10 {
return Err(ImageOptimizerAPIError::ParameterError(
"sharpen amount must be between 0 and 10, inclusive".to_string(),
));
}
if s.radius < 0.5 || s.radius > 1000.0 {
return Err(ImageOptimizerAPIError::ParameterError(
"sharpen radius must be between 0.5 and 1000, inclusive".to_string(),
));
}
}
if let Some(tc) = &self.trim_color {
if tc.color.a < 0.0 || tc.color.a > 1.0 {
return Err(ImageOptimizerAPIError::ParameterError(
"alpha must be between 0 and 1, inclusive".to_string(),
));
}
if tc.threshold < 0.0 || tc.threshold > 1.0 {
return Err(ImageOptimizerAPIError::ParameterError(
"threshold must be between 0 and 1, inclusive".to_string(),
));
}
}
return Ok(());
}
}
#[cfg(test)]
mod image_optimizer_options_test {
use super::*;
fn test_query_params(options: &ImageOptimizerOptions, expected: &str) {
match serde_urlencoded::to_string(options) {
Ok(qp) => assert_eq!(qp, expected),
Err(e) => panic!("got unexpected error: {e}"),
}
}
#[test]
fn query_params_are_valid() {
let mut options = ImageOptimizerOptions::from_region(ImageOptimizerRegion::UsEast);
test_query_params(&options, "region=us_east");
options.width = Some(PixelsOrPercentage::Percentage(50.0));
test_query_params(&options, "region=us_east&width=50p");
options.width = None;
options.auto = Some(Auto::WebP);
test_query_params(&options, "region=us_east&auto=webp");
options.auto = None;
options.bg_color = Some(HexColor {
r: 0,
g: 255,
b: 0,
a: 0.3,
});
test_query_params(&options, "region=us_east&bg-color=0%2C255%2C0%2C0.3");
options.bg_color = None;
options.blur = Some(BlurMode::Pixels(50.0));
test_query_params(&options, "region=us_east&blur=50.0");
options.blur = Some(BlurMode::Percentage(0.8));
test_query_params(&options, "region=us_east&blur=0.8p");
options.blur = None;
options.brightness = Some(-50);
test_query_params(&options, "region=us_east&brightness=-50");
options.brightness = None;
options.bw = Some(BWMode::Threshold(10));
test_query_params(&options, "region=us_east&bw=threshold%2C10");
options.bw = None;
options.contrast = Some(-5);
test_query_params(&options, "region=us_east&contrast=-5");
options.contrast = None;
options.dpr = Some(3.2);
test_query_params(&options, "region=us_east&dpr=3.2");
options.dpr = None;
options.enable = Some(EnableOpt::Upscale);
test_query_params(&options, "region=us_east&enable=upscale");
options.enable = None;
options.format = Some(Format::JPEGXL);
test_query_params(&options, "region=us_east&format=jpegxl");
options.format = None;
options.frame = Some(1);
test_query_params(&options, "region=us_east&frame=1");
options.frame = None;
options.height = Some(PixelsOrPercentage::Percentage(80.0));
test_query_params(&options, "region=us_east&height=80p");
options.height = None;
options.level = Some(Level::Level2_0);
options.format = Some(Format::MP4);
options.profile = Some(Profile::High);
test_query_params(&options, "region=us_east&format=mp4&level=2.0&profile=high");
options.level = None;
options.format = None;
options.profile = None;
options.metadata = Some(Metadata::Copyright);
test_query_params(&options, "region=us_east&metadata=copyright");
options.metadata = None;
options.optimize = Some(OptimizeLevel::High);
test_query_params(&options, "region=us_east&optimize=high");
options.optimize = None;
options.orient = Some(Orientation::FlipVertical);
test_query_params(&options, "region=us_east&orient=4");
options.orient = None;
options.pad = Some(Sides {
top: PixelsOrPercentage::Percentage(10.0),
right: PixelsOrPercentage::Percentage(10.0),
bottom: PixelsOrPercentage::Percentage(10.0),
left: PixelsOrPercentage::Percentage(10.0),
});
test_query_params(&options, "region=us_east&pad=10p%2C10p%2C10p%2C10p");
options.pad = None;
options.resize_filter = Some(ResizeAlgorithm::Lanczos3);
test_query_params(&options, "region=us_east&resize-filter=lanczos3");
options.resize_filter = None;
options.sharpen = Some(Sharpen {
amount: 5,
radius: 2.0,
threshold: 1,
});
test_query_params(&options, "region=us_east&sharpen=a5%2Cr2%2Ct1");
options.sharpen = None;
options.trim = Some(Sides {
top: PixelsOrPercentage::Percentage(20.5555),
right: PixelsOrPercentage::Percentage(33.3333),
bottom: PixelsOrPercentage::Percentage(20.555),
left: PixelsOrPercentage::Percentage(33.3333),
});
test_query_params(
&options,
"region=us_east&trim=20.556p%2C33.333p%2C20.555p%2C33.333p",
);
options.trim = None;
options.trim_color = Some(TrimColor {
color: HexColor {
r: 255,
g: 0,
b: 0,
a: 1.0,
},
threshold: 0.5,
});
test_query_params(
&options,
"region=us_east&trim-color=255%2C0%2C0%2C1.0%2Ct0.5",
);
}
#[test]
fn canvas_to_string_is_valid() {
let mut options = ImageOptimizerOptions::from_region(ImageOptimizerRegion::UsEast);
let mut canvas = Canvas {
size: Area::WidthHeight((
PixelsOrPercentage::Pixels(200),
PixelsOrPercentage::Pixels(200),
)),
position: None,
};
options.canvas = Some(canvas.clone());
test_query_params(&options, "region=us_east&canvas=200%2C200");
let mut test_position = Position {
x: Some(PointOrOffset::Point(PixelsOrPercentage::Percentage(50.0))),
y: Some(PointOrOffset::Point(PixelsOrPercentage::Percentage(50.0))),
};
canvas.position = Some(test_position);
options.canvas = Some(canvas.clone());
test_query_params(&options, "region=us_east&canvas=200%2C200%2Cx50p%2Cy50p");
test_position = Position {
x: Some(PointOrOffset::Offset(30)),
y: Some(PointOrOffset::Offset(20)),
};
canvas.position = Some(test_position);
options.canvas = Some(canvas.clone());
test_query_params(
&options,
"region=us_east&canvas=200%2C200%2Coffset-x30%2Coffset-y20",
);
canvas.size = Area::AspectRatio((16, 9));
options.canvas = Some(canvas.clone());
test_query_params(
&options,
"region=us_east&canvas=16%3A9%2Coffset-x30%2Coffset-y20",
);
}
#[test]
fn crop_to_string_is_valid() {
let mut options = ImageOptimizerOptions::from_region(ImageOptimizerRegion::UsEast);
let mut crop = Crop {
size: Area::AspectRatio((1, 1)),
position: None,
mode: None,
};
options.crop = Some(crop.clone());
test_query_params(&options, "region=us_east&crop=1%3A1");
crop.mode = Some(CropMode::Safe);
options.crop = Some(crop.clone());
test_query_params(&options, "region=us_east&crop=1%3A1%2Csafe");
crop.position = Some(Position {
x: Some(PointOrOffset::Point(PixelsOrPercentage::Pixels(30))),
y: None,
});
options.crop = Some(crop.clone());
test_query_params(&options, "region=us_east&crop=1%3A1%2Cx30%2Csafe");
crop.position = Some(Position {
x: Some(PointOrOffset::Point(PixelsOrPercentage::Percentage(30.0))),
y: Some(PointOrOffset::Point(PixelsOrPercentage::Percentage(20.0))),
});
options.crop = Some(crop.clone());
test_query_params(&options, "region=us_east&crop=1%3A1%2Cx30p%2Cy20p%2Csafe");
crop.position = Some(Position {
x: Some(PointOrOffset::Point(PixelsOrPercentage::Pixels(30))),
y: Some(PointOrOffset::Point(PixelsOrPercentage::Percentage(20.0))),
});
options.crop = Some(crop.clone());
test_query_params(&options, "region=us_east&crop=1%3A1%2Cx30%2Cy20p%2Csafe");
crop.position = Some(Position {
x: Some(PointOrOffset::Offset(30)),
y: None,
});
options.crop = Some(crop.clone());
test_query_params(&options, "region=us_east&crop=1%3A1%2Coffset-x30%2Csafe");
crop.position = Some(Position {
x: Some(PointOrOffset::Offset(30)),
y: Some(PointOrOffset::Offset(15)),
});
options.crop = Some(crop.clone());
test_query_params(
&options,
"region=us_east&crop=1%3A1%2Coffset-x30%2Coffset-y15%2Csafe",
);
options.fit = Some(Fit::Bounds);
test_query_params(
&options,
"region=us_east&crop=1%3A1%2Coffset-x30%2Coffset-y15%2Csafe&fit=bounds",
);
options.fit = None;
options.crop = None;
options.precrop = Some(crop);
test_query_params(
&options,
"region=us_east&precrop=1%3A1%2Coffset-x30%2Coffset-y15%2Csafe",
);
}
#[test]
fn invalid_params() {
let mut options = ImageOptimizerOptions::default();
match options.validate_params() {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(
e,
ImageOptimizerAPIError::ParameterError(
"region parameter on ImageOptimizerOptions must be set".to_string()
)
),
}
options.region = Some(ImageOptimizerRegion::UsEast);
options.sharpen = Some(Sharpen {
amount: 5,
radius: 0.2,
threshold: 1,
});
match options.validate_params() {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(
e,
ImageOptimizerAPIError::ParameterError(
"sharpen radius must be between 0.5 and 1000, inclusive".to_string()
)
),
}
options.sharpen = None;
options.dpr = Some(11.0);
match options.validate_params() {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(
e,
ImageOptimizerAPIError::ParameterError(
"dpr must be between 0 and 10, inclusive".to_string()
)
),
}
}
}