Friday, October 9, 2009

Display Workflow-Related Meta-Info For Your Users From An InDesign Script


When developing workflow software around Adobe® InDesign® or Adobe® InDesign Server®, as a scripter, you often find yourself attaching 'meta-info' or metadata in some form or shape to various page elements in an InDesign document.

In this blog post, the samples should work with InDesign CS and above on Mac as well as on PC, and I'll use ExtendScript for InDesign for the scripts.

But the information presented here can be applied just as well in AppleScript or VBScript. The syntax is slightly different, but it works equally well.

Meta-info would be any info that is not directly related to the page layout per se, but that needs to be associated with elements of the page layout.

Examples of meta-info you might like to attach to a page item would be things like invoice data, prices, product codes, file names, script labels, XML tags, user names, document history information, destinations, job ticket data...

In many cases, this meta-info can remain hidden from the end-user and not be displayed on the page layout at all - the data is typically picked up by various automated components of your workflow (scripts, applications, plug-ins...) and used to control processes and routing of info and documents.

One of the most popular methods to attach meta-info to InDesign page items is to use the extractLabel and insertLabel methods. These two essentially allow you to 'attach' any number of arbitrary strings to any page item, where you identify each of the strings you want to stash away with a key string.

For example, try the following script.

Open or create a document, select a page item and run the following script. (Be careful: the script has no error checking whatsoever, so it is not user-friendly. You need to select exactly one page item, and with the item selected, you need to run the script via the Scripts Palette).

// Script1.js
var theItem = app.selection[0];
theItem.insertLabel("com.rorohiko.statusquo","Whatever you want tada tada whatever you need tada tada");

Nothing apparent will seem to happen - that's OK. Now run the second script, while keeping the same page item selected:

// Script2.js
var theItem = app.selection[0];
var theData = theItem.extractLabel("com.rorohiko.statusquo");
alert(theData);

This shows how you can attach a string to a page item. I do recommend that you use a globally unique string for the keys, for example, like I did, based on my reversed domain name. That helps ascertain that you don't accidentally use the same key as another scripter.

For example, if your domain name is yourcompany.be, and you want to store, say, a user name, you should write something like:

theItem.insertLabel("be.yourcompany.username","John");

instead of

theItem.insertLabel("username","John");

The latter will also work, of course, but you always run the risk that your end user might try to run two scripts from two different developers.

Imagine she tries to use a script created by yourcompany.be and also another script created by rorohiko.com, and both scripts would use the same key username for slightly different purposes. Lots of weirdness would result.

Better be safe than sorry - so if you use be.yourcompany.username, and I use com.rorohiko.username as the key to insertLabel / extractLabel, there will never be a conflict - are we cool on that?

The string you store in the keyed location can be pretty much anything - I often store a very looong string, for example, with carriage returns separating individual records, and tabs separating individual columns, and then use .split to convert the string in to a table.

const kMyDataKey = "com.rorohiko.keyforimportantdata";
//... more code ...
// Little table of data with columns separated by tabs
// and lines separated by carriage returns
// 12 13 Kris
// 15 17 John
var myImportantData = "12\t13\tKris\r15\t17\tJohn\r";
theItem.insertLabel(kMyDataKey,myImportantData);
//... more code ...
var theData = theItem.extractLabel(kMyDataKey);
theData = theData.split("\r");
for (var recordIdx = 0; recordIdx <>
{
theData[recordIdx] = theData[recordIdx].split("\t");
}
//... more code ...

Or you could encode the data you want to stash into a big XML formatted string - that works really well too - something like:

const kMyDataKey = "com.rorohiko.thecoolKeyForXMLData";
//... more code ...
var myImportantData = "";
myImportantData += "<data>"
myImportantData += "<record>"
myImportantData += "<x>12</x><y>13</y><name>Kris</name>";
myImportantData += "</record>"
myImportantData += "<record>"
myImportantData += "<x>15</x><y>17</y><name>John</name>";
myImportantData += "</record>"
myImportantData += "</data>"
theItem.insertLabel(kMyDataKey,myImportantData);
//... more code ...
var theData = theItem.extractLabel(kMyDataKey);
// Use the built-in XML parser to parse the XML data back into its components
//... more code ...

Or any other scheme you can come up with to convert the data you want to store into a string- you can do things like ASCIIEncode or ASCII85 the data if you want to store binary info in a pure ASCII string - your imagination is the limit.

Now, what if you wanted some of this data to be visible to the end-user?

For example, the end-user could use one of your scripts to tag individual page items, maybe to let your automated workflow 'see' where it needs to insert a header and where it needs to put an image, and so on.

Wouldn't it be great if you could give the user some visual feedback in this regard?

That's where our APID ToolAssistant comes in. APID ToolAssistant was conceived as a 'scripters toolkit' and it offers a whole range of functionality that could help the serious scripter.

You might have heard about APID ToolAssistant, because it offers a very 'fine grained' event model that allows scripts to be triggered by user interaction with the document - but APID ToolAssistant has a lot more to offer than just some event-driven facilities.

One of the coolest features in the APID ToolAssistant toolkit is that it offers the scripter a way to display any 'meta-info' next to a page item. To display this meta-info, APID ToolAssistant can add little, non-printing 'labels' (also known as adornments) to any page item, and in those labels, you can display anything you like: some text, or a PNG image like a logo or an icon, all under the control of your script.

These adornments are not page items - they are instead very similar in nature to the selection handles and the overset text indicator on page item frames. They are nothing more than visual cues for the user, and they are not visible in the printed end-result.

The way APID ToolAssistant works its magic is by adding some new methods and attributes to the InDesign object model. There is a pair of methods called setDataStore / getDataStore which behave very similar in many respects to how the built-in insertLabel / extractLabel work - i.e. you store data identified by a unique key.

But there's a twist - some of the unique keys have a special meaning - they are 'magical'.

If a key starts with $ADORNMENT_ and ends with $, it tells APID ToolAssistant the stored data is something that has to be interpreted as content for a little info-label.

Install APID ToolAssistant 1.0.47 or higher (you can download it from http://www.rorohiko.com/apidtoolassistant ), and enter the following script:

// Script3.js
var theItem = app.selection[0];
theItem.setDataStore("$ADORNMENT_com.rorohiko.sample$","Hello World");

Just like with the first sample scripts you need to select a single page item, and then run the script.

Is that cool or what? (If your APID ToolAssistant has been installed for a while, it might have reverted to unlicensed mode, in which case you'd see 'DEMO: Hello World' in the little info-label).

Now, this is only a simple example - instead of a simple string "Hello World" you can actually provide an array with data to setDataStore, and through various parameter values ask APID ToolAssistant to display the label on the left, right or bottom sides of the frame.

Or you can ask it to change the background color. Or for really fancy stuff, you can display a PNG image instead of text. The possibilities are endless.

To get some ideas about what you could do with this functionality, have a look at what I've done with our FrameReporter tool at http://www.rorohiko.com/framereporter . FrameReporter is really not much more than a very thin layer of code on top of APID ToolAssistant.
For more detailed info, you need to look at the information provided in the Active Page Items Developer toolkit - http://www.rorohiko.com/activepageitemsdeveloper .

The necessary documentation is included in the free demo download - you don't need to purchase the toolkit if all you want to do is use this 'meta-info-label' feature. Check the reference manual to find out about all the variations of the adornment features.

So, keep in mind - if you want to show meta-info to an end-user, all you need is a US$25 license for APID ToolAssistant (to get rid of those 'DEMO:' prefixes added by the unlicensed version), and you can script away!

More info can also be found here:

Tuesday, August 11, 2009

Apple Mail and the Drafts folder

I use Apple Mail, and generally, it does work well. However, there's one thing that annoys me: if you don't have any 'pending' draft e-mails, the Drafts mail folder is invisible and cannot be seen in the mail window.

That seems like a non-issue, but I like to use the Drafts folder for 'repetitive' e-mail. I sometimes send out a similar e-mail to two or three people, and I like to option-drag previously sent e-mail from the Sent mail folder back to the Drafts folder to make a modifiable copy.

What I used to do in previous versions of Apple Mail was the following:

1) Type in an e-mail. Hit 'Send'.
2) Open the 'Sent' folder in Apple Mail and Option-Drag the e-mail I just sent to the Drafts folder. That makes a copy of the e-mail, and makes it available as a draft.
3) Double-click the draft in the Drafts folder, and adjust it to suit the next addressee; hit 'Send'.
4) Go back to step 2 as needed.

