Copy link to clipboard
Copied
Hi everyone,
I’m currently working on a project in Adobe Illustrator and could really use some help. I want to automate a repetitive workflow that involves:
I’ve tried several JavaScript/ExtendScript approaches, and I’ve learned the following:
Despite trying various code snippets and suggestions (including collecting all the paths in an array before looping, making sure to bring each path to the front, and so on), I’m still running into issues:
Any tips, scripts, or pointers you can share would be greatly appreciated! Thank you so much in advance—I’m really hoping there’s a tried-and-true approach to simplify this workflow.
Cheers,
K
Ah @kalle27850504iimx that's exactly what I needed!
Here's a script that will (I hope!) do what you want. It works on your sample files at least. Actually I made one of your masks into a compound path item because they are more difficult to work with. Let me know how it goes.
- Mark
/**
* @file Create Clipping Groups From Layers.js
*
* Using masks from one layer, and artwork from another,
* create clipping groups on another layer.
* See `settings` object for configuration.
*
* @aut
...
Copy link to clipboard
Copied
Hi @kalle27850504iimx, could you please post a demo file (save as pdf with illustrator editable) that shows before and another demo file showing after running the hypothetical script? This is the best way to show exactly what you want the script to do.
- Mark
Copy link to clipboard
Copied
Hi Mark! Thanks for answering.
Here’s a detailed rundown of what I’d like the script to do in Illustrator:
Copy all the artwork from a layer called “design.”
In the layer “print media,” loop through every path (including compound paths). For each path:
Paste the copied design behind the path (i.e., “Paste in Place” but behind in z-order) so it occupies the exact same coordinates as the path.
Create a clipping mask where the path is the mask, and the pasted design is the content.
(Optional) Move (or cut/paste) the newly created clipping mask into a layer named “print ready” while keeping the same position, if possible.
Repeat the above until all paths/compound paths in “print media” have become clipping masks.
Copy link to clipboard
Copied
Ah @kalle27850504iimx that's exactly what I needed!
Here's a script that will (I hope!) do what you want. It works on your sample files at least. Actually I made one of your masks into a compound path item because they are more difficult to work with. Let me know how it goes.
- Mark
/**
* @file Create Clipping Groups From Layers.js
*
* Using masks from one layer, and artwork from another,
* create clipping groups on another layer.
* See `settings` object for configuration.
*
* @author m1b
* @discussion https://community.adobe.com/t5/illustrator-discussions/need-help-automating-clipping-masks-for-multiple-paths-in-illustrator/m-p/15089842
*/
(function () {
var settings = {
// the layer names used in document
CLIPPED_LAYER_NAME: 'print ready',
MASK_LAYER_NAME: 'print media',
DESIGN_LAYER_NAME: 'design',
// whether to hide layers after making groups
hideDesignLayer: true,
hideMaskLayer: true,
};
var doc = app.activeDocument;
var clippedLayer = getThing(doc.layers, 'name', settings.CLIPPED_LAYER_NAME),
maskLayer = getThing(doc.layers, 'name', settings.MASK_LAYER_NAME),
designLayer = getThing(doc.layers, 'name', settings.DESIGN_LAYER_NAME);
if (!clippedLayer || !maskLayer || !designLayer)
return alert('Document does not have the required layers:\n' + [settings.CLIPPED_LAYER_NAME, settings.MASK_LAYER_NAME, settings.DESIGN_LAYER_NAME].join('\n'));
var readyItems = [],
maskItems = maskLayer.pageItems,
designItems = designLayer.pageItems;
// for each mask item, collect the designItems that intersect
// with it and create the clipping group using those items
for (var i = 0; i < maskItems.length; i++) {
var maskItem = maskItems[i],
maskBounds = maskItem.visibleBounds,
intersectingItems = getIntersectingItems(designItems, maskBounds);
if (0 === intersectingItems.length)
continue;
var group = clippedLayer.groupItems.add(),
groupMask = maskItem.duplicate(group, ElementPlacement.PLACEATBEGINNING),
compoundPathWorkaroundPathItem = undefined;
if ('CompoundPathItem' === groupMask.constructor.name) {
// workaround for bug with scripting API that won't allow
// making a clipping group with compound path item
compoundPathWorkaroundPathItem = group.pathItems.add();
compoundPathWorkaroundPathItem.move(group, ElementPlacement.PLACEATBEGINNING)
}
for (var j = 0; j < intersectingItems.length; j++)
intersectingItems[j].duplicate(group, ElementPlacement.PLACEATEND);
// convert group to a clipping group
group.clipped = true;
if (compoundPathWorkaroundPathItem) {
// workaround for bug with scripting API that won't allow
// making a clipping group with compound path item
// (I use a surrogate path item, which I then remove
// and switch with the compound path item we want)
compoundPathWorkaroundPathItem.remove();
groupMask.pathItems[0].clipping = true;
}
readyItems.push(group);
}
if (settings.hideDesignLayer)
designLayer.visible = false;
if (settings.hideMaskLayer)
maskLayer.visible = false;
})();
/**
* Returns a thing with matching property.
* If `key` is undefined, evaluate the object itself.
* @author m1b
* @version 2024-04-21
* @param {Array|Collection} things - the things to look through.
* @param {String} [key] - the property name (default: undefined).
* @param {*} value - the value to match.
*/
function getThing(things, key, value) {
for (var i = 0, obj; i < things.length; i++)
if ((undefined == key ? things[i] : things[i][key]) == value)
return things[i];
};
/**
* Returns array of an `items` that have
* bounds intersecting with `bounds`, if any.
* @author m1b
* @version 2025-01-15
* @param {Array<PageItem>} items - the items to check.
* @param {Array<Number>} bounds - the target bounds (LTRB for Illustrator, TLBR for Indesign).
* @param {Boolean} [useGeometricBounds] - whether to use geometricBounds (default: false, visibleBounds).
* @returns {Array<PageItem>}
*/
function getIntersectingItems(items, bounds, useGeometricBounds) {
var found = [],
boundsType = true === useGeometricBounds ? 'geometricBounds' : 'visibleBounds';
for (var i = 0; i < items.length; i++)
if (boundsDoIntersect(items[i][boundsType], bounds))
found.push(items[i]);
return found;
};
/**
* Returns true if the two bounds intersect.
* @author m1b
* @version 2024-03-10
* @param {Array} bounds1 - bounds array.
* @param {Array} bounds2 - bounds array.
* @param {Boolean} [TLBR] - whether bounds arrays are interpreted as [t, l, b, r] or [l, t, r, b] (default: based on app).
* @returns {Boolean}
*/
function boundsDoIntersect(bounds1, bounds2, TLBR) {
if (undefined == TLBR)
TLBR = (/indesign/i.test(app.name));
return !(
TLBR
// TLBR
? (
bounds2[0] > bounds1[2]
|| bounds2[1] > bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] < bounds1[1]
)
// LTRB
: (
bounds2[0] > bounds1[2]
|| bounds2[1] < bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] > bounds1[1]
)
);
};
Copy link to clipboard
Copied
Mark, I seriously can’t thank you enough for taking the time to help me.
Your solution was exactly what I needed. I’m honestly blown away. You’ve saved me so much stress, I really appreciate all your effort. Thank you a million times over!
Copy link to clipboard
Copied
I’ve recently started to realize the potential of scripts, and honestly, it feels like I’m fumbling in the dark trying to figure things out. After seeing how well your solution worked, it got me thinking about whether something like this might be possible:
I’ve put together some example files to show what I have in mind.
Of course, I completely understand if this isn’t something you have time for or want to take on. But if you do, I’d really value any guidance or advice you can offer.
Thanks so much for everything so far—it’s been a huge help.
Copy link to clipboard
Copied
Hi @kalle27850504iimx, you're very welcome. There is a complexity when it comes to aligning items which is that the geometric bounds given for a clipping group is actually the entire contents of the group, not just the visible part. However, I have written a function that handles the heavy lifting (getItemBoundsIllustrator) so it isn't a huge job to do what you want. Let me know if it works as you expect.
- Mark
/**
* @file Create Clipping Groups From Layers.js
*
* Using masks from one layer, and artwork from another,
* create clipping groups on another layer.
* See `settings` object for configuration.
*
* If a mask layer item's name matches a target layer
* item's name, then the new masked group can be
* positioned to align with that target item.
*
* @author m1b
* @version 2025-01-16
* @discussion https://community.adobe.com/t5/illustrator-discussions/need-help-automating-clipping-masks-for-multiple-paths-in-illustrator/m-p/15089842
*/
(function () {
var settings = {
// the layer names used in document
TARGET_LAYER_NAME: 'print ready',
MASK_LAYER_NAME: 'print media',
DESIGN_LAYER_NAME: 'design',
// whether to position artwork on target layer
positionArtwork: true,
deleteTargetItems: true,
// whether to hide layers after making groups
hideDesignLayer: true,
hideMaskLayer: true,
};
var doc = app.activeDocument;
var targetLayer = getThing(doc.layers, 'name', settings.TARGET_LAYER_NAME),
maskLayer = getThing(doc.layers, 'name', settings.MASK_LAYER_NAME),
designLayer = getThing(doc.layers, 'name', settings.DESIGN_LAYER_NAME);
if (!targetLayer || !maskLayer || !designLayer)
return alert('Document does not have the required layers:\n' + [settings.TARGET_LAYER_NAME, settings.MASK_LAYER_NAME, settings.DESIGN_LAYER_NAME].join('\n'));
var readyItems = [],
targetItems = targetLayer.pageItems,
maskItems = maskLayer.pageItems,
designItems = designLayer.pageItems;
// collect target item info for later
var targetBounds = [],
deleteMeUUIDs = [];
for (var i = 0, targetItem; i < maskItems.length; i++) {
if (!maskItems[i].name)
continue;
targetItem = getThing(targetItems, 'name', maskItems[i].name);
if (!targetItem)
continue;
deleteMeUUIDs.push(targetItem.uuid);
targetBounds[i] = getItemBoundsIllustrator(targetItem, true);
}
// for each mask item, collect the designItems that intersect
// with it and create the clipping group using those items
// for (var i = 0; i < maskItems.length; i++) {
for (var i = maskItems.length - 1; i >= 0; i--) {
var targetItem = undefined,
maskItem = maskItems[i],
maskBounds = maskItem.visibleBounds,
intersectingItems = getIntersectingItems(designItems, maskBounds);
if (0 === intersectingItems.length)
continue;
var group = targetLayer.groupItems.add(),
groupMask = maskItem.duplicate(group, ElementPlacement.PLACEATBEGINNING),
compoundPathWorkaroundPathItem = undefined;
if ('CompoundPathItem' === groupMask.constructor.name) {
// workaround for bug with scripting API that won't allow
// making a clipping group with compound path item
compoundPathWorkaroundPathItem = group.pathItems.add();
compoundPathWorkaroundPathItem.move(group, ElementPlacement.PLACEATBEGINNING)
}
for (var j = 0; j < intersectingItems.length; j++)
intersectingItems[j].duplicate(group, ElementPlacement.PLACEATEND);
// convert group to a clipping group
group.clipped = true;
if (compoundPathWorkaroundPathItem) {
// workaround for bug with scripting API that won't allow
// making a clipping group with compound path item
// (I use a surrogate path item, which I then remove
// and switch with the compound path item we want)
compoundPathWorkaroundPathItem.remove();
groupMask.pathItems[0].clipping = true;
}
if (
undefined != targetBounds[i]
&& settings.positionArtwork
) {
// position new group on target layer
var bounds = getItemBoundsIllustrator(group, true),
dx = targetBounds[i][0] - bounds[0],
dy = targetBounds[i][1] - bounds[1];
group.translate(dx, dy, true, true, true, true);
}
readyItems.push(group);
}
if (settings.hideDesignLayer)
designLayer.visible = false;
if (settings.hideMaskLayer)
maskLayer.visible = false;
// delete only the target items we used
if (settings.deleteTargetItems)
for (var i = deleteMeUUIDs.length - 1; i >= 0; i--)
doc.getPageItemFromUuid(deleteMeUUIDs[i]).remove();
})();
/**
* Returns a thing with matching property.
* If `key` is undefined, evaluate the object itself.
* @author m1b
* @version 2024-04-21
* @param {Array|Collection} things - the things to look through.
* @param {String} [key] - the property name (default: undefined).
* @param {*} value - the value to match.
*/
function getThing(things, key, value) {
for (var i = 0, obj; i < things.length; i++)
if ((undefined == key ? things[i] : things[i][key]) == value)
return things[i];
};
/**
* Returns array of an `items` that have
* bounds intersecting with `bounds`, if any.
* @author m1b
* @version 2025-01-15
* @param {Array<PageItem>} items - the items to check.
* @param {Array<Number>} bounds - the target bounds (LTRB for Illustrator, TLBR for Indesign).
* @param {Boolean} [useGeometricBounds] - whether to use geometricBounds (default: false, visibleBounds).
* @returns {Array<PageItem>}
*/
function getIntersectingItems(items, bounds, useGeometricBounds) {
var found = [],
boundsType = true === useGeometricBounds ? 'geometricBounds' : 'visibleBounds';
for (var i = 0; i < items.length; i++)
if (boundsDoIntersect(items[i][boundsType], bounds))
found.push(items[i]);
return found;
};
// from here down is just code to get a page item's bounds
/**
* Returns bounds of item(s).
* @author m1b
* @version 2025-01-20
* @param {PageItem|Array<PageItem>} item - an Illustrator PageItem or array of PageItems.
* @param {Boolean} [geometric] - if false, returns visible bounds.
* @param {Array} [bounds] - private parameter, used when recursing.
* @returns {Array} - the calculated bounds.
*/
function getItemBoundsIllustrator(item, geometric, bounds) {
var newBounds = [],
boundsKey = geometric ? 'geometricBounds' : 'visibleBounds';
if (undefined == item)
return;
if (
item.typename == 'GroupItem'
|| item.constructor.name == 'Array'
) {
var children = item.typename == 'GroupItem' ? item.pageItems : item,
contentBounds = [],
isClippingGroup = (item.hasOwnProperty('clipped') && item.clipped == true),
clipBounds;
for (var i = 0, child; i < children.length; i++) {
child = children[i];
if (
(
child.hasOwnProperty('clipping')
&& true === child.clipping
)
|| (
'CompoundPathItem' === child.constructor.name
&& child.pathItems.length > 0
&& child.pathItems[0].hasOwnProperty('clipping')
&& true === child.pathItems[0].clipping
)
)
// child is a clipping path
clipBounds = child.geometricBounds;
else
contentBounds.push(getItemBoundsIllustrator(child, geometric, bounds));
}
newBounds = combineBounds(contentBounds);
if (isClippingGroup)
newBounds = intersectionOfBounds([clipBounds, newBounds]);
}
else if (
'TextFrame' === item.constructor.name
&& TextType.AREATEXT !== item.kind
) {
// get bounds of outlined text
var dup = item.duplicate().createOutline();
newBounds = dup[boundsKey];
dup.remove();
}
else if (item.hasOwnProperty(boundsKey)) {
newBounds = item[boundsKey];
}
// `bounds` will exist if this is a recursive execution
bounds = (undefined == bounds)
? bounds = newBounds
: bounds = combineBounds([newBounds, bounds]);
return bounds;
};
/**
* Returns the combined bounds of all bounds supplied.
* Works with Illustrator or Indesign bounds.
* @author m1b
* @version 2024-03-09
* @param {Array<bounds>} boundsArray - an array of bounds [L, T, R, B] or [T, L , B, R].
* @returns {bounds?} - the combined bounds.
*/
function combineBounds(boundsArray) {
var combinedBounds = boundsArray[0],
comparator;
if (/indesign/i.test(app.name))
comparator = [Math.min, Math.min, Math.max, Math.max];
else if (/illustrator/i.test(app.name))
comparator = [Math.min, Math.max, Math.max, Math.min];
// iterate through the rest of the bounds
for (var i = 1; i < boundsArray.length; i++) {
var bounds = boundsArray[i];
combinedBounds = [
comparator[0](combinedBounds[0], bounds[0]),
comparator[1](combinedBounds[1], bounds[1]),
comparator[2](combinedBounds[2], bounds[2]),
comparator[3](combinedBounds[3], bounds[3]),
];
}
return combinedBounds;
};
/**
* Returns the overlapping rectangle
* of two or more rectangles.
* NOTE: Returns undefined if ANY
* rectangles do not intersect.
* @author m1b
* @version 2024-09-05
* @param {Array<bounds>} arrayOfBounds - an array of bounds [L, T, R, B] or [T, L , B, R].
* @returns {bounds?} - intersecting bounds.
*/
function intersectionOfBounds(arrayOfBounds) {
var comparator;
if (/indesign/i.test(app.name))
comparator = [Math.max, Math.max, Math.min, Math.min];
else if (/illustrator/i.test(app.name))
comparator = [Math.max, Math.min, Math.min, Math.max];
// sort a copy of array
var bounds = arrayOfBounds
.slice(0)
.sort(function (a, b) { return b[0] - a[0] || a[1] - b[1] });
// start with first bounds
var intersection = bounds.shift(),
b;
// compare each bounds, getting smaller
while (b = bounds.shift()) {
// if doesn't intersect, bail out
if (!boundsDoIntersect(intersection, b))
return;
intersection = [
comparator[0](intersection[0], b[0]),
comparator[1](intersection[1], b[1]),
comparator[2](intersection[2], b[2]),
comparator[3](intersection[3], b[3]),
];
}
return intersection;
};
/**
* Returns true if the two bounds intersect.
* @author m1b
* @version 2024-03-10
* @param {Array} bounds1 - bounds array.
* @param {Array} bounds2 - bounds array.
* @param {Boolean} [TLBR] - whether bounds arrays are interpreted as [t, l, b, r] or [l, t, r, b] (default: based on app).
* @returns {Boolean}
*/
function boundsDoIntersect(bounds1, bounds2, TLBR) {
if (undefined == TLBR)
TLBR = (/indesign/i.test(app.name));
return !(
TLBR
// TLBR
? (
bounds2[0] > bounds1[2]
|| bounds2[1] > bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] < bounds1[1]
)
// LTRB
: (
bounds2[0] > bounds1[2]
|| bounds2[1] < bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] > bounds1[1]
)
);
};
Edit 2025-01-18: Fixed bug in `getItemBoundsIllustrator` where it didn't handle compound path item correctly!
Edit 2025-01-20: added check for mal-formed compoundPathItems with no paths.
Copy link to clipboard
Copied
Hi @m1b
That works exactly as I wanted it to! The only issue comes up when compound paths are involved - it seems like it can’t find a way to align when dealing with a compound path. Maybe it’s just not possible due to the complexity you mentioned with aligning items?
Thanks again for all your help—I really appreciate it!
Copy link to clipboard
Copied
Hi @kalle27850504iimx, sorry there was a massive bug in one of the functions—oops! It now should correctly handle compound path items. Thanks for letting me know me know about the problem. I've updated the code above so please copy it again.
- Mark
Copy link to clipboard
Copied
@m1b , it seems to now work for specific designs – when the design contains a compound path, but not always, just for certain compound paths.
I’m encountering Error 1302: No such element, Line: 212, which I don’t fully understand. When the script does process, it almost works – the pieces move toward their correct positions but end up in seemingly random spots. This might be due to how clipping masks affect positioning, and there might not be a fix for that.
I’ll attach a file where the error occurs in case you’d like to take a look at it. If not, thank you so much for all your help so far!
Copy link to clipboard
Copied
Hi @kalle27850504iimx thanks for your demo file—it always helps to have that. The script error is due to a bug in Illustrator that allows us to create malformed compound path items: if we "compound" a group, it will make a seemingly normal compound path that works in every normal way via the UI, but when a script look inside it there are no paths! There is a complicated way to fix these via scripting (uncompounding, ungrouping and recompounding) but the problem is that by script we can't know if the bad compoundpath is a clipping path (because we can't ask it's paths).
Anyway, I've updated code above, putting in a check for a compoundPath with no pathItems and it will just ignore it. Give it another try and see what you think.
- Mark
P.S. one of your print ready shapes is way down on the canvas off the artboard. Still works fine for the script, but confused me for a second! 🙂
Copy link to clipboard
Copied
Hi @m1b ,
Got it, thanks! I really appreciate all your help and explanations. I added one there to test the script's limits while troubleshooting the issues. 😊
Copy link to clipboard
Copied
Glad to help. All the best!
Copy link to clipboard
Copied
Thanks for making this script available for everyone! This worked great and is a huge timesaver
Copy link to clipboard
Copied
Great to hear!
Find more inspiration, events, and resources on the new Adobe Community
Explore Now