Skip to content

Instantly share code, notes, and snippets.

@gintsmurans
Last active March 22, 2023 23:18
Show Gist options
  • Save gintsmurans/27c841c3204040c99d46cf48ec9986a8 to your computer and use it in GitHub Desktop.
Save gintsmurans/27c841c3204040c99d46cf48ec9986a8 to your computer and use it in GitHub Desktop.
Image exif helpers
#!/usr/bin/env python3
import os
import re
import shutil
from datetime import datetime
from pathlib import Path
from PIL import Image
import argparse
import logging
image_file_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff']
video_file_extensions = ['.mp4', '.mov', '.avi', '.wmv', '.mkv', '.flv']
common_file_extensions = image_file_extensions + video_file_extensions
def moveImage(date_time, image_path, output_path, dry_run=True):
year = date_time.year
month = date_time.month
if month < 10:
month = f"0{month}"
output_dir = output_path/str(year)/str(month)
if image_path.suffix.lower() in video_file_extensions:
output_dir = output_path/"videos"/str(year)/str(month)
if f"{image_path}" == f"{output_dir/image_path.name}":
return
if dry_run:
logging.info(f"### Would move {image_path} to {output_dir/image_path.name} ###")
else:
output_dir.mkdir(parents=True, exist_ok=True)
shutil.move(image_path, output_dir/image_path.name.lower())
logging.info(f"### Moved {image_path} to {output_dir/image_path.name} ###")
def fixImages(input_path, output_path, dry_run=True):
logging.info(f"Will look for images in: {input_path} and output them to {output_path}")
input_path = Path(input_path)
output_path = Path(output_path)
image_files = list(input_path.rglob("*.*"))
total_files_count = len(image_files)
image_files = [f for f in image_files if f.suffix.lower() in common_file_extensions]
filtered_files_count = len(image_files)
processed = 0
logging.info(f"Found {total_files_count} total files from which {filtered_files_count} passed the filter and {total_files_count - filtered_files_count} will be skipped. Processing...")
for image_path in image_files:
try:
with Image.open(image_path) as img:
exif_data = img._getexif()
if exif_data:
date_time_original = exif_data.get(36867) or ''
date_time_original = date_time_original.replace("\r", " ").replace("\n", " ")
date_time_original = re.sub(r"(\d{4}).(\d{2}).(\d{2}).*", r"\1-\2-\3", date_time_original)
try:
date_time = datetime.strptime(date_time_original, '%Y-%m-%d')
moveImage(date_time, image_path, output_path, dry_run)
continue
except:
logging.debug(f"Could not parse date from {date_time_original}. Moving on on file data.")
except Exception as e:
logging.debug(f"Could not process {image_path} with error: {e}")
logging.debug(f"Could not find exif data for {image_path}")
creation_time = datetime.fromtimestamp(os.path.getctime(image_path))
last_modified_time = datetime.fromtimestamp(os.path.getmtime(image_path))
use_date = last_modified_time if last_modified_time < creation_time else creation_time
moveImage(use_date, image_path, output_path, dry_run)
processed += 1
if processed % 500 == 0:
logging.info(f"Processed: {processed} / {filtered_files_count}")
def main():
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('input_path', metavar='input', type=str,
help='path to the input directory')
parser.add_argument('output_path', metavar='output', type=str, help='path to the output directory')
parser.add_argument('--no-dry-run', dest='dry_run', action='store_false',
help='Run the script and actually move the files')
args = parser.parse_args()
fixImages(args.input_path, args.output_path, args.dry_run)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
main()
#!/bin/env python3
import os
import sys
import re
import argparse
import logging
from datetime import datetime
from pathlib import Path
from PIL import Image
image_file_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff']
video_file_extensions = ['.mp4', '.mov', '.avi', '.wmv', '.mkv', '.flv']
def update_file_creation_date(filename: str, year: int, month: int = None, day: int = None, dry_run: bool = True) -> None:
"""
Update the file creation date of an image file to the specified date.
Args:
filename (str): The filename of the image file.
year (int): The year to update the creation date to.
month (int): The month to update the creation date to.
day (int): The day to update the creation date to.
dry_run (bool): If True, do a dry run without making any changes.
If False, update the EXIF data in the file.
"""
creation_time = datetime.fromtimestamp(os.path.getctime(filename))
last_modified_time = datetime.fromtimestamp(os.path.getmtime(filename))
date_time = last_modified_time if last_modified_time < creation_time else creation_time
# Create a new creation date with the same month, day, and time, but with the specified year
new_date = date_time.replace(year=year)
if month is not None:
new_date = new_date.replace(month=month)
if day is not None:
new_date = new_date.replace(day=day)
# Skip
if new_date == date_time:
logging.info(f"{filename} already has the correct creation date.")
return
# Convert the new creation date to the format expected by EXIF
new_date_string = new_date.strftime("%Y:%m:%d %H:%M:%S")
if dry_run:
logging.info(f"{filename} would be updated to creation date {new_date_string}")
else:
logging.info(f"Updating file creation date for {filename} to {new_date_string}")
new_creation_time = datetime(year, month or new_date.month, day or new_date.day, new_date.hour, new_date.minute, new_date.second)
os.utime(filename, (new_creation_time.timestamp(), new_creation_time.timestamp()))
def update_exif_creation_date(filename: str, year: int, month: int = None, day: int = None, dry_run: bool = True) -> None:
"""
Update both the EXIF creation date and file creation date of an image file to the specified date.
Args:
filename (str): The filename of the image file.
year (int): The year to update the creation date to.
month (int): The month to update the creation date to.
day (int): The day to update the creation date to.
dry_run (bool): If True, do a dry run without making any changes.
If False, update the EXIF data in the file.
"""
with Image.open(filename) as img:
exif_data = img._getexif()
if exif_data:
date_time_original = exif_data.get(36867) or ''
date_time_original = date_time_original.replace("\r", " ").replace("\n", " ")
date_time_original = re.sub(r"(\d{4}).(\d{2}).(\d{2}).*", r"\1-\2-\3", date_time_original)
date_time = datetime.strptime(date_time_original, '%Y-%m-%d')
else:
logging.debug(f"Could not find exif data for {filename}")
creation_time = datetime.fromtimestamp(os.path.getctime(filename))
last_modified_time = datetime.fromtimestamp(os.path.getmtime(filename))
date_time = last_modified_time if last_modified_time < creation_time else creation_time
# Create a new creation date with the same month, day, and time, but with the specified year
new_date = date_time.replace(year=year)
if month is not None:
new_date = new_date.replace(month=month)
if day is not None:
new_date = new_date.replace(day=day)
# Skip
if new_date == date_time:
logging.info(f"{filename} already has the correct creation date.")
return
# Convert the new creation date to the format expected by EXIF
new_date_string = new_date.strftime("%Y:%m:%d %H:%M:%S")
if dry_run:
logging.info(f"{filename} would be updated to creation date {new_date_string}")
else:
logging.info(f"Updating EXIF creation date for {filename} to {new_date_string}")
# Update the EXIF creation date in the file
exif_data = img.getexif()
# DateTimeOriginal within the Exif IFD
exif_data.get_ifd(34665)[36867] = new_date_string
# CreateDate within the Exif IFD
exif_data.get_ifd(34665)[36868] = new_date_string
img.save(filename, exif=exif_data)
new_creation_time = datetime(year, month or new_date.month, day or new_date.day, new_date.hour, new_date.minute, new_date.second)
os.utime(filename, (new_creation_time.timestamp(), new_creation_time.timestamp()))
def main():
# Set up command line arguments
parser = argparse.ArgumentParser()
parser.add_argument("date", help="The new creation date to update to, in the format YYYY[-MM[-DD]].")
parser.add_argument("files", nargs="+", help="The files to update the EXIF data for.")
parser.add_argument("--no-dry-run", action="store_true", help="Update the files for real.")
args = parser.parse_args()
# Parse the date argument into year, month, and day components
date_components = args.date.split("-")
year = int(date_components[0])
month = int(date_components[1]) if len(date_components) > 1 else None
day = int(date_components[2]) if len(date_components) > 2 else None
filenames = args.files
dry_run = not args.no_dry_run
# Process each file
for filename in filenames:
if not os.path.isfile(filename):
logging.warning(f"{filename} is not a file. Skipping.")
continue
filename_path = Path(filename)
filename_extension = filename_path.suffix.lower()
if filename_extension in image_file_extensions:
# Update the EXIF creation date for the file
logging.info(f"Updating EXIF creation date for {filename}")
update_exif_creation_date(filename, year, month, day, dry_run=dry_run)
elif filename_extension in video_file_extensions:
logging.info(f"Updating file creation date for {filename}")
update_file_creation_date(filename, year, month, day, dry_run=dry_run)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment