diff --git a/benches/benchmark.rs b/benches/benchmark.rs index 6d73915..c9b5fa9 100644 --- a/benches/benchmark.rs +++ b/benches/benchmark.rs @@ -20,6 +20,7 @@ fn criterion_benchmark(c: &mut Criterion) { do_not_check_dimensions: true, detect_anti_aliased_pixels: false, blend_factor_of_unchanged_pixels: None, + block_out_areas: None, }; c.bench_function("1000 × 667 pixels", |b| { diff --git a/benches/fixtures/tiger-blockout-diff.png b/benches/fixtures/tiger-blockout-diff.png new file mode 100644 index 0000000..7bb29f8 Binary files /dev/null and b/benches/fixtures/tiger-blockout-diff.png differ diff --git a/benches/fixtures/yellow.jpg b/benches/fixtures/yellow.jpg new file mode 100644 index 0000000..9b52497 Binary files /dev/null and b/benches/fixtures/yellow.jpg differ diff --git a/src/cli.rs b/src/cli.rs index 018cf9a..f33fd34 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Context, Result}; use colored::*; use getopts::{Matches, Options}; +use std::collections::HashSet; use std::env; const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); @@ -12,6 +13,7 @@ const SHORT_NAME_OUTPUT_IMAGE_PATH: &str = "o"; const SHORT_NAME_THRESHOLD: &str = "t"; const SHORT_NAME_DETECT_ANTI_ALIASED_PIXELS: &str = "d"; const SHORT_NAME_BLEND_FACTOR_OF_UNCHENGED_PIXELS: &str = "a"; +const SHORT_NAME_BLOCK_OUT_AREA: &str = "b"; const DEFAULT_PATH_OF_DIFF_IMAGE: &str = "diff.png"; pub enum OutputImageBase { @@ -31,46 +33,53 @@ impl Cli { let mut options = Options::new(); - options.optflag(SHORT_NAME_HELP, "help", "print this help menu"); - options.optflag(SHORT_NAME_VERSION, "version", "print the version"); + options.optflag(SHORT_NAME_HELP, "help", "Print this help menu."); + options.optflag(SHORT_NAME_VERSION, "version", "Print the version."); + + options.optmulti( + SHORT_NAME_BLOCK_OUT_AREA, + "block-out", + "Block-out area. Can be repeated multiple times.", + "x,y,w,h", + ); options.optflag( SHORT_NAME_DONT_CHECK_DIMENSIONS, "ignore-dimensions", - "don't check image dimensions", + "Do not check image dimensions.", ); options.optflagopt( SHORT_NAME_BLEND_FACTOR_OF_UNCHENGED_PIXELS, "alpha", - "blending factor of unchanged pixels in the diff output. ranges from 0 for pure white to 1 for original brightness. (default: 0.1)", + "Blending factor of unchanged pixels in the diff output. Ranges from 0 for pure white to 1 for original brightness. (default: 0.1)", "NUM" ); options.optflagopt( SHORT_NAME_COPY_IMAGE_AS_BASE, "copy-image", - "copies specific image to output as base. (default: left)", + "Copies specific image to output as base. (default: left)", "{left, right}", ); options.optflag( SHORT_NAME_DETECT_ANTI_ALIASED_PIXELS, "detect-anti-aliased", - "detect anti-aliased pixels. (default: false)", + "Detects anti-aliased pixels. (default: false)", ); options.optopt( SHORT_NAME_OUTPUT_IMAGE_PATH, "output", - "the file path of diff image, PNG only. (default: diff.png)", + "The file path of diff image, PNG only. (default: diff.png)", "OUTPUT", ); options.optopt( SHORT_NAME_THRESHOLD, "threshold", - "matching threshold, ranges from 0 to 1, less more precise. (default: 0.1)", + "Matching threshold, ranges from 0 to 1, less more precise. (default: 0.1)", "NUM", ); @@ -198,6 +207,40 @@ impl Cli { .get(1) .with_context(|| format!("the {} argument is missing", "RIGHT".magenta()).red())?; - Ok((&left_image, &right_image)) + Ok((left_image, right_image)) + } + + pub fn get_block_out_area(&self) -> Option> { + self.matches + .opt_strs(SHORT_NAME_BLOCK_OUT_AREA) + .iter() + .fold(None, |acc, area| { + let area = { + let mut segments = area + .splitn(4, ',') + .map(|segment| segment.parse::().ok().unwrap_or(0)); + let x = segments.next().unwrap_or(0); + let y = segments.next().unwrap_or(0); + let width = segments.next().unwrap_or(0); + let height = segments.next().unwrap_or(0); + + match (x, y, width, height) { + (0, _, _, _) | (_, 0, _, _) | (_, _, 0, _) | (_, _, _, 0) => None, + (x, y, width, height) => Some((x, y, width, height)), + } + }; + match area { + None => acc, + Some((x, y, width, height)) => { + let mut acc = acc.unwrap_or_default(); + for i in x..=x + width { + for j in y..=y + height { + acc.insert((i, j)); + } + } + Some(acc) + } + } + }) } } diff --git a/src/diff.rs b/src/diff.rs index 99e234a..15686fd 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -1,8 +1,9 @@ -use super::{antialiased, cli, yiq::YIQ}; +use super::{antialiased, cli, yiq::Yiq}; use anyhow::{anyhow, Context, Result}; use colored::*; use image::io::Reader as ImageIoReader; use image::{GenericImageView, ImageBuffer, ImageFormat, Pixel, Rgba, RgbaImage}; +use std::collections::HashSet; const MAX_YIQ_POSSIBLE_DELTA: f32 = 35215.0; const RED_PIXEL: Rgba = Rgba([255, 0, 0, 255]); @@ -15,6 +16,7 @@ pub enum DiffResult { Different(u32, u32), OutOfBounds(u32, u32), AntiAliased(u32, u32), + BlockedOut(u32, u32), } pub struct RunParams<'a> { @@ -26,6 +28,7 @@ pub struct RunParams<'a> { pub do_not_check_dimensions: bool, pub detect_anti_aliased_pixels: bool, pub blend_factor_of_unchanged_pixels: Option, + pub block_out_areas: Option>, } fn open_and_decode_image(path: &str, which: &str) -> Result { @@ -39,12 +42,13 @@ fn open_and_decode_image(path: &str, which: &str) -> Result { } pub fn get_results( - left_image: &RgbaImage, - right_image: &RgbaImage, + left_image: RgbaImage, + right_image: RgbaImage, threshold: f32, detect_anti_aliased_pixels: bool, blend_factor_of_unchanged_pixels: Option, output_image_base: &Option, + block_out_areas: &Option>, ) -> Option<(i32, RgbaImage)> { let (width, height) = left_image.dimensions(); @@ -54,9 +58,15 @@ pub fn get_results( if left_pixel == right_pixel { DiffResult::Identical(x, y) + } else if block_out_areas + .as_ref() + .and_then(|set| set.contains(&(x, y)).then(|| ())) + .is_some() + { + DiffResult::BlockedOut(x, y) } else { - let left_pixel = YIQ::from_rgba(left_pixel); - let right_pixel = YIQ::from_rgba(right_pixel); + let left_pixel = Yiq::from_rgba(left_pixel); + let right_pixel = Yiq::from_rgba(right_pixel); let delta = left_pixel.squared_distance(&right_pixel); if delta.abs() > threshold { @@ -90,7 +100,7 @@ pub fn get_results( DiffResult::Identical(x, y) | DiffResult::BelowThreshold(x, y) => { if let Some(alpha) = blend_factor_of_unchanged_pixels { let left_pixel = left_image.get_pixel(x, y); - let yiq_y = YIQ::rgb2y(&left_pixel.to_rgb()); + let yiq_y = Yiq::rgb2y(&left_pixel.to_rgb()); let rgba_a = left_pixel.channels()[3] as f32; let color = super::blend_semi_transparent_white(yiq_y, alpha * rgba_a / 255.0) as u8; @@ -105,6 +115,7 @@ pub fn get_results( DiffResult::AntiAliased(x, y) => { output_image.put_pixel(x, y, YELLOW_PIXEL); } + DiffResult::BlockedOut(_x, _y) => (), } } @@ -137,12 +148,13 @@ pub fn run(params: &RunParams) -> Result> { let threshold = MAX_YIQ_POSSIBLE_DELTA * params.threshold * params.threshold; match get_results( - &left_image, - &right_image, + left_image, + right_image, threshold, params.detect_anti_aliased_pixels, params.blend_factor_of_unchanged_pixels, ¶ms.output_image_base, + ¶ms.block_out_areas, ) { Some((diffs, output_image)) => { output_image @@ -169,17 +181,19 @@ mod tests { do_not_check_dimensions: true, detect_anti_aliased_pixels: false, blend_factor_of_unchanged_pixels: None, + block_out_areas: None, }; #[test] fn test_zero_width_height() { let actual = get_results( - &RgbaImage::new(0, 0), - &RgbaImage::new(0, 0), + RgbaImage::new(0, 0), + RgbaImage::new(0, 0), RUN_PARAMS.threshold, RUN_PARAMS.detect_anti_aliased_pixels, RUN_PARAMS.blend_factor_of_unchanged_pixels, &RUN_PARAMS.output_image_base, + &RUN_PARAMS.block_out_areas, ); assert_eq!(None, actual); } @@ -187,12 +201,13 @@ mod tests { #[test] fn test_1_pixel() { let actual = get_results( - &RgbaImage::new(1, 1), - &RgbaImage::new(1, 1), + RgbaImage::new(1, 1), + RgbaImage::new(1, 1), RUN_PARAMS.threshold, RUN_PARAMS.detect_anti_aliased_pixels, RUN_PARAMS.blend_factor_of_unchanged_pixels, &RUN_PARAMS.output_image_base, + &RUN_PARAMS.block_out_areas, ); assert_eq!(None, actual); } @@ -202,12 +217,13 @@ mod tests { let mut left = RgbaImage::new(1, 1); left.put_pixel(0, 0, YELLOW_PIXEL); let actual = get_results( - &left, - &RgbaImage::new(1, 1), + left, + RgbaImage::new(1, 1), RUN_PARAMS.threshold, RUN_PARAMS.detect_anti_aliased_pixels, RUN_PARAMS.blend_factor_of_unchanged_pixels, &RUN_PARAMS.output_image_base, + &RUN_PARAMS.block_out_areas, ); let mut expected_image = RgbaImage::new(1, 1); diff --git a/src/lib.rs b/src/lib.rs index a421da1..5525d16 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,9 +2,9 @@ pub mod cli; pub mod diff; mod yiq; +use crate::yiq::Yiq; use image::{Pixel, RgbaImage}; use std::cmp; -use yiq::*; fn get_diagonal_neighbours(x1: u32, y1: u32, width: u32, height: u32) -> ((u32, u32), (u32, u32)) { // (x0, y0) @@ -88,7 +88,7 @@ pub fn antialiased( } let neighbor = left.get_pixel(x, y).to_rgb(); - let delta = YIQ::delta_y(center, &neighbor); + let delta = Yiq::delta_y(center, &neighbor); if delta == 0.0 { zeros += 1; diff --git a/src/main.rs b/src/main.rs index 6a5924b..e78e91a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,7 @@ fn main() -> Result<()> { let threshold = cli.get_threshold()?; let detect_anti_aliased_pixels = cli.detect_anti_aliased_pixels(); let blend_factor_of_unchanged_pixels = cli.blend_factor_of_unchanged_pixels()?; + let block_out_areas = cli.get_block_out_area(); diff::run(&diff::RunParams { left, @@ -31,6 +32,7 @@ fn main() -> Result<()> { do_not_check_dimensions, detect_anti_aliased_pixels, blend_factor_of_unchanged_pixels, + block_out_areas, }) .map(|code| { if let Some(code) = code { diff --git a/src/yiq.rs b/src/yiq.rs index 309596f..1cccc00 100644 --- a/src/yiq.rs +++ b/src/yiq.rs @@ -1,13 +1,13 @@ use image::Pixel; #[derive(Debug, PartialEq)] -pub struct YIQ { +pub struct Yiq { y: f32, // luminance, in range [0, 1] i: f32, // hue of color, in range ~ [-0.5, 0.5] q: f32, // saturation of color, in range ~ [-0.5, 0.5] } -impl YIQ { +impl Yiq { #[allow(clippy::many_single_char_names, clippy::excessive_precision)] pub fn rgb2y(rgb: &image::Rgb) -> f32 { let rgb = rgb.channels(); @@ -68,27 +68,27 @@ impl YIQ { #[cfg(test)] mod tests { - use super::YIQ; + use super::Yiq; #[test] fn test_from_rgb() { - let expected = YIQ { + let expected = Yiq { y: 0.0, i: 0.0, q: 0.0, }; - let actual = YIQ::from_rgba(&image::Rgba([0, 0, 0, 0])); + let actual = Yiq::from_rgba(&image::Rgba([0, 0, 0, 0])); assert_eq!(expected, actual); } #[test] fn test_squared_distance_same() { - let a = YIQ { + let a = Yiq { y: 0.5, i: -0.1, q: 0.1, }; - let b = YIQ { + let b = Yiq { y: 0.5, i: -0.1, q: 0.1, diff --git a/tests/e2e.rs b/tests/e2e.rs index fa3e55c..cb458ed 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -148,3 +148,25 @@ fn test_output_image_web_page() { temp.close().unwrap(); } + +#[test] +fn test_block_out_area() { + let temp = NamedTempFile::new("tiger-blockout-diff.png").unwrap(); + let mut cmd = Command::cargo_bin("dify").unwrap(); + let assert = cmd + .arg(fs::canonicalize("./benches/fixtures/tiger.jpg").unwrap()) + .arg(fs::canonicalize("./benches/fixtures/yellow.jpg").unwrap()) + .arg("--output") + .arg(temp.path().display().to_string()) + .arg("--copy-image") + .arg("left") + .arg("--block-out") + .arg("100,50,350,400"); + + assert.assert().failure(); + temp.assert(predicate::path::eq_file( + fs::canonicalize("./benches/fixtures/tiger-blockout-diff.png").unwrap(), + )); + + temp.close().unwrap(); +}