PostScript::MailLabels - Modules for creating PostScript(tm) files of mailing address labels.


Modules for creating PostScript(tm) files of mailing address labels, to be printed on standard adhesive-backed mailing label stock. Flexible enough to tackle other printing tasks, basically anything requiring a set fields be printed on a regular grid. Also creates PostScript(tm) code for calibrating and testing mailing label printing.


The module has three distinct output modes. In my experience, printing mailing labels is a matter of tweaking parameters to get them all to fit properly on the page. This module is designed with this in mind.

The first output is the calibration sheet. This is a pair of annotated axes, either in inches or centimeters, centered on the page and covering the whole page in X and Y directions. The intent is for you to output this page first, and simply read off the relevant page dimensions directly.

The second output is the label test. This output is a series of boxes drawn on the page, meant to outline the edges of all the mailing labels. Take this sheet and line it up with a sheet of labels to see if they actually match perfectly. If not, tweak the parameters until they do. Note that sometimes you will get a message at the bottom of the sheet saying "Bottom gap too large, last row cannot be printed". This means that the printable area of your printer is too small to utilize the last row of labels. I have this problem. But I handle it for you. Note also the arrows on the test sheet. As you hold the test sheet over a sheet of labels, hold it up to the light and slide the test sheet so that the boxes match the edges of the labels. If you slide in the arrow direction, that is a positive adjustment. The other direction is negative. If the edges of some boxes come out dashed, that means that the non-printing border cuts off the end of the label, so I will adjust the printing area appropriately. Don't try to line up the dashed lines with label edges - it won't work. Just line up the solid lines.

The third output is the labels themselves. By default, I have set up a US-centric address definition :

    firstname, lastname, street address, city, state, zipcode

But with version 2.0, you can now create your own definition. You can define new fields, and you can define how those fields land on a label. You can also control the fonts on a per-field basis. Not the size, yet - later pilgrim.

Parameters you can set :

Paper size, borders on the printable area (many printers will not print right up to the edge of the paper), where the labels live on the page and how big they are, overall x-y shift of page, whether or not to print PostNET barcode, font, fontsize, units (english or metric), which Avery(tm) product code to use, and where the first label starts.

This last needs explanation. If you have a partially used sheet of labels, you might want to use it up. So you count the missing labels, starting at the upper left, and counting across, and then down. For example, if I have 3 columns of labels, label five is the second label in the second row.

The Avery(tm) label codes are woefully incomplete. I can't find the Avery(tm) specs anywhere, so the ones that are in there are for products I personally own. I e-mailed Avery(tm) and they gave me a long distance phone number to call.

If you have an Avery(tm) product that I haven't defined, send me the specs and I'll add it.

Also, if there is another brand of labels that you use, send me the relevant data and I'll add that as well. I suspect that there must be some other vendor in Europe, but I don't know who that would be.

When setting up the addresses, I check to see if they will fit on the label. If not, I try to shorten them semi-intelligently until they fit. This part could use quite a bit more work, if done right it probably merits a module all it's own.

Briefly, for the name line, I start trimming the ends off the first name, and leave the last name alone.

For the street, I look for things like Road or Avenue and nuke those first, then I trim the street name from the right.

For the last line, I trim the city and leave the state and zip alone.

The barcode will be either a 5-digit zip or 9 digit zip-plus code. I could also create the delivery point code, but since my mailing labels are not even wide enough for the 9 digit zip-plus, I haven't bothered. I have read the USPS spec on the barcode, and so far as I can tell, I meet the spec.

labelsetup :

