Aus Linux-Magazin 06/2006

Bildbearbeitungs-Plugins für KDE entwickeln

© photocase.com

Kleine GUI-Anwendungen eignen sich gut als Einstieg für Programmierer. Dieser Artikel zeigt, wie ein Wasserzeichen-Plugin für das KDE Imaging Plugin Interface entsteht.

Jede Anwendung ist eine Welt für sich. Dieser Satz gilt auch für die meisten Open-Source-Programme. Denn obwohl es logisch scheint, die Vorzüge von Programm A mit denen von Programm B zu kombinieren, bleiben Fusionen trotz Open Source und Basar-Theorie eher die Ausnahme. Eine davon ist die gemeinsame Plugin-Infrastruktur der KDE-Bildbearbeitungsprogramme Kphotoalbum alias Kimdaba [1], Digikam [2], Showimg [3] und Gwenview [4]. Dank der einheitlichen Schnittstelle arbeitet das KDE Imaging Plugin Interface (Kipi, [5]) unter allen vier KDE-Programmen. Kipi entlastet zudem die Hauptentwickler dieser Programme, da mit grundlegenden Kenntnissen in C++ jeder Programmierer Kipi-Plugins entwickeln kann.

Neues Kipi-Plugin

Der Artikel zeigt an einem Beispiel, welche Schritte zu einem neuen Kipi-Plugin führen. Das Plugin soll Bilder mit einem Wasserzeichen-Text versehen (Abbildung 1). Die Codeschnipsel und der Sourcecode des fertigen Plugins finden sich auf der Listing-Seite des Linux-Magazins [6] zum Download. Um das Plugin zu kompilieren, sind neben KDE und »libkipi« auch die zugehörigen Entwicklerpakete erforderlich.

Ein Kipi-Plugin entsteht aus folgenden Arbeitsschritten:

Abbildung 1: Das unfertige Beispiel-Plugin funktioniert bereits. Hier tippt der Benutzer den Text des Wasserzeichens ein und ändert die Schriftgröße.

Abbildung 1: Das unfertige Beispiel-Plugin funktioniert bereits. Hier tippt der Benutzer den Text des Wasserzeichens ein und ändert die Schriftgröße.

  • Das Plugin von der Klasse »KIPI::Plugin« ableiten
    und einige virtuelle Methoden überschreiben. Von dieser Klasse
    erzeugt die Host-Anwendung (zum Beispiel Gwenview) eine Instanz und
    kommuniziert damit.
  • Informationen von der Host-Anwendung anfordern. Es existieren
    bereits einige Klassen, die Informationen zum aktuellen Fotoalbum,
    den gerade ausgewählten Bildern und zum Beispiel zu
    Schlüsselwörtern für ein bestimmtes Bild
    übermitteln.
  • Zusammenarbeit mit dem Framework herstellen. Damit sie
    funktioniert, muss der Quellcode ein magisches Makro enthalten.
    Außerdem benötigt er ein »Makefile.am«, eine
    ».desktop«-Datei und einige weitere
    Framework-Dateien.

Schließlich muss der Programmierer noch den Code des eigentlichen Plugin schreiben.

Plugin-Klasse

Um ein Kipi-Plugin zu entwickeln, muss es der Entwickler von der Klasse »KIPI::Plugin« ableiten, die sich in der Include-Datei »libkipi/plugin.h« befindet. Listing 1 zeigt diese Klasse in einer gekürzten Fassung. Um ihre Funktionsweise zu verstehen, ist es wichtig, zu wissen, wie Host-Anwendungen Plugins laden. Mit Hilfe einiger KDE-internen Mechanismen findet die Anwendung sämtliche für sie relevante Plugins. KDE lädt die Plugins dynamisch, das heißt, es legt eine Instanz des Plugin an und übermittelt diese an die Host-Anwendung. Die Details dieses Vorgangs sind unter [7] näher beschrieben.

Listing 1:
»KIPI::Plugin«

01 class Plugin : public QObject
02 {
03 public:
04     Plugin( KInstance* instance, QObject *parent, const char* name);
05     virtual void setup( QWidget* widget ) = 0;
06     KActionPtrList actions( QWidget* parent = 0 );
07     KActionCollection* actionCollection( QWidget* parent = 0 );
08     virtual Category category( KAction* action ) const = 0;
09 
10 protected:
11     void addAction( KAction* action );
12 }

