Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Adobe Illustrator JSX plugin to create a contact sheet from a folder or vector icons.
/**
* Name that script.
*/
#script "Contact Sheet";
#target Illustrator
var originalInteractionLevel = userInteractionLevel;
userInteractionLevel = UserInteractionLevel.DONTDISPLAYALERTS;
/**
* @author Iconfinder.com - http://iconfinder.com
* @date 2016-09-13
*
* Installation:
*
* 1. Copy this file to Illustrator > Presets > Scripting
* 2. Restart Adobe Illustrator
* 3. Go to File > Scripts > Contact Sheet
* 4. Follow the prompts
*
* Usage:
*
* This script will create a contact sheet of vector objects from a folder structure
* that you specify. As of 13-09-2016 the script will only work with folder structures
* nested 1 level deep (Parent > Subfolders). This was done intentionally to allow
* for creating contacts sheets of categorized icons where the user wants to
* be able to specify the order of the categories.
*
* Inputs:
*
* Page Width: The width of the contact sheet in pixels
* Page Height: The height of the contact sheet in pixels
* Column Width: The width of the columns in pixels
* Row Height: The height of the rows in pixels
* Scale: The percentage (100 = 100%) to scale the objects being placed
*
* The resulting contact sheet will have margins that are calculated thus: subtracting
* Left & Right Margins = (Page Width - Column Width * Column Count) / 2
* Top & Bottom Margins = (Page Height - Row Height * Row Count) / 2
*
* Copyright:
*
* (c) copyright: Iconfinder.com - http://iconfinder.com
* copyright full text can be found in the accompanying file license.txt
*/
var LANG = {
CHOOSE_FOLDER: "Please choose your Folder of files to place...",
NO_SELECTION: "No selection",
LABEL_SETTINGS: "Contact Sheet Settings",
LABEL_PG_WIDTH: "Page Width:",
LABEL_PG_HEIGHT: "Page Height:",
LABEL_COL_COUNT: "Column Count:",
LABEL_ROW_COUNT: "Row Count:",
LABEL_SCALE: "Scale:",
LABEL_FILE_NAME: "File Name:",
LABEL_LOGGING: "Logging?",
BUTTON_CANCEL: "Cancel",
BUTTON_OK: "Ok",
DOES_NOT_EXIST: " does not exist",
LAYER_NOT_CREATED: "Could not create layer. "
}
var CONFIG = {
/**
* Whether or not to add the file name as text
* under the imported icons.
*/
ADD_LABELS: true,
/**
* Number of rows
*/
ROWS: 20,
/**
* Number of columns
*/
COLS: 10,
/**
* Top & bottom page margins
*/
VOFF: 64,
/**
* Left & Right page margins
*/
HOFF: 64,
/**
* Row height. This is set programmatically.
*/
ROW_WIDTH: 128,
/**
* Column Height. This is set programmatically.
*/
COL_WIDTH: 128,
/**
* @deprecated
*/
FRM_WIDTH: 128,
/**
* @deprecated
*/
FRM_HEIGHT: 128,
/**
* Artboard width
*
* 10 columns 128 px wide, with 64 px page margins
*/
PG_WIDTH: 1408,
/**
* Artboard height
*
* 20 rows 128 px tall, with 64 px page margins
*/
PG_HEIGHT: 2688,
/**
* Not yet fully-implemented. Will support multiple units
*/
PG_UNITS: "px",
/**
* @deprecated
*/
GUTTER: 0,
/**
* Enter scale in percentage 1-100
*/
SCALE: 100,
/**
* Illustrator version compatibility
*/
AIFORMAT: Compatibility.ILLUSTRATOR10,
/**
* If the icon is larger than the cell size, shrink it to the cell size
*/
SHRINK_TO_FIT: true,
/**
* Start folder for selection
*/
START_FOLDER: Folder.desktop,
/**
* The contact sheet file name
*/
FILENAME: "contact-sheet",
/**
* Enable logging?
*/
LOGGING: true,
/**
* Log file location
*/
LOG_FILE_PATH: Folder.desktop + "/ai-contactsheet-log.txt",
/**
* Verbose logging output?
*/
DEBUG: true,
/**
* @deprecated
*/
SKIP_COLS: 0,
/**
* Not fully-implemented
*/
STRIP: ["svg", "ai", "eps", "txt", "pdf"]
}
/**
* Displays the settings dialog
*
* Inputs:
* - skip columns
* - page width
* - page height
* - cell width
* - cell height
* - scale
* - logging enabled
*
* - number of cols = divide page width by cell width
* - number of rows = divide page height by cell height
* - side margins = (page width - (col count * col width))/2
* - top/bottom margins = (page height - (row count * row width))/2
*
* @return Settings object
*/
function doDisplayDialog() {
var dialog = new Window("dialog", LANG.LABEL_SETTINGS, [550, 350, 900, 700]);
var response = false;
try {
dialog.pageWidthLabel = dialog.add("statictext", [32, 30, 132, 60], LANG.LABEL_PG_WIDTH);
dialog.pageWidth = dialog.add("edittext", [150, 30, 200, 60], CONFIG.PG_WIDTH);
dialog.pageWidth.active = true;
dialog.pageHeightLabel = dialog.add("statictext", [32, 70, 132, 100], LANG.LABEL_PG_HEIGHT);
dialog.pageHeight = dialog.add("edittext", [150, 70, 200, 100], CONFIG.PG_HEIGHT);
dialog.pageHeight.active = true;
dialog.colsLabel = dialog.add("statictext", [32, 110, 132, 140], LANG.LABEL_COL_COUNT);
dialog.cols = dialog.add("edittext", [150, 110, 200, 140], CONFIG.COLS);
dialog.cols.active = true;
dialog.rowsLabel = dialog.add("statictext", [32, 150, 132, 180], LANG.LABEL_ROW_COUNT);
dialog.rows = dialog.add("edittext", [150, 150, 200, 180], CONFIG.ROWS);
dialog.rows.active = true;
dialog.scaleLabel = dialog.add("statictext", [32, 190, 132, 220], LANG.LABEL_SCALE);
dialog.scale = dialog.add("edittext", [150, 190, 200, 220], CONFIG.SCALE);
dialog.scale.active = true;
dialog.filenameLabel = dialog.add("statictext", [32, 230, 132, 260], LANG.LABEL_FILE_NAME);
dialog.filename = dialog.add("edittext", [150, 230, 320, 260], CONFIG.FILENAME);
dialog.filename.active = true;
dialog.logging = dialog.add('checkbox', [32, 270, 132, 340], LANG.LABEL_LOGGING);
dialog.logging.value = CONFIG.LOGGING;
dialog.cancelBtn = dialog.add("button", [80, 300, 170, 330], LANG.BUTTON_CANCEL, {
name: "cancel"
});
dialog.openBtn = dialog.add("button", [180, 300, 270, 330], LANG.BUTTON_OK, {
name: "ok"
});
dialog.cancelBtn.onClick = function() {
dialog.close();
response = false;
return false;
};
dialog.openBtn.onClick = function() {
CONFIG.PG_WIDTH = parseInt(dialog.pageWidth.text);
CONFIG.PG_HEIGHT = parseInt(dialog.pageHeight.text);
CONFIG.LOGGING = dialog.logging.value;
CONFIG.SCALE = parseInt(dialog.scale.text);
CONFIG.COLS = parseInt(dialog.cols.text);
CONFIG.ROWS = parseInt(dialog.rows.text);
CONFIG.COL_WIDTH = parseInt((CONFIG.PG_WIDTH - (CONFIG.HOFF * 2)) / CONFIG.COLS);
CONFIG.ROW_HEIGHT = parseInt((CONFIG.PG_HEIGHT - (CONFIG.VOFF * 2)) / CONFIG.ROWS);
CONFIG.FRM_WIDTH = CONFIG.COL_WIDTH;
CONFIG.FRM_HEIGHT = CONFIG.ROW_HEIGHT;
if (CONFIG.DEBUG) {
logger("CONFIG.PG_WIDTH: " + CONFIG.PG_WIDTH);
logger("CONFIG.PG_HEIGHT: " + CONFIG.PG_HEIGHT);
logger("CONFIG.FRM_WIDTH: " + CONFIG.FRM_WIDTH);
logger("CONFIG.FRM_HEIGHT: " + CONFIG.FRM_HEIGHT);
logger("CONFIG.COL_WIDTH: " + CONFIG.COL_WIDTH);
logger("CONFIG.ROW_HEIGHT: " + CONFIG.ROW_HEIGHT);
logger("CONFIG.SCALE: " + CONFIG.SCALE);
logger("CONFIG.ROWS: " + CONFIG.ROWS);
logger("CONFIG.COLS: " + CONFIG.COLS);
logger("CONFIG.VOFF: " + CONFIG.VOFF);
logger("CONFIG.HOFF: " + CONFIG.HOFF);
}
dialog.close();
response = true;
return true;
};
dialog.show();
} catch (ex) {
logger(ex);
alert(ex);
}
return response;
}
/**
* Utility function to strip the file extension from a user-supplied file name
* @param <string> filename
* @return <string> The new file name sans extension
*/
function stripFileExtension(filename) {
var bits = filename.split(".");
var bit = bits[bits.length - 1];
var found = false;
if (bits.length > 1 && bit) {
for (ext in CONFIG.STRIP) {
if (ext.toLowerCase() == bit.toLowerCase()) {
found = true;
}
}
}
if (found) bits = bits[bits.length - 1] = "";
return bits.join(".");
}
/**
* Main logic to create the contact sheet.
* @return void
*/
function doCreateContactSheet() {
var doc, fileList, i, srcFolder, svgFile,
svgFilePath, saveCompositeFile, allFiles,
theFolders, svgFileList, theLayer;
var saveCompositeFile = false;
srcFolder = Folder.selectDialog(LANG.CHOOSE_FOLDER, CONFIG.START_FOLDER);
if (srcFolder != null) {
allFiles = srcFolder.getFiles();
theFolders = [];
for (var x = 0; x < allFiles.length; x++) {
if (allFiles[x] instanceof Folder) {
theFolders.push(allFiles[x]);
}
}
svgFileList = [];
if (theFolders.length == 0) {
svgFileList = srcFolder.getFiles(/\.svg$/i);
} else {
for (var x = 0; x < theFolders.length; x++) {
// Gets just the SVG files...
fileList = theFolders[x].getFiles(/\.svg$/i);
for (var n = 0; n < fileList.length; n++) {
svgFileList.push(fileList[n]);
}
}
}
if (svgFileList.length > 0) {
if (!doDisplayDialog()) {
return;
}
if (CONFIG.FILENAME.replace(" ", "") == "") {
CONFIG.FILENAME = srcFolder.name.replace(" ", "-") + "-all";
}
// CONFIG.FILENAME = stripFileExtension(CONFIG.FILENAME);
app.coordinateSystem = CoordinateSystem.ARTBOARDCOORDINATESYSTEM;
doc = app.documents.add(
DocumentColorSpace.RGB,
CONFIG.PG_WIDTH,
CONFIG.PG_HEIGHT,
CONFIG.PG_COUNT = Math.ceil(svgFileList.length / (CONFIG.ROWS * CONFIG.COLS)),
DocumentArtboardLayout.GridByCol,
CONFIG.GUTTER,
Math.round(Math.sqrt(Math.ceil(svgFileList.length / (CONFIG.ROWS * CONFIG.COLS))))
);
for (var i = 0; i < svgFileList.length; i++) {
var board;
var bounds;
var x1 = y1 = x2 = y2 = 0;
var myRowHeight = CONFIG.ROW_HEIGHT + CONFIG.GUTTER;
var myColumnWidth = CONFIG.COL_WIDTH + CONFIG.GUTTER
var myFrameWidth = CONFIG.FRM_WIDTH
var myFrameHeight = CONFIG.FRM_HEIGHT
for (var pageCounter = CONFIG.PG_COUNT - 1; pageCounter >= 0; pageCounter--) {
doc.artboards.setActiveArtboardIndex(pageCounter);
board = doc.artboards[pageCounter];
bounds = board.artboardRect;
boardWidth = Math.round(bounds[2] - bounds[0]);
// loop through rows
var rowCount = Math.ceil((svgFileList.length / CONFIG.COLS));
rowCount = CONFIG.ROWS > rowCount ? rowCount : CONFIG.ROWS;
// If we are skipping a column, chances are we need to
// add a new row for the overflow of the shift. Even if there
// is not a new row needed, there are no consequences for
// adding one, so just in case.
if (CONFIG.SKIP_COLS > 0) {
rowCount++;
}
for (var rowCounter = 1; rowCounter <= rowCount; rowCounter++) {
myY1 = bounds[1] + CONFIG.VOFF + (myRowHeight * (rowCounter - 1));
myY2 = myY1 + CONFIG.FRM_HEIGHT;
// loop through columns
var colCount = CONFIG.COLS;
if (rowCounter > 1) {
var remaining = Math.ceil(svgFileList.length - i);
if (remaining < colCount) {
colCount = remaining;
}
}
for (var columnCounter = 1; columnCounter <= colCount; columnCounter++) {
try {
// A hack to allow merging multiple contact sheets
// Shift the starting row so it aligns nicely with
// the icons already in the master contact sheet.
if (CONFIG.SKIP_COLS > 0 && rowCounter == 1 && columnCounter <= CONFIG.SKIP_COLS) {
continue;
}
var f = new File(svgFileList[i]);
if (f.exists) {
try {
if (i == 0) {
theLayer = doc.layers[0];
} else {
theLayer = doc.layers.add();
}
theLayer.name = f.name;
} catch (ex) {
logger(LANG.LAYER_NOT_CREATED + ex);
}
svgFile = doc.groupItems.createFromFile(f);
var liveWidth = (CONFIG.COLS * (CONFIG.FRM_WIDTH + CONFIG.GUTTER)) - CONFIG.GUTTER;
var hoff = Math.ceil((CONFIG.PG_WIDTH - liveWidth) / 2);
myX1 = bounds[0] + hoff + (myColumnWidth * (columnCounter - 1));
myX2 = myX1 + CONFIG.FRM_HEIGHT;
var shiftX = Math.ceil((CONFIG.FRM_WIDTH - svgFile.width) / 2);
var shiftY = Math.ceil((CONFIG.FRM_WIDTH - svgFile.height) / 2);
x1 = myX1 + shiftX;
y1 = (myY1 + shiftY) * -1;
try {
svgFile.position = [x1, y1];
if (typeof(svgFile.resize) == "function") {
svgFile.resize(CONFIG.SCALE, CONFIG.SCALE);
}
if (CONFIG.ADD_LABELS) {
addLabel(theLayer, [x1, y1 - (svgFile.height + 20)], f.name)
}
// Only save the composite file if at least one
// icon exists and is successfully imported.
saveCompositeFile = true;
redraw();
} catch (ex) {
try {
svgFile.position = [0, 0];
logger(ex);
} catch (ex) {
/*Exit Gracefully*/ }
}
} else {
logger(svgFileList[i] + LANG.DOES_NOT_EXIT);
}
} catch (ex) {
logger(ex);
alert(ex);
}
i++;
}
}
};
if (saveCompositeFile)
saveFileAsAi(srcFolder.path + "/" + CONFIG.FILENAME);
}
};
};
};
/**
* Arranges items in the selection on a grid
* @param <selection> sel The current selection
* @return void
*/
function arrangeItems(sel) {
var board;
var bounds;
var itemBounds;
var cols;
var cellSize;
var x1 = y1 = 0;
var boardWidth, boardHeight;
board = doc.artboards[doc.artboards.getActiveArtboardIndex()];
bounds = board.artboardRect;
boardWidth = Math.round(bounds[2] - bounds[0]);
cols = CONFIG.NUM_COLS;
rows = CONFIG.NUM_ROWS;
x1 = bounds[0] + cellSize;
y1 = bounds[1] - cellSize;
for (var i = 0, slen = sel.length; i < slen; i++) {
theItem = sel[i];
itemBounds = theItem.visibleBounds;
theItem.top = y1 - ((cellSize - theItem.height) / 2);
theItem.left = x1 + ((cellSize - theItem.width) / 2);
alignToNearestPixel(theItem);
x1 += cellSize;
if (i % cols == cols - 1) {
x1 = bounds[0] + cellSize;
y1 -= cellSize;
}
}
if (CONFIG.SHRINK_TO_FIT) {
// The bounds are plotted on a Cartesian Coordinate System.
// So a 32 x 32 pixel artboard with have the following coords:
// (assumes the artboard is positioned at 0, 0)
// x1 = -16, y1 = 16, x2 = 16, y2 = -16
// board.artboardRect = [x1, y1, x2, y2];
board.artboardRect = [
bounds[0],
bounds[1],
bounds[0] + ((cols * cellSize) + (2 * cellSize)),
bounds[1] - (rows * cellSize)
];
}
};
/**
* Places a text label
* @param {string} text
* @param {string} pos - The X/Y position of the label
* @param {string} size - The text content of the label
* @returns void
*/
function addLabel(layer, pos, theText) {
try {
var theLabel = layer.textFrames.add();
theLabel.contents = theText;
var charAttributes = theLabel.textRange.characterAttributes;
var parAttributes = theLabel.paragraphs[0].paragraphAttributes;
charAttributes.size = 8;
parAttributes.justification = Justification.CENTER;
try {
theLabel.position = pos;
} catch (e) {
alert('labelPosition : ' + e)
}
return theLabel;
} catch (e) {
alert('addLabel : ' + e)
}
}
/**
* Saves the file in AI format.
* @param <string> The file destination path
* @return void
*/
function saveFileAsAi(dest) {
if (app.documents.length > 0) {
var options = new IllustratorSaveOptions();
var theDoc = new File(dest);
options.compatibility = CONFIG.AIFORMAT;
options.flattenOutput = OutputFlattening.PRESERVEAPPEARANCE;
options.pdfCompatible = true;
app.activeDocument.saveAs(theDoc, options);
}
}
/**
* Aligns selection to nearest whole pixel
* @param <selection> sel The selection object
* @return void
*/
function alignToNearestPixel(sel) {
try {
if (typeof sel != "object") {
logger(LANG.NO_SELECTION);
} else {
for (i = 0; i < sel.length; i++) {
sel[i].left = Math.round(sel[i].left);
sel[i].top = Math.round(sel[i].top);
}
redraw();
}
} catch (ex) {
logger(ex);
}
}
/**
* Logging for this script.
* @param <string> The logging text
* @return void
*/
function logger(txt) {
if (CONFIG.LOGGING == 0) return;
var file = new File(CONFIG.LOG_FILE_PATH);
file.open("e", "TEXT", "????");
file.seek(0, 2);
$.os.search(/windows/i) != -1 ? file.lineFeed = 'windows' : file.lineFeed = 'macintosh';
file.writeln("[" + new Date().toUTCString() + "] " + txt);
file.close();
}
/**
* Aligns the item to the nearest pixel for crisp rendering.
* @param <object> item The item to align
* @return void
*/
function alignToNearestPixel(item) {
if (item.height) {
item.height = moveToPixel(item.height);
}
if (item.width) {
item.width = moveToPixel(item.width);
}
item.top = moveToPixel(item.top);
item.left = moveToPixel(item.left);
};
/**
* Adjusts a value to the nearest whole number
* @param <float> n The value to adjust
* @return <int>
*/
function moveToPixel(n) {
return Math.round(n)
};
doCreateContactSheet();
userInteractionLevel = originalInteractionLevel;
@DezinerDharma
Copy link

DezinerDharma commented Jan 6, 2020

I add this icon-contact-sheet.js to the Adobe Illustrator CC & CS6 but it doesn't work. I need help how to use it. All the technique I use do it but it doesn't work.

@iconifyit
Copy link
Author

iconifyit commented Jan 6, 2020

I tested this again and it works fine for me. I wrote this on Mac so it could be that it only works on a Mac version of AI. Are you using Illustrator for Windows? If so the issue is /probably/ with the paths.

Try changing the settings for :

START_FOLDER: "~/Desktop",
LOG_FILE_PATH: "~/Desktop/ai-contactsheet-log.txt"

The '~/' portion of the path is a Mac convention so may be breaking the code on Windows.

@iconifyit
Copy link
Author

iconifyit commented Jan 6, 2020

I updated the code to remove the ~/ folder shortcut. Try the code again. That's about all the debugging I'm going to be able to do but hopefully that fixes it.

@iconifyit
Copy link
Author

iconifyit commented Jan 6, 2020

Last thing, but I would use this version instead : https://github.com/iconifyit/contact-sheet

@Artchibald
Copy link

Artchibald commented May 19, 2022

Last thing, but I would use this version instead : https://github.com/iconifyit/contact-sheet

That one was broken for me.

It would be nice this script added the filename under each icon in the contact sheet. I will try and add that now!

