Sunday, October 23, 2005

 

Vertical Justification

You'd think this was easy -- just turn it on in text frame options. But I've run into a case where that doesn't work. My client wants two columns in a text frame vertically aligned with each other, but the second column contains an inline graphic which is overlaid by some text. The graphic extends beyond the bottom of the text frame -- well, it does if I switch on vertical justification via text frame options. So that doesn't do the job because what he really wants is for the text in the left column to align with the bottom of the graphic in the right column.

So, we need a script.

The concept of the script would be that the size of the text frame has been manually set. The bottom falls where it falls and the need to is fit the text so that it nestles to that bottom. But which text? If I do this with all the text in each column of the text frame, then it will have precisely the effect that switching on VJ has. So, the user must click in the text column of interest.

So, task number one is to determine which column of text contains the insertion point. Usually, when I write scripts that depend on a text insertion point, I don't really care if the user has made a larger selection -- in that case, I just take the first insertion point of the selection, but in this case, I'm going to be more fussy. The script will insist on a single insertion point and will decline to proceed for any other kind of selection.

So, to get started, let's deal with that and then call a function to tell us which column contains the insertion point (note: the errorExit() function is discussed here):
//DESCRIPTION: Vertically "justify" indicated text column by adding space before.

if ((app.documents.length != 0) && (app.selection.length != 0)) {
 if (app.selection[0].constructor.name != "InsertionPoint") {
  errorExit("Indicate column by clicking an insertion point with the text tool.");
 }
 var myCol = getColNum(app.selection[0]);
} else {
 errorExit();
}
Well, it didn't take long for me to have second thoughts. Just writing this amount of code caused me to take pause and ask: What if the insertion point is in a cell of a table or in text on a path?

Interesting questions! Text on a path is clearly ineligible for this kind of processing so we must trap for that and issue an error message. Text in a cell could be eligible, but perhaps the user clicked in it by accident -- it is after all also in a text column -- the column that contains the table that contains the cell. So, for now, I'm also going to trap that possibility out of consideration (a decision aided by the fact that my immediate need doesn't involve tables).

Now the body of the script looks like this:
if ((app.documents.length != 0) && (app.selection.length != 0)) {
 var mySel = app.selection[0];
 if (mySel.constructor.name != "InsertionPoint") {
  errorExit("Indicate column by clicking an insertion point with the text tool.");
 }
 // What if insertion point is in a cell or text on a path?
 if (mySel.parent.constructor.name == "Cell") {
  errorExit("Script does not operate on text inside tables.");
 }
 if (mySel.parentTextFrames[0].constructor.name == "TextPath") {
  errorExit("Script does not operate on text on a path.");
 }
 var myCol = getColNum(mySel);
} else {
errorExit();
}
You'll see that I quickly tired of typing "app.selection[0]" so I created a variable to hold a reference to the selection. Because the parent of a text object (insertion point is a kind of text object) is either "Cell" or "Story" differentiating text on a path from text in a text frame requires a different technique from determining if the text is in a cell in a table.

Now it's time to take a crack at the getColNum function. Here's an irony: I don't care which column it is. All I really need to do this job is a reference to the text in the column. So, let's change the name of the function and the call to it to:
 var myText = getColTextRef(mySel);
Here's the function:
function getColTextRef(myIP) {
 // returns reference to text of text column that holds insertion point
 var myTF = myIP.parentTextFrames[0];
 var myIndex = myIP.index;
 var colIndexes = myTF.textColumns.everyItem().index;
 for ( j= colIndexes.length - 1; j>= 0; j--) {
  if (myIndex >= colIndexes[j]) {
   return myTF.textColumns[j].texts[0];
  }
 }
 errorExit("Something is ghastly wrong");
}
That final error ought to be impossible. The script will always find the column and exit accordingly. But because it is a conceptually possible logic path, I've put an error message in there to let me know if for some unforesee reason we do get there.

Thinking about errors makes me wonder what will happen if the user tries to run this script on an empty (or nearly empty) text column. The logic of adding space before to all except the first paragraph isn't going to get very far if there's one or fewer paragraphs. We'll need to check for that right now:
 var myText = getColTextRef(mySel);
 if (myText.paragraphs.length > 1) {
  spreadText(myText);
 } else {
  errorExit("Indicated column has fewer than two paragraphs.");
 }
