A long time ago I created a tool to help me in generating the gradient C++ code out of a raster image. Then I lost it, and last year I created it again. To make sure I didn’t lose it again, and in the hopes that other developers don’t have to create it too, I hereby share it with you.
Written by Denis Gofman
2021/06/11
What do the images below have in common?
Honestly, I hate questions like that, because there are a lot of formally correct answers: They all are rectangular, coded in the same format and shot by the same person (me) in the same location, and so on. But what definitely makes this question annoying is that I obviously, considering the title, want the answer to be: gradients.
The analog world around us has almost no clean colors; it is filled by millions of shades in gradients. While the gradient can be interpreted as the «direction and rate of fastest change», the digital world needs a more detailed and digital-related description. One may want to check the Wikipedia for a general definition and all that scalar-valued differentiable function F of several variables stuff, but here I will cover only necessary graphics-related details.
A color gradient (aka color map or a color progression) is a range of position-dependent colors. Usually, it stores colors for some initially known points like «start» and «end» and is used to calculate color values for all other points in-between.
Depending on the sampling step, the calculation for values in the range [0.0; 1.0] could be a CPU-consuming task. That is one of the reasons why gradients relatively recently appeared in the regular GUI (remember the Windows Vista with its Aero and the first steps of Compiz in Linux).
But there is a scope where gradients have been used for ages as more than just bells and whistles.
Gradient charts
When I got to such a task a third time, I realized two things. First, the previously created tool had been lost to oblivion in the company that I had left years ago. Second, it seems the Karma exists and I’m doomed to get such malformed tasks. So, let’s write it again, for hopefully the last time! To begin with, let’s look at the API available in the
QLinearGradient:
// QLinearGradient();
// QLinearGradient(const QPointF &start, const QPointF &finalStop);
// QLinearGradient(qreal xStart, qreal yStart, qreal xFinalStop, qreal yFinalStop)
QLinearGradient lg(10., 10., 110., 110.);
lg.setColorAt(0.0, Qt::red);
lg.setColorAt(0.5, Qt::green);
lg.setColorAt(1.0, Qt::blue);
QPainter painter(this);
painter.setBrush(lg);
painter.drawRect({10, 10, 110, 110});
Now let’s try to fill an area that is different from the one used to initialize the gradient –
painter.drawRect({10, 10, 210, 210});
QLinearGradient
operates in the Logical Coordinates mode. That means that such a gradient is fixed to the initially set shape and has to be updated manually each time the shape is changed. Considering our goal – to grab a gradient from a raster image and reuse it in our software – most probably, being locked in dimensions of the source image is not what we actually want. To make automatically stretchable gradient, we have to specify its coordinates in a different scope, bounded to the range of [0.0; 1.0
] in both axes:
QLinearGradient lg;
lg.setStart(0., 0.); // top left
lg.setFinalStop(1., 1.); // bottom right
lg.setCoordinateMode(QGradient::ObjectMode); // maps the points to a painter coordinates
…
painter.drawRect({10, 10, 210, 210});
start
and finalStop
points:
lg.setStart(1., 1.); // bottom right
lg.setFinalStop(0., 0.); // top left
or change the color point coordinates accordingly:
// original
lg.setColorAt(0.0, Qt::red);
lg.setColorAt(0.5, Qt::green);
lg.setColorAt(1.0, Qt::blue);
// reversed
lg.setColorAt(1.0, Qt::red);
lg.setColorAt(0.5, Qt::green);
lg.setColorAt(0.0, Qt::blue);
Now we know enough to grab a gradient from a raster image. The idea is quite simple: scan the lines of the image pixel by pixel and map its color to a point in the gradient.
As we have seen, a color is assigned to some abstract position in the range [0.0; 1.0
]. Obviously, the mapping of a pixel (integer coordinates) to such position and back involves floating-point calculations, which leads to the rounding problem and a loss of some information. Because of this, even grabbing a gradient locally rendered by Qt and rendering it in the same coordinates would not get us an absolute copy of the original image. Things may get even worse if a source gradient image was rendered on a different platform.
But usually, the «red» is «red» for the human eye, and it doesn’t matter if it’s specified as (247, 0, 0
) or (253, 0, 0
):
253, 0, 0
) should always be displayed as (253, 0, 0
), and the fact it was the (247, 0, 0
) in the original image does not matter. And this is exactly what we are about to achieve. So… Let’s code a bit
The only available input data we have is an image to be scanned, a line that describes the direction of the scan in the pixel coordinates, and the count of steps to be performed – the desired density of the resulting gradient:
QString grabGradientFromImage(const QImage *image, const QLine &pixelsLine, int steps) {
First of all, let’s prepare helpers based on the input. There will be a lot of work with qreal
data, so it is a good idea to wrap the pixelsLine
into a QLineF
.
To iterate pixels, we have to know the size of the single shift. It based on the total length of the pixels line, its unit vector, and the desired steps count:
const QLineF lineF(pixelsLine);
const QLineF unitVector = lineF.translated(-lineF.p1()).unitVector();
const QPointF shift = unitVector.p2() * (lineF.length() / qreal(steps));
0.0; 1.0
], I’d prefer to use the existing API of QPainterPath:
const QPainterPath linePath([](const QLineF &line) -> QPainterPath {
QPainterPath path;
path.moveTo(line.p1());
path.lineTo(line.p2());
return path;
}(lineF));
Since the original image rectangle is also in use, it worth to keep a const ref to it to avoid unnecessary calls within a loop:
const QRect imgRect = image->rect();
Now it’s the turn for the rest of the workers. There is just a couple of non-consts: a line that actually is a vector directed to the current pixel, and a collection of gradient stops to store the collected data:
QLineF progressLine(lineF.p1(), lineF.p1());
QGradientStops gradientStops;
When the full pixelsLine
hasn’t yet been processed, the main working loop checks if the current pixel is still within the image’s rectangle. Then it maps coordinates to the gradient scope and puts the color into the collection of gradient stops:
while (progressLine.length() <= lineF.length()) {
if (imgRect.contains(progressLine.p2().toPoint())) {
const QColor color = image->pixelColor(progressLine.p2().toPoint());
const qreal posInGradient = linePath.percentAtLength(progressLine.length());
gradientStops.append({ posInGradient, color });
}
progressLine.setP2(progressLine.p2() + shift);
}
start
and finalStop
points and generate the text:
QPointF start(lineF.p1()), stop(lineF.p2());
QPointF limit(imgRect.bottomRight());
for (QPointF *pnt : { &start, &stop }) {
pnt->rx() = pnt->x() / limit.x();
pnt->ry() = pnt->y() / limit.y();
}
if (unitVector.p2().x() < 0 || unitVector.p2().y() < 0)
qSwap(start, stop);
return prepareText(gradientStops, start, stop);
}
prepareText
function is something like:
QString prepareText( const QGradientStops& stops, const QPointF& start, const QPointF& end) {…};
but it is too trivial and out of the scope of our discussion.
Complete code
Although the function described here is compilable, it is more a pseudocode. Here is its complete body for those who looked for a quick solution to copy-n-paste and tune it:
QString grabGradientFromImage(const QImage *image, const QLine &pixelsLine, int steps)
{
const QLineF lineF(pixelsLine);
const QLineF unitVector = lineF.translated(-lineF.p1()).unitVector();
const QPointF shift = unitVector.p2() * (lineF.length() / qreal(steps));
const QPainterPath linePath([](const QLineF &line) -> QPainterPath {
QPainterPath path;
path.moveTo(line.p1());
path.lineTo(line.p2());
return path;
}(lineF));
const QRect imgRect = image->rect();
QLineF progressLine(lineF.p1(), lineF.p1());
QGradientStops gradientStops;
while (progressLine.length() <= lineF.length()) {
if (imgRect.contains(progressLine.p2().toPoint())) {
const QColor color = image->pixelColor(progressLine.p2().toPoint());
const qreal posInGradient = linePath.percentAtLength(progressLine.length());
gradientStops.append({ posInGradient, color });
}
progressLine.setP2(progressLine.p2() + shift);
}
QPointF start(lineF.p1()), stop(lineF.p2());
QPointF limit(imgRect.bottomRight());
for (QPointF *pnt : { &start, &stop }) {
pnt->rx() = pnt->x() / limit.x();
pnt->ry() = pnt->y() / limit.y();
}
if (unitVector.p2().x() < 0. || unitVector.p2().y() < 0.)
qSwap(start, stop);
return prepareText(gradientStops, start, stop);
}
You can find the complete source code, split in a static library and a GUI application with fancy bells and whistles, here: https://gitlab.com/vikingsoftware/qlingradgen
So there you have it. Hopefully you find my tool useful, and won’t have issues with weird tasks like I did.
This is a great write up. Helped me sort out a couple of QLinearGradient concepts.
Thank you for sharing!