The color-changing QPaintDeviceProxy

August 5, 2010

I got a feature request for KGameRenderer: One should be able to exchange certain colors in the rendered sprites. For example, the avatars of players may be generated from one sprite, but the player’s color is mixed into the sprite at given points to represent the player visually.

I remembered that Amarok has support for mixing the system color palette inside their SVG-based themes, and had a look at their code. They chose to replace the HTML color strings inside the SVG before loading it. Not quite the solution I want.

After some consideration, I figured that it shouldn’t be too difficult to write a general algorithm that replaces colors in painting operations. The obvious entry point is QPainter with its setPen() and setBrush() methods. However, these are not virtual, so a specialized implementation in a QPainter subclass will just be ignored by e.g. QSvgRenderer.

I therefore decided to go with a special QPaintDevice that acts as a proxy for a given QPaintDevice (in my case a QImage) and translates the colors in the primitive painting operations which are performed on the QPaintDevice (or, to be exact, its QPaintEngine).

It turns out that bootstrapping custom QPaintDevice/QPaintEngine subclasses isn’t particularly easy. It took 340 LOC to create a proxy device that just forwards all painting operations to the contained device, without any changes. Once that was there, plugging in the color mapping code in there was not particularly difficult, and the first test looked very promising:

On the right, green has been replaced by cyan, and black was turned into white. Doesn’t look to bad, does it? Let’s try a more complex example with a gradient:

As you see, this doesn’t work. The green in the background was not replaced by cyan (i.e., the green-blue gradient should have been a cyan-blue one). Only by chance did I find the reason, when I exchanged the brushes:

Do you spot the important difference between situation 2 and 3? The outline in the middle is not white, but black, i.e. this color has not been replaced correctly. At this point, I did some debugging and found the reason: While situation 1 (the one with the red background) calls the methods drawRect (for the background) and drawPath of my proxy engine, situation 3 uses drawRect and drawImage. That means, when it needs to paint gradients, the shape is first rendered into an image with the raster painting engine, then the rastered image is passed to my paint engine.

I cannot reasonably replace colors in images (esp. if they are antialiased), so I need to modify my strategy: Until now, I only changed the colors when I passed primitive painting operations to the inner QPaintDevice. My new strategy is to modify the brushess also inside my proxy QPaintDevice (not only in the contained QPaintDevice), so that gradient colors are modified before the gradients are rastered.


For those who are interested, I plan to put this code into libkdegames. Like mentioned before, it will be used to enable support for custom colors in KGameRenderer. So if you’re interested in the code, take a look at the review request for this patch or (if you read this when the patch has already been merged) at KDE SVN at trunk/KDE/kdegames/libkdegames/colorproxy*.


3 Responses to “The color-changing QPaintDeviceProxy”

  1. Lee Harr Says:

    I have also looked for this functionality before — though I was looking in Qt (PyQt actually). Any chance that something like this might find its way in to QtSvg?

    I wanted to use it in pybotwar so that I would only need one svg element for the tank — and so that players could choose the exact color they want for their tank.

    I think the replacement of a particular color throughout an element is interesting, but for me it would make more sense to just be able to change the color of a particular element by name.

    • Stefan Majewsky Says:

      Yes, changing the color per element would be nice, but the trolls have decided to implement QtSvg in such a way that QPaintDevice is the only entry point for hacks. And that is it indeed, a big hack. I therefore doubt highly that QtDF would want to put this into Qt, which is why I won’t actually propose it. I have better things to do with my time than arguing for upstreaming hacks.

      So if you want to use this in your own SVG rendering code, you will have to copy the code, which will in your case mean rewriting it. (The most difficult part is already done because there is a working implementation which you just need to understand and copy.)

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s