Skip to content

Instantly share code, notes, and snippets.

@kulmajaba
Last active October 15, 2025 13:27
Show Gist options
  • Select an option

  • Save kulmajaba/6ad1f94901bae0666ab14451aff6e286 to your computer and use it in GitHub Desktop.

Select an option

Save kulmajaba/6ad1f94901bae0666ab14451aff6e286 to your computer and use it in GitHub Desktop.
Illustrator ExtendScript file to merge multiple vector files (such as DXF) into one project
/** DXF merger, author Mika Kuitunen
* Takes multiple DXF files (2D) and merges them into a single Illustrator document using a packing algorithm for placement
* Create file Adobe Illustrator 2025/Presets/en_US/Scripts/DXF-merge.jsx and paste the contents there to use
*
* Instructions: This script expects the file names to end with a part number, group and possible suffix, preceded by ' ' and separated by '-'.
* E.g. 'Project 1 - Flat pattern of 1-a-1.dxf'
*
* 1 - a - 1
* <partno.> <group> <suffix>
*
* The parts are divided into artboards by group and labeled with the full info.
*
* Parts are also flipped horizontally and rotated 180 degrees.
*/
/**
* TODO
* - If an element is wider than the allowed space (A4 width with 10mm margin on both sides) the packing algorithm will fail
* - The packing algorithm will not rotate elements to fill space
*/
#target illustrator
// DEBUGGING STUFF
// https://github.com/joshbduncan/illustrator-scripts
function Logger(fp, mode, sizeLimit, console) {
var fp = Folder.userData + "/DXF-merge.log";
this.mode = "a";
this.console = false;
this.file = new File(fp);
this.badPath = false;
}
Logger.prototype = {
/**
* Write data to the log file.
* @param {String} text One or more strings to write, which are concatenated to form a single string.
* @returns {Boolean} Returns true if log file is successfully written, false if unsuccessful.
*/
log: function (text) {
// no need to keep alerting when the log path is bad
if (this.badPath) return false;
var f = this.file;
var m = this.mode;
var ts = new Date().toLocaleString();
// ensure parent folder exists
if (!f.parent.exists) {
if (!f.parent.parent.exists) {
alert("Bad log file path!\n'" + this.file + "'");
this.badPath = true;
return false;
}
f.parent.create();
}
// grab all arguments
var args = ["[" + ts + "]"];
for (var i = 0; i < arguments.length; ++i) args.push(arguments[i]);
// write the data
try {
f.encoding = "UTF-8";
f.open(m);
f.writeln(args.join(" "));
} catch (e) {
$.writeln("Error writing file:\n" + f);
return false;
} finally {
f.close();
}
// write `text` to the console if requested
if (this.console) $.writeln(args.slice(1, args.length).join(" "));
return true;
},
};
var console = new Logger();
// ----------------------------------------------------------------------------
// MAIN SCRIPT
function find(arr, callbackFn) {
for (var i = 0; i < arr.length; i++) {
if (callbackFn(arr[i])) {
return arr [i];
}
}
return undefined;
}
function pushOrAddArrayInObject(obj, key, val) {
if (obj[key] !== undefined) {
obj[key].push(val);
} else {
obj[key] = [val];
}
}
function mmToPt(mm) {
return new UnitValue(mm, "mm").as("pt");
}
var docPreset = new DocumentPreset;
docPreset.width = mmToPt(210);
docPreset.height = mmToPt(297);
docPreset.units = RulerUnits.Millimeters;
docPreset.colorMode = DocumentColorSpace.CMYK;
function topToBottomY(y) {
return docPreset.height - y;
}
function addArtboard(startX) {
// Left X, Top Y, Right X, Bottom Y
var rect = [startX, docPreset.height, startX + docPreset.width, 0];
return app.activeDocument.artboards.add(rect);
}
var paddingPt = mmToPt(5);
var pageMargin = mmToPt(10);
// Each element has a padding of 5mm on the right edge, compensate in max width to allow full pages with 10mm margin on both sides
var maxLayoutWidth = mmToPt(210 - (10 + 5));
// https://github.com/mapbox/potpack
/**
* @typedef {Object} PotpackBox
* @property {number} w Box width.
* @property {number} h Box height.
* @property {number} [x] X coordinate in the resulting container.
* @property {number} [y] Y coordinate in the resulting container.
*/
/**
* @typedef {Object} PotpackStats
* @property {number} w Width of the resulting container.
* @property {number} h Height of the resulting container.
* @property {number} fill The space utilization value (0 to 1). Higher is better.
*/
/**
* Packs 2D rectangles into a near-square container.
*
* Mutates the {@link boxes} array: it's sorted (by height/width),
* and box objects are augmented with `x`, `y` coordinates.
*
* @param {PotpackBox[]} boxes
* @return {PotpackStats}
*/
function potpack(boxes) {
// calculate total box area and maximum box width
var area = 0;
var maxWidth = 0;
for (var k = 0; k < boxes.length; k++) {
var box1 = boxes[k];
area += box1.w * box1.h;
maxWidth = Math.max(maxWidth, box1.w);
}
// sort the boxes for insertion by height, descending
boxes.sort(function(a, b) { return b.h - a.h });
// Aim for A4 width
var startWidth = maxLayoutWidth;
// start with a single empty space, unbounded at the bottom
var spaces = [{x: 0, y: 0, w: startWidth, h: Infinity}];
var width = 0;
var height = 0;
var box = undefined;
var space = undefined;
var last = undefined
for (var j = 0; j < boxes.length; j++) {
box = boxes [j];
// look through spaces backwards so that we check smaller spaces first
for (var i = spaces.length - 1; i >= 0; i--) {
space = spaces[i];
// look for empty spaces that can accommodate the current box
if (box.w > space.w || box.h > space.h) continue;
// found the space; add the box to its top-left corner
// |-------|-------|
// | box | |
// |_______| |
// | space |
// |_______________|
box.x = space.x;
box.y = space.y;
height = Math.max(height, box.y + box.h);
width = Math.max(width, box.x + box.w);
if (box.w === space.w && box.h === space.h) {
// space matches the box exactly; remove it
last = spaces.pop();
if (last && i < spaces.length) spaces[i] = last;
} else if (box.h === space.h) {
// space matches the box height; update it accordingly
// |-------|---------------|
// | box | updated space |
// |_______|_______________|
space.x += box.w;
space.w -= box.w;
} else if (box.w === space.w) {
// space matches the box width; update it accordingly
// |---------------|
// | box |
// |_______________|
// | updated space |
// |_______________|
space.y += box.h;
space.h -= box.h;
} else {
// otherwise the box splits the space into two spaces
// |-------|-----------|
// | box | new space |
// |_______|___________|
// | updated space |
// |___________________|
spaces.push({
x: space.x + box.w,
y: space.y,
w: space.w - box.w,
h: box.h
});
space.y += box.h;
space.h -= box.h;
}
break;
}
}
return {
w: width, // container width
h: height, // container height
fill: (area / (width * height)) || 0 // space utilization
};
}
function main() {
console.log("");
var fList = File.openDialog("Select DXF files to merge", "DXF:*.dxf,All files:*.*", true);
if (!fList) {
console.log("User aborted");
return;
}
var currentInteractionLevel = app.userInteractionLevel;
app.userInteractionLevel = UserInteractionLevel.DONTDISPLAYALERTS;
var dest = app.documents.addDocument(DocumentColorSpace.CMYK, docPreset, false);
for (var i = 0; i < fList.length; i++) {
app.activeDocument.groupItems.createFromFile(fList[i]);
}
var groupItems = app.activeDocument.layers[0].groupItems;
var boxes = {};
var item = undefined
var itemFileName = '';
var itemName = '';
var itemGroup = '';
// Layers are found in the document in reverse order compared to the file list, reverse file list to match
fList.reverse();
for (var j = 0; j < groupItems.length; j++) {
item = groupItems[j];
itemFileName = decodeURI(fList[j].name);
itemName = itemFileName.substring(0, itemFileName.length - 4).split(' ').pop();
itemGroup = itemName.split('-')[1];
item.name = itemName;
pushOrAddArrayInObject(boxes, itemGroup, {w: item.width + paddingPt, h: item.height + paddingPt, name: itemName})
}
var k = 0;
var box = undefined;
var layerX = 0;
var layerYTop = 0;
var layerY = 0;
var layerName = undefined;
var newBoardStartX = docPreset.width + paddingPt;
var offsetY = 0;
var currentArtBoard = app.activeDocument.artboards[0];
var mirrorMatrix = app.getScaleMatrix(-100, 100);
var transformMatrix = app.concatenateRotationMatrix(mirrorMatrix, 180);
for (var boxGroupKey in boxes) {
boxGroup = boxes[boxGroupKey];
potpack(boxGroup);
offsetY = 0;
if (k > 0) {
currentArtBoard = addArtboard(newBoardStartX);
newBoardStartX = currentArtBoard.artboardRect[2] + paddingPt;
offsetY = 0;
}
for (var l = 0; l < boxGroup.length; l++) {
box = boxGroup[l];
item = find(groupItems, function (item) { return item.name === box.name });
layerYTop = offsetY + pageMargin + box.y;
if (layerYTop + box.h - paddingPt > docPreset.height - pageMargin) {
currentArtBoard = addArtboard(newBoardStartX);
newBoardStartX = currentArtBoard.artboardRect[2] + paddingPt;
offsetY = -box.y;
layerYTop = offsetY + pageMargin + box.y;
}
layerY = topToBottomY(layerYTop);
layerX = currentArtBoard.artboardRect[0] + pageMargin + box.x;
item.position = [layerX, layerY];
item.transform(transformMatrix);
layerName = item.textFrames.pointText([layerX + (box.w - paddingPt) / 2, layerY - (box.h - paddingPt) / 2]);
layerName.contents = item.name;
layerName.paragraphs[0].justification = Justification.CENTER;
}
k++;
}
app.userInteractionLevel = currentInteractionLevel;
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment