Tuesday, July 21, 2009

Some baffling behavior in InDesign CS3 scripts explained - beware of relative object references

Lesson learned: be careful with references to page items in InDesign ExtendScript CS3 - you might be referring to the wrong object!

The issue described below seems fixed in InDesign ExtendScript CS4 - but if you need to support CS3, you should be aware of it.

Look at this code snippet:

var itemB = app.activeDocument.pageItems.item(1);
alert(itemB.id);
... do some stuff that does not affect itemB ...
alert(itemB.id);

What would you expect to happen? You'd expect to see the same id value displayed in two alerts, right?

itemB refers to a particular page item, and should continue to do so. Turns out that is not the case in InDesign CS3.

Try this:

Create a new document, and create three rectangular frames on the first page.

Create the following script:

var itemA = app.activeDocument.pageItems.item(0);
var itemB = app.activeDocument.pageItems.item(1);
var itemC = app.activeDocument.pageItems.item(2);
alert(itemB.id);
itemA.remove();
alert(itemB.id);

You would expect itemB not to be affected by removing itemA - but it is! Run the script and you'll get two different id values displayed in the dialog.

I don't really know for sure what is going on but I have a theory - it looks like itemB has 'become' itemC.

I suspect that InDesign CS3 does not really keep a 'hard' reference to any particular item in the itemB variable - instead it has what I'd call a 'relative reference'.

In other words, itemB is not fixed - it always remains equivalent to 'app.activeDocument.pageItems.item(1)' - and if the page item list changes 'underneath', then itemB suddenly will refer to another item.

After the remove(), the page item list of the document has changed - the item at index 1 is now a different item. So the variable itemB suddenly refers to a different item.

Can we create hard references that don't suffer from this issue? Turns out we can - so there is a solution.

Change the script to read:

var itemA = app.activeDocument.pageItems.item(0);
var itemB = app.activeDocument.pageItems.item(1);
var itemC = app.activeDocument.pageItems.item(2);
itemB = app.activeDocument.pageItems.itemByID(itemB.id);
alert(itemB.id);
itemA.remove();
alert(itemB.id);

That looks like a do-nothing: replace the reference to itemB with another reference to itemB

But internally, InDesign will now refer to 'the item with id=1234' (assuming itemB had an id equal to 1234). itemB.id uniquely identifies itemB, and it is not affected by items being added or removed from the page item list.

So this updated script works as expected - we see the same id displayed before and after the remove.

We can write a small helper function for this:

function FreezeReference(theItem)
{
// Harbs improved my original attempt by
// appending .getElements()[0] - thanks
// Harbs!
return theItem.parent.pageItems.itemByID(theItem.id).getElements()[0];
}

var itemA = app.activeDocument.pageItems.item(0);
var itemB = FreezeReference(app.activeDocument.pageItems.item(1));
var itemC = app.activeDocument.pageItems.item(2);
itemB = app.activeDocument.pageItems.itemByID(itemB.id);
alert(itemB.id);
itemA.remove();
alert(itemB.id);

This also works as expected.

Uncovering this issue was not as straightforward as it might seem - at first, it looked like I was struggling with a bug in our APIDToolAssistant plug-in: Harbs (www.in-tools.com) who is an advanced user of APIDToolAssistant reported some issues with the 'move into' function provided by APIDToolAssistant.

This particular ExtendScript enhancement allows you to move one page item into another very quickly; much faster than could be achieved with pure scripting - you'd do something like this:


// Some 'magic' numbers - C++ meets ExtendScript
const IID_IACTIVEPAGEITEMSCRIPTUTILITIESEXTENSION = 0x90B6C;
const kOpCode_MoveInto = 10016;

...

function MoveItemIntoItem(newParent,newChild)
{
app.callExtension(
IID_IACTIVEPAGEITEMSCRIPTUTILITIESEXTENSION,
kOpCode_MoveInto,
newParent,
newChild);
}

var itemA = app.activeDocument.pageItems.item(0);
var itemB = app.activeDocument.pageItems.item(1);
var itemC = app.activeDocument.pageItems.item(2);
alert(itemB.id);
MoveItemIntoItem(itemA,itemB);
alert(itemB.id);


app.callExension() 'calls into' the APIDToolAssistant plug-in to perform the move.

He noticed that he got invalid object references after calling this function in CS3.

Turned out APIDToolAssistant was not the culprit - after some digging I finally figured out what was going on.

Hope this is useful!

Cheers,

Kris

3 comments:

harbs said...

Cool (and weird) stuff! ;)

Great detective work!

pedalgrl said...

I have been searching all day for just a quick and dirty sample of how I might insert 2 words into an existing InDesign CS3 text box on a page.

I have recorded quick keys back in the Quark days, I have recorded PhotoShop Actions from the early days into the present time, I have dabbled a little in the OS X Automator, but for the life of me this damn CS3/JavaScript coding is making me old...fast.

How may I simple run the script and see the words "Hello World" appear in my text frame?

I have this much:


tell application "Adobe InDesign CS3"
activate

//I have no idea in here//

end tell

If there is anyone who could throw me a bone here. I am fairly certain this code shouldn't be a page long, but all of the samples I can find are so convoluted. ARGHH!! Thank you. PEACE.

Kris Coppieters said...

I am certain the solution to your question is not difficult - I don't normally do AppleScript, I prefer to use ExtendScript, and in ExtendScript it's something like (untested):

app.activeDocument.textFrames.item(0).contents = "Hello World";

In AppleScript, I am sure, it's also a trivial AppleScript expression - especially trivial compared to any 'real' useful script.

However, your question indicates you want to try to take a short cut and avoid the hard work that all of us experienced scripters have been putting in to gain the knowledge and experience. Even if I dug up my AppleScript info and gave you the correct magical incantation, that would not really help you; I am sure you'd be back pretty soon with the next trivial question.

You _must_ study the available documentation - there is plenty available, and until you do, things will remain difficult.

Use Google to find the documentation (it's easy to find on the Adobe web site). Look on the Adobe forums for usable snippets of code. Don't expect others to write your code for you.

Cheers,

Kris