File indexing completed on 2024-05-12 04:44:32
0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com> 0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT 0003 0004 // Own headers 0005 // First the interface, which forces the header to be self-contained. 0006 #include "colorwheelimage.h" 0007 0008 #include "cielchd50values.h" 0009 #include "helperconstants.h" 0010 #include "helperconversion.h" 0011 #include "helpermath.h" 0012 #include "polarpointf.h" 0013 #include "rgbcolorspace.h" 0014 #include <lcms2.h> 0015 #include <qbrush.h> 0016 #include <qmath.h> 0017 #include <qnamespace.h> 0018 #include <qpainter.h> 0019 #include <qpen.h> 0020 #include <qpoint.h> 0021 #include <qrect.h> 0022 #include <qrgb.h> 0023 #include <qsize.h> 0024 0025 namespace PerceptualColor 0026 { 0027 /** @brief Constructor 0028 * @param colorSpace The color space within which the image should operate. 0029 * Can be created with @ref RgbColorSpaceFactory. */ 0030 ColorWheelImage::ColorWheelImage(const QSharedPointer<PerceptualColor::RgbColorSpace> &colorSpace) 0031 : m_rgbColorSpace(colorSpace) 0032 { 0033 } 0034 0035 /** @brief Setter for the border property. 0036 * 0037 * The border is the space between the outer outline of the wheel and the 0038 * limits of the image. The wheel is always centered within the limits of 0039 * the image. The default value is <tt>0</tt>, which means that the wheel 0040 * touches the limits of the image. 0041 * 0042 * @param newBorder The new border size, measured in <em>physical 0043 * pixels</em>. */ 0044 void ColorWheelImage::setBorder(const qreal newBorder) 0045 { 0046 qreal tempBorder; 0047 if (newBorder >= 0) { 0048 tempBorder = newBorder; 0049 } else { 0050 tempBorder = 0; 0051 } 0052 if (m_borderPhysical != tempBorder) { 0053 m_borderPhysical = tempBorder; 0054 // Free the memory used by the old image. 0055 m_image = QImage(); 0056 } 0057 } 0058 0059 /** @brief Setter for the device pixel ratio (floating point). 0060 * 0061 * This value is set as device pixel ratio (floating point) in the 0062 * <tt>QImage</tt> that this class holds. It does <em>not</em> change 0063 * the <em>pixel</em> size of the image or the pixel size of wheel 0064 * thickness or border. 0065 * 0066 * This is for HiDPI support. You can set this to 0067 * <tt>QWidget::devicePixelRatioF()</tt> to get HiDPI images in the correct 0068 * resolution for your widgets. Within a method of a class derived 0069 * from <tt>QWidget</tt>, you could write: 0070 * 0071 * @snippet testcolorwheelimage.cpp ColorWheelImage HiDPI usage 0072 * 0073 * The default value is <tt>1</tt> which means no special scaling. 0074 * 0075 * @param newDevicePixelRatioF the new device pixel ratio as a 0076 * floating point data type. */ 0077 void ColorWheelImage::setDevicePixelRatioF(const qreal newDevicePixelRatioF) 0078 { 0079 qreal tempDevicePixelRatioF; 0080 if (newDevicePixelRatioF >= 1) { 0081 tempDevicePixelRatioF = newDevicePixelRatioF; 0082 } else { 0083 tempDevicePixelRatioF = 1; 0084 } 0085 if (m_devicePixelRatioF != tempDevicePixelRatioF) { 0086 m_devicePixelRatioF = tempDevicePixelRatioF; 0087 // Free the memory used by the old image. 0088 m_image = QImage(); 0089 } 0090 } 0091 0092 /** @brief Setter for the image size property. 0093 * 0094 * This value fixes the size of the image. The image will be a square 0095 * of <tt>QSize(newImageSize, newImageSize)</tt>. 0096 * 0097 * @param newImageSize The new image size, measured in <em>physical 0098 * pixels</em>. */ 0099 void ColorWheelImage::setImageSize(const int newImageSize) 0100 { 0101 int tempImageSize; 0102 if (newImageSize >= 0) { 0103 tempImageSize = newImageSize; 0104 } else { 0105 tempImageSize = 0; 0106 } 0107 if (m_imageSizePhysical != tempImageSize) { 0108 m_imageSizePhysical = tempImageSize; 0109 // Free the memory used by the old image. 0110 m_image = QImage(); 0111 } 0112 } 0113 0114 /** @brief Setter for the wheel thickness property. 0115 * 0116 * The wheel thickness is the distance between the inner outline and the 0117 * outer outline of the wheel. 0118 * 0119 * @param newWheelThickness The new wheel thickness, measured 0120 * in <em>physical pixels</em>. */ 0121 void ColorWheelImage::setWheelThickness(const qreal newWheelThickness) 0122 { 0123 qreal temp; 0124 if (newWheelThickness >= 0) { 0125 temp = newWheelThickness; 0126 } else { 0127 temp = 0; 0128 } 0129 if (m_wheelThicknessPhysical != temp) { 0130 m_wheelThicknessPhysical = temp; 0131 // Free the memory used by the old image. 0132 m_image = QImage(); 0133 } 0134 } 0135 0136 /** @brief Delivers an image of a color wheel 0137 * 0138 * @returns Delivers a square image of a color wheel. Its size 0139 * is <tt>QSize(imageSize, imageSize)</tt>. All pixels 0140 * that do not belong to the wheel itself will be transparent. 0141 * Antialiasing is used, so there is no sharp border between 0142 * transparent and non-transparent parts. Depending on the 0143 * values for lightness and chroma and the available colors in 0144 * the current color space, there may be some hue who is out of 0145 * gamut; if so, this part of the wheel will be transparent. 0146 * 0147 * @todo Out-of-gamut situations should automatically be handled. */ 0148 QImage ColorWheelImage::getImage() 0149 { 0150 // If image is in cache, simply return the cache. 0151 if (!m_image.isNull()) { 0152 return m_image; 0153 } 0154 0155 // If no cache is available (m_image.isNull()), render a new image. 0156 0157 // Special case: zero-size-image 0158 if (m_imageSizePhysical <= 0) { 0159 return m_image; 0160 } 0161 0162 // construct our final QImage with transparent background 0163 m_image = QImage(QSize(m_imageSizePhysical, m_imageSizePhysical), // 0164 QImage::Format_ARGB32_Premultiplied); 0165 m_image.fill(Qt::transparent); 0166 0167 // Calculate diameter of the outer circle 0168 const qreal outerCircleDiameter = // 0169 m_imageSizePhysical - 2 * m_borderPhysical; 0170 0171 // Special case: an empty image 0172 if (outerCircleDiameter <= 0) { 0173 // Make sure to return a completely transparent image. 0174 // If we would continue, in spite of an outer diameter of 0, 0175 // we might get a non-transparent pixel in the middle. 0176 // Set the correct scaling information for the image and return 0177 m_image.setDevicePixelRatio(m_devicePixelRatioF); 0178 return m_image; 0179 } 0180 0181 // Generate a temporary non-anti-aliased, intermediate, color wheel, 0182 // but with some pixels extra at the inner and outer side. The overlap 0183 // defines an overlap for the wheel, so there are some more pixels that 0184 // are drawn at the outer and at the inner border of the wheel, to allow 0185 // later clipping with anti-aliasing 0186 PolarPointF polarCoordinates; 0187 int x; 0188 int y; 0189 QRgb rgbColor; 0190 cmsCIELCh cielchD50; 0191 const qreal center = (m_imageSizePhysical - 1) / static_cast<qreal>(2); 0192 m_image = QImage(QSize(m_imageSizePhysical, m_imageSizePhysical), // 0193 QImage::Format_ARGB32_Premultiplied); 0194 // Because there may be out-of-gamut colors for some hue (depending on the 0195 // given lightness and chroma value) which are drawn transparent, it is 0196 // important to initialize this image with a transparent background. 0197 m_image.fill(Qt::transparent); 0198 cielchD50.L = CielchD50Values::neutralLightness; 0199 cielchD50.C = CielchD50Values::srgbVersatileChroma; 0200 // minimumRadius: Adding "+ 1" would reduce the workload (less pixel to 0201 // process) and still work mostly, but not completely. It creates sometimes 0202 // artifacts in the anti-aliasing process. So we don't do that. 0203 const qreal minimumRadius = // 0204 center - m_wheelThicknessPhysical - m_borderPhysical - overlap; 0205 const qreal maximumRadius = center - m_borderPhysical + overlap; 0206 for (x = 0; x < m_imageSizePhysical; ++x) { 0207 for (y = 0; y < m_imageSizePhysical; ++y) { 0208 polarCoordinates = PolarPointF(QPointF(x - center, center - y)); 0209 if (isInRange<qreal>(minimumRadius, polarCoordinates.radius(), maximumRadius) 0210 0211 ) { 0212 // We are within the wheel 0213 cielchD50.h = polarCoordinates.angleDegree(); 0214 rgbColor = m_rgbColorSpace->fromCielabD50ToQRgbOrTransparent( // 0215 toCmsLab(cielchD50)); 0216 if (qAlpha(rgbColor) != 0) { 0217 m_image.setPixelColor(x, y, rgbColor); 0218 } 0219 } 0220 } 0221 } 0222 0223 // Anti-aliased cut off everything outside the circle (that 0224 // means: the overlap) 0225 // The natural way would be to simply draw a circle with 0226 // QPainter::CompositionMode_DestinationIn which should make transparent 0227 // everything that is not in the circle. Unfortunately, this does not 0228 // seem to work. Therefore, we use a workaround and draw a very think 0229 // circle outline around the circle with QPainter::CompositionMode_Clear. 0230 const qreal circleRadius = outerCircleDiameter / 2; 0231 const qreal cutOffThickness = // 0232 qSqrt(qPow(m_imageSizePhysical, 2) * 2) / 2 // ½ of image diagonal 0233 - circleRadius // circle radius 0234 + overlap; // just to be sure 0235 QPainter myPainter(&m_image); 0236 myPainter.setRenderHint(QPainter::Antialiasing, true); 0237 myPainter.setPen(QPen(Qt::SolidPattern, cutOffThickness)); 0238 myPainter.setCompositionMode(QPainter::CompositionMode_Clear); 0239 const qreal halfImageSize = m_imageSizePhysical / static_cast<qreal>(2); 0240 myPainter.drawEllipse(QPointF(halfImageSize, halfImageSize), // center 0241 circleRadius + cutOffThickness / 2, // width 0242 circleRadius + cutOffThickness / 2 // height 0243 ); 0244 0245 // set the inner circle of the wheel to anti-aliased transparency 0246 const qreal innerCircleDiameter = // 0247 m_imageSizePhysical - 2 * (m_wheelThicknessPhysical + m_borderPhysical); 0248 if (innerCircleDiameter > 0) { 0249 myPainter.setCompositionMode(QPainter::CompositionMode_Clear); 0250 myPainter.setRenderHint(QPainter::Antialiasing, true); 0251 myPainter.setPen(QPen(Qt::NoPen)); 0252 myPainter.setBrush(QBrush(Qt::SolidPattern)); 0253 myPainter.drawEllipse( // 0254 QRectF(m_wheelThicknessPhysical + m_borderPhysical, // 0255 m_wheelThicknessPhysical + m_borderPhysical, // 0256 innerCircleDiameter, // 0257 innerCircleDiameter)); 0258 } 0259 0260 // Set the correct scaling information for the image and return 0261 m_image.setDevicePixelRatio(m_devicePixelRatioF); 0262 return m_image; 0263 } 0264 0265 } // namespace PerceptualColor