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>
370 lines
No EOL
15 KiB
Python
370 lines
No EOL
15 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Compare the original and processed Photoshop files to see what text layers were actually modified
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
def extract_text_layers_jsx():
|
|
"""Return the ExtendScript code for extracting text layers"""
|
|
return """
|
|
// Function to extract text layers from an open document
|
|
function extractTextLayers() {
|
|
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 functions
|
|
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) {
|
|
var layerPath = path ? path + "/" + layer.name : layer.name;
|
|
|
|
// Extract text content
|
|
var text = layer.textItem.contents;
|
|
|
|
// Extract style information
|
|
var styleInfo = {
|
|
font: layer.textItem.font,
|
|
size: layer.textItem.size.value,
|
|
color: null, // Will capture if defined
|
|
alignment: "left", // Default
|
|
styles: []
|
|
};
|
|
|
|
// Try to extract color if defined
|
|
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 getting it
|
|
}
|
|
|
|
// Check alignment if defined
|
|
try {
|
|
var align = layer.textItem.justification;
|
|
if (align === Justification.LEFT) {
|
|
styleInfo.alignment = "left";
|
|
} else if (align === Justification.CENTER) {
|
|
styleInfo.alignment = "center";
|
|
} else if (align === Justification.RIGHT) {
|
|
styleInfo.alignment = "right";
|
|
}
|
|
} catch (e) {
|
|
// Alignment not defined or error getting it
|
|
}
|
|
|
|
// Check if the text layer has rich text formatting
|
|
var hasRichTextFormatting = false;
|
|
try {
|
|
// 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 = text.substring(from, to);
|
|
var fontName = "Unknown";
|
|
var fontSize = styleInfo.size;
|
|
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 this style range
|
|
styleInfo.styles.push({
|
|
start: from,
|
|
end: to,
|
|
text: rangeText,
|
|
font: fontName,
|
|
style: fontStyle,
|
|
size: fontSize,
|
|
color: fontColor
|
|
});
|
|
|
|
// If we have multiple style ranges, it's rich text
|
|
if (i > 0) {
|
|
hasRichTextFormatting = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Unable to extract rich text info, continue with basic text
|
|
}
|
|
|
|
// Create the layer object
|
|
var layerInfo = {
|
|
id: layer.id,
|
|
name: layer.name,
|
|
path: layerPath,
|
|
text: text,
|
|
visible: layer.visible,
|
|
styleInfo: styleInfo,
|
|
hasRichTextFormatting: hasRichTextFormatting
|
|
};
|
|
|
|
// Add to the result
|
|
result.textLayers.push(layerInfo);
|
|
result.textLayerCount++;
|
|
}
|
|
|
|
// Start the traversal
|
|
traverseLayers(doc.layers);
|
|
|
|
// Return the result as a JSON string
|
|
return result;
|
|
}
|
|
|
|
// Execute the function and return the result
|
|
var result = extractTextLayers();
|
|
JSON.stringify(result);
|
|
"""
|
|
|
|
def compare_text_layers(original_psd, processed_psd):
|
|
"""Compare text layers between original and processed PSD files"""
|
|
import subprocess
|
|
from pprint import pprint
|
|
|
|
# Directory where the script is located
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# Create a temporary JSX file with our code
|
|
temp_jsx_path = os.path.join(script_dir, "temp_extract_layers.jsx")
|
|
|
|
try:
|
|
# Write the JSX code to the temp file
|
|
with open(temp_jsx_path, "w") as f:
|
|
f.write(extract_text_layers_jsx())
|
|
|
|
print(f"\nExtracting text layers from original PSD: {original_psd}")
|
|
|
|
if os.path.exists(original_psd):
|
|
original_info = extract_text_using_photoshop(original_psd, temp_jsx_path)
|
|
|
|
print(f"\nExtracting text layers from processed PSD: {processed_psd}")
|
|
|
|
if os.path.exists(processed_psd):
|
|
processed_info = extract_text_using_photoshop(processed_psd, temp_jsx_path)
|
|
|
|
# Compare the text layers
|
|
compare_results = compare_text_layer_data(original_info, processed_info)
|
|
|
|
# Print the comparison results
|
|
print("\nText Layer Comparison Results:")
|
|
print("=" * 50)
|
|
|
|
if len(compare_results) == 0:
|
|
print("NO DIFFERENCES FOUND - Text layers are identical!")
|
|
else:
|
|
for result in compare_results:
|
|
print(f"Layer: {result['name']} (ID: {result['id']})")
|
|
print(f" Original: \"{result['original_text']}\"")
|
|
print(f" Processed: \"{result['processed_text']}\"")
|
|
print("-" * 50)
|
|
|
|
return compare_results
|
|
else:
|
|
print(f"Error: Processed PSD file not found: {processed_psd}")
|
|
else:
|
|
print(f"Error: Original PSD file not found: {original_psd}")
|
|
|
|
except Exception as e:
|
|
print(f"Error comparing text layers: {str(e)}")
|
|
finally:
|
|
# Clean up the temporary JSX file
|
|
if os.path.exists(temp_jsx_path):
|
|
os.remove(temp_jsx_path)
|
|
|
|
return []
|
|
|
|
def extract_text_using_photoshop(psd_path, jsx_path):
|
|
"""Use macOS AppleScript to run the JSX script in Photoshop"""
|
|
# The AppleScript template to run the JSX script
|
|
applescript = f"""
|
|
tell application "Adobe Photoshop"
|
|
activate
|
|
open POSIX file "{psd_path}"
|
|
do javascript file "{jsx_path}"
|
|
close current document without saving
|
|
end tell
|
|
"""
|
|
|
|
# Create a temporary AppleScript file
|
|
temp_as_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp_extract.scpt")
|
|
|
|
try:
|
|
# Write the AppleScript to the temp file
|
|
with open(temp_as_path, "w") as f:
|
|
f.write(applescript)
|
|
|
|
# Execute the AppleScript using osascript
|
|
import subprocess
|
|
result = subprocess.run(
|
|
["osascript", temp_as_path],
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
print(f"Error running AppleScript: {result.stderr}")
|
|
return {}
|
|
|
|
# Parse the JSON result
|
|
try:
|
|
return json.loads(result.stdout)
|
|
except json.JSONDecodeError:
|
|
print(f"Error parsing JSON result: {result.stdout[:100]}...")
|
|
return {}
|
|
|
|
except Exception as e:
|
|
print(f"Error extracting text using Photoshop: {str(e)}")
|
|
return {}
|
|
finally:
|
|
# Clean up the temporary AppleScript file
|
|
if os.path.exists(temp_as_path):
|
|
os.remove(temp_as_path)
|
|
|
|
def compare_text_layer_data(original_data, processed_data):
|
|
"""Compare text layer data between original and processed files"""
|
|
results = []
|
|
|
|
# Create dictionaries of layers by ID for easier lookup
|
|
original_layers = {layer["id"]: layer for layer in original_data.get("textLayers", [])}
|
|
processed_layers = {layer["id"]: layer for layer in processed_data.get("textLayers", [])}
|
|
|
|
# First check original layers against processed
|
|
for layer_id, original_layer in original_layers.items():
|
|
if layer_id in processed_layers:
|
|
processed_layer = processed_layers[layer_id]
|
|
|
|
# Check if the text content is different
|
|
if original_layer["text"] != processed_layer["text"]:
|
|
results.append({
|
|
"id": layer_id,
|
|
"name": original_layer["name"],
|
|
"original_text": original_layer["text"],
|
|
"processed_text": processed_layer["text"],
|
|
"status": "modified"
|
|
})
|
|
else:
|
|
# Layer exists in original but not in processed
|
|
results.append({
|
|
"id": layer_id,
|
|
"name": original_layer["name"],
|
|
"original_text": original_layer["text"],
|
|
"processed_text": "LAYER MISSING",
|
|
"status": "missing_in_processed"
|
|
})
|
|
|
|
# Check for new layers in processed not in original
|
|
for layer_id, processed_layer in processed_layers.items():
|
|
if layer_id not in original_layers:
|
|
results.append({
|
|
"id": layer_id,
|
|
"name": processed_layer["name"],
|
|
"original_text": "LAYER MISSING",
|
|
"processed_text": processed_layer["text"],
|
|
"status": "new_in_processed"
|
|
})
|
|
|
|
return results
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Compare text layers between original and processed PSD files")
|
|
parser.add_argument("original_psd", help="Path to the original PSD file")
|
|
parser.add_argument("processed_psd", help="Path to the processed PSD file")
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Run the comparison
|
|
compare_text_layers(args.original_psd, args.processed_psd)
|
|
|
|
if __name__ == "__main__":
|
|
main() |