Skip to content

Commit 59e347f

Browse files
committed
Fixing HEIC metadata parsing bug
1 parent fdff9e9 commit 59e347f

File tree

2 files changed

+76
-32
lines changed

2 files changed

+76
-32
lines changed

.github/workflows/build-executable.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
2626
- name: Build executable
2727
run: |
28-
pyinstaller --noconsole --icon=file_renamer_icon.ico --add-data "file_renamer_icon.ico;." --hidden-import PIL --hidden-import pytz --exclude-module _bz2 --exclude-module _lzma --exclude-module _decimal --exclude-module zoneinfo rename_files.py
28+
pyinstaller --noconsole --icon=file_renamer_icon.ico --add-data "file_renamer_icon.ico;." --hidden-import pillow_heif --hidden-import exif --hidden-import pytz.zoneinfo --hidden-import xml.etree.ElementTree rename_files.py
2929
3030
- name: Upload artifact
3131
uses: actions/upload-artifact@v4

rename_files.py

+75-31
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import mimetypes
77
import subprocess
88
from pytz import timezone
9+
import sys
10+
from pillow_heif import register_heif_opener
11+
import pyexiv2
912

1013
# Define Eastern Time (ET)
1114
eastern = timezone('US/Eastern')
@@ -25,6 +28,9 @@
2528
# Log file name
2629
LOG_FILE_NAME = "file_rename_log.txt"
2730

31+
# Register HEIC support
32+
register_heif_opener()
33+
2834
# Function to write logs to both the GUI and the log file
2935
def write_log(message):
3036
log_widget.insert(END, message + "\n")
@@ -58,49 +64,88 @@ def generate_log_filename(base_name, directory):
5864
return os.path.join(directory, filename)
5965

6066
# Function to get the date taken
67+
from pillow_heif import register_heif_opener
68+
from PIL import Image
69+
70+
# Register HEIC support
71+
register_heif_opener()
72+
73+
# Updated get_date_taken function
6174
def get_date_taken(file_path):
6275
fallback_creation_time = datetime.datetime.fromtimestamp(os.path.getctime(file_path))
6376
fallback_modification_time = datetime.datetime.fromtimestamp(os.path.getmtime(file_path))
77+
date_taken = None # Default to None
78+
is_fallback = True # Assume fallback unless proven otherwise
6479

