1/ Pages, Coordinates, Geometry
Aligning all PageItems to the Left and Top Margin
Detecting and Fixing Corrupted Page Matrix
Resizing Pages based on some PageItem
Drawing a Square Constrained by the Page Size

2/ Document, Tables and Cells
Saving Different States of a Document during some Update Process
Counting Tables
Why do Plural Commands Fail for Header Rows while they Work for Footer Rows
Breaking Link to Cell/Table Style in the Selection

3/ UI and ScriptUI stuff
Binding two DropDownLists
Removing Dialogs when Executing a JSXBIN


1/ Pages, Coordinates, Geometry

Aligning all PageItems to the Left and Top Margin

The following code should work in most contexts, including variant page margins:

const CS_INNER = +CoordinateSpaces.innerCoordinates;  
 
var pages = app.activeDocument.pages.everyItem().getElements(),  
    p, o, xy;  
 
while( p=pages.pop() )  
    {  
    o = p.marginPreferences;  
    xy = p.resolve([[-o.left,-o.top], [0,0]], CS_INNER, true)[0];  
    p.pageItems.length &&
        p.pageItems.everyItem().move([-xy[0]+'pt',-xy[1]+'pt']);  
    }
 

• Original discussion: forums.adobe.com/thread/1489740

Detecting and Fixing Corrupted Page Matrix

tomas8 pointed out a very odd issue in overriding page items by script. Under some circumstances those items do not end up at the same place they were in the master page. This problem may be related to locked layer(s) and/or locked item(s), but it can also reveals corrupted coordinate spaces, leading to unexpected shift of master items during overrides.

Note. — Since Page supports transformations, the user can alter many parameters (via the Page tool, etc.) without even realizing those side effects.

A good starting point is always to study the affine map of the master page that seems to be broken:

var page = app.activeDocument.spreads[0].pages[0],  
    mx = pg.transformValuesOf(CoordinateSpaces.parentCoordinates)[0];  
 
alert( mx.matrixValues );   // =>  1,0,0,1,-400,-300   (!) 
 

Here the target document was based on 500×300 so the default page mapping should have been [1,0,0,1,-250,-150]. Therefore the origin [0,0] of the page (that is, its top-left corner) should have been [-250,-150] in the spread coordinate space. However, the above code shows that the origin of the page is mapped to [–400,–300], which indicates that a hidden translation (dx = –150, dy = –150) affects the regular positioning of page items. We also noted that overridden items were unexpectedly moved by (+150px, +150px) on invoking myMasterItem.override(myPage).

The issue is visible in IDML export as well. Extracting the spread-related XMLs we could observe that all page elements had the following attributes:

GeometricBounds="150 150 450 650" ItemTransform="1 0 0 1 -400 -300"  
 

instead of:

GeometricBounds="0 0 300 500" ItemTransform="1 0 0 1 -250 -150"  
 

Investigating on this issue I found that the matrix Page.masterPageTransform (property available in CS5 and later) was reflecting the shift we were trying to circumvent. When a new page—based on any master spread—is created in the corrupted document, InDesign CS6 and CC bring the following result:

    alert( myNewPage.masterPageTransform.matrixValues );  
 
    // => 1,0,0,1,-150,-150      !!! 
 

As far as I understand this matrix determines the transformation “applied to the master page before it is applied to Page,” that is, how the page is mapped to its master page. In normal case that should be—I suppose—the IDENTITY matrix. My guess is that InDesign CS6/CC improperly uses those matrix values during scripted overrides, so that we have to explicitly apply the inverse matrix to the newly created page. The following code demoes that workaround:

var document = app.activeDocument;    
 
var csvData = {
    // here some data used by the original script  
    "master" : ["A-Master", "B-Master", "C-Master"],  
    "numberOfRows" : 3  
    };  
 
loadPagesAndOverrideElements(document, csvData);  
 
 
function loadPagesAndOverrideElements(document, csvData)  
{  
    // Constants  
    // ---  
    const CS_INNER = +CoordinateSpaces.innerCoordinates,  
          ORIGIN = [[0,0],CS_INNER];  
 
    // Variables  
    // ---  
    var i, n = csvData.numberOfRows,  
        ms, pg, mx;  
 
    // Unlock ALL layers  
    // ---  
    document.layers.everyItem().locked = false;  
 
    // Page creation loop  
    // ---  
    for( i=0 ; i < n ; ++i )  
        {  
        // Select the master spread  
        // ---  
        ms = document.masterSpreads.
            itemByName(csvData["master"][i]);  
 
        // Create new page and override master items  
        // ---  
        ms.pageItems.everyItem().
            override( pg=document.pages.add({appliedMaster:ms}) );  
 
        // Revert the masterPageTransform if needed  
        // ---  
        (mx=pg.properties.masterPageTransform) &&
            pg.transform(CS_INNER, ORIGIN, mx.invertMatrix());  
        }  
 
    // Remove the 1st page  
    // ---  
    document.pages[0].remove();  
};
 

