Web-Services mit SOAP (Linux-Magazin, Juli 2002)

Web-Services über HTTP und das SOAP-Protokoll überwinden locker Hürden wie unterschiedliche Betriebssysteme oder Programmiersprachen. Heute stellen wir die Windows-Applikation Photoshop über einen Web-Service ins lokale Netz und fernsteuern sie von einem Linux-PC aus.

Meine digitalen Fotos bearbeite ich üblicherweise mit Gimp, der so ziemlich alles, was Adobes Photoshop kann, nachbildet -- bis auf eine Ausnahme: Über die Funktion Autolevels, die die Belichtung eines Fotos nachträglich automatisch korrigiert, verfügt bislang nur das 600 Dollar teure Adobe-Produkt. Da ich meist auf einem Linux-PC arbeite und üblicherweise den Windows-PC mit Photoshop irgendwo im Netz stehen habe, aber zu faul bin, dauernd hin- und herzulaufen, dachte ich mir: Warum richte ich auf der Windows-Kiste nicht einfach einen Web-Service ein, über den ich von der Linux-Kommandozeile aus Photoshop aktivieren, ihm Bilder schicken, mittels AutoLevel korrigieren lassen und gleich wieder abholen kann?

Modewort Webservices

Web-Services ist ja das neue Buzzword für Manager in der Softwareindustrie. Vielleicht werden die über das Web und das SOAP-Protokoll ansteuerbaren Programme bald den Weg von Video-On-Demand, Cue-Cat und Push-Technologie gehen und durch die Toilette des Internets gespült, aber, hey, wir sind ja noch jung und schrecken vor keinem Modegag zurück!

Mittels SOAP, dem Simple Object Access Protocol, kann ein Client im Netz einen Server anweisen, Objekte anzulegen, Methoden mit Parametern auszuführen und die Ergebnisse wieder über SOAP zurückzuliefern. SOAP ist nur ein Protokoll, das Objekte, deren innere physische Struktur üblicherweise stark von der verwendeten Programmiersprache und dem Betriebssystem abhängt, auf höherere Ebene logisch als XML darstellt, damit Client und Server sich unterhalten können, auch wenn es sich z. B. um eine .net-Applikation auf einem Windows-PC und ein Perl-Skript auf einer Linux-Kiste handelt. Oder ein Visual-Basic-Script und eine Java-Applikation. Als Transportmedium für das erzeugte XML dient üblicherweise das Webprotokoll HTTP, es kann aber genausogut Mail-SMTP oder Jabber sein, alles geht.

Das heute vorgestellte Skript soapc.pl nimmt zwei Parameter: den Hostnamen des Windows-Rechners, auf dem Photoshop installiert ist und den Namen der Bilddatei. Wie Listing soapc.pl zeigt, liest es die Bilddatei ein, verbindet sich mittels des praktischen Moduls SOAP::Lite von Pavel Kulchenko mit dem Windows-Rechner und teilt ihm über SOAP mit, die pshopit()-Methode des urn:Photoshop/Hoelle-Service (ein Phantasiename, dient nur der eindeutigen Identifizierung) aufzurufen und ihr die Bilddaten (in $data) zu übergeben.

Abbildung 1: Der Client schickt eine Anfrage mit Bild an den Server

