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.
Friday, December 30, 2005
Height of a Table (continued)
Some time has passed because I had to make progress on a different part of the project, but now I'm back to this task. So, we have a reference to our table and we know how to get the size of each gap that must be filled. But we still need to work out which rows fall on which pages.
It appears that the way to tackle this is to simple walk through all the rows of the table looking at the parent text frame of each. However, there's an immediate complicaton:
myTF is initialized to be a reference to the first text frame that contains the table and firstRow tells us the number of the first (non-header) row of the text frame we're about to process. So now we're ready to walk through the rows. We need to look for a change in the parent text frame at which point we can process the frame we just left before moving on. But we mustn't forget that the last frame will end without us moving on to the next, so we need an extra test to see if we're dealing with the last row.
For the moment, let's put off the actual processing of each text frame because we'll write a function to do that. Let's just get a reference to the frame that needs processing and hand off to the function enough information that it knows what to do. So, inside our loop, we need:
So, only if there is a frame change or we're reached the last frame will we get past that first test. But now we need a second test because for all but the last frame, the frame we need to process ends at j-1, while the last frame ends at j. So, we test again to see if we're at the last frame, and if so we set a new variable k to the value of j and otherwise to j-1.
Aha, there's a couple of syntax issues here. First, when using the ?: construction to make a test, be sure to end the previous line with a semi-colon. JavaScript is relatively loose about semi-colons at the ends of lines, but this is one case where you must have one. Otherwise, you'll get a confusing error about it expecting a semi-colon and you'll waste time checking that you got the syntax of the ?: line right when the error is actually on the previous line. [Guess how I know that!]
More of an issue is that we really want k to be a local variable. But where to declare it? If you put it inside the ?: you'll get a syntax error. So you need to declare it in advance. Thus, we need another statement which should really go before the loop that simply reads:
Then we call our function (which we still have to write) passing to it the frame to be processed, the table we're working on and the row numbers of the first an last row of the part of the table that is in the frame in question.
After the function has done its thing, we need to initialize our firstRow and variables for the next frame. Notice that we ignore the possibility at this stage that we just processed the last frame. In that case, we're going to hit the loop limit on the next pass and so these values will never actually be used.
And that's it for today. I'll write the function in the morning.
It appears that the way to tackle this is to simple walk through all the rows of the table looking at the parent text frame of each. However, there's an immediate complicaton:
var myRows = myTable.rows;reveals that the header row (in this case there is just one) is row 0. This script can't afford to change the header or footer rows (if there are any) because that will affect every page and potentially could change the contents of one of the later frames (possibly even creating an overflow). The two table properties headerRowCount and footerRowCount will help us here:
app.select(myRows[0].cells[0]);
var myRows = myTable.rowsBy starting at rows[myTable.headerRowCount] we start at the first row after the headers, and by subtracting myTable.footerRowCount from the total number of rows, we avoid operating on the footers.
var myLim = myRows.length - myTable.footerRowCount;
var myTF = myTable.rows[myTable.headerRowCount].cells[0].texts[0].parentTextFrames[0];
var firstRow = myTable.headerRowCount;
for (var j = myTable.headerRowCount; myLim > j; j++) {
// Walk through the rows
}
myTF is initialized to be a reference to the first text frame that contains the table and firstRow tells us the number of the first (non-header) row of the text frame we're about to process. So now we're ready to walk through the rows. We need to look for a change in the parent text frame at which point we can process the frame we just left before moving on. But we mustn't forget that the last frame will end without us moving on to the next, so we need an extra test to see if we're dealing with the last row.
For the moment, let's put off the actual processing of each text frame because we'll write a function to do that. Let's just get a reference to the frame that needs processing and hand off to the function enough information that it knows what to do. So, inside our loop, we need:
myCurTF = myRows[j].cells[o].texts[0].parentTextFrames[0];The first thing we do is check to see if we've reached either the start of a new frame or the end of the last frame. Hey! Wait a minute. How can you use an AND construction (&&) to test for an OR condition? Well, even as I wrote that sentence I found myself wondering the same thing. What converts an AND test to an OR is the "NOT" contained in the second test. That condition is false once only, when we get to the end of the last frame. Every other time it is true, and so, the first text (are the frames the same) totally dominates this test for all but the very last row.
if ((myCurTF == myTF) && (j != myLim - 1)) continue;
(j == myLim - 1) ? k = j : k = j-1;
processFrame(myTF, myTable, firstRow, k);
firstRow = j;
myTF = myCurTF;
So, only if there is a frame change or we're reached the last frame will we get past that first test. But now we need a second test because for all but the last frame, the frame we need to process ends at j-1, while the last frame ends at j. So, we test again to see if we're at the last frame, and if so we set a new variable k to the value of j and otherwise to j-1.
Aha, there's a couple of syntax issues here. First, when using the ?: construction to make a test, be sure to end the previous line with a semi-colon. JavaScript is relatively loose about semi-colons at the ends of lines, but this is one case where you must have one. Otherwise, you'll get a confusing error about it expecting a semi-colon and you'll waste time checking that you got the syntax of the ?: line right when the error is actually on the previous line. [Guess how I know that!]
More of an issue is that we really want k to be a local variable. But where to declare it? If you put it inside the ?: you'll get a syntax error. So you need to declare it in advance. Thus, we need another statement which should really go before the loop that simply reads:
var k;In all my examples up until now, I've declared variables to be local as I created them, but you can if you wish declare them in advance without assigning them a value.
Then we call our function (which we still have to write) passing to it the frame to be processed, the table we're working on and the row numbers of the first an last row of the part of the table that is in the frame in question.
After the function has done its thing, we need to initialize our firstRow and
And that's it for today. I'll write the function in the morning.
Height of a Table (continued)
One of the golden rules of scripting is: if you think of an ugly solution to something and you are repelled by just how ugly it is, there probably is an easier solution just waiting to be found. That certainly applies here.
So, now I need to work out which is the last row on each page. That should be easy enough.
First, we need to know which table to work on. Let's require the user to have an insertion point in the table of interest:
1. To get the parent story of the table you need:
2. To find a table in a story, you use the storyOffset property of the table, but this seems to miss by one. My story is only one character long (even though it also has 10 lines) and yet the storyOffset return is 1.
3. Obviously, in this case, to get all the text frames containing the table, I could just get all the text frames of the story because that's all there are, there ain't no more, but of a mind to experiment. And so I tried:
4. So, I tried selecting the character to see what would happen:
5. So I went for:
So, it looks as though the only way to get a list of the text frames is to walk through the table getting the parent text frame of each row and adding any not already on the list to the list. Or, I can use a priori knowledge about the particular table I'm working with and just run with the frames of the story.
Happens, that I'm also going to need to know which is the last frame of each page, so I'm going to have to walk through anyway, so perhaps I'll do that after all.
More to come ...
myStory = app.selection[0].parentStory;Gives me the base of each table section in the ten frames of this particular story (which consists entirely of the table).
myTFs = myStory.textFrames;
myBs = myTFs.everyItem().lines[-1].baseline;
So, now I need to work out which is the last row on each page. That should be easy enough.
First, we need to know which table to work on. Let's require the user to have an insertion point in the table of interest:
var ErrMsg = "Please select a table.";Next, we need to gather some data: how many frames are part of the table? That raises the question does a table have a parentTextFrames property -- answer: No! What about the "character" holding the table? Let's experiment:
if (app.selection.length == 0) { errorExit(ErrMsg) }
var myTable = app.selection[0];
while (myTable.constructor.name != "Table") {
myTable = myTable.parent;
if (myTable.constructor.name == "Application") { errorExit(ErrMsg) }
}
1. To get the parent story of the table you need:
myTable.parent.parentStorybecause the parent is the first text frame (note that if the table is nested inside another, the parent is a cell of that other table, but I'm not going to worry about that right now).
2. To find a table in a story, you use the storyOffset property of the table, but this seems to miss by one. My story is only one character long (even though it also has 10 lines) and yet the storyOffset return is 1.
3. Obviously, in this case, to get all the text frames containing the table, I could just get all the text frames of the story because that's all there are, there ain't no more, but of a mind to experiment. And so I tried:
myChar = myTable.parent.parentStory.characters[myTable.storyOffset - 1];but all I got was the one text frame.
myTFs = myChar.parentTextFrames;
4. So, I tried selecting the character to see what would happen:
app.select(myChar);and again, all I got was the one text frame. But then I noticed something interesting. Only the first part of the table was selected, so indeed a singular text frame was all I'd expect.
myTFs = app.selection[0].parentTextFrames;
5. So I went for:
app.select(myChar.parentStory.texts[0]);and even though this did select the whole length of the table, I still only got the first text frame returned (actually, that's an assumption; let's just say I only got one text frame).
myTFs = app.selection[0].parentTextFrames;
So, it looks as though the only way to get a list of the text frames is to walk through the table getting the parent text frame of each row and adding any not already on the list to the list. Or, I can use a priori knowledge about the particular table I'm working with and just run with the frames of the story.
Happens, that I'm also going to need to know which is the last frame of each page, so I'm going to have to walk through anyway, so perhaps I'll do that after all.
More to come ...
Height of a Table (continued)
It is not clear to me how to measure in a script the height of the gap at the bottom of a page (which really means bottom of a frame) when a table is split from one frame to the next. I can't think of a direct way of measuring the vertical coordinate of the bottom of the bottom row. If anyone knows of a way, I'd be interested to hear about it.
In the mean time, this trick comes to mind:
In fact, that's so ugly, I'm going to research this some more before continuing.
In the mean time, this trick comes to mind:
- The gap must be less than the height of the first row on the next page.
- The parent text frame of text in a cell is available information.
- Thus, I can adjust the height of bottom row on one page until it flips to the next and use that information to work out how much space I need to distribute.
In fact, that's so ugly, I'm going to research this some more before continuing.
Height of a Table
I have this ten-page table that looks a bit of a mess because each page is a different height. The question is: how to automatically adjust the table so each page (except perhaps the last) is the same height?
Were I doing this manually, I'd estimate how much space I had to fill at the bottom of each page and distribute that amount into the bottom inset of each row on the page. So how do I do this in a script? The first thing I checked was that the height return for myTable.height is indeed the complete height of the table:
In passing, I notice that this is a function I lifted from an old script. I rarely, these days, take advantage of the JavaScript facility whereby an if statement when true will execute the next statement only without the need to use brackets. While it saves a bit of typing, it creates an unbalanced look that can deceive me if I come back and try to edit the script.
More to come ...
Were I doing this manually, I'd estimate how much space I had to fill at the bottom of each page and distribute that amount into the bottom inset of each row on the page. So how do I do this in a script? The first thing I checked was that the height return for myTable.height is indeed the complete height of the table:
myTable = app.selection[0].parent.parent;And that returns 558.7. Hmm. I should add the measurement units into my alert:
alert(myTable.height.toPrecision(4));
//DESCRIPTION: Height of TableAnd that returned 558.7 picas.
// Assumes insertion point in text in a cell
myTable = app.selection[0].parent.parent;
alert(myTable.height.toPrecision(4) + " " + decypherUnits(app.activeDocument.viewPreferences.verticalMeasurementUnits));
function decypherUnits (theNum) {
if (theNum<257) {
// Custom settings are in points.
return "pts";
}
// Decyphers for print purposes, so inches and inches decimal are both returned as 'ins'
theNums = [2054187363,2054188905,2053729891,2053729892,2053991795,2053336435,2053335395]
theMeanings = ["picas","pts","ins","ins","mms","cms","ciceros"]
for (var i = 0; theNums.length > i; i++) {
if (theNum == theNums[i])
return theMeanings[i];
}
return theNum
}
In passing, I notice that this is a function I lifted from an old script. I rarely, these days, take advantage of the JavaScript facility whereby an if statement when true will execute the next statement only without the need to use brackets. While it saves a bit of typing, it creates an unbalanced look that can deceive me if I come back and try to edit the script.
More to come ...
Thursday, December 29, 2005
Quick & Dirty Paragraph Shuffler
My client had me insert new paragraphs into a long structured document. Due to a miscommunication, I initially inserted the paragraphs in the wrong place on each page. It transpired that the paragraphs (in the paragraph style "Workshop") needed to be immediately before the corresponding "Description" paragraph rather than after the "Isotype" paragraph where I'd put it. Here's how I solved the problem:
Document.prototype.longestStory = function() {I put in the test just in case I had misspelled the name of the style. I didn't want all those paragraphs moved to the start of the story.
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]
}
myDoc = app.activeDocument;
myStory = myDoc.longestStory();
myStyles = myStory.paragraphs.everyItem().appliedParagraphStyle;
n = 0; // Indicates most recent index of a Description paragraph
for (var j = myStyles.length - 1; j >= 0; j--) {
theName = myStyles[j].name;
if (theName == "Description") {
n = j;
continue
}
if (theName == "Workshop") {
if (n == 0 ) {
alert ("Oops");
exit();
}
myStory.paragraphs[j].move(LocationOptions.before, myStory.paragraphs[n]);
}
}
Tuesday, December 20, 2005
Q&D Leading Fixer
I was about to deliver a proof PDF to my client when I noticed that one of the heads in one of the frames (on page 531) was not aligned correctly. A quick look confirmed that the leading of the text frame had switched from Leading to Ascent. Further examination of the document revealed other instances of this problem.
A quick & dirty script to the rescue:
A quick & dirty script to the rescue:
//DESCRIPTION: Format head leading fixerAn now all is well. Time to export another proof PDF.
myDoc = app.activeDocument;
app.findPreferences = null;
app.changePreferences = null;
myFinds = myDoc.search("",false,false,undefined,{appliedParagraphStyle:myDoc.paragraphStyles.item("format")});
for (var j=myFinds.length - 1; j >= 0; j--) {
myFinds[j].parentTextFrames[0].textFramePreferences.firstBaselineOffset = FirstBaseline.leadingOffset;
}
Sunday, December 18, 2005
Really Commenting Code
Most times, when I'm writing scripts, I tend to pay little more than lip-service to commenting the code, but I'm slowly learning to regret that. Each time I have to explore a function I create more than a few days ago, I end up investing an amount of time simply trying to fathom exactly what it does. For trivial functions, this might be OK, but for those that do more, this is proving to not be OK.
So, this morning, I decided to write a function and comment it "properly". Here's the result:
So, the calling script looks like this:
An alternative approach that I rejected for now would replace those "throw" statements with direct calls to errorExit(). I chose not to do this because one day I might write a script that makes use of those thrown errors to take some action other than just reporting the problem and quitting.
I'm not sure there are any rights or wrongs here so much as philosophical decisions. If I were writing solely for myself, I'd just let the run-time errors happen and not blink, but this particular script I'm working on has potential value for a larger community, so I'm leaving things the way they are even though the main script gets a bit more cluttered.
So, this morning, I decided to write a function and comment it "properly". Here's the result:
I think this makes the function a whole lot easier to understand, whether I'm coming back to it to use it as-is or if I'm trying to revise it for some extra functionality. For example, I wrote this to help me with a larger script where I'm trying to create a preferences file in the same folder as the running script. So, the initial purpose is to create a filepath that derives from the script's filepath.
function makeFileName(Orig, New, Append) {
/*
Returns derived filename using:
Orig
FileRef or String.
If FileRef, full name of file used as basis;
If string, can be path or just name; whichever, that's what's returned
New
String
New name for file, including extension
Append (Optional; default: true)
Boolean
true: append to existing name
false: replace existing name
*/
if (arguments.length < 2) {
throw "First two arguments are required."
}
if (Orig.constructor.name != "String") {
// Should be file or folder; get path
try {
Orig = Orig.path;
Orig.length;
} catch (e) {
throw "First argument must be a string or file/folder reference."
}
}
if (New.constructor.name != "String") {
throw "Second argument must be a string."
}
if (arguments.length == 2) {
Append = true;
}
// Preliminaries complete; arguments are good.
// Get platform independent version of Orig string
var myString = File.decode(Orig);
// If last part of name includes period, strip from last period on
var lastDot = myString.lastIndexOf(".");
var lastSlash = myString.lastIndexOf("/");
if (lastDot > lastSlash) {
myString = myString.slice(0, lastDot);
}
// If we're not appending, get rid of last part of name
if (!Append) {
if (lastSlash == -1) {
myString = ""; // there only is a last part
} else {
myString = myString.slice(0, lastSlash);
}
}
// Append New, encode and return
return File.encode(myString + New);
}
So, the calling script looks like this:
var myPath = getScriptPath();where getScriptPath() is a function that does this:
var PrefsFileName = makeFileName (myPath, "prefs.txt", true);
function getScriptPath() {I'm a little torn about what to do about those errors that makeFileName() throws. If I leave the call as it is above, I'll just get a run-time error should an error be detected by the script, which would be all right if I were writing this script for myself alone (and who knows, perhaps I am). This is a better version:
// This function returns the path to the active script, even when running ESTK
try {
return app.activeScript;
} catch(e) {
return e.fileName;
}
}
var myPath = getScriptPath();but that try construction clutters up the script making it hard to follow.
try {
var PrefsFileName = makeFileName (myPath, "prefs.txt", true);
} catch (e) {
errorExit("makeFileName reports: " + e)
}
An alternative approach that I rejected for now would replace those "throw" statements with direct calls to errorExit(). I chose not to do this because one day I might write a script that makes use of those thrown errors to take some action other than just reporting the problem and quitting.
I'm not sure there are any rights or wrongs here so much as philosophical decisions. If I were writing solely for myself, I'd just let the run-time errors happen and not blink, but this particular script I'm working on has potential value for a larger community, so I'm leaving things the way they are even though the main script gets a bit more cluttered.
Saturday, December 17, 2005
Breaking a Text Reference
When I wrote this code, it crossed my mind that it might not work. It didn't take long to discover that it doesn't.
It's possible that this is breaking only because the paragraph I happen to be working with is the last in its story and so by deleting the character I'm causing the text reference to point beyond the end of the story. If that's the cause, then fixing this is easy: put the new one in and then take the old one out. Let me try that ...
Well what do you know! How's this for real-time blogging. That was the problem and this reorganization fixes it:
function processFoundClone(theText, theAsset) {The problem is that if the first character contains (consists of?) an anchored clone already, I want to replace it with the new one from the library (that's what the parameter theAsset is all about). But, if I delete it in the way shown here, the reference to the text breaks and so I'm unable to select that insertion point.
// [snip]
// Check to see if asset already there, if so replace it, if not add it
if (theText.characters[0].groups.length > 0) {
theText.characters[0].remove();
}
app.select(theText.insertionPoints[0]);
theAsset.placeAsset(app.activeDocument);
}
It's possible that this is breaking only because the paragraph I happen to be working with is the last in its story and so by deleting the character I'm causing the text reference to point beyond the end of the story. If that's the cause, then fixing this is easy: put the new one in and then take the old one out. Let me try that ...
Well what do you know! How's this for real-time blogging. That was the problem and this reorganization fixes it:
function processFoundClone(theText, theAsset) {Notice that because I added a character at the beginning of theText, I now have to check to see if the second character contains a group, and if so delete that. Because the story was first made longer, the reference to theText is still valid (albeit it doesn't point at the exact same set of characters) whereas removing the character first invalidated it.
// [snip]
// Add new asset
app.select(theText.insertionPoints[0]);
theAsset.placeAsset(app.activeDocument);
// Check to see if there was one already there; if so, delete it
if (theText.characters[1].groups.length > 0) {
theText.characters[1].remove();
}
}
Function Snippets
A Mac user, I can't begin to describe how useful iSnip is. I'm sure there must be similar utilities for Windows. Most of the snippets I keep are JavaScript related. Here are a couple of the more significant that are currently in my Functions folder:
Basically, you call it with two parameters: the document you're working on and the story you care about. If that story is overset, then pages are added to the document until either it is not overset or until a permanent overset condition is detected (adding pages doesn't help because the story won't fit into the frames being added -- this might be for two reasons I'm aware of: (1) use of No Break making a line of text too wide (2) inclusing of an inline graphic that is too tall to fit in the added frames.
Why do I call it a Project library? Well, the concept is that your currently active document (there'd better be one when you call this function) is part of a project you're working on and the library for that project is either (a) already open or (b) somewhere in the folder hierarchy that leads to the active document. So, if the library is not open, it searches up the folder hierarchy until it either finds the libary or until it reaches the top.
A word of caution: most of the file systems that InDesign works with are case-independent but JavaScript is not, so if you get the case wrong in the name of the library file, the library may indeed be opened, but if you're using that other name to later try to access the opened library it won't work.
A second word of caution: when libraries appear in the palette, they do not display the .indl extension, but this is considered part of the name by JavaScript, so if you see "B-Library" in the palette, to access it you must use "B-Library.indl".
Dumb Run Pages
function DumbRunPages(theDoc, theStory) {I called this "dumb" to distinguish it from other scripts I've written where each master page had, in effect, a "next master" associated with it. That dates back to the days when I used to manage left/right masters to overcome the difficulties of bleeding on all four sides of the page when working with facing-pages documents. It's now over three years since I did any of that kind of work, so perhaps I shouldn't have called this function dumb at all. Mind you, within the context that I wrote this function, there were some smarts in the script that called this function to revisit the added pages and update the applied masters, so that too was part of the rationale for the name.
// What makes this "dumb" is that default master pages are used.
var uRuler = theDoc.viewPreferences.rulerOrigin;
theDoc.viewPreferences.rulerOrigin = RulerOrigin.spreadOrigin;
while (theStory.textFrames[-1].overflows) {
theDoc.documentPreferences.pagesPerDocument = theDoc.documentPreferences.pagesPerDocument + 1;
var backPage = theDoc.pages[-1];
app.activeWindow.activePage = backPage;
backPage.appliedMaster = theDoc.pages[-2].appliedMaster;
var myPbounds = backPage.bounds;
var myNewTF = backPage.textFrames.add();
if ((backPage.name % 2 == 1) || (!theDoc.documentPreferences.facingPages)) {
myNewTF.geometricBounds =
[myPbounds[0] + backPage.marginPreferences.top,
myPbounds[1] + backPage.marginPreferences.left,
myPbounds[2] - backPage.marginPreferences.bottom,
myPbounds[3] - backPage.marginPreferences.right];
} else {
myNewTF.geometricBounds =
[myPbounds[0] + backPage.marginPreferences.top,
myPbounds[1] + backPage.marginPreferences.right,
myPbounds[2] - backPage.marginPreferences.bottom,
myPbounds[3] - backPage.marginPreferences.left];
}
myNewTF.itemLayer = theStory.textFrames[-1].itemLayer;
myNewTF.previousTextFrame = theStory.textFrames[-1];
myNewTF.textFramePreferences.textColumnCount = backPage.marginPreferences.columnCount;
myNewTF.textFramePreferences.textColumnGutter = backPage.marginPreferences.columnGutter;
if (myNewTF.characters.length == 0){
theDoc.viewPreferences.rulerOrigin = uRuler;
throw ("Permanently overset"); // This indicates a permanent overset condition so break out of loop
}
}
theDoc.viewPreferences.rulerOrigin = uRuler;
}
Basically, you call it with two parameters: the document you're working on and the story you care about. If that story is overset, then pages are added to the document until either it is not overset or until a permanent overset condition is detected (adding pages doesn't help because the story won't fit into the frames being added -- this might be for two reasons I'm aware of: (1) use of No Break making a line of text too wide (2) inclusing of an inline graphic that is too tall to fit in the added frames.
Get Project Library
function getProjectLibrary(libName) {This too is a dumbed down version of a longer function that gives the user the opportunity to use the file manager to go find the library if it can't be found. But that other version is so long it makes all my scripts look somewhat overwhelming and so I usually prefer this simpler function that just gives up if it fails to find the library named in the call.
try {
var myLib = app.libraries.item(libName);
myLib.name;
return myLib
} catch (e) {
var myFolder = app.activeDocument.filePath;
while (myFolder != null) {
if (File(myFolder.fsName + "/" + libName).exists) {
app.open(File(myFolder.fsName + "/" + libName));
return app.libraries.item(libName);
} else {
myFolder = myFolder.parent;
}
}
alert("Couldn't find library");
exit();
}
}
Why do I call it a Project library? Well, the concept is that your currently active document (there'd better be one when you call this function) is part of a project you're working on and the library for that project is either (a) already open or (b) somewhere in the folder hierarchy that leads to the active document. So, if the library is not open, it searches up the folder hierarchy until it either finds the libary or until it reaches the top.
A word of caution: most of the file systems that InDesign works with are case-independent but JavaScript is not, so if you get the case wrong in the name of the library file, the library may indeed be opened, but if you're using that other name to later try to access the opened library it won't work.
A second word of caution: when libraries appear in the palette, they do not display the .indl extension, but this is considered part of the name by JavaScript, so if you see "B-Library" in the palette, to access it you must use "B-Library.indl".
Thursday, December 15, 2005
Replace with Clipboard
One oft-requested feature for InDesign is the ability to replace found text with the current contents of the clipboard. It came to me this morning that this is a case where the UI can be used to set-up a script, and consequently, the script itself is very simple.
The standard advice for a scripter doing a search is to use:
But what is not available in Find/Change is the ability to replace the found text with the current contents of the clipboard. So, let's modify our workflow:
So, when might you use this script? Two possibilities leap to mind:
The standard advice for a scripter doing a search is to use:
app.findPreferences = null;before doing a search. This clears out any settings left over from the last time that Find/Change was used. But what if you want to use the search options that are already there? Well, this can certainly be done using the following workflow:
app.changePreferences = null;
- Open the Find/Change dialog and set-up a search of whatever complexity you wish.
- Click Done. Or, move the dialog aside.
- Run the following script.
app.search();And that's it. But not really all that useful in this form because you could as easily have achieved this result by simply clicking Change All.
But what is not available in Find/Change is the ability to replace the found text with the current contents of the clipboard. So, let's modify our workflow:
- Copy to the clipboard that which you wish to change the found text into.
- Open the Find/Change dialog and set-up a search of whatever complexity you wish.
- Click Done. Or, move the dialog aside.
- Run the following script.
myFinds = app.search()This version assumes that you used the dialog to set-up the change-side formatting. But if that is implicit in the contents of the clipboard, then you should run this minor variation of the script:
for (j = myFinds.length - 1; j >= 0; j--) {
app.select(myFinds[j]);
app.paste();
}
app.changePreferences = null;This version wipes out any settings on the change side before running the search.
myFinds = app.search()
for (j = myFinds.length - 1; j >= 0; j--) {
app.select(myFinds[j]);
app.paste();
}
So, when might you use this script? Two possibilities leap to mind:
- If you want to replace with an alternate glyph that can't be inserted into the Change side in the Find/Change dialog.
- If you want to replace text with an inline graphic or frame of some kind.
Saturday, December 03, 2005
How to begin scripting? (Part 1)
On December 1st, a visitor to the Adobe InDesign Scripting User to User forum asked:
I need [a] script for overset text, but I [am a] novice in scripting. Maybe I can download this?
I interpreted this as a request for help on the issue of how to set about creating a script to solve this problem. In the absence of information about which version of InDesign and which language, I chose to assume that the poster was using the latest version and that a JavaScript solution would meet the need, particularly as that would be cross-platform and again, I had no information about platform. The following is an edited version of my response:
The first step towards scripting is to choose something that you need to do and focus on what it takes to achieve that goal -- for example, if you were to do it manually, what would you do? That helps you decide which parts of the object model you need to be aware of to achieve your goal.
The reason for this is that the object model is a huge beast with everything you ever wanted for scripting anything at all (well, nearly anything) in InDesign. The good news is, you do not need to master every aspect to achieve a focused goal. Indeed, you are likely to need only a small percentage of it. If you think about it, the same is true when using the UI. Newcomers in particular just assume that the application is looking after them in countless ways when they take their first steps to creating a document.
So, in this case, we need to determine if a story or a table cell is overset. In the UI, we get a red + for the story and a red dot for the text cell.
Well, that means that a script needs to look at every story and at every table cell to see if they are overset.
Let's go to the JavaScript section of the InDesign CS2 Reference Guide and look at the properties of a Story. The first thing I'd want to know is: "Does the Story object have a property called 'overset'?"
[Aside: even here, I'm kind of leaping in without introducing at all the concept of a "property" -- does it matter at this stage? Perhaps not, but somewhere I should write about that.]
Using the Story bookmark in the reference guide, I get myself to page 1069 and there's the list of properties. I scroll down (what a long list!) until I get to the "Os" on 1073/74 and while there is no property called overset, there is one called overflows that looks promising.
Yes, it's just what we need. So, if we can find all instances in our document of stories that have the overflow property set to true, then we've found the overset stories. For the moment, let's set aside the issue of how do we inform the user that an overset story has been found and focus on the code we need to find an instance.
So, stories live in documents. Let's take a look at the document object and see what it has in the way of properties. Click the bookmark and we're on page 679. Scroll down to 683 and yes indeedy a document has a property called stories. OK, so how do we refer to the document? For that we need to know what the parent of a document is (or could be).
Unhelpfully, on page 682, the definition of the parent property reads: "The parent of the object." Well -- duh! So we'll have to use our intuition (or preknowledge). There's another object call Application. It's top of the tree (so to speak) and most high-level objects like documents have it as a parent. Another bookmark and another click and we're on page 567.
Before we storm off to find the documents property, let's spend a moment contemplating the very first property: activeDocument. Hey, that sounds useful. I think we're ready to start writing a script.
If we did any reading of the documentation, we know that the way to refer to the application in InDesign is by using the built-in object named app. So, let's make a reference to the active document:
You'd be disappointed. JavaScript doesn't work that way. It creates a reference to a null document in this case. Only if you try to use myDoc will you get an error. Often, this doesn't make a lot of difference because usually you create a reference like this and then immediately use it. But not always, so it is vital to understand this aspect of JavaScript because otherwise you'll be looking for bugs in all the wrong places.
There's two ways to deal with this particular issue: 1. check to see if there are any open documents before setting myDoc, or 2. trap the error when you do use myDoc. Choice 1 is probably the better way to go and that's what I'll use, but let me at least show you have to do it the other way because there are times when this comes in handy:
Now we could save the script if we wanted to but it's hardly worth it at this stage, let's just run it from ESTK. First, make sure that the target in the drop-down list at top left is set to InDesign CS2 and if so, click the Run button above the script (it's the left-most of the six buttons up there).
Depending on whether or not you have a document open, you'll get one or other of the two alerts.
So what's the better way to do it? Well, normally for this particular issue (is there an active document), I use the documents property of the application, like this:
There's an important point buried in this that might not be obvious to the newcomer. As the application's documents property is properly described on page 568, it refers to all open documents, not all the documents on your computer. So, checking the length of that property tells you how many are indeed open. As long as the answer isn't zero, we're in business.
So, now we want to find the overflows property of all the stories of myDoc. There's a way of doing this that sadly didn't make it into the reference guide and that is to use the everyItem() method. You use it like this:
For this we need a for-loop. I write them in a slightly odd way to avoid the use of the < because it is a bit of a pain to have to substitute < every time I need it. Hence:
The idea here is that having located an overset story, what we want to do is make it visible in the UI to the user. Well, you can't select a story because it is only a logical entity. It manifests itself in the UI as text in text frames. These can be selected but not the story itself.
Selecting the text of the story might be helpful, but more likely selecting the last text frame is what you want. That's why I passed: myDoc.stories[j].textFrames[-1] to the function we still have to write. That's a reference to the last text frame of the j-th story which at the point we make this call we know to be overset because its overflows property is true.
There are lots of ways you might go about trying to select that text frame to show it to the user, but InDesign makes this very easy -- even though it took me over two years to realize this.
So let's write the selectIt function. It takes one parameter (or argument -- depends on your background which term you use; they mean the same). In our main script, we're passing a reference to a text frame, but the function can have much wider scope. It can work with anything that is selectable.
We might want to use this same function to highlight some text and show it to the user or a group that has certain characteristics, or anything at all.
So, our formal definition of the function uses a surrogate name for the argument:
So, the first statement selects the referenced object, replacing whatever selection there might previously have been -- this is actually the default behavior of the app.select() method describe on page 676 of the guide, but it does no harm to explicitly call for this behavior.
Now, the script takes advantage of something I didn't cotton on to for a long time. Once you've made a selection, whichever page it is on becomes the activePage of the activeWindow [even if it is not visible! this is like using the vertical scroll bar to view some other part of a document -- you might be looking at page 168, but if you have a selection on page 3, that's still the active page], so the script doesn't have to worry about where it is because InDesign already knows and will do all the hard work for me -- when I think about the stumbling attempts I made to do this myself, looking for the parent to determine the active page, I could kick myself.
All I have to do is zoom the activeWindow and InDesign turns to the page all by itself. So why the double zoom?
Because if the selected object is not visible in the window, the zoom to 200% will zoom in on the current center rather than the selected object.
There you have it. Piece this lot together and you have a script that will detect and select the last text frame of the first overset story it finds.
I'll leave it as an exercise to handle the table cells.
By the way, I don't think this script, as is, will work with InDesign CS. I believe for it, you have to check the overflows property of the last text frame of each story because of a bug that is fixed in CS2.
I need [a] script for overset text, but I [am a] novice in scripting. Maybe I can download this?
I interpreted this as a request for help on the issue of how to set about creating a script to solve this problem. In the absence of information about which version of InDesign and which language, I chose to assume that the poster was using the latest version and that a JavaScript solution would meet the need, particularly as that would be cross-platform and again, I had no information about platform. The following is an edited version of my response:
The first step towards scripting is to choose something that you need to do and focus on what it takes to achieve that goal -- for example, if you were to do it manually, what would you do? That helps you decide which parts of the object model you need to be aware of to achieve your goal.
The reason for this is that the object model is a huge beast with everything you ever wanted for scripting anything at all (well, nearly anything) in InDesign. The good news is, you do not need to master every aspect to achieve a focused goal. Indeed, you are likely to need only a small percentage of it. If you think about it, the same is true when using the UI. Newcomers in particular just assume that the application is looking after them in countless ways when they take their first steps to creating a document.
So, in this case, we need to determine if a story or a table cell is overset. In the UI, we get a red + for the story and a red dot for the text cell.
Well, that means that a script needs to look at every story and at every table cell to see if they are overset.
Let's go to the JavaScript section of the InDesign CS2 Reference Guide and look at the properties of a Story. The first thing I'd want to know is: "Does the Story object have a property called 'overset'?"
[Aside: even here, I'm kind of leaping in without introducing at all the concept of a "property" -- does it matter at this stage? Perhaps not, but somewhere I should write about that.]
Using the Story bookmark in the reference guide, I get myself to page 1069 and there's the list of properties. I scroll down (what a long list!) until I get to the "Os" on 1073/74 and while there is no property called overset, there is one called overflows that looks promising.
Yes, it's just what we need. So, if we can find all instances in our document of stories that have the overflow property set to true, then we've found the overset stories. For the moment, let's set aside the issue of how do we inform the user that an overset story has been found and focus on the code we need to find an instance.
So, stories live in documents. Let's take a look at the document object and see what it has in the way of properties. Click the bookmark and we're on page 679. Scroll down to 683 and yes indeedy a document has a property called stories. OK, so how do we refer to the document? For that we need to know what the parent of a document is (or could be).
Unhelpfully, on page 682, the definition of the parent property reads: "The parent of the object." Well -- duh! So we'll have to use our intuition (or preknowledge). There's another object call Application. It's top of the tree (so to speak) and most high-level objects like documents have it as a parent. Another bookmark and another click and we're on page 567.
Before we storm off to find the documents property, let's spend a moment contemplating the very first property: activeDocument. Hey, that sounds useful. I think we're ready to start writing a script.
If we did any reading of the documentation, we know that the way to refer to the application in InDesign is by using the built-in object named app. So, let's make a reference to the active document:
myDoc = app.activeDocument;Hey, but what if you try to run the script with no documents open? Which is the active document then? Well, there isn't one and you'd expect an error if you ran this little one-line script.
You'd be disappointed. JavaScript doesn't work that way. It creates a reference to a null document in this case. Only if you try to use myDoc will you get an error. Often, this doesn't make a lot of difference because usually you create a reference like this and then immediately use it. But not always, so it is vital to understand this aspect of JavaScript because otherwise you'll be looking for bugs in all the wrong places.
There's two ways to deal with this particular issue: 1. check to see if there are any open documents before setting myDoc, or 2. trap the error when you do use myDoc. Choice 1 is probably the better way to go and that's what I'll use, but let me at least show you have to do it the other way because there are times when this comes in handy:
myDoc = app.activeDocument;OK, that's a complete script. What to do with it? If you're using CS2, copy it to the clipboard, open ExtendScript Toolkit (ESTK for short) which you'll find in your utilities folder in a folder named Adobe Utilities (at least, that's where it is on a Mac). Make a New script from the File menu and paste this script into it.
try {
myName = myDoc.name;
} catch (e) {
alert("Got an error: " + e);
exit();
}
alert("The active document is named: " + myName)
Now we could save the script if we wanted to but it's hardly worth it at this stage, let's just run it from ESTK. First, make sure that the target in the drop-down list at top left is set to InDesign CS2 and if so, click the Run button above the script (it's the left-most of the six buttons up there).
Depending on whether or not you have a document open, you'll get one or other of the two alerts.
So what's the better way to do it? Well, normally for this particular issue (is there an active document), I use the documents property of the application, like this:
if (app.documents.length == 0) {Now we know that myDoc is pointing at a real document.
alert("There are no open documents.");
exit();
}
myDoc = app.activeDocument;
There's an important point buried in this that might not be obvious to the newcomer. As the application's documents property is properly described on page 568, it refers to all open documents, not all the documents on your computer. So, checking the length of that property tells you how many are indeed open. As long as the answer isn't zero, we're in business.
So, now we want to find the overflows property of all the stories of myDoc. There's a way of doing this that sadly didn't make it into the reference guide and that is to use the everyItem() method. You use it like this:
myOsetList = myDoc.stories.everyItem().overflows;This gives you a list of the overflow state of all the stories in your document. So all we have to do is check them all to see if any is true, and if so find a way of alerting the user.
For this we need a for-loop. I write them in a slightly odd way to avoid the use of the < because it is a bit of a pain to have to substitute < every time I need it. Hence:
for (j = 0; myOsetList.length > j; j++) {If you've been following along adding these lines to the script and you try to run it now, it'll work if there are no overset stories, but if there is one, it'll have a hissy fit when it gets to the "selectIt" line. That's a call to a function that we haven't included yet.
if (myOsetList) {
// j-th story is overset; tell the user and exit
selectIt(myDoc.stories.textFrames[-1]);
exit();
}
alert("No stories are overset");
The idea here is that having located an overset story, what we want to do is make it visible in the UI to the user. Well, you can't select a story because it is only a logical entity. It manifests itself in the UI as text in text frames. These can be selected but not the story itself.
Selecting the text of the story might be helpful, but more likely selecting the last text frame is what you want. That's why I passed: myDoc.stories[j].textFrames[-1] to the function we still have to write. That's a reference to the last text frame of the j-th story which at the point we make this call we know to be overset because its overflows property is true.
There are lots of ways you might go about trying to select that text frame to show it to the user, but InDesign makes this very easy -- even though it took me over two years to realize this.
So let's write the selectIt function. It takes one parameter (or argument -- depends on your background which term you use; they mean the same). In our main script, we're passing a reference to a text frame, but the function can have much wider scope. It can work with anything that is selectable.
We might want to use this same function to highlight some text and show it to the user or a group that has certain characteristics, or anything at all.
So, our formal definition of the function uses a surrogate name for the argument:
function selectIt(theObj) {As written, this assumes that the parameter passed "theObj" is indeed selectable. I suppose it might be worth checking for that and issuing an error message. But I've been happy enough in my use of this function to not bother.
// Selects object, turns to page and zooms in on it
app.select(theObj,SelectionOptions.replaceWith);
app.activeWindow.zoom = ZoomOptions.fitPage;
app.activeWindow.zoomPercentage = 200
}
So, the first statement selects the referenced object, replacing whatever selection there might previously have been -- this is actually the default behavior of the app.select() method describe on page 676 of the guide, but it does no harm to explicitly call for this behavior.
Now, the script takes advantage of something I didn't cotton on to for a long time. Once you've made a selection, whichever page it is on becomes the activePage of the activeWindow [even if it is not visible! this is like using the vertical scroll bar to view some other part of a document -- you might be looking at page 168, but if you have a selection on page 3, that's still the active page], so the script doesn't have to worry about where it is because InDesign already knows and will do all the hard work for me -- when I think about the stumbling attempts I made to do this myself, looking for the parent to determine the active page, I could kick myself.
All I have to do is zoom the activeWindow and InDesign turns to the page all by itself. So why the double zoom?
Because if the selected object is not visible in the window, the zoom to 200% will zoom in on the current center rather than the selected object.
There you have it. Piece this lot together and you have a script that will detect and select the last text frame of the first overset story it finds.
I'll leave it as an exercise to handle the table cells.
By the way, I don't think this script, as is, will work with InDesign CS. I believe for it, you have to check the overflows property of the last text frame of each story because of a bug that is fixed in CS2.