• Original discussion: forums.adobe.com/thread/1455184

Resizing Pages based on some PageItem

therodee wrote: “I have multiple pages on single InDesign document, each page has one single text frame which contains a table with different heights. I'd need to resize the pages based on the size of the text frame. Since there is more than 100 pages, doing it by hand with Page Tool is a bit of frustrating task.”

In InDesign CS5.5 and later one can take advantage of the Page.reframe() method, which avoids to manage contexts where PageItem.geometricBounds is not reliable anymore—transformed frames, rotated views, unusual measurement units, ruler-per-page mode, etc.

From the initial state shown below,

Initial state of the pages.

the user probably expects the following result:

Expected final state.

A good approach is to consider both Page and TextFrame objects as simple spread items and to use the SPREAD COORDINATE SPACE as a reliable reference. Dimensions and bounding boxes are then seen from the spread perspective, and the code remains very compact:

// ==========================
// SHOULD WORK IN ID CS5.5 AND LATER  
// Fit pages to 1st textframe  
// ==========================
 
const CS_SPREAD = +CoordinateSpaces.spreadCoordinates,  
      BB_VISIBLE = +BoundingBoxLimits.outerStrokeBounds,  
      TOP_LEFT = [[0,0],BB_VISIBLE,CS_SPREAD],  
      BOT_RIGHT = [[1,1],BB_VISIBLE,CS_SPREAD],  
      // ---  
      getXY = function(o,c){ return o.resolve(c, CS_SPREAD)[0] },  
      // ---  
      fitToFirstFrame = function(/*Pages*/o,  i,t)  
      {  
      for(  
           i=o.count() ; i-- ; (t=o[i].textFrames[0]).isValid &&  
           o[i].reframe(CS_SPREAD,[getXY(t, TOP_LEFT),getXY(t, BOT_RIGHT)])  
         );  
      };  
 
// ---  
// SAMPLE CODE  
// ---  
var curDoc = app.properties.activeDocument;  
curDoc && fitToFirstFrame(curDoc.pages);
 

As you can see there is no need to manage custom measurement units or to manually address weird cases.

• Original discussion: forums.adobe.com/thread/1463369

Drawing a Square Constrained by the Page Size

How to draw a square located at the bottom-right corner of a page and meeting the below conditions:

How to automatically create this square?

Let W denotes the page width, H the half of the page height, and x the square edge. The Intercept theorem (Thales) leads to:
    x = ( W × H ) / ( W + H ).

The good old Intercept Theorem!

From then we have all the information to build the Rectangle. Here is a raw implementation (assuming uniform measurement units and no weird transformations applied):

var pg = app.layoutWindows[0].activePage,  
    bs = pg.bounds,  
    w = (bs[3]-bs[1]),      // page width  
    h = (bs[2]-bs[0])/2,    // half of the page height  
    x = (w*h)/(w+h);  
 
bs[0]=bs[2]-x;  
bs[1]=bs[3]-x;  
pg.rectangles.add({fillColor:'Black',geometricBounds:bs});
 

• Original discussion: forums.adobe.com/thread/1435587

2/ Document, Tables and Cells

Saving Different States of a Document during some Update Process

The method Document.saveACopy() allows you to backup a document state without altering the current Document specifier that the script is dealing with. In the following example, the task was to successively import XML data into an InDesign document (template.indd) and to save as a new document each intermediate state into an output folder. The key point here is to avoid myCurrentDoc.save() and to use instead myCurrentDoc.saveACopy(outputFile) so that one can continue to safely work with the current specifier.

// YOUR SETTINGS  
// ---  
const MY_TEMPLATE_PATH = "/E/template.indd";  
const OUTPUT_FOLDER_NAME = "Outputs";  
 
// THE WHOLE PROCESS  
// ---  
(function()  
{  
    // Best is to declare your variables at the beginning of the block  
    // ---  
    var myTemplateFile,          // the template File object  
        myFolderWithFiles,       // folder selected by the user  
        myXMLDocs,               // array of XML files  
        outPath,                 // output folder path  
        myTemplate,              // the template Document  
        i,                       // loop index  
        xml,                     // the current XML File  
        outFile;                 // the current exported doc file  
 
    // Make sure the INDD template file exists  
    // ---  
    if( !(myTemplateFile=File(MY_TEMPLATE_PATH)).exists )  
        {  
        alert("Template not found!");  
        return;  
        }  
 
    // Let the user choose a XML source folder  
    // ---  
    if( !(myFolderWithFiles=Folder.selectDialog ("Choose a source folder for XMLs")) )  
        {  
        // Cancelled by the user  
        return;  
        }  
 
    // Collect the XML files (non-empty array required to continue)  
    // ---  
    myXMLDocs = myFolderWithFiles.getFiles("*.xml");  
    if( !myXMLDocs || !myXMLDocs.length )  
        {  
        alert("No XML file found!");  
        return;  
        }  
 
    // Format the output folder path, create it if needed  
    // ---  
    outPath = myFolderWithFiles.absoluteURI + '/' + OUTPUT_FOLDER_NAME;  
    if( !Folder(outPath).exists && !(new Folder(outPath)).create() )  
        {  
        alert("Unable to create the output folder!");  
        return;  
        }  
 
    // Open the template once  
    // Note: working on a copy prevents from modifying the  
    // original. Just a good practice, not required here.  
    // ---  
    myTemplate = app.open(myTemplateFile, true, OpenOptions.OPEN_COPY);  
    if( !myTemplate.isValid )  
        {  
        alert("Unable to open the template!");  
        return;  
        }  
 
    // Loop in myXMLDocs  
    // ---  
    for( i=myXMLDocs.length ; i--  ; )  
        {  
        // Get the XML file  
        // ---  
        xml = myXMLDocs[i];  
 
        // Import (or re-import!) xml into myTemplate  
        // ---  
        myTemplate.importXML(xml);  
 
        // Make sure that the output file doesn't exist yet  
        // ---  
        outFile = File(outPath + '/' + xml.name.replace(/xml$/,'indd'));  
        if( outFile.exists )  
            {  
            alert( "The file " + outFile.name + " already exists!" );  
            break;  
            }  
 
        // Save a COPY of myTemplate in its current state  
        // ---  
        myTemplate.saveACopy(outFile);  
        }  
 
    // Finally, close the template without saving  
    // ---  
    myTemplate.close(SaveOptions.NO);  
 
})();
 

• Original discussion: forums.adobe.com/thread/1454331

Counting Tables

If you need to compute the exact number of tables contained within a document, you can take advantage of the U+0016 encoding of table anchors and then invoke myDoc.findText(), as shown below:

function countTables()  
{  
    app.findTextPreferences = null;  
    app.findTextPreferences.findWhat = "\x16";  
    var t = app.properties.activeDocument||0;  
    return t&&t.findText().length;  
};  
 
alert( countTables() ); 
 

• Original discussion: forums.adobe.com/thread/1396092

Why do Plural Commands Fail for Header Rows while they Work for Footer Rows

When myTable.rows.everyItem().properties=... is executed, rows are processed by increasing index. But row[i] cannot be set to RowTypes.BODY_ROW as long as row[i+1] is a header row. Therefore row[0], row[1] ... row[headerRowCount-2] are not converted into BODY_ROW and the command continues its course on the remaining rows. (As you know, the syntax everyItem().properties=obj bypasses unproper assignments.) By contrast, the last rows are properly set to BODY_ROW, as there is no problem in converting footer rows into body rows by increasing index.

This leads to an interesting question: can we control the order by which items are processed through a plural specifier? As to everyItem() I don't think so. But itemByRange() seems to open up possibilities.

Ideally one would like to do:

// descending indexes for header rows
// ---
rows.itemByRange( headerRowCount-1, 0 ).properties = obj;
 

and:

// ascending indexes for footer rows
// ---
rows.itemByRange( -footerRowCount, -1 ).properties = obj;
 

The footer part works fine (as expected), but the header stuff still leads to the original issue—only the last header row is converted!

However, the specifier is properly parsed "...row[N] to ...row[0]" as revealed by toSpecifier(). In my view the rows are passed in the desired order, but something else overrides this option.

It's time to remember that Row and Column objects are just wigs over actual Cells. Behind the scene our range of rows still needs to be resolved as a pure range of cells. During this process I suspect that the subsystem reorders the implied targets in an optimal way. So "...row[N] to ...row[0]" silently becomes "...cell[0] to ...cell[idx]" and our efforts in vain! Well.

This reasoning has led me to try a more explicit approach: instead of using myTable.rows.itemByRange(N,0)... let's directly provide the descending cell range: myTable.cells.itemByRange(idx,0).

Hence my final suggestion:

// var myTable = ...  
 
var o = {rowType: +RowTypes.BODY_ROW},  
    x;  
 
if( x=myTable.headerRowCount )  
    {  
    x = myTable.rows[x-1].cells[-1].id;  
    myTable.cells.itemByRange(x,0).properties = o;  
    }  
 
if( x=myTable.footerRowCount )  
    myTable.rows.itemByRange(-x,-1).properties = o;
 

• Original discussion: forums.adobe.com/thread/1389282

Breaking Link to Cell/Table Style in the Selection

