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▲
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 !