use anyhow::Context;
use oximg::pipeline::{self, Encoder, ImageFormat, Params};
pub fn print_help() {
println!(
"oximg {}\n\n\
High-performance image compression: HTTP server and one-shot CLI.\n\n\
Usage:\n \
oximg [serve]\n \
Run the HTTP resize server (the default). All server\n \
configuration is via environment variables (PORT, IMAGES_DIR,\n \
OXIMG_*); see the README.\n \
oximg resize <in> <max_w> <max_h> <out> [options]\n \
Fit one image within <max_w> x <max_h> (never enlarges) and\n \
re-encode it. Output format: --format, else the <out> file\n \
extension, else the source's own format.\n \
-q, --quality N JPEG quality, 1-100 (default 80)\n \
-f, --format FMT jpg | png | webp | avif\n \
--preset P jpegli (default) | fast | small\n \
oximg probe <file>\n \
Print the format and stored dimensions (header-only, no\n \
pixel decode).\n \
oximg --version | --help",
env!("CARGO_PKG_VERSION")
);
}
fn usage_error(msg: &str) -> ! {
eprintln!("oximg: {msg} (try --help)");
std::process::exit(2);
}
pub fn resize(args: &[String]) -> anyhow::Result<()> {
if let Err(e) = oximg::config_validate() {
eprintln!("oximg: fatal: {e}");
std::process::exit(2);
}
let mut positional: Vec<&str> = Vec::new();
let mut quality = 80.0f32;
let mut explicit: Option<ImageFormat> = None;
let mut encoder = Encoder::Jpegli;
let mut it = args.iter();
while let Some(arg) = it.next() {
match arg.as_str() {
"-h" | "--help" => {
print_help();
return Ok(());
}
"-q" | "--quality" => {
let v = it
.next()
.unwrap_or_else(|| usage_error("--quality needs a value"));
quality = v
.parse()
.ok()
.filter(|q| (1.0..=100.0).contains(q))
.unwrap_or_else(|| usage_error(&format!("invalid quality {v:?} (1-100)")));
}
"-f" | "--format" => {
let v = it
.next()
.unwrap_or_else(|| usage_error("--format needs a value"));
explicit = Some(ImageFormat::from_token(v).unwrap_or_else(|| {
usage_error(&format!("unknown format {v:?} (jpg|png|webp|avif)"))
}));
}
"--preset" => {
let v = it
.next()
.unwrap_or_else(|| usage_error("--preset needs a value"));
encoder = match v.as_str() {
"jpegli" => Encoder::Jpegli,
"fast" => Encoder::MozFast,
"small" => Encoder::MozSmall,
_ => usage_error(&format!("unknown preset {v:?} (jpegli|fast|small)")),
};
}
flag if flag.starts_with('-') && flag.len() > 1 => {
usage_error(&format!("unknown option {flag:?}"))
}
p => positional.push(p),
}
}
let [input, max_w, max_h, output] = positional[..] else {
usage_error("usage: oximg resize <in> <max_w> <max_h> <out> [-q N] [-f fmt] [--preset P]");
};
let dim = |v: &str, name: &str| -> u32 {
v.parse()
.ok()
.filter(|d| *d > 0)
.unwrap_or_else(|| usage_error(&format!("invalid {name} {v:?}")))
};
let params = Params {
max_width: dim(max_w, "max_w"),
max_height: dim(max_h, "max_h"),
quality,
encoder,
output: explicit.or_else(|| format_from_ext(output)),
..Default::default()
};
let (bytes, format) = pipeline::process_path(std::path::Path::new(input), ¶ms)
.with_context(|| format!("process {input}"))?;
std::fs::write(output, &bytes).with_context(|| format!("write {output}"))?;
let (_, w, h) = pipeline::probe(&bytes)?;
eprintln!(
"oximg: wrote {output} ({} bytes, {w}x{h}, {})",
bytes.len(),
format.content_type()
);
Ok(())
}
pub fn probe(args: &[String]) -> anyhow::Result<()> {
let [input] = args else {
usage_error("usage: oximg probe <file>");
};
if input == "-h" || input == "--help" {
print_help();
return Ok(());
}
let bytes = std::fs::read(input).with_context(|| format!("read {input}"))?;
let (format, w, h) = pipeline::probe(&bytes)?;
println!(
"{input}: {} {w}x{h} ({} stored pixels)",
format.content_type(),
w as u64 * h as u64
);
Ok(())
}
fn format_from_ext(path: &str) -> Option<ImageFormat> {
let ext = std::path::Path::new(path)
.extension()?
.to_str()?
.to_ascii_lowercase();
ImageFormat::from_token(&ext)
}