Die Host-Anwendung ruft bei der Plugin-Instanz die »setup()«-Methode auf. Deren Aufgabe ist es, die nötigen »KAction«-Instanzen anzulegen. Interessierte Host-Anwendungen spiegeln die Actions in ihren Menüs ein – wo, das entscheidet die Anwendung, nicht das Plugin

Das Plugin muss von der Host-Anwendung erfahren, um welche Art von Erweiterung es sich handelt. Diese Informationen ermittelt die Host-Anwendung mit der »category()«-Methode. Damit der Ablauf reibungslos funktioniert, ist es nötig, dass das Plugin über »Plugin::actionCollection()« die »action«-Informationen der Host-Anwendungen sammelt und später für jede Aktion »Plugin::addAction()« aufruft. Dabei gilt es, zwei häufig Fehlerquellen zu vermeiden:

  • Die »Plugin::actions()«-Methoden sind nicht
    für das Plugin relevant, sondern für die Host-Anwendung.
    Das API der Klasse betrifft hingegen das Plugin und die
    Host-Anwendung.
  • Die explizite »setup()«-Methode ist nötig,
    weil das Plugin einerseits keine Kontrolle über die aktuellen
    Parameter des Konstruktors hat – den legt das
    KDE-Plugin-Framework automatisch an. Andererseits kann die
    Host-Anwendung das Plugin mehrfach einsetzen, zum Beispiel im
    Hauptmenü und im Kontextmenü.

Der Code benötigt deshalb mehrere »KAction«-Instanzen und ruft die »setup()«-Methode mehrmals mit verschiedenen Widgets als Argument auf. Listing 3 zeigt die vollständige Implementation der Plugin-Unterklasse.

Die Zeilen 1 und 2 binden das KDE-Plugin-Framework ein. »WatermarkPlugin« bezeichnet die Plugin-Klasse. Wer ein eigenes Plugin schreibt, muss diese durch die Klasse des Plugin ersetzen. Der Eintrag »kipiplugin_watermark« (Zeilen 6 und 7) steht für den Namen der zugehörigen ».desktop«-Datei.

Kommunikation

Der bisherige Code legt ein Plugin an, das die Host-Anwendung beim Start lädt und über »KAction«-Elemente in ihre Menüs einfügt. Über die Menü-Einträge startet der Benutzer später das Plugin. In einem nächsten Schritt muss sich das Plugin Informationen über die vorhandenen Bilder beschaffen. Dazu benötigt es Instanzen aus der Klasse »KIPI::Interface«. Die übergeordnete Klasse des Plugin ist eine dieser benötigten Instanzen, es genügt also eine statische Typumwandlung, wie sie im Listing 3 in Zeile 17 stattfindet.

Um die eigentlichen virtuellen Methoden braucht sich der Programmierer nicht zu kümmern, da das Plugin keine Instanzen dieser Klasse benötigt, sondern nur einzelne Zeiger festlegt und sie benutzt. Dies ist ein typischer Fall, bei dem eine Klasse zwei Programme bedient: das Plugin und die Host-Anwendung.

Listing 2:
»KIPI::Interface«

01 class Interface : public QObject
02 {
03 public:
04     virtual ImageCollection currentAlbum() = 0;
05     virtual ImageCollection currentSelection() = 0;
06     virtual QValueList<ImageCollection> allAlbums() = 0;
07 
08     virtual ImageInfo info( const KURL& ) = 0;
09     virtual bool addImage( const KURL&, QString& err );
10     virtual void delImage( const KURL& );
11 
12     virtual void refreshImages( const KURL::List& );
13 
14     virtual QString fileExtensions();
15 
16     bool hasFeature( KIPI::Features feature );
17 
18 signals:
19     void selectionChanged( bool hasSelection );
20     void currentAlbumChanged( bool anyAlbum );
21 };

Listing 3:
»plugin_watermark.cpp«

