#!/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()