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

 
Sélectionnez
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

 
Sélectionnez
#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

 
Sélectionnez
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()

 
Sélectionnez
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()

 
Sélectionnez
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()

 
Sélectionnez
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

 
Sélectionnez
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

 
Sélectionnez
DevILHandler::DevILHandler()
{
	ilInit();
	iluInit();
}

On initialise DevIL et ILU. Le dernier ne sert que pour la gestion des effets gamma.

IV-B. setFormat()

 
Sélectionnez
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()

 
Sélectionnez
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()

 
Sélectionnez
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()

 
Sélectionnez
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()

 
Sélectionnez
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()

 
Sélectionnez
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()

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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).

 
Sélectionnez
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.

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 !