01 #include "plugin_watermark.h"
02 #include "watermark_dialog.h"
03 #include <kgenericfactory.h>
04 #include <libkipi/imageinfo.h>
05 typedef KGenericFactory<WatermarkPlugin> Factory;
06 K_EXPORT_COMPONENT_FACTORY( kipiplugin_watermark,
07                             Factory("kipiplugin_watermark"))
08 WatermarkPlugin::WatermarkPlugin( QObject *parent, const char* name,
09                               const QStringList& )
10     :KIPI::Plugin::Plugin( Factory::instance(), parent, name )
11 {
12     kdDebug( 51001 ) << "Loaded WatermarkPlugin" << endl;
13 }
14 void WatermarkPlugin::setup( QWidget* widget )
15 {
16     KIPI::Plugin::setup( widget );
17     KIPI::Interface* interface = static_cast<KIPI::Interface*>( parent() );
18     m_dialog = new WatermarkDialog( interface, 0, "watermark plugin dialog" );
19     m_dialog->resize( 800, 600 );
20     m_imageInfo = new KAction( i18n( "Watermarks" ), "",
21                          0, this, SLOT( action() ), actionCollection(),
22                                "watermark" );
23     addAction( m_imageInfo );
24     connect( interface, SIGNAL( currentAlbumChanged( bool ) ),
25              m_dialog, SLOT( reload() ) );
26     connect( interface, SIGNAL( selectionChanged( bool ) ),
27              m_dialog, SLOT( reload() ) );
28 }
29 KIPI::Category WatermarkPlugin::category( KAction* ) const
30 {
31     return KIPI::IMAGESPLUGIN;
32 }
33 void WatermarkPlugin::action()
34 {
35     m_dialog->reload();
36     m_dialog->show();
37 }

Bilder sammeln

Listing 2 zeigt die wesentlichen Ausschnitte der Klasse »KIPI::Interface«.Kipi bietet drei Wege, bei der Host-Anwendung Bilder abzufragen: die aktuelle Auswahl, das aktuelle Album und sämtliche Alben (Listing 2, Zeilen 4 bis 6). Mit Ausnahme von Kphotoalbum sortieren die eingangs erwähnten KDE-Programme Fotos in Alben. Kphotoalbum verwendet einen ähnlichen Mechanismus, sodass das Plugin auch mit ihm zusammenarbeitet. In Kphotoalbum kann »allAlbums()« jedoch dasselbe Bild mehrmals aufrufen. Es ist deshalb eine gute Idee, bereits beim Schreiben des Plugin darauf zu achten, doppelte Bilder zu eliminieren.

Listing 4 zeigt eine gekürzte Version der Klasse »ImageCollection«. Die vom Interface angemeldete »ImageCollection«-Klasse ist eine Instanz, kein Zeiger. Eine Instanz muss immer angemeldet sein, auch wenn kein Rückgabewert existiert. Beim Abfragen einer Instanz sollte man deshalb immer darauf achten, ob sie auch gültig ist. Dazu dient die Methode »isValid()«. Eine ungültige »ImageCollection« tritt zum Beispiel ein, wenn das Plugin das aktuelle Album abfragt, obwohl kein Album ausgewählt ist.

Listing 4:
»ImageCollection«

01 class ImageCollection
02 {
03 public:
04     bool isValid() const;
05     KURL::List images() const;
06     QString name() const;
07     QString comment() const;
08     QString category() const;
09     QDate date() const;
10     bool isDirectory() const;
11     KURL path() const;
12     KURL uploadPath() const;
13     KURL uploadRoot() const;
14     QString uploadRootName() const;
15 }

Die »images()«-Methode erkundigt sich nach den Bildern einer »ImageCollection«. Als Rückgabewert liefert sie die Bilder des Albums. Zusätzliche Informationen kann das Plugin mit den Methoden »name()«, »comment()«, »category()« und »date()« abfragen. Sie funktionieren jedoch nicht bei allen vier KDE-Anwendungen. Die benutzbaren Methoden lassen sich deshalb über »Interface::hasFeature()« zunächst abfragen.

Listing 5: Bilder
abfragen

01 KURL::List WatermarkDialog::images()
02 {
03     KIPI::ImageCollection collection = m_interface->currentSelection();
04     if  (!collection.isValid() )
05         collection = m_interface->currentAlbum();
06     if ( !collection.isValid() )
07         return KURL::List();
08 
09     return collection.images();
10 }

Bilder auswerten

Die Kipi-Plugins werten jedes Bild über eine URL aus. Dabei unterstützt Gwenview als einzige Anwendung auch Netzwerk-URLs. Die anderen Programme arbeiten mit lokalen URIs (»file:/«). Informationen über ein Bild verschafft sich das Plugin mit der Methode »Interface::info()«. Sie erwartet eine URL als Parameter und liefert als Rückgabewert eine Instanz der Klasse »ImageInfo«, wie Listing 6 zeigt.

Die »angle()«-Methode (Listing 6, Zeilen 16 bis 17) ist wiederum ein Ausnahmefall, um sämtlichen Programmen gerecht zu werden. Da Kphotoalbum keine Bilder verändert, sondern nur Metadaten zu den Bildern speichert, muss das Plugin diese Informationen auswerten, wenn es ein Bild anzeigt. Es liegt also in der Verantwortung des Plugin, das Bild beispielsweise zu drehen.

Listing 6: Bildinfos
auswerten

01 class ImageInfo
02 {
03 public:
04     QString title() const;
05     void setTitle( const QString& name );
06     QString description() const;
07     void setDescription( const QString& description);
08     QMap<QString,QVariant> attributes() const;
09     void clearAttributes();
10     void addAttributes( const QMap<QString,QVariant>& );
11     KURL path() const;
12     QDateTime time( TimeSpec spec = FromInfo ) const;
13     void setTime( const QDateTime& time, TimeSpec spec = FromInfo );
14     bool isTimeExact() const;
15     int size() const;
16     int angle() const;
17     void setAngle( int );
18     void cloneData( const ImageInfo& other );
19 };

Plugin schreiben

Obwohl das Plugin noch lange nicht vollständig ist – es funktioniert trotzdem schon. Es besteht aus einem Dialogfenster mit zwei Abschnitten (Abbildung 1), im oberen Teil tippt der Benutzer den Text des Wasserzeichens ein, im unteren Teil legt er fest, wohin das Plugin das veränderte Bild speichert.

Eine zentrale Aufgabe des Plugin besteht darin, die Liste der Bilder abzufragen (Listing 5). Dazu überprüft es zunächst, ob die Host-Anwendung gerade eine Auswahl anzeigt. Ist dies der Fall, arbeitet das Plugin mit ihr. Wenn nicht, benutzt es das aktuelle Album. Wichtig ist auch der Aufruf von »isValid()«, damit die Host-Anwendung keine falschen Werte bekommt.

Die nächste Funktion hilft ein Bild herunterzuladen und es bei Bedarf zu drehen (Listing 7). Da ein gutes Kipi-Plugin nicht nur mit lokalen URLs umgehen sollte, benutzt das Wasserzeichen-Plugin die Methode »KIO::NetAccess«, um das Bild zunächst als lokale Datei zwischenzuspeichern (Listing 7, Zeile 4). Dann dreht es das Bild, falls die Host-Anwendung Informationen zum Winkel zurückgibt (Zeilen 9 bis 13). Der Winkel hat nichts mit den EXIF-Informationen zu tun, sondern gibt nur die vom Benutzer geforderte Drehung an.

Listing 7: Bild holen und
drehen

01 QPixmap WatermarkDialog::pixmapForURL( const KURL& url )
02 {
03     QString tmpFile;
04     if ( KIO::NetAccess::download( url, tmpFile, this )) {
05         QImage img( tmpFile );
06         KIO::NetAccess::removeTempFile( tmpFile );
07 
08         KIPI::ImageInfo info = m_interface->info( url );
09         int angle = info.angle();
10         if ( angle != 0 ) {
11             QWMatrix matrix;
12             matrix.rotate( angle );
13             img = img.xForm( matrix );
14         }
15         return QPixmap(img);
16     }
17     return QPixmap();
18 }

Bilder speichern

Schließlich soll das Beispiel-Plugin das Bild wieder speichern (Listing 9). Die Zeilen 4 und 5 schreiben das Bild zunächst in eine temporären Datei. Dazu benutzen sie die »save()«-Funktion von »QPixmap« und »KImageIO«, um das Bild automatisch mit dem richtigen Dateinamen zu speichern. Der Destination-Reiter des Wasserzeichen-GUI bestimmt den neuen Speicherort der Grafik. Diese Aufgabe löst das Plugin einfach über das Widget »KIPI::UploadWidget«.

