Perceptual Color

chromahueimageparameters.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 "chromahueimageparameters.h"
7
8#include "asyncimagerendercallback.h"
9#include "cielchd50values.h"
10#include "helperconstants.h"
11#include "helpermath.h"
12#include "interlacingpass.h"
13#include "rgbcolorspace.h"
14#include <lcms2.h>
15#include <qcolor.h>
16#include <qimage.h>
17#include <qmath.h>
18#include <qnamespace.h>
19#include <qpainter.h>
20#include <qrgb.h>
21#include <qsharedpointer.h>
22#include <qsize.h>
23#include <type_traits>
24
25namespace PerceptualColor
26{
27/** @brief Equal operator
28 *
29 * @param other The object to compare with.
30 *
31 * @returns <tt>true</tt> if equal, <tt>false</tt> otherwise. */
32bool ChromaHueImageParameters::operator==(const ChromaHueImageParameters &other) const
33{
34 return ( //
35 (borderPhysical == other.borderPhysical) //
36 && (devicePixelRatioF == other.devicePixelRatioF) //
37 && (imageSizePhysical == other.imageSizePhysical) //
38 && (lightness == other.lightness) //
39 && (rgbColorSpace == other.rgbColorSpace) //
40 );
41}
42
43/** @brief Unequal operator
44 *
45 * @param other The object to compare with.
46 *
47 * @returns <tt>true</tt> if unequal, <tt>false</tt> otherwise. */
48bool ChromaHueImageParameters::operator!=(const ChromaHueImageParameters &other) const
49{
50 return !(*this == other);
51}
52
53/** @brief Render an image.
54 *
55 * The function will render the image with the given parameters,
56 * and deliver the result of each interlacing pass and also the final
57 * result by means of <tt>callbackObject</tt>.
58 *
59 * This function is thread-safe as long as each call of this function
60 * uses different <tt>variantParameters</tt> and <tt>callbackObject</tt>.
61 *
62 * @param variantParameters A <tt>QVariant</tt> that contains the
63 * image parameters.
64 * @param callbackObject Pointer to the object for the callbacks.
65 *
66 * @todo Could we get better performance? Even online tools like
67 * https://bottosson.github.io/misc/colorpicker/#ff2a00 or
68 * https://oklch.evilmartians.io/#65.4,0.136,146.7,100 get quite good
69 * performance. How do they do that? */
70void ChromaHueImageParameters::render(const QVariant &variantParameters, AsyncImageRenderCallback &callbackObject)
71{
72 if (!variantParameters.canConvert<ChromaHueImageParameters>()) {
73 return;
74 }
75 const ChromaHueImageParameters parameters = //
76 variantParameters.value<ChromaHueImageParameters>();
77
78 // From Qt Example’s documentation:
79 //
80 // “If we discover […] that restart has been set
81 // to true (by render()), we break out […] immediately […].
82 // Similarly, if we discover that abort has been set
83 // to true (by the […] destructor), we return from the
84 // function immediately […].”
85 if (callbackObject.shouldAbort()) {
86 return;
87 }
88 // Create a new QImage with correct image size.
89 QImage myImage(
90 // size:
91 QSize(parameters.imageSizePhysical, parameters.imageSizePhysical),
92 // format:
94 // Calculate the radius of the circle we want to paint (and which will
95 // finally have the background color, while everything around will be
96 // transparent).
97 const qreal circleRadius = //
98 (parameters.imageSizePhysical - 2 * parameters.borderPhysical) / 2.;
99 if ((circleRadius <= 0) || parameters.rgbColorSpace.isNull()) {
100 // The border is too big the and image size too small: The size
101 // of the circle is zero. Or: There is no color space with which
102 // we can work (and dereferencing parameters.rgbColorSpace will
103 // crash).
104 // In either case: The image will therefore be transparent.
105 // Initialize the image as completely transparent and return.
106 myImage.fill(Qt::transparent);
107 // Set the correct scaling information for the image and return
108 myImage.setDevicePixelRatio(parameters.devicePixelRatioF);
109 callbackObject.deliverInterlacingPass( //
110 myImage, //
111 variantParameters, //
112 AsyncImageRenderCallback::InterlacingState::Final);
113 return;
114 }
115
116 // If we continue, the circle will at least be visible.
117
118 const QColor myNeutralGray = //
119 parameters.rgbColorSpace->fromCielchD50ToQRgbBound(CielchD50Values::neutralGray);
120
121 // Initialize the hole image background to the background color
122 // of the circle:
123 myImage.fill(myNeutralGray);
124
125 // Prepare for gamut painting
126 cmsCIELab cielabD50;
127 cielabD50.L = parameters.lightness;
128 int x;
129 int y;
130 QRgb tempColor;
131 const auto chromaRange = parameters.rgbColorSpace->profileMaximumCielchD50Chroma();
132 const qreal scaleFactor = static_cast<qreal>(2 * chromaRange)
133 // The following line will never be 0 because we have have
134 // tested above that circleRadius is > 0, so this line will
135 // we > 0 also.
136 / (parameters.imageSizePhysical - 2 * parameters.borderPhysical);
137
138 // Paint the gamut.
139 // The pixel at position QPoint(x, y) is the square with the top-left
140 // edge at coordinate point QPoint(x, y) and the bottom-right edge at
141 // coordinate point QPoint(x+1, y+1). This pixel is supposed to have
142 // the color from coordinate point QPoint(x+0.5, y+0.5), which is
143 // the middle of this pixel. Therefore, with an offset of 0.5 we
144 // can convert from the pixel position to the point in the middle of
145 // the pixel.
146 constexpr qreal pixelOffset = 0.5;
147 // TODO Could this be further optimized? For example not go from zero
148 // up to imageSizePhysical, but exclude the border (and add the
149 // tolerance)? Thought anyway the color transform (which is the heavy
150 // work) is only done when within a given diameter, reducing loop runs
151 // itself might also increase performance at least a little bit…
152 constexpr auto numberOfPasses = 11;
153 static_assert(isOdd(numberOfPasses));
154 InterlacingPass currentPass = InterlacingPass::make<numberOfPasses>();
155 QPainter myPainter(&myImage);
156 myPainter.setRenderHint(QPainter::Antialiasing, false);
157 while (true) {
158 for (y = currentPass.lineOffset; //
159 y < parameters.imageSizePhysical; //
160 y += currentPass.lineFrequency) //
161 {
162 if (callbackObject.shouldAbort()) {
163 return;
164 }
165 cielabD50.b = chromaRange //
166 - (y + pixelOffset - parameters.borderPhysical) * scaleFactor;
167 for (x = currentPass.columnOffset; //
168 x < parameters.imageSizePhysical; //
169 x += currentPass.columnFrequency //
170 ) {
171 cielabD50.a = //
172 (x + pixelOffset - parameters.borderPhysical) * scaleFactor //
173 - chromaRange;
174 if ( //
175 (qPow(cielabD50.a, 2) + qPow(cielabD50.b, 2)) //
176 <= (qPow(chromaRange + overlap, 2)) //
177 ) {
178 tempColor = parameters //
179 .rgbColorSpace //
180 ->fromCielabD50ToQRgbOrTransparent(cielabD50);
181 if (qAlpha(tempColor) != 0) {
182 // The pixel is within the gamut!
183 myPainter.fillRect(
184 //
185 x, //
186 y, //
187 currentPass.rectangleSize.width(), //
188 currentPass.rectangleSize.height(), //
189 QColor(tempColor));
190 } else {
191 myPainter.fillRect(
192 //
193 x, //
194 y, //
195 currentPass.rectangleSize.width(), //
196 currentPass.rectangleSize.height(), //
197 myNeutralGray);
198 }
199 }
200 }
201 }
202
203 const AsyncImageRenderCallback::InterlacingState state = //
204 (currentPass.countdown > 1) //
205 ? AsyncImageRenderCallback::InterlacingState::Intermediate //
206 : AsyncImageRenderCallback::InterlacingState::Final;
207
208 myImage.setDevicePixelRatio(parameters.devicePixelRatioF);
209 callbackObject.deliverInterlacingPass(myImage, variantParameters, state);
210 myImage.setDevicePixelRatio(1);
211
212 if (state == AsyncImageRenderCallback::InterlacingState::Intermediate) {
213 currentPass.switchToNextPass();
214 } else {
215 return;
216 }
217 }
218}
219
220static_assert(std::is_standard_layout_v<ChromaHueImageParameters>);
221
222} // namespace PerceptualColor
The namespace of this library.
int lightness() const const
Format_ARGB32_Premultiplied
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 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.