use anyhow::Result;
use blind_watermark::prelude::*;
use clap::{ArgGroup, Args, Parser, Subcommand};
use colored::Colorize;
use ignore::WalkBuilder;
use indicatif::{ProgressBar, ProgressStyle};
use rayon::prelude::*;
use std::path::PathBuf;
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Embed(args) => run_embed(args),
Commands::Extract(args) => run_extract(args),
}
}
#[derive(Parser, Debug)]
#[command(name = "watermark", version, about, author)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Embed(EmbedArgs),
Extract(ExtractArgs),
}
#[derive(Args, Debug)]
#[command(group(
ArgGroup::new("output_mode")
.required(true)
.args(["output", "prefix"])
))]
pub struct EmbedArgs {
#[arg(short, long)]
pub input: PathBuf,
#[arg(short, long)]
pub string: String,
#[arg(short, long, conflicts_with = "prefix")]
pub output: Option<PathBuf>,
#[arg(short, long, conflicts_with = "output")]
pub prefix: Option<String>,
#[arg(long)]
pub seed: Option<u64>,
#[arg(short, long)]
pub recursive: bool,
}
#[derive(Args, Debug)]
pub struct ExtractArgs {
#[arg(short, long, value_parser = is_file)]
pub input: PathBuf,
#[arg(short, long)]
pub length: usize,
#[arg(short, long)]
pub seed: Option<u64>,
}
fn is_file(s: &str) -> Result<PathBuf, String> {
let p = PathBuf::from(s);
if p.is_file() {
Ok(p)
} else {
Err("extract --input only accepts a single file".into())
}
}
fn run_embed(args: EmbedArgs) {
if let Some(out) = args.output {
embed_watermark_string(args.input, out, &args.string, args.seed).unwrap();
} else {
let walk = match args.recursive {
true => WalkBuilder::new(&args.input)
.standard_filters(true)
.hidden(false)
.build(),
false => WalkBuilder::new(&args.input)
.standard_filters(true)
.hidden(false)
.max_depth(Some(1))
.build(),
};
let files: Vec<PathBuf> = walk
.filter_map(|e| {
if let Ok(e) = e
&& let Some(ext) = e.path().extension()
&& let Some(ext) = ext.to_str()
&& matches!(ext.to_lowercase().as_str(), "jpg" | "jpeg" | "png" | "webp")
{
Some(e.path().to_path_buf())
} else {
None
}
})
.collect();
let pb = ProgressBar::new(files.len() as u64);
let style = ProgressStyle::with_template(" Embedding [{bar:50}] {pos}/{len} ({eta})")
.unwrap()
.progress_chars("=> ");
pb.set_style(style);
let prefix = args
.prefix
.expect("--prefix is required for directory input");
files.par_iter().for_each(|input| {
let stem = input
.file_stem()
.expect("illformed input")
.to_str()
.expect("illformed input");
let ext = input
.extension()
.expect("illformed input")
.to_str()
.expect("illformed input");
let output = input.with_file_name(format!("{}{}.{}", prefix, stem, ext));
pb.println(format!(
" {} {}",
"Embedding".green().bold(),
input.display()
));
embed_watermark_string(input, &output, &args.string, args.seed).unwrap();
pb.inc(1);
});
pb.finish_and_clear();
println!("{}", " Done embedding.".yellow().bold());
}
}
fn run_extract(args: ExtractArgs) {
let extracted = extract_watermark_string(args.input, args.length, args.seed)
.expect("Failed to extract watermark");
println!(" {} {}", "Extracted".yellow().bold(), extracted);
}