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

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()