6580
try:
6681
mime_type, _ = mimetypes.guess_type(file_path)
6782
if mime_type and mime_type.startswith('image'):
6883
image = Image.open(file_path)
69-
exif_data = image._getexif()
70-
if exif_data:
71-
for tag, value in exif_data.items():
72-
decoded = TAGS.get(tag, tag)
73-
if decoded == "DateTimeOriginal":
74-
naive_time = datetime.datetime.strptime(value, '%Y:%m:%d %H:%M:%S')
75-
if naive_time.tzinfo is None:
76-
date_taken = eastern.localize(naive_time)
77-
else:
78-
date_taken = naive_time.astimezone(eastern)
79-
return date_taken, False
84+
85+
# Handle HEIC/HEIF formats
86+
if mime_type in ("image/heic", "image/heif"):
87+
try:
88+
from pillow_heif import register_heif_opener
89+
register_heif_opener()
90+
91+
# Check for EXIF metadata
92+
metadata = image.info.get("Exif")
93+
if metadata:
94+
from exif import Image as ExifImage
95+
exif_image = ExifImage(metadata)
96+
if exif_image.has_exif and exif_image.datetime_original:
97+
naive_time = datetime.datetime.strptime(exif_image.datetime_original, '%Y:%m:%d %H:%M:%S')
98+
date_taken = eastern.localize(naive_time) if naive_time.tzinfo is None else naive_time.astimezone(eastern)
99+
is_fallback = False
100+
101+
# Check for XMP metadata if EXIF fails
102+
if date_taken is None:
103+
xmp_data = image.info.get("xmp")
104+
if xmp_data:
105+
import xml.etree.ElementTree as ET
106+
root = ET.fromstring(xmp_data)
107+
create_date = root.find(".//{http://ns.adobe.com/xap/1.0/}CreateDate")
108+
if create_date is not None:
109+
naive_time = datetime.datetime.strptime(create_date.text, '%Y-%m-%dT%H:%M:%S')
110+
date_taken = eastern.localize(naive_time) if naive_time.tzinfo is None else naive_time.astimezone(eastern)
111+
is_fallback = False
112+
except Exception as e:
113+
write_log(f"Error processing HEIC/HEIF metadata for {file_path}: {e}")
114+
else:
115+
# Handle standard image formats using Pillow
116+
exif_data = image._getexif()
117+
if exif_data:
118+
for tag, value in exif_data.items():
119+
decoded = TAGS.get(tag, tag)
120+
if decoded == "DateTimeOriginal":
121+
naive_time = datetime.datetime.strptime(value, '%Y:%m:%d %H:%M:%S')
122+
date_taken = eastern.localize(naive_time) if naive_time.tzinfo is None else naive_time.astimezone(eastern)
123+
is_fallback = False
124+
break
80125
elif mime_type and mime_type.startswith('video'):
81-
# Use ffprobe to extract creation time from videos
126+
# Handle video formats using ffprobe
82127
result = subprocess.run(
83-
["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
128+
["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
84129
"format_tags=creation_time", "-of", "default=noprint_wrappers=1:nokey=1", file_path],
85130
stdout=subprocess.PIPE,
86131
stderr=subprocess.STDOUT,
87-
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0 # Suppress window on Windows
132+
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
88133
)
89134
creation_time = result.stdout.decode().strip()
90135
if creation_time:
91136
naive_time = datetime.datetime.fromisoformat(creation_time.replace('Z', ''))
92-
if naive_time.tzinfo is None:
93-
date_taken = eastern.localize(naive_time)
94-
else:
95-
date_taken = naive_time.astimezone(eastern)
96-
return date_taken, False
137+
date_taken = eastern.localize(naive_time) if naive_time.tzinfo is None else naive_time.astimezone(eastern)
138+
is_fallback = False
97139
except Exception as e:
98-
return None, True # Return None with fallback indicator
140+
write_log(f"Error processing file {file_path}: {e}")
99141

100-
# Fallback to file last modified date
101-
return eastern.localize(fallback_modification_time), True
142+
# If no date_taken was found, fallback to modification time
143+
if date_taken is None:
144+
date_taken = eastern.localize(fallback_modification_time)
102145

103-
# Function to rename files
146+
return date_taken, is_fallback
147+
148+
# Updated rename_files_by_date function with refined logging
104149
def rename_files_by_date(folder_path):
105150
global main_log_file, secondary_log_file
106151

@@ -136,10 +181,7 @@ def rename_files_by_date(folder_path):
136181

137182
for file in files:
138183
date_info = get_date_taken(file)
139-
if isinstance(date_info, tuple):
140-
date_taken, is_fallback = date_info
141-
else:
142-
date_taken, is_fallback = date_info, False
184+
date_taken, is_fallback = date_info if isinstance(date_info, tuple) else (None, True)
143185

144186
# Collect dates for logging
145187
fallback_creation_time = datetime.datetime.fromtimestamp(os.path.getctime(file))
@@ -152,18 +194,20 @@ def rename_files_by_date(folder_path):
152194
# Store file data
153195
file_dates.append((file, date_taken, is_fallback, fallback_creation_time, fallback_modification_time))
154196

155-
# Log details in real-time
197+
# Logging logic
156198
if is_fallback:
157-
write_log(f"No Date Taken Found. Fallback Date Modified for {file}: {date_taken} ({date_taken.strftime('%Y_%m_%d_%H')})")
199+
write_log(f"No Date Taken Found. Fallback Date Modified for {file}: {date_taken} ({date_taken.strftime('%Y_%m_%d_%H')})" if date_taken else f"No Date Taken Found. Fallback failed for {file}. Defaulting to unknown date.")
200+
write_log(f" Date Taken: N/A")
158201
else:
159202
write_log(f"Date Taken for {file}: {date_taken} ({date_taken.strftime('%Y_%m_%d_%H')})")
160-
write_log(f" Date Taken: {date_taken if not is_fallback else 'N/A'}")
203+
write_log(f" Date Taken: {date_taken}")
204+
161205
write_log(f" Creation: {fallback_creation_time}")
162206
write_log(f" Last Modified: {fallback_modification_time}")
163207

164208
write_log("Step 4: Sorting files by date...")
165209
update_progress_bar(4)
166-
file_dates.sort(key=lambda x: x[1]) # Sort by date_taken
210+
file_dates.sort(key=lambda x: x[1] or fallback_modification_time) # Sort by date_taken or fallback
167211
write_log("Files sorted by date.")
168212

169213
write_log("Step 5: Renaming files...")

0 commit comments

Comments
 (0)