/** * Photoshop Script to Update Text Layers * * This script reads a JSON file containing text layer information * and updates the text content of matching layers in the open PSD. * * Usage: * 1. Open the PSD file in Photoshop * 2. Run this script: File > Scripts > Browse... > updateTextLayers.jsx * 3. Select the JSON file with text layer data * 4. The script will update matching text layers */ // Enable double clicking from the Finder/Explorer #target photoshop // Function to read text file function readTextFile(fileObj) { fileObj.open('r'); var content = fileObj.read(); fileObj.close(); return content; } // Parse JSON function (safe for ExtendScript) function parseJson(text) { try { // First try the native JSON parser if available if (typeof JSON !== 'undefined' && JSON.parse) { return JSON.parse(text); } } catch (e) { $.writeln("Native JSON parse failed: " + e); } // Skip safety checks and just try eval directly try { // Direct eval as fallback var data = eval('(' + text + ')'); return data; } catch (e) { $.writeln("JSON eval failed: " + e); // Ultimate fallback: try to manually extract the layer data try { $.writeln("Attempting emergency JSON parsing..."); // Try to extract the textLayers array with a basic regex var layersMatch = text.match(/"textLayers"\s*:\s*\[([\s\S]*?)\]\s*\}/); if (layersMatch && layersMatch[1]) { var layersText = layersMatch[1]; // Parse the text layers var result = { textLayers: [] }; // Find all layer objects var layerRegex = /\{\s*"id"[\s\S]*?styleInfo[\s\S]*?\}\s*\}/g; var layerMatch; while ((layerMatch = layerRegex.exec(layersText)) !== null) { var layerText = layerMatch[0]; // Extract the key fields var nameMatch = layerText.match(/"name"\s*:\s*"([^"]*)"/); var textMatch = layerText.match(/"text"\s*:\s*"([^"]*)"/); var updatedTextMatch = layerText.match(/"updatedText"\s*:\s*"([^"]*)"/); if (nameMatch && textMatch && updatedTextMatch) { result.textLayers.push({ name: nameMatch[1].replace(/\\n/g, "\n"), text: textMatch[1].replace(/\\n/g, "\n"), updatedText: updatedTextMatch[1].replace(/\\n/g, "\n"), exists: false // Will be checked later }); } } if (result.textLayers.length > 0) { $.writeln("Successfully extracted " + result.textLayers.length + " layers with emergency parser"); return result; } } throw new Error("Emergency parsing failed"); } catch (emergencyError) { $.writeln("Emergency parsing failed: " + emergencyError); throw new Error("Failed to parse JSON: " + e.message); } } } // Function to find a layer by name function findLayerByName(doc, layerName) { $.writeln("Looking for layer: " + layerName); // Helper function to search in layer sets function searchLayers(layers, indent) { indent = indent || ""; for (var i = 0; i < layers.length; i++) { var layer = layers[i]; $.writeln(indent + "- Checking: " + layer.name + " (Type: " + layer.typename + ")"); // Check if this is the layer we're looking for if (layer.name === layerName) { $.writeln(indent + " ✓ FOUND MATCH!"); return layer; } // If it's a group, search inside if (layer.typename === "LayerSet") { $.writeln(indent + " > Searching inside group: " + layer.name); var found = searchInGroup(layer, indent + " "); if (found) return found; } } return null; } // Helper function to search in a layer group function searchInGroup(group, indent) { indent = indent || ""; for (var i = 0; i < group.layers.length; i++) { var layer = group.layers[i]; $.writeln(indent + "- Checking: " + layer.name + " (Type: " + layer.typename + ")"); // Check if this is the layer we're looking for if (layer.name === layerName) { $.writeln(indent + " ✓ FOUND MATCH!"); return layer; } // If it's a group, search inside if (layer.typename === "LayerSet") { $.writeln(indent + " > Searching inside group: " + layer.name); var found = searchInGroup(layer, indent + " "); if (found) return found; } } return null; } // Start search from the root layers var result = searchLayers(doc.layers); if (!result) { $.writeln("❌ Layer not found: " + layerName); } return result; } // Function to update text in a text layer function updateTextLayer(layer, newText, styleData) { if (layer.kind === LayerKind.TEXT) { $.writeln("Updating text layer: " + layer.name); $.writeln("Original layer text: " + layer.textItem.contents); $.writeln("New text from JSON: " + newText); try { // Process line breaks - handle various escape sequences var processedText = newText; // Replace encoded newlines with actual newlines processedText = processedText.replace(/\\n/g, "\n"); processedText = processedText.replace(/\\r/g, "\r"); // Check if we have rich text formatting data for this layer var hasRichFormatting = styleData && styleData.styleInfo && styleData.styleInfo.styles && styleData.styleInfo.styles.length > 1 && styleData.hasRichTextFormatting; if (hasRichFormatting) { $.writeln("Layer has rich text formatting - attempting to preserve styles with the new text"); try { // First update the basic text content $.writeln("Setting basic text to: " + processedText); layer.textItem.contents = processedText; // Make this the active layer to work with it app.activeDocument.activeLayer = layer; // For rich text, we need to use action descriptors to apply styling $.writeln("Rich text update is experimental and may not work perfectly"); $.writeln("Original style ranges count: " + styleData.styleInfo.styles.length); // Now we'll attempt to recreate the style formatting on the new text // This is a complex operation and may not work perfectly in all cases // Only apply this if we have the advanced formatting data if (styleData.styleInfo.styles.length > 0) { $.writeln("Attempting to apply rich text formatting to " + styleData.styleInfo.styles.length + " style ranges"); // Get the current layer ID - needed for targeting our actions var ref = new ActionReference(); ref.putEnumerated(charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt")); var layerDesc = executeActionGet(ref); // For each style range in our data, we'll attempt to apply it to the corresponding text for (var i = 0; i < styleData.styleInfo.styles.length; i++) { var style = styleData.styleInfo.styles[i]; // Get the corresponding text range in the new text // For simplicity, we'll use the same character positions // This is imperfect but should work for direct translations var rangeStart = style.start; var rangeEnd = style.end; // Safety check - make sure our range is within the text bounds if (rangeEnd > processedText.length) { rangeEnd = processedText.length; } // Only apply if we have a valid range if (rangeStart < rangeEnd && rangeStart >= 0 && rangeEnd <= processedText.length) { $.writeln("Applying style to range [" + rangeStart + "-" + rangeEnd + "]: " + style.font + " " + style.style + " " + style.size + "pt"); // Select the text range var rangeDesc = new ActionDescriptor(); var fromDesc = new ActionDescriptor(); fromDesc.putInteger(stringIDToTypeID("from"), rangeStart); fromDesc.putInteger(stringIDToTypeID("to"), rangeEnd); rangeDesc.putObject(stringIDToTypeID("range"), stringIDToTypeID("textRange"), fromDesc); // Create the font style descriptor var styleDesc = new ActionDescriptor(); // Set font family and style if available if (style.font && style.font !== "Unknown") { styleDesc.putString(stringIDToTypeID("fontName"), style.font); } if (style.style && style.style !== "Regular") { styleDesc.putString(stringIDToTypeID("fontStyleName"), style.style); } // Set font size if (style.size) { styleDesc.putUnitDouble(stringIDToTypeID("size"), charIDToTypeID("#Pnt"), style.size); } // Set color if available if (style.color && style.color.length > 0) { // Color handling is complex and would need to match the document's color mode // This is a simplified approach var colorDesc = new ActionDescriptor(); var rgb = new ActionDescriptor(); // Assuming RGB values in 0-255 range if (style.color.length >= 3) { rgb.putDouble(stringIDToTypeID("red"), style.color[0]); rgb.putDouble(stringIDToTypeID("green"), style.color[1]); rgb.putDouble(stringIDToTypeID("blue"), style.color[2]); colorDesc.putObject(stringIDToTypeID("color"), stringIDToTypeID("RGBColor"), rgb); styleDesc.putObject(stringIDToTypeID("color"), stringIDToTypeID("color"), colorDesc); } } // Apply the styling to the range rangeDesc.putObject(stringIDToTypeID("textStyle"), stringIDToTypeID("textStyle"), styleDesc); executeAction(stringIDToTypeID("setd"), rangeDesc, DialogModes.NO); } else { $.writeln("Skipping invalid range [" + rangeStart + "-" + rangeEnd + "]"); } } } return true; } catch (richTextError) { $.writeln("ERROR applying rich text formatting: " + richTextError); $.writeln("Falling back to basic text update"); // If rich text formatting fails, fall back to basic update layer.textItem.contents = processedText; return true; } } else { // Basic text update $.writeln("Setting text to: " + processedText); layer.textItem.contents = processedText; return true; } } catch (e) { $.writeln("ERROR updating text: " + e); return false; } } $.writeln("Not a text layer: " + layer.name + " (kind: " + layer.kind + ")"); return false; } // Function to list all available text layers in the document function listAvailableTextLayers(doc) { var allLayers = []; function collectLayers(layers, path) { path = path || ""; for (var i = 0; i < layers.length; i++) { var layer = layers[i]; var layerPath = path ? path + "/" + layer.name : layer.name; // Add this layer to our collection if it's a text layer if (layer.kind === LayerKind.TEXT) { allLayers.push({ name: layer.name, path: layerPath, text: layer.textItem.contents }); } // If it's a group, collect layers from inside if (layer.typename === "LayerSet") { collectLayers(layer.layers, layerPath); } } } // Start collecting from the root collectLayers(doc.layers); return allLayers; } // Main function function main() { try { // Check if a document is open if (!documents.length) { alert("Please open a PSD file before running this script."); return; } // Get the active document var doc = app.activeDocument; // Prompt user to select the JSON file var jsonFile = File.openDialog("Select the JSON file with text layer data", "*.json"); if (!jsonFile) { return; // User cancelled } // Read and parse the JSON file var jsonContent = readTextFile(jsonFile); // Clean up common JSON errors jsonContent = jsonContent // Remove "px" or other units from numeric values .replace(/(\d+)\s*px/g, '$1') // Fix unescaped backslashes .replace(/([^\\])\\([^\\"])/g, '$1\\\\$2') // Remove trailing commas in arrays and objects .replace(/,\s*([}\]])/g, '$1') // Fix newline characters .replace(/\r\n/g, '\\n') .replace(/\r/g, '\\n'); try { var textData = parseJson(jsonContent); if (!textData.textLayers || !textData.textLayers.length) { alert("No text layers found in the JSON file."); return; } // First, get all available text layers in the document $.writeln("\n=== SCANNING DOCUMENT FOR TEXT LAYERS ==="); var availableLayers = listAvailableTextLayers(doc); $.writeln("Found " + availableLayers.length + " text layers in document"); for (var i = 0; i < availableLayers.length; i++) { $.writeln((i+1) + ". " + availableLayers[i].name + ": \"" + availableLayers[i].text.substring(0, 30) + (availableLayers[i].text.length > 30 ? "..." : "") + "\""); } // Map layer names to their information for quick lookup var availableLayerMap = {}; for (var i = 0; i < availableLayers.length; i++) { availableLayerMap[availableLayers[i].name] = availableLayers[i]; } // Start undo group for the entire operation app.activeDocument.suspendHistory("Update Text Layers", "updateLayers()"); function updateLayers() { // Counter for successfully updated layers var updatedCount = 0; var failedCount = 0; var skippedCount = 0; var failedLayers = []; // First, check which layers from JSON exist in the document $.writeln("\n=== MATCHING JSON LAYERS WITH DOCUMENT ==="); for (var i = 0; i < textData.textLayers.length; i++) { var layerName = textData.textLayers[i].name; if (availableLayerMap[layerName]) { $.writeln("✓ Found match for: " + layerName); // Add a flag to indicate this layer exists in the document textData.textLayers[i].exists = true; } else { $.writeln("❌ No match found for: " + layerName); // Mark as not existing textData.textLayers[i].exists = false; } } // Process each text layer in the JSON file for (var i = 0; i < textData.textLayers.length; i++) { var layerData = textData.textLayers[i]; // Debug: Print layer data to console $.writeln("\n=== PROCESSING LAYER: " + layerData.name + " ==="); // Skip layers that don't exist in the document if (!layerData.exists) { $.writeln("⚠️ Skipping - layer doesn't exist in document"); skippedCount++; continue; } $.writeln("Original text: \"" + layerData.text + "\""); $.writeln("Updated text: \"" + layerData.updatedText + "\""); // Skip if no updatedText if (!layerData.updatedText) { $.writeln("⚠️ Skipping - no updated text"); skippedCount++; continue; } // Simple comparison to see if text has changed at all var hasExactSameText = layerData.text === layerData.updatedText; $.writeln("Exact text match? " + hasExactSameText); if (hasExactSameText) { $.writeln("⚠️ Skipping - completely identical text"); skippedCount++; continue; } // Find the layer in the document using our direct search $.writeln("Looking for layer: " + layerData.name); var layer = findLayerByName(doc, layerData.name); if (layer) { // Check if the layer has rich text formatting information var hasRichTextInfo = layerData.hasRichTextFormatting || (layerData.styleInfo && layerData.styleInfo.styles && layerData.styleInfo.styles.length > 1); if (hasRichTextInfo) { $.writeln("Layer has rich text formatting information"); } // Update the text, passing the entire layer data for styling var success = updateTextLayer(layer, layerData.updatedText, layerData); if (success) { updatedCount++; } else { failedCount++; failedLayers.push(layerData.name + " (update failed)"); } } else { failedCount++; failedLayers.push(layerData.name + " (not found)"); } } // Show results var resultMessage = "Text Layer Update Complete\n\n"; resultMessage += "Successfully updated: " + updatedCount + " layers\n"; resultMessage += "Skipped (no change): " + skippedCount + " layers\n"; resultMessage += "Failed to update: " + failedCount + " layers\n"; if (failedCount > 0) { resultMessage += "\nFailed layers:\n" + failedLayers.join("\n"); } alert(resultMessage); } } catch (e) { alert("Error parsing JSON file: " + e.message + "\n\nPlease check that the file is valid JSON."); } } catch (err) { alert("Error: " + err.message); } } // Run the script main();