InDesign Scripting Forum Roundup #12
June 11, 2018 | Snippets | en
Hey scripters, are you familiar with InDesign events, event listeners, menu actions, idle tasks? That's the hot focus of the 12th ISFR. Plus a fine selection of threads and snippets involving GREP, text, CMYK swatches, IDML, transformations… Enjoy the ride!
1/ InDesign Events
Can We Kill an Event?
Detecting TextFrame Double Click
Note on the BeforeSaveAs Event
Can We Disable InDesign Built-In Menus?
2/ Text and Typography
Removing Repeated Leading Patterns (GREP)
Improving Text Sort
3/ Graphics and Geometry
How to Normalize CMYK Swatches
Dealing with ItemTransform Rotation (IDML)
Mirroring Objects across Facing Pages
Rulers and Zero Point Mystery
1/ InDesign Events
Can We Kill an Event?
Daniel asked: “Is there any way to delete an event once created?”
Technically, an Event
is not a kind of entity you can remove. What you can do with an Event
is stopping its propagation, cancelling its default behavior—when possible—and, finally, stopping listening to it. So we don't want to actually remove an event, we want to remove an event listener, which is a callback mechanism temporarily attached to a specific event type.
The above distinction is important, as it allows to rephrase the question the right way: “How do I remove an EventListener?” Which then leads to: “How do I refer to the EventListener I want to remove?” Keep in mind that multiple event listeners might be attached to the same event type, including listeners declared by scripts external to yours. So it is not a good idea, in general, to target every event listener available for a specific event type, the problem is clearly to recover your own event listener(s).
There are at least two ways:
1. Using app.removeEventListener(eventType, handler, captures)
. — Here the recovery key is the handler function (or script file) that has been declared with the listener, with respect to a specific eventType and a specific captures flag. So you must provide a valid reference to the original handler to make it work. Maybe that's just a matter of taste but this approach doesn't sound very clean to me, as it somehow violates the encapsulation principle offered through the EventListener
object.
In the code this would look like app.removeEventListener(Event.AFTER_SELECTION_CHANGED, myEvHandler, <captures>)
.
(Note that myEvHandler is not a name, but an actual reference to the handler function, assuming this makes sense in the lexical scope from which the code is interpreted.)
2. Identifying the EventListener
by its name or its ID within the app.eventListeners
collection, then removing it. — Here the recovery key is either the name, or the ID, of the event listener you want to remove. This supposes you have specified, or stored, this information somewhere, while you were adding the listener.
var myListener = app.eventListeners.add(...); var myListenerID = myListener.id; // backup me
So later you will be able to recover that instance and remove it:
var myListener = app.eventListeners.itemByID(myListenerID); if( myListener.isValid ) myListener.remove();
If the name is your recovery key, make sure you have assigned a unique name to a unique listener (unless you need to remove multiple listeners based on the same name), then do something like this:
var myListener = app.eventListeners.itemByName(keyName); if( myListener.isValid ) myListener.remove();
• Original discussion: forums.adobe.com/message/9529300#9529300
Detecting TextFrame Double Click
Unlike ScriptUI, the Scripting DOM does not provide click or double click events, so there is no direct way to make a script specifically react to such events. However, thanks to IdleEvents
(processed through IdleTasks
) we can mimick a mouse event listener under certain conditions.
Here is a proof of concept:
#targetengine FakeDblClickHandler // YOUR HANDLER COMES HERE // --- function onTextFrameDblClick(/*TextFrame*/target) { alert( "You have just double-clicked " + target ); // etc. }; const checkDblClick = function F(/*IdleEvent*/ev, o,t) // ----------------------------------------------- { (o=(o=app.properties.selection) && (1==o.length) && o[0]); t = +ev.timeStamp; o instanceof TextFrame ? (F.Q = t) : (t -= F.Q||0); if( t < 120 && o instanceof InsertionPoint ) { onTextFrameDblClick(o.parentTextFrames[0]); } }; (function(tasks,name,rate,callback) // ----------------------------------------------- // Register the IdleTask { var t = tasks.itemByName(name); if( t.isValid ) { t.eventListeners.everyItem().remove(); t.remove(); } tasks.add({name:name, sleep:rate}) .addEventListener(IdleEvent.ON_IDLE, callback, false); })(app.idleTasks,$.engineName,15,checkDblClick);
• Original discussion: forums.adobe.com/message/9199341#9199341
Note on the BeforeSaveAs Event
From within a beforeSaveAs
handler, how can we identify the final file name of the document which, at this specific moment, still owns its previous fullName
property (including invalid cases where the document hasn't be saved yet)?
Interesting question, because we cannot retrieve the incoming file name from the document itself! Fortunately, save-as events expose a hidden property, event.fullName
that fills the gap.
The following snippet shows how to use that trick:
// The present code can be used as a startup script. #targetengine 'UpdateJobNumber' const updateJobNumber = function(/*Event*/ev, doc,m,t) // ------------------------------------- // BEFORE_SAVE_AS event handler. // Reset the `JobNumber` text variable to the 9 first characters // of the doc file name iff they are formed of uppercase letters // and/or digits. // --- // Tested conditions: // 1. The event type MUST be 'beforeSaveAs' (prevent wrong listener.) // 2. The event MUST provide a valid `fullName` prop (File), // since one can't rely on document.fullName yet! // 3. The save-as File name MUST match /^[A-Z\d]{9}/. // 4. The target MUST be a Document instance (who knows!) // 5. It MUST have a `JobNumber` TextVariable... // 6. ...of the kind 'Custom text.' // 7. No need to rewrite the variable if already set as expected. { ( 'beforeSaveAs' == ev.eventType ) && ( m=ev.properties.fullName ) && ( m=m.name.match(/^[A-Z\d]{9}/) ) && ( (doc=ev.target) instanceof Document ) && ( (t=doc.textVariables.itemByName('JobNumber')).isValid ) && ( (t=t.variableOptions) instanceof CustomTextVariablePreference ) && ( m[0] != t.contents ) && ( t.contents=m[0] ); }; app.addEventListener('beforeSaveAs', updateJobNumber);
• Original discussion: forums.adobe.com/message/9828281#9828281
Can We Disable InDesign Built-In Menus?
Sudha K wrote: “We can invoke menus using MenuActions. But how can I disable/enable InDesign's built-in menus? (...) I want to disable Export menu...”
The general issue in dealing with menu items (including associated actions, event listeners, etc.) is to get a clear understanding of object lifecycles in InDesign scripting model. What you MUST know before going any further is:
0. Menus
and Submenus
have MenuItems
which are connected to InDesign processes through MenuActions
(and ScriptMenuActions
, but I'll leave that aside.) On the other hand, Events
may be connected to custom callback functions through EventListeners
. Most—if not all—InDesign entities can be the target of some event, and this includes Menus
(cf beforeDisplay
) and MenuActions
(cf beforeInvoke
), but also PageItems
or even the app itself. In this discussion, though, MenuActions
are the center of interest.
Lifecycles.
1. As objects, native InDesign MenuActions
are permanent. That is, you can't remove or change them, ever.
2. As objects, Menus
, Submenus
and MenuItems
are application-persistent. That is, if you remove or change something in that space, these changes will persist beyond the InDesign session. That's a crucial (and not very intuitive) fact.
3. As objects, EventListeners
(and ScriptMenuActions
, but ok, let's really forget them) are always session-persistent. That is, changes in that space are intended to persist throughout the whole InDesign session, until you quit the app.
4. Finally, your event handlers—that is, the callback functions contained in the JSX code and attached to particular events via EventListeners
—are at best engine-persistent (if a #targetengine
directive is active) or simply “script-persistent”, i.e purely volatile, when executed in the default main engine.
Note: There are in fact more persistent event handlers, those which are attached as JSX File objects.
Now you can clearly see that attaching an event handler to a MenuAction
(invoked from a MenuItem
) via a freshly created EventListener
, may cause some issues if not carefully designed. For example, you may have an EventListener
still alive—in the session—whose callback function is yet dead. In other threads we also discussed the problem of having a custom Menu
and its MenuItem
children (all application-persistent) waiting for an appropriate EventListener
to wake up—which can be achieved with startup scripts only.
So, the very first step is to clearly have in mind how these respective lifecycles have to be dealt with and connected in any project that interacts with such objects. You need to separate at least two stages:
(A) The Install Stage. This is the part of your code that initializes the persistent objects to be connected and used throughout the scope (usually, the engine) so that things are guaranteed to exist when you'll need them. That's typically the place to declare event handlers (but not to call them!), and to manage EventListeners
to be either created or destroyed, not to mention menu structure, etc. In most projects the Install Stage is intended to run once per session, but since it could be manually executed again and again by an inquiring user, we must prevent troubles with object duplication.
(B) The Script-In-Action Stage. Ideally, This Should Be an Empty Zone! Indeed, when installing event handlers, what you want is just to process outside events (that may or will happen later), hence the script itself should have nothing more to do than installing stuff (once.) I think it important to stress this point because some scripters have trouble capturing it well: the script in action does basically nothing once installed. The real process is delayed to event handlers that wait patiently to take action.
However, there are cases where (B) can have a job, namely, the job of reversing (A). Then, apart from setting up listeners and event handlers, the script may be designed to toggle the whole event processing. For that purpose we create in the Install area a slightly enhanced function—say toggleListeners
—which will address this special feature.
Better is to provide a code to illustrate the whole approach I suggest:
//=============================================================== // We need a session-persistent engine to keep our event // handler(s) alive, so we use the #targetengine directive. //=============================================================== #targetengine 'ExportAndPackageSpy' //=============================================================== // Since we have a session-persistent engine, executing the // present script shouldn't cause re-declaration of existing // data. The following conditional block takes this into // account and provides the 'installation' brick. //=============================================================== if( !$.global.hasOwnProperty('DATA') ) { // Builds a set of native MenuActions of interest. //---------------------------------- $.global.DATA = (function( o,r) { // Register package&export action specifiers, // and other hosts if needed. // --- o = app.menuActions; r = { Package: { host:o.itemByName("$ID/Package...").toSpecifier(), etp:'beforeInvoke' }, Export: { host:o.itemByName("$ID/Export...").toSpecifier(), etp:'beforeInvoke' }, PdfExport: { host:o.itemByName("$ID/Export To PDF").toSpecifier(), etp:'beforeInvoke' }, }; // In addition you'd like to catch PDF 'sub-actions' which 'Export To // PDF' does not cover: [High Quality Print]..., [PDF/X-1a:2001]..., // etc. Unfortunately, the listeners of those menu actions do not work! // As a fallback mechanism you can globally spy the beforeExport event. // It occurs *after* menu actions but can still be used to prevent // the user from exporting the document by unexpected means. // --- r['AnyExport'] = { host:app.toSpecifier(), etp:'beforeExport' }; // Debug. // alert( r.toSource() ); return r; })(); // Defines the *actual function to be executed* by the // present script. In this particular case a 'toggle' feature // is wanted in order to turn listening ON/OFF. This process // is slighlty different from a pure menu installer but most // implementations use very similar tricks. //---------------------------------- $.global.toggleListeners = function(/*fct*/callback, loading,k,o,a,t) { // Even if a default callback is provided below, making it // visible as an input argument improves the modularity of // the code. (Technically, we could attach other event // handlers as well.) // --- 'function' == typeof callback || (callback=$.global.disableProcess); // Paranoid checkpoint. // --- if( 'function' != typeof callback) throw "No function callback supplied!" // Here we use the function itself (callee) as a 'flag keeper,' // taking advantage of its persistence. If loading is 1, // listeners are to be added. // --- loading = 1 - (callee.LOADED||0); // Loop in the DATA. // --- for( k in DATA ) { if( !DATA.hasOwnProperty(k) ) continue; // Collection of event listeners registered for this // menu action. // --- o = resolve(DATA[k].host).eventListeners; // *In any case*, remove any existing listener that matches // callback. This prevents debug or non-well-formed code from // duplicating listeners throughout the current session // (should #targetengine be missing or other bug occur...) // --- a = o.length ? o.everyItem().getElements() : []; while( t=a.pop() ) t.handler===callback && t.remove(); if( !loading ) continue; // If loading is required, add the listener. // --- o.add(DATA[k].etp, callback); } // Upate the internal flag. // --- callee.LOADED = loading; // Debug. alert( loading ? "Listeners LOADED." : "Listeners UNLOADED."); }; // Now comes the particular callback being invoked when any of the // spied events occurs. A critical property of this function is to // remain in memory as long as the event listeners may exist. Since // they are by nature session-persistent, we need a session-persistent // function as well (or a File, in other implementations.) // --- // This part of the code entirely depends on your specific goal. In // your example the actions have to be inhibited (likely under some // additional conditions?) Here we simply cancel the event, using // the fact that both BEFORE_INVOKE & BEFORE_EXPORT are cancelable. //---------------------------------- $.global.disableProcess = function(/*Event*/ev) { // Make sure no other hypothetical target // would catch that event (from now.) // --- ev.stopPropagation(); // Cancels the event. (In some versions, InDesign may prompt // a message telling the user that a native action has been // cancelled.) ev.preventDefault(); // Debug. alert( "Cancelled event:\r\r" + {}.toSource.call(ev.properties) ); }; } //=============================================================== // From this point the installation process is ready (data and functions // are known to the engine), but no event listener has been either added // or removed. The below instructions represent the actual finality of // executing the present script, that is, toggling event listeners. This // could be performed from a *startup script*, with the effect of turning // listeners ON (since no flag is attached to the function yet.) //=============================================================== toggleListeners();
• Original discussion: forums.adobe.com/message/10228502#10228502
• See also: How to Create your Own InDesign Menus
2/ Text and Typography
Removing Repeated Leading Patterns (GREP)
There is an interesting way of removing duplicates when some pattern repeats at the beginning of successive paragraphs. (The word ‘interesting’ does not pretend to mean ‘efficient’ but we may learn something from this ;-)
Suppose you have many paragraphs that typically have the form
<ITEM> - <TEXT>
.
The goal is to avoid repeating the <ITEM>
part. When identical leading items follow each other, we want to replace <ITEM> -
by a tabulation.
Let's consider the scheme [^-]+-\s
, meaning “anything before a hyphen, then the hyphen, then any space character.” How to detect and remove such pattern when it is repeated at the beginning of successive paragraphs?
My first idea was to use something like ^([^-]+-\s)[^\r]+\r\K\1
, but this only selects the 2nd instance of the repeated pattern, so that wouldn't work at all for arbitrary number of dups. Now the funny trick is just a slight variation of the previous regex, /^([^-]+-\s)([^\r]+\r\K\1)+/
, which then magically selects the very last duplicate of the \1
capture!
From then we can design a recurring changeGrep()
command that never needs to visit the found elements. Simply replace the captures by what you want and loop until changeGrep()
returns an empty array. In the code below I use a tab '\t'
as changeTo
parameter and my target is app.selection[0]
since I have a TextFrame
selected. Of course one could use any other target, including app.
const GREP = /^([^-]+-\s)([^\r]+\r\K\1)+/; (function(target) { app.findGrepPreferences = app.changeGrepPreferences = null; app.findGrepPreferences.findWhat = GREP.source; app.changeGrepPreferences.changeTo = '\t'; while( target.changeGrep().length ); app.findGrepPreferences = app.changeGrepPreferences = null; })(app.selection[0]);
Here is how we can visualize the iterative process (in my example all is done in two steps):
• Original discussion: forums.adobe.com/message/9940795#9940795
Improving Text Sort
Given an array of Text
instances—for example, resulting from myDoc.findGrep()
—it is of course possible to apply a custom sort function to that array. Something like
myTextArr.sort( function(a,b){ return Number(a.contents)-Number(b.contents) } );
But it's worth noting that invoking Text.contents
from within such a function is critically time consuming. The reason is the “5×N×Log(N) rule” which tells us that, on average, if you have 1,000 objects in the Array to be sorted, the compare function will be invoked about 15,000 times.
So, as each call involves two DOM commands (a.contents
and b.contents
), you hit the DOM about 30,000 times while only 1,000 objects are actually to be compared.
I did some tests based on a 5-page documents containing 4,000 text patterns (digits, in this example) at random locations. The experiment was based on the following scheme:
doc = app.properties.activeDocument; app.findGrepPreferences = null; app.findGrepPreferences.findWhat = "\\d+"; founds = doc.findGrep(); // `founds.length` ~ 4000 $.hiresTimer; //----------- <textSort>(founds); // Using various <textSort> algorithms. //----------- alert( $.hiresTimer );
where <textSort>
refers to the sort routine to be used.
To keep things absolutely uniform regarding text-to-number conversion, I used a generic VALUE function which returns the numeric value from any captured Text. It is based on parseInt(...)
since the parsed strings are supposed to represent integers.
const VALUE = function(/*Text*/tx) { return parseInt(tx.contents,10) };
1. The naive approach (our benchmark) is as follows:
const textSort1 = function(/*Text[]&*/A, f) //---------------------------------- // 1. Original method. { f = callee.sorter; A.sort( f ); }; textSort1.sorter = function(/*Text*/x,/*Text*/y) { return VALUE(x) - VALUE(y) };
It takes the Text
array and applies the original compare method, which simply returns VALUE(x)-VALUE(y)
for each x,y items. Keep in mind that any call to VALUE()
means invoking Text.contents
, which causes catastrophic results (of the order of ten seconds on my platform.)
2. How to improve textSort1
? Problem with Text
instances is, they have no ID, so you cannot easily separate the DOM-access side from the calculation side. However, a well-known fact is that toSpecifier()
is faster than any other method. So it is relevant to access Text.contents
only N times and to store the result in a map based on specifiers. Thus, the sort routine only invokes toSpecifier()
and use the proxy map for comparing data. Here is the snippet:
const textSort2 = function(/*Text[]&*/A, f,q,n,i,t) //---------------------------------- // 2. Slight optimization thru a map. ABOUT 3X FASTER // [REM] `toSpecifier()` is faster than `contents`. { f = callee.sorter; q = f.Q = {}; for( n=A.length, i=-1 ; ++i < n ; ) { t = A[i]; q[t.toSpecifier()] = VALUE(t); } A.sort( f ); }; textSort2.sorter = function(/*Text*/x,/*Text*/y) { return callee.Q[x.toSpecifier()] - callee.Q[y.toSpecifier()] };
According to my tests, this method is 3X faster than the original. Note that VALUE()
is called only from the linear loop (textSort2
). The compare function only needs to access Text.toSpecifier()
, but it stills does it 5×N×Log(N)
times. The DOM is still very much in demand, although the algorithm runs significantly faster.
3. Finally, the solution that sounds to me the best (and the most impressive!) is based, as usual, on entirely removing DOM heat from the compare function. Even better, it just uses the native Array.sort()
with no callback—which has been shown to be extremely efficient in optimization issues. The trick is based on a simple idea: find a way to store everything (computed numbers and indices) in terms of orderable UTF16 strings. Save those data in a proxy array and sort it. Then, recover the original indices and apply a merge sort on the original Text
array.
The result now comes out within 250 ms (10X faster than textSort2
, 30X faster than the naive method.)
For sure the code requires much more attention, but it's a fair illustration of my principle, the easiest path is not always the best.
const textSort3 = function(/*Text[]&*/A, q,n,i,v,t,j,k) //---------------------------------- // 3. Using a proxy array (of strings.) ABOUT 30X FASTER { const CHR = String.fromCharCode; q = Array(n=A.length); // Proxy array. // --- for( i=-1 ; ++i < n ; ) { v = ''+VALUE(A[i]); q[i] = CHR(1+v.length) + v + CHR(1+i); } q.sort(); // default sort (very fast!) // Reorder A according to q. (In-place merge sort.) // --- while( i-- ) { if( 'string' != typeof(k=q[i]) ) continue; t = A[i]; for( j=i ; 1 ; (A[j]=A[k]),(j=k),(k=q[j]) ) { 'string' == typeof k && (k=-1+k.charCodeAt(-1+k.length)); q[j] = j; if( k == i ) break; } A[j] = t; } };
• Original discussion: forums.adobe.com/message/9992688#9992688
3/ Graphics and Geometry
How to Normalize CMYK Swatches
Neal wrote, “I come across (InDesign documents) that predominantly contain swatches that are named differently but are the same exact color, value-wise, i.e. their C, M, Y, K, are the exact same. I’m looking for a script that will recognize swatches with identical CMYK values, and do away with duplicate swatches that share the same values and leave only one swatch. (...) So in the end, there is only one instance of a specific color, and it’s name as CMYK.”
Suggested code:
function normalizeCMYK(/*Document*/doc, swa,a,r,o,t,k,i) // ------------------------------------- // Remove CMYK swatch duplicates and set every name in `C= M= Y= K=` form. { if( !doc ) return; const __ = $.global.localize; const CM_PROCESS = +ColorModel.PROCESS; const CS_CMYK = +ColorSpace.CMYK; swa = doc.swatches; a = doc.colors.everyItem().properties; r = {}; // Gather CMYK swatches => { CMYK_Key => {id, name}[] } // --- while( o=a.shift() ) { if( o.model != CM_PROCESS ) continue; if( o.space != CS_CMYK ) continue; t = swa.itemByName(o.name); if( !t.isValid ) continue; for( i=(k=o.colorValue).length ; i-- ; k[i]=Math.round(k[i]) ); k = __("C=%1 M=%2 Y=%3 K=%4",k[0],k[1],k[2],k[3]); (r[k]||(r[k]=[])).push({ id:t.id, name:t.name }); } // Remove dups and normalize names. // --- for( k in r ) { if( !r.hasOwnProperty(k) ) continue; t = swa.itemByID((o=(a=r[k])[0]).id); for( i=a.length ; --i ; swa.itemByID(a[i].id).remove(t) ); if( k == o.name ) continue; // No need to rename. try{ t.name=k }catch(_){} // Prevent read-only errors. } }; normalizeCMYK(app.properties.activeDocument);
• Original discussion: forums.adobe.com/message/9819056#9819056
Dealing with ItemTransform Rotation (IDML)
Given an IDML rectangle, we can change its ItemTransform
attributes to ".86 -.5 .5 .86 0 0"
(as an example) in order to apply a 30° rotation to the whole object. Problem is, in most cases this also induces a displacement.
Indeed, if we want to get the object rotated around its center point, we need to take into account its <PathPointArray>
anchors which represent the shape in its own coordinate space.
Note. - There is a crucial difference between a TRANSLATION (which occurs through the transformation matrix, after any other task) and the particular POSITIONING of the object in its inner space.
It is easy to see that a rotation matrix applied to an off-centered object will cause it to move with respect to its barycenter. In InDesign, the result of a pure rotation θ is given by
[R] (x',y') = (xcosθ+ysinθ, -xsinθ+ycosθ)
keeping in mind that this formula holds with respect to the origin [0,0]
. Now let's consider the black rectangle in the below figure. We assume it untransformed, so its original matrix is [1 0 0 1 0 0]
, but the anchor coordinates found in the IDML indicate that its bottom left corner is at [1188,-1220]
relative to the origin of the parent space.
If we apply the formula [R] to the bottom-left corner, we see that the point moves to a location (x',y')
that clearly reveals a shift. The resulting rectangle (in blue) undergoes a move because the original object is not centered on the origin. That's the effective result of applying ItemTransform=[0.866 -0.5 0.5 0.866 0 0]
.
So, if you want to keep the center of the rectangle where it originally was, you need to compensate in some way the shift effect. This can be done by adjusting the translation attributes, say (dx,dy)
, as pictured below:
First, we calculate the center coordinates (x0,y0)
of the original rectangle using the (min+max)/2
trick on the existing anchor points. Then we calculate dx
and dy
as follows:
// Rotation angle (deg -> rad) // --- const a = 30 * Math.PI/180; // Rotation matrix will be // [cosa -sina sina cosa dx dy] // --- const cosa = Math.cos(a); const sina = Math.sin(a); // From IDML's <PathPointArray> // --- var x1 = 1188.5, x2 = 3409.5, y1 = -1731.4685039368005, y2 = -1220.5; // Midpoint. // --- var x0 = (x1+x2)/2, y0 = (y1+y2)/2; // Translation fix (dx,dy) // [dx dy] = [1-cosa -sina sina 1-cosa] × [x0 y0] // --- var k = 1 - cosa; var dx = k*x0 - sina*y0, dy = k*y0 + sina*x0; // Final matrix. // --- var mx = [cosa, -sina, sina, cosa, dx, dy];
Another side of the question is, how to reverse-engineer a rotation whose angle is known, given a point M and its transformed point M'?
For example, suppose that M=(452.7, -365.9)
transforms into M'=(388.2, -456.3)
when it undergoes a rotation of angle -35.6°
. Here is what this looks like:
It is easy to see that the origin of this coordinate system cannot be the center of the rotation, O. It's less easy to compute O's coordinates. Fortunately, a script can do the maths for us:
// MAKE SURE YOU TALK IN RADIANS! // --- var theta = (-35.6)*Math.PI/180; // M -> M' var x1 = 452.696629213484, y1 = -365.887640449439; var x2 = 388.202515466231, y2 = -456.253631357617; var dx = x2-x1, dy = y2-y1, d = Math.sqrt(dx*dx+dy*dy); var OI = d/(2*Math.tan(theta/2)); var xi = (x1+x2)/2, yi = (y1+y2)/2, alpha = Math.atan(-dy/dx); var x0 = xi + OI*Math.sin(alpha), y0 = yi + OI*Math.cos(alpha); alert( [x0,y0].join('\r') ); // --- // => 561.178125659198, -511.50845807588
• Original discussion: forums.adobe.com/message/10217409#10217409
Mirroring Objects across Facing Pages
There are at least two ways of mirroring graphical objects with respect to facing pages. In both cases the spread coordinate space will be our friend.
Case 1. We want to duplicate and reposition the selected objects across the spread, symmetrically:
Suggested code:
function mirrorSelection( a,t) // ------------------------------------- // Duplicate the selected objects across the spread symmetrically. // (This snippet only performs TRANSLATIONS. Rotation and/or shear // attributes are not mirrored.) { if( !(a=app.properties.selection) || (!a.length) || !('transform' in a[0] ) ) { alert( "Please select at least a page item." ); return; } const CS_SPREAD = +CoordinateSpaces.spreadCoordinates, AP_CENTER = +AnchorPoint.centerAnchor, MX = [1,0,0,1,0,0]; while( t=a.pop() ) { MX[4] = -2*t.resolve(AP_CENTER, CS_SPREAD)[0][0]; t.duplicate().transform(CS_SPREAD, AP_CENTER, MX); } }; app.doScript( mirrorSelection, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "Mirror Selection" );
Case 2. We want to perform a full reflection—including rotation and/or shear attributes. Interestingly, this requires a single SCALING step relative to the center of the spread. No per-item calculation needed here, so this could be applied to a plural specifier as well—stuff like ...pageItems.everyItem()
—with a significant performance gain.
function fullMirrorSelection( a,t) // ------------------------------------- // Duplicate the selected objects across the spread symmetrically. // (This snippet performs a *full* reflection.) { if( !(a=app.properties.selection) || (!a.length) || !('transform' in a[0] ) ) { alert( "Please select at least a page item." ); return; } const CS_SPREAD = +CoordinateSpaces.spreadCoordinates, SPD_CENTER = [[0,0],CS_SPREAD], MX = [-1,0,0,1,0,0]; // note the -1 for scaleX while( t=a.pop() ) t.duplicate().transform(CS_SPREAD, SPD_CENTER, MX); }; app.doScript( fullMirrorSelection, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "Full Mirror Selection" );
• Original discussion: forums.adobe.com/message/9883560#9883560
Rulers and Zero Point Mystery
Uwe Laubender reported a frustrating issue when attempting to reset the zeroPoint
to the upper left corner of a page. Settings are very specific here: we have a facing pages layout with slightly rotated pages (4° CCW) on a “counter-rotated” spread, as shown below:
Uwe found that the rulers never “align with the upper left corner of the page left from the spine” and, no matter how he proceeds, he cannot move the zero point to the exact location of the upper left corner of the page. Instead, after dragging the cross-hair to the desired location, “the zero point landed at the upper left corner of the magenta rectangle” below:
As discussed in Coordinate Spaces and Transformations in InDesign — Chapter 4, InDesign's ruler system becomes extremely complex when pages and/or spreads undergo a transformation. The default origin of the ruler system (that is, the default zero point) depends on ViewPreference.rulerOrigin
, which leads to distinct behaviours. While InDesign still displays horizontal and vertical rulers aligned with the viewport, the actual orientation of those axes may fit a coordinate space that no longer coincides with the viewport, which deeply impacts apparent coordinates.
In spreadOrigin
mode, [0,0]
refers to the “top-left corner of the in-spread box of the pages,” as pictured below:
Now here is a script that computes and fixes the observed shift relative to the upper left corner of the page.
const CS_SPREAD = +CoordinateSpaces.spreadCoordinates; // Settings. // --- var doc = app.properties.activeDocument; var spd = doc.spreads[0]; var PG_INDEX = 0; var pg = spd.pages[PG_INDEX]; // First, reset the ruler system to its default. // --- doc.zeroPoint = [0,0]; // Ruler origin in spread coords (pt units.) // --- var xyOrig = spd.resolve([[0,0],PG_INDEX], CS_SPREAD, true)[0]; // Page's top-left corner in spread coords (pt units.) // --- var xyPage = pg.resolve(AnchorPoint.topLeftAnchor, CS_SPREAD)[0]; // Offset. // --- var dx = xyPage[0] - xyOrig[0]; var dy = xyPage[1] - xyOrig[1]; // Change origin. // --- doc.zeroPoint = [dx+'pt',dy+'pt'];
• Original discussion: forums.adobe.com/message/10122475#10122475
GitHub page: github.com/indiscripts
Twitter stream: twitter.com/indiscripts
YouTube: youtube.com/user/IndiscriptsTV