Sunday, July 30, 2006
How to Crash InDesign
Use the script in that last post from yesterday on a table of 10 or more rows!
Why does it crash? Because of a stupid error that is hard to see. The problem is that the start and end values I'm calculating are strings, not numbers, so the internal loop compares (in the case I was working with) 2 with 11 and concludes that 2 is greater than 11 (because as strings, that's true) and so the loop doesn't execute. This results in the script concluding that each of the columns should have a width of Infinity.
Believe it or not, this works, causing the table to become overset (there's a surprise). You can even undo to get back to where you were. But if you don't and you try to work with the document it will crash and then it is irretrievably corrupted.
Here's a corrected version of the script. You'll notice I changed the names of the arguments to getCell because I thought that might have something to do with the problem until I realized what was really going on.
Why does it crash? Because of a stupid error that is hard to see. The problem is that the start and end values I'm calculating are strings, not numbers, so the internal loop compares (in the case I was working with) 2 with 11 and concludes that 2 is greater than 11 (because as strings, that's true) and so the loop doesn't execute. This results in the script concluding that each of the columns should have a width of Infinity.
Believe it or not, this works, causing the table to become overset (there's a surprise). You can even undo to get back to where you were. But if you don't and you try to work with the document it will crash and then it is irretrievably corrupted.
Here's a corrected version of the script. You'll notice I changed the names of the arguments to getCell because I thought that might have something to do with the problem until I realized what was really going on.
//DESCRIPTION: Table Space Equalizer
/*
Expects a multi-column selection within a table. Adjusts widths of selected
columns so that space after the longest entry in each column is equalized and
the overall width of the table is unchanged.
*/
if ((app.documents.length == 0) || (app.selection.length == 0)) { exit() }
aDoc = app.activeDocument;
aSel = app.selection[0];
try {
theCols = {start: Number(aSel.cells[0].name.split(":")[0]),end: Number(aSel.cells[-1].name.split(":")[0])};
theRows = {start: Number(aSel.cells[0].name.split(":")[1]),end: Number(aSel.cells[-1].name.split(":")[1])};
theTable = aSel.parent;
} catch (e) {
alert("Selection needs to be in a table, and rectangular"), exit();
}
// For each column, calculate the min spare space:
minSpaces = new Array();
for (c = theCols.start; theCols.end >= c; c++) {
minSpace = Infinity;
for (r = theRows.start; theRows.end >= r; r++) {
minSpace = Math.min(minSpace, space(getCell(theTable, c, r,)));
}
minSpaces.push(minSpace);
}
totSpace = 0;
for (j = 0; minSpaces.length > j; j++) {
totSpace = totSpace + minSpaces[j]
}
newSpace = totSpace/minSpaces.length;
for (c = theCols.start; theCols.end >= c; c++) {
theTable.columns[c].width = theTable.columns[c].width + newSpace - minSpaces[c - theCols.start];
}
function getCell(table, n, m) {
return table.cells.item(n + ":" + m);
}
function space(theCell) {
// This version assumes that the cell is left aligned and there's only one line
var left = theCell.texts[0].insertionPoints[0].horizontalOffset;
theCell.texts[0].justification = Justification.rightAlign;
// theCell.texts[0].recompose();
var spare = theCell.texts[0].insertionPoints[0].horizontalOffset - left;
theCell.texts[0].justification = Justification.leftAlign;
return spare;
}
Saturday, July 29, 2006
Completed Script
As the comment under the description indicates, this script only has applicability in limited situations, but it works for what I'm doing right now and the result is spectacular. I'll revisit this later and make it work for multi-line cells, but don't hold your breath if you need it urgently.
Warning: This Script Has a Serious Problem; see next post.
//DESCRIPTION: Table Space Equalizer
/*
Expects a multi-column selection within a table. Adjusts widths of selected
columns so that space after the longest entry in each column is equalized and
the overall width of the table is unchanged.
*/
if ((app.documents.length == 0) || (app.selection.length == 0)) { exit() }
aDoc = app.activeDocument;
aSel = app.selection[0];
try {
theCols = {start: aSel.cells[0].name.split(":")[0],end: aSel.cells[-1].name.split(":")[0]};
theRows = {start: aSel.cells[0].name.split(":")[1],end: aSel.cells[-1].name.split(":")[1]};
theTable = aSel.parent;
} catch (e) {
alert("Selection needs to be in a table, and rectangular"), exit();
}
// For each column, calculate the min spare space:
minSpaces = new Array();
for (c = theCols.start; theCols.end >= c; c++) {
minSpace = Infinity;
for (r = theRows.start; theRows.end >= r; r++) {
minSpace = Math.min(minSpace, space(getCell(theTable, c, r,)));
}
minSpaces.push(minSpace);
}
totSpace = 0;
for (j = 0; minSpaces.length > j; j++) {
totSpace = totSpace + minSpaces[j]
}
newSpace = totSpace/minSpaces.length;
for (c = theCols.start; theCols.end >= c; c++) {
theTable.columns[c].width = theTable.columns[c].width + newSpace - minSpaces[c - theCols.start];
}
function getCell(table, c, r) {
return table.cells.item(c + ":" + r);
}
function space(theCell) {
// This version assumes that the cell is left aligned and there's only one line
var left = theCell.texts[0].insertionPoints[0].horizontalOffset;
theCell.texts[0].justification = Justification.rightAlign;
// theCell.texts[0].recompose();
var spare = theCell.texts[0].insertionPoints[0].horizontalOffset - left;
theCell.texts[0].justification = Justification.leftAlign;
return spare;
}
Cracked it, But...
First, I walked into another trap. Here is my first attempt to address the issue. I decided to make my two variables theCols and theRows record objects that contain two properties named start and end into which I put the relevant row and column indexes, like this:
The correct way to address the cell I need is:
if ((app.documents.length == 0) || (app.selection.length == 0)) { exit() }And blow me but I still got the wrong value. This time, I got the cell below the one I was expecting to get. Why? because of a merged row in the top row of the table. That means that the column I'm looking at doesn't have a cell in that row, so using the cell row number as an index to the column's cells misses by one (in this case).
aDoc = app.activeDocument;
aSel = app.selection[0];
try {
theCols = {start: aSel.cells[0].name.split(":")[0],end: aSel.cells[-1].name.split(":")[0]};
theRows = {start: aSel.cells[0].name.split(":")[1],end: aSel.cells[-1].name.split(":")[1]};
theTable = aSel.parent;
} catch (e) {
alert("Selection needs to be in a table, and rectangular"), exit();
}
alert(theTable.columns[theCols.start].cells[theRows.start].contents);
The correct way to address the cell I need is:
alert(theTable.cells.item([theCols.start] + ":" + [theRows.start]).contents);which looks pretty ugly, but it has the benefit of working!
Addressing a Selection inside a Table
I'm working on a script this morning that is intended to equalize the spare space in part of a table so that the extra space is equally distributed across the selected columns. Life would be easier if the script had to work on the whole table, but that's not what I need. I need it to work on a selection of rows and columns. So, the first thing the script needs to do is workout which part of the table is selected.
You'd think this would be easy, but there is a bug (or an implementation decision) that really bites if you go at this what seems the reasonable way:
So watch out. I'll report back later on the technique I end up using to get around this.
You'd think this would be easy, but there is a bug (or an implementation decision) that really bites if you go at this what seems the reasonable way:
if ((app.documents.length == 0) || (app.selection.length == 0)) { exit() }Run this with a selection that does not include the top-left corner of the table and you'll be surprised to see that the alert gives you the contents of that top-left cell. It appears that when asking for the columns or rows of a selection within a table in this straightforward manner returns the wrong columns and rows.
aDoc = app.activeDocument;
aSel = app.selection[0];
try {
theCols = aSel.columns;
theRows = aSel.rows;
} catch (e) {
alert("Selection needs to be in a table"), exit();
}
alert(theCols[0].cells[0].contents);
So watch out. I'll report back later on the technique I end up using to get around this.
Friday, July 28, 2006
The Emaciated, Anonymous, Invisible Parent of Root
You would think that the Root element would be the bottom of the XML element parental tree. After all, that's what the structure panel shows. But if you ask for:
Well, it turns out that the Root element is not the end of the line when it comes to traversing the XML elements of your document. There's another element that serves as the parent of the Root element. This particular element is somewhat emaciated. It does not have the full capabilities of a regular XML element.
Contrast the result of:
Knowing that this element is there can be important if you're debugging a script that stumbles upon it, but please have the discipline to leave it alone. Attempts to change the values of its properties will at best fail and there is some danger of crashing InDesign. The reason this element is anonymous and invisible in the UI is precisely because it is not a regular XML Element to be used along with all the others.
app.activeDocument.associatedXMLElement.markUpTag.name;you'll be surprised to get an error: "Object does not support the property or method 'markUpTag'"--how can this be?
Well, it turns out that the Root element is not the end of the line when it comes to traversing the XML elements of your document. There's another element that serves as the parent of the Root element. This particular element is somewhat emaciated. It does not have the full capabilities of a regular XML element.
Contrast the result of:
app.activeDocument.associatedXMLElement.properties.toSource()which returns:
({storyOffset:0,with the result of:
parentStory:null,
contents:"",
id:1,
parent:resolve("/document[@name=\"Untitled-2\"]"),
index:-1})
app.activeDocument.associatedXMLElement.xmlElements[0].properties.toSource()which returns:
({storyOffset:1,See how this second xmlElement, which in fact is the Root element, has more properties than the first. One of those properties is a markUpTag, so let's take a look at that:
parentStory:null,
markupTag:resolve("/document[@name=\"Untitled-2\"]//XML-tag[@id=171]"),
contents:"",
id:2,
parent:resolve("/document[@name=\"Untitled-2\"]/XML-element[@id=1]"),
index:0})
app.activeDocument.xmlTags.itemByID(171).properties.toSource();returns:
({name:"Root",So there you have it, the emaciated, anonymous, invisible parent of Root.
tagColor:1766613612,
id:171,
label:"",
parent:resolve("/document[@name=\"Untitled-2\"]"),
index:0})
Update *** Warning
Knowing that this element is there can be important if you're debugging a script that stumbles upon it, but please have the discipline to leave it alone. Attempts to change the values of its properties will at best fail and there is some danger of crashing InDesign. The reason this element is anonymous and invisible in the UI is precisely because it is not a regular XML Element to be used along with all the others.
Sample Scripts Won't Work
What with family weddings and various other interferences, I've been neglecting this blog for a while. Sorry about that, but the description does warn that entries might be sporadic.
There's been a spate of complaints lately that the sample scripts provided with InDesign CS2 don't work. By which is usually meant: "I double-click the script name in the palette and nothing happens."
The cause of this problem is the userInteractionLevel property. As I wrote back in November (see: Open with no Warnings), a script can control the amount of interaction it does with the user. This is useful for causing dialogs to not appear with warnings that the script doesn't care about.
But:
The solution is simple, once you realize this possibility: simply precede any attempt to show a dialog with:
So, if you're trying to use those built-in sample scripts and they mysteriously refuse to do anything, the quick solution is to save this:
There's been a spate of complaints lately that the sample scripts provided with InDesign CS2 don't work. By which is usually meant: "I double-click the script name in the palette and nothing happens."
The cause of this problem is the userInteractionLevel property. As I wrote back in November (see: Open with no Warnings), a script can control the amount of interaction it does with the user. This is useful for causing dialogs to not appear with warnings that the script doesn't care about.
But:
- The property is application-wide and sticky.
- The property is not stored with the regular InDesign Preferences, so it survives trashing preferences.
- Scripts can accidentally (or negligently) leave it switched off.
app.scriptPreferences.userInteractionLevel = UserInteractionLevels.neverInteract;Then any other script you might run afterwards will not be able to show you dialogs. If they try, it will be as though you clicked Cancel even before the dialog was painted.
The solution is simple, once you realize this possibility: simply precede any attempt to show a dialog with:
app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;and the problem goes away. In a perfect world, this should not be necessary because any script which switches off user interaction should switch it back on again before quitting. But scripts have no control over the power company -- to name one reason why a script might not run properly to completion.
So, if you're trying to use those built-in sample scripts and they mysteriously refuse to do anything, the quick solution is to save this:
app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;as a script named "RestoreInteraction.jsx" and run it. For a more permanent solution, edit those sample scripts to include this line immediately before the line(s) where the show() method is used to display a dialog.