More Avery labels

This week I had to create lots of small labels to attach to laboratory samples. To make this easier, I modified my file folder label script to handle the smaller Avery 5167 labels, the kind usually thought of as return address labels.

The new program, called ptlabels (“print tiny labels”), follows the same logic as the old one and uses the same command-line options. You can tell it which row and column to start on through the -r and -c options. The input format is also the same:

As an example, this input

#Lorem project|1234
Sample 1

Sample 2

Sample 3

Sample 4

Sample 5

Sample 6

Sample 7

Sample 8

#Dolor project|9876
Sample 1

Sample 2

Sample 3

Sample 4

passed to

ptlist -r 3 -c 2

generates this output

As with the file folder label script, I find it easiest to run the script in TextMate via Filter Through Command… (⌥⌘R).

The script is in Perl, because that was my main language back when I wrote the original version. Rewriting in from scratch in Python would have been a waste of time.

  1:  #!/usr/bin/perl
  2:  
  3:  use Getopt::Std;
  4:  
  5:  # Usage/help message.
  6:  $usage = <<USAGE;
  7:  Usage: ptlabels [options] [filename]
  8:  Print tiny labels on Avery 5167 sheets
  9:  
 10:    -r m : start at row m (range: 1..20; default: 1)
 11:    -c n : start at column n (range 1..4; 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:  
 17:  The first line of an entry is special. If it starts with a #, then it's
 18:  considered a header line. Everything in the header line up to the | is
 19:  printed flush left in bold and everything after the | is printed flush
 20:  right in bold. Subsequent lines are printed centered in normal weight.
 21:  If the first line of an entry doesn't start with #, it uses the header
 22:  of the previous entry.
 23:  USAGE
 24:  
 25:  # Set up geometry constants for Avery 5167.
 26:  $topmargin = 0.55;
 27:  $pocol[1]  = 0.45;
 28:  $pocol[2]  = 2.50;
 29:  $pocol[3]  = 4.55;
 30:  $pocol[4]  = 6.60;
 31:  $lheight   = 0.50;
 32:  
 33:  # get starting point from command line if present
 34:  getopts('hr:c:', \%opt);
 35:  die $usage if ($opt{h});
 36:  
 37:  $row = int($opt{r}) || 1;    # chop off any fractional parts and
 38:  $col = int($opt{c}) || 1;
 39:  
 40:  # Bail out if position options are out of bounds
 41:  die $usage unless (($row >= 1 and $row <= 20) and 
 42:                     ($col >= 1 and $col <= 4));
 43:  
 44:  # Set initial horizontal and vertical positions.
 45:  $po = $pocol[$col];
 46:  $sp = ($topmargin + ($row - 1)*$lheight);
 47:  
 48:  # Pipe output through groff to printer (manual feed).
 49:  open OUT, "| groff | lpr -o ManualFeed=True";
 50:  # Change to PDF before sending to printer.
 51:  # open OUT, "| groff | ps2pdf - - | lpr -o ManualFeed=True";
 52:  # Preview output instead of printing directly.
 53:  # open OUT, "| groff | ps2pdf - - | open -a /Applications/Preview.app";
 54:  # Print raw troff code for debugging.
 55:  # open OUT, "> labels.rf";
 56:  select OUT;
 57:  
 58:  # Set up document.
 59:  print <<SETUP;
 60:  .vs 12
 61:  .nf
 62:  .ll 1.50i
 63:  .ta 1.50iR
 64:  
 65:  SETUP
 66:  
 67:  # The troff code for formatting a single entry, with placeholders for
 68:  # positioning on the page. The magic numbers embedded in the formatting
 69:  # commands make the layout look nice.
 70:  $label = <<ENTRY;
 71:  .sp |%.2fi
 72:  .po %.2fi
 73:  .ps 10
 74:  .ft HB
 75:  %s
 76:  .ps 10
 77:  .ft H
 78:  .ce 2
 79:  %s
 80:  .ce 0
 81:  ENTRY
 82:  
 83:  # Slurp all the input into an array of entries.
 84:  $/ = "";
 85:  @entries = <>;
 86:  
 87:  $bp = 0;                  # we don't want to start with a page break
 88:  
 89:  foreach $body (@entries) {
 90:    # Parse and transform the header and body.
 91:    if ($body =~ /^#/) {    # it's a header line
 92:      ($header, $body) = split(/\n/, $body, 2);
 93:      $header = substr($header, 1);
 94:      $header =~ s/\|/\t/;
 95:    }  
 96:    $body =~ s/\s+$//;
 97:  
 98:    # Break page if we ran off the end.
 99:    if ($bp) {
100:      print "\n.bp\n";      # issue the page break command
101:      $bp = 0;              # reset flag
102:    }
103:    
104:    # Print the label.
105:    printf $label, $sp, $po, $header, $body;
106:    
107:    # Now we set up for the next entry.
108:    $col = ($col % 4) + 1;      # step to next column
109:    $po = $pocol[$col];
110:    if ($col == 1) {            # we just went down a row
111:      $row++;
112:      if ($row > 20) {          # we just went off the bottom
113:        $bp = 1;                  # start a new page
114:        $row = 1;                 # at the top
115:      }
116:      $sp = ($topmargin + ($row - 1)*$lheight);
117:    }
118:  }

The geometry constants in Lines 26-31 were initially set by making measurements of the labels and then adjusted through trial and error until the printing was nicely aligned with the die cuts on the label sheets. The final values are based not only on the sheet geometry, but also on how the labels pass through my printer via the manual feed slot. For good alignment on another printer, the values might need adjusting by a few hundredths.

Line 49 was also written with my default printer in mind. Because it’s a PostScript printer, I can take the PostScript output directly from groff and pipe it to lpr—no need to convert it first to PDF and no need to use lpr’s -P option to tell it which printer to use. (The -o option should be self-explanatory.)

Line 51 (commented out) is an example of what you may need to do if you don’t have a PostScript printer.

51:  # open OUT, "| groff | ps2pdf - - | lpr -o ManualFeed=True";

Ps2pdf is part of Ghostscript, an open source suite of PostScript utilities that doesn’t come with OS X, but which I find invaluable. The two hyphens after ps2pdf tell it to use standard input and output instead of files on disk.

A more Mac-like possibility is shown in Line 53:

53:  # open OUT, "| groff | ps2pdf - - | open -a /Applications/Preview.app";

This generates the PDF and opens it in Preview so you can see it before printing. I’m a wild and impetuous sort of guy, so I just send it off the printer and let the ink fall where it may.