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

KDE's Doxygen guidelines are available online.