Perceptual Color

csscolor.cpp
1// SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
2// SPDX-License-Identifier: BSD-2-Clause OR MIT
3
4// Own header
5#include "csscolor.h"
6
7#include "helpermath.h"
8#include "helperposixmath.h"
9#include <array>
10#include <optional>
11#include <qhash.h>
12#include <qlist.h>
13#include <qnamespace.h>
14#include <qregularexpression.h>
15#include <qrgb.h>
16#include <qstringbuilder.h>
17#include <qstringliteral.h>
18#include <qstringview.h>
19#include <type_traits>
20#include <utility>
21
22namespace PerceptualColor
23{
24
25/** @brief Parses a hexadecimal color notations.
26 *
27 * Implements the
28 * <a href="https://www.w3.org/TR/css-color-4/#typedef-hex-color">hexadecimal
29 * notations as defined in CSS Color 4</a>.
30 *
31 * @param hexColor The hexadecimal color to parse, without any leading or
32 * trailing whitespace.
33 *
34 * @returns The sRGB value if the syntax is valid. An
35 * empty value otherwise. */
36std::optional<QRgb> CssColor::parseHexColor(const QString &hexColor)
37{
38 if (hexColor.length() > 9) { // Maximum 8 digits + “#” allowed
39 return std::nullopt;
40 }
41 static const QRegularExpression regex(QStringLiteral("^#([0-9A-Fa-f]*)$"));
42 QString capturedDigits = regex.match(hexColor).captured(1);
43 switch (capturedDigits.length()) {
44 // Expand short forms
45 case 3:
46 capturedDigits = capturedDigits.at(0) + capturedDigits.at(0) //
47 + capturedDigits.at(1) + capturedDigits.at(1) //
48 + capturedDigits.at(2) + capturedDigits.at(2);
49 break;
50 case 4:
51 capturedDigits = capturedDigits.at(0) + capturedDigits.at(0) //
52 + capturedDigits.at(1) + capturedDigits.at(1) //
53 + capturedDigits.at(2) + capturedDigits.at(2) //
54 + capturedDigits.at(3) + capturedDigits.at(3);
55 break;
56 }
57 if (capturedDigits.length() == 6) {
58 // Add missing opacity.
59 capturedDigits += QStringLiteral("ff");
60 }
61 if (capturedDigits.length() == 8) {
62 const auto view = QStringView{capturedDigits};
63 const auto red = view.mid(0, 2).toInt(nullptr, 16);
64 const auto green = view.mid(2, 2).toInt(nullptr, 16);
65 const auto blue = view.mid(4, 2).toInt(nullptr, 16);
66 const auto alpha = view.mid(6, 2).toInt(nullptr, 16);
67 return qRgba(red, green, blue, alpha);
68 }
69 return std::nullopt;
70}
71
72/** @brief Validate arguments.
73 *
74 * @param arguments The list of arguments to be validated.
75 *
76 * @returns For each argument, it is checked if it is valid, meaning it does
77 * not contain any whitespace in the middle, comma, or slash. If all arguments
78 * are valid, they are returned with leading and trailing whitespace removed.
79 * Otherwise, an empty value is returned. */
80std::optional<QStringList> CssColor::validateArguments(const QStringList &arguments)
81{
82 QStringList result;
83 for (const QString &argument : std::as_const(arguments)) {
84 QString simplifiedString = argument.simplified();
85 if (simplifiedString.contains(QStringLiteral(" ")) //
86 || simplifiedString.contains(QStringLiteral(",")) //
87 || simplifiedString.contains(QStringLiteral("/")) //
88 || simplifiedString.isEmpty()) {
89 return std::nullopt;
90 }
91 result.append(simplifiedString);
92 }
93 return result;
94}
95
96/** @brief Parses arguments of a CSS Color 4 function.
97 *
98 * Accepts both, standard (white-space separated) and legacy (comma-separated)
99 * syntax. It accepts an arbitrary number of normal arguments, and in standard
100 * syntax also up to one alpha argument.
101 *
102 * @param arguments The function arguments to parse.
103 * @param mode The syntaxes that are considered as valid.
104 * @param count The exact number of expected arguments. Finally accepted
105 * are arguments of this exact number, of of this exact number minus one.
106 * (It is supposed that the last argument is the alpha arguments, which is
107 * optional.) A missing argument is added automatically with the
108 * value <tt>"none"</tt>.
109 *
110 * @returns A string list containing all arguments, or an empty value if the
111 * syntax was invalid. Note that the individual arguments have leading and/or
112 * trailing white space removed and are guaranteed to not contain any comma
113 * or slash. */
114std::optional<QStringList> CssColor::parseAllFunctionArguments(const QString &arguments, const FunctionSyntax mode, const int count)
115{
116 if (arguments.count(QStringLiteral(",")) > 0) {
117 // Legacy syntax detected
118 if (mode == FunctionSyntax::BothSyntaxes || mode == FunctionSyntax::LegacySyntax) {
119 // Legacy syntax allowed, so interpret as legacy syntax.
120 if (arguments.count(QStringLiteral("/")) > 0) {
121 // No slash separator allowed in legacy function arguments.
122 return std::nullopt;
123 }
124 auto result = arguments.split(QStringLiteral(","), //
126 if (result.count() == count - 1) {
127 // Add implicit alpha argument
128 result.append(QStringLiteral("none"));
129 }
130 if (result.count() != count) {
131 return std::nullopt;
132 }
133 return validateArguments(result);
134 } else {
135 return std::nullopt;
136 }
137 }
138
139 // If it’s not legacy syntax, is must be standard syntax, so interpret as
140 // standard syntax.
141 if (mode == FunctionSyntax::LegacySyntax) {
142 // Standard syntax isn’t allowed here, so return.
143 return std::nullopt;
144 }
145 const auto parts = arguments.split(QStringLiteral("/"), //
147 if (parts.count() > 2) {
148 // Not more than one slash allowed.
149 return std::nullopt;
150 }
151 const QString alphaArgument = (parts.count() == 2) //
152 ? parts.value(1) //
153 : QStringLiteral("none");
154 auto result = parts.value(0).simplified().split(QStringLiteral(" "));
155 result.append(alphaArgument);
156 if (result.count() != count) {
157 // Wrong number of arguments
158 return std::nullopt;
159 }
160 return validateArguments(result);
161}
162
163/** @brief Parses a single argument.
164 *
165 * Accepts absolute numbers, percent values and <tt>"none"</tt>.
166 *
167 * Leading and trailing whitespace is ignored.
168 *
169 * @param argument The argument to parse.
170 * @param full The absolute value that corresponds to 100%.
171 * @param none The absolute value that correspond to <tt>"none"</tt>.
172 *
173 * @returns The absolute number if the syntax is valid. An empty value
174 * otherwise. */
175std::optional<double> CssColor::parseArgumentPercentNumberNone(const QString &argument, const double full, const double none)
176{
177 bool okay = true;
178 QString cleanArgument = argument.simplified();
179 if (cleanArgument.contains(QStringLiteral(" ")) //
180 || cleanArgument.contains(QStringLiteral(",")) //
181 || cleanArgument.contains(QStringLiteral("/")) //
182 || cleanArgument.isEmpty()) {
183 return std::nullopt;
184 }
185 std::optional<double> result;
186 if (cleanArgument == QStringLiteral("none")) {
187 return none;
188 } else if (cleanArgument.endsWith(QStringLiteral("%"))) {
189 cleanArgument.truncate(cleanArgument.length() - 1);
190 result = cleanArgument.toDouble(&okay) / 100. * full;
191 } else {
192 result = cleanArgument.toDouble(&okay);
193 }
194 if (!okay) {
195 return std::nullopt;
196 }
197 return result;
198}
199
200/** @brief Parses a single argument.
201 *
202 * Accepts percent values and <tt>"none"</tt>.
203 *
204 * Leading and trailing whitespace is ignored.
205 *
206 * @param argument The argument to parse.
207 *
208 * @returns For invalid syntax, an empty value is returned. For valid syntax,
209 * <tt>100%</tt> corresponds to <tt>1</tt>, while <tt>0%</tt> and <tt>none</tt>
210 * correspond to <tt>0</tt>. */
211std::optional<double> CssColor::parseArgumentPercentNoneTo1(const QString &argument)
212{
213 bool okay = true;
214 QString cleanArgument = argument.simplified();
215 if (cleanArgument.contains(QStringLiteral(" ")) //
216 || cleanArgument.contains(QStringLiteral(",")) //
217 || cleanArgument.contains(QStringLiteral("/")) //
218 || cleanArgument.isEmpty()) {
219 return std::nullopt;
220 }
221 if (cleanArgument == QStringLiteral("none")) {
222 return 0;
223 }
224 if (!cleanArgument.endsWith(QStringLiteral("%"))) {
225 return std::nullopt;
226 }
227 cleanArgument.truncate(cleanArgument.length() - 1);
228 const auto result = cleanArgument.toDouble(&okay) / 100.;
229 if (okay) {
230 return result;
231 }
232 return std::nullopt;
233}
234
235/** @brief Parses a single argument.
236 *
237 * Accepts percent values and <tt>"none"</tt>.
238 *
239 * Leading and trailing whitespace is ignored.
240 *
241 * @param argument The argument to parse.
242 *
243 * @returns For invalid syntax, an empty value is returned. For valid syntax,
244 * a hue in the range [0, 360[ is returned, with 360 corresponding to the
245 * full circle. */
246std::optional<double> CssColor::parseArgumentHueNoneTo360(const QString &argument)
247{
248 bool okay = true;
249 QString cleanArgument = argument.simplified();
250 if (cleanArgument.contains(QStringLiteral(" ")) //
251 || cleanArgument.contains(QStringLiteral(",")) //
252 || cleanArgument.contains(QStringLiteral("/")) //
253 || cleanArgument.isEmpty()) {
254 return std::nullopt;
255 }
256 if (cleanArgument == QStringLiteral("none")) {
257 return 0;
258 }
259 double correctionFactor = 1;
260
261 if (cleanArgument.endsWith(QStringLiteral("deg"))) {
262 cleanArgument.truncate(cleanArgument.length() - 3);
263 }
264 if (cleanArgument.endsWith(QStringLiteral("grad"))) {
265 cleanArgument.truncate(cleanArgument.length() - 4);
266 correctionFactor = 360. / 400.;
267 }
268 if (cleanArgument.endsWith(QStringLiteral("rad"))) {
269 cleanArgument.truncate(cleanArgument.length() - 3);
270 correctionFactor = 360. / (2 * pi);
271 }
272 if (cleanArgument.endsWith(QStringLiteral("turn"))) {
273 cleanArgument.truncate(cleanArgument.length() - 4);
274 correctionFactor = 360.;
275 }
276
277 const auto result = cleanArgument.toDouble(&okay);
278 if (okay) {
279 return normalizedAngle360(result * correctionFactor);
280 }
281 return std::nullopt;
282}
283
284/** @brief Parse
285 * <a href="https://www.w3.org/TR/css-color-4/#typedef-absolute-color-function">
286 * Absolute Color Functions</a> as defined in CSS Color 4.
287 *
288 * @param colorFunction The string to parse.
289 *
290 * @returns If the CSS fragment is valid, the corresponding color.
291 * @ref ColorModel::Invalid otherwise. */
292CssColor::CssColorValue CssColor::parseAbsoluteColorFunction(const QString &colorFunction)
293{
294 static const QRegularExpression regex( //
295 QStringLiteral("^(\\w+)\\s*\\((.*)\\)$"));
296 QRegularExpressionMatch match = regex.match(colorFunction);
297 QString ident = match.captured(1).simplified();
298 const QString argumentsString = match.captured(2).simplified();
299 std::optional<QStringList> maybeArguments;
300
301 if (ident == QStringLiteral("rgb") //
302 || ident == QStringLiteral("hsl")) {
303 maybeArguments = parseAllFunctionArguments( //
304 argumentsString, //
305 FunctionSyntax::BothSyntaxes, //
306 4);
307 }
308 if (ident == QStringLiteral("rgba") //
309 || ident == QStringLiteral("hsla")) {
310 maybeArguments = parseAllFunctionArguments( //
311 argumentsString, //
312 FunctionSyntax::LegacySyntax, //
313 4);
314 }
315 if (ident == QStringLiteral("hwb") //
316 || ident == QStringLiteral("lch") //
317 || ident == QStringLiteral("lab") //
318 || ident == QStringLiteral("oklch") //
319 || ident == QStringLiteral("oklab")) {
320 maybeArguments = parseAllFunctionArguments( //
321 argumentsString, //
322 FunctionSyntax::StandardSyntax, //
323 4);
324 }
325 if (ident == QStringLiteral("color")) {
326 const auto colorArguments = parseAllFunctionArguments( //
327 argumentsString, //
328 FunctionSyntax::StandardSyntax, //
329 5);
330 if (!colorArguments.has_value()) {
331 return CssColorValue();
332 }
333 ident = colorArguments.value().value(0);
334 maybeArguments = colorArguments.value().mid(1);
335 };
336 if (!maybeArguments.has_value()) {
337 return CssColorValue();
338 }
339 const auto arguments = maybeArguments.value();
340
341 QList<double> list1;
342 list1.reserve(3);
343 ColorModel model = ColorModel::Invalid;
344 CssPredefinedRgbColorSpace rgbColorSpace = //
345 CssPredefinedRgbColorSpace::Invalid;
346
347 using Pair = std::pair<QString, CssPredefinedRgbColorSpace>;
348 // clang-format off
350 Pair(QStringLiteral("srgb"), CssPredefinedRgbColorSpace::Srgb),
351 Pair(QStringLiteral("srgb-linear"), CssPredefinedRgbColorSpace::SrgbLinear),
352 Pair(QStringLiteral("display-p3"), CssPredefinedRgbColorSpace::DisplayP3),
353 Pair(QStringLiteral("a98-rgb"), CssPredefinedRgbColorSpace::A98Rgb),
354 Pair(QStringLiteral("prophoto-rgb"), CssPredefinedRgbColorSpace::ProphotoRgb),
355 Pair(QStringLiteral("rec2020"), CssPredefinedRgbColorSpace::Rec2020)
356 };
357 // clang-format on
358 if (ident == QStringLiteral("rgb") //
359 || ident == QStringLiteral("rgba") //
360 || hash.contains(ident)) {
361 model = ColorModel::Rgb1;
362 rgbColorSpace = hash.value( //
363 ident, //
364 CssPredefinedRgbColorSpace::Srgb);
365 const double full = (hash.contains(ident)) //
366 ? 1
367 : 255;
368 for (int i = 0; i < 3; ++i) {
369 const auto absValue = //
370 parseArgumentPercentNumberNone(arguments.value(i), full, 0);
371 if (absValue.has_value()) {
372 list1 += absValue.value() / full;
373 }
374 }
375 }
376
377 if (ident == QStringLiteral("xyz-d50") //
378 || ident == QStringLiteral("xyz-d65") //
379 || ident == QStringLiteral("xyz")) {
380 model = (ident == QStringLiteral("xyz-d50")) //
382 : ColorModel::XyzD65;
383 rgbColorSpace = CssPredefinedRgbColorSpace::Invalid;
384 for (int i = 0; i < 3; ++i) {
385 const auto absValue = //
386 parseArgumentPercentNumberNone(arguments.value(i), 1, 0);
387 if (absValue.has_value()) {
388 list1 += absValue.value();
389 }
390 }
391 }
392
393 if (ident == QStringLiteral("hsl") //
394 || ident == QStringLiteral("hsla") //
395 || ident == QStringLiteral("hwb")) {
396 model = (ident == QStringLiteral("hwb")) //
398 : ColorModel::Hsl360_1_1;
399 rgbColorSpace = CssPredefinedRgbColorSpace::Srgb;
400 const auto maybeHue = parseArgumentHueNoneTo360(arguments.value(0));
401 if (maybeHue.has_value()) {
402 list1 += maybeHue.value();
403 }
404 for (int i = 1; i < 3; ++i) {
405 const auto absValue = //
406 parseArgumentPercentNoneTo1(arguments.value(i));
407 if (absValue.has_value()) {
408 list1 += absValue.value();
409 }
410 }
411 }
412
413 if (ident == QStringLiteral("oklab") //
414 || ident == QStringLiteral("lab")) {
415 model = (ident == QStringLiteral("oklab")) //
417 : ColorModel::CielabD50;
418 rgbColorSpace = CssPredefinedRgbColorSpace::Invalid;
419 std::array<double, 3> full = (ident == QStringLiteral("oklab")) //
420 ? std::array<double, 3>{1, 0.4, 0.4}
421 : std::array<double, 3>{100, 125, 125};
422 for (quint8 i = 0; i < 3; ++i) {
423 const auto absValue = parseArgumentPercentNumberNone( //
424 arguments.value(i), //
425 full.at(i), //
426 0);
427 if (absValue.has_value()) {
428 list1 += absValue.value();
429 }
430 }
431 }
432
433 if (ident == QStringLiteral("oklch") //
434 || ident == QStringLiteral("lch")) {
435 model = (ident == QStringLiteral("oklch")) //
437 : ColorModel::CielchD50;
438 rgbColorSpace = CssPredefinedRgbColorSpace::Invalid;
439 std::array<double, 2> full = (ident == QStringLiteral("oklch")) //
440 ? std::array<double, 2>{1, 0.4}
441 : std::array<double, 2>{100, 150};
442 for (quint8 i = 0; i < 2; ++i) {
443 const auto absValue = parseArgumentPercentNumberNone( //
444 arguments.value(i), //
445 full.at(i), //
446 0);
447 if (absValue.has_value()) {
448 list1 += absValue.value();
449 }
450 }
451 const auto maybeHue = parseArgumentHueNoneTo360(arguments.value(2));
452 if (maybeHue.has_value()) {
453 list1 += maybeHue.value();
454 }
455 }
456
457 if (list1.count() != 3) {
458 // One or more of the first three arguments were invalid.
459 return CssColorValue();
460 }
461
462 const auto opacity1 = //
463 parseArgumentPercentNumberNone(arguments.value(3), 1, 1);
464
465 if (!opacity1.has_value()) {
466 return CssColorValue();
467 }
468 CssColorValue result;
469 result.model = model;
470 result.rgbColorSpace = rgbColorSpace;
471 result.color = GenericColor //
472 {list1.value(0), list1.value(1), list1.value(2)};
473 result.alpha1 = opacity1.value();
474 return result;
475}
476
477/** @brief Parse a CSS color value.
478 *
479 * @param string The CSS fragment to parse
480 *
481 * @returns If the CSS fragment is valid, the corresponding color.
482 * @ref ColorModel::Invalid otherwise.
483 *
484 * This parser accepts all valid <a href="https://www.w3.org/TR/css-color-4/">
485 * CSS Colors 4</a>, except those who’s value is context-dependant like for
486 * <tt><a href="https://www.w3.org/TR/css-color-4/#valdef-color-currentcolor">
487 * currentcolor</a></tt>.
488 *
489 * @note A trailing “;” is ignored for your convenance. Other supplementary
490 * characters will be considered as syntax error. For simplicity of
491 * implementation, some very limited invalid CSS colors are considered as
492 * valid when the can be no confusion about the meaning. For example,
493 * <tt>rgba()</tt> does not allow to mix absolute
494 * numbers and percent number: All values must be either a percentage or
495 * an absolute number. However this parser accepts also mixed
496 * values. */
497CssColor::CssColorValue CssColor::parse(const QString &string)
498{
499 auto myString = string.simplified();
500 if (myString.endsWith(QStringLiteral(";"))) {
501 myString.chop(1);
502 myString = myString.simplified();
503 }
504
505 std::optional<QRgb> srgb = parseNamedColor(myString);
506 if (!srgb.has_value()) {
507 srgb = parseHexColor(myString);
508 }
509 if (srgb.has_value()) {
510 const auto srgbValue = srgb.value();
511 CssColorValue result;
512 result.model = ColorModel::Rgb1;
513 result.rgbColorSpace = CssPredefinedRgbColorSpace::Srgb;
514 result.color = GenericColor{qRed(srgbValue) / 255., //
515 qGreen(srgbValue) / 255., //
516 qBlue(srgbValue) / 255.};
517 result.alpha1 = qAlpha(srgbValue) / 255.;
518 return result;
519 }
520
521 return parseAbsoluteColorFunction(myString);
522}
523
524/** @internal
525 *
526 * @brief Converts a named color to sRGB (if any)
527 *
528 * Implements the
529 * <a href="https://www.w3.org/TR/css-color-4/#typedef-named-color">
530 * Named colors</a> and the
531 * <a href="https://www.w3.org/TR/css-color-4/#transparent-color">
532 * transparent keyword</a> as defined in CSS Color 4.
533 *
534 * @param namedColor The named color to search for.
535 *
536 * @returns The sRGB value if its a CSS named color (case-insensitive). An
537 * empty value otherwise. */
538std::optional<QRgb> CssColor::parseNamedColor(const QString &namedColor)
539{
540 using NamedColor = std::pair<QString, QRgb>;
541 // clang-format off
542 static const QHash<QString, QRgb> colorList {
543 // From https://www.w3.org/TR/css-color-4/#transparent-color
544 NamedColor(QStringLiteral("transparent"), 0x00000000),
545 // From https://www.w3.org/TR/css-color-4/#named-colors
546 NamedColor(QStringLiteral("aliceblue"), 0xfff0f8ff),
547 NamedColor(QStringLiteral("antiquewhite"), 0xfffaebd7),
548 NamedColor(QStringLiteral("aqua"), 0xff00ffff),
549 NamedColor(QStringLiteral("aquamarine"), 0xff7fffd4),
550 NamedColor(QStringLiteral("azure"), 0xfff0ffff),
551 NamedColor(QStringLiteral("beige"), 0xfff5f5dc),
552 NamedColor(QStringLiteral("bisque"), 0xffffe4c4),
553 NamedColor(QStringLiteral("black"), 0xff000000),
554 NamedColor(QStringLiteral("blanchedalmond"), 0xffffebcd),
555 NamedColor(QStringLiteral("blue"), 0xff0000ff),
556 NamedColor(QStringLiteral("blueviolet"), 0xff8a2be2),
557 NamedColor(QStringLiteral("brown"), 0xffa52a2a),
558 NamedColor(QStringLiteral("burlywood"), 0xffdeb887),
559 NamedColor(QStringLiteral("cadetblue"), 0xff5f9ea0),
560 NamedColor(QStringLiteral("chartreuse"), 0xff7fff00),
561 NamedColor(QStringLiteral("chocolate"), 0xffd2691e),
562 NamedColor(QStringLiteral("coral"), 0xffff7f50),
563 NamedColor(QStringLiteral("cornflowerblue"), 0xff6495ed),
564 NamedColor(QStringLiteral("cornsilk"), 0xfffff8dc),
565 NamedColor(QStringLiteral("crimson"), 0xffdc143c),
566 NamedColor(QStringLiteral("cyan"), 0xff00ffff),
567 NamedColor(QStringLiteral("darkblue"), 0xff00008b),
568 NamedColor(QStringLiteral("darkcyan"), 0xff008b8b),
569 NamedColor(QStringLiteral("darkgoldenrod"), 0xffb8860b),
570 NamedColor(QStringLiteral("darkgray"), 0xffa9a9a9),
571 NamedColor(QStringLiteral("darkgreen"), 0xff006400),
572 NamedColor(QStringLiteral("darkgrey"), 0xffa9a9a9),
573 NamedColor(QStringLiteral("darkkhaki"), 0xffbdb76b),
574 NamedColor(QStringLiteral("darkmagenta"), 0xff8b008b),
575 NamedColor(QStringLiteral("darkolivegreen"), 0xff556b2f),
576 NamedColor(QStringLiteral("darkorange"), 0xffff8c00),
577 NamedColor(QStringLiteral("darkorchid"), 0xff9932cc),
578 NamedColor(QStringLiteral("darkred"), 0xff8b0000),
579 NamedColor(QStringLiteral("darksalmon"), 0xffe9967a),
580 NamedColor(QStringLiteral("darkseagreen"), 0xff8fbc8f),
581 NamedColor(QStringLiteral("darkslateblue"), 0xff483d8b),
582 NamedColor(QStringLiteral("darkslategray"), 0xff2f4f4f),
583 NamedColor(QStringLiteral("darkslategrey"), 0xff2f4f4f),
584 NamedColor(QStringLiteral("darkturquoise"), 0xff00ced1),
585 NamedColor(QStringLiteral("darkviolet"), 0xff9400d3),
586 NamedColor(QStringLiteral("deeppink"), 0xffff1493),
587 NamedColor(QStringLiteral("deepskyblue"), 0xff00bfff),
588 NamedColor(QStringLiteral("dimgray"), 0xff696969),
589 NamedColor(QStringLiteral("dimgrey"), 0xff696969),
590 NamedColor(QStringLiteral("dodgerblue"), 0xff1e90ff),
591 NamedColor(QStringLiteral("firebrick"), 0xffb22222),
592 NamedColor(QStringLiteral("floralwhite"), 0xfffffaf0),
593 NamedColor(QStringLiteral("forestgreen"), 0xff228b22),
594 NamedColor(QStringLiteral("fuchsia"), 0xffff00ff),
595 NamedColor(QStringLiteral("gainsboro"), 0xffdcdcdc),
596 NamedColor(QStringLiteral("ghostwhite"), 0xfff8f8ff),
597 NamedColor(QStringLiteral("gold"), 0xffffd700),
598 NamedColor(QStringLiteral("goldenrod"), 0xffdaa520),
599 NamedColor(QStringLiteral("gray"), 0xff808080),
600 NamedColor(QStringLiteral("green"), 0xff008000),
601 NamedColor(QStringLiteral("greenyellow"), 0xffadff2f),
602 NamedColor(QStringLiteral("grey"), 0xff808080),
603 NamedColor(QStringLiteral("honeydew"), 0xfff0fff0),
604 NamedColor(QStringLiteral("hotpink"), 0xffff69b4),
605 NamedColor(QStringLiteral("indianred"), 0xffcd5c5c),
606 NamedColor(QStringLiteral("indigo"), 0xff4b0082),
607 NamedColor(QStringLiteral("ivory"), 0xfffffff0),
608 NamedColor(QStringLiteral("khaki"), 0xfff0e68c),
609 NamedColor(QStringLiteral("lavender"), 0xffe6e6fa),
610 NamedColor(QStringLiteral("lavenderblush"), 0xfffff0f5),
611 NamedColor(QStringLiteral("lawngreen"), 0xff7cfc00),
612 NamedColor(QStringLiteral("lemonchiffon"), 0xfffffacd),
613 NamedColor(QStringLiteral("lightblue"), 0xffadd8e6),
614 NamedColor(QStringLiteral("lightcoral"), 0xfff08080),
615 NamedColor(QStringLiteral("lightcyan"), 0xffe0ffff),
616 NamedColor(QStringLiteral("lightgoldenrodyellow"), 0xfffafad2),
617 NamedColor(QStringLiteral("lightgray"), 0xffd3d3d3),
618 NamedColor(QStringLiteral("lightgreen"), 0xff90ee90),
619 NamedColor(QStringLiteral("lightgrey"), 0xffd3d3d3),
620 NamedColor(QStringLiteral("lightpink"), 0xffffb6c1),
621 NamedColor(QStringLiteral("lightsalmon"), 0xffffa07a),
622 NamedColor(QStringLiteral("lightseagreen"), 0xff20b2aa),
623 NamedColor(QStringLiteral("lightskyblue"), 0xff87cefa),
624 NamedColor(QStringLiteral("lightslategray"), 0xff778899),
625 NamedColor(QStringLiteral("lightslategrey"), 0xff778899),
626 NamedColor(QStringLiteral("lightsteelblue"), 0xffb0c4de),
627 NamedColor(QStringLiteral("lightyellow"), 0xffffffe0),
628 NamedColor(QStringLiteral("lime"), 0xff00ff00),
629 NamedColor(QStringLiteral("limegreen"), 0xff32cd32),
630 NamedColor(QStringLiteral("linen"), 0xfffaf0e6),
631 NamedColor(QStringLiteral("magenta"), 0xffff00ff),
632 NamedColor(QStringLiteral("maroon"), 0xff800000),
633 NamedColor(QStringLiteral("mediumaquamarine"), 0xff66cdaa),
634 NamedColor(QStringLiteral("mediumblue"), 0xff0000cd),
635 NamedColor(QStringLiteral("mediumorchid"), 0xffba55d3),
636 NamedColor(QStringLiteral("mediumpurple"), 0xff9370db),
637 NamedColor(QStringLiteral("mediumseagreen"), 0xff3cb371),
638 NamedColor(QStringLiteral("mediumslateblue"), 0xff7b68ee),
639 NamedColor(QStringLiteral("mediumspringgreen"), 0xff00fa9a),
640 NamedColor(QStringLiteral("mediumturquoise"), 0xff48d1cc),
641 NamedColor(QStringLiteral("mediumvioletred"), 0xffc71585),
642 NamedColor(QStringLiteral("midnightblue"), 0xff191970),
643 NamedColor(QStringLiteral("mintcream"), 0xfff5fffa),
644 NamedColor(QStringLiteral("mistyrose"), 0xffffe4e1),
645 NamedColor(QStringLiteral("moccasin"), 0xffffe4b5),
646 NamedColor(QStringLiteral("navajowhite"), 0xffffdead),
647 NamedColor(QStringLiteral("navy"), 0xff000080),
648 NamedColor(QStringLiteral("oldlace"), 0xfffdf5e6),
649 NamedColor(QStringLiteral("olive"), 0xff808000),
650 NamedColor(QStringLiteral("olivedrab"), 0xff6b8e23),
651 NamedColor(QStringLiteral("orange"), 0xffffa500),
652 NamedColor(QStringLiteral("orangered"), 0xffff4500),
653 NamedColor(QStringLiteral("orchid"), 0xffda70d6),
654 NamedColor(QStringLiteral("palegoldenrod"), 0xffeee8aa),
655 NamedColor(QStringLiteral("palegreen"), 0xff98fb98),
656 NamedColor(QStringLiteral("paleturquoise"), 0xffafeeee),
657 NamedColor(QStringLiteral("palevioletred"), 0xffdb7093),
658 NamedColor(QStringLiteral("papayawhip"), 0xffffefd5),
659 NamedColor(QStringLiteral("peachpuff"), 0xffffdab9),
660 NamedColor(QStringLiteral("peru"), 0xffcd853f),
661 NamedColor(QStringLiteral("pink"), 0xffffc0cb),
662 NamedColor(QStringLiteral("plum"), 0xffdda0dd),
663 NamedColor(QStringLiteral("powderblue"), 0xffb0e0e6),
664 NamedColor(QStringLiteral("purple"), 0xff800080),
665 NamedColor(QStringLiteral("rebeccapurple"), 0xff663399),
666 NamedColor(QStringLiteral("red"), 0xffff0000),
667 NamedColor(QStringLiteral("rosybrown"), 0xffbc8f8f),
668 NamedColor(QStringLiteral("royalblue"), 0xff4169e1),
669 NamedColor(QStringLiteral("saddlebrown"), 0xff8b4513),
670 NamedColor(QStringLiteral("salmon"), 0xfffa8072),
671 NamedColor(QStringLiteral("sandybrown"), 0xfff4a460),
672 NamedColor(QStringLiteral("seagreen"), 0xff2e8b57),
673 NamedColor(QStringLiteral("seashell"), 0xfffff5ee),
674 NamedColor(QStringLiteral("sienna"), 0xffa0522d),
675 NamedColor(QStringLiteral("silver"), 0xffc0c0c0),
676 NamedColor(QStringLiteral("skyblue"), 0xff87ceeb),
677 NamedColor(QStringLiteral("slateblue"), 0xff6a5acd),
678 NamedColor(QStringLiteral("slategray"), 0xff708090),
679 NamedColor(QStringLiteral("slategrey"), 0xff708090),
680 NamedColor(QStringLiteral("snow"), 0xfffffafa),
681 NamedColor(QStringLiteral("springgreen"), 0xff00ff7f),
682 NamedColor(QStringLiteral("steelblue"), 0xff4682b4),
683 NamedColor(QStringLiteral("tan"), 0xffd2b48c),
684 NamedColor(QStringLiteral("teal"), 0xff008080),
685 NamedColor(QStringLiteral("thistle"), 0xffd8bfd8),
686 NamedColor(QStringLiteral("tomato"), 0xffff6347),
687 NamedColor(QStringLiteral("turquoise"), 0xff40e0d0),
688 NamedColor(QStringLiteral("violet"), 0xffee82ee),
689 NamedColor(QStringLiteral("wheat"), 0xfff5deb3),
690 NamedColor(QStringLiteral("white"), 0xffffffff),
691 NamedColor(QStringLiteral("whitesmoke"), 0xfff5f5f5),
692 NamedColor(QStringLiteral("yellow"), 0xffffff00),
693 NamedColor(QStringLiteral("yellowgreen"), 0xff9acd32)
694 };
695 // clang-format on
696 const QString lowerCase = namedColor.toLower();
697 if (colorList.contains(lowerCase)) {
698 return colorList.value(lowerCase);
699 }
700 return std::nullopt;
701}
702
703/** @brief Provides CSS code for existing color values.
704 *
705 * This function is meant for exporting colors to CSS code.
706 *
707 * @param input A hash table with color values.
708 * @param opacity1 The opacity of the color in the range [0, 1].
709 * @param significantFigures The requested number of significant figures.
710 *
711 * @returns A list of CSS color codes, ordered by importance: oklch, oklab,
712 * lch, lab, xyz-d50, xyz-d65. oklch is considered most important, followed
713 * by its less intuitive companion oklab, followed by the less perceptually
714 * uniform lch and lab. Finally comes the technically important, but
715 * uncomfortable xyz space, starting with its D50 variant because this
716 * is more wide-spread used in color management than the D65 variant.
717 * RGB-based color models are intentionally omitted, because we can never
718 * be sure if a given color is available in all of these spaces, especially
719 * if the library is using a wide-color gamut, but the CSS code requires
720 * sRGB. And if it would sometimes work (in-gamut colors) and sometimes fail
721 * (out-of-gamut colors), this might be highly confusing for the average
722 * user. Note that the alpha value only appears explicitly if it’s partially
723 * or fully transparent. Fully opaque colors do not need to specify the
724 * alpha value in CSS explicitly, because CSS defaults to “fully opaque” if
725 * no alpha value is given. */
726QStringList CssColor::generateCss(const QHash<ColorModel, GenericColor> &input, const double opacity1, const int significantFigures)
727{
728 QStringList result;
729
730 const auto decimals1 = decimalPlaces(2, significantFigures);
731 const auto decimals2 = decimalPlaces(2, significantFigures);
732 const auto decimals100 = decimalPlaces(100, significantFigures);
733 const auto decimals255 = decimalPlaces(255, significantFigures);
734 const auto decimals360 = decimalPlaces(360, significantFigures);
735
736 const QString opacity1String = (opacity1 < 1) //
737 ? QStringLiteral(" / %1%").arg(opacity1 * 100, 0, 'f', decimals100)
738 : QString();
739
740 if (input.contains(ColorModel::OklchD65)) {
741 const auto temp = input.value(ColorModel::OklchD65);
742 result.append(QStringLiteral("oklch(%1 %2 %3%4)") //
743 .arg(temp.first, 0, 'f', decimals1) //
744 .arg(temp.second, 0, 'f', decimals2) //
745 .arg(temp.third, 0, 'f', decimals360) //
746 .arg(opacity1String));
747 }
748
749 if (input.contains(ColorModel::OklabD65)) {
750 const auto temp = input.value(ColorModel::OklabD65);
751 result.append(QStringLiteral("oklab(%1 %2 %3%4)") //
752 .arg(temp.first, 0, 'f', decimals1) //
753 .arg(temp.second, 0, 'f', decimals2) //
754 .arg(temp.third, 0, 'f', decimals2) //
755 .arg(opacity1String));
756 }
757
758 if (input.contains(ColorModel::CielchD50)) {
759 const auto temp = input.value(ColorModel::CielchD50);
760 result.append(QStringLiteral("lch(%1 %2 %3%4)") //
761 .arg(temp.first, 0, 'f', decimals100) //
762 .arg(temp.second, 0, 'f', decimals255) //
763 .arg(temp.third, 0, 'f', decimals360) //
764 .arg(opacity1String));
765 }
766
767 if (input.contains(ColorModel::CielabD50)) {
768 const auto temp = input.value(ColorModel::CielabD50);
769 result.append(QStringLiteral("lab(%1 %2 %3%4)") //
770 .arg(temp.first, 0, 'f', decimals100) //
771 .arg(temp.second, 0, 'f', decimals255) //
772 .arg(temp.third, 0, 'f', decimals255) //
773 .arg(opacity1String));
774 }
775
776 if (input.contains(ColorModel::XyzD50)) {
777 const auto temp = input.value(ColorModel::XyzD50);
778 result.append(QStringLiteral("color (xyz-d50 %1 %2 %3%4)") //
779 .arg(temp.first, 0, 'f', decimals1) //
780 .arg(temp.second, 0, 'f', decimals1) //
781 .arg(temp.third, 0, 'f', decimals1) //
782 .arg(opacity1String));
783 }
784
785 if (input.contains(ColorModel::XyzD65)) {
786 const auto temp = input.value(ColorModel::XyzD65);
787 result.append(QStringLiteral("color (xyz-d65 %1 %2 %3%4)") //
788 .arg(temp.first, 0, 'f', decimals1) //
789 .arg(temp.second, 0, 'f', decimals1) //
790 .arg(temp.third, 0, 'f', decimals1) //
791 .arg(opacity1String));
792 }
793
794 return result;
795}
796
797} // namespace PerceptualColor
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
The namespace of this library.
int decimalPlaces(const int rangeMax, const int significantFigures)
Calculates the required number of decimals to achieve the requested number of significant figures wit...
@ OklabD65
Oklab color space, which by definition always and exclusively uses a D65 illuminant.
@ XyzD50
Xyz color space using a D50 illuminant.
@ CielabD50
Cielab color space using a D50 illuminant.
@ OklchD65
Oklch color space, which by definition always and exclusively uses a D65 illuminant.
@ Hsl360_1_1
A HSL color space.
@ Invalid
Represents invalid data.
@ XyzD65
Xzy color space using a D65 illuminant.
@ Rgb1
An Rgb color space.
@ CielchD50
Cielch color space using a D50 illuminant.
@ Hwb360_1_1
A HWB color space.
bool contains(const Key &key) const const
T value(const Key &key) const const
void append(QList< T > &&value)
qsizetype count() const const
void reserve(qsizetype size)
T value(qsizetype i) const const
qsizetype count() const const
QString arg(Args &&... args) const const
const QChar at(qsizetype position) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QString simplified() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
double toDouble(bool *ok) const const
QString toLower() const const
void truncate(qsizetype position)
KeepEmptyParts
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 3 2024 11:51:26 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.