|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +import os |
| 4 | +import sys |
| 5 | +import shutil |
| 6 | +import subprocess |
| 7 | +from pathlib import Path |
| 8 | +import hashlib |
| 9 | +from PIL import Image, ExifTags, UnidentifiedImageError |
| 10 | + |
| 11 | +try: |
| 12 | + import pyheif # For HEIC files |
| 13 | +except ImportError as e: |
| 14 | + subprocess.run(['osascript', '-e', f'display alert "Error: pyheif not found. Install it using pip."']) |
| 15 | + sys.exit(1) |
| 16 | + |
| 17 | +# Constants |
| 18 | +STANDARD_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".tif", ".webp", ".psd", |
| 19 | + ".svg", ".ico", ".jfif", ".pjpeg", ".pjp", ".avif", ".apng"} |
| 20 | +PNG_EXTENSION = ".png" |
| 21 | +HEIC_EXTENSIONS = {".heic", ".heif"} |
| 22 | +DNG_RAW_EXTENSIONS = {".dng", ".raw", ".nef", ".cr2", ".cr3", ".arw", ".orf", ".rw2", ".raf", ".srw", ".kdc"} |
| 23 | +MISC_EXTENSIONS = {".doc", ".docx", ".txt", ".py", ".zsh", ".sh", ".mov", ".mp4", ".pdf", ".xlsx", ".pptx"} |
| 24 | + |
| 25 | +# Folder Names |
| 26 | +PORTRAIT_FOLDER_NAME = "Portrait" |
| 27 | +LANDSCAPE_FOLDER_NAME = "Landscape" |
| 28 | +SCREENSHOTS_FOLDER_NAME = "Screenshots" |
| 29 | +MISC_FOLDER_NAME = "Misc" |
| 30 | + |
| 31 | +def get_input_folder(): |
| 32 | + """Get the folder path from command-line arguments or show a prompt.""" |
| 33 | + if len(sys.argv) > 1: |
| 34 | + input_folder_path = sys.argv[1] |
| 35 | + return Path(input_folder_path) |
| 36 | + else: |
| 37 | + prompt_message = ( |
| 38 | + "⚠️ Please back up your files before processing them.\n\n" |
| 39 | + "Drag and drop a folder onto the droplet to begin." |
| 40 | + ) |
| 41 | + subprocess.run(['osascript', '-e', f'display alert "{prompt_message}"']) |
| 42 | + sys.exit(1) |
| 43 | + |
| 44 | +def get_prefix(folder_name): |
| 45 | + """Prompt for a prefix for Portrait and Landscape folders.""" |
| 46 | + try: |
| 47 | + result = subprocess.run( |
| 48 | + ['osascript', '-e', f'display dialog "Enter prefix for {folder_name} photos:" default answer ""'], |
| 49 | + capture_output=True, text=True, check=True |
| 50 | + ) |
| 51 | + output = result.stdout.strip().split("text returned:")[-1].strip() |
| 52 | + return output or f"{folder_name} Photo" |
| 53 | + except subprocess.CalledProcessError: |
| 54 | + return f"{folder_name} Photo" |
| 55 | + |
| 56 | +def generate_checksums(root_folder): |
| 57 | + """Generate file checksums using hashlib.""" |
| 58 | + checksums = {} |
| 59 | + for path in root_folder.rglob('*'): |
| 60 | + if path.is_file(): |
| 61 | + try: |
| 62 | + hasher = hashlib.sha256() |
| 63 | + with path.open('rb') as f: |
| 64 | + while chunk := f.read(8192): |
| 65 | + hasher.update(chunk) |
| 66 | + checksum = hasher.hexdigest() |
| 67 | + checksums.setdefault(checksum, []).append(path) |
| 68 | + except Exception: |
| 69 | + pass # Continue on error |
| 70 | + return checksums |
| 71 | + |
| 72 | +def correct_orientation(image): |
| 73 | + """Correct image orientation using EXIF metadata.""" |
| 74 | + try: |
| 75 | + exif = image._getexif() |
| 76 | + if exif is not None: |
| 77 | + orientation = exif.get(274) # EXIF tag 274 is Orientation |
| 78 | + if orientation == 3: |
| 79 | + image = image.rotate(180, expand=True) |
| 80 | + elif orientation == 6: |
| 81 | + image = image.rotate(270, expand=True) |
| 82 | + elif orientation == 8: |
| 83 | + image = image.rotate(90, expand=True) |
| 84 | + return image |
| 85 | + except Exception as e: |
| 86 | + print(f"Error correcting orientation: {e}") |
| 87 | + return image # Return original image on error |
| 88 | + |
| 89 | +def rename_files(target_folder, prefix): |
| 90 | + """Rename files with the given prefix.""" |
| 91 | + files = sorted(target_folder.glob('*')) |
| 92 | + for idx, file in enumerate(files, 1): |
| 93 | + new_name = f"{prefix} {str(idx).zfill(3)}{file.suffix}" |
| 94 | + try: |
| 95 | + file.rename(target_folder / new_name) |
| 96 | + except Exception: |
| 97 | + pass # Continue on error |
| 98 | + |
| 99 | +def process_heic_image(path): |
| 100 | + """Process HEIC image to determine its dimensions.""" |
| 101 | + try: |
| 102 | + heif_file = pyheif.read(path) |
| 103 | + width, height = heif_file.size |
| 104 | + print(f"Processed HEIC: {path.name} - {width}x{height}") # Debugging dimensions |
| 105 | + return width, height |
| 106 | + except Exception as e: |
| 107 | + print(f"Error processing HEIC {path}: {e}") |
| 108 | + return None, None |
| 109 | + |
| 110 | +def sort_images(root_folder, portrait_prefix, landscape_prefix): |
| 111 | + """Sort images into folders and apply prefixes.""" |
| 112 | + portrait_folder = root_folder / PORTRAIT_FOLDER_NAME |
| 113 | + landscape_folder = root_folder / LANDSCAPE_FOLDER_NAME |
| 114 | + |
| 115 | + portrait_folder.mkdir(exist_ok=True) |
| 116 | + landscape_folder.mkdir(exist_ok=True) |
| 117 | + |
| 118 | + screenshots_folder = None |
| 119 | + misc_folder = None |
| 120 | + |
| 121 | + for path in root_folder.rglob('*'): |
| 122 | + if path.is_file() and path.parent not in {portrait_folder, landscape_folder}: |
| 123 | + suffix = path.suffix.lower() |
| 124 | + try: |
| 125 | + if suffix in HEIC_EXTENSIONS: |
| 126 | + width, height = process_heic_image(path) |
| 127 | + if width and height: |
| 128 | + target = portrait_folder if height > width else landscape_folder |
| 129 | + shutil.move(str(path), target / path.name) |
| 130 | + elif suffix in STANDARD_IMAGE_EXTENSIONS: |
| 131 | + with Image.open(path) as img: |
| 132 | + img = correct_orientation(img) # Correct orientation for JPEGs |
| 133 | + width, height = img.size |
| 134 | + print(f"Processed JPEG: {path.name} - {width}x{height}") # Debugging dimensions |
| 135 | + target = portrait_folder if height > width else landscape_folder |
| 136 | + shutil.move(str(path), target / path.name) |
| 137 | + elif suffix == PNG_EXTENSION: |
| 138 | + if not screenshots_folder: |
| 139 | + screenshots_folder = root_folder / SCREENSHOTS_FOLDER_NAME |
| 140 | + screenshots_folder.mkdir(exist_ok=True) |
| 141 | + shutil.move(str(path), screenshots_folder / path.name) |
| 142 | + elif suffix in DNG_RAW_EXTENSIONS: |
| 143 | + shutil.move(str(path), landscape_folder / path.name) |
| 144 | + elif suffix in MISC_EXTENSIONS: |
| 145 | + if not misc_folder: |
| 146 | + misc_folder = root_folder / MISC_FOLDER_NAME |
| 147 | + misc_folder.mkdir(exist_ok=True) |
| 148 | + shutil.move(str(path), misc_folder / path.name) |
| 149 | + except Exception as e: |
| 150 | + print(f"Error processing {path}: {e}") |
| 151 | + |
| 152 | + rename_files(portrait_folder, portrait_prefix) |
| 153 | + rename_files(landscape_folder, landscape_prefix) |
| 154 | + |
| 155 | +def main(): |
| 156 | + """Main entry point.""" |
| 157 | + input_folder = get_input_folder() |
| 158 | + |
| 159 | + checksums = generate_checksums(input_folder) |
| 160 | + portrait_prefix = get_prefix("Portrait") |
| 161 | + landscape_prefix = get_prefix("Landscape") |
| 162 | + |
| 163 | + sort_images(input_folder, portrait_prefix, landscape_prefix) |
| 164 | + |
| 165 | + subprocess.run(['osascript', '-e', 'display alert "Process completed successfully!"']) |
| 166 | + |
| 167 | +if __name__ == "__main__": |
| 168 | + main() |
0 commit comments