Last active
October 11, 2022 07:11
-
-
Save iconifyit/85993fd5c61e3608bc57767e0b56e3b1 to your computer and use it in GitHub Desktop.
Adobe Illustrator JSX plugin to create a contact sheet from a folder or vector icons.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; |
My pleasure. Thanks for the error message. I will fix that in the other version.
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.
I figured it out. There was a typo in my variable name. It should be working now.
—
Twitter : @iconifyit
https://iconmason.com
…On May 20, 2022, 11:47 AM -0400, Artchibald ***@***.***>, wrote:
@Artchibald commented on this gist.
filename placement worked like a dream,
thank you! As for your other script, here is the new error.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you authored the thread.Message ID: ***@***.***>
What is the size of your icons? And can I see the settings you entered? I suspect there is a discrepancy between the settings and the actual size of the icons.
—
Twitter : @iconifyit
https://iconmason.com
…On May 23, 2022, 9:09 AM -0400, Artchibald ***@***.***>, wrote:
@Artchibald commented on this gist.
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.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.Message ID: ***@***.***>
Pretty cool implementation. Client job? I’m glad you could put it to good use and glad to hear someone is getting Adobe scripting work.
—
Twitter : @iconifyit
https://iconmason.com
…On May 19, 2022, 6:02 PM -0400, Artchibald ***@***.***>, wrote:
@Artchibald commented on this gist.
I am using your script to extend my script :)
https://github.com/Artchibald/WTW-illustrator-script
It loops through a layer of icons against a background layer of colors or whatever else you might need, and exports them to a variety of sizes and extensions. You have to use the illustrator file provided to make it work. Let me know what you think!
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you authored the thread.Message ID: ***@***.***>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
: