Saturday, October 18, 2008

 

CS4 Change

Now that CS4 is with us, it's time to start a list of changes (as opposed to out-and-out new features) that might affect existing scripts. The first one I ran into affected my WrapNudger product.

The TextWrapPreferences property previously known as textWrapType is now called textWrapMode. This change affected the function I use in WrapNudger to determine whether or not the selection has a text wrap. Here's the revised version of the function which shows how I made the script work with either CS3 (InDesign version 5) or CS4 (InDesign version 6):

function checkSelection() {
  
if (app.documents.length == 0 ||
        app.selection.length == 0) {
    alert(
"Please select an object with a text wrap."); return false;
  }
  
if (Number(app.version.split(".")[0]) > 5) {
    
if (app.selection.length == 1 &&
          (!(app.selection[0].hasOwnProperty(
"textWrapPreferences")) ||
              app.selection[0].textWrapPreferences.textWrapMode ==
TextWrapModes.none)) {      
      alert(
"Selected item must have a text wrap."); return false;
    }
  }
else {
    
if (app.selection.length == 1 &&
          (!(app.selection[0].hasOwnProperty(
"textWrapPreferences")) ||
              app.selection[0].textWrapPreferences.textWrapType ==
TextWrapTypes.none)) {      
      alert(
"Selected item must have a text wrap."); return false;
    }
  }
  
return true;
}

Wednesday, October 15, 2008

 

Story to Layer

I was working on a job this morning (actually in CS4, which is released for download today) when I realized that the text frame I was looking at was on the wrong layer. Indeed, all the frames in my story, except for the first few, were on the wrong layer. This must have been the result of my leaving that wrong layer active when I saved the template. I've now fixed the main script that created this situation, but to solve the issue in the document on which I was working, I wrote this quick script:

//DESCRIPTION: Move all text frames of story to same layer
/*
This script assumes that the first frame of the story is on the right layer
*/

(function() {
  
if (app.documents.length > 0 &&
        app.selection.length > 0) {
    
var aSel = app.selection[0];
    
if (!aSel.hasOwnProperty("parentStory")) {
      alert(
"Selection has no parent story"); return;
    }
    
var aStory = aSel.parentStory;
    
var aLayer = aStory.textContainers[0].itemLayer;
    
for (var j = aStory.textContainers.length - 1; j > 0; j--) {
      aStory.textContainers[j].itemLayer = aLayer;
    }
  }
}())

The script is contained within an anonymous function which starts out by checking that there is a selection. If there is, it confirms that the selection has a parentStory property, and if so, it moves all the text frames that constitute that story (textContainers in CS3 and CS4) on to the same layer as the first frame of the story.

Sunday, July 20, 2008

 

Orthogonal Graphic Lines

I was called upon the other day to write a script that required that the selection be an orthogonal graphic line. That is, one that was either perfectly horizontal or perfectly vertical.

A graphic line is not necessarily straight. It appears to be any object that has exactly two points, no matter how it was drawn. For example, anything drawn with the Line Tool is a graphic line; any two-point path drawn with the Pen Tool is a graphic line; any ploygon or rectangle that has been reduced to two points by deleting points from its original shape is a graphic line.

This means that finding an "orthogonal" line is not as simple a looking at the coordinates of a graphic line and checking to see if the two x-values or two y-values are the same as each other.

Here's a framework:

//DESCRIPTION: Framework for operating on selected orthogonal lines
(function() {
  if (app.documents.length > 0) {
    for (var j = app.documents[0].selection.length - 1; j >= 0; j--) {
      if (app.documents[0].selection[j] instanceof GraphicLine &&
        isOrthogonal(app.documents[0].selection[j])) {
        processOrthogLine(app.documents[0].selection[j]);  
      }
    }
  }

  function processOrthogLine(theLine) {
    var delta = 0.00001;
    var vert = Math.abs(theLine.paths[0].entirePath[0][1] - theLine.paths[0].entirePath[1][1]) > delta
    alert(
"The selected line is " + (vert ? "vertical." : "horizontal."));
  }

  function isOrthogonal(gl) {
    // By definition, a graphicLine has just one path with two pathPoints
    var myPath = gl.paths[0];
    
var entirePath = myPath.entirePath;
    
if (entirePath[0].length > 2 || entirePath[1].length > 2) {
      // one or other of the path points has control handles
      // thus, the line is not straight
      return false;
    }
    
var delta = 0.00001;
    
if (Math.abs(entirePath[0][0] - entirePath[1][0]) < delta ||
      Math.abs(entirePath[0][1] - entirePath[1][1]) < delta) {
        
return true;
    }
    
return false;
  }
}())


It is possible that lines drawn with carefully positioned control handles could result in a straight line, but we make the assumption that the lines we're looking for have no control handles.

Saturday, July 19, 2008

 

Align Left Edge

Over at InDesignSecrets.com, the topic arose about how to get rid of the space that sometimes appears at the left of the first capital letter of a paragraph. This is much more noticeable in some fonts than others. See: Removing Space Along the Left Edge of Text. The discussion gave me an idea for a script that would do the job in CS3 for paragraphs that don't have a drop-cap (for those that do, the Align Left Edge option is available in the Drop-cap dialog). That led to my posting this (slightly edited for posting here):

Here’s a proof of concept script deeply annotated. It assumes that the insertion point is in some paragraph and that temporarily making the first character into a drop cap won’t push the paragraph overset.

First, we need references to the paragraph and its first character:

    var myPara = app.selection[0].paragraphs[0];
    var myChar = myPara.characters[0];

Now we need to know the width of the character. I wrote a simple function which I called getWidth() to do this that returns the distance between the horizontalOffsets on left and right of the character.

    var firstCharWidth = getWidth(myChar);

But, as we know, the Align Left Edge feature of InDesign only works if you have a drop cap and that changes the size of the character to an unknown point size. But still, we can use relative values, so the next two lines make the first character into a drop cap:

    myPara.dropCapCharacters = 1;
    myPara.dropCapLines = 2;

And this cockamamie next line switches on Align Left Edge — the dropcapDetail property not only uses different capitalization from the other dropCap related properties, it works like a two-bit bitmap. The right-most bit controls Align Left Edge, the left-most controls Scale for Descenders. So, it has four values:

0 means both off
1 means ALE on, SfD off
2 means
SfD on, ALE off
3 means both on

    myPara.dropcapDetail = 1;


Now we get the character width again, this time we’re getting the size of the dropcap at its unknown point size.

    var largeCharWidth = getWidth(myChar);

And, having align left edge switched on, we record where the first insertion point is:

    var largeAdjLeft = myChar.horizontalOffset;

Now we switch it off and get the revised position:

    myPara.dropcapDetail = 0;
    var largeBaseLeft = myChar.horizontalOffset;

This allows us to express the width of the left side bearing as a fraction of the width of the character (remember, way back at the start we got the original width):

    var leftSideBearing = (largeBaseLeft - largeAdjLeft)/largeCharWidth;

So we can work out how much we need to shift the original character left by multiplying this fraction by the original widith:

    var neededShift = leftSideBearing * firstCharWidth;

We have all the information we need. So, let’s switch off the drop cap:

    
myPara.dropCapLines = 1;

We’re going to add a hair space to the text to give us something to kern back over. But we have to bear in mind that kerning is a relative value. The absolute amount of a kern varies according to the size of the type. What we know is that 1000 kerning units is a distance of 1 em which, for 12 point type, is a value of 12 points. So, we need to normalize the kerning amount to what it would be if the type were at 12 points. And then add the width of a 12-point hair space (24 to the em):

    
var normalizedKernAmount = (neededShift * 12/myChar.pointSize) + .5;

Now we insert the hair space:

    
myPara.insertionPoints[0].contents = SpecialCharacters.hairSpace;

And, finally, we set the kerning value of the insertion point between the hair space and the first character, multiplying by -1000/12 to (a) make it a negative kern and (b) to convert the value in points to a fraction of 1000:

    
myPara.insertionPoints[1].kerningValue = normalizedKernAmount*-1000/12;

There you go: Align Left Edge for the first line of any paragraph.




 

Delete "Empty" Pages

I often get into the state where a run of pages is dedicated to a particular story and because of editing the last pages of the story are now effectively empty; that is, the text frames on those pages are empty. This script seeks those pages and deletes them. So, the script must start with a framework that requires the selection to have a parentStory property:

//DESCRIPTION: Delete "empty" pages at end of selected story

(function() {

 if (app.documents.length > 0 &&
  app.selection.length == 1 &&
   app.selection[0].hasOwnProperty(
"parentStory")) {
  deleteEmptyPages(app.selection[0].parentStory);
 }
 
function deleteEmptyPages(story){
 }
}())

This is the first time I've used an anonymous function on this blog. I learned about them only recently (thanks Harbs and Kris). They help protect the global name space. In particular, you can have one script in this structure call another that uses the same variable or function names and there's no confusion because they're inside the anonymous function. Notice that the whole thing is inside parentheses. The pair of parentheses right at the end are the call to the function. Looks odd, but it works a charm.

So, now all we need do is write our function. This is a CS3 only script so we must use the textContainers property to get at the frames that constitute the story. Here's the function:

 
function deleteEmptyPages(story){
  var myFrames = story.textContainers;
  
for (var j = myFrames.length - 1; j >= 1; j--) {
   
if (myFrames[j].insertionPoints.length == 0 &&
    myFrames[j].parent
instanceof Page) {
    myFrames[j].parent.remove();
   }
  }
 }
Notice that the loop goes down to a value of 1. By doing this, it assumes that you don't want to completely delete the story even if it is empty. Notice also that it checks that the parent is indeed a page before deleting it. We don't want to delete a page where the frame, albeit empty, has been grouped with something else.

Friday, January 11, 2008

 

Speeding up a Script

Most of the time, I pay scant attention to how long a script takes to run because most of them are so quick it hardly matters, but every now and then a simple script comes along that takes serious time to run. I just hit one as I sit here working on a catalog. I'm getting near the end of the job, making corrections and changes (mainly changes).

I realized that I've done so much dickering around to get things to fit on the page in earlier passes that it's time to reset the space before/after of every paragraph. Well, that's easy:
//DESCRIPTION: Restore vertical paragraph style spacing

if (app.documents.length > 0 &&
app.selection.length == 1 &&
app.selection[0].hasOwnProperty("baseline")) {
restoreParagraphSpacing(app.selection[0]);
}

function restoreParagraphSpacing(myText) {
var myParas = myText.paragraphs;
for (var j = 0; myParas.length > j; j++) {
myParas[j].spaceBefore = myParas[j].appliedParagraphStyle.spaceBefore;
myParas[j].spaceAfter = myParas[j].appliedParagraphStyle.spaceAfter;
}
}
If I were just running this on the odd page here and there, it wouldn't take too long, but document has 43 pages and the story has 782 paragraphs, so this loop interacts with InDesign 1564 times.

Clearly, we could cut that in half by changing the loop to this:
  for (var j = 0; myParas.length > j; j++) {
myParas[j].properties = {
spaceBefore:myParas[j].appliedParagraphStyle.spaceBefore,
spaceAfter:myParas[j].appliedParagraphStyle.spaceAfter
}
}
But this still requires 782 interactions with InDesign. And all of them happen even if the script does nothing because I'm running it for a second time. There's one more thing worth trying:
//DESCRIPTION: Restore vertical paragraph style spacing

if (app.documents.length > 0 &&
app.selection.length == 1 &&
app.selection[0].hasOwnProperty("baseline")) {
restoreParagraphSpacing(app.selection[0]);
}

function restoreParagraphSpacing(myText) {
var myParas = myText.paragraphs;
var mySBs = myParas.everyItem().spaceBefore;
var mySAs = myParas.everyItem().spaceAfter;
for (var j = 0; myParas.length > j; j++) {
if (myParas[j].spaceBefore != mySBs[j] || myParas[j].spaceAfter != mySAs[j]) {
myParas[j].properties = {
spaceBefore:myParas[j].appliedParagraphStyle.spaceBefore,
spaceAfter:myParas[j].appliedParagraphStyle.spaceAfter
}
}
}
}
The script still does a lot of interacting, but most of it is reading. It only writes a value into the document if there is a change needed, so right now the script runs very quickly because there are no longer any changes to be made.

Ah well, back to work!

Saturday, January 05, 2008

 

Instructive Confusion

The other day, I went so far as to report a bug to Adobe only to discover that I was missing the obvious. It wasn't a bug I was experiencing, but it surely would make a nice feature request. I was working with a book and I had all the documents open. But one of the documents was of primary interest to me at that point in time, and I had two windows open into it, arranged to be side-by-side on my screen.

When I double-clicked that document in the book window, I expected both windows to come to the front. But only one did. And, as luck would have it, it was the one on the left that I had temporarily forgotten about. I was expecting the window on the right to come to the front. When it didn't after repeated tries, I fired off a bug report to Adobe, only to feel rather silly a few minutes later when I realized that the window at left was indeed a window of the document I was trying to wake up. So, I modified my report and made it a feature request instead.

