Tuesday, January 31, 2006

 

Subselecting a quote

My client sent me a set of corrections in this form:

Correct Figure Legend should read “Revised text for figure legend”

Have you ever tried selecting text between quotes? If the text is large enough, it's not that big a deal, but when you want to keep as much information on screen as possible, it is very fiddly. So, I wrote this quick script (would have been quicker had I realized that I needed the parent text flow, not the parent story -- the information was inside a table):
//DESCRIPTION: Subselect to within first pair of quotes

Object.prototype.isPureText = function() {
  switch(this.constructor.name){
    case "InsertionPoint":
    case "Character":
    case "Word":
    case "TextStyleRange":
    case "Line":
    case "Paragraph":
    case "TextColumn":
    case "Text":
      return true;
    default :
      return false;
  }
}

if ((app.documents.length != 0) && (app.selection.length == 1)) {
  var mySel = app.selection[0];
  if (!mySel.isPureText()) { errorExit("Please select some text.") }
  app.findPreferences = null;
  app.changePreferences = null;
  var myStart = mySel.search("^{",false,false)[0].index + 1
  var myEnd = mySel.search("^}",false,false)[0].index - 1
  var myStory = getParentTextFlow(mySel);
  app.select(myStory.characters.itemByRange(myStart,myEnd));
} else {
  errorExit();
}

// +++++++ Functions Start Here +++++++++++++++++++++++

function getParentTextFlow(theTextRef) {
// Returns reference to parent story or text of cell, as appropriate
  if (theTextRef.parent.constructor.name == "Cell") {
    return theTextRef.parent.texts[0];
  } else {
    return theTextRef.parentStory;
  }
}

function errorExit(message) {
  if (arguments.length > 0) {
    if (app.version != 3) { beep() } // CS2 includes beep() function.
    alert(message);
  }
  exit(); // CS exits with a beep; CS2 exits silently.
}
So, all I have to do is select the paragraph (or the cell contents) and the script sub-selects for me.

You'll notice that I've done no error-checking. If the quotes aren't there, the script will give a run-time error. I'll leave that as an exercise for the reader!

Also, of course, it is trivially easy to change this script to sub-select between the first occurrences of any two different characters.

Wednesday, January 18, 2006

 

Toggle Transform Content Preference

It's hard to imagine that it has taken me this long to address this constant source of frustration. In the UI, the only way to find out if this Transform Content preference is on or off is to activate the menu (on either the Control Palette or Transform Palette) and look at it.

You can toggle it with a shortcut, but that won't tell you which state it is in. So:
//DESCRIPTION: Beeps when preference is turned on

if (!app.transformPreferences.transformContent) beep()
app.transformPreferences.transformContent = !app.transformPreferences.transformContent
This script beeps when it turns on the preference but turns it off silently. So all I need to do is attach a shortcut and I won't have to keep visiting that blasted menu.

Sunday, January 15, 2006

 

How many changes were there?

The current document I'm working on has 161 pages. It takes about 15 minutes for my iMac G5 (2 GHz) to export a pdf. After sitting through one such export, I suddenly remembered that I'd not run the script I discussed here: Quick & Dirty Leading Fixer.

Well, that script was so quick and dirty there was no way to tell if it actually did anything -- indeed, from InDesign's point of view it changed every text frame with a paragraph in the style format. So, if I ran that script, I'd have no choice but to export the pdf again.

A new version of the script was called for that would count the changes and tell me how many. So I modified the script to this:
//DESCRIPTION: Format head leading fixer

myDoc = app.activeDocument;
app.findPreferences = null;
app.changePreferences = null;
myFinds = myDoc.search("",false,false,undefined,{appliedParagraphStyle:myDoc.paragraphStyles.item("format")});
myCount = 0;
for (var j=myFinds.length - 1; j >= 0; j--) {
 if (myFinds[j].parentTextFrames[0].textFramePreferences.firstBaselineOffset != FirstBaseline.leadingOffset) {
  myFinds[j].parentTextFrames[0].textFramePreferences.firstBaselineOffset = FirstBaseline.leadingOffset;
  myCount++;
 }
}
alert(String(myCount) + " change" + ((myCount == 1)? "" : "s") + " made.");
Notice that decision in the middle of the alert string. I hate it when alerts say things like "1 changes made." It is that easy to test for the value 1 and suppress the "s" on "changes" so why not?

Oh, and the good news is that this time I didn't have to re-export the PDF!

