""" 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.")