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. |