I. Introduction▲
I-A. DevIL▲
DevIL est une librairie cross-plateforme qui s'occupe de lire et d'écrire des images.
Elle essaye par sa structure de se rapprocher de OpenGL : trois parties la composent.
DevIL (ou IL), la partie centrale, des fonctions de très bas niveau, qui permettent la lecture de pixels.
DevILU (ou ILU), la partie des outils de la librairie, comme la lecture de plusieurs pixels.
DevILUT (ou ILUT), la boîte à outils, des fonctions de haut niveau qui permettent d'effectuer facilement des opérations complexes, comme la superposition d'images.
I-B. Les plugins Qt▲
Il existe deux types de plugins pour Qt : ceux de haut niveau, qui étendent les fonctionnalités de Qt ; et les bas niveau, qui étendent les fonctionnalités d'une application. Nous nous intéresserons à ceux de haut niveau, vu que nous voulons étendre les capacités de Qt.
Les plugins sont rangés dans un répertoire plugins. Celui-ci peut se situer à différents endroits : dans le répertoire de votre application, dans le répertoire de Qt (s'il est installé sur la machine), mais vous pouvez aussi désigner un ou plusieurs répertoires où les plugins sont rangés, à l'aide de la fonction statique setLibraryPaths(), ou bien utiliser un fichier de configuration qt.conf.
Avant de charger un plugin, Qt fait quelques recherches pour vérifier que le plugin peut être utilisé sans risque : il ne peut pas être compilé avec une version postérieure de Qt (pour éviter que des fonctions inexistantes soient utilisées), ni avec une version majeure antérieure (il existe d'énormes différences entre deux versions majeures de Qt), et, si présente, la buildkey doit être identique.
La buildkey est une clé qui identifie une compilation de Qt : deux compilateurs (ou deux versions) ne produisent pas le même code, et cela peut engendrer des problèmes ; deux configurations de Qt différentes peuvent produire des binaires incompatibles ; on peut préciser une buildkey lors de la configuration de Qt : cette chaîne est concaténée aux informations précédentes, pour éviter qu'un plugin compilé pour une application soit utilisé avec une autre.
Ce processus de validation peut être très coûteux en temps, c'est pourquoi un cache est établi. Ce cache se base sur la date de modification du fichier (ce qui réduit les comparaisons à une seule). Si le fichier est modifié après son entrée dans le cache, il est validé de nouveau.
II. Le fichier de projet .pro▲
TARGET = devil-qt
TEMPLATE = lib
CONFIG = qt plugin
VERSION = 1.0.0
HEADERS += devil-qt-plugin.hpp \
devil-qt-handler.hpp
SOURCES += devil-qt-plugin.cpp \
devil-qt-handler.cpp
target.path += $$[QT_INSTALL_PLUGINS]/imageformats
INSTALLS += target
Si vous ne comprenez pas ce code, jetez un coup d'œil à la documentation sur QMake et sur les projets .pro.
III. QImageIOPlugin▲
Cette classe définit une interface à une classe de lecture et d'écriture (le handler, littéralement, "celui qui s'en charge").
Cette classe ne définit pas beaucoup de fonctions à réimplémenter. C'est pourquoi cette partie du code reste toujours très simple. En effet, elle se charge d'indiquer à Qt tous les formats et options supportés par le handler.
III-A. L'en-tête▲
#include
<QImageIOHandler>
#include
"devil-qt-handler.hpp"
Une seule explication : le fichier devil-qt-handler.hpp est l'en-tête du handler, c'est lui qui proposera le prototype des fonctions de lecture et d'écriture.
III-B. Le corps▲
III-B-1. Le prototype▲
class
DevILPlugin : public
QImageIOPlugin
{
public
:
DevILPlugin();
~
DevILPlugin();
QStringList
keys ()
const
;
Capabilities capabilities (QIODevice
*
device, const
QByteArray
&
format)
const
;
QImageIOHandler
*
create (QIODevice
*
device, const
QByteArray
&
format =
QByteArray
())
const
;
}
;
Toutes les méthodes sont publiques : cette classe expose, et ne fait rien.
La fonction keys() renvoie toutes les chaînes de caractères qui servent d'extension aux formats (comme .bmp, .gif, .png, .jpg, .pcx ...). Peu importe si ces formats ne sont supportés qu'en éciture ou qu'en lecture, la fonction suivante se charge de l'indiquer.
La fonction capabilities() se charge d'indiquer à chaque extension de format la capacité de lecture ou d'écriture.
La fonction create() crée une instance du handler, qui sera utilisée par QImage lors de la lecture ou de l'écriture.
III-B-2. Constructeurs et destructeurs▲
Ce genre de classe n'a pas besoin de constructeur ni de destructeur : toutes ses fonctions n'ont besoin de rien, si ce n'est un endroit où parler, ce que fournit Qt.
III-B-3. keys()▲
QStringList
DevILPlugin::
keys() const
{
QStringList
supported;
supported <<
"act"
;
supported <<
"bmp"
;
/*
...
*/
supported <<
"tga"
;
supported <<
"vtf"
;
return
supported;
}
Toutes les extensions sont listées. À part ça, rien de nouveau sous le soleil.
III-B-4. capabilities()▲
QImageIOPlugin
::
Capabilities DevILPlugin::
capabilities
(QIODevice
*
device, const
QByteArray
&
format)
const
{
QImageIOPlugin
::
Capabilities cap;
if
(format.isEmpty()
||
!
( device->
isOpen() )
)
{
return
cap;
}
if
( format ==
"bmp"
/* ... */
||
format ==
"tga"
)
{
cap |=
CanRead;
cap |=
CanWrite;
return
cap;
}
else
if
(format ==
"act"
/*
...
*/
||
format ==
"vtf"
)
{
cap |=
CanRead;
return
cap;
}
else
{
return
cap;
}
}
S'il n'y a pas d'extension, ou bien que le fichier n'est pas ouvrable, on affirme que l'on ne peut rien en faire.
Si l'extension est dans la liste des supportées en écriture, on affirme qu'on sait, et la lire, et l'écrire;
Si l'extension est dans la liste des supportées en lecture uniquement, on affirme qu'on sait la lire.
Si l'extension n'est pas dans une liste, on affirme que l'on n'en rien peut faire.
III-B-5. create()▲
QImageIOHandler
*
DevILPlugin::
create(QIODevice
*
device, const
QByteArray
&
format)
const
{
QImageIOHandler
*
handler =
new
DevILHandler;
handler->
setDevice(device);
handler->
setFormat(format);
return
handler;
}
Le plugin crée une instance du handler.
III-B-6. Les macros de fin▲
Q_EXPORT_STATIC_PLUGIN
(DevILPlugin )
Q_EXPORT_PLUGIN2
(devil-
qt, DevILPlugin )
La première macro prend pour paramètre le nom de la classe de plugin.
La seconde prend comme premier paramètre le TARGET du .pro, et, comme second paramètre, le nom de la classe de plugin.
IV. QImageIOHandler▲
J'omets ici les en-têtes et la déclaration de la classe, vu qu'elles ne diffèrent que très peu par rapport à la partie précédente.
IV-A. Le constructeur▲
DevILHandler::
DevILHandler()
{
ilInit();
iluInit();
}
On initialise DevIL et ILU. Le dernier ne sert que pour la gestion des effets gamma.
IV-B. setFormat()▲
void
DevILHandler::
setFormat(ILenum _Format)
{
this
->
I_Format =
_Format;
}
void
DevILHandler::
setFormat(const
QByteArray
&
format)
{
if
(format ==
"cut"
)
this
->
I_Format =
IL_CUT;
else
if
(format ==
"dcx"
)
/*
...
*/
else
if
(format ==
"vst"
)
this
->
I_Format =
IL_TGA;
else
this
->
I_Format =
this
->
I_Format =
IL_TYPE_UNKNOWN;
}
Pourquoi deux implémentations ? La première permet d'utiliser directement un type DevIL. La seconde se base sur les extensions, le principe de base de Qt.
IV-C. setDevice()▲
void
DevILHandler::
setDevice(QIODevice
*
device)
{
this
->
Size =
device->
size ();
this
->
Lump =
(char
*
) malloc (Size);
ilLoadL (IL_TYPE_UNKNOWN, this
->
Lump, this
->
Size);
this
->
Width =
ilGetInteger (IL_IMAGE_WIDTH);
this
->
Height =
ilGetInteger (IL_IMAGE_HEIGHT);
this
->
Depth =
ilGetInteger (IL_IMAGE_DEPTH);
this
->
Size_wh =
QSize
(this
->
Width, this
->
Height);
this
->
Palette =
ilGetInteger (IL_PALETTE_BASE_TYPE);
this
->
NumImages =
ilGetInteger (IL_NUM_IMAGES);
this
->
DurImage =
ilGetInteger (IL_IMAGE_DURATION);
this
->
Gamma =
1.0
;
this
->
Data =
ilGetData();
//Détermination du QImage::Format depuis la palette de DevIL (tout en dur)
if
(this
->
Palette ==
IL_RGB)
this
->
Q_Format
=
QImage
::
Format_RGB32;
else
if
(this
->
Palette ==
IL_RGBA)
this
->
Q_Format
=
QImage
::
Format_ARGB32;
/*
...
*/
else
this
->
Q_Format
=
QImage
::
Format_Invalid;
}
On charge à partir du QIODevice tout ce dont nous avons besoin. Cette fonction est la seule qui agit avec ces QIODevice.
On récupère tous les paramètres qui nous seront importants pour la suite (dans l'ordre) : la taille (en octets), un lump, la hauteur, la largeur, la profondeur, la taille (en pixels), la palette, le nombre d'images (image animée), la durée de chaque image (image animée), le gamma (mis à 1.0, pour qu'il n'ait aucun effet), et les données, ainsi que le format.
IV-C-1. Lump et données▲
Dans DevIL, un lump est un fichier chargé en mémoire. Les données sont celles de ce lump.
Nous sommes obligés de travailler avec des lumps, car un QIODevice ne nous permet pas de trouver un nom de fichier : il peut être distant, ou local, ou en mémoire. Nous devons donc charger le fichier en mémoire.
IV-C-2. Palette et format▲
La palette regroupe l'ensemble des couleurs gérées par le format et/ou l'image (chaque image GIF peut avoir sa propre palette, limitée à 256 couleurs ; chaque image PSD a une seule palette). La palette est gérée par DevIL. Son correspondant Qt est le format.
IV-D. canRead()▲
bool
DevILHandler::
canRead()
const
{
if
(this
->
device() ==
0
);
{
qWarning
("DevILHandler::canRead() called with no device"
);
return
false
;
}
return
ilIsValidL(IL_TYPE_UNKNOWN, this
->
Lump, this
->
Size);
}
bool
DevILHandler::
canRead(QIODevice
*
device)
{
if
(!
device)
{
qWarning
("DevILHandler::canRead() called with no device or an unusable device"
);
return
false
;
}
this
->
setDevice(device);
return
ilIsValidL(IL_TYPE_UNKNOWN, this
->
Lump, this
->
Size);
}
La logique est la même : on vérifie qu'un QIODevice est bien chargé (s'il est précisé, on le charge). Puis, on demande à DevIL la validité du fichier.
IV-E. read()▲
bool
DevILHandler::
read (QImage
*
image)
{
this
->
Image =
QImage
(this
->
Size_wh, this
->
Q_Format
);
this
->
Image.fromData(this
->
Data, this
->
Size, 0
);
return
true
;
}
On crée un QImage avec les données dont nous disposons, puis nous y injectons les données.
IV-F. write()▲
bool
DevILHandler::
write (const
QImage
&
image)
{
if
( ilSaveL(this
->
I_Format, this
->
Lump, 0
) ==
0
)
return
false
;
else
return
true
;
}
On sauve dans le lump, tout simplement.
IV-G. imageCount(), loopCount(), nextImageDelay(), jumpToImage()▲
int
DevILHandler::
imageCount()
const
{
if
(this
->
I_Format !=
IL_GIF)
return
0
;
else
return
this
->
NumImages;
}
bool
DevILHandler::
jumpToImage(int
imageNumber)
const
{
return
false
;
}
Si le format est bien animé (parmi les formats supportés par DevIL, le seul animé que je connaisse est le GIF), on renvoie la variable membre correspondante.
DevIL ne supporte pas d'aller directement à une certaine image : nous devons donc renvoyer un résultat qui sanctionne cette incapacité.
IV-H. option(), setOption(), supportsOption()▲
QVariant
DevILHandler::
option(QImageIOHandler
::
ImageOption option)
const
{
if
(option ==
QImageIOHandler
::
Size)
return
this
->
Size_wh;
else
if
(option ==
QImageIOHandler
::
Gamma)
return
this
->
Gamma;
else
if
(option ==
QImageIOHandler
::
Animation)
if
(this
->
I_Format ==
IL_GIF)
return
true
;
else
return
false
;
else
return
false
;
}
void
DevILHandler::
setOption(QImageIOHandler
::
ImageOption option, const
QVariant
&
value)
{
if
(option ==
QImageIOHandler
::
Gamma)
{
this
->
Gamma =
value.toDouble();
iluGammaCorrect(this
->
Gamma);
}
}
bool
DevILHandler::
supportsOption(QImageIOHandler
::
ImageOption option)
const
{
if
(option ==
QImageIOHandler
::
Size)
return
true
;
else
if
(option ==
QImageIOHandler
::
Gamma)
return
true
;
else
if
(option ==
QImageIOHandler
::
Animation)
return
true
;
else
return
false
;
}
option() renvoie le paramètre actuel de l'option en question.
setOption() met l'option à la valeur désirée. Toutes les options ne peuvent pas être modifiées (une image statique ne peut pas devenir animée, et l'inverse n'est pas beaucoup plus possible).
supportsOption() permet de vérifier si l'option est supportée.
IV-I. Les surcharges pour QFile▲
DevIL est principalement prévu pour lire à partir de fichiers, et non de la mémoire. C'est pourquoi on peut aussi implémenter des fonctions spécialisées dans le traitement de fichiers, avec des QFile.
void
DevILHandler::
setDevice(QFile
*
device)
{
this
->
fDevice =
device;
this
->
fileName =
this
->
fDevice->
fileName().toStdString().c_str();
ilLoad (IL_TYPE_UNKNOWN, this
->
fileName);
/*
...
*/
}
Voici les seules lignes à commenter. Le pointeur pour le QFile ne peut pas être Device, vu qu'il est réservé aux QIODevice. On utilise donc fDevice.
La seule ligne un peu compliquée de tout le code prend l'objet sur lequel fDevice pointe, prend le nom de fichier, le convertit en std::string puis en char, type hérité du C, langage dans lequel DevIL est écrit.
Ensuite, on doit utiliser la fonction ilLoad(), qui prend en paramètre le type (inconnu) et le nom de fichier précédemment trouvé.
Ces modifications se trouvent dans toutes les autres surcharges pour QFile.
Il n'est pas prévu de base que les plugins puissent s'occuper directement des fichiers, il sont sensés passer par des QIODevice, comme les plugins de base de Qt. L'utilisation directe de fichiers par QImage sera donc limitée à ce plugin.
V. L'utilisation▲
Il faut préciser où se situe le plugin :
QCoreApplication
::
addLibraryPath("repertoire/des/plugins"
);
Correspondra au répertoire repertoire/des/plugins/imageformats pour ce type de plugins.
Et puis c'est tout ! QImage pourra désormais utiliser DevIL. Pas toutes ses fonctionnalités, seulement celles que QImageIOHandler supporte.
Par défaut, Qt ira chercher ce type de plug-ins dans le dossier imageformats de votre application.
Pour pouvoir utiliser ce plug-in, vous devez avoir installé DevIL : soit avec l'un des paquets de binaires fournis sur le site, soit avec l'un des paquets de votre distribution, soit compilé à la main. Vous devrez aussi, soit mettre sa librairie dans le répertoire du plug-in, soit la mettre dans un répertoire accessible par le PATH, soit, enfin, mettre un lien symbolique vers la librairie dans le dossier du plug-in. Si vous ne le faites pas, ce genre de message pourra s'afficher (dans une console sur GNU/Linux, par exemple).
symbol lookup error: /
home/
stage1/
Desktop/
imageformats/
libdevil-
qt.so: undefined symbol: ilInit
VI. Divers▲
Vous pouvez jeter un coup d'œil à ces quelques sites pour plus d'informations sur DevIL et les plugins de Qt.
- Écrire un plugin pour QImage
- Réalisation d'un moteur 3D : les textures et l'utilisation de DevIL
- Le site de DevIL, et son article encyclopédique
- La création de plugins pour Qt
- QImageIOPlugin
- QImageIOHandler
Téléchargez les sources de l'article : pour Visual Studio 2008 et pour Visual Studio 2008 avec Intel C++ Compiler 10.1, ainsi qu'avec un projet .pro.
Les sources compilent parfaitement et sans avertissement sur ces environnements :
- DevIL 1.7.7 et 1.7.8
- Qt 4.4.3, 4.5.0, 4.5.1
- Visual Studio 2008 9.0.20729.SP1
- Intel C++ Compiler 10.1.694.2008 et 11.0.759.2008
Un tout grand merci à Ikipou, pour ses encouragements, et à yan, pour les idées ! Sans oublier ram-0000, sans qui bien des fautes seraient restées !