Monday, January 09, 2012
Syntax Highlighting Here
One of the reasons I took a long hiatus at this blog was because I learned how to do syntax highlighting in my posts and it was such a laborious process that I quickly tired of it because posting became such a daunting task.
But scripts are supposed to make daunting tasks more manageable, possibly even eliminating the daunt altogether. That's the first time I've ever used "daunt" as a noun like that. Probably not good English.
If you've ever looked at ESTK's preferences, you'll see that it offers about a million syntax highlighting controls. There's no way I want to duplicate that lot. My sights are set much lower. If I can highlight reserved words in blue, strings in red, and comments in green, I'll be happy.
The interesting thing is that I've already solved this problem in InDesign itself. When I want to showJS code in an InDesign document, I use a paragraph style with a bunch of GREP styles. I can't do that here, but perhaps I can use similar GREPs to get the right tags around the right text to achieve the desired effect.
And with that, the thought occurs that perhaps I can write a script that pulls the GREP search values out of the paragraph style so I don't have to maintain them in more than one place. Sounds like a fun project. Let's give it a shot.
More to come ...
Saturday, January 07, 2012
PopTabUnleashed
When InDesign CS4 hit the streets, I thought that PopTabFmClip was obsolete. I said so on my web site and basically abandoned it. But it turns out that there's one thing that PopTab did that isn't available in native InDesign. If you want to copy the content of one InDesign table into another (target) table without losing the formatting of the target table, then PopTab is the answer.
So, over the holidays, I decided to rework PopTab to make it available for users of CS4 and later. And I also decided to release it in source form so people are free to read its code if they wish. I'll examine the code here in this blog too to explain the techniques it uses. But the purpose of this blog entry is to announce the availability of PopTabUnleashed -- there's a link at the left of this page you can use to download it.
Here's how the script describes itself in the introductory comment at the head of its code:
So, over the holidays, I decided to rework PopTab to make it available for users of CS4 and later. And I also decided to release it in source form so people are free to read its code if they wish. I'll examine the code here in this blog too to explain the techniques it uses. But the purpose of this blog entry is to announce the availability of PopTabUnleashed -- there's a link at the left of this page you can use to download it.
Here's how the script describes itself in the introductory comment at the head of its code:
This script is provided as-is. Use at your own risk.As always, just move the script into your Scripts Panel folder so it is visible in your Scripts Panel. To use it, copy appropriate content to the clipboard, make a selection in your target table and then trigger the script to move the clipboard content into the table.
The script populates a table, or part of a table, with the text/values on the clipboard without changing the formatting/styling of the target table. The effects of running the script can be undone using Edit/Undo Populate Table.
The content of the clipboard is expected to be a table or part of a table. It can be a selection from Excel or any other application that exports table data, but you hardly need this script to support external applications because since CS4, InDesign provides this functionality: just paste into an existing table.
PopTab comes into its own when you're copying from one InDesign table to another. That is not natively supported because the formatting of the source table is included when you paste.
The selection is required to be in a table. It can be text in a cell, a cell, a range of cells, or a complete table. Selecting text in a cell is regarded as the same as selecting the cell.
Saturday, October 18, 2008
CS4 Change
Now that CS4 is with us, it's time to start a list of changes (as opposed to out-and-out new features) that might affect existing scripts. The first one I ran into affected my WrapNudger product.
The TextWrapPreferences property previously known as textWrapType is now called textWrapMode. This change affected the function I use in WrapNudger to determine whether or not the selection has a text wrap. Here's the revised version of the function which shows how I made the script work with either CS3 (InDesign version 5) or CS4 (InDesign version 6):
The TextWrapPreferences property previously known as textWrapType is now called textWrapMode. This change affected the function I use in WrapNudger to determine whether or not the selection has a text wrap. Here's the revised version of the function which shows how I made the script work with either CS3 (InDesign version 5) or CS4 (InDesign version 6):
function checkSelection() {
if (app.documents.length == 0 ||
app.selection.length == 0) {
alert("Please select an object with a text wrap."); return false;
}
if (Number(app.version.split(".")[0]) > 5) {
if (app.selection.length == 1 &&
(!(app.selection[0].hasOwnProperty("textWrapPreferences")) ||
app.selection[0].textWrapPreferences.textWrapMode == TextWrapModes.none)) {
alert("Selected item must have a text wrap."); return false;
}
} else {
if (app.selection.length == 1 &&
(!(app.selection[0].hasOwnProperty("textWrapPreferences")) ||
app.selection[0].textWrapPreferences.textWrapType == TextWrapTypes.none)) {
alert("Selected item must have a text wrap."); return false;
}
}
return true;
}
if (app.documents.length == 0 ||
app.selection.length == 0) {
alert("Please select an object with a text wrap."); return false;
}
if (Number(app.version.split(".")[0]) > 5) {
if (app.selection.length == 1 &&
(!(app.selection[0].hasOwnProperty("textWrapPreferences")) ||
app.selection[0].textWrapPreferences.textWrapMode == TextWrapModes.none)) {
alert("Selected item must have a text wrap."); return false;
}
} else {
if (app.selection.length == 1 &&
(!(app.selection[0].hasOwnProperty("textWrapPreferences")) ||
app.selection[0].textWrapPreferences.textWrapType == TextWrapTypes.none)) {
alert("Selected item must have a text wrap."); return false;
}
}
return true;
}
Wednesday, October 15, 2008
Story to Layer
I was working on a job this morning (actually in CS4, which is released for download today) when I realized that the text frame I was looking at was on the wrong layer. Indeed, all the frames in my story, except for the first few, were on the wrong layer. This must have been the result of my leaving that wrong layer active when I saved the template. I've now fixed the main script that created this situation, but to solve the issue in the document on which I was working, I wrote this quick script:
The script is contained within an anonymous function which starts out by checking that there is a selection. If there is, it confirms that the selection has a parentStory property, and if so, it moves all the text frames that constitute that story (textContainers in CS3 and CS4) on to the same layer as the first frame of the story.
//DESCRIPTION: Move all text frames of story to same layer
/*
This script assumes that the first frame of the story is on the right layer
*/
(function() {
if (app.documents.length > 0 &&
app.selection.length > 0) {
var aSel = app.selection[0];
if (!aSel.hasOwnProperty("parentStory")) {
alert("Selection has no parent story"); return;
}
var aStory = aSel.parentStory;
var aLayer = aStory.textContainers[0].itemLayer;
for (var j = aStory.textContainers.length - 1; j > 0; j--) {
aStory.textContainers[j].itemLayer = aLayer;
}
}
}())
/*
This script assumes that the first frame of the story is on the right layer
*/
(function() {
if (app.documents.length > 0 &&
app.selection.length > 0) {
var aSel = app.selection[0];
if (!aSel.hasOwnProperty("parentStory")) {
alert("Selection has no parent story"); return;
}
var aStory = aSel.parentStory;
var aLayer = aStory.textContainers[0].itemLayer;
for (var j = aStory.textContainers.length - 1; j > 0; j--) {
aStory.textContainers[j].itemLayer = aLayer;
}
}
}())
The script is contained within an anonymous function which starts out by checking that there is a selection. If there is, it confirms that the selection has a parentStory property, and if so, it moves all the text frames that constitute that story (textContainers in CS3 and CS4) on to the same layer as the first frame of the story.
Sunday, July 20, 2008
Orthogonal Graphic Lines
I was called upon the other day to write a script that required that the selection be an orthogonal graphic line. That is, one that was either perfectly horizontal or perfectly vertical.
A graphic line is not necessarily straight. It appears to be any object that has exactly two points, no matter how it was drawn. For example, anything drawn with the Line Tool is a graphic line; any two-point path drawn with the Pen Tool is a graphic line; any ploygon or rectangle that has been reduced to two points by deleting points from its original shape is a graphic line.
This means that finding an "orthogonal" line is not as simple a looking at the coordinates of a graphic line and checking to see if the two x-values or two y-values are the same as each other.
Here's a framework:
//DESCRIPTION: Framework for operating on selected orthogonal lines
(function() {
if (app.documents.length > 0) {
for (var j = app.documents[0].selection.length - 1; j >= 0; j--) {
if (app.documents[0].selection[j] instanceof GraphicLine &&
isOrthogonal(app.documents[0].selection[j])) {
processOrthogLine(app.documents[0].selection[j]);
}
}
}
function processOrthogLine(theLine) {
var delta = 0.00001;
var vert = Math.abs(theLine.paths[0].entirePath[0][1] - theLine.paths[0].entirePath[1][1]) > delta
alert("The selected line is " + (vert ? "vertical." : "horizontal."));
}
function isOrthogonal(gl) {
// By definition, a graphicLine has just one path with two pathPoints
var myPath = gl.paths[0];
var entirePath = myPath.entirePath;
if (entirePath[0].length > 2 || entirePath[1].length > 2) {
// one or other of the path points has control handles
// thus, the line is not straight
return false;
}
var delta = 0.00001;
if (Math.abs(entirePath[0][0] - entirePath[1][0]) < delta ||
Math.abs(entirePath[0][1] - entirePath[1][1]) < delta) {
return true;
}
return false;
}
}())
It is possible that lines drawn with carefully positioned control handles could result in a straight line, but we make the assumption that the lines we're looking for have no control handles.
A graphic line is not necessarily straight. It appears to be any object that has exactly two points, no matter how it was drawn. For example, anything drawn with the Line Tool is a graphic line; any two-point path drawn with the Pen Tool is a graphic line; any ploygon or rectangle that has been reduced to two points by deleting points from its original shape is a graphic line.
This means that finding an "orthogonal" line is not as simple a looking at the coordinates of a graphic line and checking to see if the two x-values or two y-values are the same as each other.
Here's a framework:
//DESCRIPTION: Framework for operating on selected orthogonal lines
(function() {
if (app.documents.length > 0) {
for (var j = app.documents[0].selection.length - 1; j >= 0; j--) {
if (app.documents[0].selection[j] instanceof GraphicLine &&
isOrthogonal(app.documents[0].selection[j])) {
processOrthogLine(app.documents[0].selection[j]);
}
}
}
function processOrthogLine(theLine) {
var delta = 0.00001;
var vert = Math.abs(theLine.paths[0].entirePath[0][1] - theLine.paths[0].entirePath[1][1]) > delta
alert("The selected line is " + (vert ? "vertical." : "horizontal."));
}
function isOrthogonal(gl) {
// By definition, a graphicLine has just one path with two pathPoints
var myPath = gl.paths[0];
var entirePath = myPath.entirePath;
if (entirePath[0].length > 2 || entirePath[1].length > 2) {
// one or other of the path points has control handles
// thus, the line is not straight
return false;
}
var delta = 0.00001;
if (Math.abs(entirePath[0][0] - entirePath[1][0]) < delta ||
Math.abs(entirePath[0][1] - entirePath[1][1]) < delta) {
return true;
}
return false;
}
}())
It is possible that lines drawn with carefully positioned control handles could result in a straight line, but we make the assumption that the lines we're looking for have no control handles.
Saturday, July 19, 2008
Align Left Edge
Over at InDesignSecrets.com, the topic arose about how to get rid of the space that sometimes appears at the left of the first capital letter of a paragraph. This is much more noticeable in some fonts than others. See: Removing Space Along the Left Edge of Text. The discussion gave me an idea for a script that would do the job in CS3 for paragraphs that don't have a drop-cap (for those that do, the Align Left Edge option is available in the Drop-cap dialog). That led to my posting this (slightly edited for posting here):
Here’s a proof of concept script deeply annotated. It assumes that the insertion point is in some paragraph and that temporarily making the first character into a drop cap won’t push the paragraph overset.
First, we need references to the paragraph and its first character:
var myPara = app.selection[0].paragraphs[0];
var myChar = myPara.characters[0];
Now we need to know the width of the character. I wrote a simple function which I called getWidth() to do this that returns the distance between the horizontalOffsets on left and right of the character.
var firstCharWidth = getWidth(myChar);
But, as we know, the Align Left Edge feature of InDesign only works if you have a drop cap and that changes the size of the character to an unknown point size. But still, we can use relative values, so the next two lines make the first character into a drop cap:
myPara.dropCapCharacters = 1;
myPara.dropCapLines = 2;
And this cockamamie next line switches on Align Left Edge — the dropcapDetail property not only uses different capitalization from the other dropCap related properties, it works like a two-bit bitmap. The right-most bit controls Align Left Edge, the left-most controls Scale for Descenders. So, it has four values:
0 means both off
1 means ALE on, SfD off
2 means SfD on, ALE off
3 means both on
myPara.dropcapDetail = 1;
Now we get the character width again, this time we’re getting the size of the dropcap at its unknown point size.
var largeCharWidth = getWidth(myChar);
And, having align left edge switched on, we record where the first insertion point is:
var largeAdjLeft = myChar.horizontalOffset;
Now we switch it off and get the revised position:
myPara.dropcapDetail = 0;
var largeBaseLeft = myChar.horizontalOffset;
This allows us to express the width of the left side bearing as a fraction of the width of the character (remember, way back at the start we got the original width):
var leftSideBearing = (largeBaseLeft - largeAdjLeft)/largeCharWidth;
So we can work out how much we need to shift the original character left by multiplying this fraction by the original widith:
var neededShift = leftSideBearing * firstCharWidth;
We have all the information we need. So, let’s switch off the drop cap:
myPara.dropCapLines = 1;
We’re going to add a hair space to the text to give us something to kern back over. But we have to bear in mind that kerning is a relative value. The absolute amount of a kern varies according to the size of the type. What we know is that 1000 kerning units is a distance of 1 em which, for 12 point type, is a value of 12 points. So, we need to normalize the kerning amount to what it would be if the type were at 12 points. And then add the width of a 12-point hair space (24 to the em):
var normalizedKernAmount = (neededShift * 12/myChar.pointSize) + .5;
Now we insert the hair space:
myPara.insertionPoints[0].contents = SpecialCharacters.hairSpace;
And, finally, we set the kerning value of the insertion point between the hair space and the first character, multiplying by -1000/12 to (a) make it a negative kern and (b) to convert the value in points to a fraction of 1000:
myPara.insertionPoints[1].kerningValue = normalizedKernAmount*-1000/12;
There you go: Align Left Edge for the first line of any paragraph.
Here’s a proof of concept script deeply annotated. It assumes that the insertion point is in some paragraph and that temporarily making the first character into a drop cap won’t push the paragraph overset.
First, we need references to the paragraph and its first character:
var myPara = app.selection[0].paragraphs[0];
var myChar = myPara.characters[0];
Now we need to know the width of the character. I wrote a simple function which I called getWidth() to do this that returns the distance between the horizontalOffsets on left and right of the character.
var firstCharWidth = getWidth(myChar);
But, as we know, the Align Left Edge feature of InDesign only works if you have a drop cap and that changes the size of the character to an unknown point size. But still, we can use relative values, so the next two lines make the first character into a drop cap:
myPara.dropCapCharacters = 1;
myPara.dropCapLines = 2;
And this cockamamie next line switches on Align Left Edge — the dropcapDetail property not only uses different capitalization from the other dropCap related properties, it works like a two-bit bitmap. The right-most bit controls Align Left Edge, the left-most controls Scale for Descenders. So, it has four values:
0 means both off
1 means ALE on, SfD off
2 means SfD on, ALE off
3 means both on
myPara.dropcapDetail = 1;
Now we get the character width again, this time we’re getting the size of the dropcap at its unknown point size.
var largeCharWidth = getWidth(myChar);
And, having align left edge switched on, we record where the first insertion point is:
var largeAdjLeft = myChar.horizontalOffset;
Now we switch it off and get the revised position:
myPara.dropcapDetail = 0;
var largeBaseLeft = myChar.horizontalOffset;
This allows us to express the width of the left side bearing as a fraction of the width of the character (remember, way back at the start we got the original width):
var leftSideBearing = (largeBaseLeft - largeAdjLeft)/largeCharWidth;
So we can work out how much we need to shift the original character left by multiplying this fraction by the original widith:
var neededShift = leftSideBearing * firstCharWidth;
We have all the information we need. So, let’s switch off the drop cap:
myPara.dropCapLines = 1;
We’re going to add a hair space to the text to give us something to kern back over. But we have to bear in mind that kerning is a relative value. The absolute amount of a kern varies according to the size of the type. What we know is that 1000 kerning units is a distance of 1 em which, for 12 point type, is a value of 12 points. So, we need to normalize the kerning amount to what it would be if the type were at 12 points. And then add the width of a 12-point hair space (24 to the em):
var normalizedKernAmount = (neededShift * 12/myChar.pointSize) + .5;
Now we insert the hair space:
myPara.insertionPoints[0].contents = SpecialCharacters.hairSpace;
And, finally, we set the kerning value of the insertion point between the hair space and the first character, multiplying by -1000/12 to (a) make it a negative kern and (b) to convert the value in points to a fraction of 1000:
myPara.insertionPoints[1].kerningValue = normalizedKernAmount*-1000/12;
There you go: Align Left Edge for the first line of any paragraph.
Delete "Empty" Pages
I often get into the state where a run of pages is dedicated to a particular story and because of editing the last pages of the story are now effectively empty; that is, the text frames on those pages are empty. This script seeks those pages and deletes them. So, the script must start with a framework that requires the selection to have a parentStory property:
//DESCRIPTION: Delete "empty" pages at end of selected story
(function() {
if (app.documents.length > 0 &&
app.selection.length == 1 &&
app.selection[0].hasOwnProperty("parentStory")) {
deleteEmptyPages(app.selection[0].parentStory);
}
function deleteEmptyPages(story){
}
}())
This is the first time I've used an anonymous function on this blog. I learned about them only recently (thanks Harbs and Kris). They help protect the global name space. In particular, you can have one script in this structure call another that uses the same variable or function names and there's no confusion because they're inside the anonymous function. Notice that the whole thing is inside parentheses. The pair of parentheses right at the end are the call to the function. Looks odd, but it works a charm.
So, now all we need do is write our function. This is a CS3 only script so we must use the textContainers property to get at the frames that constitute the story. Here's the function:
function deleteEmptyPages(story){
var myFrames = story.textContainers;
for (var j = myFrames.length - 1; j >= 1; j--) {
if (myFrames[j].insertionPoints.length == 0 &&
myFrames[j].parent instanceof Page) {
myFrames[j].parent.remove();
}
}
}
Notice that the loop goes down to a value of 1. By doing this, it assumes that you don't want to completely delete the story even if it is empty. Notice also that it checks that the parent is indeed a page before deleting it. We don't want to delete a page where the frame, albeit empty, has been grouped with something else.
//DESCRIPTION: Delete "empty" pages at end of selected story
(function() {
if (app.documents.length > 0 &&
app.selection.length == 1 &&
app.selection[0].hasOwnProperty("parentStory")) {
deleteEmptyPages(app.selection[0].parentStory);
}
function deleteEmptyPages(story){
}
}())
This is the first time I've used an anonymous function on this blog. I learned about them only recently (thanks Harbs and Kris). They help protect the global name space. In particular, you can have one script in this structure call another that uses the same variable or function names and there's no confusion because they're inside the anonymous function. Notice that the whole thing is inside parentheses. The pair of parentheses right at the end are the call to the function. Looks odd, but it works a charm.
So, now all we need do is write our function. This is a CS3 only script so we must use the textContainers property to get at the frames that constitute the story. Here's the function:
function deleteEmptyPages(story){
var myFrames = story.textContainers;
for (var j = myFrames.length - 1; j >= 1; j--) {
if (myFrames[j].insertionPoints.length == 0 &&
myFrames[j].parent instanceof Page) {
myFrames[j].parent.remove();
}
}
}
Notice that the loop goes down to a value of 1. By doing this, it assumes that you don't want to completely delete the story even if it is empty. Notice also that it checks that the parent is indeed a page before deleting it. We don't want to delete a page where the frame, albeit empty, has been grouped with something else.
Friday, January 11, 2008
Speeding up a Script
Most of the time, I pay scant attention to how long a script takes to run because most of them are so quick it hardly matters, but every now and then a simple script comes along that takes serious time to run. I just hit one as I sit here working on a catalog. I'm getting near the end of the job, making corrections and changes (mainly changes).
I realized that I've done so much dickering around to get things to fit on the page in earlier passes that it's time to reset the space before/after of every paragraph. Well, that's easy:
Clearly, we could cut that in half by changing the loop to this:
Ah well, back to work!
I realized that I've done so much dickering around to get things to fit on the page in earlier passes that it's time to reset the space before/after of every paragraph. Well, that's easy:
//DESCRIPTION: Restore vertical paragraph style spacingIf I were just running this on the odd page here and there, it wouldn't take too long, but document has 43 pages and the story has 782 paragraphs, so this loop interacts with InDesign 1564 times.
if (app.documents.length > 0 &&
app.selection.length == 1 &&
app.selection[0].hasOwnProperty("baseline")) {
restoreParagraphSpacing(app.selection[0]);
}
function restoreParagraphSpacing(myText) {
var myParas = myText.paragraphs;
for (var j = 0; myParas.length > j; j++) {
myParas[j].spaceBefore = myParas[j].appliedParagraphStyle.spaceBefore;
myParas[j].spaceAfter = myParas[j].appliedParagraphStyle.spaceAfter;
}
}
Clearly, we could cut that in half by changing the loop to this:
for (var j = 0; myParas.length > j; j++) {But this still requires 782 interactions with InDesign. And all of them happen even if the script does nothing because I'm running it for a second time. There's one more thing worth trying:
myParas[j].properties = {
spaceBefore:myParas[j].appliedParagraphStyle.spaceBefore,
spaceAfter:myParas[j].appliedParagraphStyle.spaceAfter
}
}
//DESCRIPTION: Restore vertical paragraph style spacingThe script still does a lot of interacting, but most of it is reading. It only writes a value into the document if there is a change needed, so right now the script runs very quickly because there are no longer any changes to be made.
if (app.documents.length > 0 &&
app.selection.length == 1 &&
app.selection[0].hasOwnProperty("baseline")) {
restoreParagraphSpacing(app.selection[0]);
}
function restoreParagraphSpacing(myText) {
var myParas = myText.paragraphs;
var mySBs = myParas.everyItem().spaceBefore;
var mySAs = myParas.everyItem().spaceAfter;
for (var j = 0; myParas.length > j; j++) {
if (myParas[j].spaceBefore != mySBs[j] || myParas[j].spaceAfter != mySAs[j]) {
myParas[j].properties = {
spaceBefore:myParas[j].appliedParagraphStyle.spaceBefore,
spaceAfter:myParas[j].appliedParagraphStyle.spaceAfter
}
}
}
}
Ah well, back to work!