Layers Text To CSV

Script for Adobe Photoshop

Export the contents of all text layers to Comma Separated Values (CSV).

  • Process active, open, or a folder of documents
  • Generate single CSV file of many images
  • Generate separate CSV file for each image
  • Adapt open source to customize or create other scripts
Download
Layers Text To CSV
Help me keep making new scripts by supporting my work. Click the PayPal button to contribute any amount you choose. Thank you. William Campbell

How to use the script

The interface has two sections: Process and Output. Set the desired options and click the OK button to begin. A progress bar is displayed as images are processed.

Section 1: Process

Active Document — processes the document that is currently open and the top-most window if multiple documents are open.

Open documents — processes every open document.

Folder — processes every document found in the selected folder.

Include subfolders — if enabled, documents in all subfolders are also processed.

Section 2: Output

Folder — select a folder to which CSV files are saved. When files are output, any existing files of the same name in the output folder are replaced without alert.

When processing open documents or a folder, the user may choose to combine the results or keep them separate. These options have no effect when processing the active document.

All documents one CSV file — the results of each document are compiled to a single output file.

Each document separate CSV file — generates a separate CSV file for each document processed.

Source code

(download button below)

/*

Layers Text To CSV
Copyright 2024 William Campbell
All Rights Reserved
https://www.marspremedia.com/contact

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

*/

