m1b
Community Expert
m1b
Community Expert
Activity
‎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
‎Feb 11, 2025
10:47 PM
1 Upvote
Hi @kj_28, editing the contents of QR Codes is surprisingly complicated—as Robert mentioned. However, if you are able to do a bit of work up front to label each QR Code with the contents, then it becomes a lot easier. I'm not sure if you can do that in your case, but I think it should be possible for many jobs.
To use the script, you must first give each QR Code a "Script Label" that looks like this:
QRCODE:https://adobe.com
QR Codes without this labelling will be ignored. Script Label can be set in the "Script Label" panel (Window > Utility > Script Label).
Here is a quick demo running on my computer. You only select one QR Code, and it will change all of them if they have the same label.
To set the swatch color of a QR Code (like the second one above) you must set the stroke color of its frame (and give it 0% tint if you don't want to see it). This might seem a bit strange, but I didn't want to make the script too complicated and this was a way to update multiple QRCodes which can be in arbitrary colors.
Look at my attached demo.indd if you're not sure how I set it up—although it's very simple. If you end up using it, let me know!
- Mark
Here is the script:
/**
* Update Labelled QR Codes.js
*
* Based on the selected QRCode, this script will update
* all QRCodes that have been correctly labelled with
* the matching contents.
*
* Important notes:
*
* 1. Each QR Code (the graphic or its frame) must be labelled,
* via the "Script Label" panel, starting with "QRCODE:" prefix and then
* the text to be encoded, eg. QRCODE:https://adobe.com
*
* 2. To apply a swatch color to a particular QR Code, set its frame's
* stroke color to your desired color. If you want to hide the stroke
* set the tint to 0%.
*
* @author m1b
* @version 2025-02-12
* @discussion https://community.adobe.com/t5/indesign-discussions/qr-code/m-p/15144823
*/
function main() {
var settings = {
prefix: 'QRCODE:',
};
if (0 === app.documents.length)
return alert('Please open a document and try again.');
var doc = app.activeDocument,
item = doc.selection[0],
matcher = new RegExp('^' + settings.prefix + '(.+)'),
label;
if (!item)
return alert('Please select a QRCode that has been labelled correctly and try again.');
if (
undefined != item
&& item.hasOwnProperty('label')
&& matcher.test(item.label)
)
label = item.label;
else if (
item.hasOwnProperty('graphics')
&& item.graphics.length > 0
&& matcher.test(item.graphics[0].label)
)
label = item.graphics[0].label;
else if (
item.hasOwnProperty('parent')
&& matcher.test(item.parent.label)
)
label = item.parent.label;
if (!label)
return alert('Please select a QRCode that has been labelled correctly and try again.');
// the content to encode
var content = label.match(matcher)[1],
newContent = prompt('Edit QR Code:', content);
if (!newContent)
return;
// check all the page items
var items = doc.allPageItems;
for (var i = items.length - 1; i >= 0; i--) {
item = items[i];
if (
!item.label
|| !matcher.test(item.label)
)
continue;
var testContent = (item.label.match(matcher) || 0)[1];
if (testContent !== content)
continue;
if (item.graphics.length > 0)
// we want the graphic, not the frame
item = item.graphics[0];
if (!item.isValid)
continue;
var parent = item.parent;
var swatch = undefined;
if (item.parent.strokeColor.hasOwnProperty('colorValue'))
swatch = item.parent.strokeColor;
// update QR Code
item.createPlainTextQRCode(newContent, swatch);
// update the label
parent.label = settings.prefix + newContent;
}
};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Update Labelled QR Codes');
... View more
‎Feb 10, 2025
11:18 AM
Hi @Jambo_88 can you share a sample document? Showing before and after what it should look like? Are the map locations text frames in Indesign?
- Mark
... View more
‎Feb 08, 2025
05:19 AM
Hi @dublove you should use:
var desktopFolder = Folder.desktop;
See the documentation. - Mark
... View more
‎Feb 06, 2025
01:58 AM
Glad to hear it helped! 🙂
... View more
‎Feb 06, 2025
01:33 AM
Ah! Okay. 🙂
... View more
‎Feb 06, 2025
01:19 AM
Hi @Olivier-Coolgray, here's a little script that might help.
- Mark
tell application "Adobe InDesign 2025"
set exportPath to "/Users/mark/Desktop/JPGs/page.jpg"
-- Check if a document is open
if (count of documents) is 0 then
display dialog "No document is open!" buttons {"OK"} default button "OK"
return
end if
set doc to document 1
set pageNum to 1 -- Change this to export a different page
tell JPEG export preferences
set anti alias to true
set Page String to pageNum as string
set Exporting Spread to false
set JPEG export range to export range
set JPEG Quality to maximum
set export resolution to 300
set use document bleeds to false
end tell
tell doc
-- Export the page as a JPG
export to exportPath format JPG without showing options
end tell
display dialog "Export complete!" buttons {"OK"} default button "OK"
end tell
... View more