use crate::cli::app::arg_names::{
ARG_GLOB_NO_SKIP_UNSUPPORTED_EXTENSIONS, ARG_IMAGE_CRATE_FALLBACK, ARG_INPUT, ARG_INPUT_GLOB,
ARG_OUTPUT, ARG_OUTPUT_GLOB,
};
use crate::cli::common_dir::CommonDir;
use crate::cli::glob_base_dir::glob_builder_base;
use anyhow::{bail, Context};
use clap::ArgMatches;
use globwalk::{FileType, GlobWalker};
use naut_image_engine::engine::Instr;
use naut_io::load::FrameIndex;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub enum PathVariant {
StdStream,
Path(PathBuf),
}
impl PathVariant {
pub fn is_std_stream(&self) -> bool {
match self {
PathVariant::StdStream => true,
PathVariant::Path(_) => false,
}
}
}
pub enum InputOutputMode {
Single {
input: PathVariant,
output: PathVariant,
},
Batch {
inputs: CommonDir,
output_root_folder: PathBuf,
},
}
impl InputOutputMode {
pub fn try_from_matches(matches: &ArgMatches) -> anyhow::Result<Self> {
let mode = InputOutputModeType::from_arg_matches(matches)?;
match mode {
InputOutputModeType::Simple => Ok(InputOutputMode::Single {
input: match matches.value_of(ARG_INPUT) {
Some(p) => PathVariant::Path(p.into()),
None => PathVariant::StdStream,
},
output: match matches.value_of(ARG_OUTPUT) {
Some(p) => PathVariant::Path(p.into()),
None => PathVariant::StdStream,
},
}),
InputOutputModeType::Batch => {
let inputs = matches
.value_of(ARG_INPUT_GLOB)
.with_context(|| "Glob mode requires an input pattern")?;
let output = matches
.value_of(ARG_OUTPUT_GLOB)
.with_context(|| "Glob mode requires an output folder")?;
Ok(InputOutputMode::Batch {
inputs: {
let inputs = Self::create_glob_walker(inputs)?;
let paths = Self::lookup_paths(
inputs,
!matches.is_present(ARG_GLOB_NO_SKIP_UNSUPPORTED_EXTENSIONS),
matches.is_present(ARG_IMAGE_CRATE_FALLBACK),
)?;
CommonDir::try_new(paths)?
},
output_root_folder: { output.into() },
})
}
}
}
fn create_glob_walker<PAT: AsRef<str>>(pattern: PAT) -> anyhow::Result<GlobWalker> {
glob_builder_base(pattern.as_ref(), &[])?
.follow_links(true)
.file_type(FileType::FILE)
.build()
.with_context(|| "Unable to parse the given glob pattern")
}
fn lookup_paths(
inputs: impl Iterator<Item = Result<globwalk::DirEntry, globwalk::WalkError>>,
filter_unsupported: bool,
image_crate_fallback_enabled: bool,
) -> anyhow::Result<Vec<PathBuf>> {
let paths: Vec<PathBuf> = inputs
.map(|entry| {
entry
.map_err(|err| {
anyhow::anyhow!(
"Error while trying to find glob matches on the fs ({})",
err
)
})
.map(|f| f.into_path())
})
.collect::<anyhow::Result<Vec<PathBuf>>>()?;
Ok(if filter_unsupported {
filter_unsupported_paths(paths, image_crate_fallback_enabled)
} else {
paths
})
}
}
fn filter_unsupported_paths(paths: Vec<PathBuf>, fallback_enabled: bool) -> Vec<PathBuf> {
use crate::cli::pipeline::fallback::guess_output_by_path;
use crate::combinators::FallbackIf;
use naut_io::format::DetermineEncodingFormat;
use naut_io::format::EncodingFormatByExtension;
let checker = DetermineEncodingFormat::default();
paths
.into_iter()
.filter(|path| {
checker
.by_extension(path)
.fallback_if(fallback_enabled, guess_output_by_path, path)
.is_ok()
})
.collect()
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum InputOutputModeType {
Simple,
Batch,
}
impl InputOutputModeType {
pub fn from_arg_matches(matches: &ArgMatches) -> anyhow::Result<InputOutputModeType> {
Ok(
match (
matches.is_present(ARG_INPUT),
matches.is_present(ARG_INPUT_GLOB),
) {
(true, false) => InputOutputModeType::Simple,
(false, true) => InputOutputModeType::Batch,
_ => {
bail!("Unable select input/output mode: mode should either be simple xor glob")
}
},
)
}
}
#[derive(Debug, Clone)]
pub struct Config<'a> {
pub tool_name: &'static str,
pub mode: InputOutputModeType,
pub show_license_text_of: Option<SelectedLicenses>,
pub selected_frame: FrameIndex,
pub disable_automatic_color_type_adjustment: bool,
pub forced_output_format: Option<&'a str>,
pub encoding_settings: FormatEncodingSettings,
pub image_operations_program: Vec<Instr>,
}
impl Default for Config<'_> {
fn default() -> Self {
Config {
tool_name: env!("CARGO_PKG_NAME"),
mode: InputOutputModeType::Simple,
show_license_text_of: None,
selected_frame: FrameIndex::First,
disable_automatic_color_type_adjustment: false,
forced_output_format: None,
encoding_settings: FormatEncodingSettings {
jpeg_quality: 80,
pnm_use_ascii_format: false,
image_output_format_fallback: false,
},
image_operations_program: Vec::new(),
}
}
}
#[derive(Debug, Default)]
pub struct ConfigBuilder<'a> {
settings: Config<'a>,
}
impl<'a> ConfigBuilder<'a> {
pub fn new() -> Self {
ConfigBuilder::default()
}
pub fn mode(mut self, mode: InputOutputModeType) -> ConfigBuilder<'a> {
self.settings.mode = mode;
self
}
pub fn show_license_text_of(mut self, selection: SelectedLicenses) -> ConfigBuilder<'a> {
self.settings.show_license_text_of = Some(selection);
self
}
pub fn select_frame(mut self, frame: FrameIndex) -> ConfigBuilder<'a> {
self.settings.selected_frame = frame;
self
}
pub fn forced_output_format(mut self, format: &'a str) -> ConfigBuilder<'a> {
self.settings.forced_output_format = Some(format);
self
}
pub fn disable_automatic_color_type_adjustment(mut self, toggle: bool) -> ConfigBuilder<'a> {
self.settings.disable_automatic_color_type_adjustment = toggle;
self
}
pub fn jpeg_quality(mut self, quality: u8) -> ConfigBuilder<'a> {
self.settings.encoding_settings.jpeg_quality = quality;
self
}
pub fn pnm_format_type(mut self, use_ascii: bool) -> ConfigBuilder<'a> {
self.settings.encoding_settings.pnm_use_ascii_format = use_ascii;
self
}
pub fn image_output_format_decider_fallback(
mut self,
enable_fallback: bool,
) -> ConfigBuilder<'a> {
self.settings.encoding_settings.image_output_format_fallback = enable_fallback;
self
}
pub fn image_operations_program(mut self, program: Vec<Instr>) -> ConfigBuilder<'a> {
self.settings.image_operations_program = program;
self
}
pub fn build(self) -> Config<'a> {
self.settings
}
}
#[derive(Debug, Clone, Copy)]
pub enum SelectedLicenses {
ThisSoftware,
Dependencies,
}
#[derive(Debug, Clone)]
pub struct FormatEncodingSettings {
pub jpeg_quality: u8,
pub pnm_use_ascii_format: bool,
pub image_output_format_fallback: bool,
}
pub fn validate_jpeg_quality(quality: u8) -> anyhow::Result<u8> {
fn within_range(v: u8) -> anyhow::Result<u8> {
const ALLOWED_RANGE: std::ops::Range<u8> = 1..101;
if ALLOWED_RANGE.contains(&v) {
Ok(v)
} else {
bail!("JPEG Encoding Settings error: JPEG quality requires a number between 1 and 100 (inclusive).")
}
}
within_range(quality)
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use naut_image_engine::engine::Instr;
use naut_image_engine::ImgOp;
use super::*;
#[test]
fn jpeg_in_quality_range_lower_bound_inside() {
let value: &str = "1";
assert!(validate_jpeg_quality(u8::from_str(value).unwrap()).is_ok())
}
#[test]
fn jpeg_in_quality_range_lower_bound_outside() {
let value: &str = "0";
assert!(validate_jpeg_quality(u8::from_str(value).unwrap()).is_err())
}
#[test]
fn jpeg_in_quality_range_upper_bound_inside() {
let value: &str = "100";
assert!(validate_jpeg_quality(u8::from_str(value).unwrap()).is_ok())
}
#[test]
fn jpeg_in_quality_range_upper_bound_outside() {
let value: &str = "101";
assert!(validate_jpeg_quality(u8::from_str(value).unwrap()).is_err())
}
#[test]
fn config_builder_override_defaults() {
let mut builder = ConfigBuilder::new();
builder = builder.image_operations_program(vec![Instr::Operation(ImgOp::Blur(1.0))]);
let config = builder.build();
assert!(!config.image_operations_program.is_empty());
}
#[test]
fn skip_unsupported_paths() {
fn to_path_bufs<'s>(paths: impl IntoIterator<Item = &'s &'s str>) -> Vec<PathBuf> {
paths
.into_iter()
.map(|s| FromStr::from_str(s).expect("test should have valid input"))
.collect::<Vec<_>>()
}
let paths = &[
"/scope/0.png",
"/scope/1.jpg",
"/scope/2.jpeg",
"/scope/2.unsupported",
"/scope/2",
];
let path_bufs = to_path_bufs(paths);
let expected_path_bufs = to_path_bufs(&[paths[0], paths[1], paths[2]]);
let filtered = filter_unsupported_paths(path_bufs, false);
assert_eq!(filtered, expected_path_bufs);
}
mod glob_skip_unsupported {
use super::*;
use parameterized::parameterized;
parameterized::ide!();
#[parameterized(paths_in = {
&["/test/0.png", "/test/1.jpg", "/test/2.jpeg", "/test/2.unsupported", "/test/2"],
&[],
&["a.farbfeld", "a.ff"],
&["a.farbfeld", "a.ff"],
}, paths_expected = {
&["/test/0.png", "/test/1.jpg", "/test/2.jpeg"],
&[],
&["a.farbfeld", "a.ff"],
&["a.farbfeld"],
}, fallback_on_imagecrate = {
false,
false,
true,
false,
})]
fn are_unsupported_paths_getting_filtered(
paths_in: &[&str],
paths_expected: &[&str],
fallback_on_imagecrate: bool,
) {
fn to_path_bufs<'s>(paths: impl IntoIterator<Item = &'s &'s str>) -> Vec<PathBuf> {
paths
.into_iter()
.map(|s| FromStr::from_str(s).expect("test should have valid input"))
.collect::<Vec<_>>()
}
let path_bufs = to_path_bufs(paths_in);
let expected_path_bufs = to_path_bufs(paths_expected);
let filtered = filter_unsupported_paths(path_bufs, fallback_on_imagecrate);
assert_eq!(filtered, expected_path_bufs);
}
}
}