@iconifyit
Copy link
Author

iconifyit commented May 19, 2022

@Artchibald I think I have a function to add the file names. I'll see if I can find it. Pretty sure I do in fact. What error were you getting from the other version of Contact Sheet? I'd love to be able to fix that.

@Artchibald
Copy link

Artchibald commented May 19, 2022

That would be amazing! I will reproduce and get you the error, thanks.

@iconifyit
Copy link
Author

iconifyit commented May 19, 2022

@Artchibald Great. I will find the label routine after work today. Sooner if I can.

@iconifyit
Copy link
Author

iconifyit commented May 19, 2022

@Artchibald I updated the script to add the file name as a label under the icons.

@Artchibald
Copy link

Artchibald commented May 19, 2022

Thanks so much, I spent a couple of hours already trying to do that, so massive appreciation there, I will try it out tomorrow as I am enjoying a gin and tonic! Here is the error from the other script
Screenshot 2022-05-19 at 19 09 21
:

@iconifyit
Copy link
Author

iconifyit commented May 19, 2022

My pleasure. Thanks for the error message. I will fix that in the other version.

@iconifyit
Copy link
Author

iconifyit commented May 19, 2022

I think I fixed the other repo. Let me know if you still get the error. I was not getting that error but I think I know why it was happening anyway.

@Artchibald
Copy link

Artchibald commented May 20, 2022

filename placement worked like a dream,
thank you! As for your other script, here is the same error again. I did download the repo again just now to test.

Screenshot 2022-05-20 at 16 44 31

@iconifyit
Copy link
Author

iconifyit commented May 20, 2022

@Artchibald
Copy link

Artchibald commented May 23, 2022

Hi, no error this time in other script, but the print to artboard didn't work correctly. They were on top of each other. Something to do with ++coordinates in the loop I think.
Screenshot 2022-05-23 at 14 07 58

@iconifyit
Copy link
Author

iconifyit commented May 23, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment