Saturday, December 31, 2005


Justifying (that table) at Last!

Finally, we get to write the function to actually do the justification! I started with this framework:
function processFrame(frame, table, start, end) {
    frame: text frame that holds part of the table
    table: table to be processed
    start: number of first row to be considered
    end: number of last row to be considered
And then I gave some thought to whether or not to validate the passed arguments. For example, implicit in these arguments is that the parent text frame of the last row is the same as the first. But if I check that, I didn't need to pass the frame reference in the first place because I would have to calculate it in order to validate it. It was that realization combined with the fact that this is a special purpose function hardly likely to be used again outside of this script that convinced me to add this comment:
// This special purpose function is never likely to be re-used elsewhere so
// it does no validation of the arguments.
So, let's go to it. We need to start out by comparing the bottom bound of the frame with the baseline of the last line of the text frame. And then we need to "distribute that gap" to the rows themselves.

This distribution feature raises a whole bunch of issues, that are worth listing because they demonstrate one of the considerations of writing this kind of script: implicit in any script are a bunch of assumptions that hopefully hold true for the particular purpopse at hand but which might break the script were it applied to a different scenario:Wow! That's a lot of assumptions. And yet the table I'm working on falls into this category (as do the vast majority of the tables I ever build).

OK, so we're cleared for take-off. The basic procedure is bubbling away at the back of my mind. It is largely a question of implementation. A couple of considerations are:
  1. It is safer (although slower) to not assume that all cells in a row have the same bottom-inset value. Indeed, already I'm wondering if I should strike that from my list of restrictions. As long as I process all the cells individually, the distributed increment will affect the largest cell and so will have the correct effect on a row where this rule didn't apply.
  2. It is vital that we allow for the bottom-inset value to be different from one row to another (although inside any frame that happens to be not true for the table I'm working with).
  3. We must verify that after distributing the space we are not victims of a rounding error that pushed the last row to the next frame (or possibly overset) because if that has happened, we need to repair the damage by decreasing that bottom inset a tad.
Finally, I'm ready to start writing the code. Because of the last concern, I'm going to make yet another function to do the actual adjusting of the rows. It will return the parent text frame of the last row to facilitate the verification.

I'm starting my code with what looks like a framework for an infinite loop:
while(true) {
But this script will eventually return to the calling script thereby breaking out of the loop. The loop is only needed if we run into the rounding error problem mentioned above.

Ack! That was a false start, as I discovered when I got to this point:
while (true) {
  var gap = frame.geometricBounds[2] - frame.lines[-1].baseline;
  if (gap == 0) return // unlikely, but let's not sweat blood doing nothing
  var rowInc = gap/(end - start - 1) // increment is gap divided by no. of rows
  if (frame == distrIncs(rowInc, table, start, end))
And then I realized that if this condition evaluates to false, I need to apply different logic to get the row back where it belongs, so the first three statements don't belong in the loop. In fact, I'm wondering just what logic I do need to get it back. A failure here is probably the result of a microscopic rounding error, and I'm not all that good at dealing with microscopic errors. But let's give it a shot. It comes down to: how small an error can I live with in real life? How about a quarter of a point? That'll do. But that means we need to work in points (at least on the vertical dimension):
var userVert =  app.documents[0].viewPreferences.verticalMeasurementUnits;
app.documents[0].viewPreferences.verticalMeasurementUnits = MeasurementUnits.points;

var gap = frame.geometricBounds[2] - frame.lines[-1].baseline;
if (gap == 0) return; // unlikely, but let's not sweat blood doing nothing
var rowInc = gap/(end - start + 1); // increment is gap divided by no. of rows
while (frame != distIncs(rowInc, table, start, end)) {
  rowInc = -0.25/(end-start + 1); // fix for rounding errors
//restore user's vertical measurement preferences
app.documents[0].viewPreferences.verticalMeasurementUnits = userVert
return // when we get here, we've succeeded
So, now all we need is that distIncs() function.

Sometimes, I write scripts like this from the bottom up. Doing the lower level functions first and then using them as building blocks. Had I done so for this script, I wonder if I'd have thought to return the frame of the last row? I doubt it. I'd have probably tried to solve the problem inside the function. But by working top-down, I think we have a more elegant solution.

Again, I started with a framework:
function distIncs(inc, table, start, end) {
    inc: number of points to adjust bottom inset of each indicated cell
    table: table to work on
    start: first row number
    end: last row number
Notice that I'm using the same argument names. Arguments are automatically local to the function they are passed to, so this is quite safe.

My first instinct is to use a double-loop that operates on each cell in each row. It could be that it is quicker to simply process all the cells from the first of row[start] to the last of row[end], but I'm more interested in clarity than speed this time around:
for (var n = start; end >= n; n++) {
  var myCells = table.rows[n].cells;
  for (var m = myCells.length - 1; m >= 0; m--) {
    myCells[m].bottomInset = myCells[m].bottomInset + inc;
There are two drawbacks to working top down: when you finally get to do some testing, you have the whole script written so some obvious errors only surface at this point. For example, in the code I wrote yesterday, I used myRows.count in one place where I should have used myRows.length, and I also tried to get myRows[j].texts[0] when I should have asked for myRows[j].cells[0].texts[0]. I also used J in one loop control statement instead of j. [I have edited the articles to eliminate these problems.] But the second drawback to this approach is that the first test is of the whole thing, so even when we're past the obvious errors, finding the more subtle ones can be time-consuming.

Looking at this first run, the logic about whether or not adding the increment has pushed the last row to the next frame looks flawed. Ah, well that was a dumb error, wasn't it. distIncs() was supposed to return the parent frame of the last row. No wonder the script is getting nowhere!

Well, what do you know? That was the only problem. Adding:
return table.rows[end].cells[0].texts[0].parentTextFrames[0];
to the end of the distIncs() function solved the whole problem. The script has run and done its thing beautifully!

Because I ran the script from within ESTK, I was able to put a breakpoint on the line of the script that deals with the rounding error I was so worried about -- and so I know that that problem never happened because the script never hit the breakpoint.

Comments: Post a Comment

<< Home

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