Links Relink Subfolders

Script for Adobe InDesign

Search a folder and all subfolders below it for missing links, and update all.

  • Process the active document
  • Process open documents or a folder of documents
  • Update graphic links buried in subfolders
  • Specify folder to search for graphic links
  • Alerts user when multiple files the same name

To rename links, see Links GREP Rename.
For files renamed outside of InDesign, see Links GREP Relink.

Download
Links Relink Subfolders
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 Video

NOTE: after video production, features have been added in response to user feedback: added option to process active document, open documents, or a folder of documents. Option to update missing links or all links. See instructions below for details.

How to use the script

The interface has two sections: Process and Search for links in folder. Set the desired options and click the OK button to begin. A progress bar is displayed as documents are examined and placed graphics are relinked.

Because subfolders could allow files of the same name to exist, only in different folders, the script checks the file list for duplicates. For any links that have duplicate files the same name, the link is not updated, and the condition is reported in a log file. Examine the log file to determine which files are the correct link and relink each manually.

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: Search for links in folder

Folder — click to select the folder containing subfolders of files to relink.

Ignore — enter folder names to ignore or leave blank to include all subfolders. Separate multiple folder names with a comma.

Relink missing links — only links reported as missing in the Links panel are searched for, and if found, updated.

Relink all links — all links are updated including links that are not missing (most likely files in the Links folder below the document location). If the file is found in the folder to search, the placed graphic is linked to the file found in the search folder instead of the file in the Links folder.

Once all graphics are linked, if desired use the InDesign Package feature (File menu) to gather the previously missing links into a single folder.

Source code

