File indexing completed on 2024-09-01 04:30:05

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