Für komplexere Abläufe will man ja oft ein kleines Diagramm zeichnen, zum Beispiel um sie auf einer Webseite zu dokumentieren. Professionelle Grafiker machen das normalerweise mit dem Illustrator von Adobe, aber der ist nicht nur teuer sondern auch elend kompliziert. Heute gibt's statt dessen ein CGI-Skript, das aus einer minimalen textuellen Beschreibung eines technischen Ablaufs ein grafisches Diagramm konstruiert. Nicht schön, aber zweckmäßig. Schließlich sind wir Hacker, keine Models.
Was ist eine einfache Beschreibung? Jedes Ablaufdiagramm besteht aus Knoten und Kanten. Knoten beschreiben die Zustände oder Bedingungen, und die Kanten verbinden sie durch Pfeile miteinander. Beginnen wir mit dem einfachsten Beispiel, zwei Knoten mit einer verbindenden Kante:
1 Erster Knoten 2 Zweiter Knoten 1>2 Kante von Eins nach Zwei
Nach der Beschreibung erhält jeder Knoten eine Nummer,
unter der wir ihn weiter unten, wenn's
ans Verbinden geht, wieder ansprechen können. Zeile 3 legt mit
1>2
fest, dass die Kante mit der Beschriftung
"Kante von Eins nach Zwei"
von Knoten 1 nach Knoten 2 zeigt.
Abbildung 1 zeigt das Ergebnis im Browser.
Abbildung 1: Das CGI-Skript wandelt die textuelle Diagrammbeschreibung aus einem Kommentar, zwei Knoten und einer verbindenden Kante in ein PNG-Bild um und zeigt es an. |
Oder ein komplexeres Beispiel: Eine Spiel, das Fragen stellt und bei der ersten falschen Antwort brutal abbricht -- nur wer alle Fragen richtig beantwortet, gewinnt:
0 Start 0>1 1 Frage anzeigen 1>2 2 Antwort richtig? 2>3 Ja 2>4 Nein 3 Weitere Fragen? 3>5 Nein 3>1 Ja 5 Gewonnen! 4 Verloren
Kanten müssen durchaus nicht immer einen Text mit sich führen.
Ein einfaches 1>2
zieht einen Pfeil von Knoten 1
nach Knoten 2, ohne einen Text danebenzusetzen. Wichtig ist nur,
dass zwischen den Knoten/Kantenbezeichnungen und dem Beschreibungstext
mindestens ein Leerzeichen steht.
Wer zeichnet den Graphen? Hinter dem Skript steckt das Perlmodul
GraphViz
vom CPAN, das seinerseits auf einer Installation der
graphviz
-Distribution ([2]) aufbaut.
Der graphviz
-Algorithmus versucht herauszufinden,
wie sich das Diagramm am geschicktesten darstellen lässt.
graphviz
trennt den Inhalt eines Diagramms strikt von der
Darstellung. Die textuelle Beschreibung
definiert nur Knoten und Kanten.
Und obwohl sich die erzeugten Diagramme
noch etwas anarchisch geben, schreibt vielleicht irgendwann jemand
einen Prozessor, der das ganze hochglanzprospekttauglich macht.
Abbildung 2: Ein komplexeres Beispiel: Der Ablauf eines Spiels, das solange Fragen stellt, bis eine falsche Antwort kommt oder die Fragen ausgehen. |
Das ganze läuft auf einem Webserver, nimmt die Beschreibungsdaten in einem HTML-Formular im Browser entgegen und produziert auf Knopfdruck ein Bild im PNG-Format, das anschließend im Browser erscheint. Von dort kann man es einfach mit einem Rechtsklick und unter ``Save As'' für eigene Zwecke als Datei speichern. Auch die textuelle Beschreibung taucht wieder auf und lässt schnelle Änderungen zu.
Aus einer textuellen Diagrammbeschreibung tatsächlich ein grafisches Diagramm zu generieren ist gar nicht so einfach. Es gilt, die richtigen Abstände zwischen den Knoten zu finden und sie so zu positionieren, dass sich die Kanten nicht (oder kaum) überschneiden und das ganze optisch ansprechend aussieht.
Zum Glück haben sich die Jungs auf www.vizgraph.org
darüber
jahrelang Gedanken gemacht und die Implementierung ihres Verfahrens
unter die GPL gestellt: ich ziehe den Hut, Respekt! Mit dem schlauen
Perl-Modul GraphViz
von Leon Brocard integriert sich das ganze
leicht in ein CGI-Skript, das interaktiv Diagramme generiert.
vizgraph
besteht einer Reihe von ausführbaren Programmen wie
dot
und neato
, die GraphViz
einfach aus Perl ansteuert
und eine objektorientierte Schnittstelle darauf anbietet.
001 #!/usr/bin/perl 002 ########################################################## 003 # malmal -- Mike Schilli, 2002 (m@perlmeister.com) 004 ########################################################## 005 use warnings; 006 use strict; 007 008 use GraphViz; 009 use CGI qw(:all); 010 use CGI::Carp qw(fatalsToBrowser); 011 use URI::URL (); 012 use Text::Wrap; 013 $Text::Wrap::columns = 10; 014 $Text::Wrap::huge = 'overflow'; 015 016 if(param('d')) { 017 print header(-type => 'image/png', 018 -expires => '-1d'); 019 my $img = draw(param('d')); 020 if($img) { 021 print $img; 022 } else { 023 print draw("1 Error: $@"); 024 } 025 exit 0; 026 } 027 028 print header(), start_html( 029 -title => "Malmal", 030 -style => { 'code' => default_css(), }), 031 h1("Malmal"); 032 033 if(param('data')) { 034 my $u = URI::URL->new( url() ); 035 $u->query_form("d" => param('data')); 036 print img({ src => $u->as_string(), 037 border => 1 038 }); 039 } 040 041 print start_form(), 042 submit(-value => 'Zeichnen'), br(), 043 font({face => 'Helvetica', size => '2'}, 044 textarea(-name => 'data', 045 -value => (param('data') || 046 default_data()), 047 -rows => 20, -columns => 100, 048 ), 049 ), 050 br(), submit(-value => 'Zeichnen'), 051 end_form(), end_html(); 052 053 ########################################################## 054 sub draw { 055 ########################################################## 056 my ($data) = @_; 057 058 $ENV{PATH} .= ":/pfad/zur/installation"; 059 060 my $g = GraphViz->new( 061 node => { 062 fontname => 'arial', 063 fontsize => 10, 064 shape => 'box', 065 style => 'filled', 066 fillcolor => 'orange', 067 }, 068 edge => { 069 fontname => 'arial', 070 fontsize => 10, 071 fontcolor => 'blue', 072 color => 'blue', 073 style => 'bold', 074 }, 075 rankdir => 1, 076 ); 077 078 my @edges = (); 079 my %nodes = (); 080 081 for (split /\n/, $data) { 082 s/#.*//; # Kommentare 083 next if /^\s*$/; # Leerzeilen 084 s/^\s*//; # Einrückungen 085 s/\r//g; # \r-Zeichen 086 087 if(/^(\d+)>(\d+)\s*(.*)/) { 088 my $text = join "\n", wrap('', '', $3); 089 push @edges, [$1, $2, $text]; 090 } elsif(/^(\d+)\s*(.*)/) { 091 my $text = join "\n", wrap('', '', $2); 092 $nodes{$1} = $text; 093 } else { 094 $@ = "Syntax error: $_"; 095 return undef; 096 } 097 } 098 099 for my $node (keys %nodes) { 100 $g->add_node("$nodes{$node} (#$node)"); 101 } 102 103 for my $edge (@edges) { 104 my($from, $to, $text) = @$edge; 105 106 $g->add_edge("$nodes{$from} (#$from)", 107 "$nodes{$to} (#$to)", 108 label => $text); 109 } 110 111 return $g->as_png(); 112 } 113 114 ########################################################## 115 sub default_data { 116 ########################################################## 117 return <<EOT; 118 # -- Bitte hier die Beschreibung eingeben 119 1 Erster Knoten 120 2 Zweiter Knoten 121 1>2 Kante von Eins nach Zwei 122 EOT 123 } 124 125 ########################################################## 126 sub default_css { 127 ########################################################## 128 return <<EOT; 129 body { 130 background: #FFFFFF; 131 font-family: Arial; 132 } 133 input { 134 font-size: 12px; 135 } 136 h1 { 137 font-family: Arial; 138 font-size: 16px; 139 font-weight: bold; 140 color: #B82831; 141 } 142 EOT 143 }
Die Zeilen 5 und 6 schalten Perls Warnungen und strenge Programmierrichtlinien an -- immer eine gute Idee, wenn man nicht stundenlang nach idiotischen Fehlern suchen will.
Das in Zeile 8 hereingezogene Modul GraphViz
tut die
Hauptarbeit, mit CGI
lesen wir CGI-Eingabeparameter und
schreiben schönes HTML. CGI::Carp
mit dem
fatalsToBrowser
-Tag erleichtert das Debuggen von CGI-Skripts,
weil der Browser im Fehlerfall die Ursache anzeigt.
URI::URL
manipuliert URLs mit Query-Parametern.
Es exportiert eine Funktion url()
, die mit der gleichnamigen
Funktion von CGI
kollidiert -- deswegen weisen wir URI::URL
mit
()
explizit an, nichts zu exportieren.
Text::Wrap
bricht Text ordentlich an Wortgrenzen
um -- schließlich wollen wir nicht ellenlange Kästen im Diagramm,
sondern sauber formatierte. Zeile 13 setzt die Zeilenlänge auf 10
Zeichen und Zeile 14 weist Text::Wrap
an, nicht umbrechbare
Zeilen einfach fortzuführen.
Zeile 16 prüft, ob das CGI-Skript mit dem Parameter d
und
der Diagrammbeschreibung aufgerufen wurde. In diesem Fall
steckt der Browser dahinter, der das Skript über ein
<IMG>
-Tag aufrief und ein frisch generiertes PNG-Bild erwartet.
Hierzu
ruft es die ab Zeile 54 definierte Funktion
draw()
auf und gibt das zurückkommende PNG-Bild
im Binary-Format aus. Vorher jedoch wollen wir den
HTTP-Header Content-Type:
auf image/png
setzen, damit
der Browser weiß, was da ankommt. Der expire
-Parameter
ist auf gestern (-1d
) gesetzt, damit das Bild nicht im
Cache landet und der Browser immer denselben Schmarr'n darstellt,
auch wenn sich die Beschreibung geändert hat.
Im Fehlerfall liefert draw()
den Wert undef
zurück
und Zeile 23 geniert ein Pseudo-Diagramm, das eine graphische
Fehlermeldung zeigt. Andernfalls wäre im Browser allenfalls
ein zerstörtes Bild zu sehen.
Zeile 25 bricht das Skript ab. Falls es nur das Bild darstellen
sollte, ist hier die Arbeit erledigt.
Soll das Skript die Hauptseite mit (eventuell)
Image-Tag und HTML-Formular darstellen, geben die
Zeilen ab 28 einen normalen text/html
-Header aus, und schreiben
HTML, um den Titel der Seite, die Überschrift und ein
Stylesheet zu schreiben. default_css()
ab Zeile 126
liefert hierzu den passenden Stylesheet-Code.
Zeile 33 prüft, ob der CGI-Parameter data
gesetzt ist,
also ein Formular mit dem gesetzten Diagrammbeschreibungstext
abgeschickt wurde. In diesem Fall erzeugt es mit dem Modul
URI::URL
und dessen query_form()
-Methode einen URL,
der aus dem gegenwärtigen Skript-URL (gewonnen mit dem aus
CGI
exportierten url()
) und einem angehängten
d=XXX
besteht. XXX
ist hierbei der URL-codierte
Diagrammbeschreibungstext. Diesen URL flickt es in einen
<IMG>
-Tag, der den Browser wiederum später veranlasst,
das entsprechende Diagramm vom Server anzufordern und darzustellen.
So wird das Skript malmal
vom Browser gleich zweimal
hintereinander aufgerufen: Einmal explizit und später hinter
den Kulissen über den <IMG>
-Tag. Da dieser nach der
GET-Methode arbeitet, beschränkt sich die Länge der Diagrammbeschreibung je
nach Browsertyp auf 1-4 KB.
Ab Zeile 41 druckt malmal
das HTML-Formular aus, in das
der Benutzer hoffentlich die Diagrammbeschreibung eintippt.
Falls nichts im CGI-Parameter data
vorliegt, belegt
die Funktion default_data()
(ab Zeile 115 definiert)
das Beschreibungsfeld mit den in Abbildung 1 gezeigten
Testbeispieldaten vor.
Zeile 44 definiert das Eingabefeld und Zeile 50 den
Submit-Knopf.
Die Funktion draw()
ab Zeile 54 tut die eigentliche Arbeit,
nimmt die Beschreibung als $text
entgegen und liefert ein
PNG-Bild im Binärformat zurück. Alternativ könnte es auch JPG,
GIF, PostScript, BMP, SVG oder vieles anders mehr sein -- GraphViz
ist vielseitig.
Zeile 58 fügt dem Pfad
in der Umgebungsvariablen PATH
das Installationsverzeichnis
der graphviz
-Software ([2]) hinzu und erlaubt so dem
Modul GraphViz
, die notwendigen Programme im System zu finden und
aufzurufen.
Der Konstruktor des GraphViz
-Objekts ab Zeile 60 nimmt
drei Parameter mit Optionen entgegen: node
definiert
einen Optionenhash für alle Knoten im Diagramm und edge
für
alle Kanten. rankdir
legt mit einem wahren Wert fest, dass
das Diagramm von links nach rechts fortschreitet, voreingestellt
liefe es von oben nach unten.
Neben Farben, Knotenform und Strichstärke legen die Optionen auch die
verwendeten Fonts und deren Größe
fest. Während Courier
immer geht, sieht's
halt doch besser aus, wenn man kommerzielle Fonts nimmt, die
aber nicht automatisch von Linux aus erhältlich sind. Der
Abschnitt Installation zeigt einen Trick, wie man Profi-Fonts
einbindet.
Im Beschreibungstext wollen wir durchaus auch zulassen, dass
jemand eine Kante zwischen Knoten A und B definiert, ohne
dass A oder B vorher definiert wurden. Deswegen sammelt
malmal
zunächst alle Daten ein und gibt sie dann richtig
sortiert an graphviz
weiter. @edges
ist hierbei ein
Array der Kanten und %nodes
ein Hash, der jeder Knotennummer
den entsprechenden Knotentext zuordnet.
Ab Zeile 81 iteriert die for
-Schleife durch den
vom Browser gesandten
Beschreibungstext. Die Zeilen 82-85 überspringen Kommentare
(eingeleitet mit #) und Leerzeilen, eliminieren Einrückungen
und werfen die \r
-Zeichen raus, die der Browser
hineingewurschtelt hat.
Zeile 87 findet Kantenbeschreibungen im Text
(z.B. "1>2 Text"
), extrahiert Startknoten, Endknoten
und Beschreibungstext. Die wrap()
-Funktion aus dem Text::Wrap
-Modul
bricht ihn in Zeilen mit nicht mehr als 10 Buchstaben um und
join()
fügt sie zu einem mehrzeiligen String in $text
zusammen.
Zeile 89 legt alle drei Werte in einen Array und
schiebt eine Referenz darauf ans Ende von @edges
.
Zeile 90 erkennt Knotendefinitionen. Anschließend wird
der zugehörige Beschreibungstext formatiert und der eventuell mehrzeilige
String dann unter dem Schlüssel der Knotennummer im
Hash %nodes
abgelegt.
Falls Zeile 94 zum Einsatz kommt, liegt ein Syntaxfehler
in der Beschreibung vor, da es sich offenbar weder um eine Knoten- noch
um eine Kantenbeschreibung handelt. draw()
gibt in diesem
Fall undef
zurück, was das Hauptprogramm dazu veranlasst, ein
Diagramm mit einer Fehlermeldung auszugeben.
Die for
-Schleife in Zeile 99 iteriert über alle Knoten und
fügt sie der Reihe nach mit der add_node()
-Methode in
das GraphViz
-Objekt ein.
Ab Zeile 103 kommen die Kanten dran. Zeile 104 entpackt die
Elemente aus dem Unter-Array. Die add_edge()
-Methode
erwartet den Text des Startknotens, den des Endknotens und
der Parameterwert label
gibt die Kantenbeschriftung
an.
Dann ist's soweit -- Zeile 111 ruft die as_png()
-Methode
des GraphViz
-Objekts auf, das den Graph als PNG-Bild generiert
und dessen Binärdaten als Skalar zurückliefert.
Auf [2] gibt's die graphviz
-Source-Distribution kostenlos
zum Abholen. Das übliche
tar zxfv graphviz-1.8.5.tar.gz cd graphviz-1.8.5 ./configure --prefix=/pfad/zur/installation make install
installiert die Programme dot
, neato
und Konsorten in
/pfad/zur/installation
. Falls letzerer nicht in
der Umgebungsvariablen PATH
enthalten ist, muss
Zeile 58 in malmal
angepasst werden.
Das GraphViz
-Modul gibt's vom CPAN:
perl -MCPAN -e'install GraphViz'
Dann noch schnell das Skript malmal
von [1] herunterladen,
ins cgi-bin
-Verzeichnis
verlagert, ausführbar gemacht und den Browser auf
http://localhost/cgi-bin/malmal
gestellt -- fertig!
Linux kommt aus der Schachtel heraus ja nicht mit kommerziellen
Truetype-Fonts daher. Wer schon irgendann mal
ein Windows gekauft hat (auch über die Microsoft-Steuer auf PCs)
und dieses
vielleicht sogar auf demselben Rechner als Double-Boot-Installation
betreibt, kann aber die Windows-Fonts verwenden!
Man findet sie in /dos/windows/fonts
wenn /dos
auf die Windows-Platte zeigt, weil es z.B. auf /dev/hdb1
(oder
wo immer Windows installiert ist) gemountet ist.
Dann braucht man nur noch xfstt
herunterzuladen ([3]),
zu übersetzen
und zu installieren:
tar zxfv xfstt-1.1.tar.gz cd xfstt-1.1 make install
Tritt beim Übersetzen ein Fehler auf, liegt's wahrscheinlich daran, dass
in den Zeilen 348 und 357 von TTFont.cpp
vor die beiden malloc()-Befehle ein
(char *)
muss. So ist es richtig:
convbuf = (char *) malloc(sizeof(char) * 256);
Das Installationskommando versucht auch, einen Link
von /usr/share/fonts/truetype/winfonts
auf /DOS/windows/fonts
anzulegen. Das muss an die örtlichen Gegebenheiten angepasst werden.
Irgendwo unter dem truetype
-Verzeichnis wünscht xfstt
jedenfalls
die kommerziellen Font-*.ttf
-Dateien zu finden.
Dann noch schnell
echo "Incorporating True Type Fonts ..." /usr/X11R6/bin/xfstt --sync /usr/X11R6/bin/xfstt & /usr/X11R6/bin/xset fp+ unix/:7101
in /etc/rc.d/rc.local
geschrieben und schon stehen dem
X-Windows-System nach jedem
Reboot kommerzielle Fonts zur Verfügung. Auch Gimp und seine
Freunde nehmen sie dankbar an.
Es ist kaum zu glauben -- der Perl-Snapshot wird nächsten Monat fünf Jahre alt! 60 Artikel! Ich möchte mich ganz herzlich für die zahlreichen Rückmeldungen und Anregungen bedanken -- danke, liebe Snapshot-Fans, nur wegen euch schreibe ich! Bis nächsten Monat!
xfstt-1.1.tar.gz
gibt's auf ftp://www.ibiblio.org/pub/Linux/X11/fonts
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. |