Scripting: How to Position InDesign Guides
December 03, 2023 | Tips | en
InDesign guides are funny creatures. Whether or not they are assigned to a page via the fitToPage
property, they remain firmly anchored to the geometry of the spread they belong to. The most mysterious thing is their location
attribute, which does not obey all the principles ordinarily weighing on rulers coordinates. This article explores how to determine correct positions in all scenarios…
Here's a simple as pie exercise from an end-user perspective. In the configuration below, push the horizontal and vertical guides onto the target point. (Click on the image to see the whole landscape):
We have custom rulers units (e.g. millimeters horizontally, inches vertically) and an arbitrary origin. It is not even mentioned if RulerOrigin
is in “Per Page”, “Per Spread”, or “On Spine” mode. To make matters worse, both the spread and the page are transformed, independently. Of course this is not an everyday pattern, but I use it deliberately to reinforce the stability of my scripts, always keeping in mind that the user can click the fearsome Rotate Spread View feature.
The first notable thing is that, in such a situation, the Transform panel no longer shows any coordinates when one of my guides is selected:
If the GUI goes silent, it is because the guides are anchored to the Spread Coordinate Space, which then conflicts with ruler axes. The difference between a Guide
object and a traditional PageItem
is that the latter still has coordinates and bounding boxes in the different projection spaces. By nature a guide only has one location
(abscissa or ordinate, depending on its orientation
attribute) and it is only reflected in the spread canvas.
Note. - Even so, the Guide
object exposes the resolve()
and transformValuesOf()
methods from InDesign 8.0, which means it has owned a private transformation matrix since then. It does not support transform()
though.
There then remains a major problem: the location
property — as well as the move()
method — still expects coordinates in the ruler system, which is assumed to correlate with the user interface viewport and measurement units. In fact, we've already documented that things are more complicated regarding the Ruler System, but with regard specifically to guides, it seems that the whole question comes down to expressing a coordinate, dictated by the transform state of the spread, as a graduation in the ruler metrics.
It now remains to understand how this graduation is calculated. The simplest thing we can do is to display it thru alert(myGuide.location)
, which returns a value in the corresponding measurement unit (horizontal for vertical guides, vertical for horizontal guides). But with respect to the origin of the ruler system (see figure below), it does not seem easy to see what this position corresponds to.
So I fumbled very naively starting from the code below:
// Let // - `spd` be the Spread under consideration. // - `pge` be some target Page in spd, e.g `spd.pages[0]` // - `hg` be some Horizontal Guide on spd // - `vg` be some Vertical Guide on spd. const CSPB = +CoordinateSpaces.pasteboardCoordinates; const CSIN = +CoordinateSpaces.innerCoordinates; const APCC = +AnchorPoint.centerAnchor; // 1. Ruler orig. in PASTEBOARD space, aka RL->PB translation [pt] var roPB = pge.resolve( [[0,0],0], CSPB )[0]; // 2. PAGE-to-RULER matrix var PG_RL = pge.transformValuesOf(CSPB)[0] // PG->PB .translateMatrix( -roPB[0], -roPB[1] ); // PB->RL[pt] // 3. Determine the Target location in RL[pt] // (For my exercise, I chose the center of the page.) var pos = PG_RL.changeCoordinates(pge.resolve(APCC,CSIN)[0]); // 4. Reposition the guides. vg.location = pos[0] + 'pt'; hg.location = pos[1] + 'pt';
This method of calculation seems pretty logical. We obtain the coordinates of the target point (the center of the page) in the ruler system, then we assign those very coordinates to the respective guides.
But this won't work in the most devious cases, as you see below:
Let us, however, focus on the method used. First, we always rely on the PASTEBOARD_COORDINATES
space as an absolute reference frame, because we know that it reflects the orientation and orthogonality of the Ruler system. Neither rotation nor shear can take place here, and one can also ignore unit scaling issues as long as one explicitly provides final coordinates in points using ...+'pt'
. Then, whenever we need to convert a given location in ruler coordinates, we go through the associated Pasteboard matrix, but correcting it with an inverse translation relative to the ruler origin. That's all the Page-to-Ruler matrix (PG_RL
in my snippet) is about.
The reason this doesn't quite work in all possible cases is that we don't (yet) use the SPREAD transform state. Our pos
variable contains indeed coordinates (rx,ry) that point exactly to the target location, in the basic, orthogonal Ruler system.
But as for Guide
locations, InDesign requires a much more subtle value: the target location expressed in the perspective of the Spread
space, that is, with respect to the orientation of its axes.
Note, however, that the desired (X,Y) coordinates — X for the vertical guide, Y for the horizontal one — are in no way governed by the SPREAD_COORDINATES
space origin. They still depend on the ruler origin and must fit that particular system.
There are definitely ways to convert (rx,ry) to (X,Y) using trigonometry, but I would like to illustrate a method strictly based on the InDesign DOM. What we need here is, roughly speaking, a tool that maps Ruler axes to Spread axes. In other words, we are looking for the PASTEBOARD-to-SPREAD matrix stripped of any translation component — since we remain glued to the ruler origin.
Steps 4a/4b/4c introduced in the revised code below provide the fix:
// FIXED VERSION // Let // - `spd` be the Spread under consideration. // - `pge` be some target Page in spd, e.g `spd.pages[0]` // - `hg` be some Horizontal Guide on spd // - `vg` be some Vertical Guide on spd. const CSPB = +CoordinateSpaces.pasteboardCoordinates; const CSIN = +CoordinateSpaces.innerCoordinates; const APCC = +AnchorPoint.centerAnchor; // 1. Ruler orig. in PASTEBOARD space, aka RL->PB translation [pt] var roPB = pge.resolve( [[0,0],0], CSPB )[0]; // 2. PAGE-to-RULER matrix var PG_RL = pge.transformValuesOf(CSPB)[0] // PG->PB .translateMatrix( -roPB[0], -roPB[1] ); // PB->RL[pt] // 3. Determine the Target location in RL[pt] // (For my exercise, I chose the center of the page.) var pos = PG_RL.changeCoordinates(pge.resolve(APCC,CSIN)[0]); // ------------------ FIX --------------------- // 4a. PASTEBOARD-to-SPREAD matrix var mx = spd.transformValuesOf(CSPB)[0] // SP->PB .invertMatrix(); // PB->SP // 4b. Remove translation attributes var tx = -mx.horizontalTranslation; var ty = -mx.verticalTranslation; mx = mx.translateMatrix( tx, ty ); // 4c. Refine the location accordingly pos = mx.changeCoordinates(pos); // -------------------------------------------- // 5. Reposition the guides. vg.location = pos[0]+'pt'; hg.location = pos[1]+'pt';
Result:
Note. - After some quick testing, I couldn't completely figure out how the Guide.move()
method works under the same conditions. Perhaps I will elaborate on this later.
• See also:
— Spaces and Transformations in InDesign (the definitive guide);
— The Magic Parent Bounding Box.