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

748 lines
No EOL
28 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Extract Internal Layer IDs and Update JSON Files
This script first extracts the internal layer IDs from a PSD file using ExtendScript,
then updates the corresponding JSON file with those IDs. This ensures that
when the API is used to update text layers, it has the correct internal IDs
that Adobe's API requires.
Usage:
python extract_and_update_json.py [--psd-path PSD_PATH] [--json-path JSON_PATH]
If the PSD or JSON paths are not provided, the script will look for them.
"""
import os
import sys
import json
import argparse
import subprocess
import tempfile
from pathlib import Path
import glob
import time
import re
def create_jsx_script():
"""Create the ExtendScript to extract internal layer IDs"""
return """
// Extract Internal Layer IDs from Photoshop Document
// This script extracts the actual internal IDs that Adobe's API uses
// to identify text layers in a PSD file.
#target photoshop
// Function to extract text layers with their internal IDs
function extractTextLayersWithInternalIDs() {
if (!app.documents.length) {
return {
error: "No document open"
};
}
var doc = app.activeDocument;
var result = {
documentName: doc.name,
psdPath: doc.fullName.fsName,
extractedAt: new Date().toString(),
dimensions: {
width: doc.width.as('px'),
height: doc.height.as('px')
},
textLayerCount: 0,
textLayers: []
};
// Utility function to traverse layers and extract info
function traverseLayers(layers, path) {
path = path || "";
for (var i = 0; i < layers.length; i++) {
var layer = layers[i];
if (layer.typename === "LayerSet") {
// This is a layer group, traverse its children
var groupPath = path ? path + "/" + layer.name : layer.name;
traverseLayers(layer.layers, groupPath);
} else if (layer.kind === LayerKind.TEXT) {
// This is a text layer, extract its information
extractTextLayerInfo(layer, path);
}
}
}
function extractTextLayerInfo(layer, path) {
try {
var layerPath = path ? path + "/" + layer.name : layer.name;
// Get the layer's ID directly from the layer object
var directID = layer.id;
// Get the layer's internal ID using ActionManager
var internalID = getInternalLayerID(layer);
// Extract text content
var text = layer.textItem.contents;
// Extract basic style information
var styleInfo = {
font: layer.textItem.font,
size: layer.textItem.size.value,
color: null, // Will be populated if available
alignment: getTextAlignment(layer)
};
// Try to extract text color
try {
if (layer.textItem.color.rgb) {
styleInfo.color = [
layer.textItem.color.rgb.red,
layer.textItem.color.rgb.green,
layer.textItem.color.rgb.blue
];
}
} catch (e) {
// Color not defined or error accessing it
}
// Extract detailed formatting info if available
var richTextInfo = extractRichTextInfo(layer);
if (richTextInfo && richTextInfo.styles && richTextInfo.styles.length > 0) {
styleInfo.styles = richTextInfo.styles;
var hasRichTextFormatting = richTextInfo.styles.length > 1;
} else {
styleInfo.styles = [];
var hasRichTextFormatting = false;
}
// Create layer info object with both direct and internal IDs
var layerInfo = {
id: directID, // The ID we normally use
internalID: internalID, // The ID Adobe API might use
name: layer.name,
path: layerPath,
text: text,
visible: layer.visible,
styleInfo: styleInfo,
hasRichTextFormatting: hasRichTextFormatting
};
// Add to results
result.textLayers.push(layerInfo);
result.textLayerCount++;
} catch (e) {
// Log error but continue processing other layers
$.writeln("Error processing layer '" + layer.name + "': " + e.message);
}
}
// Function to get internal layer ID using ActionManager
function getInternalLayerID(layer) {
try {
// Select the layer
var idslct = charIDToTypeID("slct");
var desc = new ActionDescriptor();
var ref = new ActionReference();
ref.putIdentifier(charIDToTypeID("Lyr "), layer.id);
desc.putReference(charIDToTypeID("null"), ref);
executeAction(idslct, desc, DialogModes.NO);
// Get the layer's item index
var ref = new ActionReference();
ref.putProperty(charIDToTypeID("Prpr"), charIDToTypeID("ItmI"));
ref.putEnumerated(charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt"));
var itemIndexDesc = executeActionGet(ref);
var itemIndex = itemIndexDesc.getInteger(charIDToTypeID("ItmI"));
// Get the layer's ID
var ref = new ActionReference();
ref.putProperty(charIDToTypeID("Prpr"), stringIDToTypeID("layerID"));
ref.putEnumerated(charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt"));
var layerDesc = executeActionGet(ref);
// Check if the layer has a layerID property
if (layerDesc.hasKey(stringIDToTypeID("layerID"))) {
return layerDesc.getInteger(stringIDToTypeID("layerID"));
} else {
// Fallback to another method of ID extraction
return layer.id; // Use direct ID as fallback
}
} catch (e) {
$.writeln("Error getting internal ID for layer '" + layer.name + "': " + e.message);
return layer.id; // Use direct ID if we can't get the internal ID
}
}
// Function to get text alignment
function getTextAlignment(layer) {
try {
var align = layer.textItem.justification;
if (align === Justification.LEFT) {
return "left";
} else if (align === Justification.CENTER) {
return "center";
} else if (align === Justification.RIGHT) {
return "right";
} else {
return "left"; // Default
}
} catch (e) {
return "left"; // Default if error
}
}
// Function to extract rich text information
function extractRichTextInfo(layer) {
try {
var styles = [];
// Get a reference to the layer
var ref = new ActionReference();
ref.putIdentifier(charIDToTypeID("Lyr "), layer.id);
var desc = executeActionGet(ref);
// Check if textKey exists
if (desc.hasKey(stringIDToTypeID("textKey"))) {
var textKey = desc.getObjectValue(stringIDToTypeID("textKey"));
// Check if textStyleRange exists
if (textKey.hasKey(stringIDToTypeID("textStyleRange"))) {
var styleRanges = textKey.getList(stringIDToTypeID("textStyleRange"));
// Iterate through each style range
for (var i = 0; i < styleRanges.count; i++) {
var range = styleRanges.getObjectValue(i);
var from = range.getInteger(stringIDToTypeID("from"));
var to = range.getInteger(stringIDToTypeID("to"));
var styleRef = range.getObjectValue(stringIDToTypeID("textStyle"));
var rangeText = layer.textItem.contents.substring(from, to);
var fontName = layer.textItem.font;
var fontSize = layer.textItem.size.value;
var fontStyle = "Regular";
var fontColor = null;
// Extract font name
try {
if (styleRef.hasKey(stringIDToTypeID("fontName"))) {
fontName = styleRef.getString(stringIDToTypeID("fontName"));
}
} catch (e) {}
// Extract font style
try {
if (styleRef.hasKey(stringIDToTypeID("fontStyleName"))) {
fontStyle = styleRef.getString(stringIDToTypeID("fontStyleName"));
}
} catch (e) {}
// Extract font size
try {
if (styleRef.hasKey(stringIDToTypeID("size"))) {
fontSize = styleRef.getUnitDoubleValue(stringIDToTypeID("size"));
}
} catch (e) {}
// Extract color
try {
if (styleRef.hasKey(stringIDToTypeID("color"))) {
var colorObj = styleRef.getObjectValue(stringIDToTypeID("color"));
var colorValues = colorObj.getObjectValue(stringIDToTypeID("color"));
// Check color model
if (colorValues.hasKey(stringIDToTypeID("red"))) {
fontColor = [
Math.round(colorValues.getDouble(stringIDToTypeID("red"))),
Math.round(colorValues.getDouble(stringIDToTypeID("green"))),
Math.round(colorValues.getDouble(stringIDToTypeID("blue")))
];
}
}
} catch (e) {}
// Add style info to the array
styles.push({
start: from,
end: to,
text: rangeText,
font: fontName,
style: fontStyle,
size: fontSize,
color: fontColor
});
}
}
}
return { styles: styles };
} catch (e) {
$.writeln("Error extracting rich text info: " + e.message);
return { styles: [] };
}
}
// Start the layer traversal
traverseLayers(doc.layers);
return result;
}
// Main function to extract IDs and write to a file
function main() {
try {
var result = extractTextLayersWithInternalIDs();
// Convert result to formatted JSON
var jsonString = JSON.stringify(result, null, 2);
// Create a temp file to save the result
var tempFolder = Folder.temp;
var fileName = "ps_layer_ids_" + new Date().getTime() + ".json";
var outputFile = new File(tempFolder + "/" + fileName);
if (outputFile.open("w")) {
outputFile.write(jsonString);
outputFile.close();
alert("Layer ID information saved to:\\n" + outputFile.fsName);
return outputFile.fsName; // Return the path so the script can find it
} else {
alert("Error saving output file");
return "";
}
} catch (e) {
alert("Error: " + e.message);
return "";
}
}
// Run the script and return the path to the temp file
main();
"""
def run_photoshop_script(psd_path):
"""
Run the ExtendScript in Photoshop to extract layer IDs
Args:
psd_path: Path to the PSD file
Returns:
Path to the temporary JSON file with layer IDs
"""
print(f"Opening PSD file in Photoshop: {os.path.basename(psd_path)}")
# Create a temporary JSX file
jsx_content = create_jsx_script()
with tempfile.NamedTemporaryFile(suffix='.jsx', delete=False, mode='w') as jsx_file:
jsx_file.write(jsx_content)
jsx_path = jsx_file.name
try:
# Create AppleScript to run the JSX in Photoshop
applescript = f"""
tell application "Adobe Photoshop"
activate
open (POSIX file "{psd_path}")
do javascript (POSIX file "{jsx_path}")
close current document saving no
end tell
"""
# Save the AppleScript to a temporary file
with tempfile.NamedTemporaryFile(suffix='.scpt', delete=False, mode='w') as as_file:
as_file.write(applescript)
as_path = as_file.name
# Run the AppleScript
print("Running ExtendScript to extract internal layer IDs...")
result = subprocess.run(
["osascript", as_path],
capture_output=True,
text=True
)
if result.returncode != 0:
print(f"Error running AppleScript: {result.stderr}")
return None
# Parse the output to find the path to the temporary JSON file
output = result.stdout.strip()
# Extract the path from the alert message
match = re.search(r'Layer ID information saved to:\\n(.*)', output)
if match:
temp_json_path = match.group(1)
print(f"Extracted layer IDs saved to temp file: {temp_json_path}")
return temp_json_path
else:
print("Could not find the path to the temporary JSON file in the output")
print(f"Output was: {output}")
return None
except Exception as e:
print(f"Error running Photoshop script: {str(e)}")
return None
finally:
# Clean up the temporary JSX file
if os.path.exists(jsx_path):
os.remove(jsx_path)
# Clean up the temporary AppleScript file
if 'as_path' in locals() and os.path.exists(as_path):
os.remove(as_path)
def find_matching_json(psd_path):
"""
Find the corresponding JSON file for a PSD file
Args:
psd_path: Path to the PSD file
Returns:
Path to the matching JSON file, or None if not found
"""
psd_dir = os.path.dirname(psd_path)
psd_name = os.path.basename(psd_path).replace('.psd', '')
# Look for JSON files that match the PSD name
json_patterns = [
f"{psd_name}-textonly.json",
f"{psd_name}-textonly-updated.json"
]
for pattern in json_patterns:
json_path = os.path.join(psd_dir, pattern)
if os.path.exists(json_path):
return json_path
# If no direct match, search with glob
json_files = glob.glob(os.path.join(psd_dir, f"{psd_name}*-textonly*.json"))
if json_files:
return json_files[0]
return None
def find_matching_psd(json_path):
"""
Find the corresponding PSD file for a JSON file
Args:
json_path: Path to the JSON file
Returns:
Path to the matching PSD file, or None if not found
"""
json_dir = os.path.dirname(json_path)
json_name = os.path.basename(json_path)
# Strip common suffixes to get PSD name
psd_name = json_name.replace('-textonly-updated.json', '').replace('-textonly.json', '')
# Check if PSD exists directly
psd_path = os.path.join(json_dir, f"{psd_name}.psd")
if os.path.exists(psd_path):
return psd_path
# Read JSON file to get document name
try:
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
if 'documentName' in data:
psd_path = os.path.join(json_dir, data['documentName'])
if os.path.exists(psd_path):
return psd_path
if 'psdPath' in data:
psd_path = data['psdPath']
if os.path.exists(psd_path):
return psd_path
# Try to make path absolute
if psd_path.startswith('~'):
psd_path = os.path.expanduser(psd_path)
if os.path.exists(psd_path):
return psd_path
except Exception as e:
print(f"Error reading JSON file {json_path}: {str(e)}")
# If still not found, try globbing
psd_files = glob.glob(os.path.join(json_dir, f"{psd_name}*.psd"))
if psd_files:
return psd_files[0]
return None
def update_json_with_internal_ids(json_path, layer_ids_data):
"""
Update JSON file with internal layer IDs
Args:
json_path: Path to the JSON file to update
layer_ids_data: Data from ExtendScript with internal layer IDs
Returns:
Path to the updated JSON file
"""
print(f"Updating JSON file with internal layer IDs: {os.path.basename(json_path)}")
try:
# Load the existing JSON file
with open(json_path, 'r', encoding='utf-8') as f:
json_data = json.load(f)
# Create a dictionary of layers from the layer_ids_data for easy lookup
internal_layer_dict = {}
for layer in layer_ids_data.get('textLayers', []):
name = layer.get('name', '')
internal_id = layer.get('internalID')
direct_id = layer.get('id')
if name and internal_id:
internal_layer_dict[name] = {
'internalID': internal_id,
'directID': direct_id
}
# Update each layer in the JSON data
for layer in json_data.get('textLayers', []):
name = layer.get('name', '')
if name in internal_layer_dict:
# Add the internal ID
layer['internalID'] = internal_layer_dict[name]['internalID']
# Keep the original ID but mark it as directID if not already present
if 'directID' not in layer:
layer['directID'] = layer.get('id')
print(f"Layer '{name}': Added internal ID {layer['internalID']}")
# Save to a new file with suffix -api-ready.json
base_name = os.path.basename(json_path)
dir_name = os.path.dirname(json_path)
# Remove existing suffixes and add the new one
new_name = base_name.replace('-textonly-updated.json', '').replace('-textonly.json', '')
new_name = f"{new_name}-api-ready.json"
new_path = os.path.join(dir_name, new_name)
with open(new_path, 'w', encoding='utf-8') as f:
json.dump(json_data, f, indent=2)
print(f"Updated JSON file saved as: {new_path}")
return new_path
except Exception as e:
print(f"Error updating JSON file: {str(e)}")
return None
def process_file_pair(psd_path, json_path):
"""
Process a PSD and JSON file pair to extract and update layer IDs
Args:
psd_path: Path to the PSD file
json_path: Path to the JSON file
Returns:
Path to the updated JSON file
"""
print("\n" + "="*70)
print(f"Processing PSD: {os.path.basename(psd_path)}")
print(f"With JSON: {os.path.basename(json_path)}")
print("="*70 + "\n")
# Extract layer IDs from the PSD file
temp_json_path = run_photoshop_script(psd_path)
if not temp_json_path:
print("Failed to extract layer IDs from PSD file")
return None
# Load the layer ID data
try:
with open(temp_json_path, 'r', encoding='utf-8') as f:
layer_ids_data = json.load(f)
except Exception as e:
print(f"Error loading layer ID data: {str(e)}")
return None
# Update the JSON file with the internal IDs
updated_json_path = update_json_with_internal_ids(json_path, layer_ids_data)
# Clean up the temporary file
try:
if os.path.exists(temp_json_path):
os.remove(temp_json_path)
except:
pass
return updated_json_path
def find_all_json_files(directory):
"""
Find all JSON files with -textonly or -textonly-updated suffixes in a directory
Args:
directory: The directory to search
Returns:
List of JSON file paths
"""
json_files = []
patterns = ["*-textonly.json", "*-textonly-updated.json"]
for pattern in patterns:
json_files.extend(glob.glob(os.path.join(directory, pattern)))
return json_files
def process_directory(directory):
"""
Process all JSON files in a directory
Args:
directory: The directory to process
Returns:
List of updated JSON file paths
"""
print(f"Scanning directory for JSON files: {directory}")
json_files = find_all_json_files(directory)
if not json_files:
print("No JSON files found in the directory")
return []
print(f"Found {len(json_files)} JSON files to process")
updated_files = []
for json_path in json_files:
# Find the corresponding PSD file
psd_path = find_matching_psd(json_path)
if psd_path:
# Process the file pair
updated_json = process_file_pair(psd_path, json_path)
if updated_json:
updated_files.append(updated_json)
else:
print(f"Could not find matching PSD file for {os.path.basename(json_path)}")
return updated_files
def main():
"""Main function"""
parser = argparse.ArgumentParser(
description="Extract internal layer IDs and update JSON files for Adobe API",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Process a specific PSD and JSON file:
python extract_and_update_json.py --psd-path /path/to/file.psd --json-path /path/to/file-textonly.json
# Process all JSON files in a directory:
python extract_and_update_json.py --directory /path/to/files
# Process files in the current directory:
python extract_and_update_json.py
"""
)
parser.add_argument('--psd-path', help='Path to the PSD file')
parser.add_argument('--json-path', help='Path to the JSON file')
parser.add_argument('--directory', help='Directory containing JSON files to process')
args = parser.parse_args()
# Check arguments and determine what to process
if args.psd_path and args.json_path:
# Process a specific PSD and JSON file
if not os.path.exists(args.psd_path):
print(f"Error: PSD file not found: {args.psd_path}")
return
if not os.path.exists(args.json_path):
print(f"Error: JSON file not found: {args.json_path}")
return
updated_json = process_file_pair(args.psd_path, args.json_path)
if updated_json:
print(f"\nSuccessfully updated JSON file with internal layer IDs: {updated_json}")
print("Use this file for API requests to ensure correct layer identification.")
elif args.directory:
# Process all JSON files in a directory
if not os.path.isdir(args.directory):
print(f"Error: Directory not found: {args.directory}")
return
updated_files = process_directory(args.directory)
if updated_files:
print(f"\nSuccessfully updated {len(updated_files)} JSON files with internal layer IDs:")
for file_path in updated_files:
print(f" - {os.path.basename(file_path)}")
print("\nUse these files for API requests to ensure correct layer identification.")
else:
print("\nNo JSON files were updated. Check the directory path and file names.")
elif args.psd_path:
# Process a specific PSD file but find the matching JSON
if not os.path.exists(args.psd_path):
print(f"Error: PSD file not found: {args.psd_path}")
return
json_path = find_matching_json(args.psd_path)
if json_path:
updated_json = process_file_pair(args.psd_path, json_path)
if updated_json:
print(f"\nSuccessfully updated JSON file with internal layer IDs: {updated_json}")
print("Use this file for API requests to ensure correct layer identification.")
else:
print(f"Could not find matching JSON file for {os.path.basename(args.psd_path)}")
elif args.json_path:
# Process a specific JSON file but find the matching PSD
if not os.path.exists(args.json_path):
print(f"Error: JSON file not found: {args.json_path}")
return
psd_path = find_matching_psd(args.json_path)
if psd_path:
updated_json = process_file_pair(psd_path, args.json_path)
if updated_json:
print(f"\nSuccessfully updated JSON file with internal layer IDs: {updated_json}")
print("Use this file for API requests to ensure correct layer identification.")
else:
print(f"Could not find matching PSD file for {os.path.basename(args.json_path)}")
else:
# Process the current directory
updated_files = process_directory(os.getcwd())
if updated_files:
print(f"\nSuccessfully updated {len(updated_files)} JSON files with internal layer IDs:")
for file_path in updated_files:
print(f" - {os.path.basename(file_path)}")
print("\nUse these files for API requests to ensure correct layer identification.")
else:
print("\nNo JSON files were updated. Check the directory path and file names.")
if __name__ == "__main__":
main()