More Address Book scripting
March 26, 2012 at 11:59 PM by Dr. Drang
I’m still cleaning up my Address Book in preparation for a large mailing to my company’s clients. Today I wrote a couple of scripts, one in AppleScript and the other in Perl, to make address labels.
I already had a script for printing address labels, but it’s meant for Avery 5164 labels, the big ones you’d put on a box or a large envelope. What I need now is a script for Avery 5363 labels, more suited for a small envelope or large postcard.
Before we get into the scripts, you’re probably wondering why I don’t just use the label printing utility built in to Address Book. The easy answer is that Address Book doesn’t know about the 5363 layout, so I can’t print from there. But that’s not the real answer. I’m sure there’s some label printing application for the Mac that knows about 5363s, or maybe a Pages or FileMaker template that I could flow the addresses into, but I never even thought about these solutions because
- Given that I’ve already written several label-printing scripts, I knew I could whip out a new one with just a few edits.1
- I like the full control I get with my own scripts.
The new script is called pplabels
(print postcard labels), and it’s a minor edit of my older palabels
script. New label dimensions, three columns instead of two, but basically the same script.
perl:
1: #!/usr/bin/perl
2:
3: use Getopt::Std;
4:
5: # Usage/help message.
6: $usage = <<USAGE;
7: Usage: pflabels [options] [filename]
8: Print address labels on Avery 5363 sheets
9:
10: -r m : start at row m (range: 1..8; default: 1)
11: -c n : start at column n (range 1..3; default: 1)
12: -h : print this message
13:
14: If no filename is given, use STDIN. A label entry is a plain text
15: series of non-blank lines. Blank lines separate entries.
16: USAGE
17:
18: # Set up geometry constants for Avery 5363.
19: $topmargin = 0.125;
20: $poleft = 0.25;
21: $pomiddle = 3.10;
22: $poright = 5.93;
23: $lheight = 1.375;
24:
25: # get starting point from command line if present
26: getopts('hr:c:', \%opt);
27: die $usage if ($opt{h});
28:
29: $row = int($opt{r}) || 1; # chop off any fractional parts
30: $col = int($opt{c}) || 1; # or set defaults
31:
32: # Bail out if position options are out of bounds
33: die $usage unless (($row >= 1 and $row <= 8) and
34: ($col >= 1 and $col <= 3));
35:
36: # Set initial horizontal and vertical positions.
37: if ($col == 1) {
38: $po = $poleft;
39: } elsif ($col == 2){
40: $po = $pomiddle;
41: } else {
42: $po = $poright;
43: }
44: $sp = ($topmargin + ($row - 1)*$lheight);
45:
46: # Various outputs.
47: open OUT, "| groff -P-m | lpr"; # for printing directly
48: # open OUT, "| groff | ps2pdf - "; # for making a PDF
49: # open OUT, "> labels.rf"; # for debugging
50: select OUT;
51:
52: # Set up document.
53: print <<SETUP;
54: .ps 11
55: .vs 13
56: .nf
57: .ll 2.4i
58:
59: SETUP
60:
61: # The troff code for formatting a single entry, with placeholders for
62: # positioning on the page. The magic numbers embedded in the formatting
63: # commands make the layout look nice.
64: $label = <<ENTRY;
65: .sp |%.2fi
66: .po %.2fi
67: .ft H
68: %s
69: ENTRY
70:
71: # Slurp all the input into an array of entries.
72: $/ = "";
73: @entries = <>;
74: @sortedentries = sort @entries; # too lazy to sort on last name
75:
76: $bp = 0; # we don't want to start with a page break
77:
78: foreach $addr (@sortedentries) {
79: # Eliminate trailing whitespace
80: $addr =~ s/\s+$//;
81: # Scoot the address down if it's less than 6 lines long.
82: $nl = split(/\n/, $addr);
83: if ($nl <= 4) {
84: $addr = ".sp\n" . $addr;
85: } elsif ($nl == 5) {
86: $addr = ".sp 6.5p\n" . $addr;
87: }
88:
89: # Break page if we ran off the end.
90: if ($bp) {
91: print "\n.bp\n"; # issue the page break command
92: $bp = 0; # reset flag
93: }
94:
95: # Print the label.
96: printf $label, $sp, $po, $addr;
97:
98: # Now we set up for the next entry.
99: if ($col == 1){ # last entry was in the left column
100: $col = 2; # so the next will be in
101: $po = $pomiddle; # the middle column
102: } elsif ($col == 2){ # last entry was in the middle column
103: $col = 3; # so the next will be in
104: $po = $poright; # the right column
105: } else { # last was in the right column
106: $col = 1; # so the next will be in
107: $po = $poleft; # the left column
108: $row++; # of the next row
109: if ($row > 8) { # we're at the end of the page
110: $bp = 1; # page break flag
111: $row = 1; # new page starts at top row
112: }
113: $sp = ($topmargin + ($row - 1)*$lheight);
114: }
115: }
The script reads in a set of addresses in a plain text file that looks like this
John H. Kernighan
Thompson Industries
1337 Knuth Boulevard
Chicago, IL 60606
Cynthia P. Ossanna
Gamma, Helm, Johnson
Vlissides & Booth
314 East Addison Street
Suite 2200
New York, NY 10005
where the addresses are laid out just as if you were going to type them directly on the label, with blank lines separating the entries. The script inserts the appropriate troff codes, processes the result through groff
, and sends the PostScript file produced by groff
to the printer, which waits for the Avery 5363 sheets to be manually fed to it.
The printing can be started on any of the 24 label positions on the sheet by passing options to pplabels
. For example,
pplabels -r 3 -c 2 addresses.txt
will put the first label in the middle column of the third row and continue from there.
The code is fairly straightforward. Most of it is concerned with setting the initial label position (Lines 18-44) and then figuring out the next label position (Lines 98-113). There are a couple of things worth noting:
- Line 74 sorts the addresses by the first name of the recipient. As we will soon see, the addresses in the input file will not be any rational order, and I thought it would be useful to give them some structure before printing. But I was too lazy to write the comparison subroutines necessary to sort by anything other than first name. If I find myself using this script often, I’ll probably add options to sort by last name or zip code.
- Lines 82-87 adjust the vertical position of an individual address to make it reasonably close to centered on it label. It does this by looking at the address and scootching the top line down if it has fewer than six lines. Three- and four-line addresses get adjusted by one line height; five-line addresses get adjusted by half a line height.
OK, that’s how the labels get printed. Where does the input file come from? It comes from the Address Book and this AppleScript:
1: set out to ""
2: tell application "Address Book"
3: set cList to the selection
4: repeat with c in cList
5: tell c
6: set addrs to (every address whose label is "work")
7: set addr to item 1 of addrs
8: set out to out & first name & " "
9: if middle name is not missing value then
10: set out to out & middle name & " "
11: end if
12: set out to out & last name
13: if suffix is not missing value then
14: set out to out & " " & suffix
15: end if
16: set out to out & return & organization
17: if length of (organization as text) > 30 then
18: set out to out & "*"
19: end if
20: set out to out & return
21: tell addr
22: set out to out & street & return ¬
23: & city & ", " & state & " " & zip & return
24: if country is not "USA" then
25: set out to out & country & return
26: end if
27: end tell
28:
29: end tell
30: set out to out & return
31: end repeat
32: end tell
33: --get out
34: set the clipboard to out
This takes the contacts I have selected and puts their addresses—in the format given above (almost)—on the clipboard. I then paste that into a text file, and it’s ready to be processed by pplabels
.
Almost.
Take a look at Lines 17 and 18. If the company name is more than 30 characters long, the script puts an asterisk after it. I need that because several of my clients are law firms with long names—Goodman, Lieber, Kurtzberg & Holliway, LLP, for example—that need to be split into two lines. Because I don’t want line breaks at ampersands, I edit the input file by hand rather than trust a line-breaking algorithm. The asterisks help me find the lines to split.
If you have some experience scripting the Address Book, you may be wondering why I don’t just use
set out to out & name
instead of all the conditionals in Lines 8-15. The reason is my Address Book entries include titles (Mr., Ms., Dr.) that I don’t want on the label.
Similarly, I don’t use
set out to out & formatted address
but in this case, it’s not just me being persnickety. The formatted address
includes weird characters between the city and the state and between the state and the zip. These characters are invisible in a text editor, but show up as an accented a in the printed copy2 produced by pplabels
. I’m sure there’s a way to handle that character, but it’s easier to avoid it entirely. I just don’t use Perl enough anymore to make it worth the effort to learn its version of the Unicode sandwich.
Remember when I said the addresses in the pplabels
input file weren’t in any rational order? Line 3 of this AppleScript is the reason. It returns the list of selected contacts in an order that probably makes sense to a hashing algorithm but doesn’t make sense to humans. And because AppleScript has no builtin sorting command (I know, unbelievable, right?), and I no intention of writing a sort routine in AppleScript, the addresses this script puts on the clipboard are unsorted.
At present, the pplabels
script isn’t in my GitHub repository of similar scripts, but I’ll probably put it there soon.
-
These scripts were first written in the late ’90s when I was using Linux and had to write my own scripts, as there were no templates or label-making utilities available. ↩
-
If I remembered which accent it is, I’d tell you, but I don’t. And I don’t feel like screwing around with the script to find out. ↩