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