So now we need the spreadText() function. All the while I've been writing about this script, I've been mulling over the best way to tackle the job of spreading vertically. The process has two parts:
  1. Determine how much space needs to be consumed
  2. Apply to each paragraph in the text, except the first, its portion of that space as extra space before
Probably the easiest way to do this is to make use of the vertical justification feature of InDesign, comparing the last baseline before with the last baseline after. If we go this route, we have to make sure that the vertical justification ends up the way it started. Here's how the first part of spreadText() looks:
function spreadText(theText) {
 // Calculate spare space by comparing last baseline with VJ on and off
 // VJ is not a property of text but its container. So:
 var origVJ = theText.parentTextFrames[0].textFramePreferences.verticalJustification;
 theText.parentTextFrames[0].textFramePreferences.verticalJustification = VerticalJustification.topAlign;
 theText.recompose();
 var firstBase = theText.lines[-1].baseline;
 theText.parentTextFrames[0].textFramePreferences.verticalJustification = VerticalJustification.justifyAlign;
 theText.recompose();
 var targetBase = theText.lines[-1].baseline;
 theText.parentTextFrames[0].textFramePreferences.verticalJustification = origVJ;
}
Although this function, so far, does nothing more than collect information, we can use ESTK to test that it works. And indeed a quick test seems to confirm that it does work. I worry that the process of changing the vertical justification to collect information won't always work. For example, what if there is a text wrap interfering with the shape of the text frame? In that case, the attempt to set justifyAlign will probably fail with an error.

That's serious enough to warrant trapping the error because otherwise we could leave the text frame with the wrong vertical alignment. So
 try {
  theText.parentTextFrames[0].textFramePreferences.verticalJustification = VerticalJustification.justifyAlign;
 } catch (e) {
  // restore VJ
  theText.parentTextFrames[0].textFramePreferences.verticalJustification = origVJ;
  errorExit("Frame has text wrap and can't be processed.");
 }
OK, I'm being a wimp and rolling over in the face of text wrap, but dealing with that is a very big deal and I certainly don't have the time right now.

Uh oh! A quick test reveals that attempting to set justifyAlign when it's ineligible doesn't cause a run-time error. The command is just ignored. That sounds like a bug to me. However, it does mean that the script will do nothing in that case. Perhaps it's worth adding a test to make sure that targetBase and firstBase are different from each other (indeed, that the former is larger than the latter) otherwise the user might not notice that nothing happened.

And that's basically it. From here on, everything is simple math and a loop:
 if (firstBase >= targetBase) {
  errorExit("Text Column apparently already spread—perhaps a text wrap is interfering.");
 }
 // Calculate extra space needed per paragraph
 var myExtraSpace = (targetBase - firstBase)/(theText.paragraphs.length - 1);
 for (var j = theText.paragraphs.length - 1; j > 0; j--) {
 theText.paragraphs[j].spaceBefore = theText.paragraphs[j].spaceBefore + myExtraSpace;
 }
Notice that the first paragraph is ignored. That's because space before the first paragraph in a text column is ignored by the composer.

Comments:
Hi Dave,

a few days ago I have made some experience with script to vertcal align text. I have developed a script for "constrictive" vertical justification which add space proportionaly only to paragraphs with space before/after.

Please take a look at http://www.hilfdirselbst.ch/gforum/gforum.cgi?post=191814#191814

Martin
 
Interesting that you should have done that -- I was thinking along the same lines for a more general script.

Sorry it took me so long to find your comment -- I've been up to my eyes for the last week or so.
 
This comment has been removed by a blog administrator.
 
Only from IP?
can i use TextFramePreference.VerticalJustification.JUSTIFY_ALIGN

if my selection is to textframe?

"
myTextFrame.TextFramePreference.VerticalJustification.JUSTIFY_ALIGN;
"
-dosn't work :(
 
BoomKa,

Which language are you using? If JavaScript, then you are confusing the name of the object (TextFramePreference) with the name of the property (textFramePreferences). Use the latter.

Dave
 
Post a Comment

<< Home

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