Skip to content

Instantly share code, notes, and snippets.

@Chalkin
Last active July 14, 2025 11:59
Show Gist options
  • Save Chalkin/12c3c9fd7320cc1355ceb9eb9ee718bc to your computer and use it in GitHub Desktop.
Save Chalkin/12c3c9fd7320cc1355ceb9eb9ee718bc to your computer and use it in GitHub Desktop.
Embedding meta data to Photos use after Google Photos Takeout
"""
Google Photos EXIF Metadata Embedding Tool - Use it after using Google Photos Takeout
This command line python script processes image files (JPG, JPEG, HEIC) and their corresponding
Google Photos JSON metadata files to embed GPS coordinates and photo taken
time into the image's EXIF data.
Features:
- Reads 'photoTakenTime', 'geoDataExif', and 'geoData' from Google Photos JSONs.
- Supports various Google Photos JSON naming patterns (e.g., 'image.jpg.json', 'image(1).jpg.json').
- Uses 'piexif' for JPG/JPEG files.
- Uses 'ExifTool' (external utility) for HEIC files. If ExifTool detects a HEIC
is actually a JPEG, it renames both the image (.heic -> .jpg) and its JSON
before processing with piexif for consistency.
Usage on macOS:
1. Save this script as e.g., 'google_photos_exif_tool.py'.
2. Open Terminal.
3. (Recommended) Create & activate a Python virtual environment:
```bash
python3 -m venv venv
source venv/bin/activate
```
4. Install Python dependencies:
```bash
pip install piexif
```
5. Install ExifTool (requires Homebrew: https://brew.sh):
```bash
brew install exiftool
```
6. Run the script:
```bash
python3 google_photos_exif_tool.py "/path/to/your/photos/folder"
# Or, to process the current directory:
python3 google_photos_exif_tool.py
```
7. Deactivate virtual environment when done:
```bash
deactivate
```
IMPORTANT: Always back up your original photos and JSONs before running any script that modifies them!
"""
import os
import json
import piexif
from datetime import datetime
import sys
import re
import subprocess
# Define the common JSON suffixes for standard pattern (full image filename + suffix)
STANDARD_JSON_SUFFIXES = [
".supplemental-metadata.json",
".supp.json",
".suppl.json"
]
# Regex to detect (N) in image filenames (e.g., "IMG_1465(1).JPG")
# Captures the base name before (N) and the number N
IMAGE_DUPLICATE_PATTERN = re.compile(r"^(.*)\((\d+)\)$")
def embed_gps_data(image_path, json_path):
"""
Embeds GPS and photo taken time data from a specific Google Photos JSON file
into a JPEG/HEIC image. Uses ExifTool for HEIC, piexif for JPG.
Includes extensive debug prints. If a .HEIC file is detected as a JPEG by ExifTool,
it attempts to rename both the image and its JSON to .JPG before processing with piexif.
Args:
image_path (str): The full path to the image file.
json_path (str): The full path to the corresponding JSON file.
Returns:
tuple: (bool, bool)
First bool: True if image was successfully processed, False otherwise.
Second bool: True if the image file was renamed (HEIC to JPG), False otherwise.
"""
print(f"\n--- DEBUG: Processing Image: {os.path.basename(image_path)} ---")
print(f"DEBUG: Associated JSON: {os.path.basename(json_path)}")
# Initialize return status
processed_successfully = False
file_was_renamed = False
# 1. Load JSON metadata
try:
with open(json_path, 'r', encoding='utf-8') as f:
metadata = json.load(f)
print("DEBUG: JSON file loaded successfully.")
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"ERROR: Could not load or decode JSON from {json_path}: {e}")
return (False, False) # Cannot proceed without valid JSON
except Exception as e:
print(f"AN UNEXPECTED ERROR OCCURRED while loading JSON from {json_path}: {e}")
return (False, False)
# 2. Extract Photo Taken Time
photo_taken_time_data = metadata.get('photoTakenTime', {})
photo_taken_timestamp = photo_taken_time_data.get('timestamp')
exif_datetime = None
if photo_taken_timestamp:
try:
dt_object = datetime.fromtimestamp(int(photo_taken_timestamp))
exif_datetime = dt_object.strftime("%Y:%m:%d %H:%M:%S")
print(f"DEBUG: Extracted photoTakenTime timestamp: {photo_taken_timestamp} -> {exif_datetime}")
except ValueError as e:
print(f"DEBUG ERROR: Could not convert photoTakenTime timestamp '{photo_taken_timestamp}': {e}")
else:
print("DEBUG: 'photoTakenTime' or 'timestamp' not found in JSON.")
# 3. Extract GPS Data - Try geoDataExif first, then fallback to geoData
latitude = None
longitude = None
altitude = None
geo_data_exif = metadata.get('geoDataExif', {})
if geo_data_exif:
temp_lat_exif = geo_data_exif.get('latitude')
temp_lon_exif = geo_data_exif.get('longitude')
temp_alt_exif = geo_data_exif.get('altitude')
if temp_lat_exif is not None and temp_lon_exif is not None and (temp_lat_exif != 0.0 or temp_lon_exif != 0.0):
latitude = temp_lat_exif
longitude = temp_lon_exif
altitude = temp_alt_exif
print("DEBUG: Using GPS data from 'geoDataExif' section (original EXIF).")
print(f"DEBUG: geoDataExif: Latitude={latitude}, Longitude={longitude}, Altitude={altitude}")
else:
print("DEBUG: 'geoDataExif' section found, but coordinates are missing or 0.0.")
else:
print("DEBUG: 'geoDataExif' section not found in JSON.")
if latitude is None or longitude is None or (latitude == 0.0 and longitude == 0.0):
geo_data = metadata.get('geoData', {})
temp_lat_geo = geo_data.get('latitude')
temp_lon_geo = geo_data.get('longitude')
temp_alt_geo = geo_data.get('altitude')
if temp_lat_geo is not None and temp_lon_geo is not None and (temp_lat_geo != 0.0 or temp_lon_geo != 0.0):
latitude = temp_lat_geo
longitude = temp_lon_geo
altitude = temp_alt_geo
print("DEBUG: Using GPS data from 'geoData' section (Google Photos inferred/manual).")
print(f"DEBUG: geoData: Latitude={latitude}, Longitude={longitude}, Altitude={altitude}")
else:
print("DEBUG: 'geoData' section found, but coordinates are missing or 0.0. No valid GPS data found from either source.")
print(f"DEBUG: Final GPS values for embedding: Latitude={latitude}, Longitude={longitude}, Altitude={altitude}")
# Determine if it's a HEIC file by extension
is_heic_extension = image_path.lower().endswith(('.heic'))
# Flag to indicate if ExifTool successfully handled the file
exiftool_success = False
if is_heic_extension:
print(f"DEBUG: Attempting to use ExifTool for metadata embedding (based on .HEIC extension).")
exiftool_cmd = ["exiftool", "-overwrite_original"]
if exif_datetime:
exiftool_cmd.append(f"-DateTimeOriginal={exif_datetime}")
exiftool_cmd.append(f"-CreateDate={exif_datetime}")
exiftool_cmd.append(f"-ModifyDate={exif_datetime}")
else:
print("INFO: No photoTakenTime available for HEIC, skipping date embedding via ExifTool.")
if latitude is not None and longitude is not None and (latitude != 0.0 or longitude != 0.0):
exiftool_cmd.append(f"-GPSLatitude={latitude}")
exiftool_cmd.append(f"-GPSLongitude={longitude}")
if altitude is not None:
exiftool_cmd.append(f"-GPSAltitude={altitude}")
exiftool_cmd.append("-GPSAltitudeRef=Above Sea Level")
print("DEBUG: GPS data is non-zero, proceeding to embed via ExifTool.")
else:
print("INFO: No valid GPS data available for HEIC, skipping GPS embedding via ExifTool.")
exiftool_cmd.append(image_path)
print(f"DEBUG: ExifTool command: {' '.join(exiftool_cmd)}")
try:
if len(exiftool_cmd) > 2:
result = subprocess.run(exiftool_cmd, capture_output=True, text=True, check=True)
# Check ExifTool's stderr for the specific "Not a valid HEIC" error
if "Not a valid HEIC (looks more like a JPEG)" in result.stderr:
print(f"WARNING: ExifTool reported '{os.path.basename(image_path)}' is not a valid HEIC (looks like a JPEG).")
# --- START RENAMING LOGIC ---
old_image_path = image_path
new_image_path = os.path.splitext(old_image_path)[0] + ".jpg"
old_json_path = json_path
json_basename = os.path.basename(old_json_path)
# FIX: Replace both .HEIC. and .heic. with .jpg. (lowercase)
new_json_basename = json_basename.replace(".HEIC.", ".jpg.", 1).replace(".heic.", ".jpg.", 1)
new_json_path = os.path.join(os.path.dirname(old_json_path), new_json_basename)
print(f"INFO: Renaming '{os.path.basename(old_image_path)}' to '{os.path.basename(new_image_path)}'.")
print(f"INFO: Renaming associated JSON '{os.path.basename(old_json_path)}' to '{os.path.basename(new_json_path)}'.")
try:
os.rename(old_image_path, new_image_path)
os.rename(old_json_path, new_json_path)
print("SUCCESS: Files renamed.")
file_was_renamed = True # Set flag here
# Update variables for subsequent
import os
import json
import piexif
from datetime import datetime
import sys
import re
import subprocess
# Define the common JSON suffixes for standard pattern (full image filename + suffix)
STANDARD_JSON_SUFFIXES = [
".supplemental-metadata.json",
".supp.json",
".suppl.json"
]
# Regex to detect (N) in image filenames (e.g., "IMG_1465(1).JPG")
# Captures the base name before (N) and the number N
IMAGE_DUPLICATE_PATTERN = re.compile(r"^(.*)\((\d+)\)$")
def embed_gps_data(image_path, json_path):
"""
Embeds GPS and photo taken time data from a specific Google Photos JSON file
into a JPEG/HEIC image. Uses ExifTool for HEIC, piexif for JPG.
Includes extensive debug prints. If a .HEIC file is detected as a JPEG by ExifTool,
it attempts to rename both the image and its JSON to .JPG before processing with piexif.
Args:
image_path (str): The full path to the image file.
json_path (str): The full path to the corresponding JSON file.
Returns:
tuple: (bool, bool)
First bool: True if image was successfully processed, False otherwise.
Second bool: True if the image file was renamed (HEIC to JPG), False otherwise.
"""
print(f"\n--- DEBUG: Processing Image: {os.path.basename(image_path)} ---")
print(f"DEBUG: Associated JSON: {os.path.basename(json_path)}")
# Initialize return status
processed_successfully = False
file_was_renamed = False
# 1. Load JSON metadata
try:
with open(json_path, 'r', encoding='utf-8') as f:
metadata = json.load(f)
print("DEBUG: JSON file loaded successfully.")
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"ERROR: Could not load or decode JSON from {json_path}: {e}")
return (False, False) # Cannot proceed without valid JSON
except Exception as e:
print(f"AN UNEXPECTED ERROR OCCURRED while loading JSON from {json_path}: {e}")
return (False, False)
# 2. Extract Photo Taken Time
photo_taken_time_data = metadata.get('photoTakenTime', {})
photo_taken_timestamp = photo_taken_time_data.get('timestamp')
exif_datetime = None
if photo_taken_timestamp:
try:
dt_object = datetime.fromtimestamp(int(photo_taken_timestamp))
exif_datetime = dt_object.strftime("%Y:%m:%d %H:%M:%S")
print(f"DEBUG: Extracted photoTakenTime timestamp: {photo_taken_timestamp} -> {exif_datetime}")
except ValueError as e:
print(f"DEBUG ERROR: Could not convert photoTakenTime timestamp '{photo_taken_timestamp}': {e}")
else:
print("DEBUG: 'photoTakenTime' or 'timestamp' not found in JSON.")
# 3. Extract GPS Data - Try geoDataExif first, then fallback to geoData
latitude = None
longitude = None
altitude = None
geo_data_exif = metadata.get('geoDataExif', {})
if geo_data_exif:
temp_lat_exif = geo_data_exif.get('latitude')
temp_lon_exif = geo_data_exif.get('longitude')
temp_alt_exif = geo_data_exif.get('altitude')
if temp_lat_exif is not None and temp_lon_exif is not None and (temp_lat_exif != 0.0 or temp_lon_exif != 0.0):
latitude = temp_lat_exif
longitude = temp_lon_exif
altitude = temp_alt_exif
print("DEBUG: Using GPS data from 'geoDataExif' section (original EXIF).")
print(f"DEBUG: geoDataExif: Latitude={latitude}, Longitude={longitude}, Altitude={altitude}")
else:
print("DEBUG: 'geoDataExif' section found, but coordinates are missing or 0.0.")
else:
print("DEBUG: 'geoDataExif' section not found in JSON.")
if latitude is None or longitude is None or (latitude == 0.0 and longitude == 0.0):
geo_data = metadata.get('geoData', {})
temp_lat_geo = geo_data.get('latitude')
temp_lon_geo = geo_data.get('longitude')
temp_alt_geo = geo_data.get('altitude')
if temp_lat_geo is not None and temp_lon_geo is not None and (temp_lat_geo != 0.0 or temp_lon_geo != 0.0):
latitude = temp_lat_geo
longitude = temp_lon_geo
altitude = temp_alt_geo
print("DEBUG: Using GPS data from 'geoData' section (Google Photos inferred/manual).")
print(f"DEBUG: geoData: Latitude={latitude}, Longitude={longitude}, Altitude={altitude}")
else:
print("DEBUG: 'geoData' section found, but coordinates are missing or 0.0. No valid GPS data found from either source.")
print(f"DEBUG: Final GPS values for embedding: Latitude={latitude}, Longitude={longitude}, Altitude={altitude}")
# Determine if it's a HEIC file by extension
is_heic_extension = image_path.lower().endswith(('.heic'))
# Flag to indicate if ExifTool successfully handled the file
exiftool_success = False
if is_heic_extension:
print(f"DEBUG: Attempting to use ExifTool for metadata embedding (based on .HEIC extension).")
exiftool_cmd = ["exiftool", "-overwrite_original"]
if exif_datetime:
exiftool_cmd.append(f"-DateTimeOriginal={exif_datetime}")
exiftool_cmd.append(f"-CreateDate={exif_datetime}")
exiftool_cmd.append(f"-ModifyDate={exif_datetime}")
else:
print("INFO: No photoTakenTime available for HEIC, skipping date embedding via ExifTool.")
if latitude is not None and longitude is not None and (latitude != 0.0 or longitude != 0.0):
exiftool_cmd.append(f"-GPSLatitude={latitude}")
exiftool_cmd.append(f"-GPSLongitude={longitude}")
if altitude is not None:
exiftool_cmd.append(f"-GPSAltitude={altitude}")
exiftool_cmd.append("-GPSAltitudeRef=Above Sea Level")
print("DEBUG: GPS data is non-zero, proceeding to embed via ExifTool.")
else:
print("INFO: No valid GPS data available for HEIC, skipping GPS embedding via ExifTool.")
exiftool_cmd.append(image_path)
print(f"DEBUG: ExifTool command: {' '.join(exiftool_cmd)}")
try:
if len(exiftool_cmd) > 2:
result = subprocess.run(exiftool_cmd, capture_output=True, text=True, check=True)
# Check ExifTool's stderr for the specific "Not a valid HEIC" error
if "Not a valid HEIC (looks more like a JPEG)" in result.stderr:
print(f"WARNING: ExifTool reported '{os.path.basename(image_path)}' is not a valid HEIC (looks like a JPEG).")
# --- START RENAMING LOGIC ---
old_image_path = image_path
new_image_path = os.path.splitext(old_image_path)[0] + ".jpg"
old_json_path = json_path
json_basename = os.path.basename(old_json_path)
# FIX: Replace both .HEIC. and .heic. with .jpg. (lowercase)
new_json_basename = json_basename.replace(".HEIC.", ".jpg.", 1).replace(".heic.", ".jpg.", 1)
new_json_path = os.path.join(os.path.dirname(old_json_path), new_json_basename)
print(f"INFO: Renaming '{os.path.basename(old_image_path)}' to '{os.path.basename(new_image_path)}'.")
print(f"INFO: Renaming associated JSON '{os.path.basename(old_json_path)}' to '{os.path.basename(new_json_path)}'.")
try:
os.rename(old_image_path, new_image_path)
os.rename(old_json_path, new_json_path)
print("SUCCESS: Files renamed.")
file_was_renamed = True # Set flag here
# Update variables for subsequent processing in this function call
image_path = new_image_path
json_path = new_json_path
is_heic_extension = False # CRITICAL: Flag it as a non-HEIC so piexif is used
print(f"DEBUG: Proceeding to process with piexif on renamed file.")
except OSError as rename_error:
print(f"ERROR: Failed to rename files. Please check permissions or if files are in use. Error: {rename_error}")
print(f"--- DEBUG: Finished processing {os.path.basename(old_image_path)} ---")
return (False, False) # Return failure status
# --- END RENAMING LOGIC ---
else:
# ExifTool handled a *true* HEIC correctly
print(f"SUCCESS: ExifTool output for {os.path.basename(image_path)}:\n{result.stdout}")
if result.stderr:
print(f"EXIFTOOL STDERR: {result.stderr}")
exiftool_success = True
else:
# If no commands were appended (no date/gps) but it's a HEIC, still consider success.
print(f"INFO: No date or GPS data to embed for HEIC file '{os.path.basename(image_path)}' via ExifTool.")
exiftool_success = True
except FileNotFoundError:
print(f"ERROR: ExifTool not found. Please install ExifTool (e.g., 'brew install exiftool' on macOS, 'sudo apt-get install libimage-exiftool-perl' on Debian/Ubuntu, or download from exiftool.org).")
print(f"DEBUG: Attempting to process with piexif instead (ExifTool not found). Note: File extension will remain .HEIC if ExifTool is not present to identify it as JPEG.")
except subprocess.CalledProcessError as e:
print(f"ERROR: ExifTool failed for {os.path.basename(image_path)}. Return code: {e.returncode}")
print(f"STDOUT: {e.stdout}")
print(f"STDERR: {e.stderr}")
if "Not a valid HEIC (looks more like a JPEG)" in e.stderr:
print(f"WARNING: ExifTool reported '{os.path.basename(image_path)}' is not a valid HEIC (looks like a JPEG).")
# --- DUPLICATE RENAMING LOGIC FOR CalledProcessError (necessary for comprehensive error handling) ---
old_image_path = image_path
new_image_path = os.path.splitext(old_image_path)[0] + ".jpg"
old_json_path = json_path
json_basename = os.path.basename(old_json_path)
# FIX: Replace both .HEIC. and .heic. with .jpg. (lowercase)
new_json_basename = json_basename.replace(".HEIC.", ".jpg.", 1).replace(".heic.", ".jpg.", 1)
new_json_path = os.path.join(os.path.dirname(old_json_path), new_json_basename)
print(f"INFO: Renaming '{os.path.basename(old_image_path)}' to '{os.path.basename(new_image_path)}'.")
print(f"INFO: Renaming associated JSON '{os.path.basename(old_json_path)}' to '{os.path.basename(new_json_path)}'.")
try:
os.rename(old_image_path, new_image_path)
os.rename(old_json_path, new_json_path)
print("SUCCESS: Files renamed.")
file_was_renamed = True # Set flag here
image_path = new_image_path
json_path = new_json_path
is_heic_extension = False # CRITICAL: Flag it as a non-HEIC so piexif is used
print(f"DEBUG: Proceeding to process with piexif on renamed file.")
except OSError as rename_error:
print(f"ERROR: Failed to rename files. Please check permissions or if files are in use. Error: {rename_error}")
print(f"--- DEBUG: Finished processing {os.path.basename(old_image_path)} ---")
return (False, False)
# --- END DUPLICATE RENAMING LOGIC ---
else:
print(f"ERROR: ExifTool failed for a non-HEIC validation reason. Cannot proceed with this file via ExifTool.")
print(f"--- DEBUG: Finished processing {os.path.basename(image_path)} ---")
return (False, False)
except Exception as e:
print(f"AN UNEXPECTED ERROR OCCURRED during ExifTool execution for {os.path.basename(image_path)}: {e}")
print(f"--- DEBUG: Finished processing {os.path.basename(image_path)} ---")
return (False, False)
# This block is entered if exiftool_success is False (either it's a JPG/JPEG initially, or
# it was a mislabeled HEIC that got renamed, or ExifTool wasn't found).
if not exiftool_success:
print(f"INFO: Now processing '{os.path.basename(image_path)}' using piexif.")
# Helper functions for piexif (DMS conversion)
def decimal_to_dms(decimal_deg):
is_positive = decimal_deg >= 0
abs_decimal_deg = abs(decimal_deg)
degrees = int(abs_decimal_deg)
minutes = int((abs_decimal_deg - degrees) * 60)
seconds = int(((abs_decimal_deg - degrees) * 60 - minutes) * 60 * 100) # Multiply by 100 for fraction
return ((degrees, 1), (minutes, 1), (seconds, 100)), 'N' if is_positive else 'S'
def decimal_to_dms_lon(decimal_deg):
is_positive = decimal_deg >= 0
abs_decimal_deg = abs(decimal_deg)
degrees = int(abs_decimal_deg)
minutes = int((abs_decimal_deg - degrees) * 60)
seconds = int(((abs_decimal_deg - degrees) * 60 - minutes) * 60 * 100) # Multiply by 100 for fraction
return ((degrees, 1), (minutes, 1), (seconds, 100)), 'E' if is_positive else 'W'
# Read existing EXIF data or create new
exif_dict = {"0th": {}, "Exif": {}, "GPS": {}, "Interop": {}, "1st": {}, "thumbnail": None} # Initialize directly
try:
loaded_exif_dict = piexif.load(image_path)
exif_dict.update(loaded_exif_dict)
print("DEBUG: Existing EXIF data loaded and merged.")
except piexif.InvalidImageDataError:
print("DEBUG: Image has invalid/no EXIF data (as expected by piexif). Initializing new EXIF dictionary.")
except Exception as e:
print(f"ERROR: Could not load EXIF from {os.path.basename(image_path)}. This might indicate a corrupted JPG or unsupported format by piexif. Error: {e}")
print(f"--- DEBUG: Finished processing {os.path.basename(image_path)} ---")
return (False, False)
# Update Photo Taken Time if available
if exif_datetime:
exif_dict["Exif"][piexif.ExifIFD.DateTimeOriginal] = exif_datetime.encode("utf-8")
exif_dict["Exif"][piexif.ExifIFD.DateTimeDigitized] = exif_datetime.encode("utf-8")
exif_dict["0th"][piexif.ImageIFD.DateTime] = exif_datetime.encode("utf-8")
print(f"DEBUG: DateTimeOriginal, DateTimeDigitized, DateTime set to: {exif_datetime}")
else:
print("DEBUG: exif_datetime is None. Skipping date/time embedding.")
# Update GPS data if available
gps_ifd = {}
if latitude is not None and longitude is not None and (latitude != 0.0 or longitude != 0.0):
print("DEBUG: GPS data is non-zero, proceeding to embed.")
lat_dms, lat_ref = decimal_to_dms(latitude)
lon_dms, lon_ref = decimal_to_dms_lon(longitude)
gps_ifd[piexif.GPSIFD.GPSVersionID] = (2, 0, 0, 0)
gps_ifd[piexif.GPSIFD.GPSLatitudeRef] = lat_ref
gps_ifd[piexif.GPSIFD.GPSLatitude] = lat_dms
gps_ifd[piexif.GPSIFD.GPSLongitudeRef] = lon_ref
gps_ifd[piexif.GPSIFD.GPSLongitude] = lon_dms
if altitude is not None:
gps_ifd[piexif.GPSIFD.GPSAltitudeRef] = 0 # 0 for sea level reference (above sea level)
gps_ifd[piexif.GPSIFD.GPSAltitude] = (int(abs(altitude * 1000)), 1000) # Altitude as rational (numerator, denominator)
print(f"DEBUG: Converted Altitude: {altitude} -> {gps_ifd[piexif.GPSIFD.GPSAltitude]}")
else:
print("DEBUG: Altitude is None.")
else:
print("DEBUG: Final GPS values were missing or 0.0. Skipping GPS IFD creation.")
if gps_ifd:
exif_dict["GPS"] = gps_ifd
print("DEBUG: GPS IFD (Image File Directory) prepared for embedding.")
else:
print("DEBUG: GPS IFD is empty. No GPS data to embed.")
try:
exif_bytes = piexif.dump(exif_dict)
piexif.insert(exif_bytes, image_path)
print(f"SUCCESS: Successfully embedded data into: {os.path.basename(image_path)}")
processed_successfully = True # Set success flag for piexif path
except Exception as e:
print(f"ERROR: Failed to embed data into {os.path.basename(image_path)}: {e}")
processed_successfully = False # Set failure flag
print(f"--- DEBUG: Finished processing {os.path.basename(image_path)} ---")
return (processed_successfully, file_was_renamed)
def process_directory(directory_path):
"""
Processes all JPG/JPEG/HEIC images in the specified directory (and subdirectories)
and embeds GPS data from corresponding JSON files, handling various naming patterns.
"""
print(f"Starting to process images in: {os.path.abspath(directory_path)}")
if not os.path.isdir(directory_path):
print(f"ERROR: The specified path '{directory_path}' is not a valid directory.")
return
processed_count = 0
skipped_count = 0
images_without_json = []
renamed_files_count = 0 # Counter for renamed files
# Using os.walk to recursively process subdirectories
for root, _, files in os.walk(directory_path):
for filename in files:
# Check for image file extensions (expanded to include .heic)
if filename.lower().endswith(('.jpg', '.jpeg', '.heic')):
image_path = os.path.join(root, filename)
found_json = False
json_path = None # Initialize json_path to None
print(f"DEBUG_PDIR: Processing image: '{filename}'")
# Strategy 1: Check standard suffixes (using the full image filename)
for suffix in STANDARD_JSON_SUFFIXES:
candidate_json_filename = filename + suffix
current_json_path = os.path.join(root, candidate_json_filename)
print(f"DEBUG_PDIR: Trying standard JSON: '{os.path.basename(current_json_path)}'")
if os.path.exists(current_json_path):
json_path = current_json_path
found_json = True
print(f"DEBUG: Found JSON with standard suffix: '{os.path.basename(json_path)}'")
break # Found a JSON, no need to check other suffixes
# Strategy 2: Check for the special (N) pattern if a standard one wasn't found
if not found_json:
base_name, ext = os.path.splitext(filename)
print(f"DEBUG_PDIR: Standard JSON not found. Checking (N) pattern for base_name='{base_name}', ext='{ext}'")
match = IMAGE_DUPLICATE_PATTERN.match(base_name)
if match:
original_base_name = match.group(1)
duplicate_number = match.group(2)
print(f"DEBUG_PDIR: Image name matched (N) pattern: original_base_name='{original_base_name}', duplicate_number='{duplicate_number}'")
# Construct the JSON filename for this specific pattern
# Example: IMG_1234(1).JPG.supplemental-metadata.json
# The original image filename is embedded here
candidate_json_filename = f"{original_base_name}{ext}.supplemental-metadata({duplicate_number}).json"
current_json_path = os.path.join(root, candidate_json_filename)
print(f"DEBUG_PDIR: Trying (N) pattern JSON: '{os.path.basename(current_json_path)}'")
if os.path.exists(current_json_path):
json_path = current_json_path
found_json = True
print(f"DEBUG: Found JSON with (N) duplicate pattern: '{os.path.basename(json_path)}'")
else:
print(f"DEBUG_PDIR: (N) pattern JSON does NOT exist: '{os.path.basename(current_json_path)}'")
else:
print(f"DEBUG_PDIR: Image name '{base_name}' does NOT match (N) pattern.")
# Proceed if a JSON file was found
if found_json and json_path:
processed_status, renamed_status = embed_gps_data(image_path, json_path)
if processed_status:
processed_count += 1
if renamed_status:
renamed_files_count += 1 # Increment renamed counter
else:
print(f"\nINFO: No matching supplemental-metadata JSON found for: {os.path.basename(filename)} with any known suffix. Skipping.")
images_without_json.append(filename)
skipped_count += 1
print("\n--- Processing Summary ---")
print(f"Total images processed (with JSONs found): {processed_count}")
print(f"Total images skipped (no JSON found): {skipped_count}")
print(f"Total image files renamed (HEIC to JPG): {renamed_files_count}")
if images_without_json:
print("\nImages for which no JSON metadata was found:")
for img_name in images_without_json:
print(f" - {img_name}")
print("Processing complete.")
if __name__ == "__main__":
# --- IMPORTANT: Ensure 'piexif' is installed (pip install piexif) ---
# --- For HEIC support, you ALSO need ExifTool installed and in your system's PATH ---
# On macOS: brew install exiftool
# On Debian/Ubuntu: sudo apt-get install libimage-exiftool-perl
# For Windows/other: Download from exiftool.org and add to PATH
# Check for command-line arguments
if len(sys.argv) > 1:
target_folder = sys.argv[1] # Get the first argument as the folder path
# It's good practice to normalize the path
target_folder = os.path.abspath(target_folder)
else:
# If no argument is provided, default to the current directory
target_folder = os.getcwd()
print("No directory path provided as an argument. Processing the current directory.")
process_directory(target_folder)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment