1 /*
2     SPDX-FileCopyrightText: 2010 Simon Andreas Eugster <simon.eu@gmail.com>
3     This file is part of kdenlive. See www.kdenlive.org.
4 
5 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
6 */
7 
8 #include "colortools.h"
9 
10 #include <QColor>
11 #include <cmath>
12 
13 //#define DEBUG_CT
14 #ifdef DEBUG_CT
15 #include "kdenlive_debug.h"
16 #endif
17 
18 namespace {
preventOverflow(double value)19 double preventOverflow(double value)
20 {
21     return value < 0 ? 0 : value > 255 ? 255 : value;
22 }
23 } // namespace
24 
ColorTools(QObject * parent)25 ColorTools::ColorTools(QObject *parent)
26     : QObject(parent)
27 {
28 }
29 
yuvColorWheel(const QSize & size,int Y,float scaling,bool modifiedVersion,bool circleOnly)30 QImage ColorTools::yuvColorWheel(const QSize &size, int Y, float scaling, bool modifiedVersion, bool circleOnly)
31 {
32     QImage wheel(size, QImage::Format_ARGB32);
33     if (size.width() == 0 || size.height() == 0) {
34         qCritical("ERROR: Size of the color wheel must not be 0!");
35         return wheel;
36     }
37     if (circleOnly) {
38         wheel.fill(qRgba(0, 0, 0, 0));
39     }
40 
41     double dr, dg, db, dv;
42     double ru, rv, rr;
43     const int w = size.width();
44     const int h = size.height();
45     const float w2 = (float)w / 2;
46     const float h2 = (float)h / 2;
47 
48     for (int u = 0; u < w; ++u) {
49         // Transform u from {0,...,w} to [-1,1]
50         double du = (double)2 * u / (w - 1) - 1;
51         du = scaling * du;
52 
53         for (int v = 0; v < h; ++v) {
54             dv = (double)2 * v / (h - 1) - 1;
55             dv = scaling * dv;
56 
57             if (circleOnly) {
58                 // Ellipsis equation: x²/a² + y²/b² = 1
59                 // Here: x=ru, y=rv, a=w/2, b=h/2, 1=rr
60                 // For rr > 1, the point lies outside. Don't draw it.
61                 ru = u - double(w2);
62                 rv = v - double(h2);
63                 rr = ru * ru / (w2 * w2) + rv * rv / (h2 * h2);
64                 if (rr > 1) {
65                     continue;
66                 }
67             }
68 
69             // Calculate the RGB values from YUV
70             dr = Y + 290.8 * dv;
71             dg = Y - 100.6 * du - 148 * dv;
72             db = Y + 517.2 * du;
73 
74             if (modifiedVersion) {
75                 // Scale the RGB values down, or up, to max 255
76                 const double dmax = 255 / std::max({fabs(dr), fabs(dg), fabs(db)});
77 
78                 dr *= dmax;
79                 dg *= dmax;
80                 db *= dmax;
81             }
82 
83             // Avoid overflows (which would generate intersecting patterns).
84             // Note that not all possible (y,u,v) values with u,v \in [-1,1]
85             // have a correct RGB representation, therefore some RGB values
86             // may exceed {0,...,255}.
87             dr = preventOverflow(dr);
88             dg = preventOverflow(dg);
89             db = preventOverflow(db);
90 
91             wheel.setPixel(u, (h - v - 1), qRgba(dr, dg, db, 255));
92         }
93     }
94 
95     emit signalYuvWheelCalculationFinished();
96     return wheel;
97 }
98 
yuvVerticalPlane(const QSize & size,int angle,float scaling)99 QImage ColorTools::yuvVerticalPlane(const QSize &size, int angle, float scaling)
100 {
101     QImage plane(size, QImage::Format_ARGB32);
102     if (size.width() == 0 || size.height() == 0) {
103         qCritical("ERROR: Size of the color plane must not be 0!");
104         return plane;
105     }
106 
107     double dr, dg, db, Y;
108     const int w = size.width();
109     const int h = size.height();
110     const double uscaling = scaling * cos(M_PI * angle / 180);
111     const double vscaling = scaling * sin(M_PI * angle / 180);
112 
113     for (int uv = 0; uv < w; ++uv) {
114         double du = uscaling * ((double)2 * uv / w - 1); //(double)?
115         double dv = vscaling * ((double)2 * uv / w - 1);
116 
117         for (int y = 0; y < h; ++y) {
118             Y = (double)255 * y / h;
119 
120             // See yuv2rgb, yuvColorWheel
121             dr = preventOverflow(Y + 290.8 * dv);
122             dg = preventOverflow(Y - 100.6 * du - 148 * dv);
123             db = preventOverflow(Y + 517.2 * du);
124 
125             plane.setPixel(uv, (h - y - 1), qRgba(dr, dg, db, 255));
126         }
127     }
128 
129     return plane;
130 }
131 
rgbCurvePlane(const QSize & size,const ColorTools::ColorsRGB & color,float scaling,const QRgb & background)132 QImage ColorTools::rgbCurvePlane(const QSize &size, const ColorTools::ColorsRGB &color, float scaling, const QRgb &background)
133 {
134     Q_ASSERT(scaling > 0 && scaling <= 1);
135 
136     QImage plane(size, QImage::Format_ARGB32);
137     if (size.width() == 0 || size.height() == 0) {
138         qCritical("ERROR: Size of the color plane must not be 0!");
139         return plane;
140     }
141 
142     const int w = size.width();
143     const int h = size.height();
144 
145     double dcol;
146     double dx, dy;
147 
148     for (int x = 0; x < w; ++x) {
149         double dval = (double)255 * x / (w - 1);
150 
151         for (int y = 0; y < h; ++y) {
152             dy = (double)y / (h - 1);
153             dx = (double)x / (w - 1);
154 
155             if (1 - scaling < 0.0001) {
156                 dcol = (double)255 * dy;
157             } else {
158                 dcol = (double)255 * (dy - (dy - dx) * (1 - scaling));
159             }
160 
161             if (color == ColorTools::ColorsRGB::R) {
162                 plane.setPixel(x, (h - y - 1), qRgb(dcol, dval, dval));
163             } else if (color == ColorTools::ColorsRGB::G) {
164                 plane.setPixel(x, (h - y - 1), qRgb(dval, dcol, dval));
165             } else if (color == ColorTools::ColorsRGB::B) {
166                 plane.setPixel(x, (h - y - 1), qRgb(dval, dval, dcol));
167             } else if (color == ColorTools::ColorsRGB::A) {
168                 plane.setPixel(x, (h - y - 1), qRgb(dcol / 255. * qRed(background), dcol / 255. * qGreen(background), dcol / 255. * qBlue(background)));
169             } else {
170                 plane.setPixel(x, (h - y - 1), qRgb(dcol, dcol, dcol));
171             }
172         }
173     }
174     return plane;
175 }
176 
rgbCurveLine(const QSize & size,const ColorTools::ColorsRGB & color,const QRgb & background)177 QImage ColorTools::rgbCurveLine(const QSize &size, const ColorTools::ColorsRGB &color, const QRgb &background)
178 {
179 
180     QImage plane(size, QImage::Format_ARGB32);
181     if (size.width() == 0 || size.height() == 0) {
182         qCritical("ERROR: Size of the color line must not be 0!");
183         return plane;
184     }
185 
186     const int w = size.width();
187     const int h = size.height();
188 
189     double dcol;
190     double dy;
191 
192     for (int x = 0; x < w; ++x) {
193 
194         for (int y = 0; y < h; ++y) {
195             dy = (double)y / (h - 1);
196 
197             dcol = (double)255 * dy;
198 
199             if (color == ColorTools::ColorsRGB::R) {
200                 plane.setPixel(x, (h - y - 1), qRgb(dcol, 0, 0));
201             } else if (color == ColorTools::ColorsRGB::G) {
202                 plane.setPixel(x, (h - y - 1), qRgb(0, dcol, 0));
203             } else if (color == ColorTools::ColorsRGB::B) {
204                 plane.setPixel(x, (h - y - 1), qRgb(0, 0, dcol));
205             } else if (color == ColorTools::ColorsRGB::A) {
206                 plane.setPixel(x, (h - y - 1), qRgb(dcol / 255. * qRed(background), dcol / 255. * qGreen(background), dcol / 255. * qBlue(background)));
207             } else {
208                 plane.setPixel(x, (h - y - 1), qRgb(dcol, dcol, dcol));
209             }
210         }
211     }
212     return plane;
213 }
214 
yPbPrColorWheel(const QSize & size,int Y,float scaling,bool circleOnly)215 QImage ColorTools::yPbPrColorWheel(const QSize &size, int Y, float scaling, bool circleOnly)
216 {
217 
218     QImage wheel(size, QImage::Format_ARGB32);
219     if (size.width() == 0 || size.height() == 0) {
220         qCritical("ERROR: Size of the color wheel must not be 0!");
221         return wheel;
222     }
223     if (circleOnly) {
224         wheel.fill(qRgba(0, 0, 0, 0));
225     }
226 
227     double dr, dg, db, dpR;
228     double rB, rR, rr;
229     const int w = size.width();
230     const int h = size.height();
231     const float w2 = (float)w / 2;
232     const float h2 = (float)h / 2;
233 
234     for (int b = 0; b < w; ++b) {
235         // Transform pB from {0,...,w} to [-0.5,0.5]
236         double dpB = (double)b / (w - 1) - .5;
237         dpB = scaling * dpB;
238 
239         for (int r = 0; r < h; ++r) {
240             dpR = (double)r / (h - 1) - .5;
241             dpR = scaling * dpR;
242 
243             if (circleOnly) {
244                 // see yuvColorWheel
245                 rB = b - double(w2);
246                 rR = r - double(h2);
247                 rr = rB * rB / (w2 * w2) + rR * rR / (h2 * h2);
248                 if (rr > 1) {
249                     continue;
250                 }
251             }
252 
253             // Calculate the RGB values from YPbPr
254             dr = preventOverflow(Y + 357.5 * dpR);
255             dg = preventOverflow(Y - 87.75 * dpB - 182.1 * dpR);
256             db = preventOverflow(Y + 451.86 * dpB);
257 
258             wheel.setPixel(b, (h - r - 1), qRgba(dr, dg, db, 255));
259         }
260     }
261 
262     return wheel;
263 }
264 
hsvHueShiftPlane(const QSize & size,int S,int V,int MIN,int MAX)265 QImage ColorTools::hsvHueShiftPlane(const QSize &size, int S, int V, int MIN, int MAX)
266 {
267     Q_ASSERT(size.width() > 0);
268     Q_ASSERT(size.height() > 0);
269     Q_ASSERT(MAX > MIN);
270 
271     QImage plane(size, QImage::Format_ARGB32);
272 
273 #ifdef DEBUG_CT
274     qCDebug(KDENLIVE_LOG) << "Requested: Saturation " << S << ", Value " << V;
275     QColor colTest(-1, 256, 257);
276     qCDebug(KDENLIVE_LOG) << "-1 mapped to " << colTest.red() << ", 256 to " << colTest.green() << ", 257 to " << colTest.blue();
277 #endif
278 
279     QColor col(0, 0, 0);
280 
281     const int hueValues = MAX - MIN;
282 
283     float huediff;
284     int newhue;
285     for (int x = 0; x < size.width(); ++x) {
286         float hue = x / (size.width() - 1.0) * 359;
287         for (int y = 0; y < size.height(); ++y) {
288             huediff = (1.0f - y / (size.height() - 1.0)) * hueValues + MIN;
289             //            qCDebug(KDENLIVE_LOG) << "hue: " << hue << ", huediff: " << huediff;
290 
291             newhue = hue + huediff + 360; // Avoid negative numbers. Rest (>360) will be mapped correctly.
292 
293             col.setHsv(newhue, S, V);
294             plane.setPixel(x, y, col.rgba());
295         }
296     }
297 
298     return plane;
299 }
300 
hsvCurvePlane(const QSize & size,const QColor & baseColor,const ComponentsHSV & xVariant,const ComponentsHSV & yVariant,bool shear,const float offsetY)301 QImage ColorTools::hsvCurvePlane(const QSize &size, const QColor &baseColor, const ComponentsHSV &xVariant, const ComponentsHSV &yVariant, bool shear,
302                                  const float offsetY)
303 {
304     Q_ASSERT(size.width() > 0);
305     Q_ASSERT(size.height() > 0);
306 
307     /*int xMax, yMax;
308 
309     switch(xVariant) {
310     case COM_H:
311         xMax = 360;
312         break;
313     case COM_S:
314     case COM_V:
315         xMax = 256;
316         break;
317     }
318 
319     switch (yVariant) {
320     case COM_H:
321         yMax = 360;
322         break;
323     case COM_S:
324     case COM_V:
325         yMax = 256;
326         break;
327     }*/
328 
329     QImage plane(size, QImage::Format_ARGB32);
330 
331     QColor col(0, 0, 0);
332 
333     float hue, sat, val;
334     hue = baseColor.hueF();
335     sat = baseColor.saturationF();
336     val = baseColor.valueF();
337 
338     for (int x = 0; x < size.width(); ++x) {
339         switch (xVariant) {
340         case COM_H:
341             hue = x / (size.width() - 1.0);
342             break;
343         case COM_S:
344             sat = x / (size.width() - 1.0);
345             break;
346         case COM_V:
347             val = x / (size.width() - 1.0);
348             break;
349         }
350         for (int y = 0; y < size.height(); ++y) {
351             switch (yVariant) {
352             case COM_H:
353                 hue = 1.0 - y / (size.height() - 1.0);
354                 break;
355             case COM_S:
356                 sat = 1.0 - y / (size.height() - 1.0);
357                 break;
358             case COM_V:
359                 val = 1.0 - y / (size.height() - 1.0);
360                 break;
361             }
362 
363             col.setHsvF(hue, sat, val);
364 
365             if (!shear) {
366                 plane.setPixel(x, y, col.rgba());
367             } else {
368                 plane.setPixel(x, (2 * size.height() + y - x * size.width() / size.height() - int(offsetY) * size.height()) % size.height(), col.rgba());
369             }
370         }
371     }
372 
373     return plane;
374 }
375