Écrire un plugin pour QImage

| Kent Hansen | traducteur : Thibaut Cuvelier | Qt Quarterly |

L'API d'E/S de Qt pour images fournit une interface de haut niveau pour la lecture et l'écriture d'images dans les formats populaires supportés par Qt, dont le JPEG, le PNG et le BMP. Mais que se passe-t-il si votre application a besoin d'un support d'autres formats, non supportés par Qt ?

Cet article pose cette intrigante question.
La première partie donne un aperçu du framework de Qt, en révélant ce qui se passe derrière la scène. Spécifiquement, nous montrons comment Qt peut supporter une telle multitude de formats d'image d'une manière extensible.
La seconde continue en montrant, à l'aide d'un exemple, comment vous pouvez participer au pouvoir de ce framework, et fournir une intégration avec vos propres formats d'image.

Cet article est une traduction autorisée de Writing a Qt Image Plugin, par Kent Hansen.

Commentez

- Titre : Écrire un plugin pour QImage

- Auteurs : Kent Hansen, traducteur : Thibaut Cuvelier, Qt Quarterly

- Parution : 22 février 2009

- Mise à jour : 1 août 2011

- Licence : Copyright ® 2006 Kent Hansen. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. L'article original

- II. Entrée / sortie de base

- III. Écrire une image

- IV. Fournir son propre plug-in

- V. Fichier de projet

- VI. Utiliser le plug-in

- VII. L'emballer

- VIII. Divers

I - L'article original

Qt Quarterly est une revue trimestrielle électronique proposée par Nokia à destination des développeurs et utilisateurs de Qt. Vous pouvez trouver les versions originales.

Nokia, Qt, Qt Quarterly et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.

Cet article est la traduction de l'article Writing a Qt Image Plugin de Kent Hansen paru dans la Qt Quarterly Issue 17.

Cet article est une traduction d'un des tutoriels écrits par Nokia Corporation and/or its subsidiary(-ies) inclus dans la documentation de Qt, en anglais. Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à Nokia.

II - Entrée / sortie de base

Au niveau de l'application, vous lisez une image dans un QImage en passant le nom de fichier à son constructeur.

QImage myimage(fileName);
if (myimage.isNull())
{
// S'occuper des erreurs de lecture
}

Vous pouvez enregistrer une image en appelant sa fonction save().

// Sauvegarder l'image modifiée dans le fichier
if (!myimage.save(fileName, format))
{
// S'occuper des erreurs de sauvegarde
}

Pour comprendre comment nous pouvons lire et écrire des images dans notre propre format de la même manière (de haut niveau), nous allons passer quelques temps à considérer ce qui se passe sous ces codes.

Les fonctions de QImage reposent sur les classes QImageReader et QImageWriter pour fournir des méthodes d'écriture et de lecture. Leur principale fonction est de déléguer l'appel au bon plug-in.

Un plug-in d'E/S d'images encapsule les détails de bas niveau de l'écriture et de la lecture d'images dans un format particulier. Ce plug-in est un dérivé de l'interface QImageIOHandler, et implémente les fonctionsvirtuelles que QImageReader et QImageWriter vont utiliser pour la lecture et l'écriture.

Ce genre de plug-in rend disponible un QImageIOHandler à Qt lors de l'exécution. Le plug-in, qui est un dérivé de QImageIOPlugin, fournit deux services de base.
Premièrement, on peut demander au plug-in s'il possède les fonctions nécessaires pour effectuer l'opération demandée.
Secondement, il agit comme une usine, pour que Qt puisse obtenir une instance des classes dont il a besoin.

Cette illustration montre le schéma de base quand une application charge une image d'un fichier.

Cette application construit simplement un QImage, en passant simplement le nom de fichier comme paramètredu constructeur. En réponse, le QImage demande au QImageReader de lire l'image. Le constructeurprimaire de QImageReader prend un QIODevice en argument, ce qui signifie que tout dérivé de QIODevice peut être utilisé, de QFile à QTcpSocket, en passant par les périphériques personnalisés. Dans le cas étudié, un QFile est construit à partir du nom de fichier donné.

Optionnellement, une chaîne de caractères peut être passée au constructeur de QImage. Comme le nom de fichier, cette chaîne est passée au QImageReader. En l'absence de cette chaîne de formatage, on attend que le plug-in jette un coup d'oeil au contenu du QIODevice (comme un en-tête), et détecte automatiquement le format.

Le QImageReader demande à chaque plug-in ses capacités de lecture. Si aucun ne peut s'occuper de l'image, elle est lue par le lecteur par défaut, s'il y en a un qui existe pour un format donné (non montré sur l'illustration). Finalement, le QImageReader appelle read(), qui effectue la vraie lecture de l'image.

III - Écrire une image

Écrire une image à l'aide de QImageWriter est similaire à la lecture. La principale différence est que la chaîne indiquant le format est obligatoire, parce que l'autodétection du format depuis le périphérique n'est plus possible. De plus, vous pouvez passer de nombreuses options, si elles sont supportées, comme le niveau de compression.

IV - Fournir son propre plug-in

En dérivant de QImageIOPlugin et de QImageIOHandler, vous pouvez donner à Qt accès à votre propre format d'images, et permettre aux applications Qt d'accéder à ce format comme tous les autres (par exemple, en passant un nom de fichier au constructeur de QImage). Dans cette section, nous allons y regarder de plus près.

Considérons un plug-in d'exemple pour un format d'images ARVB très simple et non compressé. Le format lui-même a été inventé pour le bien de cet exemple. Dans ce format ingénieux, les pixels sont représentés comme des entiers non signés de 32 bits, 8 bits pour chaque canal : le rouge, le vert, le bleu et l'alpha. Ils sont précédés par un petit (12 bits) en-tête contenant le nombre magique 0xCAFE1234, suivi par la largeur et la hauteur de l'image (chacun codé sur un entier non signé de 32 bits).

La classe ArgbPlugin est un dérivé de QImageIOPlugin, et expose la manière de s'occuper de l'image par quelques fonctions standard.

class ArgbPlugin : public QImageIOplug-in
{
public:
ArgbPlugin();
~ArgbPlugin();

QStringList            keys() const;
Capabilities        capabilities(QIODevice *device,
const QByteArray &format) const;
QImageIOHandler    *    create(QIODevice *device,
const QByteArray &format = QByteArray()) const;
};

Il y a trois fonctions qui doivent être réimplémentées dans notre classe. La première, keys(), retourne une liste des formats reconnus par le plug-in. Nous avons choisi d'utiliser la plutôt générique extension .raw pour le format d'image, ce qui fait que la fonction ressemble à ceci :

QStringList ArgbPlugin::keys() const
{
return QStringList() << "raw";
}

Cette fonction est utilisée par QImage::supportedImageFormats et QImageWriter::supportedImageFormats pour consruire la liste des formats que Qt peut alors utiliser.

La fonction capabilities détermine les capacités de lecture et d'écriture du plug-in, basé sur un périphérique d'E/S donné ou une chaîne de format.

QImageIOplug-in::Capabilities ArgbPlugin::capabilities(
QIODevice *device, const QByteArray &format) const
{
if (format == "raw")
return Capabilities(CanRead | CanWrite);
if (!(format.isEmpty() && device->isOpen()))
return 0;

Capabilities cap;
if (device->isReadable() && ArgbHandler::canRead(device))
cap |= CanRead;
if (device->isWritable())
cap |= CanWrite;
return cap;
}

Notez que, quand aucune chaîne de format n'est donnée, le plug-in appelle la fonction statique canRead() de ArgbHandler (voir ci-dessous) pour déterminer si le contenu indique la présence d'une image sans compression ARVB.

La troisième et dernière fonction de ArgbPlugin crée une instance du traducteur ArgbHandler.

QImageIOHandler *ArgbPlugin::create(
QIODevice *device, const QByteArray &format) const
{
QImageIOHandler *handler = new ArgbHandler;
handler->setDevice(device);
handler->setFormat(format);
return handler;
}

C'est tout pour ArgbPlugin. Comme nous pouvons le voir, dériver de QImageIOPlugin est très simple et très rapide. Même pour les formats complexes, le code ne doit pas, probablement, faire plus que cet exemple, parce que le processus spécifique au format est dans la classe de handling.

Le dérivé de QImageIOHandler, ArgbHandler, réalise les E/S spécifiques au format d'image.

class ArgbHandler : public QImageIOHandler
{
public:
ArgbHandler();
~ArgbHandler();

bool canRead() const;
bool read(QImage *image);
bool write(const QImage &image);

QByteArray name() const;

static bool canRead(QIODevice *device);

QVariant option(ImageOption option) const;
void setOption(ImageOption option, const QVariant &value);
bool supportsOption(ImageOption option) const;
};

La première fonction intéressante est la statique canRead(), qui vérifie que l'image peut être lue. Elle vérifier simplement que les données d'entrée commencent par le nombre magique 0xCAFE1234.

bool ArgbHandler::canRead(QIODevice *device)
{
return device->peek(4) == "\xCA\xFE\x12\x34";
}

Ici, nous nous reposons sur QIODevice::peek, pour lire le contenu du périphérique sans effet de côté. À l'inverse de QIODevice::read, peek() ne consomme pas les données, en les laissant dans le même état qu'auparavant.

Le processus de lecture réside dans la fonction read().

bool ArgbHandler::read(QImage *image)
{
QDataStream input(device());
quint32 magic, width, height;
input >> magic >> width >> height;
if (input.status() != QDataStream::Ok || magic != 0xCAFE1234)
return false;

QImage result(width, height, QImage::Format_ARGB32);
for (quint32 y = 0; y < height; ++y) {
QRgb *scanLine = (QRgb *)result.scanLine(y);
for (quint32 x = 0; x < width; ++x)
input >> scanLine[x];
}
if (input.status() == QDataStream::Ok)
*image = result;
return input.status() == QDataStream::Ok;
}

Nous utilisons QDataStream pour décompacter les valeurs de chaque pixel depuis le QIODevice. Après la lecture de l'en-tête de l'image, et la vérification du nombre magique, nous construisons un QImage de la bonne taille et du bon format. Ensuite, nous lisons les pixels un à un.

Finalement, nous vérifions que le flux de données est correct (qu'il n'y a pas eu d'erreurs lors de la lecture des pixels. Si tout s'est bien passé, nous stockons le résultat et retournons le succès de l'opération. Sinon, nous indiquons l'échec.

La réimplémentation de QImageIOHandler::write effectue l'opération inverse.

bool ArgbHandler::write(const QImage &image)
{
QImage result = image.convertToFormat(QImage::Format_ARGB32);
QDataStream output(device());
quint32 magic = 0xCAFE1234;
quint32 width = result.width();
quint32 height = result.height();
output << magic << width << height;
for (quint32 y = 0; y < height; ++y) {
QRgb *scanLine = (QRgb *)result.scanLine(y);
for (quint32 x = 0; x < width; ++x)
output << scanLine[x];
}
return output.status() == QDataStream::Ok;
}

En plus de la lecture et de l'écriture dans ce format, ArgbHandler support l'option QImageIOHandler::Size, pour que la taille d'une image ARVB puisse être demandée sans avoir lu toute l'image. Pour ce faire, nous réimplémentons supportsOption() et option() comme suit.

bool ArgbHandler::supportsOption(ImageOption option) const
{
return option == Size;
}

QVariant ArgbHandler::option(ImageOption option) const
{
if (option == Size)
{
QByteArray bytes = device()->peek(12);
QDataStream input(bytes);
quint32 magic, width, height;
input >> magic >> width >> height;
if (input.status() == QDataStream::Ok && magic == 0xCAFE1234)
return QSize(width, height);
}
return QVariant();
}

Basiquement, au lieu de créer un QDataStream qui opère sur le QIODevice lui-même (comme nous avons fait dans ArgbHandler::read(), nous cherchons uniquement l'en-tête de 12 bits et créons un QDataStream qui opère sur le résultant QByteArray. De cette manière, nous pouvez lire l'en-tête et extraire la taille de l'image sans perturber l'état du QIODevice.

V - Fichier de projet

Le fichier de projet (.pro) pour le plug-in ressemble à ceci.

TARGET  = argb
TEMPLATE = lib
CONFIG = qt plug-in
VERSION = 1.0.0

HEADERS = argbhandler.h
SOURCES = argbplugin.cpp argbhandler.cpp

target.path += $$[QT_INSTALL_plug-inS]/imageformats
INSTALLS += target

Notez l'utilisation du modèle lib, pour que le plug-in soit compilé comme librairie, et que CONFIG contient plug-in, pour s'assurer qu'il s'agira bien d'un plug-in Qt après la compilation.

VI - Utiliser le plug-in

Avec le projet montré ci-dessus, make install installera le plug-in dans le répertoire des plug-ins de Qt, en rendant ce plug-in disponible à toutes les applications Qt.

Si le plug-in se trouve autre part (par exemple, si le plug-in est dans la distribution de sources de votre application), vous devrez changer la cible dans le fichier de projet, et votre application devra appeler QCoreApplication::addLibraryPath avec le chemin de votre répertoire des plug-ins.

Par exemple, si le plug-in est situé dans some_path/plug-ins/imageformats, un application qui veut l'utiliser doit inclure cette ligne.

QCoreApplication::addLibraryPath("some_path/plug-ins");

Avec le plug-in en place et les chemins bien installés, une application peut maintenant lire et écrire des fichiers ARVB non compressés avec les fonctions habituelles de QImage.

VII - L'emballer

Alors que l'exemple montré est très basique, il illustre bien ce que doit faire tout plug-in pour QImage :

- Essayer de détecter le format d'image en vérifiant la présence d'un en-tête spécifique au format.

- Créer un propre QImage basé sur les attributs de l'image trouvés dans l'en-tête (comme la taille et le format).

- S'occuper de données d'une manière indépendante de la plateforme.

- Manipuler l'image au niveau des pixels.

- Retourner les attributs de l'image (comme la taille) sans lire toute l'image.

En ce qui concerne les implémentations de référence de plug-ins non triviaux, regardez les plug-ins qui font partie de la distribution de Qt. On peut les trouver dans le répertoire src/plug-ins/imageformats. Ceux-là sont d'un intérêt particulier dans la jungle actuelle de haute complexité des formats d'image comme le JPEG et le MNG, qui montrent comment un plug-in d'image peut encapsuler des librairies externes, comme libjpeg et libmng.

VIII - Divers

Les fichiers source

Un tout grand merci à matrix788 pour sa relecture approfondie !

Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à Nokia pour nous avoir autorisé la traduction de cet article !