Perceptual Color

colorwheelimage.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 "colorwheelimage.h"
7
8#include "cielchd50values.h"
9#include "helperconstants.h"
10#include "helperconversion.h"
11#include "helpermath.h"
12#include "polarpointf.h"
13#include "rgbcolorspace.h"
14#include <lcms2.h>
15#include <qbrush.h>
16#include <qmath.h>
17#include <qnamespace.h>
18#include <qpainter.h>
19#include <qpen.h>
20#include <qpoint.h>
21#include <qrect.h>
22#include <qrgb.h>
23#include <qsize.h>
24
25namespace PerceptualColor
26{
27/** @brief Constructor
28 * @param colorSpace The color space within which the image should operate.
29 * Can be created with @ref RgbColorSpaceFactory. */
30ColorWheelImage::ColorWheelImage(const QSharedPointer<PerceptualColor::RgbColorSpace> &colorSpace)
31 : m_rgbColorSpace(colorSpace)
32{
33}
34
35/** @brief Setter for the border property.
36 *
37 * The border is the space between the outer outline of the wheel and the
38 * limits of the image. The wheel is always centered within the limits of
39 * the image. The default value is <tt>0</tt>, which means that the wheel
40 * touches the limits of the image.
41 *
42 * @param newBorder The new border size, measured in <em>physical
43 * pixels</em>. */
44void ColorWheelImage::setBorder(const qreal newBorder)
45{
46 qreal tempBorder;
47 if (newBorder >= 0) {
48 tempBorder = newBorder;
49 } else {
50 tempBorder = 0;
51 }
52 if (m_borderPhysical != tempBorder) {
53 m_borderPhysical = tempBorder;
54 // Free the memory used by the old image.
55 m_image = QImage();
56 }
57}
58
59/** @brief Setter for the device pixel ratio (floating point).
60 *
61 * This value is set as device pixel ratio (floating point) in the
62 * <tt>QImage</tt> that this class holds. It does <em>not</em> change
63 * the <em>pixel</em> size of the image or the pixel size of wheel
64 * thickness or border.
65 *
66 * This is for HiDPI support. You can set this to
67 * <tt>QWidget::devicePixelRatioF()</tt> to get HiDPI images in the correct
68 * resolution for your widgets. Within a method of a class derived
69 * from <tt>QWidget</tt>, you could write:
70 *
71 * @snippet testcolorwheelimage.cpp ColorWheelImage HiDPI usage
72 *
73 * The default value is <tt>1</tt> which means no special scaling.
74 *
75 * @param newDevicePixelRatioF the new device pixel ratio as a
76 * floating point data type. */
77void ColorWheelImage::setDevicePixelRatioF(const qreal newDevicePixelRatioF)
78{
79 qreal tempDevicePixelRatioF;
80 if (newDevicePixelRatioF >= 1) {
81 tempDevicePixelRatioF = newDevicePixelRatioF;
82 } else {
83 tempDevicePixelRatioF = 1;
84 }
85 if (m_devicePixelRatioF != tempDevicePixelRatioF) {
86 m_devicePixelRatioF = tempDevicePixelRatioF;
87 // Free the memory used by the old image.
88 m_image = QImage();
89 }
90}
91
92/** @brief Setter for the image size property.
93 *
94 * This value fixes the size of the image. The image will be a square
95 * of <tt>QSize(newImageSize, newImageSize)</tt>.
96 *
97 * @param newImageSize The new image size, measured in <em>physical
98 * pixels</em>. */
99void ColorWheelImage::setImageSize(const int newImageSize)
100{
101 int tempImageSize;
102 if (newImageSize >= 0) {
103 tempImageSize = newImageSize;
104 } else {
105 tempImageSize = 0;
106 }
107 if (m_imageSizePhysical != tempImageSize) {
108 m_imageSizePhysical = tempImageSize;
109 // Free the memory used by the old image.
110 m_image = QImage();
111 }
112}
113
114/** @brief Setter for the wheel thickness property.
115 *
116 * The wheel thickness is the distance between the inner outline and the
117 * outer outline of the wheel.
118 *
119 * @param newWheelThickness The new wheel thickness, measured
120 * in <em>physical pixels</em>. */
121void ColorWheelImage::setWheelThickness(const qreal newWheelThickness)
122{
123 qreal temp;
124 if (newWheelThickness >= 0) {
125 temp = newWheelThickness;
126 } else {
127 temp = 0;
128 }
129 if (m_wheelThicknessPhysical != temp) {
130 m_wheelThicknessPhysical = temp;
131 // Free the memory used by the old image.
132 m_image = QImage();
133 }
134}
135
136/** @brief Delivers an image of a color wheel
137 *
138 * @returns Delivers a square image of a color wheel. Its size
139 * is <tt>QSize(imageSize, imageSize)</tt>. All pixels
140 * that do not belong to the wheel itself will be transparent.
141 * Antialiasing is used, so there is no sharp border between
142 * transparent and non-transparent parts. Depending on the
143 * values for lightness and chroma and the available colors in
144 * the current color space, there may be some hue who is out of
145 * gamut; if so, this part of the wheel will be transparent.
146 *
147 * @todo Out-of-gamut situations should automatically be handled. */
148QImage ColorWheelImage::getImage()
149{
150 // If image is in cache, simply return the cache.
151 if (!m_image.isNull()) {
152 return m_image;
153 }
154
155 // If no cache is available (m_image.isNull()), render a new image.
156
157 // Special case: zero-size-image
158 if (m_imageSizePhysical <= 0) {
159 return m_image;
160 }
161
162 // construct our final QImage with transparent background
163 m_image = QImage(QSize(m_imageSizePhysical, m_imageSizePhysical), //
165 m_image.fill(Qt::transparent);
166
167 // Calculate diameter of the outer circle
168 const qreal outerCircleDiameter = //
169 m_imageSizePhysical - 2 * m_borderPhysical;
170
171 // Special case: an empty image
172 if (outerCircleDiameter <= 0) {
173 // Make sure to return a completely transparent image.
174 // If we would continue, in spite of an outer diameter of 0,
175 // we might get a non-transparent pixel in the middle.
176 // Set the correct scaling information for the image and return
177 m_image.setDevicePixelRatio(m_devicePixelRatioF);
178 return m_image;
179 }
180
181 // Generate a temporary non-anti-aliased, intermediate, color wheel,
182 // but with some pixels extra at the inner and outer side. The overlap
183 // defines an overlap for the wheel, so there are some more pixels that
184 // are drawn at the outer and at the inner border of the wheel, to allow
185 // later clipping with anti-aliasing
186 PolarPointF polarCoordinates;
187 int x;
188 int y;
189 QRgb rgbColor;
190 cmsCIELCh cielchD50;
191 const qreal center = (m_imageSizePhysical - 1) / static_cast<qreal>(2);
192 m_image = QImage(QSize(m_imageSizePhysical, m_imageSizePhysical), //
194 // Because there may be out-of-gamut colors for some hue (depending on the
195 // given lightness and chroma value) which are drawn transparent, it is
196 // important to initialize this image with a transparent background.
197 m_image.fill(Qt::transparent);
198 cielchD50.L = CielchD50Values::neutralLightness;
199 cielchD50.C = CielchD50Values::srgbVersatileChroma;
200 // minimumRadius: Adding "+ 1" would reduce the workload (less pixel to
201 // process) and still work mostly, but not completely. It creates sometimes
202 // artifacts in the anti-aliasing process. So we don't do that.
203 const qreal minimumRadius = //
204 center - m_wheelThicknessPhysical - m_borderPhysical - overlap;
205 const qreal maximumRadius = center - m_borderPhysical + overlap;
206 for (x = 0; x < m_imageSizePhysical; ++x) {
207 for (y = 0; y < m_imageSizePhysical; ++y) {
208 polarCoordinates = PolarPointF(QPointF(x - center, center - y));
209 if (isInRange<qreal>(minimumRadius, polarCoordinates.radius(), maximumRadius)
210
211 ) {
212 // We are within the wheel
213 cielchD50.h = polarCoordinates.angleDegree();
214 rgbColor = m_rgbColorSpace->fromCielabD50ToQRgbOrTransparent( //
215 toCmsLab(cielchD50));
216 if (qAlpha(rgbColor) != 0) {
217 m_image.setPixelColor(x, y, rgbColor);
218 }
219 }
220 }
221 }
222
223 // Anti-aliased cut off everything outside the circle (that
224 // means: the overlap)
225 // The natural way would be to simply draw a circle with
226 // QPainter::CompositionMode_DestinationIn which should make transparent
227 // everything that is not in the circle. Unfortunately, this does not
228 // seem to work. Therefore, we use a workaround and draw a very think
229 // circle outline around the circle with QPainter::CompositionMode_Clear.
230 const qreal circleRadius = outerCircleDiameter / 2;
231 const qreal cutOffThickness = //
232 qSqrt(qPow(m_imageSizePhysical, 2) * 2) / 2 // ½ of image diagonal
233 - circleRadius // circle radius
234 + overlap; // just to be sure
235 QPainter myPainter(&m_image);
236 myPainter.setRenderHint(QPainter::Antialiasing, true);
237 myPainter.setPen(QPen(Qt::SolidPattern, cutOffThickness));
238 myPainter.setCompositionMode(QPainter::CompositionMode_Clear);
239 const qreal halfImageSize = m_imageSizePhysical / static_cast<qreal>(2);
240 myPainter.drawEllipse(QPointF(halfImageSize, halfImageSize), // center
241 circleRadius + cutOffThickness / 2, // width
242 circleRadius + cutOffThickness / 2 // height
243 );
244
245 // set the inner circle of the wheel to anti-aliased transparency
246 const qreal innerCircleDiameter = //
247 m_imageSizePhysical - 2 * (m_wheelThicknessPhysical + m_borderPhysical);
248 if (innerCircleDiameter > 0) {
249 myPainter.setCompositionMode(QPainter::CompositionMode_Clear);
250 myPainter.setRenderHint(QPainter::Antialiasing, true);
251 myPainter.setPen(QPen(Qt::NoPen));
252 myPainter.setBrush(QBrush(Qt::SolidPattern));
253 myPainter.drawEllipse( //
254 QRectF(m_wheelThicknessPhysical + m_borderPhysical, //
255 m_wheelThicknessPhysical + m_borderPhysical, //
256 innerCircleDiameter, //
257 innerCircleDiameter));
258 }
259
260 // Set the correct scaling information for the image and return
261 m_image.setDevicePixelRatio(m_devicePixelRatioF);
262 return m_image;
263}
264
265} // namespace PerceptualColor
The namespace of this library.
Format_ARGB32_Premultiplied
CompositionMode_Clear
SolidPattern
transparent
QTextStream & center(QTextStream &stream)
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.