Saturday, January 14, 2006

 

Zoom in on Object

Having set myself the goal, I just couldn't resist tackling this job tonight (particularly as our local PBS station has pre-empted its British comedies to do fund-raising this evening). And it's quite straightforward.

This is an unusual function in that it doesn't return to the caller. It either throws an error if you try to zoom in on an object that doesn't have a geometric bounds property (this will also happen if a story editor window is at the front) or it exits back to the user on the theory that there's not much point in zooming in on something if you don't stop to let the user see the result.

The script takes advantage of the zoom feature that fits a page into a window. Because we can get the bounds of the page and the zoom percentage of the window, we know what percentage corresponds to the size of a page, so all we have to do is calculate the two ratios of page height to object height and page width to object width and then multiply the page-fitting percentage by the smaller of those two ratios:
function zoomObject(theObj) {
 try {
  var objBounds = theObj.geometricBounds;
 } catch (e) {
  throw "Object doesn't have bounds."
 }
 var ObjHeight = objBounds[2] - objBounds[0];
 var ObjWidth = objBounds[3] - objBounds[1];
 var myWindow = app.activeWindow;
 var pageBounds = myWindow.activePage.bounds;
 var PgeHeight = pageBounds[2] - pageBounds[0];
 var PgeWidth = pageBounds[3] - pageBounds[1];
 var hRatio = PgeHeight/ObjHeight;
 var wRatio = PgeWidth/ObjWidth;
 var zoomRatio = Math.min(hRatio, wRatio);
 app.select(theObj); // to make active the page that holds theObj
 myWindow.zoom(ZoomOptions.fitPage);
 myWindow.zoomPercentage = myWindow.zoomPercentage * zoomRatio;
 exit() // Because there's no point in doing this if you don't exit to let the user see
}
I'm thinking that for text, it ought to be possible to come up with a set of bounds, so perhaps this could be extended to more kinds of object, but for the moment, I shall satisfy myself with this version.

Update: This version doesn't seem to be able to turn to the page of an inline or anchored object.

 

Better View after Find

This morning, I finally bit the bullet and wrote a script to deal with one of InDesign's annoying little habits. After a Find, it will often display the found text in a remote corner of the screen, with most of what you're really interested in (the text around the found item) either off-screen to the left or below the window. So, I borrowed an idea from the selectIt() function I've written about before and came up with what I call: BetterViewAfterFind.jsx:
//DESCRIPTION: A Better View after Find

var myZoom = app.activeWindow.zoomPercentage;
app.activeWindow.zoom(ZoomOptions.showPasteboard);
app.activeWindow.zoomPercentage = myZoom;
There! That's better. The "showPasteboard" command might not be strictly necessary for this situation in that the Find command does make sure that the found item is in the window to start with.

Perhaps I should take this opportunity to rewrite the selectIt() function. You may recall that it looks like this:
function selectIt(theObj) {
 // Selects object, turns to page and zooms in on it
 app.select(theObj,SelectionOptions.replaceWith);
 app.activeWindow.zoom = ZoomOptions.fitPage;
 app.activeWindow.zoomPercentage = 200
}
I've never been particularly happy with that arbitrary 200%. I always reasoned that I could change the number for any particular use, but why not just use whatever zoom value the user has set? The objective is to get the item front and center. Also, if theObj is on the pasteboard, this particular logic will fail because the fitPage command will not bring the object into the window and consequently the zoom to 200% will merely zoom in on the center of the page, not the selected object.

But wait! Let's think this through a tad more. What this function really does is show the user the object. Selecting it is incidental (although selecting it does clarify for the user just what he's being shown). So, I'm going to change the name to showIt() and I'm going to change the functionality, making the argument optional. This way, if there is already a selection, then the function will show that rather than selecting the passed object:
function showIt(theObj) {
 if (arguments.length > 0) {
  // Select object, turn to page and center it in the window
  app.select(theObj);
 }
 // Note: if no object is passed and there is no selection the current page
 // will be centered in the window at whatever zoom is being used
 var myZoom = app.activeWindow.zoomPercentage;
 app.activeWindow.zoom(ZoomOptions.showPasteboard);
 app.activeWindow.zoomPercentage = myZoom;
}
And that means that my BetterViewAfterFind script can be rewritten to use this function:
//DESCRIPTION: A Better View after Find

showIt();
Of course, the real script follows the call with a copy of the function.