All the distances are in units of inches or centimeters, depending on the Units flag. A hash of the label definition will be returned.

    my $setup = $labels -> labelsetup( 

        #    paper size

        PaperSize        => 'Letter',
        options are : 
                    Letter Legal Ledger Tabloid A0 A1 A2 A3 A4 A5 A6 A7 A8
                    A9 B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 Envelope10 EnvelopeC5 
                    EnvelopeDL Folio Executive 
        #   printable area on physical page - these numbers represent border widths
        #    typical values might be 0.25 inches

        Printable_Left   => 0.0,
        Printable_Right  => 0.0,
        Printable_Top    => 0.0,
        Printable_Bot    => 0.0,

        #    define where the labels live (ideally)

        Output_Top     => 1.0, where does the top of the first label sit?
        Output_Left    => 0.5, where is the left edge of the first label?
        Output_Width   => 3.0, how wide are the labels?
        Output_Height  => 2.0, how tall are the labels?
        X_Gap          => 0.1, what is the vertical gap between labels?
        Y_Gap          => 0.0, what is the horizontal gap between labels?
        Number         => 30,  how many labels per page?
        Columns        => 3,   how many columns per page? (optional)

        if the number of columns are given, I'll check your math to see if the page
        width approximately equals the sum of the label widths plus gaps etc.

        #    Adjustments for printer idiosyncracies

        X_Adjust       => 0.0, shift the whole thing over by this amount.
        Y_Adjust       => 0.0  shift the whole thing down by this amount.

        #    Other controls

        Postnet    => 'yes',  barcodes? yes or no
        Font       => 'Helvetica', which default font? see below for generating a list
        FontSize   => 12, how big are they?
        Units      => 'english', english or metric
        FirstLabel => 1, where is the first label (begins at 1).

        # set equal to the Avery(tm) product code, and the label description
        # will be updated from the database.
        Avery        => undef,

        generate a PostScript(tm) file with rulers on it for making measurements
    $output = $labels->labelcalibration ;
        generate a PostScript(tm) file with boxes showing where the text will land
    $output = $labels->labeltest ;
        the main event - make me a label file
    $output = $labels->makelabels(\@addrs) ;

        translate an Avery(tm) product code into a template code.
    $templatecode = $labels->averycode($product_code) ;
        retrieve an array of the paper width, height (in points)
    @width_height = @{ $labels->papersize } ;
        get the length of a string in points using the current font
    $stringwidth = $labels->stringwidth("This is a string") ;
        get a list of the available fonts
    @fontname = $labels->ListFonts;


    Each component has a name, and four attributes. The attributes are :
        type : must be name, road, place, or bar. This defines the trimming
        adj : yes or no. Is trimming allowed?
        font : what font to use
        index : which entry in the input array will contain this component?

    Default components :
        #    first name
        fname    => { type => 'name', adj => 'yes', font => 'Helvetica', 'index' => 0 },
        #    last name
        lname    => { type => 'name', adj => 'yes', font => 'Helvetica', 'index' => 1 },
        #    street address and street
        street    => { type => 'road', adj => 'yes', font => 'Helvetica', 'index' => 2 },
        #    city
        city    => { type => 'place', adj => 'yes', font => 'Helvetica', 'index' => 3 },
        #    state
        state    => { type => 'place', adj => 'no', font => 'Helvetica', 'index' => 4 },
        #    country
        country    => { type => 'place', adj => 'no', font => 'Helvetica', 'index' => 6 },
        #    zip
        zip    => { type => 'place', adj => 'no', font => 'Helvetica', 'index' => 5 },
        #    postnet (bar code)
        postnet    => { type => 'bar', adj => 'no', font => 'PostNetJHC', 'index' => 5 },

    Editing components : with editcomponent

    #    What address components are available?
    print "components : ",join(' : ',@{$labels->editcomponent()}),"\n";

    #    Lets make the lname (lastname) bold-faced
    $labels->editcomponent('lname', 'name', 'no', 1, 'Times-Bold' );

    #    Lets create a new component
    $labels->editcomponent('company_name', 'name', 'yes', 7, 'Times-Bold');

    Label definition

    We define the label layout line by line, by describing for each line which
    components we want printed, and in what order.

    #    Default label definition
        #    line 1
        [ 'fname', 'lname' ],
        #    line 2
        [ 'street', ],
        #    line 3
        [ 'city', 'state', 'zip' ],
        #    line 4
        [ 'postnet', ],

    edit the label definition with definelabel :

    definelabel(line number, component, component, ...)

    #    first clear the old (default) definition
    #                   line number, component list

        Free Text (Page Numbering)

        You can place an (almost) constant bit of text on every page.
        This is really to allow page numbering, slightly generalized.

        $labels->freetext (
                X => 500,
                Y => 15,
                Text => "Page %page%"

        X and Y are the page coordinates, in *points* (72 points per inch), and
        remember that Y starts at the bottom and goes up, so (500, 15) puts the
        text at the bottom right corner of an 8.5x11 sheet.

        The special token %page% will be replaced with the current page number.


    #!/usr/bin/perl -w

    #        This shows the capabilities of the program...

    use PostScript::MailLabels 2.0;

    $labels = PostScript::MailLabels->new;

    #    Dumping information from the modules 

    #    What address components are available?
    print "\n****** components ******\n";
    print join(' : ',@{$labels->editcomponent()}),"\n";

    #    What is the current label layout?
    print "\n****** layout ******\n";
    my @layout = @{$labels->definelabel()};
    foreach (@layout) {
        print join(' : ',@{$_}),"\n";

    #    Here is how to list the available fonts
    print "\n****** fonts ******\n";
    @fonts = $labels->ListFonts;
    foreach (@fonts) {
        print "$_\n";

    #    Here is how to list the available papers
    print "\n****** papers ******\n";
    print join(' : ',@{$labels->papers}),"\n";

    #    Here is how to list all th Avery data
     # layout=>[paper-size,[list of product codes], description,
     #          number per sheet, left-offset, top-offset, width, height]
     #            distances measured in points

    my %avery = %{$labels->averydata};
    print "\n****** Avery(tm) data ******\n";
    foreach (keys %avery) {
        print "$_ : $avery{$_}->[0] : ",
               join(', ',@{$avery{$_}->[1]})," : ",
               join(' : ',@{$avery{$_}}[2-7]),"\n";

    #    Here are some more utilities

    print "\nString width of 'this is a test' = ", 
            $labels->stringwidth("this is a test",)/72," inches\n";

    my $setup = $labels -> labelsetup( Font => 'PostNetJHC');

    print "\nzip code tests, 6,9, and 12 digit lengths barcodes:  ", 
            $labels->stringwidth("123456",)/72," : ",
            $labels->stringwidth("123456789",)/72," : ",
            " inches\n";

    print "\nPaper size Letter = ",($labels->papersize)->[0]," x ",
                                 ($labels->papersize)->[1]," in points\n";

    print "\nAvery(t) code for 8460 is >",$labels->averycode(8460),"<\n";

    #    Simple setup using predefined Avery label
    $labels -> labelsetup(
                Avery       => $labels->averycode(8460),
                PaperSize   => 'letter',
                Font        => 'Times-Roman',

    print "\n                 num, left, top, width, height\n";
    print "label description : ", $setup->{number}, " : ",
                                  $setup->{output_left}, " : ",
                                  $setup->{output_top}, " : ",
                                  $setup->{output_width}, " : ",
                                  $setup->{output_height}, "\n";

    #    More hands-on setup defining everything. Note that Columns is optional
                        Units           => 'English',
                        PaperSize       => 'A4',

                        Printable_Left  => 0.25,
                        Printable_Right => 0.25,
                        Printable_Top   => 0.0,
                        Printable_Bot   => 0.55,
                        Output_Top      => 0.5, 
                        Output_Left     => 0.0,
                        Output_Width    => 2.625, 
                        Output_Height   => 1.0, 
                        X_Gap           => 0.16,
                        Y_Gap           => 0.0,
                        Number          => 30,
                        Columns         => 3,

                        #    Adjustments for printer idiosyncracies

                        X_Adjust   => 0.05,
                        Y_Adjust   => 0.05,

                        PostNet    => 'yes',
                        Font       => 'Helvetica',
                        FontSize   => 12,
                        FirstLabel => 1,

    #    We can fiddle the components...

    #    Lets make the lname (lastname) bold-faced
    print "\n******* make the lname field boldfaced *******\n";
    print "lname : ",join(' : ',@{$labels->editcomponent('lname')}),"\n";
    $labels->editcomponent('lname', 'name', 'no', 1, 'Times-Bold' );
    print "lname : ",join(' : ',@{$labels->editcomponent('lname')}),"\n";

    #    Lets switch the default ordering on the label from first-last to last-first
    print "\n******* swap order from first-last to last-first *******\n";
    print "Line 1 : ",join(' : ',@{$labels->definelabel(0)}),"\n";
    print "Line 1 : ",join(' : ',@{$labels->definelabel(0)}),"\n";

    #        print calibration sheet, in metric

    $labels->labelsetup( Units =>'metric');
    my $output = $labels->labelcalibration;
    open (FILE,">") || warn "Can't open, $!\n";
    print FILE $output;
    close FILE;
    print "\n******* metric Letter sized calibration sheet in *******\n";

    #        adjust printable area and draw test boxes

    $output = $labels->labeltest;
    open (FILE,">") || warn "Can't open, $!\n";
    print FILE $output;
    close FILE;
    print "\n******* Letter sized test boxes sheet in *******\n";

    #    Build a test address array
    # address array elements are : first,last,street_addr,city,state,zip
    my @addrs;
    my @address;
    my $indx = 0;
    foreach (<DATA>) {
        if ($indx%4 == 0) {
            @address = (split(':',$_));
        elsif ($indx%4 == 1) {
            push @address,$_;
        elsif ($indx%4 == 2) {
            push @address,(split(':',$_));
        elsif ($indx%4 == 3) {
            push @addrs,[@address];

    foreach (@addrs) {
        print "Address : $_->[0] $_->[1] $_->[2] $_->[3] $_->[4] $_->[5]\n";

    #    Set up a few things...

    $setup = $labels -> labelsetup( Font         => 'Helvetica');
    $setup = $labels -> labelsetup( FirstLabel   => 25);
    $setup = $labels -> labelsetup( Output_Width => 2.625), 
    $setup = $labels -> labelsetup( Columns      => 3), 

    $output = $labels->makelabels(\@addrs);
    open (OUT,">") || die "Can't open, $!\n";
    print OUT $output;
    close OUT;
    print "\n******* label output in *******\n";


    John and Jane:Doe
    1234 Robins Nest Sitting In a Tree Ave 

    1300 Pennsylvania Ave.

    98765 Birch Point Drive 

    Fred & June:Cleaver
    11221 Beaver Rd 

    Ernest and Julio:Gallo
    1987 Chardonnay 
    San Jose:CA:80880

    Orville and Wilbur:Wright
    7715 Kitty Hawk Dr 
    Kitty Hawk:NC:87220

    1856 Tomb Park Rd 


No bugs, that I am aware of.

To do list :


Need to be able to get the length of a string in ISOLatin1


Add fontsize to each component


Account for label height - currently will run off bottom


Separate module for address compression/abbreviation


Add bitmaps or images?


        Version 2.10 Sun Aug 29 14:00:53 CDT 2004
        Added parameters for 5167 Avery (tm) stock - thanks to Daniel J McDonald
        for supplying the parameters.
        Also added many new Avery (tm) parameters. Thanks to Summer Misherghi
        who pointed me to
        Added new parameter set 'freetype', to allow the user to place a text 
        string anywhere they want. Particular use is for numbering the pages.
        Code now escapes special PostScript characters (){}[]<>/% in the
        input so that they will print properly and not crash the PS interpreter.

        Version 2.03 - Fri Sep 28 07:22:28 CDT 2001
        User-defined number of columns was over-ridden with a calculated
        number. Now it works, thanks to Allan Engelhardt.

        Version 2.02 - Fri Jan 12 22:07:12 CST 2001
        Added y_gap tp Avery(tm) labels
        Updated calibration plot in BasicData to arbitrary paper size
        Minor repairs to test routine

        Version 2.01 - January 2001
        Added y_gap tp Avery(tm) labels
        Updated calibration plot in BasicData to arbitrary paper size
        Minor repairs to test routine
        Version 2.0 - December 2000
        Major revision. Added all of the component and label definition stuff. 
        Thanks to "Andrew Smith" <asmith at> for suggesting
        additional fields and inspiring the generalization.
        Thanks to Nuno Faria for assisting with the "Europeanization" of
        the code - it now works for Portuguese, and hopefully for other
        alphabets as well.
        Added pagesize so that various paper sizes are actually handled correctly.

        Version 1.0.1 - December 2000
        Bug reported by John Summerfield <summer at>
        Lowercase all SETUP parameters to avoid problems with mis-spellings.
        Do real parameter checks to check simple spelling errors.

        Bug reported by Nuno Faria <nfaria at>
        Boxes plot did not work. Frankly I can't figure out how it ever did. Anyway
        it breaks on more modern versions of ghostscript, so I fixed it. Basically
        rewrote part of the PostScript(tm) code.


    Alan Jackson
    October 1999

    The PostNET font was gotten (under Gnu copyleft) from
    James H. Cloos, Jr. <>

    The font metrics and paper sizes were pulled from the
    PostScript::Metrics module written by Shawn Wallace