#!/usr/bin/env python3 """ Mac Photoshop Text Extractor ---------------------------- A macOS-specific script to extract text from PSD files using AppleScript to control Photoshop and execute ExtendScript (JSX) code. This is designed to work on macOS without requiring the photoshop-python-api package which has Windows dependencies. """ import os import sys import time import json import argparse import subprocess from pathlib import Path import logging from typing import List, Optional # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logger = logging.getLogger(__name__) # The ExtractTextWithBreaks.jsx script as a string EXTRACT_TEXT_SCRIPT = r""" // Photoshop Script to Extract Text Layers With Exact Line Breaks #target photoshop function writeTextFile(e,t){e.encoding="UTF8",e.open("w"),e.write(t),e.close()}function escapeJsonString(e){return e?e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t").replace(/\f/g,"\\f"):""}function extractTextLayers(e){function t(e,r){r=r||"";for(var n=0;n1?($.writeln("Multi-paragraph text detected - treating each paragraph separately"),function(){for(var e=0,t=0;t20?"...":"")+"\"");var d=0===t;c.push({start:n,end:l,text:r,font:o.textItem.font||"Unknown",style:d?"Bold":"Regular",size:i,color:d?[0,0,0]:[80,80,80],isPrimary:d}),e=l,t1&&($.writeln("Created "+c.length+" different style entries for paragraphs"),window.forceRichTextFormatting=!0)}()):($.writeln("Single paragraph text - checking for character-level formatting"),function(){var e=new ActionReference;e.putEnumerated(charIDToTypeID("Lyr "),charIDToTypeID("Ordn"),charIDToTypeID("Trgt"));var t=executeActionGet(e);if(t.hasKey(stringIDToTypeID("textKey"))){var r=t.getObjectValue(stringIDToTypeID("textKey"));if(r.hasKey(stringIDToTypeID("textStyleRange"))){var n=r.getList(stringIDToTypeID("textStyleRange"));$.writeln("Found "+n.count+" text style ranges");for(var a=0;a1?($.writeln("Multi-paragraph text found: "+e+" paragraphs, marking as rich formatted"),!0):c.length>1?($.writeln("Multiple style ranges found, marking as rich formatted"),!0):function(){for(var e=0;e0)for(var f=0;f Optional[str]: """Find the Photoshop application path""" ps_paths = [ "/Applications/Adobe Photoshop 2025/Adobe Photoshop 2025.app", "/Applications/Adobe Photoshop 2024/Adobe Photoshop 2024.app", "/Applications/Adobe Photoshop/Adobe Photoshop.app", "/Applications/Adobe Photoshop CC 2025/Adobe Photoshop CC 2025.app", "/Applications/Adobe Photoshop CC 2024/Adobe Photoshop CC 2024.app", ] for path in ps_paths: if os.path.exists(path): logger.info(f"Found Photoshop at: {path}") return path return None def _launch_photoshop(self) -> bool: """Launch Photoshop if it's not already running""" try: # First determine the correct AppleScript name for Photoshop ps_names = [ "Adobe Photoshop 2025", "Adobe Photoshop 2024", "Adobe Photoshop CC 2025", "Adobe Photoshop CC 2024", "Adobe Photoshop" ] # Try to find a running instance first ps_running_script = """ tell application "System Events" set ps_processes to (every process whose name begins with "Adobe Photoshop") if (count of ps_processes) > 0 then return true else return false end if end tell """ result = subprocess.run( ["osascript", "-e", ps_running_script], capture_output=True, text=True, check=True ) is_running = result.stdout.strip() == "true" if is_running: logger.info("Photoshop is already running") return True # Try each possible application name for ps_name in ps_names: logger.info(f"Trying to launch Photoshop as: {ps_name}") try: # Check if this application exists check_app_script = f""" tell application "System Events" return exists application process "{ps_name}" end tell """ check_result = subprocess.run( ["osascript", "-e", check_app_script], capture_output=True, text=True, check=False ) if check_result.returncode == 0 and check_result.stdout.strip() == "true": # Launch this version of Photoshop launch_script = f""" tell application "{ps_name}" activate end tell """ subprocess.run(["osascript", "-e", launch_script], check=True) logger.info(f"Photoshop ({ps_name}) launched successfully") time.sleep(1) # Give Photoshop just a moment to initialize self.ps_app_name = ps_name return True else: # Try launching it anyway try: launch_script = f""" tell application "{ps_name}" activate end tell """ result = subprocess.run(["osascript", "-e", launch_script], check=False, capture_output=True, text=True) if result.returncode == 0: logger.info(f"Photoshop ({ps_name}) launched successfully") time.sleep(1) # Give Photoshop just a moment to initialize self.ps_app_name = ps_name return True except Exception as ex: logger.debug(f"Failed to launch {ps_name}: {ex}") continue except Exception as ex: logger.debug(f"Failed to check {ps_name}: {ex}") continue # If we got here, we couldn't launch any version logger.error("Couldn't launch any version of Photoshop") return False except Exception as e: logger.error(f"Error launching Photoshop: {e}") return False def open_file(self, file_path: str) -> bool: """Open a PSD file in Photoshop""" try: file_path = os.path.abspath(file_path) logger.debug(f"Attempting to open file: {file_path}") # Escape quotes and backslashes in the path file_path_escaped = file_path.replace('\\', '\\\\').replace('"', '\\"') # Use a more reliable approach with shell quoting # Create a temporary AppleScript file with properly formatted path temp_script_path = os.path.expanduser("~/Desktop/temp_ps_open.scpt") # The key is to use single quotes for the outer string and double quotes for the inner POSIX file path script_content = f''' tell application "{self.ps_app_name}" set theFile to POSIX file "{file_path_escaped}" open theFile end tell ''' with open(temp_script_path, "w") as f: f.write(script_content) # Run the AppleScript directly from the file result = subprocess.run(["osascript", temp_script_path], capture_output=True, text=True) if result.returncode != 0: logger.error(f"AppleScript error: {result.stderr}") # Try alternate method using the 'do shell script' approach logger.debug("Trying alternate method...") alt_script_content = f''' tell application "{self.ps_app_name}" activate end tell do shell script "open -a '{self.ps_app_name}' '{file_path_escaped}'" ''' with open(temp_script_path, "w") as f: f.write(alt_script_content) result = subprocess.run(["osascript", temp_script_path], capture_output=True, text=True) if result.returncode != 0: logger.error(f"Alternate method also failed: {result.stderr}") return False # Clean up the temporary script file try: os.remove(temp_script_path) except: pass logger.info(f"Opened file: {file_path}") return True except Exception as e: logger.error(f"Error opening file: {e}") return False def run_jsx_script(self, script: str, script_args: dict = None) -> bool: """Run a JSX script in Photoshop with optional arguments""" try: # First disable all dialogs in Photoshop to avoid any user interaction self._disable_dialogs() # First try to use the ExtractTextWithBreaks.jsx file directly if it's available script_dir = os.path.dirname(os.path.abspath(__file__)) jsx_file_path = os.path.join(script_dir, "ExtractTextWithBreaks.jsx") # Check if the JSX file exists if os.path.exists(jsx_file_path): logger.info(f"Using existing JSX file: {jsx_file_path}") output_path = script_args.get("OUTPUT_PATH", "") # Modify the JSX file to add OUTPUT_PATH with open(jsx_file_path, "r") as f: jsx_content = f.read() # Create a temporary version with our output path variable temp_jsx_path = os.path.expanduser("~/Desktop/temp_extract_text.jsx") with open(temp_jsx_path, "w") as f: # Add the output path variable declaration f.write(f'var OUTPUT_PATH = "{output_path}";\n\n') # Add code to suppress all dialogs f.write('// Disable all dialogs\n') f.write('app.displayDialogs = DialogModes.NO;\n') f.write('app.displayStatusDialogs = false;\n\n') f.write(jsx_content) # Use the direct do script AppleScript approach - more reliable and faster than 'open' try: script_content = f''' tell application "{self.ps_app_name}" do javascript file "{temp_jsx_path}" end tell ''' result = subprocess.run( ["osascript", "-e", script_content], check=False, capture_output=True, text=True ) if result.returncode == 0: logger.info("Successfully executed JSX script using 'do javascript' approach") return True else: logger.info("'do javascript' approach returned non-zero: falling back to open method") # Fall back to the open command as a backup result = subprocess.run( ["open", "-a", self.ps_app_name, temp_jsx_path], check=False, capture_output=True, text=True ) if result.returncode == 0: logger.info("Successfully executed JSX script using fallback 'open' command") return True else: logger.error(f"Error using fallback 'open' command: {result.stderr}") except Exception as e: logger.error(f"Error executing JSX script: {e}") # Fall back to our original script approach # Create a temporary script file script_path = os.path.expanduser("~/Desktop/temp_ps_script.jsx") # If we have arguments to pass to the script if script_args: # Add variable declarations at the top of the script var_declarations = "" for var_name, var_value in script_args.items(): if isinstance(var_value, str): # Escape backslashes and quotes in string values escaped_value = var_value.replace('\\', '\\\\').replace('"', '\\"') var_declarations += f'var {var_name} = "{escaped_value}";\n' else: var_declarations += f'var {var_name} = {var_value};\n' script = var_declarations + script # Write the script to a file with open(script_path, "w") as f: f.write(script) logger.debug(f"JSX script written to: {script_path}") # Try to run the script using the direct 'do javascript' approach try: script_content = f''' tell application "{self.ps_app_name}" do javascript file "{script_path}" end tell ''' result = subprocess.run( ["osascript", "-e", script_content], check=False, capture_output=True, text=True ) if result.returncode == 0: logger.info("Executed JSX script using 'do javascript' approach") return True else: # Fall back to the open command if do javascript fails result = subprocess.run( ["open", "-a", self.ps_app_name, script_path], check=False, capture_output=True, text=True ) if result.returncode == 0: logger.info("Executed JSX script by opening it directly") return True else: logger.error(f"Error opening JSX script directly: {result.stderr}") except Exception as e: logger.error(f"Error executing JSX script: {e}") # Clean up the temporary files try: if os.path.exists(script_path): os.remove(script_path) if os.path.exists(temp_jsx_path): os.remove(temp_jsx_path) except: pass logger.warning("JSX script execution attempts failed") return False except Exception as e: logger.error(f"Error running JSX script: {e}") return False def _disable_dialogs(self) -> bool: """Disable all dialogs in Photoshop to prevent user interaction""" try: # Execute the JavaScript directly via AppleScript - faster than creating a temporary file disable_dialogs_js = """ // Script to disable all dialogs in Photoshop app.displayDialogs = DialogModes.NO; app.displayStatusDialogs = false; // Disable all other dialog types try { // General preferences for dialog suppression var desc = new ActionDescriptor(); desc.putBoolean(stringIDToTypeID("dontShowAgain"), true); app.putCustomOptions("dontShowDialog", desc, true); } catch (e) { // Ignore errors } """ # Execute JavaScript directly - much faster applescript = f''' tell application "{self.ps_app_name}" do javascript "{disable_dialogs_js.replace('"', '\\"').replace("\n", "\\n")}" end tell ''' result = subprocess.run( ["osascript", "-e", applescript], check=False, capture_output=True, text=True ) if result.returncode == 0: logger.info("Disabled dialogs in Photoshop using direct JavaScript execution") return True else: logger.warning(f"Direct JavaScript execution failed: {result.stderr}") # Fall back to the file-based approach if direct execution fails disable_script_path = os.path.expanduser("~/Desktop/disable_dialogs.jsx") with open(disable_script_path, "w") as f: f.write(disable_dialogs_js) # Use the 'do javascript file' approach applescript = f''' tell application "{self.ps_app_name}" do javascript file "{disable_script_path}" end tell ''' result = subprocess.run( ["osascript", "-e", applescript], check=False, capture_output=True, text=True ) if result.returncode == 0: logger.info("Disabled dialogs in Photoshop using file-based JavaScript") else: # Last resort - use open command subprocess.run( ["open", "-a", self.ps_app_name, disable_script_path], check=False, capture_output=True, text=True ) logger.info("Disabled dialogs in Photoshop using open command") # Clean up try: os.remove(disable_script_path) except: pass return True except Exception as e: logger.error(f"Error disabling dialogs: {e}") return False def close_document(self, save_changes: bool = False) -> bool: """Close the active document""" try: # Create a temporary AppleScript file temp_script = os.path.expanduser("~/Desktop/temp_ps_close.scpt") with open(temp_script, "w") as f: f.write(f""" tell application "{self.ps_app_name}" close current document saving {"yes" if save_changes else "no"} end tell """) # Run the AppleScript file directly subprocess.run(["osascript", temp_script], check=True) # Clean up the temporary script file os.remove(temp_script) logger.info(f"Closed document (save={save_changes})") return True except Exception as e: logger.error(f"Error closing document: {e}") return False def extract_text_from_psd(psd_path: Path, output_dir: Path) -> str: """Extract text from a PSD file using the Mac Photoshop controller""" # Create output filename in the same directory as the PSD file # Use the same filename but with -textonly.json suffix output_filename = f"{psd_path.stem}-textonly.json" # Place the output file in the same directory as the PSD file output_path = psd_path.parent / output_filename # Ensure the output directory exists os.makedirs(output_dir, exist_ok=True) # Make sure the output path is writable test_output = output_path.as_posix() try: with open(test_output, 'w') as f: f.write('test') os.remove(test_output) logger.debug(f"Output path is writable: {test_output}") except Exception as e: logger.error(f"Output path is not writable: {test_output} - {e}") # Try using the Desktop as a fallback output_path = Path(os.path.expanduser("~/Desktop")) / output_filename logger.info(f"Using fallback output path: {output_path}") ps = MacPhotoshop() try: # Open the PSD file if not ps.open_file(str(psd_path)): logger.error(f"Failed to open {psd_path}") return None # No need to wait extra time here # Create a modified version of the script that doesn't prompt for save # by replacing the file dialog code with a direct file path script_dir = os.path.dirname(os.path.abspath(__file__)) jsx_file_path = os.path.join(script_dir, "ExtractTextWithBreaks.jsx") # Create a temporary modified version of the JSX script temp_jsx_path = os.path.expanduser("~/Desktop/temp_extract_text.jsx") if os.path.exists(jsx_file_path): # Read the original script with open(jsx_file_path, "r") as f: jsx_content = f.read() # Modify the script to automatically save to our specified location # This replaces any File.saveDialog code with direct file creation modified_content = jsx_content.replace( 'var n=t.replace(/\\.[^\\.]+$/,"-textonly.json"),o=File.saveDialog("Save text layer data as:",n);if(!o)return;', f'var n=t.replace(/\\.[^\\.]+$/,"-textonly.json"),o=new File("{output_path.as_posix()}");' ) # Also check for another possible pattern for the dialog modified_content = modified_content.replace( 'var o=File.saveDialog("Save text layer data as:",n);if(!o)return;', f'var o=new File("{output_path.as_posix()}");' ) # Write the modified script with open(temp_jsx_path, "w") as f: f.write(modified_content) # Run the modified script logger.info(f"Running modified JSX script from: {temp_jsx_path}") # Use 'open' command to run the script directly result = subprocess.run( ["open", "-a", ps.ps_app_name, temp_jsx_path], check=False, capture_output=True, text=True ) if result.returncode != 0: logger.error(f"Error running JSX script via open command: {result.stderr}") else: logger.info("JSX script executed successfully") else: # If the JSX file doesn't exist, fall back to the embedded script logger.warning(f"JSX file not found at {jsx_file_path}, using embedded script") # Use a simplified script with direct file path assignment script_args = { "OUTPUT_PATH": output_path.as_posix() } # Make a copy of the original script with the output path directly set modified_script = EXTRACT_TEXT_SCRIPT.replace( 'var n=t.replace(/\\.[^\\.]+$/,"-textonly.json"),o=File.saveDialog("Save text layer data as:",n);if(!o)return;', f'var n=t.replace(/\\.[^\\.]+$/,"-textonly.json"),o=new File("{output_path.as_posix()}");' ) if not ps.run_jsx_script(modified_script, script_args): logger.error(f"Failed to run extraction script on {psd_path}") return None # Wait for file to be created with a more efficient approach timeout = 10 # seconds - reduced timeout start_time = time.time() check_interval = 0.1 # Check more frequently but with less logging next_log_time = start_time + 1 # Log only every second # Check both the output file and the completion signal file signal_file = Path(output_path.parent) / "complete_signal.tmp" while not (output_path.exists() or signal_file.exists()) and time.time() - start_time < timeout: time.sleep(check_interval) # Only log periodically to reduce overhead current_time = time.time() if current_time >= next_log_time: logger.debug(f"Waiting for output file: {output_path}") next_log_time = current_time + 1 # Remove the signal file if it exists if signal_file.exists(): try: os.remove(signal_file) logger.debug("Removed completion signal file") except: pass # Close the document ps.close_document(save_changes=False) # Clean up the temporary script file if os.path.exists(temp_jsx_path): try: os.remove(temp_jsx_path) except: pass if output_path.exists(): logger.info(f"Successfully saved text to {output_path}") return output_path.as_posix() else: # Check if file was created with a different name or in a different location logger.warning(f"Output file not created at expected path: {output_path}") # First check for files on the desktop that might have actual content desktop_path = Path(os.path.expanduser("~/Desktop")) # Look for recently created JSON files on desktop with same base name desktop_base_name = psd_path.stem + "-textonly.json" desktop_file_path = desktop_path / desktop_base_name if desktop_file_path.exists() and desktop_file_path.stat().st_mtime > start_time: logger.info(f"Found matching JSON file on desktop: {desktop_file_path}") # Check if it has content (not empty) try: with open(desktop_file_path, 'r') as f: content = f.read() # Check if it has text layers if '"textLayerCount": 0' not in content and '"textLayers": []' not in content: logger.info("Desktop file appears to have text layer content") # Try to copy the file to be next to the original PSD file target_path = psd_path.parent / f"{psd_path.stem}-textonly.json" import shutil shutil.copy2(str(desktop_file_path), str(target_path)) logger.info(f"Copied file with text content from {desktop_file_path} to {target_path}") return target_path.as_posix() except Exception as e: logger.error(f"Error checking desktop file: {e}") # Check if there's a signal file even if the JSON wasn't created signal_file_on_desktop = desktop_path / "complete_signal.tmp" if signal_file_on_desktop.exists(): logger.info("Found completion signal file but no output file - possibly a document with no text layers") try: os.remove(signal_file_on_desktop) except: pass # Create an empty JSON file at the expected location, but only if we couldn't find content try: empty_json = { "documentName": psd_path.name, "psdPath": str(psd_path), "extractedAt": time.strftime("%Y-%m-%d %H:%M:%S"), "dimensions": {"width": 0, "height": 0}, "textLayerCount": 0, "textLayers": [] } with open(output_path, 'w') as f: json.dump(empty_json, f, indent=2) logger.info(f"Created empty JSON result for document with no text layers at {output_path}") return output_path.as_posix() except Exception as e: logger.error(f"Failed to create empty JSON file: {e}") # Look for recently created JSON files recent_json_files = [f for f in desktop_path.glob("*.json") if f.stat().st_mtime > start_time] if recent_json_files: logger.info(f"Found potentially related JSON files: {recent_json_files}") # Find the first file that matches our PSD name pattern or has non-empty content best_match = None for json_file in recent_json_files: # Check if the filename matches our PSD (highest priority) if psd_path.stem in json_file.stem: best_match = json_file logger.info(f"Found JSON file matching PSD name: {json_file}") break # Check file content for text layers try: with open(json_file, 'r') as f: content = f.read() # Check if it has text layers if '"textLayerCount": 0' not in content and '"textLayers": []' not in content: best_match = json_file logger.info(f"Found JSON file with text layer content: {json_file}") break except: pass # If no best match found, just use the first file if not best_match and recent_json_files: best_match = recent_json_files[0] logger.info(f"Using first available JSON file: {best_match}") # Try to move the file to be next to the original PSD file if best_match: try: # Create a destination file path next to the original PSD target_path = psd_path.parent / f"{psd_path.stem}-textonly.json" # Copy the file to be next to the PSD import shutil shutil.copy2(str(best_match), str(target_path)) logger.info(f"Copied file from {best_match} to {target_path}") # Return the new path return target_path.as_posix() except Exception as e: logger.error(f"Failed to copy file to output directory: {e}") # Return the original file path if copy fails return best_match.as_posix() return None except Exception as e: logger.error(f"Error extracting text from {psd_path}: {str(e)}") return None def batch_extract_text(input_dir: str, output_dir: str = None, recursive: bool = False) -> List[str]: """Extract text from all PSD files in the input directory output_dir is now optional - if None, files will be placed next to their PSDs """ input_path = Path(input_dir).resolve() output_path = Path(input_dir).resolve() # Default to same as input if output_dir: output_path = Path(output_dir).resolve() # Create output directory if it doesn't exist and explicitly specified if not output_path.exists(): output_path.mkdir(parents=True) # Find all PSD files - first check what's available directly logger.debug(f"Listing all files in directory to debug:") try: all_files = [f for f in os.listdir(input_dir) if f.lower().endswith('.psd')] logger.debug(f"Found {len(all_files)} PSD files directly: {all_files}") except Exception as e: logger.error(f"Error listing files in directory: {e}") all_files = [] # Find all PSD files using glob pattern pattern = '**/*.psd' if recursive else '*.psd' psd_files = list(input_path.glob(pattern)) if not psd_files: logger.warning(f"No PSD files found in {input_path}") return [] logger.info(f"Found {len(psd_files)} PSD files to process") # Extract text from each PSD file results = [] for psd_file in psd_files: # Replace any spaces or special characters in filename for safer handling logger.debug(f"Processing file: {psd_file}") # Try to rename the file temporarily to remove spaces (optional approach) # This is commented out as it's a more intrusive option # can be enabled if the file path escaping doesn't work # temp_filename = str(psd_file).replace(" ", "_") # try: # os.rename(str(psd_file), temp_filename) # logger.debug(f"Temporarily renamed to: {temp_filename}") # result = extract_text_from_psd(Path(temp_filename), output_path) # # Rename back after processing # os.rename(temp_filename, str(psd_file)) # except Exception as e: # logger.error(f"Error with temporary rename: {e}") # result = extract_text_from_psd(psd_file, output_path) result = extract_text_from_psd(psd_file, output_path) if result: results.append(result) logger.info(f"Successfully processed {len(results)} of {len(psd_files)} files") return results def parse_arguments(): """Parse command line arguments""" parser = argparse.ArgumentParser( description='Extract text from PSD files on macOS', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Extract text from all PSD files in the current directory python mac_ps_extract.py . # Extract text from all PSD files in a specific directory python mac_ps_extract.py /path/to/psd_files # Extract text and save JSON files to a different directory python mac_ps_extract.py /path/to/psd_files -o /path/to/output # Extract text from all PSD files including subdirectories python mac_ps_extract.py /path/to/psd_files -r """ ) parser.add_argument('input_dir', help='Directory containing PSD files') parser.add_argument('--output-dir', '-o', default=None, help='Directory to save extracted JSON files (defaults to input_dir)') parser.add_argument('--recursive', '-r', action='store_true', help='Search for PSD files in subdirectories') parser.add_argument('--verbose', '-v', action='store_true', help='Enable verbose logging') return parser.parse_args() def main(): """Main function""" args = parse_arguments() input_dir = args.input_dir output_dir = args.output_dir # This can now be None - files will be placed next to PSDs # Set logging level based on verbose flag if args.verbose: logger.setLevel(logging.DEBUG) # Set log format to include more details for handler in logger.handlers: handler.setFormatter(logging.Formatter( '%(asctime)s - %(levelname)s - %(message)s', '%Y-%m-%d %H:%M:%S' )) logger.info(f"Processing PSD files from: {input_dir}") if output_dir: logger.info(f"Saving extracted text to: {output_dir}") else: logger.info(f"Saving extracted text next to PSD files") logger.info(f"Recursive search: {args.recursive}") # Get the list of all PSD files first input_path = Path(input_dir).resolve() pattern = '**/*.psd' if args.recursive else '*.psd' psd_files = list(input_path.glob(pattern)) # Process the files results = batch_extract_text(input_dir, output_dir, args.recursive) # All files were processed successfully, but we'll keep track of how many had text # vs. how many were empty files (no text layers) processed_stems = [Path(r).stem.replace('-textonly', '') for r in results] files_with_text = [] # We can't tell directly which files had text, but we processed all files if results: logger.info(f"Extraction complete. Processed {len(results)} of {len(psd_files)} files:") for result in results: logger.info(f" - {result}") print(f"\nSuccessfully processed {len(results)} of {len(psd_files)} PSD files.") if output_dir: print(f"JSON files saved to: {output_dir}") else: print(f"JSON files saved next to their PSD files") print("\nNaming convention: [psd_filename]-textonly.json") else: logger.warning("No text was extracted from any files.") print("\nNo PSD files were processed successfully.") if len(psd_files) > 0: print(f"Found {len(psd_files)} PSD files but none could be processed:") for f in psd_files[:5]: # Show only first 5 to avoid overwhelming output print(f" - {f.name}") if len(psd_files) > 5: print(f" ... and {len(psd_files) - 5} more") print("\nCheck for errors in the log or try running with -v for verbose output.") else: print("\nNo PSD files were found. Check the input directory or enable recursive search with -r.") if __name__ == "__main__": main()