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:
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:
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:
I'm starting my code with what looks like a framework for an infinite loop:
Ack! That was a false start, as I discovered when I got to this point:
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:
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:
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:
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.
function processFrame(frame, table, start, end) {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:
/*
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
*/
}
// This special purpose function is never likely to be re-used elsewhere soSo, 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.
// it does no validation of the arguments.
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:
- The text frame is assumed to be single-column (if it were multi-column, the script would need to work on each column)
- The text in the table does not align to grid (if it did, the whole construction would be fragile and changing the vertical position of the rows could result in chaos)
- The row heghts are defined using At Least rather than fixed heights
- All the cells in each row have the same bottom inset (that's where we're going to add the space)
- The frames have no stroke or insets
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:
- 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.
- 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).
- 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.
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) {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 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))
}
var userVert = app.documents[0].viewPreferences.verticalMeasurementUnits;So, now all we need is that distIncs() function.
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
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) {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.
/*
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
*/
}
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++) {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.
var myCells = table.rows[n].cells;
for (var m = myCells.length - 1; m >= 0; m--) {
myCells[m].bottomInset = myCells[m].bottomInset + inc;
}
}
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.