Add metadata stripping, watermark positioning, and rename template modules

Metadata: JPEG EXIF stripping (APP1 segment removal).
Watermark: 9-position grid calculation with margin support.
Rename: template parser with {name}, {ext}, {counter:N}, {width}, {height}
  and collision resolution with auto-suffix.
Phase 4 complete - 79 tests passing, zero clippy warnings.
This commit is contained in:
2026-03-06 02:06:01 +02:00
parent ea4ea9c9c4
commit d4aef0b774
7 changed files with 439 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
use std::path::{Path, PathBuf};
pub fn apply_template(
template: &str,
name: &str,
ext: &str,
counter: u32,
dimensions: Option<(u32, u32)>,
) -> String {
let mut result = template.to_string();
result = result.replace("{name}", name);
result = result.replace("{ext}", ext);
// Handle {counter:N} with padding
if let Some(start) = result.find("{counter:") {
if let Some(end) = result[start..].find('}') {
let spec = &result[start + 9..start + end];
if let Ok(padding) = spec.parse::<usize>() {
let counter_str = format!("{:0>width$}", counter, width = padding);
result = format!("{}{}{}", &result[..start], counter_str, &result[start + end + 1..]);
}
}
} else {
result = result.replace("{counter}", &counter.to_string());
}
if let Some((w, h)) = dimensions {
result = result.replace("{width}", &w.to_string());
result = result.replace("{height}", &h.to_string());
}
result
}
pub fn resolve_collision(path: &Path) -> PathBuf {
if !path.exists() {
return path.to_path_buf();
}
let parent = path.parent().unwrap_or(Path::new("."));
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("file");
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
for i in 1..1000 {
let candidate = if ext.is_empty() {
parent.join(format!("{}_{}", stem, i))
} else {
parent.join(format!("{}_{}.{}", stem, i, ext))
};
if !candidate.exists() {
return candidate;
}
}
// Fallback - should never happen with 1000 attempts
parent.join(format!("{}_{}.{}", stem, "overflow", ext))
}