Skip to content

Instantly share code, notes, and snippets.

@doubleedesign
Last active November 23, 2022 15:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save doubleedesign/e535083b2986cca873203304d9dd7e7a to your computer and use it in GitHub Desktop.
Save doubleedesign/e535083b2986cca873203304d9dd7e7a to your computer and use it in GitHub Desktop.
InDesign Image Catalog for multiple folders at once (and multiple folders per page). Created for school yearbook projects (one folder per class, two classes per page) but could be adapted for other use cases where you want to lay out contact sheets of multiple folders automatically.
/**
* Custom Image Catalog script that runs for all subfolders in a selected folder.
* Lays out each folder of images in the specified number of rows and columns, 2 folders per page, shows an alert if there's more images than allowed for,
* labels each group with the folder name, creates paragraph styles for the captions and group headings, and saves the file.
*
* Based on the built-in Image Catalog script but modified and simplified (e.g. hard-coding the settings) for my use case.
* Could be modified to suit different numbers of folders per page, different image quantities etc by changing the settings at the top
* and making tweaks to other code as needed.
*
* Could also be extended to show one dialog for settings prior to the loop,
* so all settings would apply to every folder rather than it prompting for each one.
*/
main();
function main() {
var settings = {
foldersPerPage: 2,
rows: 3,
columns: 9,
removeEmptyFrames: true,
fitProportional: true,
centerContent: true,
fitFrameToContent: true,
horizontalOffset: 1.4, // 4pt
verticalOffset: 1.4, // 4pt
labels: true,
labelType: 0, // file name
labelHeight: 8.5, // 24pt in mm
labelOffset: 0.7, // 2pt in mm
labelStyle: 'Labels',
headingStyle: 'Group headings',
layerName: 'Layer 1',
pageMargin: 12 // mm
}
// Prompt for top-level folder choices
var folder = Folder.selectDialog("Select the folder containing the subfolders of images", "");
var saveLocation = Folder.selectDialog("Select the folder to save the files to", "");
// Get subfolders
var subfolders = getSubFolders(folder);
// Create the document
var document = app.documents.add();
var docPreferences = document.documentPreferences;
docPreferences.pagesPerDocument = Math.ceil(subfolders.length / foldersPerPage);
docPreferences.facingPages = false;
document.viewPreferences.horizontalMeasurementUnits = MeasurementUnits.millimeters;
document.viewPreferences.verticalMeasurementUnits = MeasurementUnits.millimeters;
docPreferences.pageWidth = 210;
docPreferences.pageHeight = 297;
// Counter for page numbers
var pageNo = 0;
// Loop through the subfolders of files
for(var i = 0; i < subfolders.length; i+=2) {
// Get (and transform) the folder name so the label can be added
// decodeURIComponent replaces characters like 20% where spaces should be
// replace(folderPath, '') removes the folder path and just leaves the folder name
// The rest is specific to the folders I had at the time of writing,
// which were in the format Year [class name] - 2021, when all I want to use is the class name
var folderPath = decodeURIComponent(folder.toString());
var files1Label = decodeURIComponent(subfolders[i].toString()).replace(folderPath, '');
files1Label = files1Label.replace('/', '')
.replace('-', '')
.replace('Year', '')
.replace('2021', '')
.replace(/\s/g, '');
var files2Label = decodeURIComponent(subfolders[i+1].toString()).replace(folderPath, '');
files2Label = files2Label.replace('/', '')
.replace('-', '')
.replace('Year', '')
.replace('2021', '')
.replace(/\s/g, '');
// Get the files
var files1 = subfolders[i].getFiles();
var files2 = subfolders[i+1].getFiles();
// Alert if there's more files than allowed for
if(files1.length > (settings.rows * settings.columns)) {
alert("Too many files in " + subfolders[i]);
}
if(files2.length > (settings.rows * settings.columns)) {
alert("Too many files in " + subfolders[i+1]);
}
// If there's files, make the catalog page with the hard-coded settings
// after making sure the images are sorted alphabetically
if(files1 || files2) {
files1.sort();
files2.sort();
makeImageCatalog([files1, files2], [files1Label, files2Label], settings, document, docPreferences, pageNo);
}
else {
$.writeln('Nope');
}
// Increment the counter so the next pair goes on the next page
pageNo++;
}
// Save the file
var newFile = new File(saveLocation + "/ImageCatalogue.indd");
document.save(newFile);
// Ok, now we're done
alert('Done!');
}
function getSubFolders(folder) {
var myStuff = [];
var myFileList = folder.getFiles();
for (var i = 0; i < myFileList.length; i++) {
if (myFileList[i].toString().indexOf('.jpg') !== -1) {
// is an image file
} else {
myStuff.push(myFileList[i]);
}
}
return myStuff;
}
function makeImageCatalog(filegroups, fileGroupLabels, settings, document, docPreferences, pageNo) {
var file, counter, X1, Y1, X2, Y2, rectangle, labelLayer, labelParagraphStyle, headingParagraphStyle;
var page = document.pages.item(pageNo);
var leftMargin = settings.pageMargin;
var topMargin = settings.pageMargin;
var rightMargin = settings.pageMargin;
var bottomMargin = settings.pageMargin;
var liveWidth = (docPreferences.pageWidth - (leftMargin + rightMargin)) + settings.horizontalOffset;
var liveHeight = docPreferences.pageHeight - (topMargin + bottomMargin);
var columnWidth = liveWidth / settings.columns;
var frameWidth = columnWidth - settings.horizontalOffset;
var rowHeight = ((liveHeight / settings.rows) / 2) - 6; // divide by 2 because we're doing 2 groups per page
var frameHeight = rowHeight - settings.verticalOffset;
// Move the view to the current page so we can watch where the script is up to
app.activeDocument.layoutWindows[0].activePage = app.activeDocument.pages[pageNo];
// If settings.labels is true, then add the label style and layer if they do not already exist.
if (settings.labels == true) {
try {
labelLayer = document.layers.item(settings.layerName);
labelLayer.name;
}
catch (error) {
labelLayer = document.layers.add({name: settings.layerName});
}
// If the paragraph styles do not exist, create them
try {
labelParagraphStyle = document.paragraphStyles.item(settings.labelStyle);
labelParagraphStyle.name;
}
catch (error) {
document.paragraphStyles.add({name: settings.labelStyle});
}
try {
headingParagraphStyle = document.paragraphStyles.item(settings.headingStyle);
headingParagraphStyle.name;
}
catch (error) {
document.paragraphStyles.add({name: settings.headingStyle});
}
}
// IMPORTANT: This loop expects 2 filegroups for a 2-group-per-page layout,
// and would need to be modified for a different use case
for(var i = 0; i < 2; i++) {
var numberOfFrames = filegroups[i].length;
// For the first group, add to the top margin to allow space for the group name
if(i == 0) {
topMargin = topMargin + 8;
}
// For the second group, add to the top margin so it appears below the first group and with some space between
if(i == 1) {
topMargin = topMargin + (rowHeight * settings.rows) + 24;
}
// Add the group labels
if(settings.labels == true) {
var textFrame = page.textFrames.add();
textFrame.geometricBounds = [topMargin - 8, leftMargin, topMargin * 2, leftMargin * 4];
textFrame.contents = fileGroupLabels[i];
}
textFrame.parentStory.texts.item(0).appliedParagraphStyle = headingParagraphStyle;
// Construct the frames in reverse order to save us time later (when we place the graphics).
for (var rowCounter = settings.rows; rowCounter >= 1; rowCounter--) {
Y1 = topMargin + (rowHeight * (rowCounter - 1));
Y2 = Y1 + frameHeight;
for (var columnCounter = settings.columns; columnCounter >= 1; columnCounter--) {
X1 = leftMargin + (columnWidth * (columnCounter - 1));
X2 = X1 + frameWidth;
rectangle = page.rectangles.add(document.layers.item(-1), undefined, undefined, {
geometricBounds: [Y1, X1, Y2, X2],
strokeWeight: 0,
strokeColor: document.swatches.item("None")
});
}
}
// Because we constructed the frames in reverse order, rectangle 1 is the first rectangle on page 1,
// so we can simply iterate through the rectangles, placing a file in each one in turn. myFiles = myFolder.Files;
for (counter = 0; counter < numberOfFrames; counter++) {
file = filegroups[i][counter];
rectangle = page.rectangles.item(counter);
rectangle.place(File(file));
rectangle.label = file.fsName.toString();
if (settings.fitProportional) {
rectangle.fit(FitOptions.proportionally);
}
if (settings.centerContent) {
rectangle.fit(FitOptions.centerContent);
}
if (settings.fitFrameToContent) {
rectangle.fit(FitOptions.frameToContent);
}
if (settings.labels == true) {
addLabel(rectangle, settings.labelType, settings.labelHeight, settings.labelOffset, settings.labelStyle, settings.layerName);
}
}
if (settings.removeEmptyFrames == 1) {
for (var myCounter = document.rectangles.length - 1; myCounter >= 0; myCounter--) {
if (document.rectangles.item(myCounter).contentType == ContentType.unassigned) {
document.rectangles.item(myCounter).remove();
} else {
//As soon as you encounter a rectangle with content, exit the loop.
break;
}
}
}
}
}
function addLabel(frame, labelType, labelHeight, labelOffset, labelStyleName, layerName) {
var document = app.documents.item(0);
var label;
var labelStyle = document.paragraphStyles.item(labelStyleName);
var labelLayer = document.layers.item(layerName);
var link = frame.graphics.item(0).itemLink;
//Label type defines the text that goes in the label.
switch (labelType) {
//File name
case 0:
label = (link.name).replace('.jpg', '');
break;
//File path
case 1:
label = link.filePath;
break;
//XMP description
case 2:
try {
label = link.linkXmp.description;
if (label.replace(/^\s*$/gi, "") == "") {
throw myError;
}
} catch (myError) {
label = "No description available.";
}
break;
//XMP author
case 3:
try {
label = link.linkXmp.author
if (label.replace(/^\s*$/gi, "") == "") {
throw myError;
}
} catch (myError) {
label = "No author available.";
}
break;
}
var X1 = frame.geometricBounds[1];
var Y1 = frame.geometricBounds[2] + labelOffset;
var X2 = frame.geometricBounds[3];
var Y2 = Y1 + labelHeight;
var textFrame = frame.parent.textFrames.add(labelLayer, undefined, undefined, {
geometricBounds: [Y1, X1, Y2, X2],
contents: label
});
textFrame.textFramePreferences.firstBaselineOffset = FirstBaseline.leadingOffset;
textFrame.parentStory.texts.item(0).appliedParagraphStyle = labelStyle;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment