Aus Linux-Magazin 05/2011

Automatisierter Labeldruck in Open Office dank Perl-Skript

© Carlos Caetano, 123RF.com

Open Office hilft mit einer Vielzahl vorkonfigurierter Formate beim Ausdrucken von selbstklebenden Etiketten. Perl speist die dazu erforderlichen Adressdaten ins Dokument ein.

Wer noch altmodisch Glückwunschkarten mit der Hand schreibt und verschickt, hat vielleicht schon mal damit geliebäugelt, sich das Adressieren zu vereinfachen und auf selbstklebende Etiketten umzustellen. Das heute vorgestellte Perl-Skript liest kommaseparierte Texte ein und druckt sie zeilenweise auf solche Labels. Die praktischen Laser-tauglichen Abziehetiketten auf A4-Papier (zum Beispiel Abbildung 1) kosten etwa einen halben Cent pro Stück und helfen nicht nur beim antiquierten Snail-Mail-Versand, sondern eignen sich auch zum Beschriften von Geräten oder Kabeln. Wie wär’s zum Beispiel damit, mal den Kabelverhau unterm Schreibtisch zu beschriften, damit der gestresste Home-Admin das Routernetzteil beim nächsten Mal sofort findet?

Abbildung 1: Laser-taugliche Etiketten, 30 pro Blatt, 4200 pro Karton. Der einzelne Aufkleber ist billig und dabei praktisch.

Abbildung 1: Laser-taugliche Etiketten, 30 pro Blatt, 4200 pro Karton. Der einzelne Aufkleber ist billig und dabei praktisch.

Vorkonfigurierte Formate

Open Office Writer kennt bereits von Haus aus die Etikettenformate vieler Hersteller und erzeugt über das Menü »Datei | Neu | Etiketten« (Abbildung 2) entsprechende tabellenartige Dokumente. Der User muss im Dialog in Abbildung 2 dazu nur den Hersteller und den Produktcode der verwendeten Etiketten eingeben, schon stimmen die Maße.

Abbildung 2: Im Open Office Writer öffnet der Labels-Eintrag aus dem »Datei | Neu«-Menü eine reiche Auswahl von Aufkleber-Formaten.

Abbildung 2: Im Open Office Writer öffnet der Labels-Eintrag aus dem »Datei | Neu«-Menü eine reiche Auswahl von Aufkleber-Formaten.

Die so frisch angelegten Dokumente befüllt der User nur noch mit Textdaten und klickt auf »Drucken« – viel einfacher, als selbst ein Programm zur Druckerpositionierung zu schreiben. Und da Open Office seine Dokumente im offenen ODF-Format ablegt, ist es ein Leichtes, die Tabellendaten mittels eines selbst gestrickten Perl-Skripts aus einer CSV-Datei zu lesen und ins Dokument einzustreuen.

Zip-Archiv in ODT

Vor dem automatisierten Einspeichern der Adressen erstellt der User manuell mit Open Office einmalig ein Testdokument als Vorlage und tippt einige Teststrings in die ersten vier Felder ein (Abbildung 3). Das dann als »template.odt« abgespeicherte Dokument besteht, wie das »unzip«-Kommando in Abbildung 4 zeigt, aus einem Zip-Archiv mit etlichen XML-Dateien, deren interessanteste »content.xml« ist, weil sie den mit XML-Markup versehenen Textinhalt des Dokuments enthält.

Abbildung 3: Der User tippt Beispieltexte in die Tabellenfelder des Open-Office-Dokuments ein.

Abbildung 3: Der User tippt Beispieltexte in die Tabellenfelder des Open-Office-Dokuments ein.

Abbildung 4: Ein Unzip-Aufruf fördert die XML-Dateien des Open-Office-Dokuments zutage, »content.xml« enthält die Texte.

Abbildung 4: Ein Unzip-Aufruf fördert die XML-Dateien des Open-Office-Dokuments zutage, »content.xml« enthält die Texte.

Was nun dort steht und in welchen Markup-Strukturen die vom User vorher in die Tabellenelemente eingegebenen Strings gelandet sind, zeigt der Aufruf des Skripts in Listing 1 mittels »oo-dumper template.odt«. Es nutzt das CPAN-Modul OpenOffice::OODoc und ruft dessen Konstruktor »ooDocument()« mit dem Namen der zu untersuchenden Datei auf. Als »member« legt Zeile 11 »content« fest, zeigt sich also am Dokumentinhalt interessiert und nicht an ausgelagerten Kopf- oder Fußzeilen, wiederverwertbaren Style-Definitionen oder Meta-Informationen.

Listing 1

»oo-dumper«

01 #!/usr/local/bin/perl -w
02 use strict;
03 use OpenOffice::OODoc;
04
05 (my $file) = @ARGV;
06
07 die "usage: $0 file" unless defined $file;
08
09 my $doc = ooDocument(
10     file   => $file,
11     member => "content",
12 );
13
14 (my $element) = $doc->selectElements(
15     '//office:body');
16
17 print $element->_dump();

XML erforschen

Die Methode »selectElements()« setzt einen Xpath-Query ab, der alle XML-Elemente unterhalb des Tag »office:body«, also des eigentlichen Textdokuments, zutage fördert. Dokumente enthalten nur einen einzigen Body, allerdings besteht OpenOffice::OODoc darauf, dass die linke Seite der Zuweisung in Zeile 14 einen List-Kontext suggeriert, deshalb die einschließenden Klammern um »$element«. Zurück kommt eine Referenz auf ein Objekt vom Typ OpenOffice::OODoc::Element, das aber aufgrund von Vererbung auch die Methoden des ausführenden XML-Parsers XML::Twig versteht.

Dieses schon einmal in einem früheren Snapshot vorgestellte [3], etwas eigenwillige XML-Modul stellt die Methode »_dump()« bereit, die eine textuelle Aufbereitung eines XML-Unterbaums generiert und als String zurückliefert.

So zeigt sich in Abbildung 5, dass im Dokument unter dem Tag »office:body« ein Tag namens »office:text« hängt, das nach einigen Sequence-Deklarationen wiederum einen Textabsatz vom Typ »text:p« (Paragraph) enthält. Dieser stellt sich als eine Tabellenzeile mit drei Spalten heraus, für deren Rahmen jeweils Elemente vom Typ »draw:frame« verantwortlich zeichnen, die wiederum ein »draw:text-box«-Element mit einem »text:p«-Element mitbringen, in denen sich endlich die eingegebenen Test-Texte (»test1«, …) wiederfinden.

Abbildung 5: Die »_dump()«-Methode zeigt die interne Verschachtelung des XML-Dokuments.

Abbildung 5: Die »_dump()«-Methode zeigt die interne Verschachtelung des XML-Dokuments.

Somit fördert ein Xpath-Query vom Format

//office:body/office:text/text:p

alle Tabellenzeilen zutage (die ihrerseits wieder Spaltenrahmen enthalten), während sich die Tabellenelemente (drei in jeder Zeile) relativ dazu unter »…/draw:frame/drawtext-box/text:p« finden. Das Skript in Listing 2 bedient sich des ersten Xpath-Query, um zunächst das Dokument um so viele Tabellenzeilen zu erweitern, wie nötig sind, damit alle auszudruckenden Adressdaten darin Platz finden. Mit dem zweiten Query stöbert es durch alle Labels und stopft jeweils die dafür bestimmten Textdaten hinein.

Listing 2

»label-writer«

01 #!/usr/local/bin/perl -w
02 use strict;
03 use OpenOffice::OODoc;
04 use Sysadm::Install qw( :all );
05 use Text::CSV_XS;
06 use POSIX qw(ceil);
07
08 my $template        = "template.odt";
09 my $file            = "ready.odt";
10 my $addr_book       = "address-book.csv";
11 my $labels_per_page = 30;
12
13 my @addresses =
14    addresses_scan( $addr_book);
15
16 my $addtl_pages =
17   ceil( scalar @addresses /
18        $labels_per_page ) - 1;
19
20   # Put template in place
21 cp $template, $file;
22
23 my $doc = ooDocument(
24  file           => $file,
25  type           => "content",
26  local_encoding => "",
27 );
28
29   # Extend document as necessary
30 my @rows = $doc->selectElements(
31     '//office:body/office:text/text:p'
32 );
33
34 for ( 1 .. $addtl_pages ) {
35   for my $row ( @rows ) {
36       $doc->replicateElement( $row, "body" );
37   }
38 }
39
40   # All labels, including new ones
41 my @labels = $doc->selectElements(
42   '//office:body/office:text/text:p/' .
43   'draw:frame/draw:text-box/text:p'
44 );
45
46 my $addr_idx = 0;
47
48 for my $label ( @labels ) {
49     $doc->setStyle( $label, "P1" );
50     $doc->setText( $label,
51                 $addresses[ $addr_idx ] );
52     $addr_idx++;
53     $addr_idx = 0 if
54       $addr_idx > $#addresses;
55 }
56
57 $doc->save();
58
59 ###########################################
60 sub addresses_scan {
61 ###########################################
62   my( $addr_book ) = @_;
63
64   my @addresses = ();
65
66   open( my $fh, "<:encoding(utf8)",
67     $addr_book ) or die "$addr_book: $!";
68
69   my $csv = Text::CSV_XS->new (
70    { binary => 1 } ) or die
71      "Cannot use CSV: " .
72      Text::CSV->error_diag ();
73
74   while( my $row = $csv->getline( $fh ) ) {
75     unshift @$row, "";
76
77     for ( @$row ) {
78       s/^/ /;
79     }
80
81     push @addresses,
82          join( "\n", @$row );
83     }
84   close $fh;
85
86   return @addresses;
87 }

Zum Öffnen der ODT-Datei bedient es sich wieder des Konstruktors »ooDocument()«, der auf die Datei »ready.odt« zugreift, die ihrerseits zuvor die Funktion »cp« aus dem Modul Sysadm::Install in Zeile 21 aus dem Template »template.odt« erzeugt hat.

Die gegenwärtige Version von OpenOffice::OODoc weist einen Bug auf, der sie UTF-8-kodierte Daten inkorrekt verarbeiten lässt, falls diese Umlaute enthalten. Die gewählte Einstellung »local_encoding =>»”””« behebt das Problem vorläufig, sollte aber eigentlich auf den Wert »utf8« gesetzt sein.

Adressbuch einpflegen

Die Rohdaten legt der User in der Datei »address-book.csv« (Abbildung 6) ab, von wo sie das Skript aus Listing 2 mit dem CPAN-Modul Text::CSV_XS zeilenweise mit »getline()« ausliest. Die ab Zeile 60 definierte Funktion »addresses_scan« öffnet dazu die Datei mit dem Pragma »:encoding(utf8)«, damit Perl dort stehende und in UTF-8 kodierte Umlaute auch korrekt einliest und gleichzeitig intern in den dafür vorgesehenen Datenstrukturen das UTF-8-Flag setzt.

Abbildung 6: Die Adressdaten-Datei im CSV-Format.

Abbildung 6: Die Adressdaten-Datei im CSV-Format.

Die Variable »$row« zeigt auf ein Array, dessen Elemente die in der CSV-Datei durch Kommata getrennten Zeileneinträge repräsentieren. Um auf der linken Seite des Labels, auf dem der Texteintrag später landet, etwas Platz zu lassen, fügt das Ersetzungskommando in der For-Schleife ab Zeile 77 vor jeder Labelzeile ein Leerzeichen ein. Zeile 82 setzt die Adresszeilen zu einem String zusammen und schiebt ihn ans Ende des Array »@addresses«, den die Funktion ans Hauptprogramm zurückreicht.

Nichts verschwenden

Um nicht unnötig Etiketten zu verschwenden, füllt das Skript eine A4-Seite immer vollständig aus, notfalls durch Wiederholen der ersten Adressen in der CSV-Datei. Andererseits muss Listing 2 bei einer Adressdatenbank, die mehr als 30 Einträge hat, zusätzliche Seiten am Ende des Dokuments einfügen. Auch in diesem Fall füllt es eine eventuell nicht ganz ausgenutzte Seite durch die Wiederholunge von Datensätzen am Ende so auf, dass keine Etiketten leer bleiben.

Zeile 16 ermittelt aus der Zahl der Adressen in der CSV-Datei und der vordefinierten Anzahl von Labels pro Seite die nötige Seitenzahl des Etikettendokuments. Die Funktion »ceil()« aus dem POSIX-Modul rundet bei gebrochenen Werten auf die nächste ganze Zahl auf. Die Anzahl der zusätzlich gebrauchten Seiten in »$addtl_pages« ist dann um eins kleiner, da bereits der User das Template-Dokument mit einer Seite angelegt hat.

Alle Tabellenzeilen der Testseite liegen nach dem ersten Xpath-Query in Zeile 30 des Listings 2 im Array »@rows« und für jede zusätzlich zu erzeugende Seite iteriert die For-Schleife ab Zeile 34 über diese Zeileneinträge, dupliziert sie mit »replicateElement()« und weist die Funktion mit dem Parameter »body« an, die Dublette am Ende des Dokumentkörpers einzufügen. Die neu erzeugten Zeilen sind exakte Kopien der Zeilen der ersten Seite, enthalten also auch teilweise Elemente mit Testdaten oder sind schlicht und einfach leer.

Der zweite Xpath-Query in Zeile 41 fördert alle Tabellenelemente (drei pro Zeile, inklusive aller Elemente auf neu erzeugten Seiten) des Dokuments hervor und legt sie im Array »@labels« ab. Die For-Schleife ab Zeile 48 klappert sie ab und weist ihnen den Style »P1« zu. Ein Dump zeigt, dass dies daher rührt, dass der User vorher beim Eingeben der Testdaten den Verana-Font eingestellt hat.

Der anschließende Aufruf von »setText()« nimmt den nächsten Datensatz aus der Adressdatei und legt den zugehörigen Textstring im gerade bearbeiteten Tabellenelement ab. Die Schleife zählt die Indexvariable »$addr_idx« des Adress-Array von Null ausgehend stetig hoch und setzt sie auf Null zurück, falls sie das Ende der Adressdatenbank erreicht, um wieder mit der ersten Adresse zu beginnen.

Label im Papierschacht richtig einlegen

Die abschließend ausgeführte Methode »save()« sichert die bislang nur im flüchtigen Speicher ausgeführten Veränderungen in der Zieldatei »ready.odt« auf der Festplatte. Ruft der User sie mit Open Office auf (getestet mit Version 3.2), zeigt sich das Dokument wie in Abbildung 8 zu sehen.

Abbildung 8: Jetzt in Open Office Write auf »Drucken« klicken, schon kommen die Etiketten aus dem Drucker.

Abbildung 8: Jetzt in Open Office Write auf »Drucken« klicken, schon kommen die Etiketten aus dem Drucker.

Nun gilt es nur noch, eine Seite mit den Selbstklebe-Etiketten in den Drucker einzulegen und in Open Office Writer den Menü-Eintrag »Drucken« anzuklicken. Um keine Etiketten zu verschwenden, empfiehlt sich ein Probelauf mit einem Blatt Papier, dessen Aufdruck man anschließend durch ein frisches Etikettenblatt durchscheinen lässt.

Ob die Etiketten im Papiereinzug nach oben oder unten zeigen müssen, damit der Drucker sie richtig bedruckt, und in welcher Richtung der Drucker das Blatt einziehen muss, lässt sich am besten herausfinden, indem man ein Blatt Papier im Einzug an einer Ecke mit Bleistift markiert, die Lage der Markierung auf einem Testdruck (Abbildung 7) begutachtet und im Kopf komplizierte raumgeometrische Transformationen anstellt.

Abbildung 7: Nach dem Skriptlauf enthält die Open-Office-Datei »ready.odt« alle eingefügten Adressen.

Abbildung 7: Nach dem Skriptlauf enthält die Open-Office-Datei »ready.odt« alle eingefügten Adressen.

Für die Installation sind die drei Module OpenOffice::OODoc, Sysadm::Install und Text::CSV_XS notwendig. Das letztgenannte ist eine geschwindigkeitsoptimierte Version des Oldtimers Text::CSV. Eine CPAN-Shell installiert diese Module, falls sie in der verwendeten Distribution nicht ohnehin schon verfügbar sind.

Anschließend öffnet der User die Applikation Open Office Writer und wählt im Etiketten-Dialog das von ihm verwendete Etikettenformat aus. Der in Zeile 11 von Listing 2 voreingestellte Wert von 30 Labels pro Zeile muss der Anwender noch an das verwendete Etikettenformat anpassen.

Fehlersuche

Falls etwas im Ergebnis nicht stimmt, dann hilft die Analyse der ODF-Datei mit dem oben schon beschriebenen »oo-dumper«, um eventuelle Abweichungen vom Format durch passende Xpath-Queries zu kompensieren. Nachdem der Anwender einige Testfelder ausgefüllt und als »template.odt« abgespeichert hat, sollte das Skript »label-writer« mit einer ordnungsgemäß in UTF-8 kodierten Adressdatei »address-book.csv« ohne Ausgabe durchlaufen und die Datei »read.odt« erzeugen, die der Printer zugeschickt bekommt.

Anwendungsmöglichkeiten

Ganz offensichtlich bieten sich noch weitere Anwendungsmöglichkeiten an: Kabelbeschriftungen wie im Rechenzentrum oder auch Gerätenummern fürs Assett-Management. Vielleicht setze ich mir ja auch mein Buchhalter-Käppi auf, streife die Ärmelschoner über und verpasse jedem Buch meiner Privatbibliothek ein Etikett, das festlegt, in welches Regal es gehört [5].

Infos

  1. Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2011/05/Perl
  2. Solveig Haugland, “A Simple Way to Do Labels in OpenOffice Writer”: http://openoffice.blogs.com/openoffice/2007/06/a-simple-way-to.html
  3. Michael Schilli, “Datenfischer”: Linux-Magazin 08/2005, S. 100, https://www.linux-magazin.de/Heft-Abo/Ausgaben/2005/08/Datenfischer2
  4. Jean-Marie Gouarné, “The Perl OpenDocument Connector”: The Perl Review, tpr-200604-v3i1.pdf
  5. Michael Schilli, “Ab die Post!”: Linux-Magazin 10/2004, S. 132, https://www.linux-magazin.de/Heft-Abo/Ausgaben/2004/10/Ab-die-Post
  6. Michael Schilli, “Kein Etikettenschwindel”:Linux-Magazin 08/10, S. 116, https://www.linux-magazin.de/Heft-Abo/Ausgaben/2008/10/Kein-Etikettenschwindel

Der Autor

Michael Schilli arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. Er hat die Bücher “Goto Perl 5” (auf Deutsch) und “Perl Power” (auf Englisch) für Addison-Wesley geschrieben und ist unter mailto:mschilli@perlmeister.com zu erreichen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben