Last active
March 22, 2023 23:18
-
-
Save gintsmurans/27c841c3204040c99d46cf48ec9986a8 to your computer and use it in GitHub Desktop.
Image exif helpers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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