Skip to content

Commit

Permalink
Add percentage support and more for rounded corners, support s.roundc…
Browse files Browse the repository at this point in the history
…orners
  • Loading branch information
lilith committed May 19, 2022
1 parent dffed49 commit 0508903
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 13 deletions.
26 changes: 21 additions & 5 deletions imageflow_core/src/graphics/rounded_corners.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,42 @@
use imageflow_types::{Color, RoundCornersMode};
use crate::graphics::prelude::*;



fn get_radius_pixels(radius: RoundCornersMode, w: u32, h: u32) -> Result<f32, FlowError>{
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<u8>,
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;

Expand Down
25 changes: 20 additions & 5 deletions imageflow_core/tests/visuals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};


Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions imageflow_core/tests/visuals/checksums.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 19 additions & 2 deletions imageflow_riapi/src/ir4/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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
Expand All @@ -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)));
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
Expand Down
55 changes: 55 additions & 0 deletions imageflow_riapi/src/ir4/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,12 @@ impl std::fmt::Display for Instructions{
}
}

pub(crate) fn iter_all_eq<T: PartialEq>(iter: impl IntoIterator<Item = T>) -> Option<T> {
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{
Expand All @@ -292,6 +298,7 @@ impl Instructions{
s
}


pub fn to_map(&self) -> HashMap<&'static str,String>{
let mut m = HashMap::new();
fn add<T>(m: &mut HashMap<&'static str,String>, key: &'static str, value: Option<T>) where T: std::fmt::Display{
Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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::<f64>()).collect::<Vec<std::result::Result<f64,::std::num::ParseFloatError>>>();
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<bool>{
self.parse(key, |s|
match s.to_lowercase().as_str(){
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -861,6 +904,7 @@ pub struct Instructions{
pub ignoreicc: Option<bool>,
pub ignore_icc_errors: Option<bool>,
pub crop: Option<[f64;4]>,
pub s_round_corners: Option<[f64;4]>,
pub cropxunits: Option<f64>,
pub cropyunits: Option<f64>,
pub zoom: Option<f64>,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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![]);
Expand Down Expand Up @@ -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()});
Expand Down
16 changes: 15 additions & 1 deletion imageflow_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,20 @@ pub struct ExecutionSecurity{
pub max_encode_size: Option<FrameSizeLimit>
}

#[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)]
Expand Down Expand Up @@ -655,7 +669,7 @@ pub enum Node {
},
#[serde(rename="round_image_corners")]
RoundImageCorners {
radius: u32,
radius: RoundCornersMode,
background_color: Color
},
#[serde(rename="decode")]
Expand Down

0 comments on commit 0508903

Please sign in to comment.