File indexing completed on 2024-05-12 04:44:30
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 "chromalightnessimageparameters.h" 0007 0008 #include "asyncimagerendercallback.h" 0009 #include "helperconversion.h" 0010 #include "helpermath.h" 0011 #include "rgbcolorspace.h" 0012 #include <lcms2.h> 0013 #include <qbitarray.h> 0014 #include <qimage.h> 0015 #include <qnamespace.h> 0016 #include <qrgb.h> 0017 0018 namespace PerceptualColor 0019 { 0020 0021 /** @brief Equal operator 0022 * 0023 * @param other The object to compare with. 0024 * 0025 * @returns <tt>true</tt> if equal, <tt>false</tt> otherwise. */ 0026 bool ChromaLightnessImageParameters::operator==(const ChromaLightnessImageParameters &other) const 0027 { 0028 return ( // 0029 (hue == other.hue) // 0030 && (imageSizePhysical == other.imageSizePhysical) // 0031 && (rgbColorSpace == other.rgbColorSpace) // 0032 ); 0033 } 0034 0035 /** @brief Unequal operator 0036 * 0037 * @param other The object to compare with. 0038 * 0039 * @returns <tt>true</tt> if unequal, <tt>false</tt> otherwise. */ 0040 bool ChromaLightnessImageParameters::operator!=(const ChromaLightnessImageParameters &other) const 0041 { 0042 return !(*this == other); 0043 } 0044 0045 /** @brief Render an image. 0046 * 0047 * The function will render the image with the given parameters, 0048 * and deliver the result by means of <tt>callbackObject</tt>. 0049 * 0050 * This function is thread-safe as long as each call of this function 0051 * uses different <tt>variantParameters</tt> and <tt>callbackObject</tt>. 0052 * 0053 * @param variantParameters A <tt>QVariant</tt> that contains the 0054 * image parameters. 0055 * @param callbackObject Pointer to the object for the callbacks. 0056 * 0057 * @todo Interlacing support. 0058 * 0059 * @todo Could we get better performance? Even online tools like 0060 * https://bottosson.github.io/misc/colorpicker/#ff2a00 or 0061 * https://oklch.evilmartians.io/#65.4,0.136,146.7,100 get quite good 0062 * performance. How do they do that? */ 0063 void ChromaLightnessImageParameters::render(const QVariant &variantParameters, AsyncImageRenderCallback &callbackObject) 0064 { 0065 if (!variantParameters.canConvert<ChromaLightnessImageParameters>()) { 0066 return; 0067 } 0068 const ChromaLightnessImageParameters parameters = // 0069 variantParameters.value<ChromaLightnessImageParameters>(); 0070 0071 // From Qt Example’s documentation: 0072 // 0073 // “If we discover […] that restart has been set 0074 // to true (by render()), we break out […] immediately […]. 0075 // Similarly, if we discover that abort has been set 0076 // to true (by the […] destructor), we return from the 0077 // function immediately […].” 0078 if (callbackObject.shouldAbort()) { 0079 return; 0080 } 0081 // Create a new QImage with correct image size. 0082 QImage myImage(QSize(parameters.imageSizePhysical), // 0083 QImage::Format_ARGB32_Premultiplied); 0084 // A mask for the gamut. 0085 // In-gamut pixel are true, out-of-gamut pixel are false. 0086 QBitArray m_mask(parameters.imageSizePhysical.width() // 0087 * parameters.imageSizePhysical.height(), 0088 // Initial boolean value of all bits (true/false): 0089 false); 0090 // Test if image size is empty. 0091 if (myImage.size().isEmpty()) { 0092 // The image must be non-empty (otherwise, our algorithm would 0093 // crash because of a division by 0). 0094 callbackObject.deliverInterlacingPass( // 0095 myImage, // 0096 QVariant::fromValue(parameters), // 0097 AsyncImageRenderCallback::InterlacingState::Final); 0098 return; 0099 } 0100 0101 myImage.fill(Qt::transparent); // Initialize background color 0102 0103 // Initialization 0104 cmsCIELCh cielchD50; 0105 QRgb rgbColor; 0106 int x; 0107 int y; 0108 const auto imageHeight = parameters.imageSizePhysical.height(); 0109 const auto imageWidth = parameters.imageSizePhysical.width(); 0110 0111 // Paint the gamut. 0112 cielchD50.h = normalizedAngle360(parameters.hue); 0113 for (y = 0; y < imageHeight; ++y) { 0114 if (callbackObject.shouldAbort()) { 0115 return; 0116 } 0117 cielchD50.L = 100 - (y + 0.5) * 100.0 / imageHeight; 0118 for (x = 0; x < imageWidth; ++x) { 0119 // Using the same scale as on the y axis. floating point 0120 // division thanks to 100 which is a "cmsFloat64Number" 0121 cielchD50.C = (x + 0.5) * 100.0 / imageHeight; 0122 rgbColor = // 0123 parameters.rgbColorSpace->fromCielabD50ToQRgbOrTransparent( // 0124 toCmsLab(cielchD50)); 0125 if (qAlpha(rgbColor) != 0) { 0126 // The pixel is within the gamut 0127 myImage.setPixelColor(x, y, rgbColor); 0128 m_mask.setBit(maskIndex(x, y, parameters.imageSizePhysical), // 0129 true); 0130 // If color is out-of-gamut: We have chroma on the x axis and 0131 // lightness on the y axis. We are drawing the pixmap line per 0132 // line, so we go for given lightness from low chroma to high 0133 // chroma. Because of the nature of many gamuts, if once in a 0134 // line we have an out-of-gamut value, often all other pixels 0135 // that are more at the right will be out-of-gamut also. So we 0136 // could optimize our code and break here. But as we are not 0137 // sure about this: It’s just likely, but not always correct. 0138 // We do not know the gamut at compile time, so 0139 // for the moment we do not optimize the code. 0140 } 0141 } 0142 } 0143 callbackObject.deliverInterlacingPass( // 0144 myImage, // 0145 QVariant::fromValue(parameters), // 0146 AsyncImageRenderCallback::InterlacingState::Final); 0147 } 0148 0149 } // namespace PerceptualColor