adobe-ps-scripts-loreal/batch_update_text.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

602 lines
No EOL
30 KiB
Python

#!/usr/bin/env python3
"""
Batch Text Updater for Photoshop
--------------------------------
This script automates updating text layers in Photoshop PSD files using JSON data.
It uses the updateTextLayers.jsx script to apply translated text while preserving formatting.
Requirements:
- Python 3.6+
- Adobe Photoshop installed
- photoshop_python_api package (install with: pip install photoshop_python_api)
"""
import os
import sys
import time
import json
import argparse
import platform
from pathlib import Path
import logging
from typing import List, Dict, Any, Optional, Tuple
# Check platform
is_windows = platform.system() == "Windows"
is_mac = platform.system() == "Darwin"
if is_mac:
# On macOS, we need to ensure we're using the right Python environment
try:
# Try to fix Mac-specific PATH issues
if "PYTHONPATH" not in os.environ:
os.environ["PYTHONPATH"] = ""
# Add the site-packages directory to path - this helps with venv environments
import site
site_packages = site.getsitepackages()
for site_path in site_packages:
if site_path not in sys.path:
sys.path.append(site_path)
print(f"Added {site_path} to Python path")
except Exception as e:
print(f"Warning: Could not set paths: {e}")
# Print some debug info on Mac
print(f"Python: {sys.version}")
print(f"System: {platform.system()} {platform.release()}")
print(f"Site packages: {', '.join(site.getsitepackages())}")
# First try to import the photoshop module - the import might be in different places
# depending on how the package was installed
found_ps_module = False
# Try to directly import the photoshop module - work around importing issues by adding to sys.modules
import importlib
import inspect
# Print path information to help debug
print("Python module search paths:")
for path in sys.path:
if 'site-packages' in path:
print(f" - {path}")
# Try to directly locate the module file
found_ps_path = None
for path in sys.path:
ps_module_path = os.path.join(path, 'photoshop')
if os.path.exists(ps_module_path):
found_ps_path = ps_module_path
print(f"Found photoshop module at: {ps_module_path}")
break
if found_ps_path:
# Check for specific module structure
init_path = os.path.join(found_ps_path, '__init__.py')
if os.path.exists(init_path):
with open(init_path, 'r') as f:
init_content = f.read()
print(f"Found __init__.py, size: {len(init_content)} bytes")
# Attempt various import strategies, trying to be very flexible
try_imports = [
# Standard import
lambda: exec('from photoshop import Session'),
# Alternative path
lambda: exec('from photoshop.api import Session'),
# Manual import construction
lambda: exec('import importlib; photoshop = importlib.import_module("photoshop"); Session = photoshop.Session'),
# Import for photoshop-python-api (with hyphen)
lambda: exec('import photoshop_python_api as photoshop; Session = photoshop.Session')
]
found_ps_module = False
last_error = None
for importer in try_imports:
try:
importer()
found_ps_module = True
break
except Exception as e:
last_error = str(e)
print(f"Import attempt failed: {e}")
continue
# Last resort - try to manually find and load the module
if not found_ps_module:
print("Attempting manual module discovery...")
try:
import subprocess
# Find where the module is installed
result = subprocess.run(
[sys.executable, "-m", "pip", "show", "photoshop-python-api"],
capture_output=True, text=True
)
if result.returncode == 0:
location_line = [line for line in result.stdout.split('\n') if line.startswith('Location:')]
if location_line:
location = location_line[0].split('Location:')[1].strip()
print(f"Package location: {location}")
# Add to Python path
if location not in sys.path:
sys.path.append(location)
print(f"Added {location} to Python path")
# Try importing again after adjusting the path
try:
from photoshop import Session
found_ps_module = True
print("Successfully imported after path adjustment")
except ImportError as e:
print(f"Still cannot import after path adjustment: {e}")
except Exception as e:
print(f"Manual module discovery failed: {e}")
# If the import failed because of 'winreg' on macOS, try creating a compatibility layer
if not found_ps_module and is_mac and "No module named 'winreg'" in str(last_error):
print("Detected 'winreg' compatibility issue on macOS.")
print("Creating a compatibility layer for Windows-specific modules...")
# Create a mock winreg module to satisfy the import
try:
import types
# Create a fake winreg module
mock_winreg = types.ModuleType("winreg")
# Add necessary constants and functions
mock_winreg.HKEY_CURRENT_USER = 0
mock_winreg.HKEY_LOCAL_MACHINE = 1
mock_winreg.KEY_ALL_ACCESS = 2
# Add mock functions
def mock_open_key(*args, **kwargs):
return None
def mock_query_value(*args, **kwargs):
# Return default Photoshop path for macOS
return "/Applications/Adobe Photoshop 2025/Adobe Photoshop 2025.app"
def mock_close_key(*args, **kwargs):
pass
# Attach functions to the mock module
mock_winreg.OpenKey = mock_open_key
mock_winreg.QueryValueEx = mock_query_value
mock_winreg.CloseKey = mock_close_key
# Add the mock module to sys.modules
sys.modules["winreg"] = mock_winreg
print("Mock winreg module created. Trying to import photoshop again...")
# Try importing again
try:
from photoshop import Session
found_ps_module = True
print("Successfully imported after adding compatibility layer")
except Exception as e:
print(f"Still cannot import after adding compatibility layer: {e}")
except Exception as e:
print(f"Failed to create compatibility layer: {e}")
# Create a custom Session class for macOS if needed
if not found_ps_module and is_mac:
print("Attempting to create a custom Photoshop Session class for macOS...")
try:
# Define a basic Session class that will work on macOS
class Session:
def __init__(self, ps_version=None):
self.app = None
self.version = ps_version or "2023"
# Try to locate Photoshop on macOS
ps_paths = [
f"/Applications/Adobe Photoshop {self.version}/Adobe Photoshop {self.version}.app",
f"/Applications/Adobe Photoshop {self.version}/Adobe Photoshop.app",
f"/Applications/Adobe Photoshop/Adobe Photoshop.app",
"/Applications/Adobe Photoshop CC 2025/Adobe Photoshop CC 2025.app",
"/Applications/Adobe Photoshop 2025/Adobe Photoshop 2025.app",
"/Applications/Adobe Photoshop 2024/Adobe Photoshop 2024.app"
]
self.ps_path = None
for path in ps_paths:
if os.path.exists(path):
self.ps_path = path
print(f"Found Photoshop at: {path}")
break
if not self.ps_path:
print("Warning: Couldn't find Photoshop application path")
# Initialize the app through AppleScript
self._initialize_app()
def _initialize_app(self):
try:
import subprocess
# Check if Photoshop is running
ps_running_script = """
tell application "System Events"
set isRunning to (count of (every process whose name is "Adobe Photoshop")) > 0
end tell
"""
# Launch Photoshop if needed
launch_script = f"""
tell application "Adobe Photoshop"
activate
end tell
"""
# Execute AppleScript to launch Photoshop
subprocess.run(["osascript", "-e", launch_script], check=True)
print("Photoshop launched successfully")
# Create a simple app object with the required methods
class PhotoshopApp:
def __init__(self):
self.activeDocument = None
def Open(self, file_path):
print(f"Opening file: {file_path}")
open_script = f"""
tell application "Adobe Photoshop"
open POSIX file "{file_path}"
end tell
"""
subprocess.run(["osascript", "-e", open_script], check=True)
self.activeDocument = self.ActiveDocument()
return True
def DoJavaScript(self, script):
# Save the script to a temporary file
script_path = os.path.expanduser("~/Desktop/temp_ps_script.jsx")
with open(script_path, "w") as f:
f.write(script)
# Run the script
run_script = f"""
tell application "Adobe Photoshop"
do javascript POSIX file "{script_path}"
end tell
"""
subprocess.run(["osascript", "-e", run_script], check=True)
# Clean up the temporary file
os.remove(script_path)
return True
class ActiveDocument:
def __init__(self):
pass
def Close(self, save_option):
close_script = f"""
tell application "Adobe Photoshop"
close current document saving {'yes' if save_option == 3 else 'no'}
end tell
"""
subprocess.run(["osascript", "-e", close_script], check=True)
self.app = PhotoshopApp()
except Exception as e:
print(f"Error initializing Photoshop on macOS: {e}")
self.app = None
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
pass # We'll let AppleScript handle the cleanup
# Set the Session class
found_ps_module = True
print("Created custom macOS Session class for Photoshop")
except Exception as e:
print(f"Failed to create custom Session class: {e}")
# If all attempts fail, notify the user
if not found_ps_module:
print("Error: Could not import the Photoshop Python API.")
print(f"Last error: {last_error}")
print("\nTroubleshooting steps:")
print("1. Verify the package is installed: pip list | grep photoshop")
print("2. Try reinstalling: pip uninstall photoshop-python-api; pip install photoshop-python-api")
print("3. Check if you're using the right version of Python")
print("4. On macOS, the photoshop-python-api package might not be compatible")
print(" - The package was designed for Windows and uses Windows-specific modules")
sys.exit(1)
else:
print("Successfully imported or created Photoshop API session handler")
# 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 (minified version)
UPDATE_TEXT_SCRIPT = r"""
// Photoshop Script to Update Text Layers
#target photoshop
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}function main(){try{if(!documents.length)return void alert("Please open a PSD file before running this script.");var e=app.activeDocument,t=File.openDialog("Select the JSON file with text layer data","*.json");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()");function s(){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)}}catch(e){alert("Error parsing JSON file: "+e.message+"\\n\\nPlease check that the file is valid JSON.")}}catch(e){alert("Error: "+e.message)}}main();
"""
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 different 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(ps_app, psd_path: Path, json_path: Path) -> bool:
"""
Opens a PSD file in Photoshop and updates text layers using the JSON data
Args:
ps_app: The Photoshop application instance
psd_path: Path to the PSD file
json_path: Path to the JSON file with updated text
Returns:
True if update was successful, False otherwise
"""
try:
# Open the PSD file
logger.info(f"Opening {psd_path}")
ps_app.Open(psd_path.as_posix())
# Modify the script to automatically use our JSON file
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=new File("{json_path.as_posix().replace("\\", "\\\\")}");'
)
# Execute the script
logger.info(f"Updating text in {psd_path.name} using {json_path.name}")
ps_app.DoJavaScript(modified_script)
# Wait a bit for the script to complete
time.sleep(2)
logger.info(f"Successfully updated text in {psd_path.name}")
return True
except Exception as e:
logger.error(f"Error updating text in {psd_path.name}: {str(e)}")
return False
finally:
# Close the document without saving
try:
ps_app.ActiveDocument.Close(2) # 2 = Don't save changes
except:
pass
def batch_update_text(json_dir: str, psd_dir: str, save_changes: bool = False) -> List[Tuple[str, str, bool]]:
"""
Processes all JSON files in the input directory and updates matching PSD files
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 with our naming convention
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 = []
with Session() as ps:
app = ps.app
# Set close option based on save_changes parameter
close_option = 3 if save_changes else 2 # 3 = Save changes, 2 = Don't save
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(app, psd_file, json_file)
# If requested to save changes and update was successful
if success and save_changes:
try:
# Save the PSD file
app.Open(psd_file.as_posix())
app.ActiveDocument.Save()
app.ActiveDocument.Close(1) # 1 = Close without dialog
logger.info(f"Saved changes to {psd_file.name}")
except Exception as e:
logger.error(f"Error saving {psd_file.name}: {str(e)}")
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='Batch update text in PSD files using JSON data',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Update PSD files using JSON files in the current directory
python batch_update_text.py .
# Update PSD files in one directory using JSON files from another
python batch_update_text.py /path/to/json_files -p /path/to/psd_files
# Update and save changes to PSD files
python batch_update_text.py /path/to/json_files --save
# Dry-run (preview updates without saving)
python batch_update_text.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()