File indexing completed on 2025-07-13 04:19:55
0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com> 0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT 0003 0004 // Own header 0005 #include "helper.h" 0006 0007 #include "absolutecolor.h" 0008 #include "genericcolor.h" 0009 #include "helperconversion.h" 0010 #include "initializelibraryresources.h" 0011 #include "rgbcolorspace.h" 0012 #include <array> 0013 #include <qchar.h> 0014 #include <qcolor.h> 0015 #include <qevent.h> 0016 #include <qkeysequence.h> 0017 #include <qpainter.h> 0018 #include <qpixmap.h> 0019 #include <qpoint.h> 0020 #include <qsize.h> 0021 #include <qstringliteral.h> 0022 #include <qstyle.h> 0023 #include <qstyleoption.h> 0024 #include <qwidget.h> 0025 0026 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) 0027 #include <qlist.h> 0028 #else 0029 #include <qstringlist.h> 0030 #endif 0031 0032 #ifndef PERCEPTUALCOLORINTERNAL 0033 #include <type_traits> 0034 #include <utility> 0035 #endif 0036 0037 namespace PerceptualColor 0038 { 0039 /** @internal 0040 * 0041 * @brief Number of vertical <em>standard</em> wheel steps done by a 0042 * wheel event 0043 * 0044 * As the QWheelEvent documentation explains, there is a common physical 0045 * standard wheel step size for mouse wheels: 15°. But there are some 0046 * mouse models which use non-standard physical wheel step sizes for 0047 * their mouse wheel, for example because they have a higher wheel 0048 * resolution. 0049 * 0050 * This function converts the values in a QMouseEvent to the 0051 * <em>standard</em> wheel step count. 0052 * 0053 * @param event the QWheelEvent 0054 * @returns the count of vertical <em>standard</em> wheel steps done 0055 * within this mouse event. The value is positive for up-steps and 0056 * negative for down-steps. On a standard mouse wheel, moving the wheel 0057 * one physical step up will return the value 1. On a non-standard, 0058 * higher resolution mouse wheel, moving the wheel one physical step up 0059 * will return a smaller value, for example 0.7 */ 0060 qreal standardWheelStepCount(QWheelEvent *event) 0061 { 0062 // QWheelEvent::angleDelta() returns 8 units for each degree. 0063 // The standard wheel step is 15°. So on a standard 0064 // mouse, one wheel step results in (8 × 15) units. 0065 return event->angleDelta().y() / static_cast<qreal>(8 * 15); 0066 } 0067 0068 /** @internal 0069 * 0070 * @brief Background for semi-transparent colors. 0071 * 0072 * When showing a semi-transparent color, there has to be a background 0073 * on which it is shown. This function provides a suitable background 0074 * for showcasing a color. 0075 * 0076 * @param devicePixelRatioF The desired device-pixel ratio. Must be ≥ 1. 0077 * 0078 * @returns An image of a mosaic of neutral gray rectangles of different 0079 * lightness. You can use this as tiles to paint a background, starting from 0080 * the top-left corner. This image is made for LTR layouts. If you have an 0081 * RTL layout, you should horizontally mirror your paint buffer after painting 0082 * the tiles. The image has its device pixel ratio set to the value that was 0083 * given in the parameter. 0084 * 0085 * @note The image is considering the given device-pixel ratio to deliver 0086 * sharp (and correctly scaled) images also for HiDPI devices. 0087 * The painting does not use floating point drawing, but rounds 0088 * to full integers. Therefore, the result is always a sharp image. 0089 * This function takes care that each square has the same pixel size, 0090 * without scaling errors or anti-aliasing errors. 0091 * 0092 * @sa @ref AbstractDiagram::transparencyBackground() 0093 * 0094 * @todo Provide color management support! Currently, we use the same 0095 * value for red, green and blue, this might <em>not</em> be perfectly 0096 * neutral gray depending on the color profile of the monitor… And: We 0097 * should make sure that transparent colors are not applied by Qt on top 0098 * of this image. Instead, add a parameter to this function to get the 0099 * transparent color to paint above, and do color-managed overlay of the 0100 * transparent color, in Lch space. For each Lab (not Lch!) channel: 0101 * result = opacity * foreground + (100% - opacity) * background. */ 0102 QImage transparencyBackground(qreal devicePixelRatioF) 0103 { 0104 // The valid lightness range is [0, 255]. The median is 127/128. 0105 // We use two color with equal distance to this median to get a 0106 // neutral gray. 0107 constexpr int lightnessDistance = 15; 0108 constexpr int lightnessOne = 127 - lightnessDistance; 0109 constexpr int lightnessTwo = 128 + lightnessDistance; 0110 constexpr int squareSizeInLogicalPixel = 10; 0111 const int squareSize = qRound(squareSizeInLogicalPixel * devicePixelRatioF); 0112 0113 QImage temp(squareSize * 2, squareSize * 2, QImage::Format_RGB32); 0114 temp.fill(QColor(lightnessOne, lightnessOne, lightnessOne)); 0115 QPainter painter(&temp); 0116 QColor foregroundColor(lightnessTwo, lightnessTwo, lightnessTwo); 0117 painter.fillRect(0, 0, squareSize, squareSize, foregroundColor); 0118 painter.fillRect(squareSize, squareSize, squareSize, squareSize, foregroundColor); 0119 temp.setDevicePixelRatio(devicePixelRatioF); 0120 return temp; 0121 } 0122 0123 /** @internal 0124 * 0125 * @brief Draws a QWidget respecting Qt Style Sheets. 0126 * 0127 * When subclassing QWidget-derived classes, the Qt Style Sheets are 0128 * considered automatically. But when subclassing QWidget itself, the 0129 * Qt Style Sheets are <em>not</em> considered automatically. Also, 0130 * calling <tt>QWidget::paintEvent()</tt> from the subclass’s paint 0131 * event does not help. Instead, call this function from within your 0132 * subclass’s paint event. It uses some special code as documented 0133 * in the <em>Qt Style Sheets Reference</em> in the section about QWidget. 0134 * 0135 * @warning This function creates a QPainter for the widget. As there 0136 * should be not more than one QPainter at the same time for a given 0137 * paint device, you may not call this function while a QPainter 0138 * exists for the widget. Therefore, it is best to call this 0139 * function as very first statement in your paintEvent() implementation, 0140 * before initializing any QPainter. 0141 * 0142 * @param widget the widget */ 0143 void drawQWidgetStyleSheetAware(QWidget *widget) 0144 { 0145 QStyleOption opt; 0146 opt.initFrom(widget); 0147 QPainter p(widget); 0148 widget->style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, widget); 0149 } 0150 0151 /** @internal 0152 * 0153 * @brief Provides prefix and suffix of a value from a given format string. 0154 * 0155 * A typical use case: You want to put a percent value into a spinbox. The 0156 * simple approach would be: 0157 * @snippet testhelper.cpp percentTraditional 0158 * It could be improved like this: 0159 * @snippet testhelper.cpp percentImproved 0160 * However, in some languages, the position of the prefix and suffix may be 0161 * reversed compared to English. Example: In English, you write 50\%, but in 0162 * Turkish, you write \%50. Qt does not offer an out-of-the-box solution for 0163 * this. This helper now provides complete internationalization for prefixes 0164 * and suffixes of spin boxes, allowing to do this easily in a fail-safe way: 0165 * @snippet testhelper.cpp percentFullyInternationalized 0166 * 0167 * @param formatString A translated string in the format "prefix%1suffix". It 0168 * should contain exactly <em>one</em> place marker as described in 0169 * <tt>QString::arg()</tt> like <tt>\%1</tt> or <tt>\%L2</tt>. This place 0170 * marker represents the value. Example: “Prefix\%1Suffix”. Prefix and suffix 0171 * may be empty. 0172 * 0173 * @returns If the <tt>formatString</tt> parameter has the correct format, 0174 * the prefix will be returned at <tt>QPair::first</tt> and the suffix will 0175 * be returned at <tt>QPair::second</tt>. Otherwise, they will be set to an 0176 * empty string. */ 0177 [[nodiscard]] QPair<QString, QString> getPrefixSuffix(const QString &formatString) 0178 { 0179 // QString::arg() support for %L2, %5 etc which translators might expect: 0180 const auto list = formatString // 0181 .arg(QStringLiteral("%1")) // 0182 .split(QStringLiteral("%1")); 0183 if (list.count() == 2) { 0184 return QPair<QString, QString>(list.at(0), list.at(1)); 0185 } 0186 return QPair<QString, QString>(QString(), QString()); 0187 } 0188 0189 /** @internal 0190 * 0191 * @brief Icon from theme. 0192 * 0193 * @param names List of names, preferred names first. The system’s icon 0194 * themes are searched for this. 0195 * @param fallback If the system icon themes do not provide an icon, use 0196 * this fallback icon from the built-in resources. 0197 * @param type Type of widget color scheme for which the fallback icon (if 0198 * used) should be suitable. 0199 * 0200 * @returns An icon from the system icons and or a fallback icons. If none is 0201 * available, an empty icon. */ 0202 QIcon qIconFromTheme(const QStringList &names, const QString &fallback, ColorSchemeType type) 0203 { 0204 #ifdef PERCEPTUALCOLORINTERNAL 0205 Q_UNUSED(names) 0206 #else 0207 // Try to find icon in theme 0208 for (auto const &name : std::as_const(names)) { 0209 const QIcon myIcon = QIcon::fromTheme(name); 0210 if (!myIcon.isNull()) { 0211 return myIcon; 0212 } 0213 } 0214 #endif 0215 0216 // Return fallback icon 0217 initializeLibraryResources(); 0218 QString path = QStringLiteral( // 0219 ":/PerceptualColor/icons/lighttheme/%1.svg"); 0220 if (type == ColorSchemeType::Dark) { 0221 path = QStringLiteral( // 0222 ":/PerceptualColor/icons/darktheme/%1.svg"); 0223 } 0224 return QIcon(path.arg(fallback)); 0225 } 0226 0227 /** @internal 0228 * 0229 * @brief Converts text with mnemonics to rich text rendering the mnemonics. 0230 * 0231 * At some places in Qt, mnemonics are used. For example, setting 0232 * <tt>QPushButton::setText()</tt> to "T&est" will make appear the text 0233 * "Test". If mnemonic support is enabled in the current platform theme, 0234 * the "e" is underlined. 0235 * 0236 * At some other places in Qt, rich text is used. For example, setting 0237 * <tt>QWidget::setToolTip()</tt> to "T<u>e</u>st" will make appear the text 0238 * "Test", but with the "e" underlined. 0239 * 0240 * @param mnemonicText A text that might contain mnemonics 0241 * 0242 * @returns A rich text that will render in rich-text-functions with the same 0243 * rendering as if the mnemonic text would have been 0244 * used in mnemonic-functions: If currently in the platform theme, 0245 * auto-mnemonics are enabled, the mnemonics are underlined. Otherwise, 0246 * the mnemonics are not underlined nor is the “&” character visible 0247 * 0248 * @note This function mimics Qt’s algorithm form mnemonic rendering quite 0249 * well, but there might be subtile differences in corner cases. Like Qt, 0250 * this function accepts multiple occurrences of "&" in the same string, even 0251 * before different characters, and underlines all of them, though 0252 * <tt>QKeySequence::mnemonic()</tt> will return only one of them as 0253 * shortcut. */ 0254 QString fromMnemonicToRichText(const QString &mnemonicText) 0255 { 0256 QString result; 0257 0258 const bool doUnderline = !QKeySequence::mnemonic(mnemonicText).isEmpty(); 0259 const auto underlineStart = doUnderline ? QStringLiteral("<u>") : QString(); 0260 const auto underlineStop = doUnderline ? QStringLiteral("</u>") : QString(); 0261 0262 bool underlineNextChar = false; 0263 for (int i = 0; i < mnemonicText.length(); ++i) { 0264 if (mnemonicText[i] == QStringLiteral("&")) { 0265 const auto nextChar = // 0266 (i + 1 < mnemonicText.length()) // 0267 ? mnemonicText[i + 1] 0268 : QChar(); 0269 if (nextChar == QStringLiteral("&")) { 0270 // Double ampersand: Escape the "&" 0271 result.append(QStringLiteral("&")); 0272 i++; // Skip the second "&" 0273 } else { 0274 // Single ampersand: Start underline 0275 underlineNextChar = true; 0276 } 0277 } else { 0278 if (underlineNextChar) { 0279 // End underline 0280 result.append(underlineStart); 0281 result.append(mnemonicText[i]); 0282 result.append(underlineStop); 0283 underlineNextChar = false; 0284 } else { 0285 result.append(mnemonicText[i]); 0286 } 0287 } 0288 } 0289 0290 return result; 0291 } 0292 0293 /** @internal 0294 * 0295 * @brief Guess the actual @ref ColorSchemeType of a given widget. 0296 * 0297 * It guesses the color scheme type actually used by the current widget style, 0298 * and not the type of the current color palette. This makes a difference 0299 * for example on the Windows Vista style, which might ignore the palette and 0300 * use always a light theme instead. 0301 * 0302 * @param widget The widget to evaluate 0303 * 0304 * @returns The guessed schema, or <tt>std::nullopt</tt> if 0305 * nothing could be guessed. 0306 * 0307 * @note The exact implementation of the guess might change over time. 0308 * 0309 * @internal 0310 * 0311 * The current implementation takes a screenshot of the widget and calculates 0312 * the average lightness of this screenshot and determines the color schema 0313 * type accordingly. It returns <tt>std::nullopt</tt> if the widget 0314 * is <tt>nullptr</tt> or its size is empty. 0315 * 0316 * @note With Qt 6.5, there is 0317 * <a href="https://www.qt.io/blog/dark-mode-on-windows-11-with-qt-6.5"> 0318 * better access to color themes</a>. Apparently, the Windows Vista style 0319 * now seems to polish the widgets by setting a light color palette, so 0320 * also on Windows Vista style we could simply rely on the color palette 0321 * and test if the text color is lighter or darker than the background color 0322 * to determine the color scheme type. This would also give us more reliable 0323 * results with color schemes that have background colors around 50% lightness, 0324 * which our currently implementation has problems to get right. But on 0325 * the other hand, other styles like Kvantum might still chose to ignore 0326 * the color palette, so it seems safer to stay with the current 0327 * implementation. */ 0328 std::optional<ColorSchemeType> guessColorSchemeTypeFromWidget(QWidget *widget) 0329 { 0330 if (widget == nullptr) { 0331 return std::nullopt; 0332 } 0333 0334 // Take a screenshot of the widget 0335 const QImage screenshot = widget->grab().toImage(); 0336 if (screenshot.size().isEmpty()) { 0337 return std::nullopt; 0338 } 0339 0340 // Calculate the average lightness of the screenshot 0341 QColorFloatType lightnessSum = 0; 0342 for (int y = 0; y < screenshot.height(); ++y) { 0343 for (int x = 0; x < screenshot.width(); ++x) { 0344 lightnessSum += QColor(screenshot.pixel(x, y)).lightnessF(); 0345 } 0346 } 0347 const auto pixelCount = screenshot.width() * screenshot.height(); 0348 constexpr QColorFloatType threeshold = 0.5; 0349 const bool isDark = // 0350 (lightnessSum / static_cast<QColorFloatType>(pixelCount)) < threeshold; 0351 if (isDark) { 0352 return ColorSchemeType::Dark; 0353 } 0354 return ColorSchemeType::Light; 0355 } 0356 0357 /** @brief Palette derived from the basic colors as by WCS (World color 0358 * survey). 0359 * 0360 * The palette contains various tints and shades of the 0361 * basic colors. The choice of the basic colors is based on the 0362 * <a href="https://en.wikipedia.org/wiki/Basic_Color_Terms:_Their_Universality_and_Evolution"> 0363 * study by Brent Berlin and Paul Kay</a>, who suggest that the 0364 * basic color terms in almost all languages on earth follow a universal 0365 * pattern. They propose that there are eleven basic color terms that appear 0366 * in this order during the evolution of a language: 0367 * 0368 * 1. black, white 0369 * 2. red 0370 * 3. green, yellow 0371 * 4. blue 0372 * 5. brown 0373 * 6. purple, pink, orange, gray 0374 * 0375 * Additionally, people worldwide seem to agree quite well on the typical 0376 * values of each of these color terms. This theory is a fascinating one 0377 * and forms a good basis for choosing basic colors for this palette. 0378 * 0379 * This widget's colors have been arranged largely according to the color 0380 * wheel of the perceptually uniform color space. We start with the saturated 0381 * basic colors: red, orange, yellow, green, blue, and purple in order of 0382 * their hue angles. Next, we have pink and brown, which have roughly the 0383 * same hue as red or orange but are less saturated. These are simply the 0384 * less chromatic parts of this hue but are nevertheless perceived by humans as 0385 * independent colors. For each of these basic colors, there are five variants 0386 * in the order of <a href="https://en.wikipedia.org/wiki/Tints_and_shades"> 0387 * tint, pure color, and shade</a>. Following the saturated colors and 0388 * eventually the less saturated ones, the gray axis comes in last place. 0389 * 0390 * What exact colors are used? What exactly is a typical “red” or a 0391 * “green”? Doesn’t every human have a slightly different 0392 * feeling what a “typical” red or a “typical” blue is? We 0393 * need a <em>focal color</em>, which is, according to 0394 * <a href="https://www.oxfordreference.com/display/10.1093/oi/authority.20110803095825870"> 0395 * Oxford Reference</a>: 0396 * 0397 * > “A colour that is a prototypical instance of a particular colour name, 0398 * > such as a shade of red that a majority of viewers consider to be the 0399 * > best example of a red colour.” 0400 * 0401 * The <a href="https://www1.icsi.berkeley.edu/wcs/">World Color Survey</a> 0402 * (WCS) is a significant study about focal colors of speakers of different 0403 * languages across the world. The data from this survey is available online, 0404 * and while it does not provide direct values for focal colors, various 0405 * studies have used this data to determine focal colors for some 0406 * <em>basic color terms</em> and a naming centroid for others. 0407 * 0408 * The table below shows the WCS grid coordinates for the basic color terms 0409 * along with the corresponding Cielab values for the focal color (where 0410 * available) or the naming centroid (where focal color data is unavailable). 0411 * 0412 * |Basic color term|WCS grid coordinates|Cielab³ L|Cielab³ a|Cielab³ b| 0413 * | :--------------| -----------------: | ------: | ------: | ------: | 0414 * | white¹ | A0 | 96.00 | -0.06 | 0.06 | 0415 * | black¹ | J0 | 15.60 | -0.02 | 0.02 | 0416 * | red¹ | G1 | 41.22 | 61.40 | 17.92 | 0417 * | yellow¹ | C9 | 81.35 | 7.28 | 109.12 | 0418 * | green¹ | F17 | 51.57 | -63.28 | 28.95 | 0419 * | blue¹ | F29 | 51.57 | -3.41 | -48.08 | 0420 * | brown³ | G7 | 41.22 | 17.04 | 45.95 | 0421 * | purple³ | G34 | 41.22 | 33.08 | -30.50 | 0422 * | pink³ | E1 | 61.70 | 49.42 | 18.23 | 0423 * | orange³ | E6 | 61.70| 29.38 | 64.40 | 0424 * | gray | not available | | | | 0425 * 0426 * ¹ Focal color as proposed by 0427 * <a href="https://www.pnas.org/doi/10.1073/pnas.0503281102">Focal colors 0428 * are universal after all</a>. 0429 * 0430 * ² Raw estimation of the naming centroid based on Fig. 4 of 0431 * <a href="https://sites.socsci.uci.edu/~kjameson/ECST/Kay_Cook_WorldColorSurvey.pdf"> 0432 * this document</a>. (Fig. 5 would be the better choice, as it gives the 0433 * focal color instead of the naming centroid, but unfortunately contains 0434 * only red, yellow, green and blue, for which we have yet direct data.) 0435 * 0436 * ³ <a href="https://www1.icsi.berkeley.edu/wcs/data/cnum-maps/cnum-vhcm-lab-new.txt"> 0437 * Lookup table providing Lab values for WCS grid coordinates</a> and the 0438 * <a href="https://www1.icsi.berkeley.edu/wcs/data/cnum-maps/cnum-vhcm-lab-new-README.txt"> 0439 * corresponding explanation</a>. 0440 * 0441 * From this data, the colors in our palette have been derived as follows: 0442 * - The gray axis has been defined manually, ignoring the WCS data. Chroma 0443 * is 0. The lightness is 100% for white, 0% for black, and 75%, 50%, 0444 * and 25% for the intermediate grays. 0445 * - The other columns for chromatic colors use the WCS data for the swatch in 0446 * the middle. Tints and shades are calculated by adding or reducing chroma 0447 * and lightness within the Oklab color space. If the resulting color falls 0448 * outside the color space, a nearby in-gamut color is chosen instead. 0449 * 0450 * @param colorSpace The color space in which the return value is calculated. 0451 * 0452 * @returns Palette derived from the basic colors. Provides as a list of 0453 * basic colors (in this order: red, orange, yellow, green, blue, purple, pink, 0454 * brown, gray axis). Each basic color is a list of 5 swatches (starting with 0455 * the lightest and finishing with the darkest: 2 tints, the tone itself, 0456 * 2 shades). 0457 * 0458 * @note The RGB value is rounded to full integers in the range [0, 255]. */ 0459 Array2D<QColor> wcsBasicColors(const QSharedPointer<PerceptualColor::RgbColorSpace> &colorSpace) 0460 { 0461 constexpr GenericColor red{41.22, 61.40, 17.92}; 0462 constexpr GenericColor orange{61.70, 29.38, 64.40}; 0463 constexpr GenericColor yellow{81.35, 07.28, 109.12}; 0464 constexpr GenericColor green{51.57, -63.28, 28.95}; 0465 constexpr GenericColor blue{51.57, -03.41, -48.08}; 0466 constexpr GenericColor purple{41.22, 33.08, -30.50}; 0467 constexpr GenericColor pink{61.70, 49.42, 18.23}; 0468 constexpr GenericColor brown{41.22, 17.04, 45.95}; 0469 constexpr std::array<GenericColor, 8> chromaticCielabColors // 0470 {{red, orange, yellow, green, blue, purple, pink, brown}}; 0471 0472 // Lowest common denominator of QList‘s and std::array’s size types: 0473 using MySizeType = quint8; 0474 0475 constexpr MySizeType columnCount = // 0476 chromaticCielabColors.size() + 1; // + 1 for gray axis 0477 constexpr auto rowCount = 5; 0478 Array2D<QColor> wcsSwatches{columnCount, rowCount}; 0479 0480 // Chromatic colors 0481 constexpr double strongTint = 0.46; 0482 constexpr double weakTint = 0.23; 0483 constexpr double weakShade = 0.18; 0484 constexpr double strongShade = 0.36; 0485 std::array<GenericColor, rowCount> tintsAndShades; 0486 for (MySizeType i = 0; i < chromaticCielabColors.size(); ++i) { // 0487 const auto oklch = AbsoluteColor::convert( // 0488 ColorModel::CielabD50, // 0489 chromaticCielabColors.at(i), 0490 ColorModel::OklchD65 // 0491 ) 0492 .value_or(GenericColor()); 0493 tintsAndShades[0] = GenericColor // 0494 {oklch.first + (1 - oklch.first) * strongTint, // 0495 oklch.second * (1 - strongTint), // 0496 oklch.third}; 0497 tintsAndShades[1] = GenericColor // 0498 {oklch.first + (1 - oklch.first) * weakTint, // 0499 oklch.second * (1 - weakTint), // 0500 oklch.third}; 0501 tintsAndShades[2] = oklch; 0502 tintsAndShades[3] = GenericColor // 0503 {oklch.first * (1 - weakShade), // 0504 oklch.second * (1 - weakShade), // 0505 oklch.third}; 0506 tintsAndShades[4] = GenericColor // 0507 {oklch.first * (1 - strongShade), // 0508 oklch.second * (1 - strongShade), // 0509 oklch.third}; 0510 for (MySizeType j = 0; j < rowCount; ++j) { 0511 const auto variationCielchD50 = AbsoluteColor::convert( // 0512 ColorModel::OklchD65, // 0513 tintsAndShades.at(j), // 0514 ColorModel::CielchD50 // 0515 ) 0516 .value_or(GenericColor()); 0517 const auto variationRgb = colorSpace->fromCielchD50ToQRgbBound( // 0518 variationCielchD50.reinterpretAsLchToLchDouble()); 0519 wcsSwatches.setValue(i, // 0520 j, 0521 variationRgb); 0522 } 0523 } 0524 0525 // Gray axis 0526 QList<double> lightnesses{1, 0.75, 0.5, 0.25, 0}; 0527 for (int j = 0; j < lightnesses.count(); ++j) { 0528 const GenericColor myOklab{lightnesses.at(j), 0, 0}; 0529 const auto cielabD50 = AbsoluteColor::convert( // 0530 ColorModel::OklabD65, // 0531 myOklab, // 0532 ColorModel::CielchD50 // 0533 ) 0534 .value_or(GenericColor()); 0535 const auto rgb = colorSpace->fromCielchD50ToQRgbBound( // 0536 cielabD50.reinterpretAsLchToLchDouble()); 0537 wcsSwatches.setValue(columnCount - 1, j, rgb); 0538 } 0539 0540 return wcsSwatches; 0541 } 0542 0543 } // namespace PerceptualColor