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

0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT
0003 
0004 // Own header
0005 #include "csscolor.h"
0006 
0007 #include "helpermath.h"
0008 #include "helperposixmath.h"
0009 #include <array>
0010 #include <optional>
0011 #include <qhash.h>
0012 #include <qlist.h>
0013 #include <qnamespace.h>
0014 #include <qregularexpression.h>
0015 #include <qrgb.h>
0016 #include <qstringbuilder.h>
0017 #include <qstringliteral.h>
0018 #include <qstringview.h>
0019 #include <type_traits>
0020 #include <utility>
0021 
0022 namespace PerceptualColor
0023 {
0024 
0025 /** @brief Parses a hexadecimal color notations.
0026  *
0027  * Implements the
0028  * <a href="https://www.w3.org/TR/css-color-4/#typedef-hex-color">hexadecimal
0029  * notations as defined in CSS Color 4</a>.
0030  *
0031  * @param hexColor The hexadecimal color to parse, without any leading or
0032  * trailing whitespace.
0033  *
0034  * @returns The sRGB value if the syntax is valid. An
0035  * empty value otherwise. */
0036 std::optional<QRgb> CssColor::parseHexColor(const QString &hexColor)
0037 {
0038     if (hexColor.length() > 9) { // Maximum 8 digits + “#” allowed
0039         return std::nullopt;
0040     }
0041     static const QRegularExpression regex(QStringLiteral("^#([0-9A-Fa-f]*)$"));
0042     QString capturedDigits = regex.match(hexColor).captured(1);
0043     switch (capturedDigits.length()) {
0044     // Expand short forms
0045     case 3:
0046         capturedDigits = capturedDigits.at(0) + capturedDigits.at(0) //
0047             + capturedDigits.at(1) + capturedDigits.at(1) //
0048             + capturedDigits.at(2) + capturedDigits.at(2);
0049         break;
0050     case 4:
0051         capturedDigits = capturedDigits.at(0) + capturedDigits.at(0) //
0052             + capturedDigits.at(1) + capturedDigits.at(1) //
0053             + capturedDigits.at(2) + capturedDigits.at(2) //
0054             + capturedDigits.at(3) + capturedDigits.at(3);
0055         break;
0056     }
0057     if (capturedDigits.length() == 6) {
0058         // Add missing opacity.
0059         capturedDigits += QStringLiteral("ff");
0060     }
0061     if (capturedDigits.length() == 8) {
0062         const auto view = QStringView{capturedDigits};
0063         const auto red = view.mid(0, 2).toInt(nullptr, 16);
0064         const auto green = view.mid(2, 2).toInt(nullptr, 16);
0065         const auto blue = view.mid(4, 2).toInt(nullptr, 16);
0066         const auto alpha = view.mid(6, 2).toInt(nullptr, 16);
0067         return qRgba(red, green, blue, alpha);
0068     }
0069     return std::nullopt;
0070 }
0071 
0072 /** @brief Validate arguments.
0073  *
0074  * @param arguments The list of arguments to be validated.
0075  *
0076  * @returns For each argument, it is checked if it is valid, meaning it does
0077  * not contain any whitespace in the middle, comma, or slash. If all arguments
0078  * are valid, they are returned with leading and trailing whitespace removed.
0079  * Otherwise, an empty value is returned. */
0080 std::optional<QStringList> CssColor::validateArguments(const QStringList &arguments)
0081 {
0082     QStringList result;
0083     for (const QString &argument : std::as_const(arguments)) {
0084         QString simplifiedString = argument.simplified();
0085         if (simplifiedString.contains(QStringLiteral(" ")) //
0086             || simplifiedString.contains(QStringLiteral(",")) //
0087             || simplifiedString.contains(QStringLiteral("/")) //
0088             || simplifiedString.isEmpty()) {
0089             return std::nullopt;
0090         }
0091         result.append(simplifiedString);
0092     }
0093     return result;
0094 }
0095 
0096 /** @brief Parses arguments of a CSS Color 4 function.
0097  *
0098  * Accepts both, standard (white-space separated) and legacy (comma-separated)
0099  * syntax. It accepts an arbitrary number of normal arguments, and in standard
0100  * syntax also up to one alpha argument.
0101  *
0102  * @param arguments The function arguments to parse.
0103  * @param mode The syntaxes that are considered as valid.
0104  * @param count The exact number of expected arguments. Finally accepted
0105  * are arguments of this exact number, of of this exact number minus one.
0106  * (It is supposed that the last argument is the alpha arguments, which is
0107  * optional.) A missing argument is added automatically with the
0108  * value <tt>"none"</tt>.
0109  *
0110  * @returns A string list containing all arguments, or an empty value if the
0111  * syntax was invalid. Note that the individual arguments have leading and/or
0112  * trailing white space removed and are guaranteed to not contain any comma
0113  * or slash. */
0114 std::optional<QStringList> CssColor::parseAllFunctionArguments(const QString &arguments, const FunctionSyntax mode, const int count)
0115 {
0116     if (arguments.count(QStringLiteral(",")) > 0) {
0117         // Legacy syntax detected
0118         if (mode == FunctionSyntax::BothSyntaxes || mode == FunctionSyntax::LegacySyntax) {
0119             // Legacy syntax allowed, so interpret as legacy syntax.
0120             if (arguments.count(QStringLiteral("/")) > 0) {
0121                 // No slash separator allowed in legacy function arguments.
0122                 return std::nullopt;
0123             }
0124             auto result = arguments.split(QStringLiteral(","), //
0125                                           Qt::KeepEmptyParts);
0126             if (result.count() == count - 1) {
0127                 // Add implicit alpha argument
0128                 result.append(QStringLiteral("none"));
0129             }
0130             if (result.count() != count) {
0131                 return std::nullopt;
0132             }
0133             return validateArguments(result);
0134         } else {
0135             return std::nullopt;
0136         }
0137     }
0138 
0139     // If it’s not legacy syntax, is must be standard syntax, so interpret as
0140     // standard syntax.
0141     if (mode == FunctionSyntax::LegacySyntax) {
0142         // Standard syntax isn’t allowed here, so return.
0143         return std::nullopt;
0144     }
0145     const auto parts = arguments.split(QStringLiteral("/"), //
0146                                        Qt::KeepEmptyParts);
0147     if (parts.count() > 2) {
0148         // Not more than one slash allowed.
0149         return std::nullopt;
0150     }
0151     const QString alphaArgument = (parts.count() == 2) //
0152         ? parts.value(1) //
0153         : QStringLiteral("none");
0154     auto result = parts.value(0).simplified().split(QStringLiteral(" "));
0155     result.append(alphaArgument);
0156     if (result.count() != count) {
0157         // Wrong number of arguments
0158         return std::nullopt;
0159     }
0160     return validateArguments(result);
0161 }
0162 
0163 /** @brief Parses a single argument.
0164  *
0165  * Accepts absolute numbers, percent values and <tt>"none"</tt>.
0166  *
0167  * Leading and trailing whitespace is ignored.
0168  *
0169  * @param argument The argument to parse.
0170  * @param full The absolute value that corresponds to 100%.
0171  * @param none The absolute value that correspond to <tt>"none"</tt>.
0172  *
0173  * @returns The absolute number if the syntax is valid. An empty value
0174  * otherwise. */
0175 std::optional<double> CssColor::parseArgumentPercentNumberNone(const QString &argument, const double full, const double none)
0176 {
0177     bool okay = true;
0178     QString cleanArgument = argument.simplified();
0179     if (cleanArgument.contains(QStringLiteral(" ")) //
0180         || cleanArgument.contains(QStringLiteral(",")) //
0181         || cleanArgument.contains(QStringLiteral("/")) //
0182         || cleanArgument.isEmpty()) {
0183         return std::nullopt;
0184     }
0185     std::optional<double> result;
0186     if (cleanArgument == QStringLiteral("none")) {
0187         return none;
0188     } else if (cleanArgument.endsWith(QStringLiteral("%"))) {
0189         cleanArgument.truncate(cleanArgument.length() - 1);
0190         result = cleanArgument.toDouble(&okay) / 100. * full;
0191     } else {
0192         result = cleanArgument.toDouble(&okay);
0193     }
0194     if (!okay) {
0195         return std::nullopt;
0196     }
0197     return result;
0198 }
0199 
0200 /** @brief Parses a single argument.
0201  *
0202  * Accepts percent values and <tt>"none"</tt>.
0203  *
0204  * Leading and trailing whitespace is ignored.
0205  *
0206  * @param argument The argument to parse.
0207  *
0208  * @returns For invalid syntax, an empty value is returned. For valid syntax,
0209  * <tt>100%</tt> corresponds to <tt>1</tt>, while <tt>0%</tt> and <tt>none</tt>
0210  * correspond to <tt>0</tt>. */
0211 std::optional<double> CssColor::parseArgumentPercentNoneTo1(const QString &argument)
0212 {
0213     bool okay = true;
0214     QString cleanArgument = argument.simplified();
0215     if (cleanArgument.contains(QStringLiteral(" ")) //
0216         || cleanArgument.contains(QStringLiteral(",")) //
0217         || cleanArgument.contains(QStringLiteral("/")) //
0218         || cleanArgument.isEmpty()) {
0219         return std::nullopt;
0220     }
0221     if (cleanArgument == QStringLiteral("none")) {
0222         return 0;
0223     }
0224     if (!cleanArgument.endsWith(QStringLiteral("%"))) {
0225         return std::nullopt;
0226     }
0227     cleanArgument.truncate(cleanArgument.length() - 1);
0228     const auto result = cleanArgument.toDouble(&okay) / 100.;
0229     if (okay) {
0230         return result;
0231     }
0232     return std::nullopt;
0233 }
0234 
0235 /** @brief Parses a single argument.
0236  *
0237  * Accepts percent values and <tt>"none"</tt>.
0238  *
0239  * Leading and trailing whitespace is ignored.
0240  *
0241  * @param argument The argument to parse.
0242  *
0243  * @returns For invalid syntax, an empty value is returned. For valid syntax,
0244  * a hue in the range [0, 360[ is returned, with 360 corresponding to the
0245  * full circle. */
0246 std::optional<double> CssColor::parseArgumentHueNoneTo360(const QString &argument)
0247 {
0248     bool okay = true;
0249     QString cleanArgument = argument.simplified();
0250     if (cleanArgument.contains(QStringLiteral(" ")) //
0251         || cleanArgument.contains(QStringLiteral(",")) //
0252         || cleanArgument.contains(QStringLiteral("/")) //
0253         || cleanArgument.isEmpty()) {
0254         return std::nullopt;
0255     }
0256     if (cleanArgument == QStringLiteral("none")) {
0257         return 0;
0258     }
0259     double correctionFactor = 1;
0260 
0261     if (cleanArgument.endsWith(QStringLiteral("deg"))) {
0262         cleanArgument.truncate(cleanArgument.length() - 3);
0263     }
0264     if (cleanArgument.endsWith(QStringLiteral("grad"))) {
0265         cleanArgument.truncate(cleanArgument.length() - 4);
0266         correctionFactor = 360. / 400.;
0267     }
0268     if (cleanArgument.endsWith(QStringLiteral("rad"))) {
0269         cleanArgument.truncate(cleanArgument.length() - 3);
0270         correctionFactor = 360. / (2 * pi);
0271     }
0272     if (cleanArgument.endsWith(QStringLiteral("turn"))) {
0273         cleanArgument.truncate(cleanArgument.length() - 4);
0274         correctionFactor = 360.;
0275     }
0276 
0277     const auto result = cleanArgument.toDouble(&okay);
0278     if (okay) {
0279         return normalizedAngle360(result * correctionFactor);
0280     }
0281     return std::nullopt;
0282 }
0283 
0284 /** @brief Parse
0285  * <a href="https://www.w3.org/TR/css-color-4/#typedef-absolute-color-function">
0286  * Absolute Color Functions</a> as defined in CSS Color 4.
0287  *
0288  * @param colorFunction The string to parse.
0289  *
0290  * @returns If the CSS fragment is valid, the corresponding color.
0291  * @ref ColorModel::Invalid otherwise. */
0292 CssColor::CssColorValue CssColor::parseAbsoluteColorFunction(const QString &colorFunction)
0293 {
0294     static const QRegularExpression regex( //
0295         QStringLiteral("^(\\w+)\\s*\\((.*)\\)$"));
0296     QRegularExpressionMatch match = regex.match(colorFunction);
0297     QString ident = match.captured(1).simplified();
0298     const QString argumentsString = match.captured(2).simplified();
0299     std::optional<QStringList> maybeArguments;
0300 
0301     if (ident == QStringLiteral("rgb") //
0302         || ident == QStringLiteral("hsl")) {
0303         maybeArguments = parseAllFunctionArguments( //
0304             argumentsString, //
0305             FunctionSyntax::BothSyntaxes, //
0306             4);
0307     }
0308     if (ident == QStringLiteral("rgba") //
0309         || ident == QStringLiteral("hsla")) {
0310         maybeArguments = parseAllFunctionArguments( //
0311             argumentsString, //
0312             FunctionSyntax::LegacySyntax, //
0313             4);
0314     }
0315     if (ident == QStringLiteral("hwb") //
0316         || ident == QStringLiteral("lch") //
0317         || ident == QStringLiteral("lab") //
0318         || ident == QStringLiteral("oklch") //
0319         || ident == QStringLiteral("oklab")) {
0320         maybeArguments = parseAllFunctionArguments( //
0321             argumentsString, //
0322             FunctionSyntax::StandardSyntax, //
0323             4);
0324     }
0325     if (ident == QStringLiteral("color")) {
0326         const auto colorArguments = parseAllFunctionArguments( //
0327             argumentsString, //
0328             FunctionSyntax::StandardSyntax, //
0329             5);
0330         if (!colorArguments.has_value()) {
0331             return CssColorValue();
0332         }
0333         ident = colorArguments.value().value(0);
0334         maybeArguments = colorArguments.value().mid(1);
0335     };
0336     if (!maybeArguments.has_value()) {
0337         return CssColorValue();
0338     }
0339     const auto arguments = maybeArguments.value();
0340 
0341     QList<double> list1;
0342     list1.reserve(3);
0343     ColorModel model = ColorModel::Invalid;
0344     CssPredefinedRgbColorSpace rgbColorSpace = //
0345         CssPredefinedRgbColorSpace::Invalid;
0346 
0347     using Pair = std::pair<QString, CssPredefinedRgbColorSpace>;
0348     // clang-format off
0349     static const QHash<QString, CssPredefinedRgbColorSpace> hash {
0350         Pair(QStringLiteral("srgb"), CssPredefinedRgbColorSpace::Srgb),
0351         Pair(QStringLiteral("srgb-linear"), CssPredefinedRgbColorSpace::SrgbLinear),
0352         Pair(QStringLiteral("display-p3"), CssPredefinedRgbColorSpace::DisplayP3),
0353         Pair(QStringLiteral("a98-rgb"), CssPredefinedRgbColorSpace::A98Rgb),
0354         Pair(QStringLiteral("prophoto-rgb"), CssPredefinedRgbColorSpace::ProphotoRgb),
0355         Pair(QStringLiteral("rec2020"), CssPredefinedRgbColorSpace::Rec2020)
0356     };
0357     // clang-format on
0358     if (ident == QStringLiteral("rgb") //
0359         || ident == QStringLiteral("rgba") //
0360         || hash.contains(ident)) {
0361         model = ColorModel::Rgb1;
0362         rgbColorSpace = hash.value( //
0363             ident, //
0364             CssPredefinedRgbColorSpace::Srgb);
0365         const double full = (hash.contains(ident)) //
0366             ? 1
0367             : 255;
0368         for (int i = 0; i < 3; ++i) {
0369             const auto absValue = //
0370                 parseArgumentPercentNumberNone(arguments.value(i), full, 0);
0371             if (absValue.has_value()) {
0372                 list1 += absValue.value() / full;
0373             }
0374         }
0375     }
0376 
0377     if (ident == QStringLiteral("xyz-d50") //
0378         || ident == QStringLiteral("xyz-d65") //
0379         || ident == QStringLiteral("xyz")) {
0380         model = (ident == QStringLiteral("xyz-d50")) //
0381             ? ColorModel::XyzD50
0382             : ColorModel::XyzD65;
0383         rgbColorSpace = CssPredefinedRgbColorSpace::Invalid;
0384         for (int i = 0; i < 3; ++i) {
0385             const auto absValue = //
0386                 parseArgumentPercentNumberNone(arguments.value(i), 1, 0);
0387             if (absValue.has_value()) {
0388                 list1 += absValue.value();
0389             }
0390         }
0391     }
0392 
0393     if (ident == QStringLiteral("hsl") //
0394         || ident == QStringLiteral("hsla") //
0395         || ident == QStringLiteral("hwb")) {
0396         model = (ident == QStringLiteral("hwb")) //
0397             ? ColorModel::Hwb360_1_1
0398             : ColorModel::Hsl360_1_1;
0399         rgbColorSpace = CssPredefinedRgbColorSpace::Srgb;
0400         const auto maybeHue = parseArgumentHueNoneTo360(arguments.value(0));
0401         if (maybeHue.has_value()) {
0402             list1 += maybeHue.value();
0403         }
0404         for (int i = 1; i < 3; ++i) {
0405             const auto absValue = //
0406                 parseArgumentPercentNoneTo1(arguments.value(i));
0407             if (absValue.has_value()) {
0408                 list1 += absValue.value();
0409             }
0410         }
0411     }
0412 
0413     if (ident == QStringLiteral("oklab") //
0414         || ident == QStringLiteral("lab")) {
0415         model = (ident == QStringLiteral("oklab")) //
0416             ? ColorModel::OklabD65
0417             : ColorModel::CielabD50;
0418         rgbColorSpace = CssPredefinedRgbColorSpace::Invalid;
0419         std::array<double, 3> full = (ident == QStringLiteral("oklab")) //
0420             ? std::array<double, 3>{1, 0.4, 0.4}
0421             : std::array<double, 3>{100, 125, 125};
0422         for (quint8 i = 0; i < 3; ++i) {
0423             const auto absValue = parseArgumentPercentNumberNone( //
0424                 arguments.value(i), //
0425                 full.at(i), //
0426                 0);
0427             if (absValue.has_value()) {
0428                 list1 += absValue.value();
0429             }
0430         }
0431     }
0432 
0433     if (ident == QStringLiteral("oklch") //
0434         || ident == QStringLiteral("lch")) {
0435         model = (ident == QStringLiteral("oklch")) //
0436             ? ColorModel::OklchD65
0437             : ColorModel::CielchD50;
0438         rgbColorSpace = CssPredefinedRgbColorSpace::Invalid;
0439         std::array<double, 2> full = (ident == QStringLiteral("oklch")) //
0440             ? std::array<double, 2>{1, 0.4}
0441             : std::array<double, 2>{100, 150};
0442         for (quint8 i = 0; i < 2; ++i) {
0443             const auto absValue = parseArgumentPercentNumberNone( //
0444                 arguments.value(i), //
0445                 full.at(i), //
0446                 0);
0447             if (absValue.has_value()) {
0448                 list1 += absValue.value();
0449             }
0450         }
0451         const auto maybeHue = parseArgumentHueNoneTo360(arguments.value(2));
0452         if (maybeHue.has_value()) {
0453             list1 += maybeHue.value();
0454         }
0455     }
0456 
0457     if (list1.count() != 3) {
0458         // One or more of the first three arguments were invalid.
0459         return CssColorValue();
0460     }
0461 
0462     const auto opacity1 = //
0463         parseArgumentPercentNumberNone(arguments.value(3), 1, 1);
0464 
0465     if (!opacity1.has_value()) {
0466         return CssColorValue();
0467     }
0468     CssColorValue result;
0469     result.model = model;
0470     result.rgbColorSpace = rgbColorSpace;
0471     result.color = GenericColor //
0472         {list1.value(0), list1.value(1), list1.value(2)};
0473     result.alpha1 = opacity1.value();
0474     return result;
0475 }
0476 
0477 /** @brief Parse a CSS color value.
0478  *
0479  * @param string The CSS fragment to parse
0480  *
0481  * @returns If the CSS fragment is valid, the corresponding color.
0482  * @ref ColorModel::Invalid otherwise.
0483  *
0484  * This parser accepts all valid <a href="https://www.w3.org/TR/css-color-4/">
0485  * CSS Colors 4</a>, except those who’s value is context-dependant like for
0486  * <tt><a href="https://www.w3.org/TR/css-color-4/#valdef-color-currentcolor">
0487  * currentcolor</a></tt>.
0488  *
0489  * @note A trailing “;” is ignored for your convenance. Other supplementary
0490  * characters will be considered as syntax error. For simplicity of
0491  * implementation, some very limited invalid CSS colors are considered as
0492  * valid when the can be no confusion about the meaning. For example,
0493  * <tt>rgba()</tt> does not allow to mix absolute
0494  * numbers and percent number: All values must be either a percentage or
0495  * an absolute number. However this parser accepts also mixed
0496  * values. */
0497 CssColor::CssColorValue CssColor::parse(const QString &string)
0498 {
0499     auto myString = string.simplified();
0500     if (myString.endsWith(QStringLiteral(";"))) {
0501         myString.chop(1);
0502         myString = myString.simplified();
0503     }
0504 
0505     std::optional<QRgb> srgb = parseNamedColor(myString);
0506     if (!srgb.has_value()) {
0507         srgb = parseHexColor(myString);
0508     }
0509     if (srgb.has_value()) {
0510         const auto srgbValue = srgb.value();
0511         CssColorValue result;
0512         result.model = ColorModel::Rgb1;
0513         result.rgbColorSpace = CssPredefinedRgbColorSpace::Srgb;
0514         result.color = GenericColor{qRed(srgbValue) / 255., //
0515                                     qGreen(srgbValue) / 255., //
0516                                     qBlue(srgbValue) / 255.};
0517         result.alpha1 = qAlpha(srgbValue) / 255.;
0518         return result;
0519     }
0520 
0521     return parseAbsoluteColorFunction(myString);
0522 }
0523 
0524 /** @internal
0525  *
0526  * @brief Converts a named color to sRGB (if any)
0527  *
0528  * Implements the
0529  * <a href="https://www.w3.org/TR/css-color-4/#typedef-named-color">
0530  * Named colors</a> and the
0531  * <a href="https://www.w3.org/TR/css-color-4/#transparent-color">
0532  * transparent keyword</a> as defined in CSS Color 4.
0533  *
0534  * @param namedColor The named color to search for.
0535  *
0536  * @returns The sRGB value if its a CSS named color (case-insensitive). An
0537  * empty value otherwise. */
0538 std::optional<QRgb> CssColor::parseNamedColor(const QString &namedColor)
0539 {
0540     using NamedColor = std::pair<QString, QRgb>;
0541     // clang-format off
0542     static const QHash<QString, QRgb> colorList {
0543         // From https://www.w3.org/TR/css-color-4/#transparent-color
0544         NamedColor(QStringLiteral("transparent"), 0x00000000),
0545         // From https://www.w3.org/TR/css-color-4/#named-colors
0546         NamedColor(QStringLiteral("aliceblue"), 0xfff0f8ff),
0547         NamedColor(QStringLiteral("antiquewhite"), 0xfffaebd7),
0548         NamedColor(QStringLiteral("aqua"), 0xff00ffff),
0549         NamedColor(QStringLiteral("aquamarine"), 0xff7fffd4),
0550         NamedColor(QStringLiteral("azure"), 0xfff0ffff),
0551         NamedColor(QStringLiteral("beige"), 0xfff5f5dc),
0552         NamedColor(QStringLiteral("bisque"), 0xffffe4c4),
0553         NamedColor(QStringLiteral("black"), 0xff000000),
0554         NamedColor(QStringLiteral("blanchedalmond"), 0xffffebcd),
0555         NamedColor(QStringLiteral("blue"), 0xff0000ff),
0556         NamedColor(QStringLiteral("blueviolet"), 0xff8a2be2),
0557         NamedColor(QStringLiteral("brown"), 0xffa52a2a),
0558         NamedColor(QStringLiteral("burlywood"), 0xffdeb887),
0559         NamedColor(QStringLiteral("cadetblue"), 0xff5f9ea0),
0560         NamedColor(QStringLiteral("chartreuse"), 0xff7fff00),
0561         NamedColor(QStringLiteral("chocolate"), 0xffd2691e),
0562         NamedColor(QStringLiteral("coral"), 0xffff7f50),
0563         NamedColor(QStringLiteral("cornflowerblue"), 0xff6495ed),
0564         NamedColor(QStringLiteral("cornsilk"), 0xfffff8dc),
0565         NamedColor(QStringLiteral("crimson"), 0xffdc143c),
0566         NamedColor(QStringLiteral("cyan"), 0xff00ffff),
0567         NamedColor(QStringLiteral("darkblue"), 0xff00008b),
0568         NamedColor(QStringLiteral("darkcyan"), 0xff008b8b),
0569         NamedColor(QStringLiteral("darkgoldenrod"), 0xffb8860b),
0570         NamedColor(QStringLiteral("darkgray"), 0xffa9a9a9),
0571         NamedColor(QStringLiteral("darkgreen"), 0xff006400),
0572         NamedColor(QStringLiteral("darkgrey"), 0xffa9a9a9),
0573         NamedColor(QStringLiteral("darkkhaki"), 0xffbdb76b),
0574         NamedColor(QStringLiteral("darkmagenta"), 0xff8b008b),
0575         NamedColor(QStringLiteral("darkolivegreen"), 0xff556b2f),
0576         NamedColor(QStringLiteral("darkorange"), 0xffff8c00),
0577         NamedColor(QStringLiteral("darkorchid"), 0xff9932cc),
0578         NamedColor(QStringLiteral("darkred"), 0xff8b0000),
0579         NamedColor(QStringLiteral("darksalmon"), 0xffe9967a),
0580         NamedColor(QStringLiteral("darkseagreen"), 0xff8fbc8f),
0581         NamedColor(QStringLiteral("darkslateblue"), 0xff483d8b),
0582         NamedColor(QStringLiteral("darkslategray"), 0xff2f4f4f),
0583         NamedColor(QStringLiteral("darkslategrey"), 0xff2f4f4f),
0584         NamedColor(QStringLiteral("darkturquoise"), 0xff00ced1),
0585         NamedColor(QStringLiteral("darkviolet"), 0xff9400d3),
0586         NamedColor(QStringLiteral("deeppink"), 0xffff1493),
0587         NamedColor(QStringLiteral("deepskyblue"), 0xff00bfff),
0588         NamedColor(QStringLiteral("dimgray"), 0xff696969),
0589         NamedColor(QStringLiteral("dimgrey"), 0xff696969),
0590         NamedColor(QStringLiteral("dodgerblue"), 0xff1e90ff),
0591         NamedColor(QStringLiteral("firebrick"), 0xffb22222),
0592         NamedColor(QStringLiteral("floralwhite"), 0xfffffaf0),
0593         NamedColor(QStringLiteral("forestgreen"), 0xff228b22),
0594         NamedColor(QStringLiteral("fuchsia"), 0xffff00ff),
0595         NamedColor(QStringLiteral("gainsboro"), 0xffdcdcdc),
0596         NamedColor(QStringLiteral("ghostwhite"), 0xfff8f8ff),
0597         NamedColor(QStringLiteral("gold"), 0xffffd700),
0598         NamedColor(QStringLiteral("goldenrod"), 0xffdaa520),
0599         NamedColor(QStringLiteral("gray"), 0xff808080),
0600         NamedColor(QStringLiteral("green"), 0xff008000),
0601         NamedColor(QStringLiteral("greenyellow"), 0xffadff2f),
0602         NamedColor(QStringLiteral("grey"), 0xff808080),
0603         NamedColor(QStringLiteral("honeydew"), 0xfff0fff0),
0604         NamedColor(QStringLiteral("hotpink"), 0xffff69b4),
0605         NamedColor(QStringLiteral("indianred"), 0xffcd5c5c),
0606         NamedColor(QStringLiteral("indigo"), 0xff4b0082),
0607         NamedColor(QStringLiteral("ivory"), 0xfffffff0),
0608         NamedColor(QStringLiteral("khaki"), 0xfff0e68c),
0609         NamedColor(QStringLiteral("lavender"), 0xffe6e6fa),
0610         NamedColor(QStringLiteral("lavenderblush"), 0xfffff0f5),
0611         NamedColor(QStringLiteral("lawngreen"), 0xff7cfc00),
0612         NamedColor(QStringLiteral("lemonchiffon"), 0xfffffacd),
0613         NamedColor(QStringLiteral("lightblue"), 0xffadd8e6),
0614         NamedColor(QStringLiteral("lightcoral"), 0xfff08080),
0615         NamedColor(QStringLiteral("lightcyan"), 0xffe0ffff),
0616         NamedColor(QStringLiteral("lightgoldenrodyellow"), 0xfffafad2),
0617         NamedColor(QStringLiteral("lightgray"), 0xffd3d3d3),
0618         NamedColor(QStringLiteral("lightgreen"), 0xff90ee90),
0619         NamedColor(QStringLiteral("lightgrey"), 0xffd3d3d3),
0620         NamedColor(QStringLiteral("lightpink"), 0xffffb6c1),
0621         NamedColor(QStringLiteral("lightsalmon"), 0xffffa07a),
0622         NamedColor(QStringLiteral("lightseagreen"), 0xff20b2aa),
0623         NamedColor(QStringLiteral("lightskyblue"), 0xff87cefa),
0624         NamedColor(QStringLiteral("lightslategray"), 0xff778899),
0625         NamedColor(QStringLiteral("lightslategrey"), 0xff778899),
0626         NamedColor(QStringLiteral("lightsteelblue"), 0xffb0c4de),
0627         NamedColor(QStringLiteral("lightyellow"), 0xffffffe0),
0628         NamedColor(QStringLiteral("lime"), 0xff00ff00),
0629         NamedColor(QStringLiteral("limegreen"), 0xff32cd32),
0630         NamedColor(QStringLiteral("linen"), 0xfffaf0e6),
0631         NamedColor(QStringLiteral("magenta"), 0xffff00ff),
0632         NamedColor(QStringLiteral("maroon"), 0xff800000),
0633         NamedColor(QStringLiteral("mediumaquamarine"), 0xff66cdaa),
0634         NamedColor(QStringLiteral("mediumblue"), 0xff0000cd),
0635         NamedColor(QStringLiteral("mediumorchid"), 0xffba55d3),
0636         NamedColor(QStringLiteral("mediumpurple"), 0xff9370db),
0637         NamedColor(QStringLiteral("mediumseagreen"), 0xff3cb371),
0638         NamedColor(QStringLiteral("mediumslateblue"), 0xff7b68ee),
0639         NamedColor(QStringLiteral("mediumspringgreen"), 0xff00fa9a),
0640         NamedColor(QStringLiteral("mediumturquoise"), 0xff48d1cc),
0641         NamedColor(QStringLiteral("mediumvioletred"), 0xffc71585),
0642         NamedColor(QStringLiteral("midnightblue"), 0xff191970),
0643         NamedColor(QStringLiteral("mintcream"), 0xfff5fffa),
0644         NamedColor(QStringLiteral("mistyrose"), 0xffffe4e1),
0645         NamedColor(QStringLiteral("moccasin"), 0xffffe4b5),
0646         NamedColor(QStringLiteral("navajowhite"), 0xffffdead),
0647         NamedColor(QStringLiteral("navy"), 0xff000080),
0648         NamedColor(QStringLiteral("oldlace"), 0xfffdf5e6),
0649         NamedColor(QStringLiteral("olive"), 0xff808000),
0650         NamedColor(QStringLiteral("olivedrab"), 0xff6b8e23),
0651         NamedColor(QStringLiteral("orange"), 0xffffa500),
0652         NamedColor(QStringLiteral("orangered"), 0xffff4500),
0653         NamedColor(QStringLiteral("orchid"), 0xffda70d6),
0654         NamedColor(QStringLiteral("palegoldenrod"), 0xffeee8aa),
0655         NamedColor(QStringLiteral("palegreen"), 0xff98fb98),
0656         NamedColor(QStringLiteral("paleturquoise"), 0xffafeeee),
0657         NamedColor(QStringLiteral("palevioletred"), 0xffdb7093),
0658         NamedColor(QStringLiteral("papayawhip"), 0xffffefd5),
0659         NamedColor(QStringLiteral("peachpuff"), 0xffffdab9),
0660         NamedColor(QStringLiteral("peru"), 0xffcd853f),
0661         NamedColor(QStringLiteral("pink"), 0xffffc0cb),
0662         NamedColor(QStringLiteral("plum"), 0xffdda0dd),
0663         NamedColor(QStringLiteral("powderblue"), 0xffb0e0e6),
0664         NamedColor(QStringLiteral("purple"), 0xff800080),
0665         NamedColor(QStringLiteral("rebeccapurple"), 0xff663399),
0666         NamedColor(QStringLiteral("red"), 0xffff0000),
0667         NamedColor(QStringLiteral("rosybrown"), 0xffbc8f8f),
0668         NamedColor(QStringLiteral("royalblue"), 0xff4169e1),
0669         NamedColor(QStringLiteral("saddlebrown"), 0xff8b4513),
0670         NamedColor(QStringLiteral("salmon"), 0xfffa8072),
0671         NamedColor(QStringLiteral("sandybrown"), 0xfff4a460),
0672         NamedColor(QStringLiteral("seagreen"), 0xff2e8b57),
0673         NamedColor(QStringLiteral("seashell"), 0xfffff5ee),
0674         NamedColor(QStringLiteral("sienna"), 0xffa0522d),
0675         NamedColor(QStringLiteral("silver"), 0xffc0c0c0),
0676         NamedColor(QStringLiteral("skyblue"), 0xff87ceeb),
0677         NamedColor(QStringLiteral("slateblue"), 0xff6a5acd),
0678         NamedColor(QStringLiteral("slategray"), 0xff708090),
0679         NamedColor(QStringLiteral("slategrey"), 0xff708090),
0680         NamedColor(QStringLiteral("snow"), 0xfffffafa),
0681         NamedColor(QStringLiteral("springgreen"), 0xff00ff7f),
0682         NamedColor(QStringLiteral("steelblue"), 0xff4682b4),
0683         NamedColor(QStringLiteral("tan"), 0xffd2b48c),
0684         NamedColor(QStringLiteral("teal"), 0xff008080),
0685         NamedColor(QStringLiteral("thistle"), 0xffd8bfd8),
0686         NamedColor(QStringLiteral("tomato"), 0xffff6347),
0687         NamedColor(QStringLiteral("turquoise"), 0xff40e0d0),
0688         NamedColor(QStringLiteral("violet"), 0xffee82ee),
0689         NamedColor(QStringLiteral("wheat"), 0xfff5deb3),
0690         NamedColor(QStringLiteral("white"), 0xffffffff),
0691         NamedColor(QStringLiteral("whitesmoke"), 0xfff5f5f5),
0692         NamedColor(QStringLiteral("yellow"), 0xffffff00),
0693         NamedColor(QStringLiteral("yellowgreen"), 0xff9acd32)
0694     };
0695     // clang-format on
0696     const QString lowerCase = namedColor.toLower();
0697     if (colorList.contains(lowerCase)) {
0698         return colorList.value(lowerCase);
0699     }
0700     return std::nullopt;
0701 }
0702 
0703 /** @brief Provides CSS code for existing color values.
0704  *
0705  * This function is meant for exporting colors to CSS code.
0706  *
0707  * @param input A hash table with color values.
0708  * @param opacity1 The opacity of the color in the range [0, 1].
0709  * @param significantFigures The requested number of significant figures.
0710  *
0711  * @returns A list of CSS color codes, ordered by importance: oklch, oklab,
0712  * lch, lab, xyz-d50, xyz-d65. oklch is considered most important, followed
0713  * by its less intuitive companion oklab, followed by the less perceptually
0714  * uniform lch and lab. Finally comes the technically important, but
0715  * uncomfortable xyz space, starting with its D50 variant because this
0716  * is more wide-spread used in color management than the D65 variant.
0717  * RGB-based color models are intentionally omitted, because we can never
0718  * be sure if a given color is available in all of these spaces, especially
0719  * if the library is using a wide-color gamut, but the CSS code requires
0720  * sRGB. And if it would sometimes work (in-gamut colors) and sometimes fail
0721  * (out-of-gamut colors), this might be highly confusing for the average
0722  * user. Note that the alpha value only appears explicitly if it’s partially
0723  * or fully transparent. Fully opaque colors do not need to specify the
0724  * alpha value in CSS explicitly, because CSS defaults to “fully opaque” if
0725  * no alpha value is given. */
0726 QStringList CssColor::generateCss(const QHash<ColorModel, GenericColor> &input, const double opacity1, const int significantFigures)
0727 {
0728     QStringList result;
0729 
0730     const auto decimals1 = decimalPlaces(2, significantFigures);
0731     const auto decimals2 = decimalPlaces(2, significantFigures);
0732     const auto decimals100 = decimalPlaces(100, significantFigures);
0733     const auto decimals255 = decimalPlaces(255, significantFigures);
0734     const auto decimals360 = decimalPlaces(360, significantFigures);
0735 
0736     const QString opacity1String = (opacity1 < 1) //
0737         ? QStringLiteral(" / %1%").arg(opacity1 * 100, 0, 'f', decimals100)
0738         : QString();
0739 
0740     if (input.contains(ColorModel::OklchD65)) {
0741         const auto temp = input.value(ColorModel::OklchD65);
0742         result.append(QStringLiteral("oklch(%1 %2 %3%4)") //
0743                           .arg(temp.first, 0, 'f', decimals1) //
0744                           .arg(temp.second, 0, 'f', decimals2) //
0745                           .arg(temp.third, 0, 'f', decimals360) //
0746                           .arg(opacity1String));
0747     }
0748 
0749     if (input.contains(ColorModel::OklabD65)) {
0750         const auto temp = input.value(ColorModel::OklabD65);
0751         result.append(QStringLiteral("oklab(%1 %2 %3%4)") //
0752                           .arg(temp.first, 0, 'f', decimals1) //
0753                           .arg(temp.second, 0, 'f', decimals2) //
0754                           .arg(temp.third, 0, 'f', decimals2) //
0755                           .arg(opacity1String));
0756     }
0757 
0758     if (input.contains(ColorModel::CielchD50)) {
0759         const auto temp = input.value(ColorModel::CielchD50);
0760         result.append(QStringLiteral("lch(%1 %2 %3%4)") //
0761                           .arg(temp.first, 0, 'f', decimals100) //
0762                           .arg(temp.second, 0, 'f', decimals255) //
0763                           .arg(temp.third, 0, 'f', decimals360) //
0764                           .arg(opacity1String));
0765     }
0766 
0767     if (input.contains(ColorModel::CielabD50)) {
0768         const auto temp = input.value(ColorModel::CielabD50);
0769         result.append(QStringLiteral("lab(%1 %2 %3%4)") //
0770                           .arg(temp.first, 0, 'f', decimals100) //
0771                           .arg(temp.second, 0, 'f', decimals255) //
0772                           .arg(temp.third, 0, 'f', decimals255) //
0773                           .arg(opacity1String));
0774     }
0775 
0776     if (input.contains(ColorModel::XyzD50)) {
0777         const auto temp = input.value(ColorModel::XyzD50);
0778         result.append(QStringLiteral("color (xyz-d50 %1 %2 %3%4)") //
0779                           .arg(temp.first, 0, 'f', decimals1) //
0780                           .arg(temp.second, 0, 'f', decimals1) //
0781                           .arg(temp.third, 0, 'f', decimals1) //
0782                           .arg(opacity1String));
0783     }
0784 
0785     if (input.contains(ColorModel::XyzD65)) {
0786         const auto temp = input.value(ColorModel::XyzD65);
0787         result.append(QStringLiteral("color (xyz-d65 %1 %2 %3%4)") //
0788                           .arg(temp.first, 0, 'f', decimals1) //
0789                           .arg(temp.second, 0, 'f', decimals1) //
0790                           .arg(temp.third, 0, 'f', decimals1) //
0791                           .arg(opacity1String));
0792     }
0793 
0794     return result;
0795 }
0796 
0797 } // namespace PerceptualColor