Fix pipeline order, add selective metadata stripping, rename case/regex

This commit is contained in:
2026-03-06 18:12:18 +02:00
parent 8edaab0e2b
commit 62ad99d254
10 changed files with 208 additions and 17 deletions

View File

@@ -356,11 +356,6 @@ impl PipelineExecutor {
}
}
// Watermark (after resize so watermark is at correct scale)
if let Some(ref config) = job.watermark {
img = apply_watermark(img, config)?;
}
// Determine output format
let output_format = if let Some(ref convert) = job.convert {
let input_fmt = source.original_format.unwrap_or(ImageFormat::Jpeg);
@@ -401,9 +396,13 @@ impl PipelineExecutor {
let dims = Some((img.width(), img.height()));
let original_ext = source.path.extension()
.and_then(|e| e.to_str());
// Apply regex on the stem before template expansion
let working_stem = crate::operations::rename::apply_regex_replace(
stem, &rename.regex_find, &rename.regex_replace,
);
let new_name = crate::operations::rename::apply_template_full(
template,
stem,
&working_stem,
ext,
rename.counter_start + index as u32,
dims,
@@ -411,6 +410,17 @@ impl PipelineExecutor {
Some(&source.path),
None,
);
// Apply case conversion to the final name (without extension)
let new_name = if rename.case_mode > 0 {
if let Some(dot_pos) = new_name.rfind('.') {
let (name_part, ext_part) = new_name.split_at(dot_pos);
format!("{}{}", crate::operations::rename::apply_case_conversion(name_part, rename.case_mode), ext_part)
} else {
crate::operations::rename::apply_case_conversion(&new_name, rename.case_mode)
}
} else {
new_name
};
job.output_dir.join(new_name)
} else {
let new_name = rename.apply_simple(stem, ext, index as u32 + 1);
@@ -444,16 +454,31 @@ impl PipelineExecutor {
std::fs::create_dir_all(parent).map_err(PixstripError::Io)?;
}
// Watermark (after compress settings determined, before encode)
if let Some(ref config) = job.watermark {
img = apply_watermark(img, config)?;
}
// Encode and save
encoder.encode_to_file(&img, &output_path, output_format, quality)?;
// Metadata stripping: re-encoding through the image crate naturally
// strips all EXIF/metadata. No additional action is needed for
// StripAll, Privacy, or Custom modes. KeepAll mode would require
// copying EXIF tags back from the source file using little_exif.
// Metadata handling: re-encoding strips all EXIF by default.
// KeepAll: copy everything back from source.
// Privacy/Custom: copy metadata back, then selectively strip certain tags.
// StripAll: do nothing (already stripped by re-encoding).
if let Some(ref meta_config) = job.metadata {
if matches!(meta_config, crate::operations::MetadataConfig::KeepAll) {
copy_metadata_from_source(&source.path, &output_path);
match meta_config {
crate::operations::MetadataConfig::KeepAll => {
copy_metadata_from_source(&source.path, &output_path);
}
crate::operations::MetadataConfig::StripAll => {
// Already stripped by re-encoding - nothing to do
}
_ => {
// Privacy or Custom: copy all metadata back, then strip unwanted tags
copy_metadata_from_source(&source.path, &output_path);
strip_selective_metadata(&output_path, meta_config);
}
}
}
@@ -509,3 +534,67 @@ fn copy_metadata_from_source(source: &std::path::Path, output: &std::path::Path)
};
let _: std::result::Result<(), std::io::Error> = metadata.write_to_file(output);
}
fn strip_selective_metadata(
path: &std::path::Path,
config: &crate::operations::MetadataConfig,
) {
use little_exif::exif_tag::ExifTag;
use little_exif::metadata::Metadata;
// Read the metadata we just wrote back
let Ok(source_meta) = Metadata::new_from_path(path) else {
return;
};
// Build a set of tag IDs to strip
let mut strip_ids: Vec<u16> = Vec::new();
if config.should_strip_gps() {
// GPSInfo pointer (0x8825) - removing it strips the GPS sub-IFD reference
strip_ids.push(ExifTag::GPSInfo(Vec::new()).as_u16());
}
if config.should_strip_camera() {
strip_ids.push(ExifTag::Make(String::new()).as_u16());
strip_ids.push(ExifTag::Model(String::new()).as_u16());
strip_ids.push(ExifTag::LensModel(String::new()).as_u16());
strip_ids.push(ExifTag::LensMake(String::new()).as_u16());
strip_ids.push(ExifTag::SerialNumber(String::new()).as_u16());
strip_ids.push(ExifTag::LensSerialNumber(String::new()).as_u16());
strip_ids.push(ExifTag::LensInfo(Vec::new()).as_u16());
}
if config.should_strip_software() {
strip_ids.push(ExifTag::Software(String::new()).as_u16());
strip_ids.push(ExifTag::MakerNote(Vec::new()).as_u16());
}
if config.should_strip_timestamps() {
strip_ids.push(ExifTag::ModifyDate(String::new()).as_u16());
strip_ids.push(ExifTag::DateTimeOriginal(String::new()).as_u16());
strip_ids.push(ExifTag::CreateDate(String::new()).as_u16());
strip_ids.push(ExifTag::SubSecTime(String::new()).as_u16());
strip_ids.push(ExifTag::SubSecTimeOriginal(String::new()).as_u16());
strip_ids.push(ExifTag::SubSecTimeDigitized(String::new()).as_u16());
strip_ids.push(ExifTag::OffsetTime(String::new()).as_u16());
strip_ids.push(ExifTag::OffsetTimeOriginal(String::new()).as_u16());
strip_ids.push(ExifTag::OffsetTimeDigitized(String::new()).as_u16());
}
if config.should_strip_copyright() {
strip_ids.push(ExifTag::Copyright(String::new()).as_u16());
strip_ids.push(ExifTag::Artist(String::new()).as_u16());
strip_ids.push(ExifTag::OwnerName(String::new()).as_u16());
}
// Build new metadata with only the tags we want to keep
let mut new_meta = Metadata::new();
for tag in source_meta.data() {
if !strip_ids.contains(&tag.as_u16()) {
new_meta.set_tag(tag.clone());
}
}
let _: std::result::Result<(), std::io::Error> = new_meta.write_to_file(path);
}