Add EXIF-based rename template variables
Support {date}, {exif_date}, {camera}, and {original_ext} in rename
templates. Reads DateTimeOriginal and Model from EXIF metadata via
little_exif when these variables are used in templates.
This commit is contained in:
@@ -399,12 +399,17 @@ impl PipelineExecutor {
|
|||||||
|
|
||||||
if let Some(ref template) = rename.template {
|
if let Some(ref template) = rename.template {
|
||||||
let dims = Some((img.width(), img.height()));
|
let dims = Some((img.width(), img.height()));
|
||||||
let new_name = crate::operations::rename::apply_template(
|
let original_ext = source.path.extension()
|
||||||
|
.and_then(|e| e.to_str());
|
||||||
|
let new_name = crate::operations::rename::apply_template_full(
|
||||||
template,
|
template,
|
||||||
stem,
|
stem,
|
||||||
ext,
|
ext,
|
||||||
rename.counter_start + index as u32,
|
rename.counter_start + index as u32,
|
||||||
dims,
|
dims,
|
||||||
|
original_ext,
|
||||||
|
Some(&source.path),
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
job.output_dir.join(new_name)
|
job.output_dir.join(new_name)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -6,11 +6,25 @@ pub fn apply_template(
|
|||||||
ext: &str,
|
ext: &str,
|
||||||
counter: u32,
|
counter: u32,
|
||||||
dimensions: Option<(u32, u32)>,
|
dimensions: Option<(u32, u32)>,
|
||||||
|
) -> String {
|
||||||
|
apply_template_full(template, name, ext, counter, dimensions, None, None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_template_full(
|
||||||
|
template: &str,
|
||||||
|
name: &str,
|
||||||
|
ext: &str,
|
||||||
|
counter: u32,
|
||||||
|
dimensions: Option<(u32, u32)>,
|
||||||
|
original_ext: Option<&str>,
|
||||||
|
source_path: Option<&Path>,
|
||||||
|
exif_info: Option<&ExifRenameInfo>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let mut result = template.to_string();
|
let mut result = template.to_string();
|
||||||
|
|
||||||
result = result.replace("{name}", name);
|
result = result.replace("{name}", name);
|
||||||
result = result.replace("{ext}", ext);
|
result = result.replace("{ext}", ext);
|
||||||
|
result = result.replace("{original_ext}", original_ext.unwrap_or(ext));
|
||||||
|
|
||||||
// Handle {counter:N} with padding
|
// Handle {counter:N} with padding
|
||||||
if let Some(start) = result.find("{counter:") {
|
if let Some(start) = result.find("{counter:") {
|
||||||
@@ -30,9 +44,111 @@ pub fn apply_template(
|
|||||||
result = result.replace("{height}", &h.to_string());
|
result = result.replace("{height}", &h.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// {date} - today's date
|
||||||
|
if result.contains("{date}") {
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
let days = now / 86400;
|
||||||
|
// Simple date calculation (good enough for filenames)
|
||||||
|
let (y, m, d) = days_to_ymd(days);
|
||||||
|
result = result.replace("{date}", &format!("{:04}-{:02}-{:02}", y, m, d));
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXIF-based variables
|
||||||
|
if let Some(info) = exif_info {
|
||||||
|
result = result.replace("{exif_date}", &info.date_taken);
|
||||||
|
result = result.replace("{camera}", &info.camera_model);
|
||||||
|
} else if result.contains("{exif_date}") || result.contains("{camera}") {
|
||||||
|
// Try to read EXIF from source file
|
||||||
|
let info = source_path.map(read_exif_rename_info).unwrap_or_default();
|
||||||
|
result = result.replace("{exif_date}", &info.date_taken);
|
||||||
|
result = result.replace("{camera}", &info.camera_model);
|
||||||
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ExifRenameInfo {
|
||||||
|
pub date_taken: String,
|
||||||
|
pub camera_model: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_exif_rename_info(path: &Path) -> ExifRenameInfo {
|
||||||
|
let Ok(metadata) = little_exif::metadata::Metadata::new_from_path(path) else {
|
||||||
|
return ExifRenameInfo::default();
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut info = ExifRenameInfo::default();
|
||||||
|
let endian = metadata.get_endian();
|
||||||
|
|
||||||
|
// Read DateTimeOriginal
|
||||||
|
if let Some(tag) = metadata.get_tag(&little_exif::exif_tag::ExifTag::DateTimeOriginal(String::new())) {
|
||||||
|
let bytes = tag.value_as_u8_vec(endian);
|
||||||
|
if let Ok(s) = std::str::from_utf8(&bytes) {
|
||||||
|
// Convert "2026:03:06 14:30:00" to "2026-03-06"
|
||||||
|
let clean = s.trim_end_matches('\0');
|
||||||
|
if let Some(date_part) = clean.split(' ').next() {
|
||||||
|
info.date_taken = date_part.replace(':', "-");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read camera Model
|
||||||
|
if let Some(tag) = metadata.get_tag(&little_exif::exif_tag::ExifTag::Model(String::new())) {
|
||||||
|
let bytes = tag.value_as_u8_vec(endian);
|
||||||
|
if let Ok(s) = std::str::from_utf8(&bytes) {
|
||||||
|
info.camera_model = s.trim_end_matches('\0')
|
||||||
|
.replace(' ', "_")
|
||||||
|
.replace('/', "-")
|
||||||
|
.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.date_taken.is_empty() {
|
||||||
|
info.date_taken = "unknown-date".to_string();
|
||||||
|
}
|
||||||
|
if info.camera_model.is_empty() {
|
||||||
|
info.camera_model = "unknown-camera".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn days_to_ymd(total_days: u64) -> (u64, u64, u64) {
|
||||||
|
// Simplified Gregorian calendar calculation
|
||||||
|
let mut days = total_days;
|
||||||
|
let mut year = 1970u64;
|
||||||
|
loop {
|
||||||
|
let days_in_year = if is_leap(year) { 366 } else { 365 };
|
||||||
|
if days < days_in_year {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
days -= days_in_year;
|
||||||
|
year += 1;
|
||||||
|
}
|
||||||
|
let months_days: [u64; 12] = if is_leap(year) {
|
||||||
|
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||||
|
} else {
|
||||||
|
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||||
|
};
|
||||||
|
let mut month = 1u64;
|
||||||
|
for md in &months_days {
|
||||||
|
if days < *md {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
days -= md;
|
||||||
|
month += 1;
|
||||||
|
}
|
||||||
|
(year, month, days + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_leap(year: u64) -> bool {
|
||||||
|
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
|
||||||
|
}
|
||||||
|
|
||||||
pub fn resolve_collision(path: &Path) -> PathBuf {
|
pub fn resolve_collision(path: &Path) -> PathBuf {
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return path.to_path_buf();
|
return path.to_path_buf();
|
||||||
|
|||||||
Reference in New Issue
Block a user