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

500 lines
No EOL
22 KiB
JavaScript

/**
* 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();