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.