(function () {

    var title = "Layers Text To CSV";

    if (!/photoshop/i.test(app.name)) {
        alert("Script for Photoshop", title, false);
        return;
    }

    app.displayDialogs = DialogModes.ERROR;

    // Script variables.
    var count;
    var data;
    var doneMessage;
    var error;
    var folderInput;
    var folderOutput;
    var log;
    var progress;

    // Reusable UI variables.
    var g; // group
    var p; // panel
    var w; // window

    // Permanent UI variables.
    var btnCancel;
    var btnFolderInput;
    var btnFolderOutput;
    var btnOk;
    var cbSubfolders;
    var grpCsvSingle;
    var grpFolderInput;
    var rbCsvSingle;
    var rbProcessActiveDoc;
    var rbProcessFolder;
    var rbProcessOpenDocs;
    var txtFolderInput;
    var txtFolderOutput;

    // LANGUAGE EXTENSIONS

    if (!Array.prototype.indexOf) {
        Array.prototype.indexOf = function (x) {
            for (var i = 0; i < this.length; i++) {
                if (this[i] == x) {
                    return i;
                }
            }
            return -1;
        };
    }

    if (!String.prototype.trim) {
        String.prototype.trim = function () {
            return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "");
        };
    }

    // SETUP

    // LOG

    log = {
        entries: [],
        file: null,
        // Default write log to user desktop.
        // Preferably set 'log.path' to a meaningful
        // location in later code once location exists.
        path: Folder.desktop,
        add: function (message) {
            if (message instanceof Array) {
                while (message.length) {
                    this.entries.push(message.shift());
                }
            } else {
                this.entries.push(message);
            }
        },
        addFile: function (fileName, entries) {
            this.add(File.decode(fileName));
            if (entries instanceof Array) {
                while (entries.length) {
                    log.add("----> " + entries.shift());
                }
            } else {
                log.add("----> " + entries);
            }
        },
        cancel: function () {
            if (log.entries.length) {
                log.add("Canceled");
            }
        },
        write: function () {
            var contents;
            var d;
            var fileName;
            var padZero = function (v) {
                return ("0" + v).slice(-2);
            };
            if (!this.entries.length) {
                // No log entries to report.
                this.file = null;
                return;
            }
            contents = this.entries.join("\r") + "\r";
            // Create file name.
            d = new Date();
            fileName =
                title +
                " Log " +
                d.getFullYear() +
                "-" +
                padZero(d.getMonth() + 1) +
                "-" +
                padZero(d.getDate()) +
                "-" +
                padZero(d.getHours()) +
                padZero(d.getMinutes()) +
                padZero(String(d.getSeconds()).substr(0, 2)) +
                ".txt";
            // Open and write log file.
            this.file = new File(this.path + "/" + fileName);
            this.file.encoding = "UTF-8";
            try {
                if (!this.file.open("w")) {
                    throw new Error("Failed to open log file.");
                }
                if (!this.file.write(contents)) {
                    throw new Error("Failed to write log file.");
                }
            } catch (e) {
                this.file = null;
                throw e;
            } finally {
                this.file.close();
            }
            // Log successfully written.
            // log.file == true (not null) indicates a log was written.
        }
    };

    // CREATE PROGRESS WINDOW

    progress = new Window("palette", "Progress");
    progress.t = progress.add("statictext");
    progress.t.preferredSize.width = 450;
    progress.b = progress.add("progressbar");
    progress.b.preferredSize.width = 450;
    progress.add("statictext", undefined, "Press ESC to cancel");
    progress.display = function (message) {
        message && (this.t.text = message);
        this.show();
        app.refresh();
    };
    progress.increment = function () {
        this.b.value++;
    };
    progress.set = function (steps) {
        this.b.value = 0;
        this.b.minvalue = 0;
        this.b.maxvalue = steps;
    };

    // CREATE USER INTERFACE

    w = new Window("dialog", title);
    w.alignChildren = "fill";

    // Panel 'Process'
    p = w.add("panel", undefined, "Process");
    p.alignChildren = "left";
    p.margins = [24, 24, 24, 18];
    g = p.add("group");
    rbProcessActiveDoc = g.add("radiobutton", undefined, "Active document");
    rbProcessOpenDocs = g.add("radiobutton", undefined, "Open documents");
    rbProcessFolder = g.add("radiobutton", undefined, "Folder");
    g = g.add("group");
    g.margins.left = 9;
    cbSubfolders = g.add("checkbox", undefined, "Include subfolders");
    grpFolderInput = p.add("group");
    grpFolderInput.margins.top = 9;
    btnFolderInput = grpFolderInput.add("button", undefined, "Folder...");
    txtFolderInput = grpFolderInput.add("statictext", undefined, "", {
        truncate: "middle"
    });
    txtFolderInput.preferredSize.width = 360;

    // Panel 'Output'
    p = w.add("panel", undefined, "Output");
    p.alignChildren = "left";
    p.margins = [24, 24, 24, 18];
    g = p.add("group");
    btnFolderOutput = g.add("button", undefined, "Folder...");
    txtFolderOutput = g.add("statictext", undefined, "", {
        truncate: "middle"
    });
    txtFolderOutput.preferredSize.width = 360;
    grpCsvSingle = p.add("group");
    grpCsvSingle.margins.top = 9;
    rbCsvSingle = grpCsvSingle.add("radiobutton", undefined, "All documents one CSV file");
    grpCsvSingle.add("radiobutton", undefined, "Each document separate CSV file");

    // Action Buttons
    g = w.add("group");
    g.alignment = "center";
    btnOk = g.add("button", undefined, "OK");
    btnCancel = g.add("button", undefined, "Cancel");

    // Panel Copyright
    p = w.add("panel");
    p.add("statictext", undefined, "Copyright 2024 William Campbell");

    // SET UI VALUES

    if (app.documents.length) {
        rbProcessActiveDoc.value = true;
    } else {
        rbProcessActiveDoc.enabled = false;
        rbProcessOpenDocs.enabled = false;
        rbProcessFolder.value = true;
    }
    txtFolderOutput.text = "";
    rbCsvSingle.value = true;
    configureUi();

    // UI ELEMENT EVENT HANDLERS

    // Panel 'Process'
    rbProcessActiveDoc.onClick = configureUi;
    rbProcessOpenDocs.onClick = configureUi;
    rbProcessFolder.onClick = configureUi;
    btnFolderInput.onClick = function () {
        var f = Folder.selectDialog("Select input folder", txtFolderInput.text);
        if (f) {
            txtFolderInput.text = Folder.decode(f.fullName);
        }
    };

    // Panel 'Output'
    btnFolderOutput.onClick = function () {
        var f = Folder.selectDialog("Select output folder", txtFolderOutput.text);
        if (f) {
            txtFolderOutput.text = Folder.decode(f.fullName);
        }
    };

    // Action Buttons
    btnOk.onClick = function () {
        if (rbProcessFolder.value) {
            folderInput = new Folder(txtFolderInput.text);
            if (!(folderInput && folderInput.exists)) {
                txtFolderInput.text = "";
                alert("Select input folder", " ", false);
                return;
            }
        }
        folderOutput = new Folder(txtFolderOutput.text);
        if (!(folderOutput && folderOutput.exists)) {
            txtFolderOutput.text = "";
            alert("Select output folder", " ", false);
            return;
        }
        w.close(1);
    };
    btnCancel.onClick = function () {
        w.close(0);
    };

    // DISPLAY THE DIALOG

    if (w.show() == 1) {
        doneMessage = "";
        try {
            process();
            if (rbProcessActiveDoc.value) {
                doneMessage = "Done";
            } else {
                doneMessage = doneMessage || count + " files processed";
            }
        } catch (e) {
            if (/User cancel/.test(e.message)) {
                doneMessage = "Canceled";
            } else {
                error = error || e;
                doneMessage = "An error has occurred.\nLine " + error.line + ": " + error.message;
            }
        }
        app.bringToFront();
        progress.close();
        if (rbProcessFolder.value) {
            // Only log when processing a folder.
            try {
                log.path = folderOutput;
                log.write();
            } catch (e) {
                alert("Error writing log:\n" + e.message, title, true);
            }
        }
        if (log.file) {
            if (
                confirm(doneMessage + //``` why prefix with doneMessage ???
                    // confirm(
                    "\nAlerts reported. See log for details:\n" +
                    File.decode(log.file.fullName) +
                    "\n\nOpen log?", false, title)
            ) {
                log.file.execute();
            }
        } else {
            doneMessage && alert(doneMessage, title, error);
        }
    }

    //====================================================================
    //               END PROGRAM EXECUTION, BEGIN FUNCTIONS
    //====================================================================

    function configureUi() {
        if (rbProcessActiveDoc.value) {
            // Process active document.
            rbProcessOpenDocs.value = false;
            rbProcessFolder.value = false;
            grpFolderInput.enabled = false;
            cbSubfolders.enabled = false;
            grpCsvSingle.enabled = false;
        } else if (rbProcessOpenDocs.value) {
            // Process open documents.
            rbProcessActiveDoc.value = false;
            rbProcessFolder.value = false;
            grpFolderInput.enabled = false;
            cbSubfolders.enabled = false;
            grpCsvSingle.enabled = true;
        } else if (rbProcessFolder.value) {
            // Process folder.
            rbProcessActiveDoc.value = false;
            rbProcessOpenDocs.value = false;
            grpFolderInput.enabled = true;
            grpCsvSingle.enabled = true;
        }
    }

    function getFiles(folder, subfolders) {
        // folder = folder object, not folder name.
        // subfolders = true to include subfolders.
        // Ignores hidden files and folders.
        var f;
        var files;
        var i;
        var pattern;
        var results = [];
        pattern = new RegExp(
            "\.8PBS|AFX|AI|ARW|BLZ|BMP|CAL|CALS|CIN|CR2|CRW|CT|DCM|DCR|DCS|DCX|DDS|" +
            "DIB|DIC|DNG|DPX|EPS|EPSF|EXR|FFF|FIF|GIF|HDP|HDR|HEIC|HEIF|ICB|ICN|ICO|" +
            "ICON|IIQ|IMG|J2C|J2K|JIF|JIFF|JP2|JPC|JPE|JPEG|JPF|JPG|JPS|JPX|JXR|KDK|" +
            "KMZ|KODAK|MOS|MRW|NCR|NEF|ORF|PAT|PBM|PCT|PCX|PDD|PDF|PDP|PEF|PGM|PICT|" +
            "PMG|PNG|PPM|PS|PSB|PSD|PSDC|PSID|PVR|PXR|RAF|RAW|RLE|RSR|SCT|SRF|TGA|TIF|" +
            "TIFF|TRIF|U3D|VDA|VST|WBMP|WDP|WEBP|WMP|X3F$", "i");
        files = folder.getFiles();
        for (i = 0; i < files.length; i++) {
            f = files[i];
            if (!f.hidden) {
                if (f instanceof Folder && subfolders) {
                    // Recursive (function calls itself).
                    results = results.concat(getFiles(f, subfolders));
                } else if (f instanceof File && pattern.test(f.name)) {
                    results.push(f);
                }
            }
        }
        return results;
    }

    function getTextLayers() {
        var kind;
        var layers = [];
        var searchLayers = function (o) {
            for (var i = 0; i < o.layers.length; i++) {
                if (o.layers[i].typename == "LayerSet") {
                    layers.concat(searchLayers(o.layers[i]));
                } else {
                    kind = o.layers[i].kind;
                    if (kind == LayerKind.TEXT) {
                        layers.push(o.layers[i]);
                    }
                }
            }
        };
        searchLayers(app.activeDocument);
        return layers;
    }

    function process() {
        app.displayDialogs = DialogModes.NO;
        progress.display("Initializing...");
        try {
            data = [];
            count = 0;
            if (rbProcessActiveDoc.value) {
                progress.set(1);
                processDoc(app.activeDocument);
            } else if (rbProcessOpenDocs.value) {
                processOpenDocs();
                if (rbCsvSingle.value) {
                    writeCsv(data, title);
                }
            } else if (rbProcessFolder.value) {
                processFolder();
                if (rbCsvSingle.value) {
                    writeCsv(data, title);
                }
            }
        } finally {
            app.displayDialogs = DialogModes.ERROR;
        }
    }

    function processDoc(doc) {
        var baseName;
        var i;
        var layer;
        var layers;
        try {
            progress.increment();
            baseName = doc.name.replace(/\.[^\.]*$/, "");
            layers = getTextLayers();
            for (i = 0; i < layers.length; i++) {
                layer = layers[i];
                progress.display(baseName);
                data.push([baseName, layer.name, layer.textItem.contents]);
            }
            if (!rbCsvSingle.value) {
                writeCsv(data, baseName);
                data = [];
            }
            count++;
        } catch (e) {
            log.addFile(doc.fullName.fullName, "Line " + e.line + ": " + e.message);
        }
    }

    function processFolder() {
        var doc;
        var file;
        var files;
        var i;
        progress.display("Reading folder...");
        files = getFiles(folderInput, cbSubfolders.value);
        if (!files.length) {
            doneMessage = "No files found in selected folder";
            return;
        }
        progress.set(files.length);
        for (i = 0; i < files.length; i++) {
            file = files[i];
            try {
                doc = app.open(file);
            } catch (_) {
                log.addFile(file.fullName, "Cannot open the file.");
                continue;
            }
            processDoc(doc);
            doc.close(SaveOptions.DONOTSAVECHANGES);
        }
    }

    function processOpenDocs() {
        var i;
        progress.set(app.documents.length);
        for (i = 0; i < app.documents.length; i++) {
            processDoc(app.documents[i]);
        }
    }

    function writeCsv(data, baseName) {
        var contents;
        var fileOutput;
        var fullPath;
        var i;
        var ii;
        var nameOutput;
        var pathOutput;
        var row;
        var encodeCsv = function (v) {
            var s = String(v);
            // Escape quotation marks.
            s = s.replace(/"/g, "\"\"");
            if (s.indexOf(",") > -1 || s.indexOf("\"") > -1) {
                // Wrap in quotation marks.
                s = "\"" + s + "\"";
            }
            return s;
        };
        // Start data with header.
        contents = "document name,layer name,contents\r\n";
        // Add rows.
        for (i = 0; i < data.length; i++) {
            row = encodeCsv(data[i][0]);
            for (ii = 1; ii < data[i].length; ii++) {
                row += "," + encodeCsv(data[i][ii]);
            }
            contents += row + "\r\n";
        }
        pathOutput = folderOutput.fullName;
        nameOutput = baseName + ".csv";
        fullPath = pathOutput + "/" + nameOutput;
        fileOutput = new File(fullPath);
        fileOutput.encoding = "UTF-8";
        try {
            if (!fileOutput.open("w")) {
                throw new Error("Failed to open CSV file.");
            }
            if (!fileOutput.write(contents)) {
                throw new Error("Failed to write CSV file.");
            }
        } finally {
            fileOutput.close();
        }
    }

})();
Help me keep making new scripts by supporting my work. Click the PayPal button to contribute any amount you choose. Thank you. William Campbell
Download
Layers Text To CSV

For help installing scripts, see How to Install and Use Scripts in Adobe Creative Cloud Applications.

IMPORTANT: scripts are developed for the latest Adobe Creative Cloud applications. Many scripts work in CC 2018 and later, even some as far back as CS6, but may not perform as expected, or run at all, when used in versions prior to 2018. Photoshop features Select Subject and Preserve Details 2.0 definitely fail prior to CC 2018 (version 19) as the features do not exist in earlier versions. For best results use the latest versions of Adobe Creative Cloud applications.

IMPORTANT: by downloading any of the scripts on this page you agree that the software is provided without any warranty, express or implied. USE AT YOUR OWN RISK. Always make backups of important data.

IMPORTANT: fees paid for software products are the purchase of a non-exclusive license to use the software product and do not grant the purchaser any degree of ownership of the software code. Author of the intellectual property and copyright holder William Campbell retains 100% ownership of all code used in all software products regardless of the inspiration for the software product design or functionality.