m1b
Community Expert
m1b
Community Expert
Activity
‎Feb 17, 2025
04:03 PM
Oh sorry @vectora98504848 I somehow didn't copy half of the script! I have fixed it now. - Mark
... View more
‎Feb 17, 2025
06:08 AM
Okay @vectora98504848 here is the script again, now with René's improved function. Let us know how it goes!
- Mark
/**
* @file Unrotate Selected Items 2.js
*
* Makes an attempt to remove any recorded rotation
* that can be derived from the selected items.
* Finds rotation either in the item's BBAccumRotation
* tag or the item's matrix.
*
* @author m1b
* @version 2025-02-17
* @discussion https://community.adobe.com/t5/illustrator-discussions/script-to-return-or-restore-rotation-of-multiple-selected-objects/m-p/15146692
*/
(function () {
var doc = app.activeDocument;
var items = getItems({
from: doc.selection,
getPageItems: true,
getGroupItems: true,
});
if (0 === items.length)
return alert('Please select some items to rotate and try again.');
for (var i = 0; i < items.length; i++)
unrotate2(items[i]);
})();
/**
* Attempt to return a page item to unrotated state.
* Modified by renél80416020 to apply rotation
* to group items and compound path items correctly.
* Important: will only work if the item has a valid
* BBAccumRotation tag or a matrix that is ammenable
* to derivation of the rotation.
* @author m1b and renél80416020
* @version 2025-02-14
* @param {PageItem} item - the item to unrotate.
* @return {Number} - the amount of rotation, in degrees.
*/
function unrotate2(item) {
var grp = false, trs = false;
var accumRotation = undefined,
rotationTag;
if (item.hasOwnProperty('tags')) {
if (item.typename == "GroupItem" || item.typename == "CompoundPathItem") {
if (item.typename == "CompoundPathItem") {
rotationTag = getThing(item.pathItems[0].tags, 'name', 'BBAccumRotation');
trs = true;
}
else {
rotationTag = getThing(item.pageItems[0].tags, 'name', 'BBAccumRotation');
grp = true;
}
}
else rotationTag = getThing(item.tags, 'name', 'BBAccumRotation');
if (rotationTag)
accumRotation = rotationTag.value * 180 / Math.PI;
}
if (
undefined == accumRotation
&& item.hasOwnProperty('matrix')
)
// derive rotation from the matrix
accumRotation = getRotationFromMatrix(item.matrix);
if (undefined == accumRotation)
return;
// rotate the item
item.rotate(- accumRotation);
if (rotationTag) {
// update tag
var pi;
if (grp)
for (var i = 0; i < item.pageItems.length; i++) {
pi = item.pageItems[i];
rotationTag = getThing(pi.tags, 'name', 'BBAccumRotation');
if (rotationTag != undefined) { rotationTag.value = 0; }
}
if (trs) {
pi = item.pathItems[0];
rotationTag = getThing(pi.tags, 'name', 'BBAccumRotation');
if (rotationTag != undefined) { rotationTag.value = 0; }
rotationTag = getThing(item.tags, 'name', 'BBAccumRotation');
if (rotationTag != undefined) { rotationTag.value = 0; }
}
if (!trs && !grp)
rotationTag.value = 0;
}
};
/**
* 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 the rotation amount, in degrees,
* of the given (not skewed) matrix.
* @author m1b
* @version 2023-10-20
* @param {Matrix} matrix - an Illustrator Matrix.
* @returns {Number}
*/
function getRotationFromMatrix(matrix) {
if (!matrix.hasOwnProperty('mValueA'))
throw new Error('getRotationFromMatrix: bad `matrix` supplied.');
// scaling factors
var scaleX = Math.sqrt(matrix.mValueA * matrix.mValueA + matrix.mValueC * matrix.mValueC);
// scaleY = Math.sqrt(matrix.mValueB * matrix.mValueB + matrix.mValueD * matrix.mValueD);
// rotation angle
var radians = Math.acos(matrix.mValueA / scaleX),
degrees = radians * (180 / Math.PI);
return Math.round(degrees * 1000) / 1000;
};
/** ------------------------------------------------------------------- *
* GET ITEMS *
* -------------------------------------------------------------------- *
* @author m1b *
* @version 2024-03-01 *
* -------------------------------------------------------------------- *
* Collects page items from a `from` source, eg. a Document, Layer, *
* GroupItem, or Array. Will look inside group items up to `maxDepth`. *
* Search can be filtered using `filter` function. Note that the *
* filter function is evaluated last in the filtering process. *
* -------------------------------------------------------------------- *
* Example 1. Get all items in document: *
* *
* var myItems = getItems({ from: app.activeDocument }); *
* *
* -------------------------------------------------------------------- *
* Example 2. Get all selected items except groups: *
* *
* var myItems = getItems({ *
* from: app.activeDocument.selection, *
* getGroupItems: false, *
* }); *
* *
* -------------------------------------------------------------------- *
* Example 3. Using `filter` function to choose item type: *
* *
* var myItems = getItems({ *
* from: app.activeDocument, *
* filter: function (item) { *
* return ( *
* 'PathItem' === item.typename *
* || 'CompoundPathItem' === item.typename *
* ); *
* } *
* }); *
* *
* -------------------------------------------------------------------- *
* Example 4. Using `filter` function: *
* *
* var myItems = getItems({ *
* from: app.activeDocument, *
* filter: onlyPngLinks *
* }); *
* *
* function onlyPngLinks(item, depth) { *
* return ( *
* 'PlacedItem' === item.typename *
* && '.png' === item.file.name.slice(-4).toLowerCase() *
* ); *
* }; *
* *
* -------------------------------------------------------------------- *
* Example 4. Using the `filter` function for custom collecting: *
* *
* This example bypasses the normal returned array and instead *
* captures items in an "external" array `itemsByDepth`. *
* *
* var itemsByDepth = []; *
* *
* function getItemsByDepth(item, depth) { *
* if (undefined == itemsByDepth[depth]) *
* itemsByDepth[depth] = []; *
* itemsByDepth[depth].push(item); *
* }; *
* *
* getItems({ *
* from: app.activeDocument, *
* filter: getItemsByDepth *
* }); *
* *
* -------------------------------------------------------------------- *
* @param {Object} options - parameters
* @param {PageItem|Array<PageItem>|Document|Layer} options.from - the thing(s) to look in, eg. a selection.
* @param {Function} [options.filter] - function that, given a found item, must return true (default: no filtering).
* @param {Boolean} [options.getPageItems] - whether to include page items in returned items (default: true).
* @param {Boolean} [options.getGroupItems] - whether to include GroupItems in returned items (default: true).
* @param {Boolean} [options.getLayers] - whether to include Layers in returned items (default: false).
* @param {Boolean} [options.getHiddenItems] - whether to include hidden items in returned items (default: true).
* @param {Boolean} [options.getLockedItems] - whether to include locked items in returned items (default: true).
* @param {Boolean} [options.getGuideItems] - whether to include guide items in returned items (default: false).
* @param {Number} [options.maxDepth] - deepest folder level (recursion depth limit) (default: 99).
* @param {Boolean} [options.returnFirstMatch] - whether to return only the first found item (default: false).
* @param {Number} [depth] - the current depth (private).
* @returns {Array|PageItem} - all the found items in a flat array, or the first found item if `returnFirstMatch`.
*/
function getItems(options, depth) {
// defaults
options = options || {};
var found = [],
depth = depth || 0,
items = options.from;
if (!options.initialized)
// once-off initialization
if (!initialize())
return [];
itemsLoop:
for (var i = 0, item, len = items.length; i < len; i++) {
item = items[i];
if (
false === excludeFilter(item)
&& true === includeFilter(item)
) {
// item found!
found.push(item);
if (options.returnFirstMatch)
break itemsLoop;
}
if (
'GroupItem' !== item.constructor.name
&& 'Layer' !== item.typename
)
// only items with children from here
continue itemsLoop;
if (
excludeHidden(item)
|| excludeLocked(item)
)
// don't look into excluded containers
continue itemsLoop;
if (depth >= options.maxDepth)
// don't go deeper
continue itemsLoop;
// set up for the next depth
options.from = item.pageItems;
// look inside
found = found.concat(getItems(options, depth + 1));
}
// this level done
if (true == options.returnFirstMatch)
return found[0];
else
return found;
/**
* Returns true when the item should be not be found.
* @param {PageItem|Layer} item
* @returns {Boolean}
*/
function excludeFilter(item) {
return (
isAlreadyFound(item)
// is hidden
|| excludeHidden(item)
// is locked
|| excludeLocked(item)
// is guide
|| (
false === options.getGuideItems
&& true === item.guides
)
// is layer
|| (
false === options.getLayers
&& 'Layer' === item.typename
)
// is group item
|| (
false === options.getGroupItems
&& 'GroupItem' === item.typename
)
// is page item
|| (
false === options.getPageItems
&& 'GroupItem' !== item.typename
&& undefined != item.uuid
)
);
};
/**
* Returns true when the item should be included.
* @param {PageItem|Layer} item
* @returns {Boolean}
*/
function includeFilter(item) {
return (
undefined == options.filter
|| options.filter(item, depth)
);
};
/**
* Returns true when the item should
* be excluded because it is hidden.
* @param {PageItem|Layer} item
* @returns {Boolean}
*/
function excludeHidden(item) {
return (
false === options.getHiddenItems
&& (
true === item.hidden
|| false === item.visible
)
);
};
/**
* Returns true when the item should
* be excluded because it is locked.
* @param {PageItem|Layer} item
* @returns {Boolean}
*/
function excludeLocked(item) {
return (
false === options.getLockedItems
&& true === item.locked
);
};
/**
* Returns true if item was already
* found, and marks item as found,
* to avoid finding same item twice.
* @param {PageItem|Layer} item
* @returns {Boolean}
*/
function isAlreadyFound(item) {
var uuid = item.hasOwnProperty('uuid')
? item.uuid
: item.typename + item.zOrderPosition,
isFound = !!options.isFound[uuid];
options.isFound[uuid] = true;
return isFound;
}
/**
* Returns the initialised `options` object.
* @returns {Object}
*/
function initialize() {
// make a new object, so we don't pollute the original
options = {
initialized: true,
depth: 0,
isFound: {},
filter: options.filter,
getPageItems: false !== options.getPageItems,
getGroupItems: false !== options.getGroupItems,
getLayers: true === options.getLayers,
getHiddenItems: false !== options.getHiddenItems,
getLockedItems: false !== options.getLockedItems,
getGuideItems: true === options.getGuideItems,
maxDepth: options.maxDepth,
returnFirstMatch: options.returnFirstMatch,
};
if (
undefined == options.maxDepth
|| !options.maxDepth instanceof Number
)
options.maxDepth = 99;
// items is a single layer
if ('Layer' === items.typename)
items = [items];
// items is a document
else if ('Document' === items.constructor.name) {
var layers = items.layers;
items = [];
for (var i = 0; i < layers.length; i++)
items.push(layers[i]);
}
else if ('Array' !== items.constructor.name)
items = [items];
return items.length > 0;
};
};
... View more
‎Feb 17, 2025
02:35 AM
@Marc Autret, what fun! Beautiful!
- Mark
... View more
‎Feb 17, 2025
02:25 AM
Thanks for letting us know, Olivier. All the best.
... View more
‎Feb 16, 2025
06:09 AM
Hi @Peter Kahrel I am conscious that you have given this your last shot, but (sorry!) I do seem to want to comment on a couple of things. 😬
> ... and some people do, not only because it's more flexible, but also because it's less typing and you don't have to wonder whether the < comes before the = or the other way around.
Totally valid point, and gives a good reason to use \K over a lookbehind. It is not a valid reason to *call* it a lookbehind, however.
> As to your comparison chart, you can phrase thing any way to suit a purpose. ...
Yes! And that's my point: I prefer to use phrases—where pedagogically appropriate—to correspond to how it actually is. I mean, your hypothetical phrasing isn't remotely correct—on the right, the entire matched contents is *never* discarded and the "apple" in the lookbehind is never captured. On these two points the \K performs the exact opposite to a positive lookbehind!
Anyway, while I might enjoy talking about this kind of minutiae when it takes my fancy, I don't expect others to be similarly moved—except maybe Robert 😉 I see you there!—so I fully understand if you want to leave this where it is.
- Mark
... View more
‎Feb 16, 2025
05:52 AM
Oh boy, Robert, perhaps I am just not correcty parsing your conversational style here. It seems to me that you have asked several closed-ended questions to which I answered immediately. I don't remember telling you to get lost.
Perhaps it is the difference between your post
> Or it all works as intended - and I just misunderstood it?
and Peter's post
> I don't see why you wouldn't want to call \K a lookbehind. Like the classic lookbehind, it finds things if they're preceded by a certain pattern. Lookbehind is a functional notion, not a formal one.
Can you see the difference? I didn't even know how to answer your question, whereas Peter's reply—which wasn't even a question—not only clarified his thoughts on the topic somewhat, but also gave me something concrete to respond-to, and I responded at considerable length, for no other reasons than (a) I wanted Peter and other readers to understand where I was coming from, (b) I wanted to explore a subtle philosophical concept, and (c) I wanted to practice writing and presenting a topic in a helpful way (and yes, yes, I know I did a poor job—but I've done worse before so I am not totally unhappy). A long, detailed response does not mean the topic is "a big deal"—it might be just interesting, or whatever.
Also, Peter is not being weirdly combative, which I appreciate.
- Mark
... View more
‎Feb 15, 2025
11:37 PM
@renél80416020 Wonderful! Thanks René! I'll add it in when I get a chance and OP can test it out. I didn't remember that compound path items also don't have BBAccumRotation tags, so that's for including that logic!
- Mark
... View more
‎Feb 15, 2025
11:27 PM
Hi @j.khakase can you share a sample file? There are particular difficulties with your question based on exactly how the text frames are styled. I'd be happy to have a look at it and let you know if it's possible.
- Mark
... View more
‎Feb 15, 2025
11:22 PM
Hi @Fuland111, one approach is to create the artwork in a separate document—eg. sized to fit your L tshirt size—and then place that .ai document three times into your output document and just scale down for the M and S tshirts.
Then, next time, when you have a new tshirt to make, duplicate that artwork and output file, edit the artwork to your new design (but—important—keep it the same size) and then relink the three placed images to the new artwork file. A that point it will update, and all the sizes will be correct, ready to print.
By the way, scripting this is also possible, but it is a complex topic and involves a lot of time, not least to just understand *exactly* what you want. If you want to go that route, you should contact a skilled scripter with a budget in mind.
- Mark
... View more
‎Feb 15, 2025
11:06 PM
Hi @Moiz5FB2, well done on getting your script to work!
Just for your learning here is your same script but I've adjusted a few things to how I like them, and also used Robert's idea of using the `geometricBounds` to set the size. Using the `resize` method is just fine, too—this is just showing another way.
- Mark
const mm = 2.834645;
function main() {
var item = app.selection[0];
var lineCount = prompt("Please set type a line number (1 to 4)", "1");
if (!lineCount)
return;
setHeightByLineCount(item, lineCount);
};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Set Frame Height');
function setHeightByLineCount(item, lineCount) {
app.scriptPreferences.measurementUnit = MeasurementUnits.POINTS;
lineCount = Number(lineCount);
if (isNaN(lineCount))
return;
// get height value, in mm
// depending on line count
var height = [
0, // 0 lines
206.6, // 1 lines
213.0, // 2 lines
219.5, // 3 lines
225.7, // 4 lines
231.9, // 5 lines
][lineCount] * mm;
if (!height)
return;
// change height only using `geometricBounds` property:
item.geometricBounds = [
item.geometricBounds[0],
item.geometricBounds[1],
item.geometricBounds[0] + height,
item.geometricBounds[3],
];
};
... View more
‎Feb 15, 2025
08:59 PM
I appreciate your comments @James Gifford—NitroPress and I suspect that you have the type of mind that will appreciate the details here.
> ... that GREP, in the larger universe of systems and coding, seems more extensive and not entirely congruent with the version embedded in InDesign. Is the debate here, and disagreement on places like SE, based on that larger scope and variant function, rather than on how ID implements this function?
Great question! This particular issue—the issue of how to tell people what \K is or does—has nothing to do with the "flavour" of grep (in this case PCRE) although there will be small version differences in different environments. I think the question only arises because of the way Indesign uses the engine, as I speculate in my post, in such a way that a positive lookbehind and \K produce the same results, almost always.
After reading this thread and writing my post I realise that core question for me is: is this the right place to employ a lie-to-children? There is a good case to be made for it given the unusual situation specific to grep in Indesign. I suspect Peter thinks yes; I think no. And I don't care—both positions are perfectly reasonable—it only came up because I had trouble parsing your cheat sheet in that small particular. 🙂
- Mark
... View more
‎Feb 15, 2025
08:39 PM
1 Upvote
Hi @Peter Kahrel, this is actually a quite interesting topic—in an incredibly niche area—and I think, as technical writers, philosophy may come into it too.
Of course I am in no way implying that you—Peter—don't know any of this, and this reply to your post is also for James' and any other readers' benefits, and to be honest I find this kind of exercise interesting anyway. And, importantly, the mock-dialogues I use are abstract ideas that are me trying to convey my ideas; they make no reference to yourself, or anything you've said or written—or anybody else. And if I have created a strawman in the first dialog below, please forgive me, it is just for illustration and I did it on purpose.
To finish this preamble, I reiterate what we have both agreed: all this is no big deal! 🙂
The reason it is no big deal is that in Indesign, positive lookbehind and \K both give—almost always—the same results.
The only practical difference that I can see between using \K and a positive lookbehind is that \K effectively allows variable length matches. To make that clear, with a lookbehind you can do this:
(?<=apple|grape)\d+
but not this
(?<=apple|banana)\d+
because the engine doesn't know how long the lookbehind result will be—could be 5, or 6, characters long.
So instead you can do
apple|banana\K\d+
or maybe even
\D+\K\d+
So, that's it. Finished. They both do the same thing in Indesign—so let's call them both "lookbehinds". Okay, fair.
So why do I bother mentioning it? It is philosophical. Consider the following dialogue:
Dialogue between Indesign student and teacher:
Student: "My positive lookbehind failed."
Teacher: "It is because you are defining a variable-length expression in the lookbehind which isn't supported by the grep engine that Indesign uses. You need to use \K instead."
Student: "What is \K?"
Teacher: "It is also a positive lookbehind but it allows you to use a variable length pattern."
Student: "Well why don't we *always* use \K?"
Teacher: "Um, well, yeah okay why not? Let's just use \K whenever we need a postive lookbehind. Good point."
(Teacher is thinking, correctly: there is literally no reason to ever teach (?<= ) to Indesign students. Huh.)
Student: "Okay, but my grep still isn't working."
Teacher: "Oh, you don't need parenthesis or any other symbols. Just use \K after the pattern you want to match."
Student: "Ah got it, that works. So negative lookbehind is (?<!my pattern here) and positive lookbehind is my pattern here\K. Got it."
Teacher: "Um, yeah... Yes."
Now consider making some changes to that dialogue:
Teacher: " ... You need to use \K instead."
Student: "What is \K?"
Teacher: "It causes any matched content up to that point to be discarded. Imagine that each character that is matched is collected in a bucket, one-by-one, but when \K comes along, the bucket is emptied and it will start to fill again if more of the grep—after the \K—is matched."
Student: "So it's like empty the bucket."
Teacher: "Sure! Whatever has been matched is emptied out at that point and is gone. Yeah, we use \K instead of a positive lookbehind because lookbehinds have that limitation that you found earlier."
Student: "Okay got it."
The first dialogue made no attempt at mnemonics, but just for fun, the following addendum to this dialogue is possible:
Student: "Why is it the letter K? That's annoying to remember."
Teacher: "I guess the better symbols were already taken when this feature was added to the grep engine. Many people call it Keep Out which is a bit awkward, but I guess it means keep the bucket results out of the final results."
Student: "Fine."
You will notice that adding that mnemonic exercise to the end of the first dialogue will just confuse the student.
Okay, back to my philosophical point: I much prefer the second dialog because (a) it is factually correct, describing what the PCRE engine actually does, (b) it makes no strong connection to the positive lookbehind (see the sentence I've underlined), and therefore doesn't introduce the additional cognitive load of so we should never use (?<= ) but \K is the same but better; instead it introduces \K as just another symbol that does a distinct operation, and finally (c) this dialogue matches the wider reality, so the hypothetical student could go on to learn, say, perl, and their grep knowledge would already be compatible, assuming the engines were the same.
That's my thoughts on the matter. Sorry I didn't know how to make this post shorter—or didn't have time to make it shorter, haha. As this reply relates specifically to my first reading of James' handy cheat sheet, I will remind you all that my actual experience was to have no idea what "(Inclusive) lookbehind" and the asterisked remark was talking about next to \K. My experience might be atypical, and what you have written may resonate better on average with users. I don't know.
It was quite fun to think about all this, but ... one last time(!) ... no big deal. 🙂
- Mark
P.S. for the visually inclined, here is a comparison character-by-character as the PCRE state machine traverses the string, left-to-right comparing \K to positive lookbehind:
Note: the whole discussion here is a *very* high-level view of the topic. In no way does it reflect lower-level implentation details, which will relate to hideously complicated performance optimization etc.
Edit: very minor typos.
Edit: removed wrong example from my poor memory.
Edit 2025-02-19: improved Comparison diagram for clarity, to show current token being evaluated. Added disclaimer note.
... View more
‎Feb 15, 2025
04:11 PM
1 Upvote
Thanks @Tá´€W, I didn't know about those methods. Nice!
- Mark
... View more
‎Feb 15, 2025
06:11 AM
I don't know if you misunderstood anything because you haven't made any claims.
... View more
‎Feb 15, 2025
06:10 AM
@Robert at ID-Tasker please don't make a huge deal out of this—it really isn't. I was only observing in James' cheat sheet that a better explanation would be helpful to understand \K.
By the way, the stackexchange link agrees with me 100%, and again uses the word "keep" which doesn't help the mental picture of what \K actually is:
> There is a special form of this construct, called \K (available since Perl 5.10.0), which causes the regex engine to "keep" everything it had matched prior to the \K and not include it in $& .
Maybe you are being confused by the fact that (as per the same stackoverflow link) This effectively provides non-experimental variable-length lookbehind of any length.
So, again—no big deal—but \K is not a lookbehind, despite that it effectively provides the same result. It is about the mental model and the ease of remembering. I noted that James' current wording was difficult to understand and it is up to him whether he agrees with me, or even cares.
And one last time, Robert... please... this is no big deal.
- Mark
... View more
‎Feb 15, 2025
05:50 AM
Yes Robert, I'm talking about \K. The first two links you posted give wrong information. It is not a "lookbehind" of any kind. The third post was correct.
But here's a more straightforward explanation, from regexr.com (my highlight in magenta rectangle):
- Mark
... View more
‎Feb 15, 2025
04:38 AM
Another idea is to set the size to match the master before applying it — something like:
page.bounds = master.bounds:
- Mark
... View more
‎Feb 15, 2025
03:40 AM
Hi @James Gifford—NitroPress, thanks for posting this. I think it's great!
I'd make one observation: that the explanation for \K is surprisingly difficult to understand for me. A clearer description, in my opinion, would be "Reset Match" or something like that. The K is usually referred to as "keep out" but that doesn't help my mental model because what it actually does is discard the current match and start again. In fact "discard" might be a good word to use. Anyway, with that in mind, the explanation via the asterisk is quite opaque, at least to me.
Otherwise, thanks for sharing.
- Mark
Edit 2025-02-16: changed "keep" to "keep out" which I hadn't remembered fully (probably due to it not really being a good explanation for what it actually does! 😛 ).
... View more
‎Feb 15, 2025
02:02 AM
Hi @Olivier Beltrami, I don't know the answer, but does it help to insert near the start of the script:
app.scriptPreferences.userInteractionLevel = UserInteractionLevels.NEVER_INTERACT;
I wondered if this might avoid the dialog and choose a default behaviour?
Otherwise, if you don't get a good answer here, please post a simple demo .indd document so we can test with it.
- Mark
... View more
‎Feb 13, 2025
06:56 PM
Hi @vectora98504848 @yep that's what I meant in my P.S. above. Sadly groups don't seem to have rotation tags or any way to derive a rotation value.
What do you think of the following idea: that for every group (a clipping group is also a group by the way) I read the rotation of the first path item and then unrotate the group by that amount, not doing anything to the contents beyond that? When I get a chance I'll try it out.
- Mark
... View more
‎Feb 13, 2025
02:06 PM
Oh yes you are right! In that case I need to be a bit more sophisticated about getting the selected objects...
I have updated the script above. Please try it again.
- Mark
P.S. Note that Illustrator does not assign rotation values to GroupItems, per se, so the contents of the groups will (in most cases!) unrotate, but not the groups themselves.
... View more
‎Feb 13, 2025
05:31 AM
Hi @dublove, this is a weird case—when the selection is a range of cells, app.selection[0] gives you a [Cell] object, but it isn't a normal Cell, it contains all the cells in it's `cells` property, as Robert mentioned. It is actually a similar object that you get when you make an itemByRange collection, so that you can do something like this:
app.activeDocument.selection[0].contents = '-';
and it will put text in every selected Cell.
But for most purposes, getting the selection's cells like Robert said, is best.
- Mark
... View more
‎Feb 13, 2025
02:35 AM
Hi @vectora98504848, to "unrotate" something—back to zero degrees rotation—means you must first derive the item's rotation amount. In Illustrator, page items don't have an absolute rotation amount, however it does (usually!) keep track of rotations in a "BBAccumRotation" tag assigned to the item. The other possibility is if the item is a placed image, or a text frame, then it may also have a matrix, and sometimes it is possible to derive a rotation value from that.
I've written a script to derive the rotation, and then unrotate the selected items. Let me know how it goes, but bear in mind that there will be certain objects for which a rotation value cannot be determined in this way. There are other approaches to the problem, but they are not general—we must know more about the type of item.
- Mark
/**
* @file Unrotate Selected Items.js
*
* Makes an attempt to remove any recorded rotation
* that can be derived from the selected items.
* Finds rotation either in the item's BBAccumRotation
* tag or the item's matrix.
*
* @author m1b
* @version 2025-02-14
* @discussion https://community.adobe.com/t5/illustrator-discussions/script-to-return-or-restore-rotation-of-multiple-selected-objects/m-p/15146692
*/
(function () {
var doc = app.activeDocument;
var items = getItems({
from: doc.selection,
getPageItems: true,
getGroupItems: true,
});
if (0 === items.length)
return alert('Please select some items to rotate and try again.');
for (var i = 0; i < items.length; i++)
unrotate(items[i]);
})();
/**
* Attempt to return a page item to unrotated state.
* Important: will only work if the item has a valid
* BBAccumRotation tag or a matrix that is ammenable
* to derivation of the rotation.
* @author m1b
* @version 2025-02-13
* @param {PageItem} item - the item to unrotate.
* @return {Number} - the amount of rotation, in degrees.
*/
function unrotate(item) {
var accumRotation = undefined,
rotationTag;
if (item.hasOwnProperty('tags')) {
// derive rotation from the BBAccumRotation tag
rotationTag = getThing(item.tags, 'name', 'BBAccumRotation');
if (rotationTag)
accumRotation = rotationTag.value * 180 / Math.PI;
}
if (
undefined == accumRotation
&& item.hasOwnProperty('matrix')
)
// derive rotation from the matrix
accumRotation = getRotationFromMatrix(item.matrix);
if (undefined == accumRotation)
return;
// rotate the item
item.rotate(- accumRotation);
if (rotationTag)
// update tag
rotationTag.value = 0;
return accumRotation;
};
/**
* 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 the rotation amount, in degrees,
* of the given (not skewed) matrix.
* @author m1b
* @version 2023-10-20
* @param {Matrix} matrix - an Illustrator Matrix.
* @returns {Number}
*/
function getRotationFromMatrix(matrix) {
if (!matrix.hasOwnProperty('mValueA'))
throw new Error('getRotationFromMatrix: bad `matrix` supplied.');
// scaling factors
var scaleX = Math.sqrt(matrix.mValueA * matrix.mValueA + matrix.mValueC * matrix.mValueC);
// scaleY = Math.sqrt(matrix.mValueB * matrix.mValueB + matrix.mValueD * matrix.mValueD);
// rotation angle
var radians = Math.acos(matrix.mValueA / scaleX),
degrees = radians * (180 / Math.PI);
return Math.round(degrees * 1000) / 1000;
};
/** ------------------------------------------------------------------- *
* GET ITEMS *
* -------------------------------------------------------------------- *
* @author m1b *
* @version 2024-03-01 *
* -------------------------------------------------------------------- *
* Collects page items from a `from` source, eg. a Document, Layer, *
* GroupItem, or Array. Will look inside group items up to `maxDepth`. *
* Search can be filtered using `filter` function. Note that the *
* filter function is evaluated last in the filtering process. *
* -------------------------------------------------------------------- *
* Example 1. Get all items in document: *
* *
* var myItems = getItems({ from: app.activeDocument }); *
* *
* -------------------------------------------------------------------- *
* Example 2. Get all selected items except groups: *
* *
* var myItems = getItems({ *
* from: app.activeDocument.selection, *
* getGroupItems: false, *
* }); *
* *
* -------------------------------------------------------------------- *
* Example 3. Using `filter` function to choose item type: *
* *
* var myItems = getItems({ *
* from: app.activeDocument, *
* filter: function (item) { *
* return ( *
* 'PathItem' === item.typename *
* || 'CompoundPathItem' === item.typename *
* ); *
* } *
* }); *
* *
* -------------------------------------------------------------------- *
* Example 4. Using `filter` function: *
* *
* var myItems = getItems({ *
* from: app.activeDocument, *
* filter: onlyPngLinks *
* }); *
* *
* function onlyPngLinks(item, depth) { *
* return ( *
* 'PlacedItem' === item.typename *
* && '.png' === item.file.name.slice(-4).toLowerCase() *
* ); *
* }; *
* *
* -------------------------------------------------------------------- *
* Example 4. Using the `filter` function for custom collecting: *
* *
* This example bypasses the normal returned array and instead *
* captures items in an "external" array `itemsByDepth`. *
* *
* var itemsByDepth = []; *
* *
* function getItemsByDepth(item, depth) { *
* if (undefined == itemsByDepth[depth]) *
* itemsByDepth[depth] = []; *
* itemsByDepth[depth].push(item); *
* }; *
* *
* getItems({ *
* from: app.activeDocument, *
* filter: getItemsByDepth *
* }); *
* *
* -------------------------------------------------------------------- *
* @param {Object} options - parameters
* @param {PageItem|Array<PageItem>|Document|Layer} options.from - the thing(s) to look in, eg. a selection.
* @param {Function} [options.filter] - function that, given a found item, must return true (default: no filtering).
* @param {Boolean} [options.getPageItems] - whether to include page items in returned items (default: true).
* @param {Boolean} [options.getGroupItems] - whether to include GroupItems in returned items (default: true).
* @param {Boolean} [options.getLayers] - whether to include Layers in returned items (default: false).
* @param {Boolean} [options.getHiddenItems] - whether to include hidden items in returned items (default: true).
* @param {Boolean} [options.getLockedItems] - whether to include locked items in returned items (default: true).
* @param {Boolean} [options.getGuideItems] - whether to include guide items in returned items (default: false).
* @param {Number} [options.maxDepth] - deepest folder level (recursion depth limit) (default: 99).
* @param {Boolean} [options.returnFirstMatch] - whether to return only the first found item (default: false).
* @param {Number} [depth] - the current depth (private).
* @returns {Array|PageItem} - all the found items in a flat array, or the first found item if `returnFirstMatch`.
*/
function getItems(options, depth) {
// defaults
options = options || {};
var found = [],
depth = depth || 0,
items = options.from;
if (!options.initialized)
// once-off initialization
if (!initialize())
return [];
itemsLoop:
for (var i = 0, item, len = items.length; i < len; i++) {
item = items[i];
if (
false === excludeFilter(item)
&& true === includeFilter(item)
) {
// item found!
found.push(item);
if (options.returnFirstMatch)
break itemsLoop;
}
if (
'GroupItem' !== item.constructor.name
&& 'Layer' !== item.typename
)
// only items with children from here
continue itemsLoop;
if (
excludeHidden(item)
|| excludeLocked(item)
)
// don't look into excluded containers
continue itemsLoop;
if (depth >= options.maxDepth)
// don't go deeper
continue itemsLoop;
// set up for the next depth
options.from = item.pageItems;
// look inside
found = found.concat(getItems(options, depth + 1));
}
// this level done
if (true == options.returnFirstMatch)
return found[0];
else
return found;
/**
* Returns true when the item should be not be found.
* @param {PageItem|Layer} item
* @returns {Boolean}
*/
function excludeFilter(item) {
return (
isAlreadyFound(item)
// is hidden
|| excludeHidden(item)
// is locked
|| excludeLocked(item)
// is guide
|| (
false === options.getGuideItems
&& true === item.guides
)
// is layer
|| (
false === options.getLayers
&& 'Layer' === item.typename
)
// is group item
|| (
false === options.getGroupItems
&& 'GroupItem' === item.typename
)
// is page item
|| (
false === options.getPageItems
&& 'GroupItem' !== item.typename
&& undefined != item.uuid
)
);
};
/**
* Returns true when the item should be included.
* @param {PageItem|Layer} item
* @returns {Boolean}
*/
function includeFilter(item) {
return (
undefined == options.filter
|| options.filter(item, depth)
);
};
/**
* Returns true when the item should
* be excluded because it is hidden.
* @param {PageItem|Layer} item
* @returns {Boolean}
*/
function excludeHidden(item) {
return (
false === options.getHiddenItems
&& (
true === item.hidden
|| false === item.visible
)
);
};
/**
* Returns true when the item should
* be excluded because it is locked.
* @param {PageItem|Layer} item
* @returns {Boolean}
*/
function excludeLocked(item) {
return (
false === options.getLockedItems
&& true === item.locked
);
};
/**
* Returns true if item was already
* found, and marks item as found,
* to avoid finding same item twice.
* @param {PageItem|Layer} item
* @returns {Boolean}
*/
function isAlreadyFound(item) {
var uuid = item.hasOwnProperty('uuid')
? item.uuid
: item.typename + item.zOrderPosition,
isFound = !!options.isFound[uuid];
options.isFound[uuid] = true;
return isFound;
}
/**
* Returns the initialised `options` object.
* @returns {Object}
*/
function initialize() {
// make a new object, so we don't pollute the original
options = {
initialized: true,
depth: 0,
isFound: {},
filter: options.filter,
getPageItems: false !== options.getPageItems,
getGroupItems: false !== options.getGroupItems,
getLayers: true === options.getLayers,
getHiddenItems: false !== options.getHiddenItems,
getLockedItems: false !== options.getLockedItems,
getGuideItems: true === options.getGuideItems,
maxDepth: options.maxDepth,
returnFirstMatch: options.returnFirstMatch,
};
if (
undefined == options.maxDepth
|| !options.maxDepth instanceof Number
)
options.maxDepth = 99;
// items is a single layer
if ('Layer' === items.typename)
items = [items];
// items is a document
else if ('Document' === items.constructor.name) {
var layers = items.layers;
items = [];
for (var i = 0; i < layers.length; i++)
items.push(layers[i]);
}
else if ('Array' !== items.constructor.name)
items = [items];
return items.length > 0;
};
};
Edit 2025-02-14: added `getItems` function to recursively search inside groups.
... View more
‎Feb 12, 2025
06:50 PM
Robert, it's just a quick demo of merging paragraphs, not collecting the paragraphs.
... View more
‎Feb 12, 2025
06:14 PM
Hi @ep_nna, similar to the answers you already have, here is the way I would approach it.
- Mark
/**
* Demonstration of getting a Page's textFrame from a layer.
*
* @author m1b
* @version 2025-02-13
* @discussion https://community.adobe.com/t5/indesign-discussions/get-content-of-textframe-on-specific-layer-and-page/m-p/15145413
*/
function main() {
var doc = app.activeDocument,
// the layer containing the "names" text frame(s)
namesLayer = doc.layers.itemByName('Names Layer');
if (!namesLayer.isValid)
return alert('There is no "Names Layer".');
var pages = doc.pages,
names = [];
for (var i = 0; i < pages.length; i++) {
// call the function to get the name
var pageName = getFirstTextOnLayerOnPage(namesLayer, pages[i]);
if (!pageName)
// couldn't get name!
continue;
// just for demo
names.push(pageName);
// now that you have the name, you can do the export here
};
// just for demo
alert('Names:\n' + names.join('\n'));
};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Do Script');
/**
* Returns contents of the first text frame on `layer` on `page`.
* @author m1b
* @version 2025-02-13
* @param {Layer} layer - the target Layer.
* @param {Page} page - the target page.
* @returns {String?}
*/
function getFirstTextOnLayerOnPage(layer, page) {
if (!layer || !page)
throw new Error('getFirstTextOnLayerOnPage: bad parameter.');
var textFrames = page.textFrames;
for (var i = 0; i < textFrames.length; i++) {
var frame = textFrames[i];
if (frame.itemLayer === layer)
return frame.contents;
}
};
... View more
‎Feb 12, 2025
05:40 PM
Hi @Niko32511349sm76, I've read the posts on this page and they are all good answers, but I didn't see my specific approach so I'll share a demo script here. It sounds like it might not exactly be what you need, but maybe it helps your understanding? Test first with the attached demo.indd if you like. I apply the paragraph style without overriding local styling, as @Peter Kahrel showed, and remove the line break as @Robert at ID-Tasker showed.
- Mark
/**
* Demonstration of merging paragraphs.
*
* @author m1b
* @version 2025-02-13
* @discussion https://community.adobe.com/t5/indesign-discussions/how-to-preserve-character-styles-when-merging-text-via-javascript/m-p/15145001
*/
function main() {
var doc = app.activeDocument,
testParagraphs = doc.stories[0].paragraphs.everyItem().getElements();
var mergedParagraph = mergeParagraphs(testParagraphs);
if (mergedParagraph.isValid)
// applying a demonstration paragraph style
mergedParagraph.applyParagraphStyle(doc.paragraphStyles.itemByName('Demo Style'), false);
};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Merge Paragraphs');
/**
* Merge multiple paragraphs into one.
*
* Important: assumes paragraphs are
* continguous in the same story.
* @author m1b
* @version 2025-02-13
* @param {Array<Paragraph>} paragraphs - the paragraphs to merge.
* @param {String} [delimiter] - a string to replace line breaks.
* @returns {Paragraph} - the merged paragraph;
*/
function mergeParagraphs(paragraphs, delimiter) {
var endingLinefeed = /\s*[\n\r]$/,
para;
for (var i = paragraphs.length - 1, match; i >= 0; i--) {
para = paragraphs[i];
match = para.contents.match(endingLinefeed);
if (!match)
continue;
// remove the line feed and replace with delimiter
para.characters
.itemByRange(para.characters[-match[0].length], para.characters[-1])
.contents = delimiter || '\u0020';
}
// return the first paragraph, which is the merged paragraph
return para;
};
... View more
‎Feb 12, 2025
05:56 AM
Mike, in scripts we use .insertLabel and .extractLabel methods in cases where it is nothing to do with the user directly and is handled completely by scripts. Example usage is if I want to keep the settings that a user last used with a particular document—I can store them in a label attached to that document. They are not accessible via the GUI, so they are just for scripts. It is also cool to store a label in a DOM object with one script and then have another different script use it later on.
The Script Label panel is a special case because it allows a user to configure the script in some way. In the case of my QR Code script above, it would be useless if the user could not edit the labels.
- Mark
... View more
‎Feb 12, 2025
05:39 AM
Hi Robert, I am thinking of a case where you set up a document with multiple (same URL) QR Codes. Not data merge or anything. Then running script with any one selected will update all of them. I don't know if that is what OP wants—as Eugene mentioned, we don't have a lot of information yet— but I can see that being handy in some real situations.
- Mark
... View more
‎Feb 12, 2025
05:35 AM
I'm very glad it can help Mike. Thanks for letting me know!
You're right that the Script Label panel isn't used much, but it does provide a very useful place to share data between a user-defined DOM object (eg. a QR Code frame) and a script. Look for .label in the script.
- Mark
... View more