Perceptual Color

gradientimageparameters.cpp
1// SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
2// SPDX-License-Identifier: BSD-2-Clause OR MIT
3
4// Own headers
5// First the interface, which forces the header to be self-contained.
6#include "gradientimageparameters.h"
7
8#include "asyncimagerendercallback.h"
9#include "helper.h"
10#include "helperqttypes.h"
11#include "lchadouble.h"
12#include "lchdouble.h"
13#include "rgbcolorspace.h"
14#include <cmath>
15#include <qbrush.h>
16#include <qcolor.h>
17#include <qimage.h>
18#include <qnamespace.h>
19#include <qpainter.h>
20#include <qsharedpointer.h>
21
22namespace PerceptualColor
23{
24/** @brief Constructor */
25GradientImageParameters::GradientImageParameters()
26{
27 setFirstColor(LchaDouble{0, 0, 0, 1});
28 setFirstColor(LchaDouble{1000, 0, 0, 1});
29}
30
31/** @brief Normalizes the value and bounds it to the LCH color space.
32 * @param color the color that should be treated.
33 * @returns A normalized and bounded version. If the chroma was negative,
34 * it gets positive (which implies turning the hue by 180°). The hue is
35 * normalized to the range <tt>[0°, 360°[</tt>. Lightness is bounded to the
36 * range <tt>[0, 100]</tt>. Alpha is bounded to the range <tt>[0, 1]</tt>. */
37LchaDouble GradientImageParameters::completlyNormalizedAndBounded(const LchaDouble &color)
38{
39 LchaDouble result;
40 if (color.c < 0) {
41 result.c = color.c * (-1);
42 result.h = fmod(color.h + 180, 360);
43 } else {
44 result.c = color.c;
45 result.h = fmod(color.h, 360);
46 }
47 if (result.h < 0) {
48 result.h += 360;
49 }
50 result.l = qBound<qreal>(0, color.l, 100);
51 result.a = qBound<qreal>(0, color.a, 1);
52 return result;
53}
54
55/** @brief Setter for the first color property.
56 * @param newFirstColor The new first color.
57 * @sa @ref m_firstColorCorrected */
58void GradientImageParameters::setFirstColor(const LchaDouble &newFirstColor)
59{
60 LchaDouble correctedNewFirstColor = //
61 completlyNormalizedAndBounded(newFirstColor);
62 if (!m_firstColorCorrected.hasSameCoordinates(correctedNewFirstColor)) {
63 m_firstColorCorrected = correctedNewFirstColor;
64 updateSecondColor();
65 // Free the memory used by the old image.
66 m_image = QImage();
67 }
68}
69
70/** @brief Setter for the second color property.
71 * @param newSecondColor The new second color.
72 * @sa @ref m_secondColorCorrectedAndAltered */
73void GradientImageParameters::setSecondColor(const LchaDouble &newSecondColor)
74{
75 LchaDouble correctedNewSecondColor = //
76 completlyNormalizedAndBounded(newSecondColor);
77 if (!m_secondColorCorrectedAndAltered.hasSameCoordinates(correctedNewSecondColor)) {
78 m_secondColorCorrectedAndAltered = correctedNewSecondColor;
79 updateSecondColor();
80 // Free the memory used by the old image.
81 m_image = QImage();
82 }
83}
84
85/** @brief Updates @ref m_secondColorCorrectedAndAltered
86 *
87 * This update takes into account the current values of
88 * @ref m_firstColorCorrected and @ref m_secondColorCorrectedAndAltered. */
89void GradientImageParameters::updateSecondColor()
90{
91 m_secondColorCorrectedAndAltered = //
92 completlyNormalizedAndBounded(m_secondColorCorrectedAndAltered);
93 if (qAbs(m_firstColorCorrected.h - m_secondColorCorrectedAndAltered.h) > 180) {
94 if (m_firstColorCorrected.h > m_secondColorCorrectedAndAltered.h) {
95 m_secondColorCorrectedAndAltered.h += 360;
96 } else {
97 m_secondColorCorrectedAndAltered.h -= 360;
98 }
99 }
100}
101
102/** @brief Render an image.
103 *
104 * The function will render the image with the given parameters,
105 * and deliver the result by means of <tt>callbackObject</tt>.
106 *
107 * This function is thread-safe as long as each call of this function
108 * uses different <tt>variantParameters</tt> and <tt>callbackObject</tt>.
109 *
110 * @param variantParameters A <tt>QVariant</tt> that contains the
111 * image parameters.
112 * @param callbackObject Pointer to the object for the callbacks.
113 *
114 * @todo Could we get better performance? Even online tools like
115 * https://bottosson.github.io/misc/colorpicker/#ff2a00 or
116 * https://oklch.evilmartians.io/#65.4,0.136,146.7,100 get quite good
117 * performance. How do they do that? */
118void GradientImageParameters::render(const QVariant &variantParameters, AsyncImageRenderCallback &callbackObject)
119{
120 if (!variantParameters.canConvert<GradientImageParameters>()) {
121 return;
122 }
123 const GradientImageParameters parameters = //
124 variantParameters.value<GradientImageParameters>();
125 if (parameters.rgbColorSpace.isNull()) {
126 return;
127 }
128
129 // From Qt Example’s documentation:
130 //
131 // “If we discover […] that restart has been set
132 // to true (by render()), we break out […] immediately […].
133 // Similarly, if we discover that abort has been set
134 // to true (by the […] destructor), we return from the
135 // function immediately […].”
136 if (callbackObject.shouldAbort()) {
137 return;
138 }
139
140 // First, create an image of the gradient with only one pixel thickness.
141 // (Color management operations are expensive in CPU time; we try to
142 // minimize this.)
143 QImage onePixelLine(parameters.m_gradientLength, //
144 1, //
146 onePixelLine.fill(Qt::transparent); // Initialize image with transparency.
147 LchaDouble color;
148 LchDouble cielchD50;
149 QColor temp;
150 for (int i = 0; i < parameters.m_gradientLength; ++i) {
151 color = parameters.colorFromValue( //
152 (i + 0.5) / static_cast<qreal>(parameters.m_gradientLength));
153 cielchD50.l = color.l;
154 cielchD50.c = color.c;
155 cielchD50.h = color.h;
156 temp = parameters.rgbColorSpace->fromCielchD50ToQRgbBound(cielchD50);
157 temp.setAlphaF(
158 // Reduce floating point precision if necessary.
159 static_cast<QColorFloatType>(color.a));
160 onePixelLine.setPixelColor(i, 0, temp);
161 }
162 if (callbackObject.shouldAbort()) {
163 return;
164 }
165
166 // Now, create a full image of the gradient
167 QImage result = QImage(parameters.m_gradientLength, //
168 parameters.m_gradientThickness, //
170 if (result.isNull()) {
171 // Make sure that no QPainter can be created on a null image
172 // (because this would trigger warning messages on the command
173 // line).
174 return;
175 }
176 QPainter painter(&result);
177
178 // Transparency background
179 if ( //
180 (parameters.m_firstColorCorrected.a != 1) //
181 || (parameters.m_secondColorCorrectedAndAltered.a != 1) //
182 ) {
183 // Fill the image with tiles. (QBrush will ignore
184 // the devicePixelRatioF of the image of the tile.)
185 const auto background = transparencyBackground( //
186 parameters.m_devicePixelRatioF);
187 painter.fillRect(0, //
188 0, //
189 parameters.m_gradientLength, //
190 parameters.m_gradientThickness, //
191 QBrush(background));
192 }
193
194 // Paint the gradient itself.
195 for (int i = 0; i < parameters.m_gradientThickness; ++i) {
196 painter.drawImage(0, i, onePixelLine);
197 }
198
199 result.setDevicePixelRatio(parameters.m_devicePixelRatioF);
200
201 if (callbackObject.shouldAbort()) {
202 return;
203 }
204
205 callbackObject.deliverInterlacingPass( //
206 result, //
207 variantParameters, //
208 AsyncImageRenderCallback::InterlacingState::Final);
209}
210
211/** @brief The color that the gradient has at a given position of the gradient.
212 * @param value The position. Valid range: <tt>[0.0, 1.0]</tt>. <tt>0.0</tt>
213 * means the first color, <tt>1.0</tt> means the second color, and everything
214 * in between means a color in between.
215 * @returns If the position is valid: The color at the given position and
216 * its corresponding alpha value. If the position is out-of-range: An
217 * arbitrary value. */
218LchaDouble GradientImageParameters::colorFromValue(qreal value) const
219{
220 LchaDouble color;
221 color.l = m_firstColorCorrected.l //
222 + (m_secondColorCorrectedAndAltered.l - m_firstColorCorrected.l) * value;
223 color.c = m_firstColorCorrected.c + //
224 (m_secondColorCorrectedAndAltered.c - m_firstColorCorrected.c) * value;
225 color.h = m_firstColorCorrected.h + //
226 (m_secondColorCorrectedAndAltered.h - m_firstColorCorrected.h) * value;
227 color.a = m_firstColorCorrected.a + //
228 (m_secondColorCorrectedAndAltered.a - m_firstColorCorrected.a) * value;
229 return color;
230}
231
232/** @brief Setter for the device pixel ratio (floating point).
233 *
234 * This value is set as device pixel ratio (floating point) in the
235 * <tt>QImage</tt> that this class holds. It does <em>not</em> change
236 * the <em>pixel</em> size of the image or the pixel size of wheel
237 * thickness or border.
238 *
239 * This is for HiDPI support. You can set this to
240 * <tt>QWidget::devicePixelRatioF()</tt> to get HiDPI images in the correct
241 * resolution for your widgets. Within a method of a class derived
242 * from <tt>QWidget</tt>, you could write:
243 *
244 * @snippet testgradientimageparameters.cpp GradientImage HiDPI usage
245 *
246 * The default value is <tt>1</tt> which means no special scaling.
247 *
248 * @param newDevicePixelRatioF the new device pixel ratio as a
249 * floating point data type. (Values smaller than <tt>1.0</tt> will be
250 * considered as <tt>1.0</tt>.) */
251void GradientImageParameters::setDevicePixelRatioF(const qreal newDevicePixelRatioF)
252{
253 const qreal tempDevicePixelRatioF = qMax<qreal>(1, newDevicePixelRatioF);
254 if (m_devicePixelRatioF != tempDevicePixelRatioF) {
255 m_devicePixelRatioF = tempDevicePixelRatioF;
256 // Free the memory used by the old image.
257 m_image = QImage();
258 }
259}
260
261/** @brief Setter for the gradient length property.
262 *
263 * @param newGradientLength The new gradient length, measured
264 * in <em>physical pixels</em>. */
265void GradientImageParameters::setGradientLength(const int newGradientLength)
266{
267 const int temp = qMax(0, newGradientLength);
268 if (m_gradientLength != temp) {
269 m_gradientLength = temp;
270 // Free the memory used by the old image.
271 m_image = QImage();
272 }
273}
274
275/** @brief Setter for the gradient thickness property.
276 *
277 * @param newGradientThickness The new gradient thickness, measured
278 * in <em>physical pixels</em>. */
279void GradientImageParameters::setGradientThickness(const int newGradientThickness)
280{
281 const int temp = qMax(0, newGradientThickness);
282 if (m_gradientThickness != temp) {
283 m_gradientThickness = temp;
284 // Free the memory used by the old image.
285 m_image = QImage();
286 }
287}
288
289/** @brief Equal operator
290 *
291 * @param other The object to compare with.
292 *
293 * @returns <tt>true</tt> if equal, <tt>false</tt> otherwise. */
294bool GradientImageParameters::operator==(const GradientImageParameters &other) const
295{
296 return ( //
297 (m_devicePixelRatioF == other.m_devicePixelRatioF) //
298 && (m_firstColorCorrected.l == other.m_firstColorCorrected.l) //
299 && (m_firstColorCorrected.c == other.m_firstColorCorrected.c) //
300 && (m_firstColorCorrected.h == other.m_firstColorCorrected.h) //
301 && (m_firstColorCorrected.a == other.m_firstColorCorrected.a) //
302 && (m_gradientLength == other.m_gradientLength) //
303 && (m_gradientThickness == other.m_gradientThickness) //
304 && (rgbColorSpace == other.rgbColorSpace) //
305 && (m_secondColorCorrectedAndAltered.l == other.m_secondColorCorrectedAndAltered.l) //
306 && (m_secondColorCorrectedAndAltered.c == other.m_secondColorCorrectedAndAltered.c) //
307 && (m_secondColorCorrectedAndAltered.h == other.m_secondColorCorrectedAndAltered.h) //
308 && (m_secondColorCorrectedAndAltered.a == other.m_secondColorCorrectedAndAltered.a) //
309 );
310}
311
312/** @brief Unequal operator
313 *
314 * @param other The object to compare with.
315 *
316 * @returns <tt>true</tt> if unequal, <tt>false</tt> otherwise. */
317bool GradientImageParameters::operator!=(const GradientImageParameters &other) const
318{
319 return !(*this == other);
320}
321
322} // namespace PerceptualColor
The namespace of this library.
void setAlphaF(float alpha)
Format_ARGB32_Premultiplied
bool isNull() const const
void setDevicePixelRatio(qreal scaleFactor)
transparent
bool canConvert() const const
T value() const const
bool hasSameCoordinates(const LchaDouble &other) const
Compares coordinates with another object.
double a
Opacity (alpha channel)
Definition lchadouble.h:73
double h
Hue, measured in degree.
Definition lchadouble.h:68
double l
Lightness, mesured in percent.
Definition lchadouble.h:56
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:20:36 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.