Wiki

Writing a Qt Image Plugin

by Kent Hansen
The Qt image I/O API provides a high-level API for reading and writing images in popular formats supported by Qt, including JPEG, PNG and BMP. But what if your application needs to read or write images in a format not currently supported by Qt?

This article addresses this intriguing question. The first part gives an overview of the Qt image I/O framework, revealing what goes on behind the scenes; specifically, we show how Qt enables support for a multitude of image formats in an extensible fashion. The second part goes on to show -- with an example -- how you can tap into the power of that framework, and provide seamless integration with your own image formats.

[Download source code]

Basic I/O

At the application level, you typically read an image into a QImage by passing a filename to the QImage constructor:

QImage myimage(fileName);
if (myimage.isNull()) {
    // Handle load error
}

You can write an image by calling its save() function:

// Save the modified image to file
if (!myimage.save(fileName, format)) {
    // Handle save error
}

In order to gain an understanding of how we can achieve reading and writing of images in our custom image format in the same high-level way, we will spend a few moments to consider what goes on behind the scenes of the above code.

The QImage functions rely on the classes QImageReader and QImageWriter to provide image reading and writing, respectively. The main job of these two classes is to delegate the actual image I/O to the proper image I/O handler.

An image I/O handler encapsulates the details of low-level image I/O for a particular image format. The handler is a subclass of the interface class QImageIOHandler, and implements the virtual functions that QImageReader and QImageWriter will use to delegate image I/O to the handler.

A Qt image I/O plugin makes a QImageIOHandler available to Qt at run-time. The plugin, which is a subclass of QImageIOPlugin, provides two basic services. First, the plugin can be queried to determine whether it has an image I/O handler that can perform the requested I/O. Second, the plugin acts as a factory for the image I/O handler itself, allowing Qt to obtain an instance of the handler.

QImage Control Flow

The above figure shows the basic control flow when an application loads an image from a file.

The application simply constructs a QImage, handing the QImage constructor the filename. In response, the QImage instructs a QImageReader to read the image. The primary constructor of QImageReader takes a QIODevice as argument, which means that any subclass of QIODevice can be used, from QFile to QTcpSocket, and even custom devices. (In the case illustrated by the above figure, a QFile would be constructed from the given filename.)

Optionally, a format string can be passed to the QImage constructor; like the filename, this string is passed along to the QImageReader. In the absence of a format string, an image I/O plugin is expected to "peek" at the contents of the given QIODevice (e.g. check the image header) and auto-detect the format.

The QImageReader queries the reading capabilities of each plugin in turn. If no suitable handler can be provided by any plugin, the reader falls back to a default handler, if one exists for the given format (not shown in the figure). Finally, the QImageReader calls read() on the established I/O handler, which does the real image reading.

Writing an Image

Writing an image with the help of QImageWriter is similar to reading; the main difference is that passing the image format string is now mandatory, because auto-detecting the format from the I/O device is no longer a possibility. Additionally, you can set various I/O handler options if the handler supports them, such as the compression level that is to be used.

Providing a Custom Image I/O Handler

By subclassing QImageIOPlugin and QImageIOHandler, you can give Qt access to your own image I/O code, effectively allowing Qt applications to access your image formats just like any other supported image format (e.g., by passing a filename to the QImage constructor). In this section, we look at what this entails.

Let us consider an example image I/O plugin for a very simple "raw" (uncompressed) ARGB image format. The format itself has been invented just for the sake of this example. In this ingenious format, pixels are represented as 32-bit unsigned integers, with 8 bits allocated for each of the red, green, blue, and alpha channels, and they are preceded by a small (12-byte) header containing the "magic" number, 0xCAFE1234, followed by the width and height of the image (each 32-bit unsigned integers).

The ArgbPlugin class is an QImageIOPlugin subclass, and exposes the image handler to Qt via a set of standard functions:

class ArgbPlugin : public QImageIOPlugin
{
public:
    ArgbPlugin();
     ArgbPlugin();
 
    QStringList keys() const;
    Capabilities capabilities(QIODevice *device,
                 const QByteArray &format) const;
    QImageIOHandler *create(QIODevice *device,
                            const QByteArray &format = QByteArray()) const;
};

There are three functions that must be reimplemented in our subclass. The first function, keys(), returns a list of the format strings recognized by the plugin. We've chosen to use the rather generic .raw extension for the image format, so keys() looks like this:

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