Der Service sendet die Daten des transformierten Bildes zurück, die result()-Methode in Zeile 27 in Listing soapc.pl fängt sie ein und die Variable $rc in soapc.pl nimmt sie auf. Es braucht uns nicht zu kümmern, dass das Bild binäre Daten enthält -- SOAP steht dafür grade, dass der Datensalat schön in gültiges XML umgewandelt wird, indem es sie Base64-kodiert. Abbildung 1 zeigt den abgeschickten HTTP-Request mit den typischen und einigen SOAP-spezifischen HTTP-Headern. Anschließend folgt im Body der Nachricht ein XML-Dokument, das dem Server mitteilt, was zu tun ist und die Bilddaten (in Abbildung 1 mit /9j/4 ### BILDDATEN ### BQf/Z abgekürzt, da es sich um 64 Kilobytes Base64-Daten handelt) überreicht. Eine ausgezeichnete Referenz zum SOAP-Protokoll mit vielen Anwendungsbeispielen ist [3].

Der Server (implementiert in Listing soaps.pl) entpackt die Requestdaten, speichert das Bild unter einem temporären Namen auf der Festplatte, nutzt das CPAN-Modul Win32::OLE, um Adobes Photoshop fernzusteuern. Letzteres lädt die temporäre Bilddatei von den Platte, führt die Funktion Image->Enhance->Autolevels aus, skaliert das Bild auf 600 Pixel Breite bei gleichen Proportionen und speichert es im PNG-Format auf der Platte ab. Der SOAP-Server kratzt das Bild von der Platte und schickt es zurück an den SOAP-Client auf der Linux-Kiste, der es auf STDOUT ausgibt. Abbildung 2 zeigt, was tatsächlich über die Leitung geht.

Abbildung 2: Der Server antwortet mit dem konvertierten Bild

Billig-SOAP als CGI

Für SOAP gibt's sowohl client- als auch serverseitig haufenweise Implementierungen: ob in Java, Perl, C++ oder C#, alle reden (fast) die gleiche Sprache miteinander.

Da SOAP wegen eleganter Firewallaustricksung hauptsächlich über HTTP serviert wird, liegt es nahe, unseren SOAP-Server mit einem Webserver zu verkuppeln. Und da Performance in diesem Fall keine Rolle spielt, implementieren wir's einfach, wie in Listing soaps.pl gezeigt, mittels SOAP::Lite als CGI-Skript auf einem Apache-Server. Dem Client ist egal, wie der Server implementiert ist -- Hauptsache er spricht SOAP.

Aber wie kommt ein Webserver auf ein Windows-98/2000/NT-Betriebssytem? Ganz einfach: Mit einem fertig compilierten, kinderleicht zu installierenden Apache von www.apache.org, wie der Abschnitt ``Installation'' weiter unten zeigt.

Client/Server-Paar in 10 Zeilen

Der SOAP-Code in Client und Server ist wirklich kompakt. Schon mit 10 Zeilen Code und SOAP::Lite schaffen wir ein voll funktionsfähiges Client/Server-Paar! Die Codezeilen 22 bis 27 im Client soapc.pl und die Zeilen 9 bis 11 im Server soaps.pl erledigen alles.

Wie funktioniert der Client soapc.pl? Die Zeilen 7 und 8 in soapc.pl schalten Perls Warnungen bei schlampigem Code an, zwingen zu Disziplin beim Deklarieren von Variablen und verbieten zweifelhafte Perl-Konstrukte wie weiche Referenzen und benutzerdefinierte Barewords. Zeile 10 zieht das allmächtige SOAP::Lite-Modul herein. Zeile 12 erfasst die beiden hoffentlich vorliegenden Kommandozeilenparameter, die den Namen (oder die IP-Adresse) der Windows-Kiste und den Namen der zu bearbeitenden Bilddatei angeben. Fehlt einer oder beide, bricht Zeile 14 das Programm ab. Die Zeilen 17 bis 20 öffnen die JPG-Datei und lesen sie ganz in den Skalar $data ein.

Zeile 22 erzeugt ein neues SOAP::Lite-Objekt, dessen Methoden uri(), proxy(), pshopit() und result() die nachfolgenden Zeilen mit der ->-Syntax aufrufen -- der Trick ist einfach, dass jede Methode wieder eine Referenz auf das ursprüngliche SOAP::Lite-Objekt zurückgibt und so die ``Verkettung'' dieser Aufrufe erlaubt. Sogar die new-Methode kann entfallen, da SOAP::Lite diese automatisch aufruft.

uri() spezifiziert einen eindeutigen Uniform Resource Name für die Applikation. proxy() legt über einen URL den Einstiegspunkt auf dem dem SOAP servierenden HTTP-Server fest. pshopit() ist jedoch keine standardmäßig von SOAP::Lite zur Verfügung gestellte Methode -- vielmehr wird sie samt dem Wert des Parameters $data an den SOAP-Server weitergeleitet, der sie ausführt und die Ergebnisse zurückschickt, sodass sie result() aufschnappt und an $rc weitergibt.

Ist beispielsweise die IP-Adresse des Windows-Rechners auf dem internen Netzwerk 192.168.0.3, die JPG-Datei führt den Namen in.jpg und die Ergebnisdatei soll in out.png liegen, führt folgender Aufruf zum Ziel:

    soapc.pl 192.168.0.3 in.jpg >out.png

Wen die hin- und herflitzenden XML-Nachrichten interessieren, der kann mit

    use SOAP::Lite +trace => qw(debug);

in soapc.pl den Debug-Modus anstellen und STDERR auf der Kommandozeile in eine Logdatei umleiten:

    soapc.pl 192.168.0.3 in.jpg 2>log.txt >out.png

Listing 1: soapc.pl

    01 #!/usr/bin/perl
    02 ###########################################
    03 # SOAP client
    04 #   soapc.pl host image_file
    05 # Mike Schilli, 2002 (m@perlmeister.com)
    06 ###########################################
    07 use warnings;
    08 use strict;
    09 
    10 use SOAP::Lite;
    11 
    12 my($windows_host, $file) = @ARGV;
    13 
    14 die "usage: $0 host file\n" 
    15     unless defined $file;
    16 
    17 open FILE, "<$file" or 
    18     die "Cannot open $file";
    19 my $data = join '', <FILE>;
    20 close FILE;
    21 
    22 my $rc = SOAP::Lite
    23     -> uri("urn:Photoshop/Hoelle")
    24     -> proxy("http://$windows_host" .
    25              "/cgi-bin/soaps.pl")
    26     -> pshopit($data)
    27     -> result;
    28 
    29 print $rc;

Windows-Applikationen mit OLE fernsteuern

Auch soaps.pl ist sehr kompakt -- nur die Zeilen 7 bis 11 implementieren den eigentlichen SOAP-Server als CGI-Skript. Zeile 10 teilt dem Skript mit, dass es einfach alle ankommenden Methodenaufrufe an das Package Photoshop::Hoelle weitergeben soll, das weiter unten definiert ist. Die oben erwähnte Methode pshopit() ist so ein Fall: Der Client schickt sie, und der Server macht einfach Photoshop::Hoelle->pshopit() daraus. Fertig ist der SOAP-Server.

Was soaps.pl etwas haariger gestaltet, ist, dass das CGI-Skript eine proprietäre Windows-Applikation wie Photoshop fernsteuern muss. Das geht in der Windows-Welt über die OLE-(Object Linking and Embedding)-Schnittstelle, der so ziemlich jedes Windows-Programm (wie auch Word, Excel etc.) gehorcht. [2] und [5] schildern detailliert, wie das geht.

Prinzipiell zieht man einfach das ActivePerl standardmäßig beiligende Modul Win32::OLE herein. use Win32::OLE::Const mit dem Parameter 'Photoshop' lädt die unter Windows gängige Type Library für Photoshop und kann so auf Hunderte von Photoshopvariablen zugreifen. Oder wusste jemand bereits, dass der Code für die Autolevel-Funktion 1098216559 ist? Ich dachte es mir.

Adobe bietet unter [4] ein SDK (Software Developer Kit) an, das Beispiele anführt, wie man Photoshop über OLE fernsteuern kann. Sogar ein kleines Perl-Skript ist dabei! Allerdings ist die Dokumentation unvollständig und so etwas führt gerade bei einem Closed-Source-Produkt schnell in die Sackgasse.

Die Methode pshopit() erhält neben dem Klassennamen (es wird mit Photoshop::Hoelle->pshopit() aufgerufen) auch die binären Bilddaten als $data und speichert sie in den Zeilen 30-34 sofort in einer temporären Datei auf der Festplatte, da die Photoshop-Fernsteuerung sich darauf beschränkt, Menüpunkte wie File->Open auszuwählen, wir aber keine Daten direkt ``reinpumpen'' können. Die vorgestellte Implementierung geht davon aus, dass jeweils nur ein Client gleichzeitig andockt -- hämmerten mehrere gleichzeitig, kämen sich die temporären Dateien ins Gehege und auch Photoshop käme wahrscheinlich ins Schleudern.

Die binmode-Funktion in Zeile 32 ist unter Windows lebenswichtig, da sonst binäre Daten nicht richtig abgespeichert werden.

Zeile 37 öffnet Photoshop. Falls es schon lief, kommt die bereits laufende Instanz zum Einsatz. Zeile 39 lässt es vom Desktop verschwinden, damit eventuell unter Windows arbeitende Leute nicht gestört werden.

Aktionen in Photoshop wie das Öffnen einer Datei, der Aufruf der Autolevel-Funktion, das Verkleinern oder das Abspeichern unter einem Namen in einem bestimmten Format erledigt die Play()-Methode eines sogenanten Control-Objektes, das wiederum die MakeControlObject-Methode des Win32::OLE-Objektes erzeugt, das letzteres nicht mal interpretiert sondern an Photoshop weiterreicht. Verlangt die auszuführende Photoshop-Funktion Parameter, gelangen diese über ein Deskriptor-Objekt hinein, das von Photoshop mit der MakeDescriptor-Methode des Win32::OLE-Objektes erzeugt wird und eine Reihe von Wertepaaren verschiedener Typen aufnehmen kann. Mit PutPath, PutBoolean, PutObject etc. gelangen die Wertepaare in das Deskriptor-Objekt.

Woher ich die Parameter für diese irren Funktionen weiss? Adobe hat sie auch nicht dokumentiert, sondern liefert im SDK die Sourcen für einen sogenannten Listener-Plugin mit, den man in Photoshop einstöpselt, die gewünschten Aktionen von Hand mit der Maus ausführt, und der dann eine Art C-Code in eine Logdatei schreibt, der diese Aufgaben automatisiert.

Ganz sauber ist die Sache nicht -- so gibt es Probleme, falls etwas schiefgeht, denn die Methoden melden auftretende Fehler nicht. Für einfache Zwecke, in denen meist eh alles glatt geht, reicht es jedoch.

So lassen die Zeilen 42 bis 46 Photoshop die temporäre Datei öffnen, 49 bis 52 führen die Autolevel-Funktion aus, 56 bis 67 verkleinern das Bild und 69 bis 90 speichern es als PNG ab. Die Parameter habe ich 1:1 vom Listener-Plugin übernommen.

Zeile 81 wandelt den *.jpg-Dateinamen in eine *.png-Endung um und anschließend speichert Photoshop die Datei im PNG-Format auf der Platte ab. Die Zeilen 92 bis 96 lesen die binären Daten (wieder wichtig: das binmode-Kommando) in den Skalar $data ein, der sie in Zeile 100 an den Aufrufer und damit an den SOAP-Client zurückgibt. Die Zeilen 97 und 98 räumen die temporären Dateien wieder ab.

Listing 2: soaps.pl

    001 #!/perl/bin/perl
    002 ############################################
    003 # Photoshop SOAP server
    004 # Mike Schilli <m@perlmeister.com>
    005 ############################################
    006 
    007 use SOAP::Transport::HTTP;
    008 
    009 SOAP::Transport::HTTP::CGI
    010    ->dispatch_to("Photoshop::Hoelle")
    011    ->handle;
    012 
    013 ############################################
    014 package Photoshop::Hoelle;
    015 
    016 use Win32::OLE;
    017 use Win32::OLE::Const 'Photoshop';
    018 
    019 ############################################
    020 sub pshopit {
    021 ############################################
    022     my($self, $data) = @_;
    023     my($desc, $control, $pngfile);
    024 
    025     my $class   = "Photoshop.Application";
    026     my $tmpfile = "c:\\tmp\\ps.jpg";
    027 
    028     unlink $tmpfile;
    029 
    030     open FILE, ">$tmpfile" or
    031         die "Cannot open tmp file $tmpfile";
    032     binmode FILE;
    033     print FILE $data;
    034     close FILE;
    035 
    036         # An Photoshop andocken
    037     my $ps = Win32::OLE->new($class);
    038 
    039     $ps->{Visible} = 0;
    040 
    041         # Datei öffnen
    042     $desc    = $ps->MakeDescriptor();
    043     $control = $ps->MakeControlObject();
    044     $desc->PutPath(phKeyNull, "$tmpfile");
    045     $control->Play(phEventOpen, $desc, 
    046                    phDialogSilent);
    047 
    048         # AutoLevels
    049     $desc    = $ps->MakeDescriptor();
    050     $control = $ps->MakeControlObject();
    051     $desc->PutBoolean(phKeyAuto, 1);
    052     $control->Play(phEventLevels, $desc, 
    053                    phDialogSilent);
    054 
    055         # Resize auf 600*800
    056     $desc    = $ps->MakeDescriptor();
    057     $control = $ps->MakeControlObject();
    058     $desc->PutUnitDouble(phKeyWidth, 
    059              phUnitPixels, 600);
    060     $desc->PutBoolean(
    061              phKeyConstrainProportions, 1);
    062     $desc->PutEnumerated(
    063              phKeyInterfaceIconFrameDimmed, 
    064              phTypeInterpolation,
    065              phEnumBicubic);
    066     $control->Play(phEventImageSize, $desc, 
    067              phDialogSilent);
    068 
    069     $control = $ps->MakeControlObject();
    070     my $d1   = $ps->MakeDescriptor();
    071     my $d2   = $ps->MakeDescriptor();
    072     $d2->PutEnumerated(
    073         phKeyPNGInterlaceType, 
    074         phTypePNGInterlaceType,
    075         phEnumPNGInterlaceNone);
    076     $d2->PutEnumerated(phKeyPNGFilter, 
    077         phTypePNGFilter, 
    078         phEnumPNGFilterAdaptive);
    079     $d1->PutObject(phKeyAs, 
    080                    phClassPNGFormat, $d2);
    081     ($pngfile = $tmpfile) =~ s/jpg$/png/;
    082     $d1->PutPath(phKeyIn, $pngfile);
    083     $d1->PutBoolean(phKeyLowercase, 1);
    084     $control->Play(phEventSave, $d1, 
    085         phDialogSilent);
    086 
    087     $desc    = $ps->MakeDescriptor();
    088     $control = $ps->MakeControlObject();
    089     $control->Play(phEventClose, $desc, 
    090                    phDialogSilent);
    091 
    092     open FILE, "<$pngfile" or
    093         die "Cannot open tmp file $pngfile";
    094     binmode FILE;
    095     my $data = join '', <FILE>;
    096     close FILE;
    097     unlink $pngfile;
    098     unlink $tmpfile;
    099 
    100     return $data;
    101 }

Installation

Auf www.apache.org unter dist/httpd/binaries/win32 steht apache_1.3.24-win32-x86-no_src.msi für Windows als vollautomatisches Installationspaket bereit. Schnell durch den Installationsdialog durchgeklickt und immer die Standardeinstellungen übernommen, kommt ohne Mühe -- und ohne Reboot! -- ein Apache auf die Windows-Machine, der über einen Eintrag in der Startleiste unter ``Apache Group''->``Apache Server''->``Start Apache in Console'' gestartet wird.

Falls auf der Windows-Maschine noch kein Perl installiert ist, holen wir das schnell nach, indem wir das gute ActivePerl von www.activestate.com runterladen und ebenfalls durch den Installationsdialog klicken.

Das Modul SOAP::Lite lässt sich mit dem ActivePerl beiliegenden Paketmanager ppm abholen und installieren:

    dos> ppm
    dos> install SOAP::Lite

Anschließend muss das heute vorgestellte CGI-Skript soaps.pl in das cgi-bin-Verzeichnis des Webservers wandern, typischerweise C:\Program Files\Apache Group\Apache\cgi-bin. Es ist darauf zu achten, dass die erste Zeile von soaps.pl den korrekten Pfad zur Perl-Installation enthält -- im Gegensatz zum sonst gängigen #!/usr/bin/perl kann es unter Windows auch schon mal #!/perl/bin/perl oder ähnliches sein.

Neben dem Webserver sollte man auch noch Photoshop auf der Windows-Kiste anwerfen -- soaps.pl startet es zwar, falls es noch nicht läuft, aber die lange Verzögerung, bis das Monstrum startet, lässt den Client nicht zurückkehren, sodass man ihn sonst das erste Mal mit Control-C stoppen und nochmal starten muss.

Abbildung 3: Das CGI-Skript mit dem SOAP-Server kommt einfach ins C-Verzeichnis des Webservers

Auf der Linux-Seite müssen wir nur SOAP::Lite vom CPAN holen und installieren, wie üblich mit der CPAN-Shell:

    $ perl -MCPAN -eshell
    cpan> install SOAP::Lite

Und schon geht's los: Ein digitales JPG-Bild (z.B. test.jpg) auf der Linux-Kiste und die IP-Addresse der Windows-Box (z.B. 192.168.0.3 ausfindig gemacht, und

    soapc.pl 192.168.0.3 test.jpg >test.png

eingetippt -- je nach Bildgröße dauert es ein Weilchen, aber dann kommt das fertige Bild zurück!

Das vorgestellte Verfahren, Windows-Applikationen ueber Webservices fernzusteuern, ist natuerlich nicht auf Photoshop beschränkt. Alle OLE-fähigen Windows-Programme lassen sich so kontrollieren -- probiert fleißig!

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2002/07/Perl

[2]
Cameron Laird, ``Easy COM-Web Services Gateways'', The Perl Journal, Spring 2002, http://www.samag.com/documents/s=4075/sam1013019525698/sam0203j.htm

[3]
James Snell, Doug Tidwell, Pavel Kulchenko, ``Programming Web Services with SOAP'', O'Reilly, 2002.

[4]
``Adobe Photoshop 6.0 SDK'', http://partners.adobe.com/asn/developer/gapsdk/PhotoshopSDK.html

[5]
``Win32 Perl Programming: The Standard Extensions'', Dave Roth, MacMillan, 1998

Michael Schilli

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