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:
91
pixstrip-core/src/operations/metadata.rs
Normal file
91
pixstrip-core/src/operations/metadata.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::error::{PixstripError, Result};
|
||||
|
||||
use super::MetadataConfig;
|
||||
|
||||
pub fn strip_metadata(
|
||||
input: &Path,
|
||||
output: &Path,
|
||||
config: &MetadataConfig,
|
||||
) -> Result<()> {
|
||||
match config {
|
||||
MetadataConfig::KeepAll => {
|
||||
std::fs::copy(input, output).map_err(PixstripError::Io)?;
|
||||
}
|
||||
MetadataConfig::StripAll => {
|
||||
strip_all_exif(input, output)?;
|
||||
}
|
||||
MetadataConfig::Privacy => {
|
||||
// Privacy mode strips GPS and camera info but keeps copyright.
|
||||
// For now, we strip all EXIF as a safe default.
|
||||
// Selective tag preservation requires full EXIF parsing.
|
||||
strip_all_exif(input, output)?;
|
||||
}
|
||||
MetadataConfig::Custom { .. } => {
|
||||
// Custom selective stripping - simplified to strip-all for now.
|
||||
// Full selective stripping requires per-tag EXIF manipulation.
|
||||
strip_all_exif(input, output)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn strip_all_exif(input: &Path, output: &Path) -> Result<()> {
|
||||
let data = std::fs::read(input).map_err(PixstripError::Io)?;
|
||||
let cleaned = remove_exif_from_jpeg(&data);
|
||||
std::fs::write(output, cleaned).map_err(PixstripError::Io)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_exif_from_jpeg(data: &[u8]) -> Vec<u8> {
|
||||
// Only process JPEG files (starts with FF D8)
|
||||
if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
|
||||
return data.to_vec();
|
||||
}
|
||||
|
||||
let mut result = Vec::with_capacity(data.len());
|
||||
result.push(0xFF);
|
||||
result.push(0xD8);
|
||||
|
||||
let mut i = 2;
|
||||
while i + 1 < data.len() {
|
||||
if data[i] != 0xFF {
|
||||
result.extend_from_slice(&data[i..]);
|
||||
break;
|
||||
}
|
||||
|
||||
let marker = data[i + 1];
|
||||
|
||||
// APP1 (0xE1) contains EXIF - skip it
|
||||
if marker == 0xE1 && i + 3 < data.len() {
|
||||
let len = ((data[i + 2] as usize) << 8) | (data[i + 3] as usize);
|
||||
i += 2 + len;
|
||||
continue;
|
||||
}
|
||||
|
||||
// SOS (0xDA) - start of scan, copy everything from here
|
||||
if marker == 0xDA {
|
||||
result.extend_from_slice(&data[i..]);
|
||||
break;
|
||||
}
|
||||
|
||||
// Other markers - keep them
|
||||
if i + 3 < data.len() {
|
||||
let len = ((data[i + 2] as usize) << 8) | (data[i + 3] as usize);
|
||||
let end = i + 2 + len;
|
||||
if end <= data.len() {
|
||||
result.extend_from_slice(&data[i..end]);
|
||||
i = end;
|
||||
} else {
|
||||
result.extend_from_slice(&data[i..]);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
result.extend_from_slice(&data[i..]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
pub mod metadata;
|
||||
pub mod rename;
|
||||
pub mod resize;
|
||||
pub mod watermark;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
64
pixstrip-core/src/operations/rename.rs
Normal file
64
pixstrip-core/src/operations/rename.rs
Normal 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))
|
||||
}
|
||||
31
pixstrip-core/src/operations/watermark.rs
Normal file
31
pixstrip-core/src/operations/watermark.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use crate::types::Dimensions;
|
||||
use super::WatermarkPosition;
|
||||
|
||||
pub fn calculate_position(
|
||||
position: WatermarkPosition,
|
||||
image_size: Dimensions,
|
||||
watermark_size: Dimensions,
|
||||
margin: u32,
|
||||
) -> (u32, u32) {
|
||||
let iw = image_size.width;
|
||||
let ih = image_size.height;
|
||||
let ww = watermark_size.width;
|
||||
let wh = watermark_size.height;
|
||||
|
||||
let center_x = (iw - ww) / 2;
|
||||
let center_y = (ih - wh) / 2;
|
||||
let right_x = iw - ww - margin;
|
||||
let bottom_y = ih - wh - margin;
|
||||
|
||||
match position {
|
||||
WatermarkPosition::TopLeft => (margin, margin),
|
||||
WatermarkPosition::TopCenter => (center_x, margin),
|
||||
WatermarkPosition::TopRight => (right_x, margin),
|
||||
WatermarkPosition::MiddleLeft => (margin, center_y),
|
||||
WatermarkPosition::Center => (center_x, center_y),
|
||||
WatermarkPosition::MiddleRight => (right_x, center_y),
|
||||
WatermarkPosition::BottomLeft => (margin, bottom_y),
|
||||
WatermarkPosition::BottomCenter => (center_x, bottom_y),
|
||||
WatermarkPosition::BottomRight => (right_x, bottom_y),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user