Sie benötigen Hilfe bei der Erweiterung oder Automatisierung von GIMP oder bei der Entwicklung in Scheme, LISP oder anderen funktionalen Sprachen?
Dann nehmen Sie Kontakt mit uns auf. Wir nehmen uns gerne Zeit für eine ausführliche und kostenlose Erstberatung.
Dieser Artikel wurde in etwas anderer Form im entwickler-magazin (Ausgabe 3.10) veröffentlicht.
Dieser Artikel wurde im Oktober 2010 erstmals auf unserer Website veröffentlicht und seitdem nicht aktualisiert; der Inhalt ist eventuell veraltet. Wir planen keine Überarbeitung des Artikels, werden aber Fehler bei Bekanntwerden korrigieren.
Mitteilungen über Fehler nehmen wir gerne per E-Mail entgegen.
Gimp ist eine freie Bildbearbeitung, die bekannten Programmen aus dem Profibereich bezüglich des Funktionsumfangs und der Qualität der implementierten Algorithmen über weite Strecken zumindest gleichwertig ist. Der professionelle Einsatz wird im Moment noch durch manches fehlende Feature verhindert, doch die Entwicklung schreitet voran.
Gimp steht unter der GPL und ist für mehrere Betriebssysteme verfügbar, darunter Windows, Unix, Linux und Mac OS X. Die zum Zeitpunkt der Erstellung dieses Artikels aktuelle Version war 2.6.7. Alle Aussagen in diesem Artikel beziehen sich auf diese Version. Das hier vorgestellte Script wurde für diese Version entwickelt, sollte aber mindestens mit jeder Version und jeder Sprachvariante aus der Reihe 2.6 zusammenarbeiten.
Dieser Artikel zeigt, wie Serien von Digitalfotos bequem und zeitsparend von der Kamera zur Veröffentlichung gelangen können. Wesentliche Arbeitsschritte werden dabei von Gimp erledigt, wobei die Steuerung durch ein Script erfolgt. Das Script wird im weiteren Verlauf genauer vorgestellt und kann heruntergeladen werden [Download].
Die einfachste Methode, das Script in Gimp einzubinden, besteht darin, es ins Script-Verzeichnis von Gimp zu kopieren (bei einer Standard-Installation von Gimp unter deutschem Windows XP SP3: C:\Programme\GIMP-2.0\share\gimp\2.0\scripts). Können Sie dieses Verzeichnis nicht ausfindig machen, so suchen Sie auf Ihrer Festplatte nach einem Verzeichnis, welches Dutzende von Dateien mit der Endung .scm enthält; mit hoher Wahrscheinlichkeit ist dies das Script-Verzeichnis von Gimp. Nach einem Neustart von Gimp ist das Script dann registriert und einsatzbereit; später dazu mehr.
Der vorgeschlagene Workflow ist ungefähr der folgende: Auslesen der RAW-Dateien aus der Kamera, Umwandlung in TIFF, Bearbeitung, Skalierung, Umwandlung in JPEG. Im folgenden werden die einzelnen Schritte genauer beschrieben und deren Notwendigkeit begründet.
Moderne Digitalkameras stellen die Ergebnisse ihrer Arbeit zumeist unter anderem in Form von JPEG-Dateien zur Verfügung, die zur weiteren Verwendung von der Kamera auf den PC heruntergeladen werden können. Da die meisten Bilder im JPEG-Format archiviert oder veröffentlicht werden, mag es zunächst bequem erscheinen, diese Bilder als Ausgangspunkt für weitere Aktionen zu verwenden. Dies sollte jedoch aus mehreren Gründen nicht geschehen:
Die Firmware der Kamera "entwickelt" JPEG-Bilder aus den Rohdaten, die der Bildsensor der Kamera bei der Aufnahme erzeugt hat. An diesem Vorgang ist eine Vielzahl komplexer Algorithmen beteiligt, deren jeder von einer Unzahl von Parametern gesteuert wird. Der Benutzer hat auch bei guten Kameras nur auf einen winzigen Bruchteil dieser Parameter Einfluß.
Darüber hinaus bringt die JPEG-Komprimierung immer einen Qualitätsverlust mit sich, so daß dieses Datenformat als Ausgangspunkt für die Archivierung oder Nachbearbeitung denkbar ungeeignet ist. Es liegt auf der Hand, daß die Bearbeitung von Anfang an und so lange wie möglich auf vollständigen Bildinformationen durchgeführt werden sollte. Die Konvertierung in ein verlustbehaftetes Format wie JPEG sollte der letzte Schritt in der Bearbeitungskette sein.
Ambitionierte Fotografen greifen deshalb gerne auf Bilder im RAW-Format zurück. Dieser Begriff bezeichnet noch kein konkretes Datenformat, sondern ist ein Oberbegriff für ein (mehr oder weniger) unverändertes Bild, wie es der Bildsensor aufgenommen hat. Die meisten aktuellen Digitalkameras geben die erzeugten Bilder auch im RAW-Format nach außen weiter. Wir halten diese Fähigkeit für sehr wichtig und empfehlen, sie als unabdingbare Voraussetzung für einen Kauf festzulegen.
Die konkrete Datenanordnung in einem RAW-Bild unterscheidet sich von Hersteller zu Hersteller, teilweise sogar innerhalb der verschiedenen Baureihen eines Herstellers. Deshalb kann bislang kein Bildverarbeitungsprogramm alle existierenden RAW-Formate direkt lesen. Es gibt jedoch immer mehr auf die RAW-Verarbeitung spezialisierte Software, die zumindest einige der RAW-Formate der bekannten Hersteller verarbeiten kann, und auch immer mehr der gewöhnlichen Bildbearbeitungen können einen Teil der RAW-Formate lesen.
Daß diese Entwicklung der Qualität der Ergebnisse förderlich ist, darf bezweifelt werden. Im Idealfall sind RAW-Dateien direkte, unveränderte Abbildungen des Sensors, und es ist Expertenwissen über den technischen Aufbau des jeweiligen Sensors und seines Umfeldes sowie über die Physik und die elektronischen Eigenschaften der einzelnen Sensorzellen erforderlich, um die RAW-Dateien bestmöglich in Bilder umzuwandeln.
Aus diesem Grund sollte nach unserer Ansicht der erste kritische Schritt der Bildverarbeitung, nämlich die Umwandlung der RAW-Dateien in Dateien eines gängigen Formats, mittels der dafür vorgesehenen Programme des jeweiligen Herstellers geschehen. Dieser kennt die Eigentümlichkeiten seiner Sensoren und der umgebenden Elektronik schließlich am besten - trotz sicher nicht schlechter Programme wie Adobe Lightroom oder Apple Aperture.
Viele Hersteller liefern die entsprechende Software kostenlos mit ihren Kameras aus, andere nur gegen einen Obulus. Da die Software unverzichtbarer Bestandteil der Kamera ist, sollte bei einer anstehenden Kaufentscheidung beim Preisvergleich zwischen den Herstellern immer der Preis der Software zum Kamerapreis hinzuaddiert werden.
Software zur RAW-Verarbeitung ist oft auf eben diese Aufgabe optimiert, ohne weitere Funktionen einer typischen Bildverarbeitung zu bieten. Dies ist eher ein Vorteil denn ein Nachteil: Die RAW-Software ist gut bedienbar, weil auf ihre speziellen Aufgaben fokussiert. Nach getaner Arbeit werden die Bilder in ein bekömmliches Format exportiert; ab dann kann das ganze Spektrum der Bildverarbeitungsprogramme für weitere Schritte benutzt werden.
Das Exportformat sollte verlustfrei sein, JPEG scheidet also aus. Stattdessen bieten sich TIFF oder PNG an, die mittlerweile jeder ernstzunehmenden Bildverarbeitung bekannt sind.
Gimp 2.6 besitzt gegenüber anderen Programmen (im Moment noch) einen schwerwiegenden Nachteil: Es kann derzeit nur mit 8 Bit Farbtiefe (pro Kanal) umgehen. Fast alle Kamera-Sensoren liefern jedoch eine höhere Farbtiefe, die von RAW-Software auch korrekt behandelt wird. Mehr Farbtiefe bedeutet im wesentlichen einen größeren Dynamikumfang des Bildes und mehr Reserven bei vielen Bearbeitungsschritten wie der Aufhellung dunkler Bereiche, der Änderung von Kontrasten, der Entfernung von Farbstichen und anderen.
Deshalb ist es beim hier vorgestellten Workflow mit Gimp als wichtigem Bestandteil empfehlenswert, diejenigen Korrekturen, die von der RAW-Software angeboten werden, auch dort durchzuführen, bevor das Ergebnis zwar verlustfrei, aber farbreduziert nach 8-Bit-TIFF exportiert wird. Typische Korrekturen, die am besten noch in der RAW-Software erledigt werden, sind Schärfung, Histogramm- und Tonwertkorrektur, Verzeichnungskorrektur und Rauschunterdrückung.
Im folgenden wird davon ausgegangen, daß eine Serie von TIFF-Bildern mit 8 Bit pro Kanal skaliert und nach JPEG konvertiert werden soll. Es gibt viele Programme, die dies leisten können, darunter auch kostenlose. Benutzer von Gimp werden allerdings ungern auf andere Tools ausweichen wollen und stattdessen lieber die gewohnten Routinen von Gimp einsetzen. Leider bietet Gimp 2.6 von sich aus keine bequeme Batch-Verarbeitung für diese Aufgabe an.
Hier stellt sich als nützlich heraus, daß Gimp sehr flexibel erweitert werden kann, unter anderem durch Einbindung von Plugins. Ein Plugin kann in Form eines Scripts realisiert werden, welches in verschiedenen Programmiersprachen implementiert werden kann [Link]. Am häufigsten wird dabei Scheme verwendet, eine Sprache ähnlich LISP. Die Entwicklung von Scripts für Gimp wird durch die extrem schlechte Dokumentation massiv erschwert. Uns ist keine Dokumentation bekannt, die das Thema erschöpfend behandelt; für diesbezügliche Hinweise sind wir dankbar.
Der Teil von Gimp, der für die Ausführung solcher Scripts verantwortlich ist, heißt Script-Fu (auch Script Fu) und wird Interpreter genannt. Script-Fu war ursprünglich im wesentlichen eine Implementierung von SIOD-Scheme, ist mittlerweile aber durch einen moderneren Scheme-Interpreter namens TinyScheme ersetzt worden [Link].
Der ursprüngliche Name Script-Fu blieb dabei erhalten, und nahezu alle der Scripts, die für die SIOD-basierte Version des Interpreters geschrieben wurden, sind auch im neuen Interpreter lauffähig. Für alte Versionen von Gimp erstellte Scripts funktionieren also auch mit den aktuellen Versionen oder bedürfen nur geringer Anpassungen [Link].
Scheme ist für Programmierer, die sich sonst nur mit gängigeren Sprachen wie C, Assembler oder Basic befassen, gewöhnungsbedürftig. Konzepte wie die Präfix-Notation, die Notwendigkeit vollständiger Klammerung, Lambda-Ausdrücke und einiges mehr wollen verinnerlicht werden, was auch hier durch spärliche und abstrakte Dokumentation erschwert wird. Als Referenzwerk darf aktuell der Sprachstandard R6RS gelten; eine weitere (nicht vollständige, aber kompakte) Übersicht ist auf der Homepage von SIOD-Scheme zu finden.
So schlecht die Dokumentation, so vorbildlich das Zusammenspiel zwischen Scheme und Gimp: Jede über die normale Benutzeroberfläche von Gimp angebotene Funktion steht auch zur Benutzung durch Scripte zur Verfügung. Zur Steuerung der Scripte können simple Benutzeroberflächen auf denkbar einfache Weise erstellt werden.
Im folgenden wird gezeigt, wie die oben erwähnte Aufgabe (Skalierung und Konvertierung nach JPEG für eine Serie von TIFF-Dateien) in Gimp mithilfe eines in Scheme geschriebenen Scripts durchgeführt werden kann. Wir stellen den vollständigen Quellcode des Scripts vor, erläutern diesen und zeigen, wie das Script in Gimp eingebunden wird.
Das Script soll eine Serie von TIFF-Bildern skalieren und ins JPEG-Format wandeln. Welche Bilder behandelt werden, soll ein Dateifilter angeben, den der Benutzer in eine Dialogbox einträgt. Als einziger weiterer Parameter soll die Länge der längeren Seite der skalierten Bilder angegeben werden; die kürzere Seite soll automatisch aus dem Seitenverhältnis des Original-Bildes errechnet werden.
Diese Vorgehensweise ist auch für Bildserien geeignet, die sowohl Bilder im Hochformat als auch Bilder im Querformat enthalten. Wäre hier nur die Eingabe der gewünschten Breite erlaubt, so wären die Bilder im Hochformat nach der Skalierung größer als die Bilder im Querformat; wäre nur die Eingabe der gewünschten Höhe erlaubt, verhielte es sich umgekehrt.
Wir wählen hier den Top-Down-Ansatz zur Erklärung des Scripts. Dieses besteht aus drei Teilen. Der erste Teil deklariert die einfache Benutzeroberfläche zur Eingabe der Parameter und verbindet diese mit einer Hauptprozedur, dem zweiten Teil des Scripts. Die Hauptprozedur durchläuft alle Dateien, deren Name auf den Filter paßt, und ruft für jede Datei eine Hilfsprozedur auf, die die Datei skaliert und im JPEG-Format speichert. Diese Hilfsprozedur ist der dritte Teil des Scripts.
Zunächst wird die Benutzeroberfläche deklariert und mit dem Script verbunden:
1 ;In GIMP / im Menü registrieren 2 (script-fu-register 3 "script-fu-Tiff-To-JPEG-Size" 4 "<Toolbox>/File/Tiffs To JPEGs (Size Pixels)..." 5 "Tiffs To JPEG (Dest Size of longest Side in Pixels)" 6 "Peter Pfannenschmid, Binarus GmbH & Co. KG" 7 "(c) Binarus GmbH & Co. KG" 8 "2008-12-16" 9 "" 10 SF-STRING "Quell-Dateien" "d:\\test\\*.tif" 11 SF-VALUE "Laengste Seite (in px)" "400")
Diese Zeilen stehen im Script außerhalb anderer Prozeduren und stellen die Verbindung zwischen der Benutzeroberfläche, dem Script und Gimp her. Die erste Zeile ist ein Kommentar, in Scheme durch einen Strichpunkt eingeleitet. Die Prozedur script-fu-register wird von Gimp innerhalb von Scheme zur Verfügung gestellt und erwartet beim Aufruf einige Parameter:
Die letzten sechs Parameter (Zeilen 10 bis 11) deklarieren zwei Eingabefelder einer Dialogbox mit den üblichen Controls (Help, Reset, Cancel, OK). Die Deklaration eines solchen Eingabefeldes besteht aus drei Parametern: Typ (zum Beispiel SF-STRING oder SF-VALUE), Bezeichnung (diese steht in der Dialogbox als Label vor dem Feld) und Vorgabewert. Hier nimmt das erste Feld den Dateifilter entgegen (Vorgabe d:\test\*.tif), das zweite die Länge der längeren Seite (Vorgabe 400 Pixel).
Wird die Dialogbox mit OK bestätigt, wird eine Prozedur aufgerufen, der die Inhalte der Eingabefelder als Parameter übergeben werden. Die Reihenfolge der Parameter der Prozedur entspricht der Reihenfolge, in der die Eingabefelder deklariert sind.
Der Name der aufzurufenden Prozedur wird beim Aufruf von script-fu-register als erster Parameter übergeben (Zeile 3). Der zweite Parameter (Zeile 4) bestimmt, wo das Script in der Menüstruktur von Gimp zum Aufruf bereitsteht, sowie den Wortlaut des Menüeintrags. Der gezeigte Code erzeugt den Eintrag "Tiffs To JPEGs (Size Pixels)..." im Datei-Menü.
Ab und an findet seitens der Entwickler eine Umstrukturierung der Menüs in Gimp statt, so daß es manchmal nicht einfach ist, diesen Parameter so zu bestimmen, daß der neue Menüeintrag an der gewünschten Stelle auftaucht. Diese Programmzeile (Zeile 4) ist denn auch die einzige Zeile des gesamten Scripts, die eventuell für andere Gimp-Versionen der Reihe 2.x angepaßt werden muß oder dort anders wirkt als in 2.6.7.
Die nächsten fünf Parameter (Zeilen 5 bis 9) sind in dieser Reihenfolge: Text, der im Tooltip erscheint, wenn der Mauszeiger auf dem zugehörigen Menüeintrag verweilt; Autor des Scripts; Informationen zum Copyright; Datum der letzten Änderung des Scripts; Typ des Bilds, auf dem das Script arbeitet. Die letztgenannten vier Texte (Zeilen 6 bis 9) sind für den Nutzer des Scripts normalerweise nicht sichtbar, werden aber im Prozedurbrowser angezeigt – dazu später mehr. Der letzte Parameter (Zeile 9) kann in unserem Fall leer bleiben, weil Gimp den Bildtyp automatisch erkennt.
Der Aufruf von script-fu-register muß in Klammern stehen, da Statements in Scheme grundsätzlich vollständig zu klammern sind. Jedoch müssen ironischerweise die Parameterlisten von Prozeduren weder bei Deklaration noch bei Verwendung geklammert werden.
Der aufmerksame Leser wird an dieser Stelle bereits ein Henne-Ei-Problem bemerkt haben: Die Prozedur script-fu-register vermag wohl das Script in Gimp zu registrieren; selbstredend wäre dazu allerdings ihr Aufruf nötig, und dieser kann eigentlich nicht erfolgen, so lange das Script noch nicht registriert und damit im Menü noch nicht sichtbar ist.
Deshalb scannt Gimp beim Start die Verzeichnisse, in denen Scripte liegen könnten, untersucht jedes Script auf das Vorhandensein von script-fu-register, registriert neue oder geänderte Scripte in seiner internen Datenbank und fügt entsprechende Einträge im Menü hinzu [Link]. Um Gimp in der Entwicklungsphase eines Scripts zum Testen nicht ständig neu starten zu müssen, kann dieser Vorgang auch manuell angestoßen werden (Filter - Skript-Fu - Skripte auffrischen).
Wie oben erläutert, benötigt das Script eine Prozedur, die nach Abschluß der Dialogbox mit OK aufgerufen wird. Im Beispiel muß diese Prozedur den Namen script-fu-Tiff-To-JPEG-Size tragen und zwei Parameter verarbeiten, nämlich den Dateifilter und die gewünschte Länge der längeren Seite. Wir haben diese Prozedur auf folgende Weise implementiert:
1 (define (script-fu-Tiff-To-JPEG-Size FilePattern LongestSide) 2 3 (gimp-message-set-handler MESSAGE-BOX) 4 5 (let* 6 7 ;Variable FileList gemaess Filter fuellen 8 ;FileList hat folgenden Aufbau: (Anzahl (File_1 File_2 ...)) 9 ;Mindestens der Listeneintrag Anzahl_Files ist vorhanden 10 ( 11 (FileList (file-glob FilePattern 0)) 12 (NumberOfFiles (car FileList)) 13 (ActFile "") 14 ) 15 16 ;FileList auf die Liste der Dateinamen setzen, 17 ;erstes Element (Anzahl) entfernen 18 19 (set! FileList (car (cdr FileList))) 20 21 ;Liste mit Dateinamen durchlaufen und Skalierung durchführen 22 23 (while (> NumberOfFiles 0) 24 25 (set! ActFile (car FileList)) 26 (ResizeBySize ActFile LongestSide ".jpg") 27 28 (set! NumberOfFiles (- NumberOfFiles 1)) 29 (set! FileList (cdr FileList)) 30 ) 31 ) 32 )
Das Schlüsselwort define (Zeile 1) ist in Scheme für die Definition globaler Variablen und Prozeduren zuständig; die Definition der Prozedur muß wieder geklammert sein. Der Prozedur sind beim Aufruf folgende Parameter zu übergeben: FilePattern ist der Dateifilter, LongestSide die gewünschte Länge der längeren Seite.
Der Aufruf von gimp-message-set-handler (Zeile 3) sorgt dafür, daß Meldungen, die von Gimp bei der Ausführung des Scripts erzeugt werden, in einer Dialogbox auftauchen. Gimp kennt für derartige Meldungen, zu denen auch Fehlermeldungen gehören, noch andere Ausgabeziele wie die Script-Fu-Konsole oder die Fehlerkonsole.
Die eigentliche Arbeit der Prozedur besteht in der Definition einiger lokaler Variablen und der Modifizierung derselben, wofür eine krude Syntax notwendig ist: Mit der Konstruktion
(let* ((Var_1 Wert_1) ... (Var_N Wert_N)) (Anw_1) ... (Anw_K))
werden die Variablen Var_1 bis Var_N definiert und dabei mit den Werten Wert_1 bis Wert_N initialisiert, danach werden die Anweisungen (Anw_1) bis (Anw_K) ausgeführt, jeweils in der notierten Reihenfolge. So definierte Variablen sind im lokalen Kontext gültig, also innerhalb der Klammern, die das let*-Konstrukt umgeben. Die Anweisungen haben Zugriff auf die Variablen, sofern sie im selben Kontext stehen, also ebenfalls innerhalb dieser Klammern [Link].
Im Beispiel wird zunächst die Variable FileList definiert und mit dem Rückgabewert eines Aufrufs von file-glob initialisiert (Zeile 11), einer von Gimp zur Verfügung gestellten Prozedur, die ihrerseits als Plugin realisiert ist [Link]. Diese erwartet als ersten Parameter einen Dateifilter, als zweiten Parameter die gewünschte Zeichencodierung für die Rückgabe.
file-glob konstruiert aus dem übergebenen Filter zunächst eine Liste mit Dateinamen, die auf den Filter passen, und ermittelt die Anzahl dieser Dateinamen. Die Rückgabe von file-glob ist nun eine Liste, die zwei Elemente enthält, nämlich die Anzahl der passenden Dateinamen und die Liste mit den Namen selbst. Der Begriff Liste ist dabei nicht im Sinne des alltäglichen Sprachgebrauchs zu verstehen, sondern im Sinne der Syntax von Scheme.
file-glob wird hier mit dem Dateifilter aufgerufen, den der Benutzer in die Dialogbox eingetragen hatte; die Zeichencodierung 0 entspricht UTF8. Nach Abarbeitung der Zuweisung (Zeile 11) enthält die Variable FileList eine Liste, bestehend aus der Anzahl passender Dateien und einer Liste, in der die Dateipfade UTF8-codiert enthalten sind.
Es folgt der Einsatz einer der am häufigsten gebrauchten Prozeduren von Scheme: car nimmt genau einen Parameter entgegen; ist dies eine Liste, so gibt car das erste Element dieser Liste zurück. Im Beispiel wird die Variable NumberOfFiles definiert, und es wird ihr das erste Element der in der vorhergehenden Zeile initialisierten Variablen FileList zugewiesen, mithin die Anzahl der Dateien, deren Name auf den übergebenen Dateifilter paßt (Zeile 12).
Mit der Definition der Variablen ActFile und deren Vorbesetzung mit dem leeren String (Zeile 13) ist die Definition und Initialisierung der benötigten Variablen (Zeilen 11 bis 13) abgeschlossen. ActFile wird später stets einen Dateipfad enthalten.
Da Scheme mit latenten Typen arbeitet, ist bei der Definition von Variablen keine Typangabe notwendig. Dieser Artikel soll keine Kopie der Spezifikation von Scheme sein, deshalb verweisen wir bezüglich dieser und weiterer Feinheiten auf die einschlägige Literatur [Link]. Insbesondere sollten die Unterschiede zwischen let* und let studiert werden [Link].
Der Code der nächsten Zeile dürfte selbst für Hartgesottene erklärungsbedürftig sein, sofern sie nicht bereits mit funktionaler Programmierung vertraut sind. Wir gehen ausführlich darauf ein, weil ähnliche Konstrukte in vielen Beispielen verwendet, aber selten verständlich erklärt werden. Der Sinn dieser Zeile liegt darin, aus der Variablen FileList, deren bisheriger Inhalt oben erläutert wurde, das erste Element zu entfernen, so daß nur die Liste mit den Dateinamen verbleibt, die dann bequem durchlaufen werden kann (Zeile 19).
Um den Code zu verstehen, muß zunächst auf den Zusammenhang zwischen Paaren und Listen in Scheme eingegangen sowie die Prozedur car nochmals näher erläutert werden. Ein Paar, auch cons genannt, ist in Scheme eine Verbundstruktur (compound structure) aus zwei Teilen, die jeweils von beliebigem Typ sein können, insbesondere auch vom Typ Paar [Link].
Paare werden in Scheme unter anderem mittels der Prozedur cons erzeugt. So liefert beispielsweise das Statement
(cons 1 2)
ein Paar, dessen beide Teile aus den Integer-Werten 1 und 2 bestehen. Die Prozedur car gibt den ersten (linken) Teil des Paares zurück und findet ihren Gegenpart in der Prozedur cdr, die den zweiten (rechten) Teil des Paares zurückgibt. Im weiteren Verlauf des Artikels werden wir die Begriffe Paar und cons synonym verwenden, sofern nicht anders vermerkt.
Eine Liste in Scheme ist per definitionem eine rekursiv verschachtelte Aneinanderreihung von Paaren, deren innerstes eine leere Liste als rechten Teil besitzt. Endet die Reihe nicht in einer leeren Liste, heißt das betreffende Objekt uneigentliche Liste oder ungültige Liste [Link].
Ein Paar wird in Scheme gemeinhin in folgender Weise notiert:
(Links . Rechts)
Die übliche Schreibweise für eine Liste mit N Elementen ist:
'(Element_1 Element_2 ... Element_N).
Der Zusammenhang zwischen Paaren und Listen soll an einigen Beispielen deutlich werden:
1 (cons 1 2) ;(1 . 2) (als Paar, KEINE gültige Liste) 2 (1 . 2) ;äquivalent zu Zeile 1 ;direkt notiert statt per cons erzeugt 3 (car (cons 1 2)) ;1 (linke Hälfte des Paars aus Zeilen 1,2, ; skalarer Wert) 4 (cdr (1 . 2)) ;2 (rechte Hälfte des Paars aus Zeilen 1,2, ; skalarer Wert) 5 (cons 2 '()) ;(2 . '()) (Paar, linke Hälfte Integer 2, ; rechte Hälfte () leere Liste) 6 (2 . '()) ;äquivalent zu Zeile 5 ;direkt notiert statt per cons erzeugt 7 '(2) ;'(2) (die Liste '(2)) ;äquivalent mit Zeile 5, Interpreter ;konstruiert Liste rekursiv aus Paaren. ;Zeilen 5, 6, 7 liefern identische Objekte. 8 (car '(2)) ;2 (als skalarer Wert, linke Hälfte des ; Paars aus Zeilen 5 und 6, erstes ; Element der Liste aus Zeile 7) 9 (cdr '(2)) ;'() (leere Liste '(), rechte Hälfte des ; Paars aus Zeilen 5 und 6, Liste aus ; Zeile 7 OHNE ihr erstes Element) 10 (cons 1 (cons 2 '())) ;(1 . (cons 2 '())) ;(Paar mit linker Hälfte 1 und rechter ;Hälfte (2 . '()), letztere ist ein Paar, ;das auch als Liste '(2) geschrieben werden ;kann, siehe Zeilen 6 und 7) ;äquivalent zu Zeilen 11 und 12 11 (1 . (2 . '())) ;äquivalent zu Zeilen 10 und 12 ;lediglich direkt geschrieben anstatt ;durch Aufrufe der Prozedur cons erzeugt 12 '(1 2) ;'(1 2) (die Liste '(1 2)) ;äquivalent zu Zeilen 10 und 11, Interpreter ;bildet Liste rekursiv aus Paaren. 13 (car '(1 2)) ;1 (als Skalar, linker Teil des äußeren ; Paares aus Zeilen 10 und 11, erstes ; Element der Liste aus Zeile 12) 14 (cdr '(1 2)) ;(2 . '()) (Paar, rechter Teil des äußeren ; Paares aus Zeilen 10 und 11, Liste aus ; Zeile 12 OHNE ihr erstes Element; ; entspricht Liste '(2), siehe Zeilen 6, 7) 15 (car (cdr '(1 2))) ;2 (als skalarer Wert, linke Hälfte von ; (cdr '(1 2)), also von (2 . '()), erstes ; Element der Liste '(2)) ;formal: (car (cdr '(1 2))) = ; (car (cdr (1 . (2 . ()))) = ; (car (2 . ())) = 2
Die von Gimp zur Verfügung gestellten Funktionen geben immer gültige Listen zurück [Link]. Damit gilt für die Variable FileList vor Ausführung des Codes (Zeile 19) Folgendes (N sei die Anzahl der Dateien, deren Pfadname auf den Filter paßt, und L die Liste der Dateinamen):
FileList = '(N L) ;Listenschreibweise FileList = (N . (L . '())) ;Paar-Schreibweise (cdr FileList) = (cdr (N . (L . '()))) = = (L . '()) ;Paar-Schreibweise (car (cdr FileList)) = (car (L . '())) = L ;L ist Liste mit Namen
In der letzten Zeile ist eventuell nicht sofort verständlich, warum hier die Liste L zurückgegeben wird und nicht deren erstes Element. Der Grund wird klar, wenn L ausführlicher geschrieben wird (im Beispiel besitzt L drei Elemente):
L = '(Name_1 Name_2 Name_3) = = (Name_1 . (Name_2 . (Name_3 . '()))) FileList = '(N L) = = (N . (L . '())) = = (N . ((Name_1 . (Name_2 . (Name_3 . '()))) . '())) (cdr FileList) = ((Name_1 . (Name_2 . (Name_3 . '()))) . '()) (car (cdr FileList)) = (Name_1 . (Name_2 . (Name_3 . '())))
Bei der Auswertung von Listen durch car oder cdr wird stets das äußerste der rekursiv verschachtelten Paare herangezogen, die die Liste repräsentieren. Deshalb gibt der Ausdruck in der letzten Zeile nicht etwa Name_1 zurück, sondern das linke Element des in der vorletzten Zeile gezeigten Paares, das eben aus der Liste mit den Namen und der leeren Liste besteht.
Der Ausdruck (car (cdr FileList)) liefert demnach in der Tat die reine Liste der Dateinamen. Nun muß noch auf die dort zu sehende Wertzuweisung mittels set! eingegangen werden:
Zunächst ist zu konstatieren, daß set! nicht, wie andernorts dargestellt, zuständig für die Definition globaler Variablen ist. set! kann zwar in diesem Sinne verwendet werden, ist aber primär die Zuweisungsprozedur von Scheme und wird insofern hauptsächlich dazu gebraucht, einer im jeweiligen Kontext bereits existenten Variablen einen neuen Wert zuzuweisen [Link]. Der technische Hintergrund hierfür ist der folgende:
Bei der Definition von Variablen in Scheme, beispielsweise mithilfe von let*, wird der betreffenden Variablen nicht wirklich ein Wert zugewiesen, sondern lediglich eine Speicherstelle, die diesen Wert enthält [Link]. Nur mithilfe von set! kann diese Speicherstelle destruktiv überschrieben werden. Dies gilt auch für Variablen, die im lokalen Kontext definiert wurden; set! ist unabhängig vom Gültigkeitsbereich der betreffenden Variablen zu verwenden.
Nach Durchlauf dieses Codes (Zeile 19) enthält die Variable FileList also nur noch die Liste mit den Dateinamen. Vor dem Zugriff von car auf diese Liste wäre nun eigentlich eine Abfrage daraufhin nötig, ob diese mindestens ein Element enthält: car und cdr dürfen nicht verwendet werden, um auf die leere Liste zuzugreifen [Link].
Der Zugriff von cdr auf die von file-glob gelieferte Rückgabe muß nicht abgesichert werden, weil file-glob auf jeden Fall eine nichtleere Liste zurückliefert, deren erstes Element die Anzahl der passenden Dateien ist. Das zweite Element dieser Liste wäre jedoch die leere Liste, falls die Anzahl der passenden Dateien 0 betrüge. In diesem Fall dürfte mit car nicht darauf zugegriffen werden.
Der Scheme-Interpreter in Gimp 2.6.7 hat sich in unseren Tests daran allerdings nicht gestört: car verursachte beim Zugriff auf die leere Liste keine Fehlermeldung. Die Behandlung dieser Sondersituation betrachten wir deshalb als Übung für den Leser.
Im letzten Codeblock (Zeile 23 bis 30) wird schließlich die Liste mit den Dateinamen durchlaufen und für jedes ihrer Elemente eine weitere Prozedur aufgerufen, die die Skalierung des betreffenden Bildes durchführt. Der Durchlauf geschieht mithilfe einer while-Schleife: Solange noch mindestens ein Listenelement vorhanden ist (Zeile 23), wird das erste Element der Liste, also der erste der noch zu verarbeitenden Dateinamen, mittels der Prozedur car in die Hilfsvariable ActFile kopiert (Zeile 25) und der weiteren Verarbeitung zugeführt (Zeile 26).
Dann wird die Anzahl der noch zu verarbeitenden Listenelemente (Dateinamen) dekrementiert und die Liste selbst um ihr erstes Element verkürzt (Zeilen 28, 29). Auf diese Weise ist eine jederzeitige Übereinstimmung zwischen der Variablen, die die Anzahl der verbleibenden Listenelemente enthält und der Variablen, die die zu behandelnde Liste selbst enthält, gegeben. Die gesonderte Variable für die Anzahl der Listenelemente könnte auch komplett eingespart werden, weil Scheme Möglichkeiten bietet, zu prüfen, ob eine Liste leer ist [Link]; dann wäre nur die Eintrittsbedingung für die while-Schleife entsprechend zu ändern.
In den meisten funktionalen Sprachen spielt das Konzept der Rekursion eine zentrale Rolle. Auch Scheme ist derart konsequent auf dieses Konzept ausgerichtet, daß beispielsweise die in imperativen Programmiersprachen essentielle while-Schleife kein Bestandteil von Scheme ist [Link]. Jeder auf einer Schleife iterierende Algorithmus kann nämlich durch einen äquivalenten Algorithmus mit endständiger (repetitiver) Rekursion ersetzt werden und umgekehrt [Link]. Sofern ein Stack einbezogen werden kann, kann sogar jede Art von Rekursion innerhalb eines Algorithmus durch Iteration ersetzt werden [Link]. Weitere Erläuterungen zu diesem Thema würden den Rahmen dieses Artikels leider sprengen, so daß wir Interessierte auf einschlägige Literatur verweisen müssen [Link].
Die endständige Rekursion muß von allen Scheme-Implementierungen auf effiziente Weise umgesetzt werden, ohne Stack zu verbrauchen [Link]; dies ist Bestandteil der Sprachdefinition [Link]. Ganz stringent ist die Umsetzung des Sprachparadigmas, Rekursion möglichst als alleinige Methode der Wiederholung zu verwenden, in Scheme aber nicht: Die do-Schleife ist laut Sprachdefinition zwar nicht Bestandteil des Sprachkerns, gehört aber immerhin zu den Standard-Libraries [Link].
Zum Verständnis des gezeigten Beispiels ist nur wichtig, daß die while-Schleife hier wie gewohnt benutzt werden kann, auch wenn sie hinter den Kulissen auf unerwartete Weise umgesetzt wird. Letztlich handelt es sich dabei um ein Makro, welches von TinyScheme der Kompatibilität zu SIOD-Scheme wegen zur Verfügung gestellt wird [Link].
Bislang fehlt noch die Funktion, die die eigentliche Arbeit erledigt, nämlich die Skalierung eines Bildes und die Speicherung im JPEG-Format:
1 (define (ResizeBySize ImageFilename SizeLongestSide NameAppend) 2 3 (gimp-message-set-handler MESSAGE-BOX) 4 5 (let* 6 ( 7 (ActImage (car (gimp-file-load RUN-NONINTERACTIVE ImageFilename ImageFilename))) 8 (OrgWidth (car (gimp-image-width ActImage))) 9 (OrgHeight (car (gimp-image-height ActImage))) 10 (AspectRatio (/ OrgWidth OrgHeight)) 11 (NewWidth 0) 12 (NewHeight 0) 13 (NamePartList (strbreakup ImageFilename ".")) 14 (NewFilename "") 15 ) 16 17 ;Test, ob Punkt im Filenamen vorhanden 18 (if (= (length NamePartList) 1) 19 (set! NewFileName (string-append ImageFilename NameAppend)) 20 (set! NewFileName (string-append (substring ImageFilename 0 (- (string-length ImageFilename) (+ (string-length (car (last NamePartList))) 1))) NameAppend)) 21 ) 22 23 ;Falls mehr als eine Ebene vorhanden: 24 ;Ebene mit Index 0 (oberste Ebene, Miniaturbild) entfernen 25 26 (gimp-image-undo-disable ActImage) 27 28 (if (> (car (gimp-image-get-layers ActImage)) 1) 29 (begin 30 (gimp-image-remove-layer ActImage (aref (car (cdr (gimp-image-get-layers ActImage))) 0)) 31 (gimp-image-merge-visible-layers ActImage EXPAND-AS-NECESSARY) 32 ) 33 ) 34 35 ;Neue Breite und Höhe so berechnen, daß das Seitenverhältnis 36 ;gleich bleibt und die längere Seite die gewünschte Länge 37 ;erhält 38 39 (if (> OrgWidth OrgHeight) 40 (begin 41 (set! NewWidth (inexact->exact (round SizeLongestSide))) 42 (set! NewHeight (inexact->exact (round (/ SizeLongestSide AspectRatio)))) 43 ) 44 (begin 45 (set! NewWidth (inexact->exact (round (* SizeLongestSide AspectRatio)))) 46 (set! NewHeight (inexact->exact (round SizeLongestSide))) 47 ) 48 ) 49 50 (gimp-image-scale-full ActImage NewWidth NewHeight INTERPOLATION-CUBIC) 51 (gimp-file-save RUN-NONINTERACTIVE ActImage (car (gimp-image-get-active-layer ActImage)) NewFileName NewFilename) 52 (gimp-image-delete ActImage) 53 ) 54 )
Die Funktion ResizeBySize wird mit entsprechenden Parametern von dem im vorhergehenden Kapitel erläuterten Code aufgerufen. Der Parameter ImageFilename enthält den Namen (inclusive Pfad) der zu verarbeitenden Datei, SizeLongestSide enthält die gewünschte Länge der längeren Seite (in Pixel), und NameAppend enthält die Dateiendung der zu speichernden Datei; Gimp wird daraus beim Speichern das zu verwendende Dateiformat ableiten.
Auch ResizeBySize beginnt ihre Arbeit mit der Definition und Initialisierung einiger lokaler Variablen (Zeilen 6 bis 15). Die Wirkung von gimp-message-set-handler (Zeile 3) sowie die Syntax von let* (Zeile 5) sind im vorhergehenden Kapitel beschrieben.
An diesem Code-Abschnitt wird wieder die bereits erwähnte Tatsache deutlich, daß die Rückgabe der von Gimp zur Verfügung gestellten Prozeduren stets eine Liste ist, auch wenn diese nur aus einem einzigen Wert besteht [Link]. Um die Rückgabewerte selbst (und nicht die sie enthaltende Liste) zu verarbeiten, kann wieder car eingesetzt werden.
So öffnet gimp-file-load (Zeile 7) die Datei, deren Namen und Pfad im zweiten und dritten Parameter übergeben wurden, und gibt eine Liste zurück, die als einziges Element ein Handle auf das geöffnete Bild enthält; vor der Verwendung und Zuweisung an die Variable ActImage muß es erst aus der Liste herausgelöst werden. Gimp versucht beim Öffnen, das Dateiformat aus dem Inhalt der Datei automatisch zu erkennen; falls dies nicht gelingt, wird die Erweiterung des Dateinamens als Indikator für das Dateiformat benutzt, sofern vorhanden.
Eine nähere Betrachtung der Parameter von gimp-file-load führt zu nur begrenzt nützlichen Erkenntnissen: Die Angabe von RUN-NONINTERACTIVE im ersten Parameter sorgt dafür, daß das Laden der Bilddatei ohne Rückfragen an den Benutzer geschieht, was bei Batch-Verarbeitung die einzig angemessene Wahl darstellen dürfte. Seltsam mutet an, daß für die Übergabe des Dateipfades zwei Parameter vorgesehen sind, nämlich der zweite und dritte. Dies bietet allerdings die Möglichkeit, eine Datei direkt von einer URL aus öffnen zu lassen. Beim Öffnen von Dateien aus dem lokalen Dateisystem sollte für beide Parameter derselbe Wert übergeben werden [Link].
Als nächstes wird das Seitenverhältnis des soeben geöffneten Bildes berechnet (Zeilen 8 bis 10). Die so besetzte Variable AspectRatio wird später Verwendung finden. Auch hier müssen benötigte Werte wieder aus von GIMP zurückgegebenen (einelementigen) Listen entnommen werden. Zu beachten ist wieder die Präfix-Notation der Division. Wichtig ist, daß gimp-image-height und gimp-image-width die Dimensionen des gesamten Bildes zurückgeben, auch wenn das Bild mehrere Ebenen enthält, die womöglich andere Abmessungen besitzen sind als das Bild selbst.
NewWidth und NewHeight (Zeilen 11, 12) sind Variablen für die Breite und Höhe des skalierten Bildes (in Pixeln) und werden einfach mit 0 vorbesetzt; NewFileName (Zeile 14) nimmt später den vollständigen Pfadnamen der neu zu erstellenden Datei auf.
Interessanter ist die Initialisierung von NamePartList: strbreakup nimmt einen String und ein Trennzeichen als Parameter entgegen und konstruiert daraus eine Liste mit den einzelnen Teilen des Strings, die durch das Trennzeichen getrennt sind. strbreakup gehört zu den Prozeduren, die von TinyScheme aus Gründen der Kompatibilität zu SIOD angeboten werden; immerhin ist diese Prozedur aber nicht als veraltet markiert [Link].
Im Beispiel wird der Pfadname der Quelldatei (ImageFilename) an vorhandenen Punkten aufgetrennt; die einzelnen Bestandteile werden in der Liste NamePartList gespeichert (Zeile 13). Der Austausch der Dateiendung könnte nun bequem stattfinden, indem das letzte Element der Liste mittels einschlägiger Prozeduren geändert wird, beispielsweise durch Kombination von reverse und set-car!.
Um weitere Aspekte von Scheme demonstrieren zu können, werden hierfür aber String-Funktionen eingesetzt (Zeilen 18 bis 21). Zunächst wird überprüft, ob die Liste mit den Bestandteilen des Dateinamens genau ein Element enthält (Zeile 18). Trifft dies zu, so enthielt der Dateiname keinen Punkt, und die neue Endung (dritter Parameter von ResizeBySize) wird einfach angehängt (Zeile 19). string-append gibt einen String zurück, der durch Aneinanderreihung der im ersten und zweiten Parameter übergebenen Strings entsteht.
Trifft die if-Bedingung nicht zu, so enthielt der Dateiname mindestens einen Punkt. In diesem Fall wird davon ausgegangen, daß der letzte Teil des Dateinamens als Dateierweiterung fungiert und daß diese gegen die neue Endung ausgetauscht werden soll. Der Name der neuen Datei sollte auf jeden Fall eine der üblichen Endungen aufweisen, weil Gimp daraus bei der Erzeugung der Datei das gewünschte Dateiformat erkennt; dazu später.
Der Code zum Austausch der Endung (Zeile 20) verwendet drei bislang nicht erläuterte Prozeduren: last erwartet eine Liste als einzigen Parameter und gibt deren letztes (innerstes) cons zurück, im Falle einer gültigen Liste also ein Paar, welches aus dem letzten Element der Liste und der leeren Liste besteht (ausführliche Erläuterung im vorstehenden Kapitel). Damit ist klar, daß zum Zugriff auf das letzte Element der Liste wieder car benötigt wird.
string-length arbeitet wie erhofft und liefert die Länge des als einzigen Parameter übergebenen Strings. Auch substring verhält sich wie erwartet: die drei Parameter sind ein String, ein Index und eine Länge, die Rückgabe besteht im durch Index und Länge bestimmten Teil des übergebenen Strings. Der Index wird dabei ab 0 gezählt.
Der Austausch der Dateiendung (Zeile 20) erfolgt so: Berechnung der Länge der bisherigen Dateiendung inclusive Trennzeichen (Zeile 20, dritte Textzeile; NamePartList enthält die Teile des ursprünglichen Namens ohne Trennzeichen, deshalb muß zur aus dem letzten Element von NamePartList gewonnenen Länge der bisherigen Dateiendung 1 addiert werden); Berechnung der Länge des ursprünglichen Dateinamens ohne Endung durch Subtraktion der Länge der bisherigen Dateiendung inclusive Trennzeichen von der Gesamtlänge des bisherigen Dateinamens (Zeile 20, zweite Textzeile); Verwendung dieser Länge zur Gewinnung des ursprünglichen Dateinamens ohne Endung (Zeile 20, erste Textzeile); Anhängen der neuen Endung an diesen (Zeile 20, erste / vierte Textzeile).
Die Konstruktion des Namens für die neue Datei könnte auf einfachere Weise stattfinden, zum Beispiel durch Aneinanderreihung aller Bestandteile von NamePartList außer dem letzten (unter Hinzufügung der Punkte zwischen den Bestandteilen) sowie Anhängen der neuen Endung (NameAppend), oder, wie oben erwähnt, durch Verwendung anderer Prozeduren für die Listenverarbeitung, die in typischen Libraries für Scheme existieren [Link]. Die Wahl der hier gezeigten Lösung ist dem Wunsch geschuldet, den Umgang mit den wichtigen Prozeduren string-length, substring und last zu demonstrieren.
Bislang wurde die Syntax der Fallunterscheidung mittels if (Zeile 18) noch nicht formal erläutert: (if Bed Anw_1 Anw_2) führt den Anweisungsblock Anw_1 aus, falls die Bedingung Bed wahr ist, andernfalls den Anweisungsblock Anw_2, falls dieser existiert. Der Rückgabewert des ausgeführten Anweisungsblocks ist jeweils der Rückgabewert des Gesamtkonstrukts. Trifft die Bedingung nicht zu und existiert Anw_2 nicht, so ist der Rückgabewert des Gesamtkonstrukts undefiniert [Link].
Einige weitergehende Bemerkungen zu den gerade erläuterten Konstruktionen und Prozeduren sind angebracht: TinyScheme basiert nach eigener Aussage auf dem (veralteten) Standard R5RS und setzt diesen zwar nicht vollständig, aber so weit wie möglich um [Link]; dies ist auch der Grund dafür, daß unsere Literaturhinweise zu diesem Artikel oft auf Seiten führen, die R5RS zum Gegenstand haben. Im Gegensatz zur Iteration mittels while ist nun die Fallunterscheidung mittels if Bestandteil von R5RS und damit von TinyScheme. Gleiches gilt für die Prozeduren substring und string-length.
last hingegen wird trotz häufiger Verwendung von TinyScheme nur aus Gründen der Kompatibilität zu SIOD-Scheme zur Verfügung gestellt und ist als veraltet gekennzeichnet [Link]. Insofern hätten wir eine entsprechende andere Prozedur verwenden oder eine eigene Prozedur als Ersatz implementieren sollen. Aus Gründen der besseren Verständlichkeit haben wir jedoch darauf verzichtet, zumal last in real existierendem Code noch häufig Einsatz findet.
Das folgende Statement (Zeile 26) sorgt für einen signifikanten Gewinn an Geschwindigkeit und Speicher bei der Ausführung des Scripts. gimp-image-undo-disable nimmt als einzigen Parameter ein Handle auf ein geöffnetes Bild entgegen und deaktiviert für dieses Bild die Mechanismen zum Undo, die bei Batch-Verarbeitung meist ohnehin verzichtbar sind.
Der folgende Codeblock (Zeilen 28 bis 33) kann eventuell weggelassen werden. Er erfüllt folgenden Zweck: Falls das ursprüngliche Bild mehr als eine Ebene enthält, wird die erste Ebene (mit dem Index 0) aus dem Bild entfernt. Für unseren Workflow ist er nötig, weil die zu verarbeitenden Bilder großenteils von Canon-Equipment stammen, als Raw in Canon DPP importiert und nach ersten Bearbeitungsschritten von dort nach TIFF exportiert werden. Bei diesem Vorgang erzeugt DPP in jedem Bild zwei Ebenen, nämlich eine Ebene mit dem begehrten Bild in voller Größe (Index 1) und eine Ebene mit einem Miniaturbild (Ebene 0).
Da wir keinen Weg gefunden haben, diesen Unfug beim TIFF-Export aus dem (sonst sehr guten) DPP abzustellen, bot es sich an, die Entfernung der überflüssigen Ebene ebenfalls zu automatisieren. Um unerwünschte Fehlermeldungen und Nebenwirkungen bei normalen Bildern auszuschließen, bei denen von vornherein nur eine Ebene vorhanden ist, wird zunächst überprüft, ob der Delinquent überhaupt mehr als eine Ebene enthält (Zeile 28).
begin (Zeile 29) dient zur Gruppierung von Befehlen, genauer zur syntaktischen Zusammenfassung mehrerer Ausdrücke zu einem einzigen. Die hier gezeigte Anwendung ist typisch: Ohne entsprechende Gruppierung würde bei zutreffender if-Bedingung die erste Zeile (Zeile 30) ausgeführt, bei nicht zutreffender if-Bedingung die zweite Zeile (Zeile 31). begin ist ein Sequenzierer, der seine Ausdrücke der Reihe nach abarbeitet und den Rückgabewert des letzten Ausdrucks zurückgibt; somit werden die betreffenden Ausdrücke auch implizit gruppiert [Link]. Im Beispiel wird deshalb wie gewünscht bei zutreffender if-Bedingung der gesamte mit begin umschlossene Block (Zeilen 30, 31) ausgeführt.
gimp-image-get-layers erwartet als Parameter ein Handle auf ein geöffnetes Bild und liefert eine Liste zurück, die als erstes Element die Anzahl der enthaltenen Ebenen enthält, als zweites Element ein Array mit den einzelnen Ebenen. Mittels car wird der Liste wieder das erste Element entnommen, womit die Wirkungsweise der if-Abfrage (Zeile 28) bereits geklärt ist.
Wieder etwas schwerer verständlich ist die Zeile, die den Layer entfernt (Zeile 30); eventuell sollten nochmals die Ausführungen zum Zusammenhang zwischen Listen und Paaren (vorhergehendes Kapitel) überflogen werden. Dabei wird klar: (car (cdr (gimp- ...))) (Zeile 30) liefert ein Array, welches die Ebenen des betreffenden Bildes enthält. Die Prozedur aref nimmt als Parameter ein Array und einen Index entgegen und gibt das dadurch bestimmte Element des Arrays zurück. Der Ausdruck (aref (...) 0) (Zeile 30) liefert demnach für das betreffende Bild die Ebene mit dem Index 0, genauer ausgedrückt ein Handle auf diese Ebene, also auf die Ebene, die entfernt werden soll.
Für die Entfernung einer Ebene aus einem Bild ist gimp-image-remove-layer zuständig. Dieser Prozedur wird ein Handle auf das betreffende Bild als erster Parameter übergeben, ein Handle auf die zu entfernende Ebene (nicht deren Index!) als zweiter. Die gezeigte Codezeile (Zeile 30) entfernt also insgesamt die Ebene mit dem Index 0 aus dem Bild. Auch aref ist übrigens in der Dokumentation zu TinyScheme als veraltet markiert [Link]. Aus den gleichen Gründen wie bei last haben wir uns dennoch zur Verwendung entschieden.
Viele Dateiformate für Bilder können keine einzelnen Ebenen speichern. Deshalb muß das Script, sofern mehrere Ebenen im betreffenden Bild enthalten sind, diese vor dem Speichern zusammenführen. Dies gilt auch, wenn nach der Entfernung einer Ebene nur noch eine Ebene vorhanden ist. Entscheidend ist, daß im ursprünglichen Bild mehrere Ebenen vorhanden waren.
Die Prozedur gimp-image-merge-visible-layers führt mehrere Ebenen eines Bildes auf eine Ebene zusammen (Zeile 31). Der erste Parameter der Prozedur ist wieder ein Handle auf das zu behandelnde Bild, der zweite Parameter bestimmt die Canvas-Änderung und Clipping-Optionen für die resultierende Ebene. Der im Beispiel gewählte Wert EXPAND-AS-NECESSARY bedeutet, daß die resultierende Ebene so groß sein sein soll, daß alle im Moment der Verschmelzung sichtbaren Ebenen darin Platz finden.
Der nächste Abschnitt (Zeilen 39 bis 48) dient der Berechnung der Breite und Höhe des skalierten Bildes. Hierzu wird das eingangs der Prozedur berechnete Seitenverhältnis des Original-Bildes verwendet, welches in der Variablen AspectRatio gespeichert ist; die Berechnungsmethodik ist selbsterklärend. Die beiden verwendeten mathematischen Prozeduren dagegen bedürfen einer Erklärung:
round rundet seinen einzigen Parameter gegen die nächstliegende Ganzzahl und liefert Letztere als Rückgabewert; im Gegensatz zu kaufmännischen Gepflogenheiten, aber in Übereinstimmung mit der Standard-Rundungsmethode in den einschlägigen IEEE-Spezifikationen [Link] wird gegen die gerade Ganzzahl gerundet, falls der zu rundende Wert genau in der Mitte zwischen zwei Ganzzahlen liegt. Die Rundung von SizeLongestSide (Zeilen 41, 46) ist nur nötig, falls die gewünschte Länge der längeren Seite keine Ganzzahl ist.
Zur Erläuterung der Prozedur inexact->exact muß etwas weiter ausgeholt werden. Die Menge von Zahlenwerten, die in der Hardware oder Software eines Rechners repräsentiert werden können, ist begrenzt. Arbeitet die Maschine zum Beispiel mit 32 Bit breiten Registern oder Speicherstellen, so kann jedes Register ungefähr 4 Milliarden verschiedene Werte repräsentieren, unabhängig von der konkreten Codierung. Deshalb können nicht alle Zahlen exakt abgebildet werden; so ist beispielsweise das Ergebnis des Befehls "Bilde die Wurzel aus 2" in jeder Programmiersprache und auf jeder Hardware prinzipiell nur eine Näherung an den exakten Wert [Link].
Für viele Programmiersprachen und Hardwarearchitekturen existieren Bibliotheken, die Berechnungen mit beliebiger Genauigkeit ermöglichen [Link]. Auch diese lösen das Problem nicht grundsätzlich, da hier die Genauigkeit zumindest durch den verfügbaren Hauptspeicher begrenzt wird. Scheme kann ebenfalls keine grundsätzliche Lösung des Problems bieten, stellt aber mächtige Hilfsmittel zur Verfügung, um mit den Auswirkungen bestmöglich umzugehen.
Deren wichtigstes ist das Konzept der Exaktheit: Jede Variable, die eine Zahl enthält, kann mittels Prozeduren wie exact?, inexact? und anderer daraufhin überprüft werden, ob der enthaltene Wert genau ist oder eben ungenau, also lediglich eine Näherung an den eigentlich gemeinten Wert, der nicht repräsentiert werden kann und nirgends (mehr) vorliegt. Darüber hinaus behält Scheme zumindest für Zahlen, die in exakter Form eingegeben oder berechnet wurden, die Exaktheit so lange wie möglich bei, und es existiert ein ausgefeiltes System von Vererbungsregeln, die bestimmen, unter welchen Umständen eine Variable ihre Exaktheit verliert oder gewinnt. Ferner gibt es Prozeduren, die explizit zwischen exakter und unexakter Repräsentation konvertieren [Link].
Weitere Ausführungen zu dieser interessanten Materie würden den Rahmen dieses Artikels bei weitem sprengen; es gibt jedoch gute Literatur hierzu [Link]. In unserem Beispiel verwenden wir die Prozedur inexact->exact, um die betreffenden in Variablen gespeicherten Zahlenwerte in die exakte Repräsentation zu überführen, bevor sie an die von Gimp zur Verfügung gestellten Prozeduren übergeben werden. Zugegebenermaßen geschieht dies hier eher, um einen Grund für die Ausführungen der letzten Abschnitte zu gewinnen, als um dramatische Fehlfunktionen zu verhindern.
Die Leser, die bis hierher gefolgt sind, könnten sich fragen, warum die Exaktheit des Inhalts der als Parameter übergebenen Variablen nicht auch beim Aufruf der anderen Prozeduren explizit sichergestellt wurde. Der Einwand ist berechtigt, denn auch Ganzzahlen können in Scheme unexakt repräsentiert werden (und tatsächlich akzeptiert inexact->exact sogar ausschließlich Ganzzahlen als Parameter). Die von Gimp zur Verfügung gestellten Prozeduren, die Ganzzahlen zurückgeben (unter anderem auch Handles auf Bilder oder Ebenen), tun dies jedoch stets in exakter Repräsentation. Auch direkt im Quellcode exakt eingegebene Zahlen (beispielsweise bei der Addition von 1, Zeile 20) werden vom Interpreter normalerweise exakt repräsentiert [Link].
Der Codeblock zur Berechnung der Breite und Höhe des skalierten Bildes ist nun der einzige Ort in unserem Beispiel, an dem ohne Zutun Unexaktheit entstehen könnte. Zwar führt die Division von Ganzzahlen stets zu rationalen Ergebnissen, und diese werden der reinen Lehre nach in Scheme so lange wie möglich exakt repräsentiert; viele Interpreter führen in zukünftigen Rechenoperationen so lange wie möglich Zähler und Nenner des Divisionsausdrucks getrennt mit, anstatt den Ausdruck zu berechnen und nur das Ergebnis mitzuführen [Link].
TinyScheme scheint an dieser Stelle aber unsauber implementiert: (exact? (/ 1 2)) liefert in TinyScheme #f, (/ 1 2) wird dort also unexakt repräsentiert. In DrScheme 4.2.4, eingestellt auf R5RS, liefert derselbe Ausdruck #t, (/ 1 2) wird dort also wie vermutet exakt repräsentiert. Diese Überlegungen sind für das Beispiel durchaus relevant, weil der Code Divisionen verwendet (Zeile 42) und der Wert der Variablen AspectRatio (Zeilen 42, 45) seinerseits durch Division errechnet wurde (Zeile 10).
Abschließend muß das Bild skaliert und unter neuem Namen im entsprechenden Format gespeichert werden (Zeilen 50, 51), und die belegten Ressourcen sind freizugeben (Zeile 52).
gimp-image-scale-full (Zeile 50) arbeitet wie erwartet; die Parameter sind selbsterklärend.
Interessanter ist die Prozedur gimp-file-save (Zeile 51): deren erste beide Parameter erklären sich zwar wieder von selbst, und für den vierten und fünften Parameter gilt das bei der Erläuterung von gimp-file-load Gesagte. Als dritten Parameter jedoch erwartet gimp-file-save ein "Drawable", welches das eigentlich zu speichernde Objekt darstellt; die Dokumentation läßt sich zu diesem Begriff leider nicht weiter aus. In unserem Fall kann die aktive Ebene dafür verwendet werden. gimp-image-get-active-layer gibt ein Handle auf genau diese zurück, wie immer als Element einer Liste, aus der es zunächst entnommen werden muß. Das beim Speichern gewünschte Dateiformat leitet gimp-file-save aus der Endung des übergebenen Dateinamens ab.
gimp-image-delete (Zeile 52) schließlich erwartet als Parameter ein Handle auf ein geöffnetes Bild, um dieses dann zu schließen und alle damit verbundenen Ressourcen (beispielsweise Hauptspeicher, temporäre Dateien auf der Festplatte, Dateisperren) freizugeben.
Damit sind alle Bestandteile des Scripts erläutert. Das Script kann der Bequemlichkeit halber komplett heruntergeladen werden [Download].
Unser kleiner Kochkurs, der sich hauptsächlich den Zutaten Scheme und Gimp gewidmet hat, ist fast beendet. Die Zubereitung des Desserts überlassen wir unseren Lesern; das Script bietet genügend Raum für Experimente und Verbesserungen (die in unserem Fall nicht gewünscht waren und deshalb nicht implementiert wurden). Einige Vorschläge:
- Finden Sie heraus, was passiert, wenn Sie das Script auf JPEG-Dateien operieren lassen (also als Dateifilter beispielsweise c:\test\*.jpg) angeben. Korrigieren Sie das Verhalten, falls es unerwünscht ist.
- Erweitern Sie die Bedienoberfläche und das Script so, daß das Dateiformat der erzeugten skalierten Bilder nicht grundsätzlich JPEG ist, sondern festgelegt werden kann.
- Sparen Sie Variablen im Programmcode, oder erhöhen Sie die Lesbarkeit durch Einführung zusätzlicher Variablen.
- Ersetzen Sie die von uns verwendeten, aber in der Dokumentation von TinyScheme als veraltet gekennzeichneten Prozeduren durch ihre Nachfolger, und falls solche nicht existieren, durch selbst implementierte Prozeduren.
- Fügen Sie, wie oben diskutiert, beim Zugriff von car auf die Liste mit den Dateinamen die Überprüfung auf die leere Liste hinzu.
Zuletzt noch ein Tip zur Entwicklung eigener Gerichte:
Das bequemste Mittel, die von Gimp und TinyScheme zur Verfügung gestellten Prozeduren zu erforschen, ist der in Gimp integrierte Prozedurbrowser (in Gimp: Menü Filter -> Skript-Fu -> Konsole, dann den Button "Durchsuchen..." betätigen). Wer sich damit befaßt, erkennt schnell die sinnvolle Systematik hinter der Benennung der Prozeduren; aus deren Namen läßt sich meist sofort die entsprechende Funktion von Gimp erkennen.
Im Prozedurbrowser tauchen auch die nach außen exportierten Funktionen der eigenen Scripte auf. Wie eingangs versprochen, sind denn dabei auch im rechten Fenster des Prozedurbrowsers die Metainformationen sichtbar, die beim Aufruf von script-fu-register angegeben worden waren.
Sie benötigen Hilfe bei der Erweiterung oder Automatisierung von GIMP oder bei der Entwicklung in Scheme, LISP oder anderen funktionalen Sprachen?
Dann nehmen Sie Kontakt mit uns auf. Wir nehmen uns gerne Zeit für eine ausführliche und kostenlose Erstberatung.