I'm thinking that a smarter version of showIt could be written that would provide functionality similar to that of clicking the Go To Link button in the Links palette. I think this could only work for non-text selections, but basically, the script could calculate the zoom value needed to "fill the window" with the selection. But I'll leave that for another day when I'm less busy.

 

Updating Whole Library

I've just worked my way through a 161-page document updating some 20 figures. During the course of doing this, I created a library and popped a copy of each updated figure into the library. However, at some point, I became suspicious that the justification settings for the legend paragraph in each of these figures left something to be desired -- I was getting awful word spacing. Indeed, I needed to change them from the default (on the left) to my preferred settings (on the right) [Sorry about the space that follows -- I don't seem to have control over it.]:






























Default Min. Des. Max
Word Spacing 80% 100% 133%
Letter Spacing 0% 0% 0%
Glyph Scaling 100% 100% 100%



























Preferred Min. Des. Max
80% 100% 125%
-2% 0% 1%
98% 100% 101%



So, I did that. Of course, when I say "preferred" I'm talking about preferred for this particular paragraph style used in the particular way it is being used. We could have a long discussion about this, but it is a side issue as far as the script I now need is concerned.

The issue is to update the library with the latest version of each of these figures without having to step through each of the pages manually looking for them. When I created them, I labeled each of them. The script I used to move them into the library named the library items (assets in a script) for those labels. So, it ought to be as simple as interating through the page items of the document looking for the labeled item and then updating it in the library.

Well, not quite because I have another series of figures that use the same labels! Aargh. Happens, though, that in those other figures, the labeled item is a rectangle (with an image in it) while the figures I actually care about are groups. So, that makes it easy to tell one from another.

Because they're all anchored objects, though, I have to get at them indirectly -- I can't just ask for myDoc.groups. But I can ask for myStory.groups if I can get myStory pointing at the right story -- and that's easiest because it is the longest story in the document.

So, here goes:
//DESCRIPTION: Grab library elements from longest story

Document.prototype.longestStory = function() {
var myStories = this.stories.everyItem().length;
var myLim = myStories.length;
var longStory = 0;
for (var i = 0; myLim > i; i++) {
if (myStories[i] > longStory) {
var myStory = i;
longStory = myStories[i];
}
}
return this.stories[myStory]
}


if (app.libraries.length != 1) errorExit ("Please have exactly one library open for this script");
myDoc = app.activeDocument;
myLib = app.libraries[0];
myStory = myDoc.longestStory();
And we're all set. We have the library, the document, and the story.

But I'm starting to think I need to improve my longestStory() method. The problem is that some of my documents have a prodigious number of stories in them -- this one has 1947 stories -- and so checking the lot takes a fair amount of time. But that's for another day. Right now, I need this script fast and I don't have time for strategic improvements.

So let's gather the information we need:
myAssetNames = myLib.assets.everyItem().name;
myGroupNames = myStory.groups.everyItem().label;
That was easy. So now we need to walk down myAssetNames and find the corresponding group and update the asset. We need to allow for the possibility that the group doesn't exist in this document. The best way to do this is to add another method. This is one I lifted from the Adobe User to User InDesign_Scripting forum, although the original message seems to have disappeared into the ether.

Here's the method:
Array.prototype.indexOf = function(find,offs) {
for( var i = offs == undefined ? 0 : offs; this.length > i; i++ ) {
if( this[i]==find ) {return i}
}
return -1;
}
So, with this available to us, our main loop looks like this:
for (var j = myAssetNames.length - 1; j >= 0; j--) {
var myIndex = myGroupNames.indexOf(myAssetNames[j]);
if (myIndex != -1) {
replaceAsset(myLib, myLib.assets[j], myStory.groups[myIndex]);
}
}
And that's it, except for writing the replaceAsset(theLib, theAsset, theObject) function. Notice that this function could easily be useful in another context. That's one reason for making it a function.
function replaceAsset(theLib, theAsset, theObject) {
var theProps = theAsset.properties
theAsset.remove();
var myNewAsset = theLib.store(theObject);
myNewAsset.properties = theProps;
}
And that should be that! And, indeed it was -- once I'd fixed a couple of minor typos.

Friday, January 13, 2006

 

Working on Selected Group

In the job I'm doing right now, I have a lot of grouped objects. I keep them in a library and plop them on to their appropriate pages as needed. Then, I run a script primarily to get the proper formatting on the group as a whole and its contents.

