File indexing completed on 2024-05-12 04:44:29

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 "chromahueimageparameters.h"
0007 
0008 #include "asyncimagerendercallback.h"
0009 #include "cielchd50values.h"
0010 #include "helperconstants.h"
0011 #include "helpermath.h"
0012 #include "interlacingpass.h"
0013 #include "rgbcolorspace.h"
0014 #include <lcms2.h>
0015 #include <qcolor.h>
0016 #include <qimage.h>
0017 #include <qmath.h>
0018 #include <qnamespace.h>
0019 #include <qpainter.h>
0020 #include <qrgb.h>
0021 #include <qsharedpointer.h>
0022 #include <qsize.h>
0023 #include <type_traits>
0024 
0025 namespace PerceptualColor
0026 {
0027 /** @brief Equal operator
0028  *
0029  * @param other The object to compare with.
0030  *
0031  * @returns <tt>true</tt> if equal, <tt>false</tt> otherwise. */
0032 bool ChromaHueImageParameters::operator==(const ChromaHueImageParameters &other) const
0033 {
0034     return ( //
0035         (borderPhysical == other.borderPhysical) //
0036         && (devicePixelRatioF == other.devicePixelRatioF) //
0037         && (imageSizePhysical == other.imageSizePhysical) //
0038         && (lightness == other.lightness) //
0039         && (rgbColorSpace == other.rgbColorSpace) //
0040     );
0041 }
0042 
0043 /** @brief Unequal operator
0044  *
0045  * @param other The object to compare with.
0046  *
0047  * @returns <tt>true</tt> if unequal, <tt>false</tt> otherwise. */
0048 bool ChromaHueImageParameters::operator!=(const ChromaHueImageParameters &other) const
0049 {
0050     return !(*this == other);
0051 }
0052 
0053 /** @brief Render an image.
0054  *
0055  * The function will render the image with the given parameters,
0056  * and deliver the result of each interlacing pass and also the final
0057  * result by means of <tt>callbackObject</tt>.
0058  *
0059  * This function is thread-safe as long as each call of this function
0060  * uses different <tt>variantParameters</tt> and <tt>callbackObject</tt>.
0061  *
0062  * @param variantParameters A <tt>QVariant</tt> that contains the
0063  *        image parameters.
0064  * @param callbackObject Pointer to the object for the callbacks.
0065  *
0066  * @todo Could we get better performance? Even online tools like
0067  * https://bottosson.github.io/misc/colorpicker/#ff2a00 or
0068  * https://oklch.evilmartians.io/#65.4,0.136,146.7,100 get quite good
0069  * performance. How do they do that? */
0070 void ChromaHueImageParameters::render(const QVariant &variantParameters, AsyncImageRenderCallback &callbackObject)
0071 {
0072     if (!variantParameters.canConvert<ChromaHueImageParameters>()) {
0073         return;
0074     }
0075     const ChromaHueImageParameters parameters = //
0076         variantParameters.value<ChromaHueImageParameters>();
0077 
0078     // From Qt Example’s documentation:
0079     //
0080     //     “If we discover […] that restart has been set
0081     //      to true (by render()), we break out […] immediately […].
0082     //      Similarly, if we discover that abort has been set
0083     //      to true (by the […] destructor), we return from the
0084     //      function immediately […].”
0085     if (callbackObject.shouldAbort()) {
0086         return;
0087     }
0088     // Create a new QImage with correct image size.
0089     QImage myImage(
0090         // size:
0091         QSize(parameters.imageSizePhysical, parameters.imageSizePhysical),
0092         // format:
0093         QImage::Format_ARGB32_Premultiplied);
0094     // Calculate the radius of the circle we want to paint (and which will
0095     // finally have the background color, while everything around will be
0096     // transparent).
0097     const qreal circleRadius = //
0098         (parameters.imageSizePhysical - 2 * parameters.borderPhysical) / 2.;
0099     if ((circleRadius <= 0) || parameters.rgbColorSpace.isNull()) {
0100         // The border is too big the and image size too small: The size
0101         // of the circle is zero. Or: There is no color space with which
0102         // we can work (and dereferencing parameters.rgbColorSpace will
0103         // crash).
0104         // In either case: The image will therefore be transparent.
0105         // Initialize the image as completely transparent and return.
0106         myImage.fill(Qt::transparent);
0107         // Set the correct scaling information for the image and return
0108         myImage.setDevicePixelRatio(parameters.devicePixelRatioF);
0109         callbackObject.deliverInterlacingPass( //
0110             myImage, //
0111             variantParameters, //
0112             AsyncImageRenderCallback::InterlacingState::Final);
0113         return;
0114     }
0115 
0116     // If we continue, the circle will at least be visible.
0117 
0118     const QColor myNeutralGray = //
0119         parameters.rgbColorSpace->fromCielchD50ToQRgbBound(CielchD50Values::neutralGray);
0120 
0121     // Initialize the hole image background to the background color
0122     // of the circle:
0123     myImage.fill(myNeutralGray);
0124 
0125     // Prepare for gamut painting
0126     cmsCIELab cielabD50;
0127     cielabD50.L = parameters.lightness;
0128     int x;
0129     int y;
0130     QRgb tempColor;
0131     const auto chromaRange = parameters.rgbColorSpace->profileMaximumCielchD50Chroma();
0132     const qreal scaleFactor = static_cast<qreal>(2 * chromaRange)
0133         // The following line will never be 0 because we have have
0134         // tested above that circleRadius is > 0, so this line will
0135         // we > 0 also.
0136         / (parameters.imageSizePhysical - 2 * parameters.borderPhysical);
0137 
0138     // Paint the gamut.
0139     // The pixel at position QPoint(x, y) is the square with the top-left
0140     // edge at coordinate point QPoint(x, y) and the bottom-right edge at
0141     // coordinate point QPoint(x+1, y+1). This pixel is supposed to have
0142     // the color from coordinate point QPoint(x+0.5, y+0.5), which is
0143     // the middle of this pixel. Therefore, with an offset of 0.5 we
0144     // can convert from the pixel position to the point in the middle of
0145     // the pixel.
0146     constexpr qreal pixelOffset = 0.5;
0147     // TODO Could this be further optimized? For example not go from zero
0148     // up to imageSizePhysical, but exclude the border (and add the
0149     // tolerance)? Thought anyway the color transform (which is the heavy
0150     // work) is only done when within a given diameter, reducing loop runs
0151     // itself might also increase performance at least a little bit…
0152     constexpr auto numberOfPasses = 11;
0153     static_assert(isOdd(numberOfPasses));
0154     InterlacingPass currentPass = InterlacingPass::make<numberOfPasses>();
0155     QPainter myPainter(&myImage);
0156     myPainter.setRenderHint(QPainter::Antialiasing, false);
0157     while (true) {
0158         for (y = currentPass.lineOffset; //
0159              y < parameters.imageSizePhysical; //
0160              y += currentPass.lineFrequency) //
0161         {
0162             if (callbackObject.shouldAbort()) {
0163                 return;
0164             }
0165             cielabD50.b = chromaRange //
0166                 - (y + pixelOffset - parameters.borderPhysical) * scaleFactor;
0167             for (x = currentPass.columnOffset; //
0168                  x < parameters.imageSizePhysical; //
0169                  x += currentPass.columnFrequency //
0170             ) {
0171                 cielabD50.a = //
0172                     (x + pixelOffset - parameters.borderPhysical) * scaleFactor //
0173                     - chromaRange;
0174                 if ( //
0175                     (qPow(cielabD50.a, 2) + qPow(cielabD50.b, 2)) //
0176                     <= (qPow(chromaRange + overlap, 2)) //
0177                 ) {
0178                     tempColor = parameters //
0179                                     .rgbColorSpace //
0180                                     ->fromCielabD50ToQRgbOrTransparent(cielabD50);
0181                     if (qAlpha(tempColor) != 0) {
0182                         // The pixel is within the gamut!
0183                         myPainter.fillRect(
0184                             //
0185                             x, //
0186                             y, //
0187                             currentPass.rectangleSize.width(), //
0188                             currentPass.rectangleSize.height(), //
0189                             QColor(tempColor));
0190                     } else {
0191                         myPainter.fillRect(
0192                             //
0193                             x, //
0194                             y, //
0195                             currentPass.rectangleSize.width(), //
0196                             currentPass.rectangleSize.height(), //
0197                             myNeutralGray);
0198                     }
0199                 }
0200             }
0201         }
0202 
0203         const AsyncImageRenderCallback::InterlacingState state = //
0204             (currentPass.countdown > 1) //
0205             ? AsyncImageRenderCallback::InterlacingState::Intermediate //
0206             : AsyncImageRenderCallback::InterlacingState::Final;
0207 
0208         myImage.setDevicePixelRatio(parameters.devicePixelRatioF);
0209         callbackObject.deliverInterlacingPass(myImage, variantParameters, state);
0210         myImage.setDevicePixelRatio(1);
0211 
0212         if (state == AsyncImageRenderCallback::InterlacingState::Intermediate) {
0213             currentPass.switchToNextPass();
0214         } else {
0215             return;
0216         }
0217     }
0218 }
0219 
0220 static_assert(std::is_standard_layout_v<ChromaHueImageParameters>);
0221 
0222 } // namespace PerceptualColor