sic 0.22.4

Accessible image processing and conversion from the terminal (and a front-end for the 'image' crate).
Documentation
#![deny(clippy::all)]
#![allow(clippy::items_after_test_module)]

#[macro_use]
extern crate parameterized;

use std::io::Read;
use std::path::{Path, PathBuf};

use sic_core::image;

use sic::cli::app::{build_app_config, create_app as get_app}; // build_app_config
use sic::cli::config::InputOutputMode;
use sic::cli::pipeline::run_with_devices;

// copied from sic_lib::processor::mod_test_includes
// I preferred to not make that module public (2018-11-28)
// Originally named: setup_test_image.
fn setup_input_path(test_image_path: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("resources")
        .join(test_image_path)
}

// copied from sic_lib::processor::mod_test_includes
// I preferred to not make that module public (2018-11-28)
fn setup_output_path(test_output_path: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("target")
        .join(test_output_path)
}

fn path_buf_str(pb: &Path) -> &str {
    pb.to_str().unwrap()
}

// copied from sic_lib::processor::mod_test_includes
// I preferred to not make that module public (2018-11-28)
fn clean_up_output_path(test_output_path: &str) {
    std::fs::remove_file(setup_output_path(test_output_path))
        .expect("Unable to remove file after test.");
}

// by image header
fn is_image_format(output_path: &str, hope: image::ImageFormat) -> bool {
    let mut file = std::fs::File::open(setup_output_path(output_path))
        .expect("Failed to find (produced) test image.");

    let mut bytes = vec![];

    file.read_to_end(&mut bytes)
        .expect("Failed to read (produced) test image.");

    hope == image::guess_format(&bytes).expect("Format could not be guessed.")
}

// convert_to_X_by_extension
// BMP, GIF, JPEG, PNG, ICO, PBM, PGM, PPM, PAM

#[cfg(test)]
mod convert_to_x {
    use super::*;

    ide!();

    #[parameterized(ext = {
        "bmp",
        "farbfeld",
        "gif",
        "jpg",
        "jpeg",
        "ico",
        "png",
        "pbm",
        "pgm",
        "ppm",
        "pam",
        "qoi",
        "tif",
        "tiff",
        "webp",
    }, expected_format = {
        image::ImageFormat::Bmp,
        image::ImageFormat::Farbfeld,
        image::ImageFormat::Gif,
        image::ImageFormat::Jpeg,
        image::ImageFormat::Jpeg,
        image::ImageFormat::Ico,
        image::ImageFormat::Png,
        image::ImageFormat::Pnm,
        image::ImageFormat::Pnm,
        image::ImageFormat::Pnm,
        image::ImageFormat::Pnm,
        image::ImageFormat::Qoi,
        image::ImageFormat::Tiff,
        image::ImageFormat::Tiff,
        image::ImageFormat::WebP,

    })]
    fn convert_to_x_by_extension(ext: &str, expected_format: image::ImageFormat) {
        let input_path = setup_input_path("palette_4x4.png");
        let output_path = setup_output_path(&["cli_convert_to_x_by_extension", ext].join("."));

        let args = vec![
            "sic",
            "--input",
            input_path.to_str().unwrap(),
            "--output",
            path_buf_str(&output_path),
        ];
        let matches = get_app("", "", "").get_matches_from(args);

        let complete = run_with_devices(
            InputOutputMode::try_from_matches(&matches).unwrap(),
            &build_app_config(&matches).unwrap(),
        );

        complete.unwrap();
        assert!(output_path.exists());
        assert!(is_image_format(path_buf_str(&output_path), expected_format));

        clean_up_output_path(path_buf_str(&output_path));
    }
}

#[cfg(test)]
mod convert_to_x_by_ff {
    use super::*;

    ide!();

    fn args<'a>(which: &'a str, input: &'a Path, output: &'a Path) -> Vec<&'a str> {
        vec![
            "sic",
            "--output-format",
            which,
            "--input",
            path_buf_str(input),
            "--output",
            path_buf_str(output),
        ]
    }

    #[parameterized(which = {
        "bmp",
        "farbfeld",
        "gif",
        "jpg",
        "jpeg",
        "ico",
        "png",
        "pbm",
        "pgm",
        "ppm",
        "pam",
        "qoi",
        "tif",
        "tiff",
        "webp",
    }, expected_format = {
        image::ImageFormat::Bmp,
        image::ImageFormat::Farbfeld,
        image::ImageFormat::Gif,
        image::ImageFormat::Jpeg,
        image::ImageFormat::Jpeg,
        image::ImageFormat::Ico,
        image::ImageFormat::Png,
        image::ImageFormat::Pnm,
        image::ImageFormat::Pnm,
        image::ImageFormat::Pnm,
        image::ImageFormat::Pnm,
        image::ImageFormat::Qoi,
        image::ImageFormat::Tiff,
        image::ImageFormat::Tiff,
        image::ImageFormat::WebP,
    })]
    fn convert_to_bmp_by_ff(which: &str, expected_format: image::ImageFormat) {
        let input_path = setup_input_path("palette_4x4.png");
        let output_path = setup_output_path(&format!("cli_convert_to_x_by_extension_ff_{}", which));

        let args = args(which, &input_path, &output_path);
        let matches = get_app("", "", "").get_matches_from(args);
        let complete = run_with_devices(
            InputOutputMode::try_from_matches(&matches).unwrap(),
            &build_app_config(&matches).unwrap(),
        );

        complete.unwrap();
        assert!(output_path.exists());
        assert!(is_image_format(path_buf_str(&output_path), expected_format));

        clean_up_output_path(path_buf_str(&output_path));
    }
}

