diff --git a/imageflow_core/src/graphics/rounded_corners.rs b/imageflow_core/src/graphics/rounded_corners.rs index 865ef389b..331f14830 100644 --- a/imageflow_core/src/graphics/rounded_corners.rs +++ b/imageflow_core/src/graphics/rounded_corners.rs @@ -1,26 +1,42 @@ +use imageflow_types::{Color, RoundCornersMode}; use crate::graphics::prelude::*; + + +fn get_radius_pixels(radius: RoundCornersMode, w: u32, h: u32) -> Result{ + match radius{ + RoundCornersMode::Percentage(p) => Ok(w.min(h) as f32 * p / 200f32), + RoundCornersMode::Pixels(p) => Ok(p), + RoundCornersMode::Circle => Err(unimpl!("RoundCornersMode::Circle is not implemented")), + RoundCornersMode::PercentageCustom {.. } => Err(unimpl!("RoundCornersMode::PercentageCustom is not implemented")), + RoundCornersMode::PixelsCustom {.. } => Err(unimpl!("RoundCornersMode::PixelsCustom is not implemented")) + } +} + pub unsafe fn flow_bitmap_bgra_clear_around_rounded_corners( b: &mut BitmapWindowMut, - radius: u32, + radius_mode: RoundCornersMode, color: imageflow_types::Color ) -> Result<(), FlowError> { if b.info().pixel_layout() != PixelLayout::BGRA { return Err(nerror!(ErrorKind::InvalidArgument)); } + let radius = get_radius_pixels(radius_mode, b.w(), b.h())?; + let radius_ceil = radius.ceil() as usize; + let rf = radius as f32; let r2f = rf * rf; - let mut clear_widths = Vec::with_capacity(radius as usize); - for y in (0..=radius).rev(){ + let mut clear_widths = Vec::with_capacity(radius_ceil); + for y in (0..=radius_ceil).rev(){ let yf = y as f32 - 0.5; - clear_widths.push((radius - f32::sqrt(r2f - yf * yf).round() as u32) as usize); + clear_widths.push(radius_ceil - f32::sqrt(r2f - yf * yf).round() as usize); } let bgcolor = color.to_color_32().unwrap().to_bgra8(); - let radius_usize = radius as usize; + let radius_usize = radius_ceil; let width = b.w() as usize; let height = b.h() as usize; diff --git a/imageflow_core/tests/visuals.rs b/imageflow_core/tests/visuals.rs index 765569c86..bbca3fca6 100644 --- a/imageflow_core/tests/visuals.rs +++ b/imageflow_core/tests/visuals.rs @@ -11,7 +11,7 @@ use crate::common::*; use imageflow_types; use imageflow_core::{Context, ErrorKind, FlowError, CodeLocation}; -use imageflow_types::{PixelFormat, Color, Node, ColorSrgb, EncoderPreset, ResampleHints, Filter, CommandStringKind, ConstraintMode, Constraint, PngBitDepth, PixelLayout}; +use imageflow_types::{PixelFormat, Color, Node, ColorSrgb, EncoderPreset, ResampleHints, Filter, CommandStringKind, ConstraintMode, Constraint, PngBitDepth, PixelLayout, RoundCornersMode}; use imageflow_core::graphics::bitmaps::{BitmapCompositing, ColorSpace}; @@ -272,7 +272,7 @@ fn test_round_corners_large(){ let blue = Color::Srgb(ColorSrgb::Hex("0000FFFF".to_owned())); let matched = compare(None, 1, "RoundCornersLarge", POPULATE_CHECKSUMS, DEBUG_GRAPH, vec![ Node::CreateCanvas {w: 400, h: 400, format: PixelFormat::Bgra32, color: Color::Srgb(ColorSrgb::Hex("FFFF00FF".to_owned()))}, - Node::RoundImageCorners { background_color: blue, radius: 200} + Node::RoundImageCorners { background_color: blue, radius: RoundCornersMode::Pixels(200f32)} ] ); assert!(matched); @@ -284,7 +284,7 @@ fn test_round_corners_small(){ let blue = Color::Srgb(ColorSrgb::Hex("0000FFFF".to_owned())); let matched = compare(None, 1, "RoundCornersSmall", POPULATE_CHECKSUMS, DEBUG_GRAPH, vec![ Node::CreateCanvas {w: 100, h: 100, format: PixelFormat::Bgra32, color: Color::Srgb(ColorSrgb::Hex("FFFF00FF".to_owned()))}, - Node::RoundImageCorners { background_color: blue, radius: 5} + Node::RoundImageCorners { background_color: blue, radius: RoundCornersMode::Pixels(5f32)} ] ); assert!(matched); @@ -297,7 +297,7 @@ fn test_round_corners_excessive_radius(){ let blue = Color::Srgb(ColorSrgb::Hex("0000FFFF".to_owned())); let matched = compare(None, 1, "RoundCornersExcessiveRadius", POPULATE_CHECKSUMS, DEBUG_GRAPH, vec![ Node::CreateCanvas {w: 200, h: 150, format: PixelFormat::Bgra32, color: Color::Srgb(ColorSrgb::Hex("FFFF00FF".to_owned()))}, - Node::RoundImageCorners { background_color: blue, radius: 100} + Node::RoundImageCorners { background_color: blue, radius: RoundCornersMode::Pixels(100f32)} ] ); assert!(matched); @@ -310,7 +310,7 @@ fn test_round_image_corners_transparent() { "RoundImageCornersTransparent", POPULATE_CHECKSUMS, DEBUG_GRAPH, vec![ Node::Decode {io_id: 0, commands: None}, Node::Resample2D{ w: 400, h: 300, hints: Some(ResampleHints::new().with_bi_filter(Filter::Robidoux)) }, - Node::RoundImageCorners { background_color: Color::Transparent, radius: 100} + Node::RoundImageCorners { background_color: Color::Transparent, radius: RoundCornersMode::Pixels(100f32)} ] ); assert!(matched); @@ -656,6 +656,21 @@ fn test_rot_90_and_red_dot_command_string() { assert!(matched); } +#[test] +fn test_round_corners_command_string() { + let url = "https://s3-us-west-2.amazonaws.com/imageflow-resources/test_inputs/orientation/Landscape_1.jpg".to_owned(); + let title = "test_round_corners_command_string".to_owned(); + let matched = compare(Some(IoTestEnum::Url(url)), 500, &title, POPULATE_CHECKSUMS, DEBUG_GRAPH, + vec![Node::CommandString { + kind: CommandStringKind::ImageResizer4, + value: "w=70&h=70&s.roundcorners=100".to_string(), + decode: Some(0), + encode: None, + watermarks: None + }]); + assert!(matched); +} + #[test] fn test_jpeg_rotation() { diff --git a/imageflow_core/tests/visuals/checksums.json b/imageflow_core/tests/visuals/checksums.json index 35a574acb..05f2c22c0 100644 --- a/imageflow_core/tests/visuals/checksums.json +++ b/imageflow_core/tests/visuals/checksums.json @@ -56,6 +56,7 @@ "test rotate jpeg 90 degrees": "0285B89DDE072042D_0AE4839D1D9B04C57", "test_rot_90_and_red_dot": "0176FBE641002F3ED_0AE4839D1D9B04C57", "test_rot_90_and_red_dot_command_string": "0C790C6600AFBBADA_0AE4839D1D9B04C57", + "test_round_corners_command_string": "0961D137E68D8D1AB_0BF80F0AE71CD9A63", "transparent_png_to_jpeg": "0DC709F50C5148224.jpg", "transparent_png_to_jpeg_constrained": "0D6B7D34193494A6C.jpg", "transparent_trim_whitespace": "0FBC6A5C3930ADEF7.png", diff --git a/imageflow_riapi/src/ir4/layout.rs b/imageflow_riapi/src/ir4/layout.rs index 7c66433c1..afa6bbd25 100644 --- a/imageflow_riapi/src/ir4/layout.rs +++ b/imageflow_riapi/src/ir4/layout.rs @@ -4,7 +4,7 @@ use imageflow_types as s; use crate::sizing; use crate::sizing::prelude::*; use crate::ir4::parsing::*; -use imageflow_types::{ConstraintMode, ConstraintGravity, WatermarkConstraintBox}; +use imageflow_types::{ConstraintMode, ConstraintGravity, WatermarkConstraintBox, RoundCornersMode}; pub struct Ir4Layout{ @@ -444,6 +444,15 @@ impl Ir4Layout{ }) }); + if let Some(quadrants) = self.i.s_round_corners{ + if let Some(all) = iter_all_eq(quadrants){ + b.add(s::Node::RoundImageCorners { radius: RoundCornersMode::Percentage(all as f32), background_color: bgcolor.clone() }) + }else{ + b.add(s::Node::RoundImageCorners { radius: + RoundCornersMode::PercentageCustom {top_left: quadrants[0] as f32, top_right: quadrants[1] as f32, bottom_right: quadrants[2] as f32, bottom_left: quadrants[3] as f32}, + background_color: bgcolor.clone() }) + } + } // Perform white balance @@ -452,6 +461,13 @@ impl Ir4Layout{ threshold: None }); } + // TODO: Decide if we should match ImageResizer order of operations below + // if (!string.IsNullOrEmpty(alpha) && double.TryParse(alpha, ParseUtils.FloatingPointStyle, NumberFormatInfo.InvariantInfo, out temp)) filters.Add(Alpha((float)temp)); + // if (!string.IsNullOrEmpty(brightness) && double.TryParse(brightness, ParseUtils.FloatingPointStyle, NumberFormatInfo.InvariantInfo, out temp)) filters.Add(Brightness((float)temp)); + // if (!string.IsNullOrEmpty(contrast) && double.TryParse(contrast, ParseUtils.FloatingPointStyle, NumberFormatInfo.InvariantInfo, out temp)) filters.Add(Contrast((float)temp)); + // if (!string.IsNullOrEmpty(saturation) && double.TryParse(saturation, ParseUtils.FloatingPointStyle, NumberFormatInfo.InvariantInfo, out temp)) filters.Add(Saturation((float)temp)); + // + if let Some(c) = self.i.s_contrast { b.add(s::Node::ColorFilterSrgb(s::ColorFilterSrgb::Contrast(c as f32))); @@ -498,7 +514,7 @@ impl Ir4Layout{ //Add padding. This may need to be revisited - how do jpegs behave with transparent padding? if left > 0 || top > 0 || right > 0 || bottom > 0 { if left >= 0 && top >= 0 && right >= 0 && bottom >= 0 { - b.add(s::Node::ExpandCanvas { color: bgcolor, left: left as u32, top: top as u32, right: right as u32, bottom: bottom as u32 }); + b.add(s::Node::ExpandCanvas { color: bgcolor.clone(), left: left as u32, top: top as u32, right: right as u32, bottom: bottom as u32 }); } else { panic!("Negative padding showed up: {},{},{},{}", left, top, right, bottom); } @@ -520,6 +536,7 @@ impl Ir4Layout{ b.add_rotate(self.i.rotate); b.add_flip(self.i.flip); + //We apply red dot watermarking after rotate/flip unlike imageresizer if self.i.watermark_red_dot == Some(true){ b.add(s::Node::WatermarkRedDot); diff --git a/imageflow_riapi/src/ir4/parsing.rs b/imageflow_riapi/src/ir4/parsing.rs index eeed42ff1..fa67913b1 100644 --- a/imageflow_riapi/src/ir4/parsing.rs +++ b/imageflow_riapi/src/ir4/parsing.rs @@ -270,6 +270,12 @@ impl std::fmt::Display for Instructions{ } } +pub(crate) fn iter_all_eq(iter: impl IntoIterator) -> Option { + let mut iter = iter.into_iter(); + let first = iter.next()?; + iter.all(|elem| elem == first).then(|| first) +} + impl Instructions{ pub fn to_string(&self) -> String{ @@ -292,6 +298,7 @@ impl Instructions{ s } + pub fn to_map(&self) -> HashMap<&'static str,String>{ let mut m = HashMap::new(); fn add(m: &mut HashMap<&'static str,String>, key: &'static str, value: Option) where T: std::fmt::Display{ @@ -351,6 +358,14 @@ impl Instructions{ add(&mut m, "trim.percentpadding", self.trim_whitespace_padding_percent); add(&mut m, "trim.threshold", self.trim_whitespace_threshold); + add(&mut m, "s.roundcorners", self.s_round_corners.map(|a| + if let Some(v) = iter_all_eq(a.iter()){ + format!("{}", v) + }else { + format!("{},{},{},{}", a[0], a[1], a[2], a[3]) + } + )); + add(&mut m, "crop", self.crop.map(|a| format!("{},{},{},{}", a[0],a[1],a[2],a[3]))); add(&mut m, "anchor", self.anchor_string()); @@ -400,6 +415,9 @@ impl Instructions{ i.ignoreicc = p.parse_bool("ignoreicc"); i.ignore_icc_errors = p.parse_bool("ignore_icc_errors"); i.crop = p.parse_crop_strict("crop").or_else(|| p.parse_crop("crop")); + + i.s_round_corners = p.parse_round_corners("s.roundcorners"); + i.cropxunits = p.parse_f64("cropxunits"); i.cropyunits = p.parse_f64("cropyunits"); i.quality = p.parse_i32("quality").or_else(||p.parse_i32("jpeg.quality")); @@ -568,6 +586,25 @@ impl<'a> Parser<'a>{ } + fn parse_round_corners(&mut self, key: &'static str) -> Option<[f64;4]> { + self.warning_parse(key, |s| { + let values = s.split(',').map(|v| v.trim().parse::()).collect::>>(); + if let Some(&Err(ref e)) = values.iter().find(|v| v.is_err()) { + Err(ParseRoundCornersError::InvalidNumber(e.clone())) + } else if values.len() == 4{ + Ok(([*values[0].as_ref().unwrap(), *values[1].as_ref().unwrap(), *values[2].as_ref().unwrap(), *values[3].as_ref().unwrap()], None, true)) + } else if values.len() == 1{ + let v = *values[0].as_ref().unwrap(); + Ok(([v,v,v,v], None, true)) + } else{ + Err(ParseRoundCornersError::InvalidNumberOfValues("s.roundcorners must contain exactly 1 value or 4 values, separated by commas")) + } + + } + ) + } + + fn parse_bool(&mut self, key: &'static str) -> Option{ self.parse(key, |s| match s.to_lowercase().as_str(){ @@ -751,6 +788,12 @@ enum ParseCropError{ InvalidNumberOfValues(&'static str) } +#[derive(Debug,Clone,PartialEq)] +enum ParseRoundCornersError{ + InvalidNumber(std::num::ParseFloatError), + InvalidNumberOfValues(&'static str) +} + impl OutputFormatStrings{ pub fn clean(&self) -> OutputFormat{ match *self{ @@ -861,6 +904,7 @@ pub struct Instructions{ pub ignoreicc: Option, pub ignore_icc_errors: Option, pub crop: Option<[f64;4]>, + pub s_round_corners: Option<[f64;4]>, pub cropxunits: Option, pub cropyunits: Option, pub zoom: Option, @@ -964,8 +1008,10 @@ fn test_url_parsing() { let _ = write!(::std::io::stderr(), "Expected bgcolor={}, actual={}\n", expected.bgcolor_srgb.unwrap().to_aarrggbb_string(), i.bgcolor_srgb.unwrap().to_aarrggbb_string()); } debug_diff(&i, &expected); + assert_eq!(i, expected); assert_eq!(warns, expected_warnings); + } fn expect_warning(key: &'static str, value: &str, expected: Instructions){ let mut expect_warnings = Vec::new(); @@ -1055,6 +1101,11 @@ fn test_url_parsing() { t("crop=0,0,40,50", Instructions { crop: Some([0f64,0f64,40f64,50f64]), ..Default::default() }, vec![]); t("crop= 0, 0,40 , 50", Instructions { crop: Some([0f64,0f64,40f64,50f64]), ..Default::default() }, vec![]); + + + t("s.roundcorners= 0, 0,40 , 50", Instructions { s_round_corners: Some([0f64,0f64,40f64,50f64]), ..Default::default() }, vec![]); + t("s.roundcorners= 100", Instructions { s_round_corners: Some([100f64,100f64,100f64,100f64]), ..Default::default() }, vec![]); + t("a.balancewhite=true", Instructions{a_balance_white: Some(HistogramThresholdAlgorithm::Area), ..Default::default()}, vec![]); t("a.balancewhite=area", Instructions{a_balance_white: Some(HistogramThresholdAlgorithm::Area), ..Default::default()}, vec![]); t("down.colorspace=linear", Instructions{down_colorspace: Some(ScalingColorspace::Linear), ..Default::default()}, vec![]); @@ -1114,6 +1165,10 @@ fn test_tostr(){ t("down.colorspace=linear", Instructions{down_colorspace: Some(ScalingColorspace::Linear), ..Default::default()}); t("decoder.min_precise_scaling_ratio=3.5", Instructions { min_precise_scaling_ratio: Some(3.5f64), ..Default::default() }); + t("s.roundcorners=0,0,40,50", Instructions { s_round_corners: Some([0f64,0f64,40f64,50f64]), ..Default::default() }); + t("s.roundcorners=100", Instructions { s_round_corners: Some([100f64,100f64,100f64,100f64]), ..Default::default() }); + + t("f.sharpen=10", Instructions{ f_sharpen: Some(10f64), ..Default::default()}); t("f.sharpen_when=always", Instructions{ f_sharpen_when: Some(SharpenWhen::Always), ..Default::default()}); t("f.sharpen_when=downscaling", Instructions{ f_sharpen_when: Some(SharpenWhen::Downscaling), ..Default::default()}); diff --git a/imageflow_types/src/lib.rs b/imageflow_types/src/lib.rs index aff7c01c4..588e45093 100644 --- a/imageflow_types/src/lib.rs +++ b/imageflow_types/src/lib.rs @@ -613,6 +613,20 @@ pub struct ExecutionSecurity{ pub max_encode_size: Option } +#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Debug)] +pub enum RoundCornersMode { + #[serde(rename = "percentage")] + Percentage(f32), + #[serde(rename = "pixels")] + Pixels(f32), + #[serde(rename = "circle")] + Circle, + #[serde(rename = "percentage_custom")] + PercentageCustom{top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32 }, + #[serde(rename = "pixels_custom")] + PixelsCustom{top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32 }, +} + /// Represents a image operation. Currently used both externally (for JSON API) and internally. /// The most important data type #[allow(unreachable_patterns)] @@ -655,7 +669,7 @@ pub enum Node { }, #[serde(rename="round_image_corners")] RoundImageCorners { - radius: u32, + radius: RoundCornersMode, background_color: Color }, #[serde(rename="decode")]