Anyway, it being Saturday evening and I'm sitting here amusing myself messing around with scripts while listening to Mahler's Ninth Symphony, it occurred to me that I could write a script to bring all the windows of the active document to the front. I decided that I wanted them to retain whatever stacking order they already had, so I thought that all that meant was I should cycle through all the active document's windows and bring them to the front starting with the back one.

It came as quite a blow when this script didn't work:
//DESCRIPTION: Bring all Windows of active document to the front

if (app.windows.length > 0) {
bringToFront(app.windows[0].parent);
}

function bringToFront(aDoc) {
var myWindows = aDoc.windows;
for (var j = myWindows.length - 1; j >= 0; j--) {
app.activeWindow = myWindows[j];
}
} // end bringToFront
In as much as it did bring all the active document's windows to the front, you could say it did work, but it reversed the order of the windows. That which was at the back was now at the front (I only had two windows open; with more the result would have been more muddled).

Why is this? Because myWindows is pointing at the collection of windows owned by the active document, so each time I changed the order of the windows, the order of their references in in myWindows also changed. I've fallen into this trap so many times with objects of various sorts, you'd think I'd see it coming. The solution is to deploy getElements() to create an array that captures a snapshot of the order of the windows before we perform the loop, like this:
//DESCRIPTION: Bring all Windows of active document to the front

if (app.windows.length > 0) {
bringToFront(app.windows[0].parent);
}

function bringToFront(aDoc) {
var myWindows = aDoc.windows.everyItem().getElements();
for (var j = myWindows.length - 1; j >= 0; j--) {
app.activeWindow = myWindows[j];
}
} // end bringToFront
Now, the contents of myWindows is not affected by the changes made in the loop and so the windows end up in the order I wanted them, at the front of any other windows that might be open into other documents.

Note: app.windows[0].parent is equivalent to app.documents[0], but this is a script about windows, so I used the window-based construction.

Sunday, September 09, 2007

 

Placing Snippets Inline/Anchored

I've found InDesign's snippets feature to be a constant source of frustration because you simply can't place them as inline or anchored objects. So much of my work involves inline/anchored objects that this makes snippets just about useless. But the alternative of using library items isn't that great either. Library panels take up space on screen and they clutter up the the Open Recent menu. Also, in the past, they've been a source of frustration when crashes occur and they have to be recovered. Untitled assets seem to pop into existence in a recovered library, or worse yet, a previously named asset suddenly loses its name.

Snippets do not take up space on screen, are impervious to crash/recovery issues, and they do not appear on the Open Recent menu. So, how to work around the inability to place them inline? Whats more, how to do it without using the clipboard which might or might not have important information on it at any point in time.

I came up with the idea of using a temporary library. Even on a G4, creating a library, putting something into it, pulling it back out again and deleting the library is pretty swift, while on a MacIntel it positively flies. So, I wrote this function to do the work:
function placeSnipInline( mySnipFile, text ) {
var myDoc = app.documents.add(false);
app.scriptPreferences.userInteractionLevel = UserInteractionLevels.neverInteract;
myDoc.pages[0].place(mySnipFile);
app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
myLib = app.libraries.add(File("~/Desktop/templib.indl"));
myLib.store(myDoc.pageItems[0]);
myDoc.close(SaveOptions.no);
myLib.assets[0].placeAsset(text);
myLib.close();
File("~/Desktop/templib.indl").remove();
}
One odd thing here might leap out at you. Why did I not take advantage of the returned object from the place() call to get a reference to the placed object rather than rely on myDoc.pageItems[0] in the store call? The reason is that I have just this morning discovered that if you place a snippet that consists of just a text frame, what's returned by place() is the story, not the text frame.

To turn the above function into a complete script I added this front-end:
//DESCRIPTION: Place Snippet Inline/Anchored

if (app.documents.length > 0 &&
app.selection.length > 0 &amp;&
app.selection[0].hasOwnProperty("baseline")) {
placeSnippet(app.selection[0]);
} else {
alert("There must be a text selection to run this script");
}

function placeSnippet(sel) {
if (File.fs == "Windows") {
var Filter = "Snippet files: *.inds";
} else {
var xmlFilter = function(file) {
while(file.alias){
file = file.resolve();
if (file == null) return false;
}
if (file instanceof Folder) return true;
return (file.name.slice(file.name.lastIndexOf(".")).toLowerCase() == ".inds");
}
var Filter = xmlFilter
}
var myFile = File.openDialog("Choose a snippet file", Filter);
if (myFile == null) { return }
placeSnipInline(myFile, app.selection[0]);
}

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