131 lines
4.2 KiB
Python
131 lines
4.2 KiB
Python
|
|
"""
|
||
|
|
Given a source folder and a destination folder, this script will copy all images
|
||
|
|
and add to them a white date stamp in the correct orientation at the correct size.
|
||
|
|
|
||
|
|
It uses ImageMagick, so make sure you've it installed already!
|
||
|
|
http://www.imagemagick.org
|
||
|
|
"""
|
||
|
|
import logging
|
||
|
|
from pathlib import Path
|
||
|
|
import subprocess
|
||
|
|
|
||
|
|
import exifread as exif
|
||
|
|
|
||
|
|
import logger
|
||
|
|
|
||
|
|
# log is preset by the logger module
|
||
|
|
log = logging.getLogger(logger.LOG_NAME)
|
||
|
|
|
||
|
|
|
||
|
|
SRC_PATH = Path("test-folder/source")
|
||
|
|
DST_PATH = Path("test-folder/destination")
|
||
|
|
USED_KEYS = {
|
||
|
|
"Image Orientation",
|
||
|
|
"EXIF DateTimeOriginal",
|
||
|
|
"EXIF ExifImageWidth",
|
||
|
|
"EXIF ExifImageLength",
|
||
|
|
}
|
||
|
|
DATE_SEP = "."
|
||
|
|
SIZE_REF = 2160
|
||
|
|
|
||
|
|
COVERAGE_FILE = Path("coverage.txt")
|
||
|
|
|
||
|
|
|
||
|
|
def get_metadata(path, keys):
|
||
|
|
"""Helper function to retrieve specific tags."""
|
||
|
|
with open(path, "rb") as file:
|
||
|
|
tags = exif.process_file(file, details=False)
|
||
|
|
|
||
|
|
shorten = lambda item: item[0] if len(item) == 1 else item
|
||
|
|
|
||
|
|
return {k: shorten(tags[k].values) for k in keys}
|
||
|
|
|
||
|
|
|
||
|
|
def stamp(file):
|
||
|
|
"""Datestamps the image pointed to by file."""
|
||
|
|
meta = get_metadata(file, USED_KEYS)
|
||
|
|
|
||
|
|
# EXIF Date format: "YYYY:MM:DD HH:MM:SS" with time shown in 24-hour format
|
||
|
|
date = meta["EXIF DateTimeOriginal"].split()[0].split(":")[::-1]
|
||
|
|
date_stamp = DATE_SEP.join(date)
|
||
|
|
|
||
|
|
min_dimension = min(meta["EXIF ExifImageWidth"], meta["EXIF ExifImageLength"])
|
||
|
|
pointsize = (min_dimension * 100) // SIZE_REF
|
||
|
|
offset_ratio = (min_dimension * 15) // SIZE_REF
|
||
|
|
|
||
|
|
# ⚠ Warning: magic numbers ahead
|
||
|
|
orientation = meta["Image Orientation"]
|
||
|
|
if orientation == 6:
|
||
|
|
angle = "270x270"
|
||
|
|
gravity = "NorthEast"
|
||
|
|
y_offset = 2 * offset_ratio
|
||
|
|
x_offset = 6 * offset_ratio
|
||
|
|
elif orientation == 8:
|
||
|
|
angle = "90x90"
|
||
|
|
gravity = "SouthWest"
|
||
|
|
y_offset = 37 * offset_ratio
|
||
|
|
x_offset = 2 * offset_ratio
|
||
|
|
elif orientation == 3:
|
||
|
|
angle = "180x180"
|
||
|
|
gravity = "NorthWest"
|
||
|
|
y_offset = 8 * offset_ratio
|
||
|
|
x_offset = 37 * offset_ratio
|
||
|
|
elif orientation == 1:
|
||
|
|
angle = ""
|
||
|
|
gravity = "SouthEast"
|
||
|
|
y_offset = 0
|
||
|
|
x_offset = offset_ratio
|
||
|
|
else:
|
||
|
|
log.error(f"🔴 Unkown orientation '{orientation}' for file {file}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
log.info(f"Processing {file}...")
|
||
|
|
log.debug(
|
||
|
|
f"Metadata: {orientation=}, "
|
||
|
|
f"{meta['EXIF ExifImageWidth']}x{meta['EXIF ExifImageLength']}, "
|
||
|
|
f"{meta['EXIF DateTimeOriginal']}"
|
||
|
|
)
|
||
|
|
# Gestion de l'argument de commande sur Windows : https://stackoverflow.com/a/35900070
|
||
|
|
# À explorer, notamment pour l'utilisation de l'argument "shell"
|
||
|
|
command = (
|
||
|
|
f'convert "{file}" '
|
||
|
|
f"-gravity {gravity} "
|
||
|
|
f"-fill white "
|
||
|
|
f"-pointsize {pointsize} "
|
||
|
|
f"-annotate {angle}+{x_offset}+{y_offset} {date_stamp} "
|
||
|
|
f'"{DST_PATH}\{file.name}"'
|
||
|
|
)
|
||
|
|
log.debug(f"Executing command: {command}")
|
||
|
|
result = subprocess.run(command, capture_output=True, text=True)
|
||
|
|
if result.returncode != 0:
|
||
|
|
log.error(f"🔴 Command failed for {file}: {result.stderr}")
|
||
|
|
return None
|
||
|
|
else:
|
||
|
|
return f"{meta['EXIF ExifImageWidth']}x{meta['EXIF ExifImageLength']}_{orientation}{file.suffix.lower()}"
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
log.info("📷📆 - Welcome to Photo DateStamper!")
|
||
|
|
log.info(f"📝 Log file: {logger.LOG_FILE}")
|
||
|
|
DST_PATH.mkdir(parents=True, exist_ok=True)
|
||
|
|
COVERAGE_FILE.touch(exist_ok=True)
|
||
|
|
with open(COVERAGE_FILE, "r") as file:
|
||
|
|
lines = file.readlines()
|
||
|
|
coverage = {line.split()[0]: line.split()[1] for line in lines}
|
||
|
|
new_coverage = dict()
|
||
|
|
try:
|
||
|
|
for file in SRC_PATH.iterdir():
|
||
|
|
fingerprint = stamp(file)
|
||
|
|
if fingerprint is not None and fingerprint not in coverage:
|
||
|
|
new_coverage[fingerprint] = str(file)
|
||
|
|
finally:
|
||
|
|
if new_coverage:
|
||
|
|
log.info(
|
||
|
|
f"🟡 Execution encountered {len(new_coverage)} new combinations, please report them."
|
||
|
|
)
|
||
|
|
log.info(f"📝 Updating coverage file: {COVERAGE_FILE}")
|
||
|
|
with open(COVERAGE_FILE, "a", encoding="UTF-8") as file:
|
||
|
|
for k, v in new_coverage.items():
|
||
|
|
file.write(f"{k} {v}\n")
|
||
|
|
|
||
|
|
log.info("🟢 Processing done.")
|