QLinear Gradient strikes back

Gradients

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

I have been involved in developing both data and navigational chart plotters a few times. During these projects I would sometimes get weird tasks, like «improve/reflect the chart and its legend, as shown in…». This would come with a screenshot in a bug tracker or just a chain of clicks to be performed to get to the related screen in an existing application we didn’t have sources on.

The first time I got such a task, I was too young and in need of money active. So I spent some time with a screenshot and the color picker tool…

The next time, I tried to «work smart, not hard» and made a tool for single-time use. Or at least I thought it would be single-time use.

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});
				
			
QLinearGradient 1

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 2
As shown in the image above, the last color is used to fill the rest of the increased area. By default, a 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});
				
			
QLinearGradient 3
To reverse the gradient flow direction, one may either swap its start and finalStop points:
				
					   lg.setStart(1., 1.);     // bottom right
   lg.setFinalStop(0., 0.); // top left

				
			
QLinearGradient 4

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):

And that is acceptable since we need a gradient for a chart or its legend. In other words, the data mapped to a point “red” (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));
				
			
Instead of manual mapping of a pixel position to the range [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);
   }

				
			
After that we just have to calculate appropriate values for gradient’s 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);
}

				
			
Obviously, the 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 finde the complete source code, split in a static library and a GUI application with fancy bells and whistles, here: https://gitlab.com/vikingsoftware/qlingradgen

QLinGradGen H

So there you have it. Hopefully you find my tool useful, and won’t have issues with weird tasks like I did.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments