adobe-ps-scripts-loreal/mac_ps_update.py
DJP 4a192a8c97 Initial commit: Adobe Photoshop API text management scripts
Local and cloud-based workflows for extracting and updating
text layers in PSD files via ExtendScript and Adobe PS API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 13:46:52 -05:00

620 lines
No EOL
29 KiB
Python

#!/usr/bin/env python3
"""
Mac Photoshop Text Updater
--------------------------
A macOS-specific script to update text in 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, Dict, Any, Optional, Tuple
# 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 updateTextLayers.jsx script as a string
UPDATE_TEXT_SCRIPT = r"""
// Photoshop Script to Update Text Layers
#target photoshop
// Disable all dialogs
app.displayDialogs = DialogModes.NO;
app.displayStatusDialogs = false;
function readTextFile(e){return e.open("r"),content=e.read(),e.close(),content}function parseJson(e){try{if("undefined"!=typeof JSON&&JSON.parse)return JSON.parse(e)}catch(e){$.writeln("Native JSON parse failed: "+e)}try{var t=eval("("+e+")");return t}catch(e){$.writeln("JSON eval failed: "+e);try{$.writeln("Attempting emergency JSON parsing...");var r=e.match(/"textLayers"\\s*:\\s*\\[(([\\s\\S]*?))\\]\\s*\\}/);if(r&&r[1]){for(var n={textLayers:[]},a=/\\{\\s*"id"[\\s\\S]*?styleInfo[\\s\\S]*?\\}\\s*\\}/g,i;null!==(i=a.exec(r[1]));){var l=i[0],s=l.match(/"name"\\s*:\\s*"([^"]*)"$/),o=l.match(/"text"\\s*:\\s*"([^"]*)"$/),y=l.match(/"updatedText"\\s*:\\s*"([^"]*)"$/);s&&o&&y&&n.textLayers.push({name:s[1].replace(/\\n/g,"\\n"),text:o[1].replace(/\\n/g,"\\n"),updatedText:y[1].replace(/\\n/g,"\\n"),exists:!1})}if(n.textLayers.length>0)return $.writeln("Successfully extracted "+n.textLayers.length+" layers with emergency parser"),n}throw new Error("Emergency parsing failed")}catch(t){throw $.writeln("Emergency parsing failed: "+t),new Error("Failed to parse JSON: "+e.message)}}}function findLayerByName(e,t){function r(e,r){r=r||"";for(var n=0;n<e.length;n++){var a=e[n];if($.writeln(r+"- Checking: "+a.name+" (Type: "+a.typename+")"),a.name===t)return $.writeln(r+" ✓ FOUND MATCH!"),a;if("LayerSet"===a.typename){$.writeln(r+" > Searching inside group: "+a.name);var i=l(a,r+" ");if(i)return i}}return null}function l(e,t){t=t||"";for(var r=0;r<e.layers.length;r++){var n=e.layers[r];if($.writeln(t+"- Checking: "+n.name+" (Type: "+n.typename+")"),n.name===t)return $.writeln(t+" ✓ FOUND MATCH!"),n;if("LayerSet"===n.typename){$.writeln(t+" > Searching inside group: "+n.name);var a=l(n,t+" ");if(a)return a}}return null}return $.writeln("Looking for layer: "+t),result=r(e.layers),result||($.writeln("❌ Layer not found: "+t),null)}function updateTextLayer(e,t,r){if(e.kind===LayerKind.TEXT){$.writeln("Updating text layer: "+e.name),$.writeln("Original layer text: "+e.textItem.contents),$.writeln("New text from JSON: "+t);try{var n=t;if(n=n.replace(/\\n/g,"\\n"),n=n.replace(/\\r/g,"\\r"),r&&r.styleInfo&&r.styleInfo.styles&&r.styleInfo.styles.length>1&&r.hasRichTextFormatting){$.writeln("Layer has rich text formatting - attempting to preserve styles with the new text");try{$.writeln("Setting basic text to: "+n),e.textItem.contents=n,app.activeDocument.activeLayer=e,$.writeln("Rich text update is experimental and may not work perfectly"),$.writeln("Original style ranges count: "+r.styleInfo.styles.length),r.styleInfo.styles.length>0&&($.writeln("Attempting to apply rich text formatting to "+r.styleInfo.styles.length+" style ranges"),new ActionReference,new ActionReference,ref.putEnumerated(charIDToTypeID("Lyr "),charIDToTypeID("Ordn"),charIDToTypeID("Trgt")),executeActionGet(ref),function(){for(var a=0;a<r.styleInfo.styles.length;a++){var i=r.styleInfo.styles[a],l=i.start,s=i.end;s>n.length&&(s=n.length),l<s&&l>=0&&s<=n.length&&($.writeln("Applying style to range ["+l+"-"+s+"]: "+i.font+" "+i.style+" "+i.size+"pt"),function(){var e=new ActionDescriptor,t=new ActionDescriptor;t.putInteger(stringIDToTypeID("from"),l),t.putInteger(stringIDToTypeID("to"),s),e.putObject(stringIDToTypeID("range"),stringIDToTypeID("textRange"),t);var r=new ActionDescriptor;i.font&&"Unknown"!==i.font&&r.putString(stringIDToTypeID("fontName"),i.font),i.style&&"Regular"!==i.style&&r.putString(stringIDToTypeID("fontStyleName"),i.style),i.size&&r.putUnitDouble(stringIDToTypeID("size"),charIDToTypeID("#Pnt"),i.size),i.color&&i.color.length>0&&function(){var e=new ActionDescriptor,t=new ActionDescriptor;if(i.color.length>=3){t.putDouble(stringIDToTypeID("red"),i.color[0]),t.putDouble(stringIDToTypeID("green"),i.color[1]),t.putDouble(stringIDToTypeID("blue"),i.color[2]),e.putObject(stringIDToTypeID("color"),stringIDToTypeID("RGBColor"),t),r.putObject(stringIDToTypeID("color"),stringIDToTypeID("color"),e)}}(),e.putObject(stringIDToTypeID("textStyle"),stringIDToTypeID("textStyle"),r),executeAction(stringIDToTypeID("setd"),e,DialogModes.NO)}())}else $.writeln("Skipping invalid range ["+l+"-"+s+"]")}})();return!0}catch(r){return $.writeln("ERROR applying rich text formatting: "+r),$.writeln("Falling back to basic text update"),e.textItem.contents=n,!0}}else return $.writeln("Setting text to: "+n),e.textItem.contents=n,!0}catch(r){return $.writeln("ERROR updating text: "+r),!1}}return $.writeln("Not a text layer: "+e.name+" (kind: "+e.kind+")"),!1}function listAvailableTextLayers(e){function t(e,r){r=r||"";for(var n=0;n<e.length;n++){var a=e[n],i=r?r+"/"+a.name:a.name;a.kind===LayerKind.TEXT&&l.push({name:a.name,path:i,text:a.textItem.contents}),"LayerSet"===a.typename&&t(a.layers,i)}}var l=[];return t(e.layers),l}
// Define updateLayers function explicitly
function updateLayers() {
var t=0,r=0,a=0,i=[];
$.writeln("\\n=== MATCHING JSON LAYERS WITH DOCUMENT ===");
for(var s=0;s<n.textLayers.length;s++){
var o=n.textLayers[s].name;
l[o]?($.writeln("✓ Found match for: "+o),n.textLayers[s].exists=!0):($.writeln("❌ No match found for: "+o),n.textLayers[s].exists=!1)
}
for(s=0;s<n.textLayers.length;s++){
var y=n.textLayers[s];
if($.writeln("\\n=== PROCESSING LAYER: "+y.name+" ==="),!y.exists)
$.writeln("⚠️ Skipping - layer doesn't exist in document"),a++;
else if($.writeln('Original text: "'+y.text+'"'),$.writeln('Updated text: "'+y.updatedText+'"'),!y.updatedText)
$.writeln("⚠️ Skipping - no updated text"),a++;
else{
var d=y.text===y.updatedText;
if($.writeln("Exact text match? "+d),d)
$.writeln("⚠️ Skipping - completely identical text"),a++;
else{
$.writeln("Looking for layer: "+y.name);
var u=findLayerByName(e,y.name);
if(u){
var c=y.hasRichTextFormatting||y.styleInfo&&y.styleInfo.styles&&y.styleInfo.styles.length>1;
c&&$.writeln("Layer has rich text formatting information");
updateTextLayer(u,y.updatedText,y)?t++:(r++,i.push(y.name+" (update failed)"))
}else r++,i.push(y.name+" (not found)")
}
}
}
var g="Text Layer Update Complete\\n\\n";
g+="Successfully updated: "+t+" layers\\n";
g+="Skipped (no change): "+a+" layers\\n";
g+="Failed to update: "+r+" layers\\n";
r>0&&(g+="\\nFailed layers:\\n"+i.join("\\n"));
alert(g);
}
function main(){try{if(!documents.length)return void alert("Please open a PSD file before running this script.");var e=app.activeDocument,t=File(JSON_FILE_PATH);if(!t)return;var r=readTextFile(t);r=(r=(r=(r=r.replace(/(\d+)\\s*px/g,"$1")).replace(/([^\\\\])\\\\([^\\\\"])/g,"$1\\\\\\\\$2")).replace(/,\\s*([}\\]])/g,"$1")).replace(/\\r\\n/g,"\\\\n").replace(/\\r/g,"\\\\n");try{var n=parseJson(r);if(!n.textLayers||!n.textLayers.length)return void alert("No text layers found in the JSON file.");$.writeln("\\n=== SCANNING DOCUMENT FOR TEXT LAYERS ===");var a=listAvailableTextLayers(e);$.writeln("Found "+a.length+" text layers in document");for(var i=0;i<a.length;i++)$.writeln(i+1+". "+a[i].name+': "'+a[i].text.substring(0,30)+(a[i].text.length>30?"...":"")+'"');var l={};for(i=0;i<a.length;i++)l[a[i].name]=a[i];app.activeDocument.suspendHistory("Update Text Layers","updateLayers()")}catch(e){alert("Error parsing JSON file: "+e.message+"\\n\\nPlease check that the file is valid JSON.")}}catch(e){alert("Error: "+e.message)}}
// Execute main function immediately - don't wait for user input
main();
"""
class MacPhotoshop:
"""A class for controlling Photoshop on macOS using AppleScript"""
def __init__(self):
"""Initialize and connect to Photoshop"""
self.ps_path = self._find_photoshop()
if not self.ps_path:
logger.warning("Could not find Photoshop installation path")
# Default application name (will be updated during launch)
self.ps_app_name = "Adobe Photoshop 2025"
# Launch or connect to Photoshop
self._launch_photoshop()
def _find_photoshop(self) -> 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(5) # Give Photoshop more time 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(5) # Give Photoshop more time 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:
# 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}")
# Create a temporary AppleScript file with properly formatted path
temp_applescript_path = os.path.expanduser("~/Desktop/temp_ps_run.scpt")
# Escape quotes and backslashes in the path
script_path_escaped = script_path.replace('\\', '\\\\').replace('"', '\\"')
# Use the proper syntax for the do javascript file command
script_content = f'''
tell application "{self.ps_app_name}"
do javascript file "{script_path_escaped}"
end tell
'''
with open(temp_applescript_path, "w") as f:
f.write(script_content)
# Run the AppleScript file directly
result = subprocess.run(["osascript", temp_applescript_path],
capture_output=True, text=True)
if result.returncode != 0:
logger.error(f"AppleScript error: {result.stderr}")
# Try alternate method using the 'open' command
logger.debug("Trying alternate method to run JSX...")
result = subprocess.run(
["open", "-a", self.ps_app_name, script_path],
check=False, capture_output=True, text=True
)
if result.returncode != 0:
logger.error(f"Alternate method also failed: {result.stderr}")
return False
# Clean up the temporary files
try:
os.remove(script_path)
os.remove(temp_applescript_path)
except:
pass
logger.info("JSX script executed successfully")
return True
except Exception as e:
logger.error(f"Error running JSX script: {e}")
return False
def close_document(self, save_changes: bool = False) -> bool:
"""Close the active document"""
try:
close_script = f"""
tell application "{self.ps_app_name}"
close current document saving {"yes" if save_changes else "no"}
end tell
"""
subprocess.run(["osascript", "-e", close_script], check=True)
logger.info(f"Closed document (save={save_changes})")
return True
except Exception as e:
logger.error(f"Error closing document: {e}")
return False
def find_psd_for_json(json_path: Path, psd_dir: Path) -> Optional[Path]:
"""
Attempts to find the matching PSD file for a given JSON file
Args:
json_path: Path to the JSON file
psd_dir: Directory to search for PSD files
Returns:
Path to the matching PSD file, or None if not found
"""
# First check if the JSON filename contains the PSD name
json_base = json_path.stem
# Handle both -textonly.json and -textonly-updated.json naming conventions
psd_name = json_base.replace("-textonly-updated", "").replace("-textonly", "")
# Look for exact filename match
psd_path = psd_dir / f"{psd_name}.psd"
if psd_path.exists():
logger.info(f"Found matching PSD: {psd_path.name} for {json_path.name}")
return psd_path
# If exact match not found, try to load JSON and check documentName
try:
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
if 'documentName' in data:
doc_name = data['documentName']
# Remove extension if present
doc_name = doc_name.rsplit('.', 1)[0] if '.' in doc_name else doc_name
# Try to find PSD matching the document name
psd_path = psd_dir / f"{doc_name}.psd"
if psd_path.exists():
logger.info(f"Found matching PSD based on documentName: {psd_path.name}")
return psd_path
# Try searching for any PSD with a similar name
for psd_file in psd_dir.glob("*.psd"):
# Simple similarity check - if document name is contained in PSD name
if doc_name.lower() in psd_file.stem.lower():
logger.info(f"Found likely matching PSD: {psd_file.name}")
return psd_file
except Exception as e:
logger.warning(f"Error reading JSON {json_path.name}: {str(e)}")
logger.warning(f"No matching PSD found for {json_path.name}")
return None
def update_text_in_psd(psd_path: Path, json_path: Path, save_changes: bool = False) -> bool:
"""
Updates text layers in a PSD file using data from a JSON file
Args:
psd_path: Path to the PSD file
json_path: Path to the JSON file with updated text
save_changes: Whether to save changes to the PSD file
Returns:
True if the update was successful, False otherwise
"""
ps = MacPhotoshop()
try:
# Open the PSD file
if not ps.open_file(str(psd_path)):
logger.error(f"Failed to open {psd_path}")
return False
# Run the update script with the JSON file path
script_args = {
"JSON_FILE_PATH": json_path.as_posix()
}
# Replace the part that opens a file dialog with our direct file path
modified_script = UPDATE_TEXT_SCRIPT.replace(
'var t=File.openDialog("Select the JSON file with text layer data","*.json");if(!t)return;',
f'var t=File(JSON_FILE_PATH);if(!t)return;'
)
if not ps.run_jsx_script(modified_script, script_args):
logger.error(f"Failed to run update script on {psd_path}")
return False
# Close the document, optionally saving changes
ps.close_document(save_changes=save_changes)
logger.info(f"Successfully updated text in {psd_path.name}" +
(" and saved changes" if save_changes else ""))
return True
except Exception as e:
logger.error(f"Error updating text in {psd_path}: {str(e)}")
return False
def batch_update_text(json_dir: str, psd_dir: str, save_changes: bool = False) -> List[Tuple[str, str, bool]]:
"""
Updates text in PSD files based on JSON files in the input directory
Args:
json_dir: Directory containing JSON files
psd_dir: Directory containing PSD files
save_changes: Whether to save changes to PSD files
Returns:
List of tuples (json_path, psd_path, success)
"""
json_path = Path(json_dir).resolve()
psd_path = Path(psd_dir).resolve()
# Find all JSON files
json_files = list(json_path.glob('*-textonly*.json'))
if not json_files:
logger.warning(f"No text JSON files found in {json_path}")
return []
logger.info(f"Found {len(json_files)} JSON files to process")
# Process each JSON file
results = []
for json_file in json_files:
# Find matching PSD file
psd_file = find_psd_for_json(json_file, psd_path)
if not psd_file:
logger.warning(f"Skipping {json_file.name}: No matching PSD found")
results.append((json_file.as_posix(), None, False))
continue
# Update text in PSD
success = update_text_in_psd(psd_file, json_file, save_changes)
results.append((json_file.as_posix(), psd_file.as_posix(), success))
logger.info(f"Successfully processed {len([r for r in results if r[2]])} of {len(results)} files")
return results
def parse_arguments():
"""Parse command line arguments"""
parser = argparse.ArgumentParser(
description='Update text in PSD files on macOS using JSON data',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Update PSD files using JSON files in the current directory
python mac_ps_update.py .
# Update PSD files in one directory using JSON files from another
python mac_ps_update.py /path/to/json_files -p /path/to/psd_files
# Update and save changes to PSD files
python mac_ps_update.py /path/to/json_files --save
# Preview updates without making changes
python mac_ps_update.py /path/to/json_files --dry-run
"""
)
parser.add_argument('json_dir',
help='Directory containing JSON files with text data')
parser.add_argument('--psd-dir', '-p', default=None,
help='Directory containing PSD files (defaults to json_dir)')
parser.add_argument('--save', '-s', action='store_true',
help='Save changes to PSD files')
parser.add_argument('--dry-run', '-d', action='store_true',
help='Preview updates without making changes')
parser.add_argument('--verbose', '-v', action='store_true',
help='Enable verbose logging')
return parser.parse_args()
def main():
"""Main function"""
args = parse_arguments()
json_dir = args.json_dir
psd_dir = args.psd_dir or json_dir
save_changes = args.save and not args.dry_run
# 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'
))
if args.dry_run:
print("\nDRY RUN MODE: Changes will be previewed but not applied.\n")
logger.info(f"Processing JSON files from: {json_dir}")
logger.info(f"Looking for PSD files in: {psd_dir}")
logger.info(f"Save changes to PSDs: {save_changes}")
if args.dry_run:
# In dry-run mode, we only validate the files exist
json_path = Path(json_dir).resolve()
psd_path = Path(psd_dir).resolve()
json_files = list(json_path.glob('*-textonly*.json'))
if not json_files:
logger.warning(f"No JSON files found in {json_path}")
print(f"\nNo JSON files found in {json_path}. Check directory or naming convention.")
return
print(f"\nFound {len(json_files)} JSON files that would be processed:")
for json_file in json_files:
psd_file = find_psd_for_json(json_file, psd_path)
if psd_file:
print(f" MATCH: {json_file.name}{psd_file.name}")
else:
print(f" NO MATCH: {json_file.name} (No matching PSD found)")
print("\nDry run complete. No changes were made to PSD files.")
return
results = batch_update_text(json_dir, psd_dir, save_changes)
if results:
success_count = len([r for r in results if r[2]])
logger.info(f"Update complete. Successfully processed {success_count} of {len(results)} files:")
for json_path, psd_path, success in results:
status = "SUCCESS" if success else "FAILED"
if psd_path:
logger.info(f" {status}: {Path(json_path).name}{Path(psd_path).name}")
else:
logger.info(f" {status}: {Path(json_path).name} (No matching PSD found)")
print(f"\nUpdate complete:")
print(f" Successfully processed: {success_count} of {len(results)} files")
if save_changes:
print(f" Changes have been saved to PSD files")
else:
print(f" Changes were applied but NOT saved (use --save to save changes)")
else:
logger.warning("No files were processed.")
print(f"\nNo files were processed. Check that JSON files exist with '-textonly.json' suffix.")
if __name__ == "__main__":
main()