Here's a simple example:
//DESCRIPTION: Fix up Graphs

myDoc = app.activeDocument;

myGroup = app.selection[0];

processGraph(myGroup);

function processGraph(theGroup) {
  theGroup.applyObjectStyle(myDoc.objectStyles.item("GraphAnchor"));
  var thePIs = theGroup.pageItems;
  for (var j = thePIs.length - 1; j >= 0; j--) {
    var myItem = thePIs[j].getElements()[0];
  if (myItem.constructor.name == "TextFrame") {
      myItem.textFramePreferences.ignoreWrap = true;
      myItem.textFramePreferences.firstBaselineOffset = FirstBaseline.leadingOffset;
      continue;
    }
    if (myItem.graphics.length == 0) {
      myItem.applyObjectStyle(myDoc.objectStyles.item("DataGroupTextFrame"));
    } else {
      myItem.blendMode = BlendMode.multiply;
    }
  }
}
This script falls into the quick & dirty category. I wrote it for a particular kind of group in a particular kind of document containing a specific object style. I don't bother to check that there is a document open nor that there is a selection. I just go ahead and do my thing.

But, after many uses, it occurred to me that more than 90% of the time, I run this script immediately after placing the group inline from the Library. This means that most of the time, I've been switching to the pointer tool and selecting the group before running the script. Finally, this morning, I realized that by adding:
if (myGroup.constructor.name == "InsertionPoint") {
  var myStory = myGroup.parentStory;
  var myGroup = myStory.characters[myGroup.index - 1].groups[0];
}
to the script immediately after setting myGroup to point at the selection, I could avoid the need to switch pointer tools and let the script find the group -- again, I've done this in a quick and dirty fashion, relying on myself to remember the appropriate time to run the script and the state it expects.

You might be wondering why I would have a function to process the contents of the group. Why not just build the group right in the first place. Two reasons:
  1. The groups were prepared by a separate process on a different computer before all the details of how it would be used were fully worked out.
  2. Applying an object style to a group tends to mess up settings inside the group, so they have to be repaired even if they were right in the first place.

Sunday, January 01, 2006

 

LocationOptions Enumeration

Values

  1650812527      LocationOptions.before

  1634104421      LocationOptions.after

  1650945639      LocationOptions.atBeginning

  1701733408      LocationOptions.atEnd

  1433299822      LocationOptions.unknown

Discussion

As is implicit in the names of the enumerated values (the first four, anyway), this value is relative to some object or to the members of a collection of objects. The purpose of the fifth value, the "unknown" location option, is not at all clear. You certainly would never use it when calling a method that takes a location as an argument (e.g., a move method).

There are times when InDesign loses track of the location of various items. For example, in order to speed the progress of scripts, they generally run with window updating and story composing switched off. In these circumstances, I could see a method returning the unknown location value, except that a careful check of the reference guide reveals that not a single method returns this information. Perhaps it's there for future expansion.

Because this enumeration is only ever used as an argument when calling a method, the numerical values above have little more than academic interest. You could use them if you wished, but the result would be a more obscure script -- of course, there are times when obscurity has value, but most of the time you're better off writing "in the clear" by using the named variants.

Usage Examples

Many variants of the move() method have a to argument that takes a member of this enumeration as a parameter. Similarly, some add() methods have an at argument to indicate where a new object should be added, and some duplicate() methods have the to argument.

To move the first paragraph of text frame myTF to the end of story myStory:
  myTF.paragraphs[0].move(LocationOptions.atEnd, myStory);
To move a range of paragraphs in a story to before another paragraph in the same story (as you might do if you received a Word document with sections in the wrong order):
  myStory.paragraphs.itemByRange(ObjStart,i-1).move(LocationOptions.before,myStory.paragraphs[IntroStart]);
To add a new row to the end of table myTable:
  myTable.rows.add(LocationOptions.atEnd,myTable);
To add a new page to a document immediately after page myPage, updating the reference contained in myPage to refer to the newly added page:
  myPage = myDocument.pages.add(LocationOptions.after, myPage);
To duplicate page n of document myFdoc after the last page of document myDoc:
  myFdoc.pages[n].duplicate(LocationOptions.after,myDoc.pages[-1]);
  This is equivalent to:
  myFdoc.pages[n].duplicate(LocationOptions.atEnd,myDoc);
Sometimes, the form you use for an operation like this depends on the way you happen to think about the maneuver.

This page is powered by Blogger. Isn't yours?