dify 0.8.0

A fast pixel-by-pixel image comparison tool in Rust
Documentation
use anyhow::{Context, Result, anyhow};
use colored::*;
use getopts::{Matches, Options};
use std::collections::HashSet;
use std::env;

const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
const SHORT_NAME_HELP: &str = "h";
const SHORT_NAME_VERSION: &str = "v";
const SHORT_NAME_DONT_CHECK_DIMENSIONS: &str = "i";
const SHORT_NAME_COPY_IMAGE_AS_BASE: &str = "c";
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 {
    LeftImage,
    RightImage,
}

pub struct Cli {
    program: String,
    matches: Matches,
    options: Options,
}

impl Cli {
    pub fn new() -> Result<Self> {
        let args: Vec<String> = env::args().collect();

        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.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",
            "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)",
            "NUM"
        );

        options.optflagopt(
            SHORT_NAME_COPY_IMAGE_AS_BASE,
            "copy-image",
            "Copies specific image to output as base.",
            "{left, right}",
        );

        options.optflag(
            SHORT_NAME_DETECT_ANTI_ALIASED_PIXELS,
            "detect-anti-aliased",
            "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)",
            "OUTPUT",
        );

        options.optopt(
            SHORT_NAME_THRESHOLD,
            "threshold",
            "Matching threshold, ranges from 0 to 1, less more precise. (default: 0.1)",
            "NUM",
        );

        match options.parse(&args[1..]) {
            Ok(matches) => Ok(Self {
                program: args[0].clone(),
                matches,
                options,
            }),
            Err(f) => Err(anyhow!(f)),
        }
    }

    pub fn print_help(&self) {
        let brief = format!("Usage: {} [options] <LEFT> <RIGHT>", self.program);
        print!("{}", self.options.usage(&brief));
    }

    pub fn print_version(&self) {
        println!("{}", VERSION.unwrap_or(""));
    }

    pub fn show_help(&self) -> bool {
        self.matches.opt_present(SHORT_NAME_HELP)
    }

    pub fn show_version(&self) -> bool {
        self.matches.opt_present(SHORT_NAME_VERSION)
    }

    pub fn copy_specific_image_to_output_as_base(&self) -> Result<Option<OutputImageBase>> {
        if !self.matches.opt_present(SHORT_NAME_COPY_IMAGE_AS_BASE) {
            return Ok(None);
        }

        match self.matches.opt_str(SHORT_NAME_COPY_IMAGE_AS_BASE) {
            Some(value) => match &value.to_lowercase()[..] {
                "left" => Ok(Some(OutputImageBase::LeftImage)),
                "right" => Ok(Some(OutputImageBase::RightImage)),
                unsupported => Err(anyhow!(
                    format!(
                        "-c/--copy-image \"{}\" is not supported, possible values: left, right",
                        unsupported.magenta()
                    )
                    .red()
                )),
            },
            None => Ok(Some(OutputImageBase::LeftImage)),
        }
    }

    pub fn do_not_check_dimensions(&self) -> bool {
        self.matches.opt_present(SHORT_NAME_DONT_CHECK_DIMENSIONS)
    }

    pub fn detect_anti_aliased_pixels(&self) -> bool {
        self.matches
            .opt_present(SHORT_NAME_DETECT_ANTI_ALIASED_PIXELS)
    }

    pub fn blend_factor_of_unchanged_pixels(&self) -> Result<Option<f32>> {
        if !self
            .matches
            .opt_present(SHORT_NAME_BLEND_FACTOR_OF_UNCHENGED_PIXELS)
        {
            return Ok(None);
        }

        match self
            .matches
            .opt_str(SHORT_NAME_BLEND_FACTOR_OF_UNCHENGED_PIXELS)
        {
            Some(s) => s
                .parse::<f32>()
                .with_context(|| {
                    format!(
                        "the value of {} is invalid",
                        format!("-a/--alpha {s}").magenta()
                    )
                    .red()
                })
                .and_then(|n| {
                    if (0.0..=1.0).contains(&n) {
                        Ok(Some(n))
                    } else {
                        Err(anyhow!(
                            format!(
                                "the value of {} should be in range 0 to 1",
                                format!("-a/--alpha {s}").magenta()
                            )
                            .red()
                        ))
                    }
                }),
            None => Ok(Some(0.1)),
        }
    }

    pub fn get_output_image_path(&self) -> String {
        self.matches
            .opt_str(SHORT_NAME_OUTPUT_IMAGE_PATH)
            .unwrap_or_else(|| DEFAULT_PATH_OF_DIFF_IMAGE.to_owned())
    }

    pub fn get_threshold(&self) -> Result<f32> {
        self.matches
            .opt_str(SHORT_NAME_THRESHOLD)
            .map_or(Ok(0.1), |s| {
                s.parse::<f32>().with_context(|| {
                    format!(
                        "the value of {} is invalid",
                        format!("-t/--threshold {s}").magenta()
                    )
                    .red()
                })
            })
    }

    pub fn get_image_paths_of_left_right_diff(&self) -> Result<(&str, &str)> {
        let left_image = self
            .matches
            .free
            .first()
            .with_context(|| format!("the {} argument is missing", "LEFT".magenta()).red())?;

        let right_image = self
            .matches
            .free
            .get(1)
            .with_context(|| format!("the {} argument is missing", "RIGHT".magenta()).red())?;

        Ok((left_image, right_image))
    }

    pub fn get_block_out_area(&self) -> Option<HashSet<(u32, u32)>> {
        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::<u32>().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)
                    }
                }
            })
    }
}