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>
620 lines
No EOL
29 KiB
Python
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() |