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