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