Perceptual Color

chromalightnessimageparameters.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 "chromalightnessimageparameters.h"
7
8#include "asyncimagerendercallback.h"
9#include "helperconversion.h"
10#include "helperimage.h"
11#include "helpermath.h"
12#include "rgbcolorspace.h"
13#include <lcms2.h>
14#include <qbitarray.h>
15#include <qimage.h>
16#include <qnamespace.h>
17#include <qrgb.h>
18
19namespace PerceptualColor
20{
21
22/** @brief Equal operator
23 *
24 * @param other The object to compare with.
25 *
26 * @returns <tt>true</tt> if equal, <tt>false</tt> otherwise. */
27bool ChromaLightnessImageParameters::operator==(const ChromaLightnessImageParameters &other) const
28{
29 return ( //
30 (hue == other.hue) //
31 && (imageSizePhysical == other.imageSizePhysical) //
32 && (rgbColorSpace == other.rgbColorSpace) //
33 );
34}
35
36/** @brief Unequal operator
37 *
38 * @param other The object to compare with.
39 *
40 * @returns <tt>true</tt> if unequal, <tt>false</tt> otherwise. */
41bool ChromaLightnessImageParameters::operator!=(const ChromaLightnessImageParameters &other) const
42{
43 return !(*this == other);
44}
45
46/** @brief Render an image.
47 *
48 * The function will render the image with the given parameters,
49 * and deliver the result by means of <tt>callbackObject</tt>.
50 *
51 * This function is thread-safe as long as each call of this function
52 * uses different <tt>variantParameters</tt> and <tt>callbackObject</tt>.
53 *
54 * @param variantParameters A <tt>QVariant</tt> that contains the
55 * image parameters.
56 * @param callbackObject Pointer to the object for the callbacks.
57 *
58 * @todo Interlacing support.
59 *
60 * @todo Could we get better performance? Even online tools like
61 * https://bottosson.github.io/misc/colorpicker/#ff2a00 or
62 * https://oklch.evilmartians.io/#65.4,0.136,146.7,100 get quite good
63 * performance. How do they do that? */
64void ChromaLightnessImageParameters::render(const QVariant &variantParameters, AsyncImageRenderCallback &callbackObject)
65{
66 if (!variantParameters.canConvert<ChromaLightnessImageParameters>()) {
67 return;
68 }
69 const ChromaLightnessImageParameters parameters = //
70 variantParameters.value<ChromaLightnessImageParameters>();
71
72 // From Qt Example’s documentation:
73 //
74 // “If we discover […] that restart has been set
75 // to true (by render()), we break out […] immediately […].
76 // Similarly, if we discover that abort has been set
77 // to true (by the […] destructor), we return from the
78 // function immediately […].”
79 if (callbackObject.shouldAbort()) {
80 return;
81 }
82 // Create a new QImage with correct image size.
83 QImage myImage(QSize(parameters.imageSizePhysical), //
85 // Test if image size is empty.
86 if (myImage.size().isEmpty()) {
87 // The image must be non-empty (otherwise, our algorithm would
88 // crash because of a division by 0).
89 callbackObject.deliverInterlacingPass( //
90 myImage, //
91 QImage(), //
92 QVariant::fromValue(parameters), //
93 AsyncImageRenderCallback::InterlacingState::Final);
94 return;
95 }
96
97 myImage.fill(Qt::transparent); // Initialize background color
98
99 // Initialization
100 cmsCIELCh cielchD50;
101 QRgb rgbColor;
102 int x;
103 int y;
104 const auto imageHeight = parameters.imageSizePhysical.height();
105 const auto imageWidth = parameters.imageSizePhysical.width();
106
107 // Paint the gamut.
108 const auto normalizedHue = normalizedAngle360(parameters.hue);
109 cielchD50.h = normalizedHue;
110 for (y = 0; y < imageHeight; ++y) {
111 if (callbackObject.shouldAbort()) {
112 return;
113 }
114 cielchD50.L = 100 - (y + 0.5) * 100.0 / imageHeight;
115 for (x = 0; x < imageWidth; ++x) {
116 // Using the same scale as on the y axis. floating point
117 // division thanks to 100 which is a "cmsFloat64Number"
118 cielchD50.C = (x + 0.5) * 100.0 / imageHeight;
119 rgbColor = //
120 parameters.rgbColorSpace->fromCielabD50ToQRgbOrTransparent( //
121 toCmsLab(cielchD50));
122 if (qAlpha(rgbColor) != 0) {
123 // The pixel is within the gamut
124 myImage.setPixelColor(x, y, rgbColor);
125 // If color is out-of-gamut: We have chroma on the x axis and
126 // lightness on the y axis. We are drawing the pixmap line per
127 // line, so we go for given lightness from low chroma to high
128 // chroma. Because of the nature of many gamuts, if once in a
129 // line we have an out-of-gamut value, often all other pixels
130 // that are more at the right will be out-of-gamut also. So we
131 // could optimize our code and break here. But as we are not
132 // sure about this: It’s just likely, but not always correct.
133 // We do not know the gamut at compile time, so
134 // for the moment we do not optimize the code.
135 }
136 }
137 }
138
139 if (callbackObject.shouldAbort()) {
140 return;
141 }
142
143 // A 1-bit mask for the gamut.
144 // transparent = white
145 // opaque = black
146 const auto myMask = myImage.createAlphaMask();
147
148 callbackObject.deliverInterlacingPass( //
149 myImage, //
150 myMask, //
151 QVariant::fromValue(parameters), //
152 AsyncImageRenderCallback::InterlacingState::Intermediate);
153
154 // cppcheck-suppress knownConditionTrueFalse // false positive
155 if (callbackObject.shouldAbort()) {
156 return;
157 }
158
159 QList<QPoint> antiAliasCoordinates = findBoundary(myImage);
160
161 // cppcheck-suppress knownConditionTrueFalse // false positive
162 if (callbackObject.shouldAbort()) {
163 return;
164 }
165
166 const auto myColorFunction = [normalizedHue, imageHeight, parameters](const double colorFunctionX, const double colorFunctionY) -> QRgb {
167 cmsCIELCh myCielchD50;
168 myCielchD50.h = normalizedHue;
169 myCielchD50.L = 100 - (colorFunctionY + 0.5) * 100.0 / imageHeight;
170 myCielchD50.C = (colorFunctionX + 0.5) * 100.0 / imageHeight;
171 return parameters.rgbColorSpace->fromCielabD50ToQRgbOrTransparent( //
172 toCmsLab(myCielchD50));
173 };
174 doAntialias(myImage, antiAliasCoordinates, myColorFunction);
175
176 if (callbackObject.shouldAbort()) {
177 return;
178 }
179
180 callbackObject.deliverInterlacingPass( //
181 myImage, //
182 myMask, //
183 QVariant::fromValue(parameters), //
184 AsyncImageRenderCallback::InterlacingState::Final);
185}
186
187} // namespace PerceptualColor
The namespace of this library.
void doAntialias(QImage &image, const QList< QPoint > &antiAliasCoordinates, const std::function< QRgb(const double x, const double y)> &colorFunction)
Calculates anti-alias for gamut diagrams.
QList< QPoint > findBoundary(const QImage &image)
Find boundaries between fully opaque and fully transparent pixels.
Format_ARGB32_Premultiplied
transparent
bool canConvert() const const
QVariant fromValue(T &&value)
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 25 2025 12:03:13 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.