(download button below)

 " + entries.shift());
                }
            } else {
                log.add("----> " + entries);
            }
        },
        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

    // Variation with second static text and progress bar.
    progress = new Window("palette", "Progress", undefined, {
        "closeButton": false
    });
    progress.t1 = progress.add("statictext");
    progress.t1.preferredSize.width = 450;
    progress.b1 = progress.add("progressbar");
    progress.b1.preferredSize.width = 450;
    progress.t2 = progress.add("statictext");
    progress.t2.preferredSize.width = 450;
    progress.b2 = progress.add("progressbar");
    progress.b2.preferredSize.width = 450;
    progress.display1 = function (message1) {
        message1 && (this.t1.text = message1);
        this.show();
        this.update();
    };
    progress.display2 = function (message2) {
        message2 && (this.t2.text = message2);
        this.show();
        this.update();
    };
    progress.increment1 = function () {
        this.b1.value++;
    };
    progress.increment2 = function () {
        this.b2.value++;
    };
    progress.set1 = function (steps) {
        this.b1.value = 0;
        this.b1.minvalue = 0;
        this.b1.maxvalue = steps;
    };
    progress.set2 = function (steps) {
        this.b2.value = 0;
        this.b2.minvalue = 0;
        this.b2.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");
    btnFolderInput = grpFolderInput.add("button", undefined, "Folder...");
    txtFolderInput = grpFolderInput.add("statictext", undefined, "", {
        truncate: "middle"
    });
    txtFolderInput.preferredSize.width = 360;

    // Panel 'Search for links in folder'
    p = w.add("panel", undefined, "Search for links in folder");
    p.alignChildren = "left";
    p.margins = [24, 24, 24, 18];
    g = p.add("group");
    btnFolderSearch = g.add("button", undefined, "Folder...");
    txtFolderSearch = g.add("statictext", undefined, "", {
        truncate: "middle"
    });
    txtFolderSearch.preferredSize.width = 360;
    g = p.add("group");
    g.margins.top = 9;
    g.add("statictext", undefined, "Ignore:");
    inpIgnore = g.add("edittext", undefined, "");
    inpIgnore.preferredSize.width = 370;
    g = p.add("group");
    g.margins.top = 9;
    rbMissingLinks = g.add("radiobutton", undefined, "Relink missing links");
    g.add("radiobutton", undefined, "Relink all links");

    // 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 DEFAULTS

    rbProcessActiveDoc.enabled = false;
    rbProcessOpenDocs.enabled = false;
    rbProcessFolder.value = true;
    if (app.documents.length > 0) {
        rbProcessActiveDoc.enabled = true;
        rbProcessActiveDoc.value = true;
        rbProcessFolder.value = false;
        try {
            folderSearch = new Folder(app.activeDocument.fullName.path + "/Links");
            txtFolderSearch.text = Folder.decode(folderSearch.fullName);
        } catch (_) {
            alert("Save documents before launching script.");
            return;
        }
        if (app.documents.length > 1) {
            rbProcessOpenDocs.enabled = true;
        }
    }
    rbMissingLinks.value = true;
    configureUi();

    // UI ELEMENT EVENT HANDLERS

    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);
        }
    };
    btnFolderSearch.onClick = function () {
        var f = Folder.selectDialog("Select folder to search for links", txtFolderSearch.text);
        if (f) {
            txtFolderSearch.text = Folder.decode(f.fullName);
        }
    };
    btnOk.onClick = function () {
        if (rbProcessFolder.value) {
            folderInput = new Folder(txtFolderInput.text);
            if (!(folderInput && folderInput.exists)) {
                alert("Select folder to process", " ", false);
                return;
            }
        }
        folderSearch = new Folder(txtFolderSearch.text);
        if (!(folderSearch && folderSearch.exists)) {
            alert("Select folder to search for links", " ", false);
            return;
        }
        w.close(1);
    };
    btnCancel.onClick = function () {
        w.close(0);
    };

    // DISPLAY THE DIALOG

    if (w.show() == 1) {
        doneMessage = "";
        try {
            if (rbProcessActiveDoc) {
                app.doScript(process, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, title);
            } else {
                process();
            }
            doneMessage = doneMessage || count + " links relinked";
        } catch (e) {
            error = error || e;
            doneMessage = "An error has occurred.\nLine " + error.line + ": " + error.message;
        }
        progress.close();
        try {
            if (rbProcessFolder.value) {
                log.path = folderInput;
            } else {
                log.path = app.activeDocument.fullName.path;
            }
            log.write();
        } catch (e) {
            alert("Error writing log:\n" + e.message, title, true);
        }
        if (log.file) {
            if (
                confirm(doneMessage +
                    "\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;
            cbSubfolders.value = false;
        } else if (rbProcessOpenDocs.value) {
            // Process open documents.
            rbProcessActiveDoc.value = false;
            rbProcessFolder.value = false;
            grpFolderInput.enabled = false;
            cbSubfolders.enabled = false;
            cbSubfolders.value = false;
        } else if (rbProcessFolder.value) {
            // Process folder.
            rbProcessActiveDoc.value = false;
            rbProcessOpenDocs.value = false;
            grpFolderInput.enabled = true;
            cbSubfolders.enabled = true;
        }
    }

    function getFiles(folder, subfolders, extensions, ignore) {
        // folder = folder object, not folder name.
        // subfolders = true to include subfolders.
        // extensions = string, extensions to include.
        // ignore = folder names to ignore.
        // Combine multiple extensions with RegExp OR i.e. jpg|psd|tif
        // extensions case-insensitive.
        // extensions undefined = any.
        // Ignores hidden files and folders.
        var f;
        var files;
        var i;
        var pattern;
        var results = [];
        if (extensions) {
            pattern = new RegExp("\." + extensions + "$", "i");
        } else {
            // Any extension.
            pattern = new RegExp(".*");
        }
        files = folder.getFiles();
        for (i = 0; i < files.length; i++) {
            f = files[i];
            if (!f.hidden) {
                if (f instanceof Folder && subfolders) {
                    if (ignore && ignore.indexOf(f.name) > -1) {
                        // Skip if folder in list to ignore.
                        continue;
                    }
                    // Recursive (function calls itself).
                    results = results.concat(getFiles(f, subfolders, extensions));
                } else if (f instanceof File && pattern.test(f.name)) {
                    results.push(f);
                }
            }
        }
        return results;
    }

    function process() {
        var i;
        var ignore;
        var ii;
        app.scriptPreferences.userInteractionLevel = UserInteractionLevels.NEVER_INTERACT;
        progress.display1("Initializing...");
        try {
            ignore = inpIgnore.text.split(",");
            files = getFiles(folderSearch, true, null, ignore);
            // Make array of file names.
            fileNames = [];
            for (i = 0; i < files.length; i++) {
                fileNames[i] = File.decode(files[i].name).toLowerCase();
            }
            // Make array of duplicates.
            fileNameDupes = [];
            for (i = 0; i < fileNames.length; i++) {
                fileNameDupes[i] = [];
                for (ii = 0; ii < fileNames.length; ii++) {
                    if (i != ii && fileNames[i] == fileNames[ii]) {
                        fileNameDupes[i].push(ii);
                    }
                }
            }
            count = 0;
            if (rbProcessActiveDoc.value) {
                progress.set1(1);
                processDoc(app.activeDocument);
            } else if (rbProcessOpenDocs.value) {
                processOpenDocs();
            } else if (rbProcessFolder.value) {
                processFolder();
            }
        } catch (e) {
            error = e;
            throw e;
        } finally {
            app.scriptPreferences.userInteractionLevel = UserInteractionLevels.NEVER_INTERACT;
        }
    }

    function processDoc(doc) {
        var i;
        var ii;
        var iii;
        var link;
        var name;
        progress.increment1();
        progress.display1(File.decode(doc.name));
        progress.set2(doc.links.length);
        loopLinks:
            for (i = 0; i < doc.links.length; i++) {
                link = doc.links[i];
                name = link.name.toLowerCase();
                progress.increment2();
                progress.display2(link.name);
                if (rbMissingLinks.value && link.status == LinkStatus.NORMAL) {
                    continue loopLinks;
                }
                for (ii = 0; ii < fileNames.length; ii++) {
                    if (fileNames[ii] == name) {
                        if (fileNameDupes[ii].length) {
                            // There are duplicates of this link.
                            // Log it, and don't relink.
                            log.add("Multiple files the same name. Manually update the link to the desired file.");
                            log.add("----> " + File.decode(files[ii].fullName));
                            for (iii = 0; iii < fileNameDupes[ii].length; iii++) {
                                log.add("----> " + File.decode(files[fileNameDupes[ii][iii]].fullName));
                            }
                            continue loopLinks;
                        }
                        // Found a match.
                        link.relink(files[ii]);
                        link.update();
                        count++;
                        continue loopLinks;
                    }
                }
            }
    }

    function processFolder() {
        var doc;
        var file;
        var files;
        var i;
        progress.display1("Reading folder...");
        files = getFiles(folderInput, cbSubfolders.value, "indd");
        if (!files.length) {
            doneMessage = "No files found in selected folder";
            return;
        }
        progress.set1(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.save(new File(doc.filePath + "/" + doc.name));
            doc.save();
            doc.close(SaveOptions.NO);
        }
    }

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

})();
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
Links Relink Subfolders

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.