Linie für Linie

Wer einmal versucht hat, eine HTML-Seite ähnlich attraktiv zu gestalten wie die eines gedruckten Magazins, dürfte relativ bald vor der Alternative Magengeschwür oder graue Haare gestanden haben. Das Mittel der Wahl heißt hier PDF, und auch für dieses Format gibt es die passende Perl-Anbindung.

vorlesen Druckansicht
Lesezeit: 9 Min.
Von
  • Christian Kirsch
Inhaltsverzeichnis

In der Zielsetzung ähnelt Acrobats Portable Document Format (PDF) der Web-Sprache HTML: Geräteunabhängiger Code, Hyperlinks und integrierte Bilder sind mit beiden realisierbar. Der PostScript-Abkömmling PDF jedoch erlaubt anders als HTML eine genaue Festlegung des Layouts - Schriften, Größen, Abstände, alles kann die Designerin vorgeben.

Wo es also darauf ankommt, gestalterische Vorgaben exakt umzusetzen, ist PDF das geeignete Format. Zur Erzeugung kann man sich der PDFlib von Thomas Merz bedienen, erhältlich bei www.pdflib.com. Sie ist für nicht-kommerzielle Anwendungen frei. Der folgende Text beruht noch auf der Version 2.01, zur Zeit befindet sich Version 3.0 im Beta-Test. Die Bibliothek ist in C implementiert und bietet Bindings für C++, Tcl, Python, Perl, Visual Basic und Java.

Zum Erstellen der Script-Bindings diente SWIG [1], ab Version 3.0 sind die Bindings manuell geschrieben. Das automatische Verfahren erzeugt funktionierende, aber nicht unbedingt elegante Schnittstellen. So definiert das Perl-Binding kein echtes Objekt und jede seiner Funktionen erwartet als ersten Parameter eine opake Variable. Das sieht etwa so aus:

$p = PDF_new()
PDF_open_file($p,"mein.pdf");
PDF_begin_page($p,595,842);

Perlianer würden eher Folgendes erwarten:

$p = new PDF;
$p->open_file("mein.pdf");
$p->begin_page("A4");

Um diese gewohnte Schreibweise zu ermöglichen, habe ich ein Wrapper-Modul PDF.pm geschrieben, das das von SWIG erstellte Perl-Binding benutzt. Zur Erzeugung eines PDF-Objekts dient wie immer die Funktion new, die klassisch so definiert ist:

sub new {
my $p = pdflib::PDF_new();
bless { ’_pdf’ => $p } , $_[0];
}

Sie besorgt lediglich das opake Objekt aus der PDFlib und speichert es im Objekt-Hash unter dem Schlüssel _pdf. Deutliche Vorzüge gegenüber der automatisch erzeugten Schnittstelle bietet beispielsweise die in Listing 1 wiedergegebene Methode begin_page(). Sie verarbeitet neben den direkten Angaben für Seitenbreite und -höhe symbolische Namen wie ‘A0’ oder ‘Letter’.

Mehr Infos

Listing 1: Seitendefinition PDF.pm

%pagesize = ( "A0" => [2380, 3368],
"A1" => [1684, 2380],
"A2" => [1190, 1684],
"A3" => [842, 1190],
"A4" => [595, 842],
"A5" => [421, 595],
"A6" => [297, 421],
"B5" => [501, 709],
"LETTER"=> [612,792],
"LEGAL" => [612, 1008],
"LEDGER" => [1224, 792] );

sub begin_page {
my ($self,$a,$b) = @_;
if ($a && $b) {
$self->{'_pagesize'} = [ $a, $b];
pdflib::PDF_begin_page($self->{'_pdf'}, $a, $b);
} elsif (exists ($pagesize{uc($a)}) ) {
$a = uc($a); $self->{'_pagesize'} = $pagesize{$a};
pdflib::PDF_begin_page($self->{'_pdf'},
$pagesize{$a}->[0], $pagesize{$a}->[1]);
} else {
$self->{'_pagesize'} = $pagesize{'A4'};
pdflib::PDF_begin_page($self->{'_pdf'},
$pagesize{'A4'}->[0], $pagesize{'A4'}->[1]);
warn "Illegal pagesize $a, using A4";
}
}

Wie zu sehen, reicht sie nicht nur die Seitenabmessungen an das ursprüngliche Perl-Binding pdflib::begin_page weiter, sondern speichert die Werte auch im Objekt-Hash. Ist es möglich, diese Werte wieder aus dem Objekt herauszuholen, erleichtert das die Erstellung flexibler und robuster Programme.

Soll beispielsweise eine Funktion eine Überschrift am Kopf der Seite ausgeben, kann man ihr die Seitenhöhe als Parameter übergeben. Dafür muss das Hauptprogramm sich diesen Wert aber gemerkt haben oder ihn bei symbolischen Bezeichnungen wie ‘A4’ kennen. Kann man jedoch das Objekt selbst nach der Seitengröße fragen, ist solches Wissen überflüssig.

Die nahe liegende Lösung wäre nun, eine Funktion get_pagesize() zu schreiben, die die Seitengröße zurückliefert. Wie jedes andere Grafiksystem verwaltet PDF jedoch zahlreiche Angaben dieser Art: Liniendicke, Füll- und Linienfarbe, Schriftart und so weiter. Für jeden dieser Werte wäre eine eigene get_-Funktion zu bauen. Das ist sicherlich machbar, aber nicht sonderlich elegant.

Und natürlich bietet Perl eine elegante Lösung. Diese heißt ‘Autoloader’. Enthält ein Modul eine Funktion AUTOLOAD, ruft es bei Verwendung einer nicht-definierten Methode stets diese auf. Am Beispiel des PDF-Moduls: Ohne AUTOLOAD()-Funktion würde

$p->get_font() 

zu einem Fehler führen, wenn PDF.pm keine Funktion get_font() enthält. Ist jedoch AUTOLOAD() definiert, wird es stattdessen aufgerufen.

Auf den ersten Blick ist damit nicht viel gewonnen - es fehlt noch Code, sodass der Autoloader brauchbare Ergebnisse liefern kann. Im PDF-Modul sieht dieser Code so aus:

sub AUTOLOAD {
no strict "refs";
if ($AUTOLOAD =~ /.*::get(_\w+)/ ) {
my $attr_name = $1;
*{$AUTOLOAD} =
sub {return $_[0]->{$attr_name}};
return $_[0]->{$attr_name};
}
croak "No such method: $AUTOLOAD";
}

$AUTOLOAD ist eine globale Variable, die den vollständigen Namen der verwendeten (aber nicht definierten) Funktion enthält. Der reguläre Ausdruck prüft, ob dieser Name mit ‘.*::get_’ beginnt. Vor den Doppelpunkten steht der vollständige Modulname, in der Regel also ‘PDF’. Der Unterstrich und ihm folgender Text landet in $attr_name, nach Benutzung von $p->get_font() stände dort also ‘_font’. Im nächsten Schritt definiert AUTOLOAD() genau diese Funktion: Es trägt in die Symboltabelle eine anonyme Subroutine ein, die den Wert von $attr_name zurückliefert. Im Falle von $p->get_font() liest sich die fragliche Zeile:

*{PDF::get_font} = sub {return $_[0]->{_font} 

Anschließend ist die Funktion PDF::get_font definiert, sodass weitere Aufrufe von $p->get_font() gar nicht mehr den Autoloader aktivieren. Wer sich intensiver für die Details interessiert, sei auf das Buch von Damian Conway verwiesen [2], aus dem das beschriebene Verfahren stammt.

Mit Hilfe des Autoloaders kann man jetzt die Werte aller mit set_...() eingestellten Attribute abfragen. Damit lassen sich nicht nur robustere Unterprogramme schreiben, die beispielsweise die Schriftgröße zwar verändern, am Ende aber wieder auf den ursprünglichen Wert zurücksetzen - es ist auch möglich, Werte abhängig von ihren vorherigen zu ändern. So könnte eine Routine zum Ausgeben von Überschriften schlicht

$oldsize = $p->get_fontsize();
$curfont = $p->get_font();
$p->set_font($curfont, $oldsize*1.5);
...
$p->set_font($curfont, $cursize);

benutzen, um immer eine anderthalbmal größere Schrift als im restlichen Text zu verwenden.

Zur PDF-Erstellung sind mindestens drei Aufrufe nötig:

$p = new PDF;
$p->open_file("datei.pdf");
$p->begin_page("A4");

Wer Wert auf Ordnung legt, kann mit drei weiteren Funktionen aufräumen:

$p->end_page(); 
$p->close();
$p->destroy();

Nötig ist das aber nicht, denn PDF.pm enthält eine DESTROY()-Methode, die automatisch alles Nötige erledigt, wenn ein PDF-Objekt aufhört zu existieren. Noch kann man nicht auf end_page()-Aufrufe zwischen einzelnen Seiten verzichten, aber ein solches Verhalten lässt sich leicht in PDF.pm einbauen.

Aus einer XML-Datei generiertes PDF-Dokument

Die Funktionen zum Erzeugen von Text und Grafik ähneln stark dem von PostScript Bekannten. Auszüge aus einem Perl-Script zur Ausgabe von Benchmark-Ergebnissen zeigt Listing 2. Das vollständige Programm ist ebenso wie PDF.pm auf dem iX-Listingserver zu finden. Es verarbeitet ein vom XML::Parser-Modul erzeugtes globales Array @benches, aus dem es die Bonnie-Resultate der einzelnen Maschinen geordnet nach den verschiedenen Kategorien (Lesen, Schreiben, block-/zeichenweise und so weiter) herausholt und daraus das in der iX übliche Bild erstellt (siehe Abbildung). Zur Funktionsweise von XML::Parser sei auf einen älteren iX-Artikel verwiesen [3].

Download: bonnie.pdf

Mehr Infos

Listing 2: Bonnie-Ergebnisse als PDF ausgeben

...
my $pdf = new PDF;
die "Konnte PDF-Datei nicht erzeugen"
if ($pdf->open_file("bonnie.pdf") == -1);
$pdf->set_info("Creator",$0);
$pdf->set_info("Author","Christian Kirsch");
$pdf->set_info("Title","Bonnie-Ergebnisse");
$pdf->set_parameter("FontAFM","$HelveticaNarrow=$GSDIR\/n019043l.afm");
$pdf->set_parameter("FontOutline","$HelveticaNarrow=$GSDIR\/n019043l.afm");

$pdf->begin_page("A4");

$x = $Picture_Defs{'leftMargin'};

my $image = $pdf->open_GIF("logo.gif");
my $imageh = @{$pdf->get_image_size($image)}[1];
...
$pdf->place_image($image,$x,$top);
$pdf->set_font($Picture_Defs{'headFont'},
$Picture_Defs{'headSize'}*1.4);

$x += @{$pdf->get_image_size($image)}[0] + 3;

$pdf->text("Bonnie-Ergebnisse",$x,$top);
$top -= $Picture_Defs{'lineSkip'} + $Picture_Defs{'headSize'}*1.4;
#
# Schleife über alle Benchmarks
#
for (my $i = 0; $i < $benchCount; $i++) {
$x = $Picture_Defs{'leftMargin'};
#
# Überschrift erzeugen: <Maschine>, <MHz>, <CPU>, <OS>
#
my $mach = $benches->[$i]->{"machine"};
my $head = "$mach->{'machine'}";
$pdf->set_font($Picture_Defs{'headFont'},
$Picture_Defs{'headSize'});
$pdf->text($head, $x, $top);
...
$pdf->set_font($Picture_Defs{'bodyFont'}, $Picture_Defs{'bodySize'});
#
# Teilergebnisse dieses Benchmarks ausgeben
#
foreach $t ("cwrite", "bwrite", "cread", "bread", "seeks") {
my $max = ($t eq "seeks" ? $maxSeek : $maxMB );
#
# Länge des Balkens berechnen
#
my $length = $totalwidth * ( $bonnie_results[$i]->{$t} / $max );
$pdf->set_rgbcolor_fill($legend{$t}[0]);
$pdf->fill_rect($x, $top, $length, $Picture_Defs{'lineWidth'});
$pdf->set_rgbcolor_fill('black');
$pdf->set_text_pos($x+$length+$Picture_Defs{'leftMargin'}, $top);
$pdf->text($bonnie_results[$i]->{$t});
$top -= ($Picture_Defs{'lineWidth'} + $Picture_Defs{'lineSkip'});
}
$top -= ($Picture_Defs{'headSize'} + $Picture_Defs{'paragraphSkip'});
} # for $i ... $benchCount
...
$pdf->set_linewidth($Picture_Defs{'lineWidth'});
$length = $Picture_Defs{'legendLength'};
$pdf->set_font($Picture_Defs{'legendFont'},
$Picture_Defs{'bodySize'});
#
# @tpos enthält zwei Werte, jeweils für die obere und
# die untere Legendenzeile
#
my @tpos = ($top,
$top - $Picture_Defs{'bodySize'} -
$Picture_Defs{'lineSkip'} );
#
# Legende ausgeben
#
my ($leftmax,$rightmax) = (-1, -1);
my ($left, $right, $w1, $w2);
foreach $t ("cwrite", "bwrite", "cread", "bread") {

$left = $legend{$t}[1];
$w1 = $pdf->get_textwidth($left);
$leftmax = $w1 if $w1 > $leftmax;

$right = $legend{$t}[2];
$w2 = $pdf->get_textwidth($right);
$rightmax = $w2 if $w2 > $rightmax;

}

$x = $Picture_Defs{'leftMargin'};
my $ind = 0;
my $bs = $Picture_Defs{'bodySize'} / 2;
foreach $t ("cwrite", "bwrite", "cread", "bread", "seeks") {
my $x1 = $x + $leftmax + $bs;
my $x2 = $x1 + $length + $bs;

$left = $legend{$t}[1];
$right = $legend{$t}[2];

$top = $tpos[$ind];
$pdf->set_rgbcolor_fill('black');
$pdf->text($left,$x,$top);
$pdf->set_rgbcolor_fill($legend{$t}[0]);
$pdf->rect( $x1,$top, $length,
$Picture_Defs{'lineWidth'});
$pdf->fill();
$pdf->set_rgbcolor_fill('black');
$pdf->text($right, $x2 , $top);
$ind = 1- $ind;
if ($ind == 0) {
$x += $length + $rightmax + 4*$bs;
$x += $leftmax + $bs unless $t eq "bread";
}
}

$pdf->end_page();
}

Zunächst erzeugt das Script das Dokument bonnie.pdf mit einer DIN-A4-Seite. Einige Aufrufe von set_info() stellen Autor, Titel und Namen des erzeugenden Programms ein. Anschließend sorgen zwei Einsätze von set_parameter() dafür, dass im Folgenden $HelveticaNarrow als Schriftname verfügbar ist. Hier kann man leider nicht "HelveticaNarrow" als String benutzen - PDFlib schaut in der AFM-Datei nach, ob der dort angegebene Schriftname mit dem verlangten übereinstimmt, und bricht ab, wenn das nicht der Fall ist. Es hilft also nur der Blick in die AFM-Datei und die Benutzung des dort gespeicherten Font-Namens. PDFlib ermöglicht die Benutzung beliebiger Type1-Schriften, wie beispielsweise der bei Ghostscript seit einiger Zeit enthaltenen Versionen von URW. Dadurch ist man nicht auf die üblichen 13 Standardfonts angewiesen.

Im weiteren Verlauf gibt das Script ein zuvor via open_GIF() geladenes Logo mit place_image() aus und platziert daneben eine Überschrift. Parameter wie Ränder, Schriftnamen und -größen sind im globalen Hash %Picture_Defs enthalten. Um die Teilergebnisse eines Benchmarks auszugeben, erzeugt das Programm mit

$pdf->set_rgbcolor_fill(...); 
$pdf->fill_rect(....);
$pdf->set_rgbcolor_fill(’black’);
$pdf->set_text_pos(...);
$pdf->text(...);

farbige Balken, die es am rechten Ende mit dem gemessenen Bonnie-Wert versieht.

Zur Ausgabe der Legende ist noch ein bisschen Rechnerei nötig: Damit die Legendenbeschriftungen und die dazugehörigen Kästchen immer untereinander stehen, ist zunächst die für den längsten Text erforderliche Breite zu bestimmen. Das erledigt eine foreach-Schleife, die sich der Methode get_textwidth() bedient. Spätestens jetzt ist klar, warum für das Einstellen einer Schrift nicht nur deren Umrisse (als pfa-Datei), sondern auch die Fontmetrik in Form einer AFM-Datei anzugeben sind.

Für die nächste Version von PDFlib sind einige Neuerungen zu erwarten. So soll es möglich sein, Dokumente im Speicher zu erzeugen, was das Filesystem entlastet und die Auslieferung via HTTP beschleunigt. Transparente Bilder soll der Nachfolger ebenso behandeln können wie Fonts im pfb-Format und Schriften aus dem asiatischen Raum.

[1] Matthias Kilian; Eingewickelt; Verwendung von C-Code in Skriptsprachen; iX 9/1999, S. 126 ff.

[2] Damian Conway; Object Oriented Perl; Greenwich (CT), Manning 1999; ISBN 1-884777-79-1

[3] Christian Kirsch; Elemente mit System; XML-Parser für Perl-Scripts

(ck)