How to develop (simple) Quill filters (and make them available to users)
21 September 2011
So, you are a developer and want to get your hands dirty coding for the for the Nokia N9 (and maybe you even got one of those nifty Nokia N950 handsets), or just want to add some extra capabilities to Quill… Anyway, for me the journey started while toying with the Gallery application: I noticed that three is no tool that would convert images to grayscale in the image edition mode! So I decided to step in and hack on a bit.
The result
Take a close look at the middle screenshot in the image below. Can you spot the “Desaturate” tool?
That tool is added to the Gallery application by the plugin I have implemented. The source code is available at Gitorious and is licensed under the LGPLv2. A binary package can be downloaded here.
Tools of the trade
This is what was needed to get the job done:
- The MeeGo 2.1 Harmattan SDK, installed and working.
- A peek at the API documentation of libquillimagefilter, GalleryEditPlugin and GalleryEditUiProvider.
- The source code of the existing Quill filters — because it’s always nice to get “inspiration” from somewhere ;-)
- Some coffee to drink.
Gallery plugin
The Gallery application in the N9/N950 supports loading the so-called
“edit plugins”. It will try to load all the .so
files it finds in
/usr/lib/gallery
. Each of those should contain a subclass of
GalleryEditPlugin. A minimum subclass declaration would look like
this:
class GalleryDesaturatePlugin: public GalleryEditPlugin {
Q_OBJECT
Q_INTERFACES(GalleryEditPlugin);
public:
GalleryDesaturatePlugin(QObject* parent = 0);
virtual ~GalleryDesaturatePlugin();
QString name() const;
QString iconID() const;
public Q_SLOTS:
void performEditOperation();
private:
Q_DISABLE_COPY(GalleryDesaturatePlugin)
};
The first interesting method is name()
. It must return the string
which will be shown as tool name when using the edition mode in Gallery.
The simplest possible implementation is just to return a constant
string:
QString
GalleryDesaturatePlugin::name() const
{
return QString("Desaturate");
}
As an additional improvement, it would have been nice to provide internationalization support, and return a string translated to the language configured by the user in the system settings. I had no time to figure out how to provide translations for a plugin yet, so the simple approach will do for now.
The next method is iconID()
, which returns a string with an identifier
of the icon to be presented next to the tool name. It is enough to
return the file name without the suffix, and the icon will be picked
from /usr/share/themes/blanco/meegotouch/icons/
. In our case, let’s
use one of the images included by default:
QString
GalleryDesaturatePlugin::iconID() const
{
return QString("icon-m-camera-filter-black-white-screen");
}
Next step is to implement the juicy part: performEditOperation()
.
Let’s first see the code:
void
GalleryDesaturatePlugin::performEditOperation()
{
if (editUiProvider()) {
QHash options;
editUiProvider()->runEditFilter("com.igalia.aperez.desaturate", options);
}
emit editOperationPerformed();
}
Now maybe you are wondering where pixel data of the image is being
manipulated… that’s actually a good question! It turns out that
Gallery uses the Quill library to do image handling, so it off-loads the
actual implementation of image filters to it. The snippet above just
calls the runEditFilter()
method of GalleryEditUiProvider, which
takes an identifier of a Quill filter plus a set of options, and runs it
over the image. It would be tempting to just implement our pixel
mangling here, but nor the GalleryEditPlugin
, nor the
GalleryEditUiProvider
will give us access to the image data! Reading
through the Quill documentation I noticed that it supports infinite
undo and redo… same as the edit mode of the Gallery application! It
makes sense that the Gallery API forces the developer to use a Quill
filter to ensure that image edit history is seamlessly supported. If
Gallery allowed to directly manipulate data, a plugin could go against
the user experience presented by the application allowing to
destructively edit images.
Quill filter plugin
The good thing about having to implement a Quill plugin is the fact
that any application using libquillimagefilter
will be able of using
it. Also, when also using libquill
, support for infinite undo and redo
is provided. For free, no strings attached. Plugins are loaded from
/usr/lib/qt4/plugins/quillimagefilters
.
Quill filter plugins are a bit different from the Gallery edit tool plugins. A subclass of QuillImageFilterInterface will act as a factory class that the library uses for creating getting information about the filters implemented by a plugin, and for instantiating the filters themselves. This means one plugin can define more than one filter, which is useful for implementing filters that have common parts in their implementation, or that are logically related. On the other side, each filter is a subclass of QuillImageFilterImplementation, and will do the actual pixel mangling.
Let’s look at the plugin declaration first:
class QuillDesaturatePlugin: public QObject, public QuillImageFilterInterface
{
Q_OBJECT
Q_INTERFACES(QuillImageFilterInterface)
public:
QuillImageFilterImplementation* create(const QString& name);
const QStringList name() const;
private:
Q_DISABLE_COPY(QuillDesaturatePlugin)
};
The method name()
returns a list of the names of all the filters
implemented by the plugin. Or, to be more precise, the names of the
filters that the plugin can actually instantiate. Our simple plugin
returns only one name:
#define FILTER_NAME_DESATURATE "com.igalia.aperez.desaturate"
const QStringList QuillDesaturatePlugin::name() const
{
return QStringList() << FILTER_NAME_DESATURATE;
}
Instantiation of the filters happens in the create()
method. Note
that, if some filter name is not known, or for some reason it is
impossible to instantiate the filter, it is valid to return a null
pointer:
QuillImageFilterImplementation*
QuillDesaturatePlugin::create(const QString& name)
{
if (name == FILTER_NAME_DESATURATE) {
return new Desaturate;
} else {
return 0;
}
}
The only remaining bit is the filter implementation itself. The declaration looks like:
class Desaturate: public QuillImageFilterImplementation
{
public:
QuillImage apply(const QuillImage& image) const;
virtual const QString name() const;
};
The name()
method is trivial to implement, and it must return the name
used to identify the filter itself:
const QString
Desaturate::name() const
{
return FILTER_NAME_DESATURATE;
}
Actual image data manipulation (at last!) happens in the apply()
method. The original image is passed as argument, but it is declared as
constant and it must not be modified. The proper modus operandi is to
create a copy (or a completely new image) and modify it in-place. The
images passed back and forth are QuillImage instances, which is a
subclass of the well-known QImage. Any algorithm and operation that
can be done with a QImage will work just right. To make things even
easier, images will be always RGBA, so it is not needed to handle
different pixel formats. The implementation for converting images to
grayscale (i.e. “desaturating” them) is quite straightforward:
QuillImage
Desaturate::apply(const QuillImage& image) const
{
QuillImage result(image);
QRgb* endp(reinterpret_cast(result.bits() + result.numBytes()));
for (QRgb *p = reinterpret_cast(result.bits()); p < endp; p++) {
int value = (qRed(*p) + qGreen(*p) + qBlue(*p)) / 3;
*p = qRgba(value, value, value, qAlpha(*p));
}
return result;
}
For each pixel in the image, the alpha value is kept (to leave the transparency untouched, in formats that support it like PNG), and an average of the red, green and blue components is done to get the intensity of grey for each pixel. This algorithm, even when being completely unoptimized and naïve (there are for sure algorithms that render better quality), works “well enough” and is “fast enough” even for huge images.
Bottom line
Wrapping up, this is how things work, bottom up:
- A filter implementation (subclass of QuillImageFilterImplementation) does the pixel data manipulation.
- To make Quill use the filter, a plug-in implementing QuillImageFilterInterface should declare it as supported, and it must be able to instantiate it.
- Quill handles undo/redo for us :-)
- Gallery plug-ins forward the actual work to Quill filters. This ensures that undo/redo is not broken.
- Gallery plug-ins are merely an stub which informs the application about how to display a tool. It may also implement the user interface for filters needing it.
There are a number of topics that are not covered in this how to, like how to add an user interface to a Gallery tool plugin, or how to package the plugins to make them user-installable. Also, some details were left off to avoid unnecessary noise, so I would encourage readers to take a look at the source code for the plugin presented here.
Side note: As a bonus, plugins made this way will also work from the post-capture view of the Camera application of the N9/N950.
Now, go and do your own filters! Happy hacking…