This function is used by QImageReader::supportedImageFormats() and QImageWriter::supportedImageFormats() to build the list of image format strings that Qt can provide handlers for.

The capabilities() function determines the read/write capabilities of the plugin (or rather, of the handler it provides) based on a given I/O device or format string:

QImageIOPlugin::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;
}

Note that when a format string is not given, the plugin calls the static function canRead() of ArgbHandler (see below) to determine if the device's contents indicate the presence of a raw ARGB image.

The third and final function of ArgbPlugin creates an instance of the image I/O handler itself, ArgbHandler:

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

That's it for ArgbPlugin. As we can see, subclassing QImageIOPlugin is a straightforward process. Even for complex image formats, the code probably doesn't have to do much more than in this example because the image format-specific processing is in the image I/O handler.

The QImageIOHandler subclass, ArgbHandler, performs I/O specific to the raw ARGB image format.

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;
};

The first function of interest is the static function, canRead(), which checks whether a raw ARGB image can be read from a given device. It simply checks that the input data starts with the magic number, 0xCAFE1234:

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

Here, we rely on QIODevice::peek() to look at the contents of the device without side effects; unlike QIODevice::read(), peek() does not consume the data, leaving the device in the same state as it was prior to the invocation of peek().

The handler processes the image files in its read() function:

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;
}

We use QDataStream to unpack the multi-byte values for each pixel from the QIODevice. After reading the image header and verifying the magic number, we construct a QImage of the proper size and format, then read the pixels one at a time, scanline by scanline.

Finally, we check that the input data stream is correct (that there were no input errors while reading the pixels). If so, we store the resulting image and report success by returning a true value; otherwise, we indicate failure with a false value.

The reimplementation of QImageIOHandler::write() performs the "inverse" process to that implemented in the read() function:

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;
}

In addition to reading and writing raw ARGB images, ArgbHandler supports the QImageIOHandler::Size option, so that the size of an ARGB image can be queried without actually having to read the entire image. To achieve this, we reimplement supportsOption() and option() as follows:

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();
}

Basically, instead of creating a QDataStream that operates on the QIODevice itself (like we did in ArgbHandler::read()), we only peek at the 12-byte header and create a QDataStream that operates on the resulting QByteArray. This way, we can parse the header and extract the image size without disturbing the state of the QIODevice.

Qt Project File

The project (.pro) file for the raw ARGB plugin looks as follows:

TARGET  = argb
TEMPLATE = lib
CONFIG = qt plugin
VERSION = 1.0.0
 
HEADERS = argbhandler.h
SOURCES = argbplugin.cpp argbhandler.cpp
 
target.path += $$[QT_INSTALL_PLUGINS]/imageformats
INSTALLS += target

Note that we use the lib template (the plugin is compiled as a library), and that the CONFIG definition contains plugin to ensure that a Qt plugin is built.

Using the Plugin

With the project file shown above, make install will install the plugin in the Qt installation's plugin directory, making the plugin available to all Qt applications.

If the plugin resides in a different directory (e.g., if the plugin is bundled with your application's source distribution), you have to change the target path in the project file, and your application needs to call QCoreApplication::addLibraryPath() with the path of your plugin directory.

For example, if the ARGB plugin is located in some_path/plugins/imageformats, an application that wants to use it has to include the following line:

QCoreApplication::addLibraryPath("some_path/plugins");

With the plugin in place and the library path correctly set, an application can now read and write raw ARGB image files with the usual QImage functions, which is what we set out to achieve.

Wrapping Up

While the example we have looked at is very basic, it nicely illustrates some issues that most Qt image I/O handlers need to deal with:

  • Attempting to detect the image format by verifying the presence of format-specific header information.
  • Creating a proper QImage based on image attributes found in the header (e.g., size and format).
  • Handling multi-byte data in a platform-independent manner.
  • Pixel and scanline-level image manipulation.
  • Returning image attributes (e.g., size) without actually reading the whole image.
For reference implementations of non-trivial image plugins, take a look at the plugins that are part of the Qt source distribution. These can be found in the src/plugins/imageformats directory. Those of particular interest in today's jungle of high-complexity image formats are the JPEG and MNG plugins, which demonstrate how an image plugin can wrap existing third party libraries: libjpeg and libmng respectively.


Copyright © 2006 Trolltech Trademarks