fn read_file_to_bytes<P: AsRef<Path>>(path: P) -> Vec<u8> {
    let mut f = std::fs::File::open(path).unwrap();
    let mut buffer = Vec::new();

    // read the whole file
    f.read_to_end(&mut buffer).unwrap();

    buffer
}

// Try to determine that PBM, PGM, PPM in ascii mode (P1, P2, P3 resp.) are ascii encoded
// and if they are 'binary' encoded (P4, P5, P6), they are obviously not ascii encoded.
#[cfg(test)]
mod pnm_ascii_and_binary {
    use super::*;

    ide!();

    fn guess_is_ascii_encoded(input: &[u8]) -> bool {
        // The character P in ascii encoding
        let is_ascii_p = |c: u8| c == 0x50;

        // P1, P2, P3 are ascii
        // Checks for numbers 1, 2, 3, binary known as: 0x31, 0x32, 0x33 respectively
        let is_ascii_magic_number = |c| c == 0x31 || c == 0x32 || c == 0x33;

        let mut iter = input.iter();
        let first = iter.next().unwrap();
        let second = iter.next().unwrap();

        // check also to be sure that every character in the file can be ascii encoded
        is_ascii_p(*first) && is_ascii_magic_number(*second) && iter.all(|c| *c <= 0x7F)
    }

    fn pnm_encode_test_case(which: &str, is_ascii: bool) {
        let input_path = setup_input_path("palette_4x4.png");
        let ascii_str = |bool: bool| if bool { "ascii" } else { "binary" };
        let output_path = setup_output_path(&format!(
            "cli_convert_pnm_encoding_test_{}_{}",
            which,
            ascii_str(is_ascii)
        ));

        let mut args = Vec::with_capacity(8);
        args.push("sic");
        if is_ascii {
            args.push("--pnm-encoding-ascii");
        }
        args.push("--output-format");
        args.push(which);
        args.push("--input");
        args.push(path_buf_str(&input_path));
        args.push("--output");
        args.push(path_buf_str(&output_path));

        let matches = get_app("", "", "").get_matches_from(args);
        let complete = run_with_devices(
            InputOutputMode::try_from_matches(&matches).unwrap(),
            &build_app_config(&matches).unwrap(),
        );

        complete.unwrap();
        assert!(output_path.exists());

        // read file contents
        let contents = read_file_to_bytes(path_buf_str(&output_path));

        // is it just ascii?
        if is_ascii {
            assert!(guess_is_ascii_encoded(&contents));
        } else {
            assert!(!guess_is_ascii_encoded(&contents));
        }

        clean_up_output_path(path_buf_str(&output_path));
    }

    #[parameterized(which = {
        "pbm",
        "pgm",
        "ppm",
    })]
    fn pnm_encode_ascii(which: &str) {
        pnm_encode_test_case(which, true)
    }

    #[parameterized(which = {
        "pbm",
        "pgm",
        "ppm",
    })]
    fn pnm_encode_binary(which: &str) {
        pnm_encode_test_case(which, false)
    }
}

// JPEG different quality
// Currently just tested by default 80 ?!= not(80)
// Can we do better?
#[test]
fn convert_jpeg_quality_different() {
    let which = "jpeg";

    let our_input = setup_input_path("palette_4x4.png");
    let out1 = setup_output_path("out_02_jpeg_1.jpeg");
    let out2 = setup_output_path("out_02_jpeg_2.jpeg");

    let args1 = vec![
        "sic",
        "--output-format",
        which,
        "--input",
        path_buf_str(&our_input),
        "--output",
        path_buf_str(&out1),
    ];

    let args2 = vec![
        "sic",
        "--jpeg-encoding-quality",
        "81",
        "--output-format",
        which,
        "--input",
        path_buf_str(&our_input),
        "--output",
        path_buf_str(&out2),
    ];

    let matches1 = get_app("", "", "").get_matches_from(args1);
    run_with_devices(
        InputOutputMode::try_from_matches(&matches1).unwrap(),
        &build_app_config(&matches1).unwrap(),
    )
    .unwrap();

    let matches2 = get_app("", "", "").get_matches_from(args2);
    run_with_devices(
        InputOutputMode::try_from_matches(&matches2).unwrap(),
        &build_app_config(&matches2).unwrap(),
    )
    .unwrap();

    assert!(out1.exists() && out2.exists());

    // read file contents
    let contents1 = read_file_to_bytes(path_buf_str(&out1));
    let contents2 = read_file_to_bytes(path_buf_str(&out2));

    assert_ne!(contents1, contents2);

    clean_up_output_path(path_buf_str(&out1));
    clean_up_output_path(path_buf_str(&out2));
}