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*.