use std::path::PathBuf;
use std::process::ExitCode;
use snitch::cli::{self, Args, Parsed};
use snitch::strip;
const HELP: &str = "\
usage: no-comment [-o OUT] [--in-place] [--force] [--] FILE...
Strip metadata out of an image. Losslessly: the pixels do not change.
JPEG and PNG only. Nothing is re-encoded, and every write is checked by comparing the decoded
pixels before and after, so \"pixels byte-identical\" is a result and not a promise.
options:
-o, --out OUT output file, or an existing directory for multiple inputs
--in-place atomically replace each input file
--force replace an existing output file
--version print the version
-h, --help this
In-pixel watermarks are not touched by metadata stripping.";
const WATERMARK_HEAD: &str = "In-pixel watermarks are not touched by metadata stripping.";
const WATERMARK_TAIL: &str =
" SynthID and its relatives live in image data; removal attempts repaint pixels.";
fn main() -> ExitCode {
cli::quiet_on_closed_pipe();
let argv: Vec<String> = std::env::args().skip(1).collect();
let parsed = match Args::parse(argv, &["in-place", "force"], &["out", "o"]) {
Ok(p) => p,
Err(e) => {
eprintln!("no-comment: {e}\n\n{HELP}");
return ExitCode::from(2);
}
};
let a = match parsed {
Parsed::Help => {
println!("{HELP}");
return ExitCode::SUCCESS;
}
Parsed::Version => {
println!("no-comment {}", snitch::VERSION);
return ExitCode::SUCCESS;
}
Parsed::Args(a) => a,
};
if a.files.is_empty() {
eprintln!("{HELP}");
return ExitCode::from(2);
}
let out = a.get("out").or_else(|| a.get("o")).map(PathBuf::from);
if a.has("in-place") && out.is_some() {
eprintln!("no-comment: --in-place and --out are alternatives, not a pair");
return ExitCode::from(2);
}
if a.has("in-place") && a.has("force") {
eprintln!("no-comment: --force is only meaningful with copied output");
return ExitCode::from(2);
}
let targets = match resolve_targets(&a.files, out.as_deref(), a.has("in-place")) {
Ok(targets) => targets,
Err(e) => {
eprintln!("no-comment: {e}");
return ExitCode::from(2);
}
};
let mut failed = false;
for (source, target) in a.files.iter().zip(targets) {
if !source.exists() {
eprintln!(" {}: not found", source.display());
failed = true;
continue;
}
if !source.is_file() {
eprintln!(" {}: not a regular file", source.display());
failed = true;
continue;
}
let in_place = a.has("in-place");
if !in_place && target.exists() && !a.has("force") {
eprintln!(
" {}: output exists; pass --force to replace it",
target.display()
);
failed = true;
continue;
}
match strip::strip_atomic(source, &target) {
Ok((removed, identical)) => {
let proof = if identical {
cli::c(" pixels byte-identical", cli::GRN)
} else {
cli::c(
" PIXELS CHANGED, this is a bug, please report it",
cli::RED,
)
};
let name = if in_place { source } else { &target };
println!(
" {} removed {} bytes of metadata{proof}",
name.file_name().unwrap_or_default().to_string_lossy(),
cli::thousands(removed),
);
}
Err(e) => {
eprintln!(" {}: {e}", source.display());
failed = true;
}
}
}
println!("\n {}", cli::c(WATERMARK_HEAD, cli::BOLD));
println!("{WATERMARK_TAIL}");
if failed {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
fn resolve_targets(
files: &[PathBuf],
out: Option<&std::path::Path>,
in_place: bool,
) -> Result<Vec<PathBuf>, String> {
if in_place {
return Ok(files.to_vec());
}
let targets: Vec<PathBuf> = match out {
Some(out) if out.is_dir() => files
.iter()
.map(|f| {
let name = f.file_name().unwrap_or_default();
strip::outpath(&out.join(name), None, "-clean")
})
.collect(),
Some(_) if files.len() > 1 => {
return Err(
"--out must be an existing directory when processing multiple files".into(),
);
}
Some(out) => vec![out.to_path_buf()],
None => files
.iter()
.map(|f| strip::outpath(f, None, "-clean"))
.collect(),
};
let mut seen: Vec<PathBuf> = Vec::new();
for target in &targets {
let key = target.canonicalize().unwrap_or_else(|_| target.clone());
if seen.contains(&key) {
return Err("multiple inputs would write the same output filename".into());
}
seen.push(key);
}
Ok(targets)
}