Mit Perls PostScript-Modulen und einer Adressdatenbank lassen sich massenweise Umschläge für Serienbriefe beschriften.
Mittlerweile geht ja einfach alles mit Linux: Digitale Bilder von der Kamera
einlesen, digitale Musik hören, CDs brennen,
sogar mein USB-Scanner brummt zufrieden mit xsane.
Doch halt, für eine Aufgabe musste ich bislang noch Windows
booten: Um einmal im Monat etwa 20 Umschläge für einen Serienbrief
mit Absender und Empfängeradressen aus einer Datenbank zu
beschriften, nutzte ich bis vor kurzem noch ein Uralt-Programm aus der
Windows-Welt.
Schluss damit! Mit Ghostscript wird aus dem heimischen 08/15-Drucker schnell ein zwölfzylindriger PostScript-Bolide. [2] zeigt wie's geht, wenn's nicht sowieso offensichtlich ist. Die bislang unter Windows genutzte Adressdatenbank ließ sich, wie Abbildung 1 zeigt, problemlos im Komma-separierten CSV-Format exportieren.
|
| Abbildung 1: Die Felder der komma-separierten Adressdatei |
Was blieb, war, für jeden Briefumschlag eine PostScript-Datei zu generieren
und diese dann an den Drucker zu schicken. Kinderleicht mit mit
PostScript::File und PostScript::TextBlock vom CPAN, wie ich
aus [3] erfuhr.
PostScript sieht ja fast aus wie eine Programmiersprache. Die PostScript-Dateien bestehen aus lesbarem ASCII-Text, der die zur Seitengenierung notwendigen Kommandos aneinanderreiht.
PostScript arbeitet mit einem etwas unkonventionellen Koordinatensystem: Der Ursprung ist die linke untere (!) Ecke des Papiers. Die x-Achse führt von dort aus nach rechts, die y-Achse zeigt nach oben, wie man das aus der Schule gewöhnt ist. Als Einheit dient der PostScript-Point, ein Zweiundsiebzigstel Inch, das wiederum etwa 2.54 Zentimeter misst. 100 PostScript-Punkte sind also ungefähr 3.5 Zentimeter.
Um zum Beispiel den
Text Max Schuster ins Adressfeld des Briefumschlags zu platzieren,
sind folgende Kommandos notwendig:
0 setgray 401.95 156 moveto
/Helvetica-iso findfont
18 scalefont setfont
(Max Schuster) show
Hierbei bewegen wir uns zunächst fast 402 Points (etwa 14,2 cm) von der linken unteren Ecke des Umschlags nach rechts und dann 156 Points (etwa 5,5 cm) nach oben, um dann dort die angegebene Buchstabenkette im angegeben Font (Helvetica-iso) in der gesetzten Größe 18 von links nach rechts aufs Papier zu setzen.
Etwas vereinfacht wird dieses Kauderwelsch durch die vom
CPAN erhältlichen Module PostScript::File und PostScript::TextBlock.
PostScript::File übernimmt die Aufgabe, den PostScript-Header, beginnend
mit
%!PS-Adobe-3.0
zu schreiben und sich um die Seitenorientierung, die Randmaße und
die Seitenfolge zu kümmern.
PostScript::TextBlock nimmt mehrzeilige Strings entgegen und fängt an
einer angegebenen Koordinate an zu schreiben.
Allerdings verlangen diese Module, dass man stundenlang herumtüftelt, damit das Layout einigermaßen an der richtigen Stelle landet. Für die Briefumschläge soll folgendes Layout gelten:
|
| Abbildung 2: Egal ob der Absender einen kurzen oder ... |
|
| Abbildung 3: einen langen Namen hat: Der seitliche Abstand bleibt konstant. |
Das Skript in Listing envelope definiert einen konstanten Absender
in $SENDER (Zeile 14), durchläuft eine Adressdatei und gibt zu
jeder gefundenen Adresse einen Umschlag nach Abbildung 2 oder 3 auf
dem Drucker aus.
Zeile 13 definiert in $ADDR_CSV den Namen der Adressdatei, die ein
Format nach Abbildung 1 aufweisen sollte. Das Kommando, um eine
PostScript-Datei durch den Drucker zu jagen, definiert $PRINT_CMD
in Zeile 17. Wer das Skript nur im Trockenlauf ausprobieren möchte,
ohne gleich Tonnen von Papier zu produzieren, ersetzt "lpr"
einfach
durch "ghostview", dann erscheinen die Kuverts auf dem Bildschirm.
Zeile 19 öffnet die Adressdatei und der while-Block ab Zeile 22
iteriert durch die Einträge, die jeweils mittels regulärer Ausdrücke
extrahiert werden. Statt dessen wäre auch der Einsatz des CPAN-Moduls
Text::CSV_XS denkbar gewesen, aber nachdem die Adresseinträge
denkbar simpel und ohne Komplikationen wie wörtliche Anführungszeichen
oder eingebettete Kommas auskommen, wollen wir's mal nicht
übertreiben.
Zeile 23 interpretiert alle mit # (und wahlweise führendem
Whitespace) anfangenden Zeilen als Kommentare.
Praktisch, falls man nur selektierte Einträge
drucken will, dann kommentiert man alle anderen einfach schnell
mit # aus. Der split-Befehl in Zeile 24 bricht die Kolonnen
an trennenden Kommas auf, map entfernt anschließend alle doppelten
Anführungszeichen. s/"//g; gibt nicht den Ergebnisstring zurück,
deswegen wird einfach $_; nachgeschaltet.
Ab Zeile 27 entsteht das PostScript::File-Objekt, das mit
landscape ins Querformat rotiert, mit
reencode => 'ISOLatin1Encoding' auch Umlaute unterstützt
und das amerikanische Geschäftsbriefformat ``Envelope-DL'' wählt.
Zeile 36 legt die Adressfeldkolumnen in den Variablen
$last, $first, $city und $str ab.
Zeile 39 ruft die weiter unten definierte Funktion textbox() auf,
die einen mehrzeiligen String, einen Fontnamen, eine Fontgröße und
einen Zeilenabstand in PostScript-Points
entgegennimmt. Als Fontnamen wählen wir Helvetica-iso, da Helvetica
standardmäßig vorhanden ist und mit dem -iso-Zusatz auch Umlaute
führt.
textbox() liefert drei Rückgabewerte: Ein
PostScript::TextBlock-Objekt sowie die Breite und Höhe des
erzeugten Textblocks in PostScript-Points.
Anschließend ruft Zeile 41 die Write()-Methode des
PostScript::TextBlock-Objekts auf, um den PostScript-Code zu
generieren. Write() nimmt vier Parameter: die Breite und Höhe
des Textblocks, sowie den X- und Y-Offset der Startkoordinate. Breite
und Höhe übernehmen wir einfach von vorher von der textbox()-Funktion.
Als X-Offset (Abstand vom linken Rand) kommen zwei Zentimeter dran,
die einfach als cm(2) notieren, denn die weiter unten definierte Funktion
cm() rechnet einfach Zentimeter in PostScript-Points um.
Der Y-Offset ist schon komplizierter, denn Write() erwartet ihn als
Abstand vom unteren Rand, während wir 2cm vom oberen Rand wollen.
Aber kein Problem: Methode $ps->get_width() des vorher definierten
PostScript::File-Objekts liefert die Höhe des Kuverts und davon zieht
Zeile 42 einfach cm(2) ab.
Zu beachten ist, dass PostScript::File trotz landscape-Modus und
damit um 90 Grad gedrehter Seite die
ursprüngliche Idee von Breite und Höhe beibehält -- für unsere Zwecke
liefert jetzt get_width() die Höhe und get_height() die Breite.
Write liefert eine Liste zurück, deren erstes Element der PostScript-Code
des Textblocks ist. Zeile 43 jubelt ihn der aktuellen PostScript-Seite unter.
Ähnlich verfahren wir mit dem Empfänger: Zeile 46 fügt Vor- und Nachnamen,
Straße und Wohnort zu einem mehrzeiligen String zusammen. Die
textbox()-Funktion nimmt diesmal einen leicht größeren Font und
Zeilenabstand.
Der X-Offset der linken oberen Ecke der Textbox vom PostScript-Ursprung ist
diesmal die Länge des Kuverts ($ps->get_height()) minus der
Breite des Textkastens ($bw) minus 2 Zentimeter (cm(2)).
Der Y-Offset, also der Abstand der oberen Ecke der Textbox vom unteren
Briefrand ist die Höhe des Textkastens plus 2cm ($bh + cm(2)).
Die Funktion tempfile() aus dem Modul File::Temp legt
in Zeile 34 eine
temporäre Datei mit einer .ps-Endung
an und gibt ein beschreibbares File-Handle sowie den
Dateinamen zurück. Die in Zeile 56 aufgerufene output()-Methode
soll die PostScript-Daten in dieser Datei ab, erwartet aber deren
Namen ohne .ps-Endung, deswegen putzt Zeile 55 diese schnell
weg und legt das Ergebnis in $base ab.
Nach dem Aufruf des Druckerkommandos in Zeile 59
muss Zeile 63 nur noch die temporäre Datei löschen.
textbox() ab Zeile 68 erzeugt ein neues
PostScript::TextBlock-Objekt und ruft dessen addText-Methode auf.
Übergeben werden der Fontname, dessen Größe, der Zeilenabstand ($leading)
und der zu platzierende Text.
Um die Größe des erzeugten Textkastens zu bestimmen, ruft es die weiter
unten definierten Funktionen tb_width() und tb_height() (tb für
Text Block) auf.
Während tb_height lediglich den Zeilenabstand mit der Anzahl der im
Text übergebenen Zeilen multiplizieren muss, gestaltet sich der
horizontale Platzverbrauch eines Strings in einem Proportionalfont etwas
schwieriger, denn die Buchstaben sind auf dem Papier unterschiedlich breit.
Zum Glück gibt es das Modul PostScript::Metrics mit seiner Funktion
stringwidth(), die das mittels intern gespeicherter Fonttabellen
erledigt. Allerdings kennt sie Helvetica-iso nicht, weswegen Zeile
96 den -iso-Zusatz einfach entfernt. Dann allerdings kennt es keine
Umlaute mehr, weshalb Zeile 101 diese einfach durch schlichte Platzhalter
in Form des Buchstabens A ersetzt -- das stimmt zwar nicht genau,
erfüllt aber den Zweck. Die längste Zeile bestimmt die Breite des Textblocks.
001 #!/usr/bin/perl
002 ###########################################
003 # envelope - Print paper envelopes
004 # Mike Schilli, 2003 (m@perlmeister.com)
005 ###########################################
006 use warnings;
007 use strict;
008
009 use PostScript::File;
010 use PostScript::TextBlock;
011 use File::Temp qw(tempfile);
012
013 my $ADDR_CSV = "mailaddr.csv";
014 my $SENDER = q{Ansel Absender
015 Amselweg 9
016 D-78333 Ansbach};
017 my $PRINT_CMD = "ghostview";
018
019 open FILE, $ADDR_CSV or
020 die "Cannot open $ADDR_CSV";
021
022 while(<FILE>) {
023 next if /^\s*#/;
024 my @addr = split /,/, $_;
025 @addr = map { s/"//g; $_; } @addr;
026
027 my $ps = PostScript::File->new(
028 landscape => 1,
029 reencode => 'ISOLatin1Encoding',
030 paper => "Envelope-DL",
031 );
032
033 my ($tmp_fh, $tmp_file) =
034 tempfile(SUFFIX => ".ps");
035
036 my($last, $first, $city, $str) = @addr;
037
038 # Sender
039 my($bw, $bh, $b) = textbox($SENDER,
040 "Helvetica-iso", 10, 12);
041 my ($code) = $b->Write($bw, $bh, cm(2),
042 $ps->get_width() - cm(2));
043 $ps->add_to_page($code);
044
045 # Recipient
046 my $to = "$first $last\n$str\n\n$city\n";
047 ($bw, $bh, $b) = textbox($to,
048 "Helvetica-iso", 18, 20);
049 ($code) = $b->Write($bw, $bh,
050 $ps->get_height() - $bw - cm(2),
051 $bh + cm(2));
052 $ps->add_to_page($code);
053
054 # Print to temporary file
055 (my $base = $tmp_file) =~ s/\.ps$//;
056 $ps->output($base);
057
058 print "Showing $tmp_file\n";
059
060 # Send to printer
061 system("$PRINT_CMD $tmp_file") and
062 die "$PRINT_CMD $tmp_file: $!";
063
064 # Delete
065 unlink "$tmp_file" or
066 die "Cannot unlink $tmp_file: $!";
067 }
068
069 ###########################################
070 sub textbox {
071 ###########################################
072 my($text, $font, $size, $leading) = @_;
073
074 my $b = PostScript::TextBlock->new();
075
076 $b->addText(
077 font => $font,
078 text => $text,
079 size => $size,
080 leading => $leading);
081
082 return(tb_width($text, $font, $size),
083 tb_height($text, $leading),
084 $b);
085 }
086
087 ###########################################
088 sub cm {
089 ###########################################
090 return int($_[0]*72/2.54);
091 }
092
093 ###########################################
094 sub tb_width {
095 ###########################################
096 my($text, $font, $size) = @_;
097
098 $font =~ s/-iso//;
099
100 my $max_width = 0;
101
102 for(split /\n/, $text) {
103 s/[äÄöÖüÜß]/A/ig;
104 my $w =
105 PostScript::Metrics::stringwidth(
106 $_, $font, $size);
107 $max_width = $w if $w > $max_width;
108 }
109
110 return $max_width;
111 }
112
113 ###########################################
114 sub tb_height {
115 ###########################################
116 my($text, $leading) = @_;
117
118 my $lines = 1;
119 $lines++ for $text =~ /\n/g;
120
121 return $lines*$leading;
122 }
Wer andere Umschläge als den angegebenen verwendet, kann das Skript leicht anpassen. Ein DIN-A-6-Umschlag misst beispielsweise etwa 10.47 mal 14.81 Zentimeter, also ist einfach
my $ps = new PostScript::File(
landscape => 1,
reencode => 'ISOLatin1Encoding',
width => cm(10.47),
height => cm(14.81),
);
einzusetzen.
Zugegeben, diesmal musste ich mit Haken und Ösen programmieren, um die
teilweise eigenwillige Implementierung der PostScript::*-Module zu
überlisten. Life ain't easy -- aber den Preis zahle ich gerne für die
Freiheit, die Linux bietet.
![]() |
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |