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