Der schwierige Teil besteht dabei darin, dass einige Anwendungen davon ausgehen, dass sich sämtliche Bilder im selben Hauptverzeichnis befinden. Das Plugin überprüft deshalb mit der »path()«-Methode des Upload-Widget die gemeinsame Wurzel von Ausgangs- und Zieldatei (Listing 9, Zeile 6). Anschließend sucht es über »KIO::NetAcess« einen passenden Dateinamen, der keine bestehenden Dateien überschreibt, und lädt das Bild an den Bestimmungsort.

Im letzten Schritt teilt das Plugin der Host-Anwendung mit, dass sich ein neues Bild in der Sammlung befindet. Dies geschieht in Zeile 19 über den Aufruf von »addImage()« bei der Instanz der Interface-Klasse.

Listing 8:
Desktop-Datei

01 [Desktop Entry]
02 Encoding=UTF-8
03 Name=Watermark Plugin
04 Name[de]=Wasserzeichen-Plugin
05 Comment=Watermark Plugin
06 Comment[de]=KIPI-Wasserzeichen-Plugin
07 Type=Service
08 ServiceTypes=KIPI/Plugin
09 X-KDE-Library=kipiplugin_watermark
10 Author=Jesper K. Pedersen, blackie@kde.org

Listing 9: Bild
speichern

01 void WatermarkDialog::savePixmap( const KURL& source, const QPixmap& pixmap )
02 {
03     QString ext = QFileInfo( source.fileName() ).extension(false);
04     KTempFile tmpFile;
05     pixmap.save( tmpFile.name(), KImageIO::type( source.fileName() ) );
06     KURL dest = m_uploadWidget->path();
07     KURL destFile = dest;
08     destFile.addPath( source.fileName() );
09     int count = 0;
10     while ( KIO::NetAccess::exists( destFile, false, this ) ) {
11         destFile = dest;
12         QString fileName = source.fileName();
13         fileName = QString( "%1-%2.%3" ).arg( QFileInfo( fileName ) .baseName() ).arg( count++ ).arg( ext );
14         destFile.addPath( fileName );
15     }
16     KIO::NetAccess::upload( tmpFile.name(), destFile, this );
17     tmpFile.unlink();
18     QString err;
19     if ( !m_interface->addImage( destFile, err ) ) {
20         KMessageBox::error( this, QString( "Unable to add file %1 to database. Error was %2" ).arg( destFile.url() ).arg( err ) );
21     }
22 }

Framework-Dateien

Um das Plugin komplett zu machen, fehlen noch einige Framework-Dateien. Zunächst benötigt der Quellcode ein »Makefile.am«. Als Vorlage eignet sich das Makefile eines vorhandenen Plugin mit angepasstem Plugin-Namen. Details zum »Makefile.am« beschreibt ein Howto für KDE-Entwickler [8].

Zudem benötigt das Plugin noch eine ».desktop«-Datei. Sie fungiert als zentrale Informationsstelle. Ein Beispiel zeigt Listing 8. Das Feld »ServiceTypes« identifiziert das Programm etwa als Kipi-Plugin, »Name« und »Comment« sind für die Host-Anwendungen wichtig. Durch den Landescode in eckigen Klammern benutzt die Host-Anwendung automatisch die passenden Übersetzungen.

Die Infrastruktur des Plugin ist damit komplett. Sie erlaubt es, ein eigenes Kipi-Plugin zu schreiben oder das Wasserzeichen-Plugin zu vervollständigen. Bei Fragen und Problemen hilft die KDE-Imaging-Mailingliste [9]. (mhi)

Infos

[1] Kphotoalbum: [http://ktown.kde.org/kphotoalbum]

[2] Digikam: [http://www.digikam.org]

[3] Showimg: [http://www.jalix.org/projects/showimg/]

[4] Gwenview: [http://gwenview.sf.net]

[5] Kipi: [http://extragear.kde.org/apps/kipi/]

[6] Listings: [https://www.linux-magazin.de/Service/Listings]

[7] Building a Plugin Structure for KDE applications: [http://developer.kde.org/documentation/tutorials/developing-a-plugin-structure/index.html]

[8] »Makefile.am«-Howto: [http://developer.kde.org/documentation/makefile_am_howto/en/]

[9] KDE Imaging mailing list: [https://mail.kde.org/mailman/listinfo/kde-imaging]

Der Autor


Jesper Pedersen ist Autor des KDE-Tools Kphotoalbum alias Kimdaba und hat bereits einige Kipi-Plugins entwickelt.

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