Copy link to clipboard
Copied
Hi folks,
I'm struggling a bit with a script. Here in the forum I saw a script which creates a rectangle around a single object with a certain offset and so on. This script doesn't work unfortunately, once you select multiple objects and want to create a rectangle along those.
My workflow by hand currently looks like this. I'm having those objects on the artboard, then I'm creating a rectangle exacly along the borders of all objects (basically recreating the bounding-box with an actual rectangle if you'd like to say so) and then I'm giving the newly created rectangle a black contour with 0.25pts and finally I'm ofsetting the path by 1mm and delete the inner rectangle.
So basically starting point on the left, and the end result to the right when I'm doing it by hand.
Pretty time consuming and tedious to do, especially because I find it really hard to place the initial rectangle on the first try, without the need to gently fondle it into the right place afterwards.
So heres the script I find which would possibly do the work for me, if it was for only one object:
function makeRect() {
if (selection.length == 0) {
alert("No object …");
return;
}else{
if (selection.length > 1) {
alert("Too many objects …");
return;
}else{
var myDoc = app.activeDocument;
var Sel = myDoc.selection
var SelVB = Sel[0].visibleBounds;
var dMarg = 28.3464567;
var Links = SelVB[0];
var Oben = SelVB[1];
var SelBreite = (SelVB[2] - SelVB[0]);
var SelHoehe = (SelVB[1] - SelVB[3]);
var newCMYK = new CMYKColor();
newCMYK.cyan = 100;
newCMYK.magenta = 0;
newCMYK.yellow = 0;
newCMYK.black = 0;
var myGroup =myDoc.groupItems.add();
Sel[0].move(myGroup, ElementPlacement.PLACEATBEGINNING);
var MargBox = myDoc.pathItems.rectangle(Oben+dMarg, Links-dMarg, SelBreite+2*dMarg, SelHoehe+2*dMarg);
MargBox.stroked = true;
MargBox.strokeWidth = 10;
//MargBox.filled = false;
MargBox.fillColor = newCMYK;
MargBox.move(myGroup, ElementPlacement.PLACEATEND);
}
}
}
makeRect();
The script makes a rectangle, with stroke of 10, cyan fill and an offset of... 28,something - whatever that exactly means - is it px?
However, this doesn't work when it comes down to multiple objects that need to be rectangle-outlined. I mean there's obviously right at the beginning an alert if you select more than one object.
So, could someone help me out on this one? You'd really make my day.
Best regards,
Thomas
Hi @TW-22341, I spent a lot of time some while ago trying to work out a good way to get an item's bounds, even if it was clipped or whatever, and I made a function that has a good try at doing so, although it won't be perfect in strange case, eg. where effects like drop shadows are added.
I've thrown together a quick script that uses those functions I already wrote and I think it should do what you ask for. Let me know how it goes for you. You can adjust the "settings" object.
- Mark
Edit 1: i
...Copy link to clipboard
Copied
Not a script solution, but you can achieve what you've been doing by hand by grouping the objects, adding a stroke to the group and using Effect > Convert to Shape > Rectangle on the stroke:
You could save that as a graphic style.
Copy link to clipboard
Copied
Actually yes, if it was only for the optical effect. But I do need an actual path for anything that comes after this step 🙂
Copy link to clipboard
Copied
Object > Expand Appearance
Copy link to clipboard
Copied
Perfectly match to my requirement, Thanks lot :), you make my day
Copy link to clipboard
Copied
Hi @TW-22341, I spent a lot of time some while ago trying to work out a good way to get an item's bounds, even if it was clipped or whatever, and I made a function that has a good try at doing so, although it won't be perfect in strange case, eg. where effects like drop shadows are added.
I've thrown together a quick script that uses those functions I already wrote and I think it should do what you ask for. Let me know how it goes for you. You can adjust the "settings" object.
- Mark
Edit 1: improved handling of text.
Edit 2: simplified code, changed getItemBounds to handle multiple items as parameter, moved rect behind objects as per OP's code.
(function () {
var mm = 2.834645;
// the colors of the rectangle
var black = new GrayColor(),
cyan = new CMYKColor();
black.gray = 100
cyan.cyan = 100;
cyan.magenta = 0;
cyan.yellow = 0;
cyan.black = 0;;
// user settings:
var settings = {
margin: 1 * mm,
strokeColor: black,
strokeWidth: 1
};
var doc = app.activeDocument,
items = doc.selection;
if (items.length == 0) {
alert('Please make a selection and try again.');
return;
}
var lastItem = items[items.length - 1],
bounds = getItemBounds(items);
if (bounds == undefined)
return;
// add margin
bounds[0] -= settings.margin;
bounds[1] += settings.margin;
bounds[2] += settings.margin;
bounds[3] -= settings.margin;
// draw the rectangle
var rect = doc.pathItems.rectangle(bounds[1], bounds[0], bounds[2] - bounds[0], bounds[1] - bounds[3]);
rect.filled = true;
rect.fillColor = cyan;
rect.stroked = true;
rect.strokeWidth = settings.strokeWidth;
rect.strokeColor = settings.strokeColor;
// put the rectangle behind all the items
rect.move(lastItem, ElementPlacement.PLACEAFTER);
// leave just the rectangle selected
app.selection = null;
rect.selected = true;
// finished
/**
* Returns bounds of item.
* @author m1b
* @version 2022-10-28
* Attempts to get correct bounds
* of clipped groups.
* @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 getItemBounds(item, geometric, bounds) {
var newBounds = [];
if (
item.typename == 'GroupItem'
|| item.constructor.name == 'Array'
) {
var children = item.typename == 'GroupItem' ? item.pageItems : item,
contentBounds = [];
if (
item.hasOwnProperty('clipped')
&& item.clipped == true
) {
// item is clipping group
var clipBounds;
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (
child.hasOwnProperty('clipping')
&& child.clipping == true
)
// the clipping item
clipBounds = child.geometricBounds;
else
// a clipped content item
var b = expandBounds(getItemBounds(child, geometric, bounds), contentBounds);
}
newBounds = intersectionOfBounds([clipBounds, contentBounds]);
}
else {
// item is a normal group
for (var i = 0; i < children.length; i++) {
var child = children[i];
var b = expandBounds(getItemBounds(child, geometric, bounds), contentBounds);
}
newBounds = contentBounds;
}
}
else if (item.typename == 'TextFrame') {
// item is a text frame
var dup = item.duplicate().createOutline();
newBounds = geometric ? dup.geometricBounds : dup.visibleBounds;
dup.remove();
}
else {
// item is not clipping group
newBounds = geometric ? item.geometricBounds : item.visibleBounds;
}
bounds = (bounds == undefined)
? bounds = newBounds
: bounds = expandBounds(newBounds, bounds);
return bounds;
};
/**
* Returns bounds that encompass both bounds.
* @author m1b
* @version 2022-07-24
**
* @param {Array} b1 - bounds array [l, t, r, b].
* @param {Array} b2 - bounds array [l, t, r, b].
* @returns {Array} - the encompassing bounds.
*/
function expandBounds(b1, b2) {
var expanded = b2;
for (var i = 0; i < 4; i++) {
if (b1[i] != undefined && b2[i] == undefined) expanded[i] = b1[i];
if (b1[i] == undefined && b2[i] != undefined) expanded[i] = b2[i];
if (b1[i] == undefined && b2[i] == undefined) return;
}
if (b1[0] < b2[0]) expanded[0] = b1[0];
if (b1[1] > b2[1]) expanded[1] = b1[1];
if (b1[2] > b2[2]) expanded[2] = b1[2];
if (b1[3] < b2[3]) expanded[3] = b1[3];
return expanded;
};
/**
* Returns the overlapping rectangle
* of two or more rectangles.
* @author m1b
* @version 2022-07-24
* NOTE: Returns undefined if ANY
* rectangles do not intersect.
* @param {Array} arrayOfBounds - an array of bounds arrays [l, t, r, b].
* @returns {Array} - bounds array [l, t, r, b] of overlap.
*/
function intersectionOfBounds(arrayOfBounds) {
// 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;
var l = Math.max(intersection[0], b[0]),
t = Math.min(intersection[1], b[1]),
r = Math.min(intersection[2], b[2]),
b = Math.max(intersection[3], b[3]);
intersection = [l, t, r, b];
}
return intersection;
};
/**
* Returns true if the two bounds intersect.
* @author m1b
* @version 2022-10-04
* @param {Array} bounds1 - bounds array [l, t, r, b].
* @param {Array} bounds2 - bounds array [l, t, r, b].
* @param {Boolean} TLBR - whether bounds arrays are interpreted as [t, l, b, r].
* @returns {Boolean}
*/
function boundsDoIntersect(bounds1, bounds2, TLBR) {
if (TLBR === true)
// TLBR
return !(
bounds2[0] > bounds1[2]
|| bounds2[1] > bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] < bounds1[1]
);
else
// LTRB
return !(
bounds2[0] > bounds1[2]
|| bounds2[1] < bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] > bounds1[1]
);
};
})();
Copy link to clipboard
Copied
Thank you, works like a charm up to almost 99%. I found a strange behaviour I didn't expect and can't find out where the script is getting distracted. As long as its basic Shapes it's working as intended. But as soon as there is text along a path it get's distracted and doesn't offset by 1mm as programmed. The example to the right shows it. As you can see in comparison to the actual bounding-box it even offset the rectangle unevenly. Buy overlooking the script I can't see where this might occur. It just seems to be text on the circle there. If I delete that and just put text inside the circle the offset is correctly executed. You got any idea why it is behaving like this?
Copy link to clipboard
Copied
Okay the problem is that the bounds of textFrames include some "invisible" space, eg. between the cap height and the top of the em square. Same at the bottom, between the descender and the bottom of the em square. I've added a potential fix: to duplicate and outline and get the bounds of that. See what you think. I've updated the script above. - Mark
Copy link to clipboard
Copied
This version seems to work flawlessly - very impressive. Ghank you for your help. I just adjusted the settings a bit, since I need a white stroke with 0.25 and no fill. Heres my result:
(function () {
var mm = 2.834645;
// the colors of the rectangle
var black = new GrayColor(),
cyan = new CMYKColor();
white = new CMYKColor();
black.gray = 100
white.cyan = 0;
white.magenta = 0;
white.yellow = 0;
white.black = 0;
cyan.cyan = 100;
cyan.magenta = 0;
cyan.yellow = 0;
cyan.black = 0;;
// user settings:
var settings = {
margin: 1 * mm,
strokeColor: white,
strokeWidth: 0.25
};
var doc = app.activeDocument,
items = doc.selection;
if (items.length == 0) {
alert('Please make a selection and try again.');
return;
}
var lastItem = items[items.length - 1],
bounds = getItemBounds(items);
if (bounds == undefined)
return;
// add margin
bounds[0] -= settings.margin;
bounds[1] += settings.margin;
bounds[2] += settings.margin;
bounds[3] -= settings.margin;
// draw the rectangle
var rect = doc.pathItems.rectangle(bounds[1], bounds[0], bounds[2] - bounds[0], bounds[1] - bounds[3]);
rect.filled = false;
//rect.fillColor = cyan;
rect.stroked = true;
rect.strokeWidth = settings.strokeWidth;
rect.strokeColor = settings.strokeColor;
// put the rectangle behind all the items
rect.move(lastItem, ElementPlacement.PLACEAFTER);
// leave just the rectangle selected
app.selection = null;
rect.selected = true;
// finished
/**
* Returns bounds of item.
* @author m1b
* @version 2022-10-28
* Attempts to get correct bounds
* of clipped groups.
* @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 getItemBounds(item, geometric, bounds) {
var newBounds = [];
if (
item.typename == 'GroupItem'
|| item.constructor.name == 'Array'
) {
var children = item.typename == 'GroupItem' ? item.pageItems : item,
contentBounds = [];
if (
item.hasOwnProperty('clipped')
&& item.clipped == true
) {
// item is clipping group
var clipBounds;
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (
child.hasOwnProperty('clipping')
&& child.clipping == true
)
// the clipping item
clipBounds = child.geometricBounds;
else
// a clipped content item
var b = expandBounds(getItemBounds(child, geometric, bounds), contentBounds);
}
newBounds = intersectionOfBounds([clipBounds, contentBounds]);
}
else {
// item is a normal group
for (var i = 0; i < children.length; i++) {
var child = children[i];
var b = expandBounds(getItemBounds(child, geometric, bounds), contentBounds);
}
newBounds = contentBounds;
}
}
else if (item.typename == 'TextFrame') {
// item is a text frame
var dup = item.duplicate().createOutline();
newBounds = geometric ? dup.geometricBounds : dup.visibleBounds;
dup.remove();
}
else {
// item is not clipping group
newBounds = geometric ? item.geometricBounds : item.visibleBounds;
}
bounds = (bounds == undefined)
? bounds = newBounds
: bounds = expandBounds(newBounds, bounds);
return bounds;
};
/**
* Returns bounds that encompass both bounds.
* @author m1b
* @version 2022-07-24
**
* @Param {Array} b1 - bounds array [l, t, r, b].
* @Param {Array} b2 - bounds array [l, t, r, b].
* @Returns {Array} - the encompassing bounds.
*/
function expandBounds(b1, b2) {
var expanded = b2;
for (var i = 0; i < 4; i++) {
if (b1[i] != undefined && b2[i] == undefined) expanded[i] = b1[i];
if (b1[i] == undefined && b2[i] != undefined) expanded[i] = b2[i];
if (b1[i] == undefined && b2[i] == undefined) return;
}
if (b1[0] < b2[0]) expanded[0] = b1[0];
if (b1[1] > b2[1]) expanded[1] = b1[1];
if (b1[2] > b2[2]) expanded[2] = b1[2];
if (b1[3] < b2[3]) expanded[3] = b1[3];
return expanded;
};
/**
* Returns the overlapping rectangle
* of two or more rectangles.
* @author m1b
* @version 2022-07-24
* NOTE: Returns undefined if ANY
* rectangles do not intersect.
* @Param {Array} arrayOfBounds - an array of bounds arrays [l, t, r, b].
* @Returns {Array} - bounds array [l, t, r, b] of overlap.
*/
function intersectionOfBounds(arrayOfBounds) {
// 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;
var l = Math.max(intersection[0], b[0]),
t = Math.min(intersection[1], b[1]),
r = Math.min(intersection[2], b[2]),
b = Math.max(intersection[3], b[3]);
intersection = [l, t, r, b];
}
return intersection;
};
/**
* Returns true if the two bounds intersect.
* @author m1b
* @version 2022-10-04
* @Param {Array} bounds1 - bounds array [l, t, r, b].
* @Param {Array} bounds2 - bounds array [l, t, r, b].
* @Param {Boolean} TLBR - whether bounds arrays are interpreted as [t, l, b, r].
* @Returns {Boolean}
*/
function boundsDoIntersect(bounds1, bounds2, TLBR) {
if (TLBR === true)
// TLBR
return !(
bounds2[0] > bounds1[2]
|| bounds2[1] > bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] < bounds1[1]
);
else
// LTRB
return !(
bounds2[0] > bounds1[2]
|| bounds2[1] < bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] > bounds1[1]
);
};
})();
So I basically added white as CMYK color, couldn't find an easier way, but thats ok I guess, set the stroke Color to the newly defined white, changed rect.filled to false and out-commented the rect.fillColor. The result is now just as I wanted it. If you got any further improvements or if I did something wrong with my changes, feel free to correct me. Unfortunately I'm only able to read and understand code, but writing it myself is a thing I haven't managed to truly learn for years... But right now I'm pretty happy that you helped me out on this one, so I don't have to do this dull task over and over again by hand 🙂 You really saved my day, thank you very, very much!
Copy link to clipboard
Copied
You're welcome! The changes you made look good to me.
Copy link to clipboard
Copied
I am looking similer script, I want to add rectangle around each object group indivisually at once, based on defined margin through the prompt dialog. is that possible?
Copy link to clipboard
Copied
Thank you Doug A Roberts
I got what I was looking
Copy link to clipboard
Copied
Here's a modified version that asks for margin value and puts rectangle behind each item. - Mark
/**
* Draws bounding rectangle behind each item.
* @discussion https://community.adobe.com/t5/illustrator-discussions/creating-a-rectangle-around-multiple-objects-with-a-script/m-p/13300468
*/
(function () {
// the colors of the rectangle
var black = new GrayColor(),
cyan = new CMYKColor();
black.gray = 100
cyan.cyan = 100;
cyan.magenta = 0;
cyan.yellow = 0;
cyan.black = 0;
var defaultMargin = '1 mm',
margin = prompt('Draw Rectangles Around Items\nEnter Margin:', defaultMargin);
if (margin == undefined)
return;
// user settings:
var settings = {
margin: stringToPoints(margin) || stringToPoints(defaultMargin),
strokeColor: black,
strokeWidth: 1
};
var doc = app.activeDocument,
items = doc.selection;
if (items.length == 0) {
alert('Please make a selection and try again.');
return;
}
for (var i = 0; i < items.length; i++) {
var bounds = getItemBounds(items[i]);
if (bounds == undefined)
return;
// add margin
bounds[0] -= settings.margin;
bounds[1] += settings.margin;
bounds[2] += settings.margin;
bounds[3] -= settings.margin;
// draw the rectangle
var rect = doc.pathItems.rectangle(bounds[1], bounds[0], bounds[2] - bounds[0], bounds[1] - bounds[3]);
rect.filled = true;
rect.fillColor = cyan;
rect.stroked = true;
rect.strokeWidth = settings.strokeWidth;
rect.strokeColor = settings.strokeColor;
// put the rectangle behind all the items
rect.move(items[i], ElementPlacement.PLACEAFTER);
}
// leave just the rectangle selected
app.selection = null;
rect.selected = true;
// finished
/**
* Converts a string measurement value,
* to a value in points, for example:
* '1.5 mm' --> 4.2519675.
* @author m1b
* @version 2022-10-29
* @Param {String} str - the string to convert, eg. '1mm'.
* @Returns {Number} - the value in points.
*/
function stringToPoints(str) {
var n;
if (/\s*[\d\.]+\s*(mm|millimeters)/.test(str))
n = parseFloat(str) * 2.834645;
else if (/\s*[\d\.]+\s*(cm|centimeters)/.test(str))
n = parseFloat(str) * mm * 28.34645;
else if (/\s*[\d\.]+\s*("|in)/.test(str))
n = parseFloat(str) * 72;
else
n = parseFloat(str);
if (n === n)
return n;
};
/**
* Returns bounds of item(s).
* @author m1b
* @version 2022-10-28
* Attempts to get correct bounds
* of clipped groups.
* @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 getItemBounds(item, geometric, bounds) {
var newBounds = [];
if (
item.typename == 'GroupItem'
|| item.constructor.name == 'Array'
) {
var children = item.typename == 'GroupItem' ? item.pageItems : item,
contentBounds = [];
if (
item.hasOwnProperty('clipped')
&& item.clipped == true
) {
// item is clipping group
var clipBounds;
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (
child.hasOwnProperty('clipping')
&& child.clipping == true
)
// the clipping item
clipBounds = child.geometricBounds;
else
// a clipped content item
var b = expandBounds(getItemBounds(child, geometric, bounds), contentBounds);
}
newBounds = intersectionOfBounds([clipBounds, contentBounds]);
}
else {
// item is a normal group
for (var i = 0; i < children.length; i++) {
var child = children[i];
var b = expandBounds(getItemBounds(child, geometric, bounds), contentBounds);
}
newBounds = contentBounds;
}
}
else if (item.typename == 'TextFrame') {
// item is a text frame
var dup = item.duplicate().createOutline();
newBounds = geometric ? dup.geometricBounds : dup.visibleBounds;
dup.remove();
}
else {
// item is not clipping group
newBounds = geometric ? item.geometricBounds : item.visibleBounds;
}
bounds = (bounds == undefined)
? bounds = newBounds
: bounds = expandBounds(newBounds, bounds);
return bounds;
};
/**
* Returns bounds that encompass both bounds.
* @author m1b
* @version 2022-07-24
**
* @Param {Array} b1 - bounds array [l, t, r, b].
* @Param {Array} b2 - bounds array [l, t, r, b].
* @Returns {Array} - the encompassing bounds.
*/
function expandBounds(b1, b2) {
var expanded = b2;
for (var i = 0; i < 4; i++) {
if (b1[i] != undefined && b2[i] == undefined) expanded[i] = b1[i];
if (b1[i] == undefined && b2[i] != undefined) expanded[i] = b2[i];
if (b1[i] == undefined && b2[i] == undefined) return;
}
if (b1[0] < b2[0]) expanded[0] = b1[0];
if (b1[1] > b2[1]) expanded[1] = b1[1];
if (b1[2] > b2[2]) expanded[2] = b1[2];
if (b1[3] < b2[3]) expanded[3] = b1[3];
return expanded;
};
/**
* Returns the overlapping rectangle
* of two or more rectangles.
* @author m1b
* @version 2022-07-24
* NOTE: Returns undefined if ANY
* rectangles do not intersect.
* @Param {Array} arrayOfBounds - an array of bounds arrays [l, t, r, b].
* @Returns {Array} - bounds array [l, t, r, b] of overlap.
*/
function intersectionOfBounds(arrayOfBounds) {
// 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;
var l = Math.max(intersection[0], b[0]),
t = Math.min(intersection[1], b[1]),
r = Math.min(intersection[2], b[2]),
b = Math.max(intersection[3], b[3]);
intersection = [l, t, r, b];
}
return intersection;
};
/**
* Returns true if the two bounds intersect.
* @author m1b
* @version 2022-10-04
* @Param {Array} bounds1 - bounds array [l, t, r, b].
* @Param {Array} bounds2 - bounds array [l, t, r, b].
* @Param {Boolean} TLBR - whether bounds arrays are interpreted as [t, l, b, r].
* @Returns {Boolean}
*/
function boundsDoIntersect(bounds1, bounds2, TLBR) {
if (TLBR === true)
// TLBR
return !(
bounds2[0] > bounds1[2]
|| bounds2[1] > bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] < bounds1[1]
);
else
// LTRB
return !(
bounds2[0] > bounds1[2]
|| bounds2[1] < bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] > bounds1[1]
);
};
})();
Copy link to clipboard
Copied
Another scripting approach with a pretty versatile dialog would be this one, provided by Sergey Anosov. It still works in recent Illustrator versions.