use std::{
fs::File,
io::Write,
num::NonZeroU32,
path::{Path, PathBuf},
};
use artem::config::{self, ConfigBuilder, TargetType};
mod cli;
fn main() {
let matches = cli::build_cli().get_matches();
env_logger::builder()
.format_target(false)
.format_timestamp(None)
.filter_level(
(*matches
.get_one::<cli::Verbosity>("verbosity")
.unwrap_or(&cli::Verbosity::Warn))
.into(),
)
.init();
log::trace!("Started logger with trace");
log::trace!("Feature web_image: {}", cfg!(feature = "web_image"));
let mut config_builder = ConfigBuilder::new();
let input = matches.get_many::<String>("INPUT").unwrap();
let mut img_paths = Vec::with_capacity(input.len());
log::info!("Checking inputs");
for value in input {
#[cfg(feature = "web_image")]
if value.starts_with("http") {
log::debug!("Input {} is a URL", value);
img_paths.push(value);
continue;
}
let path = Path::new(value);
if !path.exists() {
fatal_error(&format!("File {value} does not exist"), Some(66));
} else if !Path::new(path).is_file() {
fatal_error(&format!("{value} is not a file"), Some(66));
}
log::debug!("Input {} is a file", value);
img_paths.push(value);
}
let density = match matches
.get_one::<String>("characters")
.map(|res| res.as_str())
{
Some("short") | Some("s") | Some("0") => r#"Ñ@#W$9876543210?!abc;:+=-,._ "#,
Some("flat") | Some("f") | Some("1") => r#"MWNXK0Okxdolc:;,'... "#,
Some("long") | Some("l") | Some("2") => {
r#"$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,"^`'. "#
}
Some(chars) if !chars.is_empty() => {
log::debug!("Using user provided characters");
chars
}
_ => {
log::debug!("Using default characters");
r#"MWNXK0Okxdolc:;,'... "#
}
};
log::debug!("Characters used: '{density}'");
config_builder.characters(density.to_string());
config_builder.dimension(config::ResizingDimension::Width);
let terminal_size = |height: bool| -> u32 {
terminal_size::terminal_size()
.map(|size| if height { size.1 .0 } else { size.0 .0 } as u32)
.unwrap_or_else(|| {
fatal_error(
"Failed to read terminal size, STDOUT is not a tty",
Some(72),
)
})
};
let height = matches.get_flag("height");
let target_size = if matches.get_flag("width") || height {
if height {
config_builder.dimension(config::ResizingDimension::Height);
}
terminal_size(height)
} else {
log::trace!("Using user input size as target size");
*matches.get_one::<u32>("size").unwrap_or_else(|| {
fatal_error(
"Failed to read terminal size, STDOUT is not a tty",
Some(72),
)
})
}
.max(20);
log::debug!("Target Size: {target_size}");
config_builder.target_size(NonZeroU32::new(target_size).unwrap());
let Some(scale) = matches.get_one::<f32>("scale").map(|scale| {
scale.clamp(
0.1f32, 1f32, )
}) else {
fatal_error("Could not work with ratio input value", Some(65));
};
log::debug!("Scale: {scale}");
config_builder.scale(scale);
let invert = matches.get_flag("invert-density");
log::debug!("Invert is set to: {invert}");
config_builder.invert(invert);
let background_color = matches.get_flag("background-color");
log::debug!("BackgroundColor is set to: {background_color}");
config_builder.background_color(background_color);
let color = if matches.get_flag("no-color") {
log::info!("Using non-colored ascii");
false
} else {
if matches.get_flag("outline") {
log::warn!("Using outline, result will only be in grayscale");
}
log::info!("Using colored ascii");
if !*artem::SUPPORTS_TRUECOLOR {
if background_color {
log::warn!("Background flag will be ignored, since truecolor is not supported.")
}
log::warn!("Truecolor is not supported. Using ansi color.")
} else {
log::info!("Using truecolor ascii")
}
true
};
config_builder.color(color);
let border = matches.get_flag("border");
config_builder.border(border);
log::info!("Using border: {border}");
let transform_x = matches.get_flag("flipX");
config_builder.transform_x(transform_x);
log::debug!("Flipping X-Axis: {transform_x}");
let transform_y = matches.get_flag("flipY");
config_builder.transform_y(transform_y);
log::debug!("Flipping Y-Axis: {transform_y}");
let center_x = matches.get_flag("centerX");
config_builder.center_x(center_x);
log::debug!("Centering X-Axis: {center_x}");
let center_y = matches.get_flag("centerY");
config_builder.center_y(center_y);
log::debug!("Center Y-Axis: {center_y}");
let outline = matches.get_flag("outline");
config_builder.outline(outline);
log::debug!("Outline: {outline}");
if outline {
let hysteresis = matches.get_flag("hysteresis");
config_builder.hysteresis(hysteresis);
log::debug!("Hysteresis: {hysteresis}");
if hysteresis {
log::warn!("Using hysteresis might result in an worse looking ascii image than only using --outline")
}
}
if let Some(output_file) = matches.get_one::<PathBuf>("output-file") {
log::debug!("Output-file: {}", output_file.to_str().unwrap());
let file_extension = output_file.extension().and_then(std::ffi::OsStr::to_str);
log::debug!("FileExtension: {:?}", file_extension);
config_builder.target(match file_extension {
Some("html") | Some("htm") => {
log::debug!("Target: Html-File");
TargetType::HtmlFile
}
Some("ansi") | Some("ans") => {
log::debug!("Target: Ansi-File");
if matches.get_flag("no-color") {
log::warn!("The --no-color argument conflicts with the target file type. Falling back to plain text file without colors.");
TargetType::File
} else {
if !*artem::SUPPORTS_TRUECOLOR {
log::warn!("truecolor is disabled, output file will not use truecolor chars")
}
TargetType::AnsiFile
}
}
Some("svg") => {
log::debug!("Target: SVG");
TargetType::Svg
}
_ => {
log::debug!("Target: File");
if !matches.get_flag("no-color") {
log::warn!("Filetype does not support using colors. For colored output file please use either .html or .ansi files");
}
TargetType::File
}
});
} else {
log::debug!("Target: Shell");
config_builder.target(TargetType::Shell);
}
let config = config_builder.build();
let mut output = img_paths
.iter()
.map(|path| load_image(path))
.filter(|img| img.height() != 0 || img.width() != 0)
.map(|img| artem::convert(img, &config))
.collect::<String>();
if output.ends_with('\n') {
output.remove(output.len() - 1);
}
if let Some(output_file) = matches.get_one::<PathBuf>("output-file") {
log::info!("Writing output to output file");
let Ok(mut file) = File::create(output_file) else {
fatal_error("Could not create output file", Some(73));
};
if config.target == TargetType::Svg {
output = anstyle_svg::Term::new().render_svg(&output);
}
log::trace!("Created output file");
let Ok(bytes_count) = file.write(output.as_bytes()) else {
fatal_error("Could not write to output file", Some(74));
};
log::info!("Written ascii chars to output file");
println!("Written {} bytes to {}", bytes_count, output_file.display())
} else {
log::info!("Printing output");
println!("{}", output);
}
}
fn load_image(path: &str) -> image::DynamicImage {
#[cfg(feature = "web_image")]
if path.starts_with("http") {
log::info!("Started to download image from: {}", path);
let now = std::time::Instant::now();
let Ok(resp) = ureq::get(path).call() else {
fatal_error(
&format!("Failed to load image bytes from {}", path),
Some(66),
);
};
let mut bytes: Vec<u8> = Vec::new();
resp.into_reader()
.read_to_end(&mut bytes)
.expect("Failed to read bytes");
log::info!("Downloading took {:3} ms", now.elapsed().as_millis());
log::debug!("Opening downloaded image from memory");
return match image::load_from_memory(&bytes) {
Ok(img) => img,
Err(err) => fatal_error(&err.to_string(), Some(66)),
};
}
log::info!("Opening image");
match image::open(path) {
Ok(img) => img,
Err(err) => fatal_error(&err.to_string(), Some(66)),
}
}
pub fn fatal_error(message: &str, code: Option<i32>) -> ! {
log::error!("{}", message);
log::error!("Artem exited with code: {}", code.unwrap_or(1));
std::process::exit(code.unwrap_or(1));
}