File indexing completed on 2024-05-12 04:44:33
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 "gradientimageparameters.h" 0007 0008 #include "asyncimagerendercallback.h" 0009 #include "helper.h" 0010 #include "helperqttypes.h" 0011 #include "lchadouble.h" 0012 #include "lchdouble.h" 0013 #include "rgbcolorspace.h" 0014 #include <cmath> 0015 #include <qbrush.h> 0016 #include <qcolor.h> 0017 #include <qimage.h> 0018 #include <qnamespace.h> 0019 #include <qpainter.h> 0020 #include <qsharedpointer.h> 0021 0022 namespace PerceptualColor 0023 { 0024 /** @brief Constructor */ 0025 GradientImageParameters::GradientImageParameters() 0026 { 0027 setFirstColor(LchaDouble{0, 0, 0, 1}); 0028 setFirstColor(LchaDouble{1000, 0, 0, 1}); 0029 } 0030 0031 /** @brief Normalizes the value and bounds it to the LCH color space. 0032 * @param color the color that should be treated. 0033 * @returns A normalized and bounded version. If the chroma was negative, 0034 * it gets positive (which implies turning the hue by 180°). The hue is 0035 * normalized to the range <tt>[0°, 360°[</tt>. Lightness is bounded to the 0036 * range <tt>[0, 100]</tt>. Alpha is bounded to the range <tt>[0, 1]</tt>. */ 0037 LchaDouble GradientImageParameters::completlyNormalizedAndBounded(const LchaDouble &color) 0038 { 0039 LchaDouble result; 0040 if (color.c < 0) { 0041 result.c = color.c * (-1); 0042 result.h = fmod(color.h + 180, 360); 0043 } else { 0044 result.c = color.c; 0045 result.h = fmod(color.h, 360); 0046 } 0047 if (result.h < 0) { 0048 result.h += 360; 0049 } 0050 result.l = qBound<qreal>(0, color.l, 100); 0051 result.a = qBound<qreal>(0, color.a, 1); 0052 return result; 0053 } 0054 0055 /** @brief Setter for the first color property. 0056 * @param newFirstColor The new first color. 0057 * @sa @ref m_firstColorCorrected */ 0058 void GradientImageParameters::setFirstColor(const LchaDouble &newFirstColor) 0059 { 0060 LchaDouble correctedNewFirstColor = // 0061 completlyNormalizedAndBounded(newFirstColor); 0062 if (!m_firstColorCorrected.hasSameCoordinates(correctedNewFirstColor)) { 0063 m_firstColorCorrected = correctedNewFirstColor; 0064 updateSecondColor(); 0065 // Free the memory used by the old image. 0066 m_image = QImage(); 0067 } 0068 } 0069 0070 /** @brief Setter for the second color property. 0071 * @param newSecondColor The new second color. 0072 * @sa @ref m_secondColorCorrectedAndAltered */ 0073 void GradientImageParameters::setSecondColor(const LchaDouble &newSecondColor) 0074 { 0075 LchaDouble correctedNewSecondColor = // 0076 completlyNormalizedAndBounded(newSecondColor); 0077 if (!m_secondColorCorrectedAndAltered.hasSameCoordinates(correctedNewSecondColor)) { 0078 m_secondColorCorrectedAndAltered = correctedNewSecondColor; 0079 updateSecondColor(); 0080 // Free the memory used by the old image. 0081 m_image = QImage(); 0082 } 0083 } 0084 0085 /** @brief Updates @ref m_secondColorCorrectedAndAltered 0086 * 0087 * This update takes into account the current values of 0088 * @ref m_firstColorCorrected and @ref m_secondColorCorrectedAndAltered. */ 0089 void GradientImageParameters::updateSecondColor() 0090 { 0091 m_secondColorCorrectedAndAltered = // 0092 completlyNormalizedAndBounded(m_secondColorCorrectedAndAltered); 0093 if (qAbs(m_firstColorCorrected.h - m_secondColorCorrectedAndAltered.h) > 180) { 0094 if (m_firstColorCorrected.h > m_secondColorCorrectedAndAltered.h) { 0095 m_secondColorCorrectedAndAltered.h += 360; 0096 } else { 0097 m_secondColorCorrectedAndAltered.h -= 360; 0098 } 0099 } 0100 } 0101 0102 /** @brief Render an image. 0103 * 0104 * The function will render the image with the given parameters, 0105 * and deliver the result by means of <tt>callbackObject</tt>. 0106 * 0107 * This function is thread-safe as long as each call of this function 0108 * uses different <tt>variantParameters</tt> and <tt>callbackObject</tt>. 0109 * 0110 * @param variantParameters A <tt>QVariant</tt> that contains the 0111 * image parameters. 0112 * @param callbackObject Pointer to the object for the callbacks. 0113 * 0114 * @todo Could we get better performance? Even online tools like 0115 * https://bottosson.github.io/misc/colorpicker/#ff2a00 or 0116 * https://oklch.evilmartians.io/#65.4,0.136,146.7,100 get quite good 0117 * performance. How do they do that? */ 0118 void GradientImageParameters::render(const QVariant &variantParameters, AsyncImageRenderCallback &callbackObject) 0119 { 0120 if (!variantParameters.canConvert<GradientImageParameters>()) { 0121 return; 0122 } 0123 const GradientImageParameters parameters = // 0124 variantParameters.value<GradientImageParameters>(); 0125 if (parameters.rgbColorSpace.isNull()) { 0126 return; 0127 } 0128 0129 // From Qt Example’s documentation: 0130 // 0131 // “If we discover […] that restart has been set 0132 // to true (by render()), we break out […] immediately […]. 0133 // Similarly, if we discover that abort has been set 0134 // to true (by the […] destructor), we return from the 0135 // function immediately […].” 0136 if (callbackObject.shouldAbort()) { 0137 return; 0138 } 0139 0140 // First, create an image of the gradient with only one pixel thickness. 0141 // (Color management operations are expensive in CPU time; we try to 0142 // minimize this.) 0143 QImage onePixelLine(parameters.m_gradientLength, // 0144 1, // 0145 QImage::Format_ARGB32_Premultiplied); 0146 onePixelLine.fill(Qt::transparent); // Initialize image with transparency. 0147 LchaDouble color; 0148 LchDouble cielchD50; 0149 QColor temp; 0150 for (int i = 0; i < parameters.m_gradientLength; ++i) { 0151 color = parameters.colorFromValue( // 0152 (i + 0.5) / static_cast<qreal>(parameters.m_gradientLength)); 0153 cielchD50.l = color.l; 0154 cielchD50.c = color.c; 0155 cielchD50.h = color.h; 0156 temp = parameters.rgbColorSpace->fromCielchD50ToQRgbBound(cielchD50); 0157 temp.setAlphaF( 0158 // Reduce floating point precision if necessary. 0159 static_cast<QColorFloatType>(color.a)); 0160 onePixelLine.setPixelColor(i, 0, temp); 0161 } 0162 if (callbackObject.shouldAbort()) { 0163 return; 0164 } 0165 0166 // Now, create a full image of the gradient 0167 QImage result = QImage(parameters.m_gradientLength, // 0168 parameters.m_gradientThickness, // 0169 QImage::Format_ARGB32_Premultiplied); 0170 if (result.isNull()) { 0171 // Make sure that no QPainter can be created on a null image 0172 // (because this would trigger warning messages on the command 0173 // line). 0174 return; 0175 } 0176 QPainter painter(&result); 0177 0178 // Transparency background 0179 if ( // 0180 (parameters.m_firstColorCorrected.a != 1) // 0181 || (parameters.m_secondColorCorrectedAndAltered.a != 1) // 0182 ) { 0183 // Fill the image with tiles. (QBrush will ignore 0184 // the devicePixelRatioF of the image of the tile.) 0185 const auto background = transparencyBackground( // 0186 parameters.m_devicePixelRatioF); 0187 painter.fillRect(0, // 0188 0, // 0189 parameters.m_gradientLength, // 0190 parameters.m_gradientThickness, // 0191 QBrush(background)); 0192 } 0193 0194 // Paint the gradient itself. 0195 for (int i = 0; i < parameters.m_gradientThickness; ++i) { 0196 painter.drawImage(0, i, onePixelLine); 0197 } 0198 0199 result.setDevicePixelRatio(parameters.m_devicePixelRatioF); 0200 0201 if (callbackObject.shouldAbort()) { 0202 return; 0203 } 0204 0205 callbackObject.deliverInterlacingPass( // 0206 result, // 0207 variantParameters, // 0208 AsyncImageRenderCallback::InterlacingState::Final); 0209 } 0210 0211 /** @brief The color that the gradient has at a given position of the gradient. 0212 * @param value The position. Valid range: <tt>[0.0, 1.0]</tt>. <tt>0.0</tt> 0213 * means the first color, <tt>1.0</tt> means the second color, and everything 0214 * in between means a color in between. 0215 * @returns If the position is valid: The color at the given position and 0216 * its corresponding alpha value. If the position is out-of-range: An 0217 * arbitrary value. */ 0218 LchaDouble GradientImageParameters::colorFromValue(qreal value) const 0219 { 0220 LchaDouble color; 0221 color.l = m_firstColorCorrected.l // 0222 + (m_secondColorCorrectedAndAltered.l - m_firstColorCorrected.l) * value; 0223 color.c = m_firstColorCorrected.c + // 0224 (m_secondColorCorrectedAndAltered.c - m_firstColorCorrected.c) * value; 0225 color.h = m_firstColorCorrected.h + // 0226 (m_secondColorCorrectedAndAltered.h - m_firstColorCorrected.h) * value; 0227 color.a = m_firstColorCorrected.a + // 0228 (m_secondColorCorrectedAndAltered.a - m_firstColorCorrected.a) * value; 0229 return color; 0230 } 0231 0232 /** @brief Setter for the device pixel ratio (floating point). 0233 * 0234 * This value is set as device pixel ratio (floating point) in the 0235 * <tt>QImage</tt> that this class holds. It does <em>not</em> change 0236 * the <em>pixel</em> size of the image or the pixel size of wheel 0237 * thickness or border. 0238 * 0239 * This is for HiDPI support. You can set this to 0240 * <tt>QWidget::devicePixelRatioF()</tt> to get HiDPI images in the correct 0241 * resolution for your widgets. Within a method of a class derived 0242 * from <tt>QWidget</tt>, you could write: 0243 * 0244 * @snippet testgradientimageparameters.cpp GradientImage HiDPI usage 0245 * 0246 * The default value is <tt>1</tt> which means no special scaling. 0247 * 0248 * @param newDevicePixelRatioF the new device pixel ratio as a 0249 * floating point data type. (Values smaller than <tt>1.0</tt> will be 0250 * considered as <tt>1.0</tt>.) */ 0251 void GradientImageParameters::setDevicePixelRatioF(const qreal newDevicePixelRatioF) 0252 { 0253 const qreal tempDevicePixelRatioF = qMax<qreal>(1, newDevicePixelRatioF); 0254 if (m_devicePixelRatioF != tempDevicePixelRatioF) { 0255 m_devicePixelRatioF = tempDevicePixelRatioF; 0256 // Free the memory used by the old image. 0257 m_image = QImage(); 0258 } 0259 } 0260 0261 /** @brief Setter for the gradient length property. 0262 * 0263 * @param newGradientLength The new gradient length, measured 0264 * in <em>physical pixels</em>. */ 0265 void GradientImageParameters::setGradientLength(const int newGradientLength) 0266 { 0267 const int temp = qMax(0, newGradientLength); 0268 if (m_gradientLength != temp) { 0269 m_gradientLength = temp; 0270 // Free the memory used by the old image. 0271 m_image = QImage(); 0272 } 0273 } 0274 0275 /** @brief Setter for the gradient thickness property. 0276 * 0277 * @param newGradientThickness The new gradient thickness, measured 0278 * in <em>physical pixels</em>. */ 0279 void GradientImageParameters::setGradientThickness(const int newGradientThickness) 0280 { 0281 const int temp = qMax(0, newGradientThickness); 0282 if (m_gradientThickness != temp) { 0283 m_gradientThickness = temp; 0284 // Free the memory used by the old image. 0285 m_image = QImage(); 0286 } 0287 } 0288 0289 /** @brief Equal operator 0290 * 0291 * @param other The object to compare with. 0292 * 0293 * @returns <tt>true</tt> if equal, <tt>false</tt> otherwise. */ 0294 bool GradientImageParameters::operator==(const GradientImageParameters &other) const 0295 { 0296 return ( // 0297 (m_devicePixelRatioF == other.m_devicePixelRatioF) // 0298 && (m_firstColorCorrected.l == other.m_firstColorCorrected.l) // 0299 && (m_firstColorCorrected.c == other.m_firstColorCorrected.c) // 0300 && (m_firstColorCorrected.h == other.m_firstColorCorrected.h) // 0301 && (m_firstColorCorrected.a == other.m_firstColorCorrected.a) // 0302 && (m_gradientLength == other.m_gradientLength) // 0303 && (m_gradientThickness == other.m_gradientThickness) // 0304 && (rgbColorSpace == other.rgbColorSpace) // 0305 && (m_secondColorCorrectedAndAltered.l == other.m_secondColorCorrectedAndAltered.l) // 0306 && (m_secondColorCorrectedAndAltered.c == other.m_secondColorCorrectedAndAltered.c) // 0307 && (m_secondColorCorrectedAndAltered.h == other.m_secondColorCorrectedAndAltered.h) // 0308 && (m_secondColorCorrectedAndAltered.a == other.m_secondColorCorrectedAndAltered.a) // 0309 ); 0310 } 0311 0312 /** @brief Unequal operator 0313 * 0314 * @param other The object to compare with. 0315 * 0316 * @returns <tt>true</tt> if unequal, <tt>false</tt> otherwise. */ 0317 bool GradientImageParameters::operator!=(const GradientImageParameters &other) const 0318 { 0319 return !(*this == other); 0320 } 0321 0322 } // namespace PerceptualColor