use crate::cli::config::{
validate_jpeg_quality, Config, ConfigBuilder, InputOutputModeType, SelectedLicenses,
};
use anyhow::{anyhow, bail};
use arg_names::*;
use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches};
use naut_cli_ops::create_image_ops;
use naut_cli_ops::operations::OperationId;
use naut_io::load::FrameIndex;
use std::path::Path;
use std::str::FromStr;
use strum::VariantNames;
macro_rules! define_arg_consts {
($mod:ident, { $($argdef:ident),+ $(,)? } ) => {
pub mod $mod {
$(
pub const $argdef: &str = stringify!($argdef);
)+
}
};
}
define_arg_consts!(arg_names, {
ARG_LICENSE,
ARG_DEP_LICENSES,
ARG_INPUT,
ARG_INPUT_GLOB,
ARG_OUTPUT,
ARG_OUTPUT_GLOB,
ARG_GLOB_NO_SKIP_UNSUPPORTED_EXTENSIONS,
ARG_SELECT_FRAME,
ARG_DISABLE_AUTOMATIC_COLOR_TYPE_ADJUSTMENT,
ARG_FORCED_OUTPUT_FORMAT,
ARG_JPEG_ENCODING_QUALITY,
ARG_PNM_ENCODING_ASCII,
ARG_IMAGE_CRATE_FALLBACK,
ARG_APPLY_OPERATIONS,
ARG_OPERATIONS_SCRIPT,
GROUP_IMAGE_OPERATIONS,
});
#[cfg(not(feature = "imageproc-ops"))]
fn wrap_with(app: App<'static, 'static>) -> App<'static, 'static> {
app
}
#[cfg(feature = "imageproc-ops")]
fn wrap_with(app: App<'static, 'static>) -> App<'static, 'static> {
app.arg(
Arg::with_name(OperationId::DrawText.as_str())
.help("Operation: draw-text.")
.long(OperationId::DrawText.as_str())
.takes_value(true)
.value_name(
"<text> <coord(x, y)> <rgba(r,g,b,a)> <size(s)> <font(\"path/to/font.ttf\">)",
)
.number_of_values(5)
.multiple(true),
)
}
pub fn create_app(
version: &'static str,
about: &'static str,
help_ops: &'static str,
) -> App<'static, 'static> {
wrap_with(App::new("naut")
.version(version)
.about(about)
.after_help("For more information, visit: https://github.com/foresterre/naut")
.author("Martijn Gribnau <garm@ilumeo.com>")
.global_setting(AppSettings::NextLineHelp)
.global_setting(AppSettings::ColoredHelp)
.global_setting(AppSettings::ColorAuto)
.global_setting(AppSettings::DontCollapseArgsInUsage)
.global_setting(AppSettings::UnifiedHelpMessage)
.max_term_width(120)
.arg(Arg::with_name(ARG_LICENSE)
.long("license")
.help("Displays the license of this piece of software (`naut`).")
.takes_value(false)
.conflicts_with_all(&[ARG_DEP_LICENSES, ARG_INPUT, ARG_OUTPUT, ARG_INPUT_GLOB, ARG_OUTPUT_GLOB]))
.arg(Arg::with_name(ARG_DEP_LICENSES)
.long("dep-licenses")
.help("Displays the licenses of the dependencies on which this software relies.")
.takes_value(false)
.conflicts_with_all(&[ARG_LICENSE, ARG_INPUT, ARG_OUTPUT, ARG_INPUT_GLOB, ARG_OUTPUT_GLOB]))
.arg(Arg::with_name(ARG_INPUT)
.long("input")
.short("i")
.value_name("INPUT_PATH")
.takes_value(true)
.help("Input image path. When using this option, input piped from stdin will be ignored. \
If using unexpanded globs as argument, use --glob-input instead.")
.conflicts_with_all(&[ARG_LICENSE, ARG_DEP_LICENSES, ARG_INPUT_GLOB, ARG_OUTPUT_GLOB]))
.arg(Arg::with_name(ARG_INPUT_GLOB)
.long("glob-input")
.takes_value(true)
.value_name("GLOB_INPUT_PATTERN")
.help("Input glob path which attempts to match all files matching the given glob pattern. Use with --glob-output. \
Depending on your shell you may need to add explicit quotation marks around the argument.")
.conflicts_with_all(&[ARG_LICENSE, ARG_DEP_LICENSES, ARG_INPUT, ARG_OUTPUT])
)
.arg(Arg::with_name(ARG_OUTPUT)
.long("output")
.short("o")
.value_name("OUTPUT_PATH")
.takes_value(true)
.help("Output image path. When using this option, output won't be piped to stdout.")
.conflicts_with_all(&[ARG_LICENSE, ARG_DEP_LICENSES, ARG_INPUT_GLOB, ARG_OUTPUT_GLOB])
)
.arg(Arg::with_name(ARG_OUTPUT_GLOB)
.long("glob-output")
.value_name("GLOB_OUTPUT_ROOT_FOLDER")
.takes_value(true)
.help("This output should point to a folder in which the greatest root common directory of the glob input will be mirrored")
.conflicts_with_all(&[ARG_LICENSE, ARG_DEP_LICENSES, ARG_INPUT, ARG_OUTPUT])
)
.arg(Arg::with_name(ARG_GLOB_NO_SKIP_UNSUPPORTED_EXTENSIONS)
.long("no-skip-unsupported-extensions")
.help("Files which don't have a known extension will not be skipped in glob mode")
.long_help("Only has an effect when combined with --glob-input")
.takes_value(false)
)
.arg(Arg::with_name(ARG_SELECT_FRAME)
.long("select-frame")
.value_name("#FRAME")
.help("Frame to be loaded as still image if the input image is an animated image.\
To pick the first and last frame respectively, you can provide 'first' and 'last' as arguments. \
Otherwise provide a single one-indexed positive number which corresponds with the frame index. \
For example, to select the first frame, the argument would be '1', for the second '2', etc.")
.takes_value(true))
.arg(Arg::with_name(ARG_DISABLE_AUTOMATIC_COLOR_TYPE_ADJUSTMENT)
.long("disable-automatic-color-type-adjustment")
.help("Some image output formats do not support the color type of the image buffer prior to encoding. \
By default the program tries to adjust the color type. If this flag is provided, \
the program will not try to adjust the color type."))
.arg(Arg::with_name(ARG_FORCED_OUTPUT_FORMAT)
.short("f")
.long("output-format")
.value_name("FORMAT")
.help("Force the output image format to use FORMAT, regardless of the (if any) extension of the given output file path. \
Output formats (FORMAT values) supported: AVIF, BMP, Farbfeld, GIF, ICO, JPEG, PNG, PAM, PBM, PGM, PPM and TGA.")
.takes_value(true))
.arg(Arg::with_name(ARG_JPEG_ENCODING_QUALITY)
.long("jpeg-encoding-quality")
.help("Set the jpeg quality to QUALITY. Valid values are positive numbers from 1 up to and including 100. Will only be used when the output format is determined to be jpeg.")
.value_name("QUALITY")
.takes_value(true))
.arg(Arg::with_name(ARG_PNM_ENCODING_ASCII)
.long("pnm-encoding-ascii")
.help("Use ascii based encoding when using a PNM image output format (pbm, pgm or ppm). Doesn't apply to 'pam' (PNM Arbitrary Map)."))
.arg(Arg::with_name(ARG_IMAGE_CRATE_FALLBACK)
.long("enable-output-format-decider-fallback")
.help("[experimental] When this flag is set, naut will attempt to fallback to an alternative output format decider (image crate version), \
*if* naut's own decider can't find a suitable format. Setting this flag may introduce unwanted behaviour; use with caution."))
.arg(Arg::with_name(ARG_APPLY_OPERATIONS)
.long("apply-operations")
.short("x")
.alias("A")
.help(help_ops)
.value_name("OPERATIONS")
.takes_value(true)
.conflicts_with(ARG_OPERATIONS_SCRIPT))
.arg(Arg::with_name(ARG_OPERATIONS_SCRIPT)
.long("operations-script")
.help("Like '--apply-operations' but takes a file path where the file contains the script instead of taking it as value directly")
.value_name("SCRIPT_FILE")
.takes_value(true)
.conflicts_with(ARG_APPLY_OPERATIONS))
.group(ArgGroup::with_name(GROUP_IMAGE_OPERATIONS)
.args(&OperationId::VARIANTS)
.conflicts_with(ARG_APPLY_OPERATIONS)
.multiple(true))
.arg(Arg::with_name(OperationId::Blur.as_str())
.help("Operation: perform a gaussian blur on the input image")
.long(OperationId::Blur.as_str())
.takes_value(true)
.value_name("fp")
.number_of_values(1)
.multiple(true)
.allow_hyphen_values(true))
.arg(Arg::with_name(OperationId::Brighten.as_str())
.help("Operation: increase or decrease the brightness of the input image")
.long(OperationId::Brighten.as_str())
.takes_value(true)
.value_name("int")
.number_of_values(1)
.multiple(true)
.allow_hyphen_values(true))
.arg(Arg::with_name(OperationId::Contrast.as_str())
.help("Operation: increase or decrease the contrast of the input image")
.long(OperationId::Contrast.as_str())
.takes_value(true)
.value_name("fp")
.number_of_values(1)
.multiple(true)
.allow_hyphen_values(true))
.arg(Arg::with_name(OperationId::Crop.as_str())
.help("Operation: crop the input image to a bounding rectangle ranging from top-left (lx, ly) to bottom-right (rx, ry) coordinates")
.long(OperationId::Crop.as_str())
.takes_value(true)
.value_names(&["lx", "ly", "rx", "ry"])
.number_of_values(4)
.multiple(true))
.arg(Arg::with_name(OperationId::Diff.as_str())
.help("Operation: show ")
.long(OperationId::Diff.as_str())
.takes_value(true)
.value_name("path to image")
.number_of_values(1)
.multiple(true))
.arg(Arg::with_name(OperationId::Filter3x3.as_str())
.help("Operation: apply a 3x3 convolution filter to the input image (matrix arguments should be given left-to-right, top-to-bottom)")
.long(OperationId::Filter3x3.as_str())
.takes_value(true)
.value_names(&["fp", "fp", "fp", "fp", "fp", "fp", "fp", "fp", "fp"])
.number_of_values(9)
.multiple(true)
.allow_hyphen_values(true))
.arg(Arg::with_name(OperationId::FlipHorizontal.as_str())
.help("Operation: flip the input image horizontally")
.long(OperationId::FlipHorizontal.as_str())
.multiple(true))
.arg(Arg::with_name(OperationId::FlipVertical.as_str())
.help("Operation: flip the input image vertically")
.long(OperationId::FlipVertical.as_str())
.multiple(true))
.arg(Arg::with_name(OperationId::Grayscale.as_str())
.help("Operation: discard the chrominance signal from the input image, so it becomes achromatic")
.long_help("Note that (depending on the provided settings flags), the processed image may still be stored in a format which encodes its chrominance")
.long(OperationId::Grayscale.as_str())
.multiple(true))
.arg(Arg::with_name(OperationId::HueRotate.as_str())
.help("Operation: rotate the hue for each pixel of the input image by a provided degree")
.long_help("Range is 0-360 degrees, any other value will be mapped to that range by rotation")
.long(OperationId::HueRotate.as_str())
.takes_value(true)
.value_name("int")
.number_of_values(1)
.multiple(true)
.allow_hyphen_values(true))
.arg(Arg::with_name(OperationId::Invert.as_str())
.help("Operation: invert the each pixel of the input image ")
.long(OperationId::Invert.as_str())
.multiple(true))
.arg(Arg::with_name(OperationId::Overlay.as_str())
.help("Operation: overlay an image loaded from the provided path argument, over the input image (at a certain position)")
.long(OperationId::Overlay.as_str())
.value_names(&["overlay image path", "x", "y"])
.takes_value(true)
.number_of_values(3)
.multiple(true))
.arg(Arg::with_name(OperationId::Resize.as_str())
.help("Operation: resize the input image to x by y pixels")
.long(OperationId::Resize.as_str())
.takes_value(true)
.value_names(&["x", "y"])
.number_of_values(2)
.multiple(true))
.arg(Arg::with_name(OperationId::Rotate90.as_str())
.help("Operation: rotate the input image by 90 degrees")
.long(OperationId::Rotate90.as_str())
.multiple(true))
.arg(Arg::with_name(OperationId::Rotate180.as_str())
.help("Operation: rotate the input image by 180 degrees")
.long(OperationId::Rotate180.as_str())
.multiple(true))
.arg(Arg::with_name(OperationId::Rotate270.as_str())
.help("Operation: rotate the input image by 270 degrees")
.long(OperationId::Rotate270.as_str())
.multiple(true))
.arg(Arg::with_name(OperationId::Unsharpen.as_str())
.help("Operation: sharpen an image by combining an unsharp (blurred) mask of the input image with the (original) input image, sharpening for pixels where the difference is bigger than the provided threshold")
.long(OperationId::Unsharpen.as_str())
.takes_value(true)
.value_names(&["blur amount","threshold"])
.number_of_values(2)
.multiple(true)
.allow_hyphen_values(true))
.arg(Arg::with_name(OperationId::PreserveAspectRatio.as_str())
.help("Operation modifier for 'resize': preserve the aspect ratio of the original input image")
.long(OperationId::PreserveAspectRatio.as_str())
.takes_value(true)
.value_name("bool")
.number_of_values(1)
.multiple(true)
.possible_values(&["true", "false"])
)
.arg(Arg::with_name(OperationId::SamplingFilter.as_str())
.help("Operation modifier for 'resize': resize the image using a specific sampling-filter")
.long(OperationId::SamplingFilter.as_str())
.takes_value(true)
.value_name("sampling filter")
.number_of_values(1)
.multiple(true)
.possible_values(&["catmullrom", "gaussian", "lanczos3", "nearest", "triangle"])
))
}
pub fn build_app_config<'a>(matches: &'a ArgMatches) -> anyhow::Result<Config<'a>> {
let mut builder = ConfigBuilder::new();
let show_license = if matches.is_present(ARG_LICENSE) {
builder = builder.show_license_text_of(SelectedLicenses::ThisSoftware);
Some(())
} else if matches.is_present(ARG_DEP_LICENSES) {
builder = builder.show_license_text_of(SelectedLicenses::Dependencies);
Some(())
} else {
None
};
if show_license.is_some() {
return Ok(builder.build());
}
builder = builder.mode(InputOutputModeType::from_arg_matches(matches)?);
if let Some(frame_in) = matches.value_of(ARG_SELECT_FRAME) {
let frame_out = match frame_in {
"first" => FrameIndex::First,
"last" => FrameIndex::Last,
n => {
let pick = n.parse::<usize>().map_err(|_| {
anyhow!(
"Provided argument for --select-frame is not a valid option. \
Valid options are 'first', 'last' or a (one-indexed) positive number."
)
})?;
if pick == 0 {
bail!(
"Provided argument for --select-frame is not a valid option. \
If a number is provided, the number should be positive and larger than 0. \
To select the first frame, provide the argument '1'."
);
}
FrameIndex::Nth(pick - 1)
}
};
builder = builder.select_frame(frame_out);
}
if matches.is_present(ARG_DISABLE_AUTOMATIC_COLOR_TYPE_ADJUSTMENT) {
builder = builder.disable_automatic_color_type_adjustment(true);
}
if let Some(format) = matches.value_of(ARG_FORCED_OUTPUT_FORMAT) {
builder = builder.forced_output_format(format);
}
if let Some(value) = matches.value_of(ARG_JPEG_ENCODING_QUALITY) {
let requested_jpeg_quality = u8::from_str(value)
.map_err(|_| {
anyhow!("JPEG Encoding quality should be a value between 1 and 100 (inclusive).")
})
.and_then(validate_jpeg_quality)?;
builder = builder.jpeg_quality(requested_jpeg_quality);
}
if matches.is_present(ARG_PNM_ENCODING_ASCII) {
builder = builder.pnm_format_type(true);
}
builder =
builder.image_output_format_decider_fallback(matches.is_present(ARG_IMAGE_CRATE_FALLBACK));
let program = if let Some(script) = matches.value_of(ARG_APPLY_OPERATIONS) {
naut_parser::parse_script(script)?
} else if let Some(path) = matches.value_of(ARG_OPERATIONS_SCRIPT) {
let contents = std::fs::read_to_string(Path::new(path))
.map_err(|err| anyhow::anyhow!("unable to read script file: {}", err))?;
naut_parser::parse_script(&contents)?
} else {
create_image_ops(std::env::args())?
};
builder = builder.image_operations_program(program);
Ok(builder.build())
}