The process of cleaning cell styles is somewhat ambiguous. The following function provides increasing levels of cleaning, from the minimalist clearCellStyleOverrides(false) to a complete removing of style attributes for the currently selected cells:

var breakLinkToCellStyleSelection = function F(/*0|1|2|3*/LEVEL)  
// -------------------------------------  
// LEVEL 0  => only clear overrides relative to *defined* style attrs
// LEVEL 1  => clear all overrides (through root style)  
// LEVEL 2  => clear all overrides and apply [None]  
// LEVEL 3  => apply [None] *in depth* (i.e. release all attributes)  
{  
    // Cache  
    // ---  
    F.RE_DOC_SPEC || (F.RE_DOC_SPEC=/\/document\[[^\]]+\]/);  
 
    var o = app.properties.selection && app.selection[0];  
 
    // Validation  
    // ---  
    if( !o ) return 0;  
    while( !('cells' in o) )  
        {  
        if( 'cells' in o.parent )  
            { o=o.parent; break; }  
        if( ('tables' in o) && o.tables.length )  
            { o=o.tables.everyItem(); break; }  
        return 0;  
        }  
 
    // [None] style  
    // ---  
    var NONE_STYLE = resolve((o.toSpecifier().
        match(F.RE_DOC_SPEC)||['/'])[0]).  
        cellStyles.itemByName("$ID/[None]");  
 
    // Process  
    // ---  
    o = o.cells.everyItem();  
    o.clearCellStyleOverrides(!!LEVEL);  
    if( 1 < LEVEL ) o.appliedCellStyle = NONE_STYLE;  
    if( 2 < LEVEL ) o.properties = NONE_STYLE.properties;  
};  
 
// warning: deep cleaning!
// ---
breakLinkToCellStyleSelection(3);
 

• Original discussion: forums.adobe.com/thread/1358267

3/ UI and ScriptUI stuff

Binding two DropDownLists

Having a master DropDownList containing [itemA, itemB, itemC...], we want to dynamically feed a slave DropDownList depending on the selected item.

An associative array of arrays may be used to represent that data structure, for example:

var data = {  
    'ItemA' : ['SubA1', 'SubA2', 'SubA3', 'SubA4' /*...*/],  
    'ItemB' : ['SubB1', 'SubB2', 'SubB3' /*...*/],  
    'ItemC' : ['SubC1', 'SubC2', 'SubC3', 'SubC4', 'SubC5' /*...*/],  
    // etc.  
    }; 
 

Two suggestions before we go further:
• Keep your data structure as abstract as possible, and independent from the UI.
• Do not hardcode the bounds of your controls (as this inhibits the auto layout mechanism).

Now to the final code:

const MIN_DD_SIZE = [100, 50];  
 
var data = {  
    'ItemA' : ['SubA1', 'SubA2', 'SubA3', 'SubA4' /*...*/],  
    'ItemB' : ['SubB1', 'SubB2', 'SubB3' /*...*/],  
    'ItemC' : ['SubC1', 'SubC2', 'SubC3', 'SubC4', 'SubC5' /*...*/],  
    // etc.  
    }; 
 
var keys = [], z = 0,  
    w, g, col1, col2;  
 
// Feed keys (= col1 strings)  
// ---  
for( keys[z++] in data );  
 
// Create the UI  
// ---  
w = new Window('dialog',"Test");  
(g = w.add('group')).orientation = 'row';  
(col1 = g.add('dropdownlist', undefined, keys)).minimumSize = MIN_DD_SIZE;  
(col2 = g.add('dropdownlist')).minimumSize = MIN_DD_SIZE;  
col1.proxy = col2;  
 
// Change item event  
// ---  
col1.onChange = function()  
{  
    var dd = this.proxy,  
        t, a, i;  
 
    dd.items.length && dd.removeAll();  
    if( !(t=this.selection) ) return;  
    for( a=data[t.text],t=a.length,i=0 ; i < t ; dd.add('item',a[i++]) );  
    t && (dd.selection=a[0]);  
};  
 
// Init. & Go  
// ---  
col1.selection = keys[0];  
col1.onChange();  
w.show(); 
 

• Original discussion: forums.adobe.com/thread/1391891

Removing Dialogs when Executing a JSXBIN

callentill1308 wrote: “I am using a jsxbin file inside a loop. After the jsxbin file is finished a script alert popup dialog appears saying it is done and I am forced to click the OK button before it will allow my script to continue. I am running the jsxbin file from app.doscript("[jsxbin string here]"). (…) I am trying to avoid having to hit the OK button multiple times as the script runs.”

If you are sure that the JSXBIN script actually calls the alert() function, a radical way to bypass the message is to rewrite that function itself!

// inhibits alert()
// ---
$.global.alert = function(){ };
 
app.doScript( "[jsxbin string here]" );
 

• Original discussion: forums.adobe.com/thread/1379256