Because in more recent versions of Apple Mail, the Drafts folder becomes invisible if it is empty, I used the following clumsy workaround:

0) Create an empty, dummy e-mail and hit 'Save as Draft'. That puts something in the Drafts folder, and I can start using my previous trick (option-drag sent e-mail into Drafts to make a modifiable copy). Go to step 1) above.
...

That works OK, but the dummy e-mail is blank, and you end up with a blank e-mail in the Drafts folder - I don't really like that.

So, finally my current workaround is to create a single, non-blank e-mail in the Drafts folder, and leave it there for eternity. It's not perfect, but I can live with that.


The Drafts folder now has a permanent mail in it.

Not the greatest trick in the world, but it made my life a little bit easier, so I thought I'd share it.

Cheers,

Kris

Tuesday, July 28, 2009

Little Known Facts About InDesign Plug-Ins - There is More To It Than Meets The Eye

Installing a plug-in for Adobe InDesign is normally fairly straightforward.

• Quit or exit out of InDesign.
• Navigate to the folder that contains the InDesign application file.
• Find the folder called 'Plug-Ins'.
• For neatness, create a subfolder inside 'Plug-Ins' - e.g. 'Third Party Plug-Ins'. Drag the plug-in icons (and possibly any associated files) into that subfolder.
• Relaunch InDesign

That's fairly easy.

But when it comes to upgrading things might become a bit more complex.

In short, you might want to first remove the previous version of the plug-in, then start and immediately exit InDesign, and only then install the updated version.

In other words - perform an 'empty' start/stop of InDesign before installing the updated plug-in.

Whether this is really necessary depends on the plug-in - most plug-ins won't need this. But if you're battling a mysterious problem, this is worth a try.

Here's what might happen.

Each time you launch InDesign it will scan the Plug-Ins folder and its subfolders, looking for any new plug-ins. If it cannot find any new plug-ins, nothing special happens - we have a bog-standard, normal InDesign launch.

But when InDesign does find a new plug-in - a plug-in it has not 'seen' before, things are different, and the launch takes longer than normal.

Most people won't notice it - the difference in launch time is not very large. If you're really observant, you'll also see some extra messages flash by in the InDesign splash screen.

Behind the curtain, InDesign is analyzing the new plug-in and extracting information from it.

For example, information on how the new plug-in can be scripted, and information about the wording on the dialogs and palettes the plug-in can display.

A lot of this extracted information is then stashed away by InDesign for 'later reference' - this to avoid having to re-analyze the same plug-in on each re-start.

So, the next time InDesign launches, it again checks for new plug-ins. If no new stuff can be found, it relies on its information stash (also known as 'cache'), and it can launch faster, because it does not have to spend time analyzing any plug-ins.

This 'trick' is called 'caching' - caches are used in many situations, to avoid needless effort to re-obtain data that has been obtained before.

For example, your web browser does it too - the first time ever you access a particular web page, it will be a tad sluggish, especially when there are a lot of graphics. Navigate to that same page a little bit later, and in most cases it will come up a lot faster.

The browser has cached the page content and instead of accessing the web site far, far away, it is showing you a copy of the page it had cached in its local web file cache. The web page comes straight off your hard disk, which is a lot faster than pulling it through the internet.

So, keep in mind: to improve launch times, InDesign caches a lot of information that is normally stored inside of the plug-in files.

Now suppose you have a plug-in installed. It's working fairly well, but there are some issues.

Some time later the software developer releases an updated version of the plug-in, which should fix the issues. You download the new version, and overwrite the old version with it.

Done? Is it that easy? Not always!

The problem is that InDesign might not scan the updated plug-in: for all it knows, it 'saw' file 'xyz.pln' before, and it still sees 'xyz.pln' - InDesign might feel no urge to re-scan 'xyz.pln'.

As a result, InDesign might be relying on the outdated info from the previous version of the plug-in that it has in its information stash.

And weird things might start to happen - things mysteriously don't work well on some computers, but work fine on others, that kind of stuff.

Morale: it might be a good idea to force InDesign to re-scan the new plug-in. To achieve that, you can use the following procedure:

