Overview
The InDesign Scripting DOM provides full access to the creation and manipulation of all content within InDesign. By combining this the speed of Lua with the data access and control facilities of EasyCatalog, it’s possible to create data driven content in virtually any format.
Accessing
The InDesign Scripting DOM can be accessed via Lua within Formatting Rules and Script Label contexts. The DOM is accessed by calling the getdom() method of a Frame or Formatting Rule. An optional parameter can be used to specify the object required, the default being the frame type. Options also include Document or Application. For formatting rules we recommend using getdom(“Document”) rather than navigating the hierarchy as there are instances where the rules are placed outside page bounds.
1 2 3 4 5 | -- access the DOM of a frame framedom = frame:getdom(); -- access the DOM of the document the frame is in documentdom = frame:getdom("Document"); |
Once the DOM is acquired, properties and methods are accessed using the same naming conventions as Extendscript. However there are few language related differences:
Properties are functions
To set a property, provide a parameter. To get a property omit the parameter:
1 2 3 4 5 6 7 8 | -- aquire the DOM domframe = frame:getdom(); -- set a property domframe:bottomRightCornerOption("CornerOptions.ROUNDED_CORNER"); -- get a property value = domframe:bottomRightCornerOption(); |
Enumeration are strings
Lua does not support typed enumerations. They are implemented as strings as shown the following example. Also notice the use of colors():add()ย (as properties are functions):
1 2 3 4 | col = document:colors():add(); col:model("ColorModel.PROCESS"); col:space("ColorSpace.CMYK"); col:colorValue( {20,100,100,0} ); |
Compared this to the ExtendScript version:
1 2 3 4 | var myColorA:Color = myDocument.colors.add(); myColorA.model = ColorModel.PROCESS; myColorA.space = ColorSpace.CMYK; myColorA.colorValue = [20, 100, 100, 0]; |
Determining object types
The Javascript constructor.name method should be replaced with a string comparison on the object itself. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function tablelength(T) local count = 0 for _ in pairs(T) do count = count + 1 end return count end app = frame:getdom("Application"); -- 'selection' returns a table of objects. Tables need to be counted in Lua s = app:selection(); if tablelength(s) > 0 then if tostring(s[1]) == "Rectangle" then -- it's a rectangle! end end; |
Adding an Object With Properties
Some add methods allow the specification of properties. Lua tables can be used where the table entry key is the property name and the value is the value:
1 2 3 4 | framedom = frame:getdom(); page = framedom:parentPage(); frames = page:textFrames(); myframe = frames:add( { bottomRightCornerOption = "CornerOptions.ROUNDED_CORNER", geometricBounds ={72, 72, 190, 190} }); |
Working with Collections
Most of the available objects in the DOM have a collection counterpart. Regarded as a class, a collection is nothing but a generic interface giving an easy and ordered access to homogeneous InDesign objects that live together. Collections support the methods add, itemByName, itemByID, item, firstItem, lastItem, middleItem, anyItem, previousItem, nextItem, itemByRange, everyItem.
Accessing a collection is a case of accessing the property containing the collection. If the property method is passed a parameter, this is assumed to be an implied item() call. For example:
1 2 3 4 5 6 7 8 | -- frame is a text frame, get the DOM: framedom = frame:getdom(); -- this uses the collection method item to return the nth item framedom:parentStory():texts():item(0):contents("This is an InDesign story with a table.") -- can also be expressed by passing the index of the text item to the texts method: framedom:parentStory():texts(0):contents("This is an InDesign story with a table.") |
Special Collection Objects
All collections support theย everyItem() and theย itemByRange() methods. These return a single object containing references to multiple objects of the same type. Properties and methods which are exposed to this object apply to all contained objects. This gives a performance boost as it reduces the number of transactions in the document database. For example:
1 2 3 4 | framedom = frame:getdom(); mytable = framedom:tables():add( { columnCount = 2, bodyRowCount = 2}); t_properties = { fillColor = "Black", bottomInset = "15mm", topInset = "15mm"}; mytable:cells():everyItem():properties(t_properties); |
In addition to this method of access, an optional false boolean parameter can be supplied to return a table of objects rather than a special collection object:
1 2 3 4 5 6 7 8 | framedom = frame:getdom(); mytable = framedom:tables():add( { columnCount = 2, bodyRowCount = 2}); table_of_cells = mytable:cells():everyItem(false) count = 0 for _ in pairs(table_of_cells) do count = count + 1 end error(count) |
Because collections offer a performance boost when setting properties a Collection.new allows the creation of singleton object from a table of objects(of the same type):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | framedom = frame:getdom(); mytable = framedom:tables():add( { columnCount = 10, bodyRowCount = 10}); -- populate a table with every other cell every_other_cell = { } for i=1,mytable:cells():count(),2 do table.insert(every_other_cell, mytable:cells(i-1)); end -- create a new singleton new_collection = Collection.new(every_other_cell); -- set the properties in one command cell_properties = { fillColor = "Black" }; new_collection:properties(cell_properties); |
Examples
Formatting Rule Post Processing Example to iterate through the frames in a rule:
1 2 3 4 5 6 7 8 9 10 11 | -- aquire the DOM dom = formattingrule:getdom(); domdoc = formattingrule:getdom("Document"); blackcolor = domdoc:colors():itemByName("Black"); tbl = dom:allPageItems(); for _,item in pairs(tbl) do if item:label() == "b" then item:fillColor(blackcolor) end item:bottomRightCornerOption("CornerOptions.ROUNDED_CORNER"); end |
Creating a dialog box:
1 2 3 4 5 6 7 8 9 10 11 | app = frame:getdom("Application"); dlgRef = app:dialogs():add({name = "dlgName", canCancel = true , label = "dlgLabel"}); dlgColumn = dlgRef:dialogColumns():add(); dlgRow = dlgColumn:dialogRows():add(); rGroup = dlgRow:radiobuttonGroups():add(); rGroup:radiobuttonControls():add({staticLabel = "Hi-res_PDF", checkedState = true}); rGroup:radiobuttonControls():add({staticLabel = "Low-res_PDF"}); rGroup:radiobuttonControls():add({staticLabel = "PRINTER"}); ok = dlgRef:show(); if ok == true then error ("ok was pressed") end Script Label example to insert text and a table into a text frame: |
Creating a table and inserting text:
1 2 3 4 5 6 7 8 9 10 | framedom = frame:getdom(); framedom:parentStory():texts():item(0):contents("This is an InDesign story with a table.") mytable = framedom:tables():add( { columnCount = 2, bodyRowCount = 2}); framedom = frame:getdom(); framedom:parentStory():texts():item(0):contents("This is an InDesign story.") -- insert a table mytable = framedom:tables():add( { columnCount = 2, bodyRowCount = 2}); for i=1,mytable:cells():count() do mytable:cells():item(i-1):texts():item(0):contents("hello" .. i); end |
Populating Tables with Tabular Fields
In this example, a tabular field is used to populate a prototype table, which is repeated enough times to hold data for each row:
Template frame contains 4 column table with no data:
The Script label on the frame controls population of the data:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | function duplicate_table(t) myInsertionPoint = t:storyOffset(); imyInsPointIndex = myInsertionPoint:index(); myStory = myInsertionPoint:parentStory(); myCharThatHoldsMyTable = myStory:characters(imyInsPointIndex); myNewTablePosition = dom:insertionPoints():nextItem(myInsertionPoint); myCharThatHoldsMyTable:duplicate("LocationOptions.AFTER",myNewTablePosition) end data = field('tabular field') myRowCount = data:rowcount(); tables_needed = myRowCount /4 if myRowCount % 4 == 0 then tables_needed = tables_needed -1 end dom = frame:getdom(); t = dom:tables(0); for i = 1, tables_needed do duplicate_table(t) end dom = frame:getdom("TextFrame"); offset = t:storyOffset(); t:cells():length(); table_index = 0; table_col_index = 0 for row =1,data:rowcount() do cell_content =data:cell(row,1):getcontent() dom:tables(table_index):cells(table_col_index):texts(0):contents(cell_content); table_col_index = table_col_index + 1 if table_col_index == 4 then table_index = table_index + 1; table_col_index = 0 end end |
Result when populated:
The technique can also be applied to Formatting Rules, with the advantage of updating if the original data changes.
Accessing EasyCatalog Scripting Functions
With the scripting module installed, it’s possible to access methods exposed by EasyCatalog. The FIELD, DATASOURCE and RECORD objects are automatically converted to compatible types. The following example acquires a FIELD object and then inserts it using the ‘insertField’ method which is added by EasyCatalog to the InsertionPoint class:
1 2 3 4 5 6 7 8 | -- Get the FIELD object field = FIELD.get("some field name"); -- -1 is the end of the story insertionPoint = frame:getdom():parentStory():insertionPoints(-1); -- Insert the field insertionPoint:insertField(field); |
Example using Formatting Rule Post Processing Logic to create a table that mirrors the contents of a named tabular field. The Formatting Rule is an empty text box.
1 2 3 4 5 6 7 8 9 10 | framedom = formattingrule:getdom(); field = SELECTION:root():getfield("producto_imgs"); table = field:content(); mytable = framedom:tables():add( { columnCount = table:colcount(), bodyRowCount = table:rowcount()}); for row=1,table:rowcount() do for col=1,table:colcount() do insertionPoint = mytable:rows(row-1):cells(col-1):insertionPoints(-1) insertionPoint:insertField(field,row-1,col-1); end end |
Debugging
In CC 2018 and above remote debugging is supported using the ZeroBrane Studio IDE. Debugging is only available when “Enable Sockets” is set to true in Advanced preferences. Re-launch InDesign to apply this setting. To Debug:
Copy mobdebug.lua and socket.lua into any location checked by the ‘require’ command (A list is shown if the module cannot be found). These are available from the ZeroBrane download.
Launch the IDE. Go to Project | Start Debugger Server and start the debugger server (if this menu item is disabled, the server is already started)
Add the following to your script:
1 | require('mobdebug').start() |
Test your script. You should see a green arrow pointing to the next statement after the start() call in the IDE and should be able to step through the code.