Copy link to clipboard
Copied
Hey - So....
For every campaign i work on i have around 20 files, all with 40 or more links to jpegs/tiffs > 'filename_V1.jpg' for example.
I now have images back from the retouchers and the assets all have varying different version numbers (they are not all the same version) and LR at the end 'filename_V3_LR.jpg' - and i have to keep this naming convension so i cant just re name the finals the same and my originally placed assets then replace them in the links folder unfortunately,.
Could someone pleeeaseee help me with a script that re links but ignores the last few characters before the file name or searches for the highest number after the last letter V or something? or a strategy to work around this
I would VERY much appreciate it
Please please please and thank you thank you thank you!
Hi @Luke29383164kjl6, your situation intrigued me, as it seemed like a fairly common problem, and I wondered if I could make a script that would solve it, but also be a bit flexible to handle variations. Here's my attempt.
I've tried to put documentation into the script if you want to check it out, and I've pre-configured it to work the way you describe for your situation. Every time you run it, you choose a folder to search in. This way, if you receive another batch of updated versions later,
...Copy link to clipboard
Copied
Copy link to clipboard
Copied
Thanks for this.. Im not that good at scripting though so its a bit out of my range
If anyone can help me i am happy to pay a little for their time / help
Thanks
Copy link to clipboard
Copied
You are not writing a script; just running the script by double-clicking it. Here is how to install a script:
https://help.redokun.com/article/38-install-an-indesign-script
Copy link to clipboard
Copied
Apologies - i wasnt clear - the i ran the script just fine but it didnt actually re link to any of my files with the alt name... i got the done! message but it didnt actually change anything..
Would you be able to advice further if i message you directly?
Copy link to clipboard
Copied
Copy link to clipboard
Copied
Hi @Luke29383164kjl6, your situation intrigued me, as it seemed like a fairly common problem, and I wondered if I could make a script that would solve it, but also be a bit flexible to handle variations. Here's my attempt.
I've tried to put documentation into the script if you want to check it out, and I've pre-configured it to work the way you describe for your situation. Every time you run it, you choose a folder to search in. This way, if you receive another batch of updated versions later, just run it again and choose that new folder.
I think it will be useful. Let me know how it goes for you and please report bugs—there are bound to be some! Feel free to post bugs here or send me message directly.
- Mark
UPDATE: I've added a UI, with the option to save the settings in the document. This will make it easier to use I hope.
Here's a visualisation of the matching process, showing that both link name and file name must be matched:
Here's the script:
/**
* Re-link Indesign linked files by matching link names
* to files in a target folder.
*
* Example use case:
* We have to a folder of links, and some links have version
* number, eg. _V1. A third party updates the links and
* increments the version numbers, returning an updated
* folder of links. We want our indesign file to be linked
* to the updated files, but this won't happen automatically
* because they have different file names.
*
* We can achieve this with the following parameters:
* linkMatcher: /(^.*_V)\d/
* fileMatcher: '^$1\\d'
*
* This will match a linked file 'foo_V1.jpg' and, when
* pointed to the updated folder, replace with, for
* example, 'foo_V2.jpg'.
*
* Note, that you should be able to go back and forth,
* choosing either the old or new folder. Try it!
*
* See further details in the function documentation below.
*
* @file Relink Matching Links.js
* @author m1b
* @discussion https://community.adobe.com/t5/indesign-discussions/how-to-batch-relink-asset-to-same-file-name-but-with-different-version-number-different-ending/m-p/14358429
*/
function main() {
var settings = {
doc: app.activeDocument,
/*
* (A) linkMatcher - regex to match a LINK NAME
* In this example, we match any link whose name includes
* _V with a digit after it, and we capture the name and
* the _V for matching the file later in (B).
*/
linkMatcher: /^(.*_V)\d/i,
/*
* (B) fileMatcher - string to match a FILE NAME
* In this example, we match a filename starting with
* the contents of capture group 1 from (A) and a digit
* (backslash must be escaped in a string).
*/
fileMatcher: '^$1\\d',
/*
* whether to search for links in the current link's folder
* if this is false, will ask user to select folder
*/
lookInSameFolder: false,
/*
* whether to only update only matched links that are MISSING
* if this is false, will attempt to match *all* links
*/
onlyMissingLinks: false,
// whether to show results after running script
showResults: true,
// whether to show the UI
showUI: true,
};
if (settings.showUI) {
var result = ui(settings);
if (2 === result)
// user cancelled
return;
if (undefined != settings.cleanup) {
settings.cleanup();
delete settings.cleanup;
}
}
// do the relinking
relinkByMatchingLinkAndFile(settings);
};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "Relink Matching Links");
/**
* User interface for this script.
* Will update the `settings` object and,
* optionally, save settings in document.
* @param {Object} settings - the script settings.
* @returns {1|2}
*/
function ui(settings) {
var key = 'relinkMatchingLinks',
savedValues = settings.doc.extractLabel(key).split('\n'),
// controls
w = new Window("dialog { text:'Relink Matching Links' }"),
columns = w.add("group {orientation:'row', margins:[0,0,0,0] }"),
leftColumn = columns.add("group {orientation:'column', margins:[0,0,0,0] }"),
matchLinkGroup = leftColumn.add("group {orientation:'row', margins:[0,0,0,0] }"),
matchLinkLabel = matchLinkGroup.add('statictext', undefined, 'Match Link Name Grep:'),
matchLinkField = matchLinkGroup.add('edittext {preferredSize: [260,30]}'),
matchFileGroup = leftColumn.add("group {orientation:'row', margins:[0,0,0,0] }"),
matchFileLabel = matchFileGroup.add('statictext', undefined, 'Match File Name Grep:'),
matchFileField = matchFileGroup.add('edittext {preferredSize: [260,30]}'),
rightColumn = columns.add("group {orientation:'column', margins:[0,0,0,0] }"),
sideControls = rightColumn.add("group {orientation:'column', alignChildren: ['left','top'] alignment:['fill','top'] }"),
ignoreCaseCheckBox = sideControls.add("CheckBox { text: 'Ignore case' }"),
lookInSameFolderCheckBox = sideControls.add("CheckBox { text: 'Look in same folder as link' }"),
onlyMissingLinksCheckBox = sideControls.add("CheckBox { text: 'Only missing links' }"),
bottomUI = w.add("group {orientation:'row', alignment:['fill','top'], margins: [0,20,0,0] }"),
extraControls = bottomUI.add("group {orientation:'column', alignChildren: ['left','top'] alignment:['fill','top'] }"),
saveInDocCheckBox = extraControls.add("CheckBox { text: 'Save these settings in document' }"),
buttons = bottomUI.add("group {orientation:'row', alignment:['right','top'], alignChildren:'right' }"),
cancelButton = buttons.add('button', undefined, 'Cancel', { name: 'cancel' }),
relinkButton = buttons.add('button', undefined, 'Re-link', { name: 'ok' });
// event handling
relinkButton.onClick = relinkButtonClicked;
if (savedValues.length > 1) {
saveInDocCheckBox.value = true;
matchLinkField.text = regexSource(savedValues[0]);
matchFileField.text = savedValues[1];
ignoreCaseCheckBox.value = 1 === Number(savedValues[2]);
lookInSameFolderCheckBox.value = 1 === Number(savedValues[3]);
onlyMissingLinksCheckBox.value = 1 === Number(savedValues[4]);
}
else {
saveInDocCheckBox.value = false;
matchLinkField.text = settings.linkMatcher.source;
matchFileField.text = settings.fileMatcher;
ignoreCaseCheckBox.value = settings.linkMatcher.ignoreCase;
lookInSameFolderCheckBox.value = settings.lookInSameFolder;
onlyMissingLinksCheckBox.value = 1 === Number(savedValues[4]);
}
w.center();
return w.show();
function relinkButtonClicked() {
try {
var matchLinkRegex = new RegExp(matchLinkField.text, ignoreCaseCheckBox.value ? 'i' : '');
} catch (error) {
return alert('Could not make RegExp "' + matchLinkField.text + '".');
}
// update settings
settings.linkMatcher = matchLinkRegex;
settings.fileMatcher = matchFileField.text;
settings.lookInSameFolder = lookInSameFolderCheckBox.value;
settings.onlyMissingLinks = onlyMissingLinksCheckBox.value;
if (true == saveInDocCheckBox.value) {
settings.cleanup = function () {
// save values in document
settings.doc.insertLabel(key, [
String(settings.linkMatcher),
matchFileField.text,
ignoreCaseCheckBox.value ? 1 : 0,
lookInSameFolderCheckBox.value ? 1 : 0,
onlyMissingLinksCheckBox.value ? 1 : 0,
].join('\n'));
};
}
// finished with dialog
w.close(1);
};
};
/**
* Returns source string from a stringified RegExp.
* eg, '/^Z.*$/gi' -> '^Z.*$'
* @param {String/RegExp} obj - the object to parse.
* @returns {?String}
*/
function regexSource(obj) {
var parts = String(obj).match(/^\/(.*)\/([gimsuy]*)$/) || {};
if (parts[1])
return parts[1];
}
/**
* Relinks a documents links where `linkMatcher` (A)
* matches a link's name, and where a file is found
* in the target folder matching `fileMatcher` (B).
*
* Explanation of parameters:
*
* (A) linkMatcher (RegExp)
* Create a regex that will match the link names you
* are interested in. Include one or more capture groups
* if you wish to reference them in (B).
*
* (B) fileMatcher (String for regex source)
* Create a string that will, as a RegExp, match a file
* in the target folder. If the string includes any capture
* group references (eg. '$1') they will be replaced with
* the string matched using linkMatcher (A).
*
* @author m1b
* @version 2024-01-18
* @param {Object} options
* @param {Document} options.doc - an Indesign Document.
* @param {RegExp} options.linkMatcher - RegExp to match link name.
* @param {String} options.fileMatcher - RegExp source string to match file name.
* @param {Boolean} [options.lookInSameFolder] - whether to search in the current link's folder (default: false, will ask user for folder).
* @param {Boolean} [options.onlyMissingLinks] - whether to update only missing links (default: false, update all links).
* @param {Boolean} [options.showResults] - whether to display a count of the changes (default: true).
*/
function relinkByMatchingLinkAndFile(options) {
// options
options = options || {};
var doc = options.doc || app.activeDocument,
linkMatcher = options.linkMatcher,
fileMatcher = options.fileMatcher,
lookInSameFolder = true === options.lookInSameFolder,
onlyMissingLinks = true === options.onlyMissingLinks,
showResults = false !== options.showResults;
var links = doc.links,
matchedLinkCounter = 0,
matchedFileCounter = 0,
folder,
searchFileOptions,
file,
matchLink,
matchFile;
linksLoop:
for (var i = 0; i < links.length; i++) {
if (
LinkStatus.LINK_MISSING !== links[i].status
&& onlyMissingLinks
)
continue;
// choose a folder
if (
lookInSameFolder
&& folder !== File(links[i].filePath).parent
) {
// folder to search in
folder = File(links[i].filePath).parent;
if (!folder.exists)
folder = undefined;
}
matchLink = decodeURI(links[i].name).match(linkMatcher);
if (
null == matchLink
|| matchLink.length < 2
)
// this link doesn't match with the regex
continue linksLoop;
matchedLinkCounter++;
// this is to match any file according to `matchFileName` with capture groups expanded
matchFile = new RegExp(fileMatcher.replace(/\$(\d)+/g, expandFileMatcher));
// settings for getFilesOfFolder
searchFileOptions = {
folder: folder,
filterRegex: matchFile,
maxDepth: 1,
returnFirstMatch: true,
};
file = getFilesOfFolder(searchFileOptions);
if (undefined == folder)
folder = searchFileOptions.folder;
if (undefined == folder)
return;
if (
undefined == file
|| File(links[i].filePath).absoluteURI === file.absoluteURI
)
continue linksLoop;
links[i].relink(file);
matchedFileCounter++
}
if (showResults)
return alert('Relinked ' + matchedFileCounter + ' links.\n (' + matchedLinkCounter + ' matched links of ' + links.length + ' total)');
/**
* Return matched index of `matchLink`, for use
* with String.replace() method.
* @params arguments provided by String.replace.
* @returns {?String}
*/
function expandFileMatcher() {
if (arguments.length > 1)
return matchLink[Number(arguments[1])];
};
};
/**
* Collects all files inside a Folder, recusively searching in sub-folders.
* If no folder is supplied, will ask user. Search can be filtered using
* `filterRegex`, `fileFilter` or `folderFilter`, and also limited to `maxDepth`.
* @author m1b
* @version 2023-10-02
* @param {Object} options - parameters
* @param {Folder} [options.folder] - the folder to look in (default: ask).
* @param {RegExp} [options.filterRegex] - regex to match file/folder's name (default: no filter).
* @param {Function} [options.fileFilter] - function, given a File, must return true (default: no filter).
* @param {Function} [options.folderFilter] - function, given a Folder, must return true (default: no filter).
* @param {Boolean} [options.returnFirstMatch] - whether to return only the first found file (default: false).
* @param {Number} [options.maxDepth] - deepest folder level (recursion depth limit) (default: 99).
* @param {Boolean} [options.includeFiles] - whether to include files (default: true).
* @param {Boolean} [options.includeFolders] - whether to include folders (default: false).
* @param {Boolean} [options.includeHiddenItems] - whether to include hidden items (default: false).
* @param {Number} [options.depth] - the current depth private param.
* @returns {Array|File} - all the found files, or the first found file if `returnFirstMatch`.
*/
function getFilesOfFolder(options) {
// defaults
options = options || {};
var found = [],
folder = options.folder;
if ((options.depth || 0) === 0)
// once-off initialization
// to keep the object clean
if (!(options = initializeOptions()))
return [];
// get files from this level
var files = folder.getFiles(),
hiddenFile = /^[\.\~]/;
filesLoop:
for (var i = 0; i < files.length; i++) {
if (excludeFilter(files[i]))
// file/folder excluded!
continue filesLoop;
if (includeFilter(files[i]))
// file/folder matched!
found.push(files[i]);
if (
!(files[i] instanceof Folder)
|| options.depth >= options.maxDepth
)
// we only want folders
continue filesLoop;
// set up for the next depth
options.folder = files[i];
options.depth++;
// look inside the folder
found = found.concat(getFilesOfFolder(options));
}
// this level done
if (true == options.returnFirstMatch)
return found[0];
else
return found;
/**
* Returns true when the file/folder should be excluded.
* @param {File|Folder} file
* @returns {Boolean}
*/
function excludeFilter(file) {
return (
// is hidden
(
false == options.includeHiddenItems
&& hiddenFile.test(file.name)
)
// an ignored folder
|| (
file instanceof Folder
&& !(
undefined == options.folderFilter
|| options.folderFilter(file)
)
)
);
};
/**
* Returns true when the file/folder should be included.
* @param {File|Folder} file
* @returns {Boolean}
*/
function includeFilter(file) {
return (
(
// file passes filter
(
options.includeFiles
&& file instanceof File
)
// folder passes filter
|| (
options.includeFolders
&& file instanceof Folder
)
)
// match regex
&& (
undefined == options.filterRegex
|| options.filterRegex.test(decodeURI(file.name))
)
// pass filter
&& (
undefined == options.fileFilter
|| options.fileFilter(file)
)
);
};
/**
* Returns the initialised `options` object.
* @returns {Object}
*/
function initializeOptions() {
// make a new object, so we don't pollute the original
var newOptions = {
depth: 0,
filterRegex: options.filterRegex,
fileFilter: options.fileFilter,
folderFilter: options.folderFilter,
returnFirstMatch: options.returnFirstMatch,
maxDepth: options.maxDepth,
includeFiles: options.includeFiles !== false,
includeFolders: true == options.includeFolders,
includeHiddenItems: true == options.includeHiddenItems,
};
if (
undefined == options.maxDepth
|| !options.maxDepth instanceof Number
)
newOptions.maxDepth = 99;
if (
undefined != folder
&& 'String' == folder.constructor.name
&& File(folder).exists
)
folder = File(folder);
else if (
undefined != folder
&& 'String' == folder.constructor.name
)
folder = undefined;
if (undefined == folder)
// ask user for folder
folder = Folder.selectDialog("Select a folder:");
if (
null === folder
|| !(folder instanceof Folder)
)
// folder bad or user cancelled
return;
// update folder in the *original* object
options.folder = folder;
return newOptions;
};
};
Edit 2024-01-17: fixed bug triggered when cancelling folder select dialog.
Edit 2024-01-18: added better result alert to show how many links were matched as well as how many links were re-linked. Also adjusted example configuration because some of lukes files use a lowercase _v.
Edit 2024-01-18: added optional UI, tweaked some of the variable names to make more sense.
Copy link to clipboard
Copied
Duuuudeeee this looks amazing! and thank you soooooo much for taking it on!
I loaded and ran it but it didnt give me the option to select a folder to get the new links to and then it just popped up with 'done - relinked 0 of 16 images'
Which would make sense if it didnt have anywhere to get them from i guess...
Let me know if you need anything else from me - i will owe you a drink if this works!
Copy link to clipboard
Copied
Very strange that it didn't ask you for a folder. Just checking that you still have this line in the params area?
/*
* whether to ask user to choose the target folder; switch this
* to false if the new links are in the same folder as the old
*/
true,
If so, I honestly cannot understand why that didn't ask you. Can you try the following script on its own?
var folder = Folder.selectDialog();
if (folder) alert('success!');
If all goes well it will ask you to select a folder and, if you don't cancel, will alert "success". Let me know what happens.
This is same command is performed on line 371 of my script. Can you check that line 371 looks okay on your text file, in case something happened during the copy/paste.
- Mark
Copy link to clipboard
Copied
I also fixed a small bug in the code above. I don't think it will help your problem, but you should update your script with the new code. - Mark
Copy link to clipboard
Copied
V strange - the open folder only, worked but same situation for the re pasted full script - I have DMd you a test file if thats helpful
Thank youuu
Copy link to clipboard
Copied
Hi @Luke29383164kjl6, I've had a look at your demo files and discovered two things:
(1) it wasn't really a fault with the code, but just wasn't configured right for you. In your original post you had _V1 (capital V) and my example Regex matched capital V. I've changed it to be case insensitive so it'll match either _v or _V, and
(2) it *was* a fault with the code... that it didn't make it clear to the user what the problem was. I've had a go at fixing this by changing the result alert message to include the number of *matched* links. If this was present when you tested it the first time you would at least have known that not only did it not relink 16 links, but it *matched* zero links, and therefore the problem may have been the linkMatcher variable.
I'll update the code listing above, and you can try again.
- Mark
Copy link to clipboard
Copied
This is incredible! Thank you soooo much dude!
Find more inspiration, events, and resources on the new Adobe Community
Explore Now