0) Exit out of InDesign
1) Completely remove the plug-in you're about to upgrade
2) Launch InDesign (without the plug-in installed). It will notice the plug-in is missing, and it will erase any cached data it had extracted from it.
3) Exit out of InDesign
4) Install the updated version of the plug-in
5) Launch InDesign (with the updated plug-in installed). It will see the 'new' plug-in and re-scan it - so the cached data is now up-to-date.

Keep in mind: this info is quite general, and might not apply to your situation. But if all else fails - give it a go!

Cheers,

Kris

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

Thursday, July 16, 2009

VMware Fusion and TimeMachine

Been a while since I blogged; swamped in work.

This quick post to share a solution I found on Mac OS X to make VMware Fusion (or Parallels) and Apple's Time Machine backup work better together.

I use a fair amount of virtual Windows and Linux machines, and these tend to be primarily stored in large virtual hard disk files - 40GB, 60GB,... pretty massive.

Because Time Machine finds and copies any modified file, and the mere act of running the virtual Windows machines caused these large files to be marked as 'modified', I was faced with endless copies of massive virtual hard disk files to my Time Machine hard drive.

Figuratively speaking, a one-byte change in one of those massive Windows virtual hard disk files would cause a 60GB copy operation - which also indirectly forced older backup files to be erased from the Time Machine drive to make room for these behemoths.

Initially, I put all my virtual machines in a single folder, and explicitly excluded that folder from the Time Machine backup - not a good solution, but at least, my backup drive was not swamped with those big files.

But then, a few days ago it dawned on me - I could have my cake and eat it too!

The solution is to use the 'snapshot' feature of VMware (Parallels also has it) - you can make a snapshot of a virtual machine, so you can always 'undo' whatever happened to that virtual machine since the last snapshot.

The way VMware handles this is by 'freezing' the underlying virtual hard disk, and storing any changes made since the snapshot to the frozen hard disk in separate files.

And that's the solution: I first make sure my virtual machine is in a useful, stable state, and then I make a snapshot (I call it 'Base').

That effectively 'locks' the massive many-GB virtual drive - so running VMware does not cause it to be modified any more, and any changes from then on are kept in a bunch of much smaller snapshot files.

Time Machine now makes a copy of my 'frozen' many-GB virtual drive once, and from then on only backs up the changes that are kept in the snapshot files - which results in a much smaller backup set.

After a while, when the snapshot file size grows to many GB in size, I make a new snapshot, and delete the old snapshot. That 'merges' all changes that occurred 'between' the two snapshots into the main virtual drive, and starts with a fresh slate.

After I do that, Time Machine backs up the behemoth once again, and from then again only backs up the changes in the new set of snapshot files.

I now have a good backup of my virtual machines again without having to jump through hoops!

Cheers,

Kris

Sunday, March 22, 2009

May 11-15 InDesign Developer Training Sessions

If you’re involved in automation around the Adobe Creative Suite, you need to mark the week of May 11 - 15, 2009 on your calendar - there’s another Creative Suite Developer Summit coming up in Seattle, on the Adobe Fremont Campus

We’re running two training sessions this year - click the lines below for more info:

On May 11: InDesign Plugin Development Workshop - Getting Started With The InDesign SDK

On May 15: Feature Development for InDesign Using ExtendScript

Both workshops are limited to 12 seats, which will be allocated on a first come, first served basis - so don’t delay booking.

More about the Adobe Creative Suite Developer Conference can be found here:


http://niemannross.host.adobe.com/2009csbuDeveloperSummit/

As space is limited to 12 attendees per course, you should make sure you enroll early. There's also a 10% price reduction for people who enroll in both courses before April, 15.

Cheers,


Kris

Thursday, October 16, 2008

New Cookbook Blog

I've started a new blog which will be mainly like a 'cookbook' for Adobe Creative Suite scripters - how-to articles that show how to write much more powerful scripts.

http://www.rorohiko.com/wordpress

The first entry in the blog is up - it's about how you can 'attach' little icons or labels to the four sides of page items as a means of feedback to the user of your scripts. The sample script adds a little floating label with the word count to each text frame - so the user can see the amount of words in each text frame in the blink of an eye.

Check it out!

Cheers,

Kris