Perceptual Color

rgbcolorspace.cpp
1// SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
2// SPDX-License-Identifier: BSD-2-Clause OR MIT
3
4// Own headers
5// First the interface, which forces the header to be self-contained.
6#include "rgbcolorspace.h"
7// Second, the private implementation.
8#include "rgbcolorspace_p.h" // IWYU pragma: associated
9
10#include "absolutecolor.h"
11#include "constpropagatingrawpointer.h"
12#include "constpropagatinguniquepointer.h"
13#include "genericcolor.h"
14#include "helperconstants.h"
15#include "helperconversion.h"
16#include "helpermath.h"
17#include "helperqttypes.h"
18#include "initializetranslation.h"
19#include "iohandlerfactory.h"
20#include "lchdouble.h"
21#include <algorithm>
22#include <limits>
23#include <optional>
24#include <qbytearray.h>
25#include <qcolor.h>
26#include <qcoreapplication.h>
27#include <qfileinfo.h>
28#include <qlocale.h>
29#include <qmath.h>
30#include <qnamespace.h>
31#include <qrgba64.h>
32#include <qsharedpointer.h>
33#include <qstringliteral.h>
34
35#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
36#include <qcontainerfwd.h>
37#include <qlist.h>
38#else
39#include <qstringlist.h>
40#endif
41
42// Include the type “tm” as defined in the C standard (time.h), as LittleCMS
43// expects, preventing IWYU < 0.19 to produce false-positives.
44#include <time.h> // IWYU pragma: keep
45// IWYU pragma: no_include <bits/types/struct_tm.h>
46
47namespace PerceptualColor
48{
49/** @internal
50 *
51 * @brief Constructor
52 *
53 * @attention Creates an uninitialised object. You have to call
54 * @ref RgbColorSpacePrivate::initialize() <em>successfully</em>
55 * before actually use object. */
56RgbColorSpace::RgbColorSpace(QObject *parent)
57 : QObject(parent)
58 , d_pointer(new RgbColorSpacePrivate(this))
59{
60}
61
62/** @brief Create an sRGB color space object.
63 *
64 * This is build-in, no external ICC file is used.
65 *
66 * @pre This function is called from the main thread.
67 *
68 * @returns A shared pointer to the newly created color space object.
69 *
70 * @sa @ref RgbColorSpaceFactory::createSrgb()
71 *
72 * @internal
73 *
74 * @note This function has to be called from the main thread because
75 * <a href="https://doc.qt.io/qt-6/qobject.html#tr">it is not save to use
76 * <tt>QObject::tr()</tt> while a new translation is loaded into
77 * QCoreApplication</a>, which should happen within the main thread. Therefore,
78 * if this function is also called within the main thread, we can use
79 * QObject::tr() safely because there will be not be executed simultaneously
80 * with loading a translation. */
81QSharedPointer<PerceptualColor::RgbColorSpace> RgbColorSpace::createSrgb()
82{
83 // Create an invalid object:
84 QSharedPointer<PerceptualColor::RgbColorSpace> result{new RgbColorSpace()};
85
86 // Transform it into a valid object:
87 cmsHPROFILE srgb = cmsCreate_sRGBProfile(); // Use build-in profile
88 const bool success = result->d_pointer->initialize(srgb);
89 cmsCloseProfile(srgb);
90
91 if (!success) {
92 // This should never fail. If it fails anyway, that’s a
93 // programming error and we throw an exception.
94 throw 0;
95 }
96
97 initializeTranslation(QCoreApplication::instance(),
98 // An empty std::optional means: If in initialization
99 // had been done yet, repeat this initialization.
100 // If not, do a new initialization now with default
101 // values.
102 std::optional<QStringList>());
103
104 // Fine-tuning (and localization) for this build-in profile:
105 result->d_pointer->m_profileCreationDateTime = QDateTime();
106 /*: @item Manufacturer information for the built-in sRGB color. */
107 result->d_pointer->m_profileManufacturer = tr("LittleCMS");
108 result->d_pointer->m_profileModel = QString();
109 /*: @item Name of the built-in sRGB color space. */
110 result->d_pointer->m_profileName = tr("sRGB color space");
111 result->d_pointer->m_profileMaximumCielchD50Chroma = 132;
112
113 // Return:
114 return result;
115}
116
117/** @brief Try to create a color space object for a given ICC file.
118 *
119 * @note This function may fail to create the color space object when it
120 * cannot open the given file, or when the file cannot be interpreted.
121 *
122 * @pre This function is called from the main thread.
123 *
124 * @param fileName The file name. See <tt>QFile</tt> documentation
125 * for what are valid file names. The file is only used during the
126 * execution of this function and it is closed again at the end of
127 * this function. The created object does not need the file anymore,
128 * because all necessary information has already been loaded into
129 * memory. Accepted are most RGB-based ICC profiles up to version 4.
130 *
131 * @returns A shared pointer to a newly created color space object on success.
132 * A shared pointer to <tt>nullptr</tt> on fail.
133 *
134 * @sa @ref RgbColorSpaceFactory::createFromFile()
135 *
136 * @internal
137 *
138 * @todo The value for @ref profileMaximumCielchD50Chroma should be the actual maximum
139 * chroma value of the profile, and not a fallback default value as currently.
140 *
141 * @note Currently, there is no function that loads a profile from a memory
142 * buffer instead of a file. However it would easily be possible to implement
143 * this if necessary, because LittleCMS allows loading from a memory buffer.
144 *
145 * @note While it is not strictly necessary to call this function within
146 * the main thread, we put it nevertheless as precondition because of
147 * consistency with @ref createSrgb().
148 *
149 * @note The new <a href="https://www.color.org/iccmax/index.xalter">version 5
150 * (iccMax)</a> is <em>not</em> accepted. <a href="https://www.littlecms.com/">
151 * LittleCMS</a> does not support ICC version 5, but only
152 * up to version 4. The ICC organization itself provides
153 * a <a href="https://github.com/InternationalColorConsortium/DemoIccMAX">demo
154 * implementation</a>, but this does not seem to be a complete color
155 * management system. */
156QSharedPointer<PerceptualColor::RgbColorSpace> RgbColorSpace::createFromFile(const QString &fileName)
157{
158 // TODO xxx Only accept Display Class profiles
159
160 // Definitions
161 constexpr auto myContextID = nullptr;
162
163 // Create an IO handler for the file
164 cmsIOHANDLER *myIOHandler = //
165 IOHandlerFactory::createReadOnly(myContextID, fileName);
166 if (myIOHandler == nullptr) {
167 return nullptr;
168 }
169
170 // Create a handle to a LittleCMS profile representation
171 cmsHPROFILE myProfileHandle = //
172 cmsOpenProfileFromIOhandlerTHR(myContextID, myIOHandler);
173 if (myProfileHandle == nullptr) {
174 // If cmsOpenProfileFromIOhandlerTHR fails to create a profile
175 // handle, it deletes the IO handler. Therefore, we do not
176 // have to delete the underlying IO handler manually.
177 return nullptr;
178 }
179
180 // Create an invalid object:
181 QSharedPointer<PerceptualColor::RgbColorSpace> newObject{new RgbColorSpace()};
182
183 // Try to transform it into a valid object:
184 const QFileInfo myFileInfo{fileName};
185 newObject->d_pointer->m_profileAbsoluteFilePath = //
186 myFileInfo.absoluteFilePath();
187 newObject->d_pointer->m_profileFileSize = myFileInfo.size();
188 const bool success = newObject->d_pointer->initialize(myProfileHandle);
189
190 // Clean up
191 cmsCloseProfile(myProfileHandle); // Also deletes the underlying IO handler
192
193 // Return
194 if (success) {
195 return newObject;
196 }
197 return nullptr;
198}
199
200/** @brief Basic initialization.
201 *
202 * This function is meant to be called when constructing the object.
203 *
204 * @param rgbProfileHandle Handle for the RGB profile
205 *
206 * @pre rgbProfileHandle is valid.
207 *
208 * @returns <tt>true</tt> on success. <tt>false</tt> otherwise (for example
209 * when it’s not an RGB profile but an CMYK profile). When <tt>false</tt>
210 * is returned, the object is still in an undefined state; it cannot
211 * be used, but only be destroyed. This should happen as soon as
212 * possible to reduce memory usage.
213 *
214 * @note rgbProfileHandle is <em>not</em> deleted in this function.
215 * Remember to delete it manually.
216 *
217 * @internal
218 *
219 * @todo LUT profiles should be detected and refused, as the actual diagram
220 * results are currently bad. (LUT profiles for RGB are not common among
221 * the usual standard profile files. But they might be more common among
222 * individually calibrated monitors?)
223 *
224 * @todo This function is used in @ref RgbColorSpace::createSrgb()
225 * and @ref RgbColorSpace::createFromFile(), but some of the initialization
226 * is changed afterwards (file name, file size, profile name, maximum chroma).
227 * Is it possible to find a more elegant design? */
228bool RgbColorSpacePrivate::initialize(cmsHPROFILE rgbProfileHandle)
229{
230 constexpr auto renderingIntent = INTENT_ABSOLUTE_COLORIMETRIC;
231
232 m_profileClass = cmsGetDeviceClass(rgbProfileHandle);
233 m_profileColorModel = cmsGetColorSpace(rgbProfileHandle);
234 m_profileCopyright = getInformationFromProfile(rgbProfileHandle, //
235 cmsInfoCopyright);
236 m_profileCreationDateTime = //
237 getCreationDateTimeFromProfile(rgbProfileHandle);
238 const bool inputUsesCLUT = cmsIsCLUT(rgbProfileHandle, //
239 renderingIntent, //
240 LCMS_USED_AS_INPUT);
241 const bool outputUsesCLUT = cmsIsCLUT(rgbProfileHandle, //
242 renderingIntent, //
243 LCMS_USED_AS_OUTPUT);
244 // There is a third value, LCMS_USED_AS_PROOF. This value seem to return
245 // always true, even for the sRGB built-in profile. Not sure if this is
246 // a bug? Anyway, as we do not actually use the profile in proof mode,
247 // we can discard this information.
248 m_profileHasClut = inputUsesCLUT || outputUsesCLUT;
249 m_profileHasMatrixShaper = cmsIsMatrixShaper(rgbProfileHandle);
250 m_profileIccVersion = getIccVersionFromProfile(rgbProfileHandle);
251 m_profileManufacturer = getInformationFromProfile(rgbProfileHandle, //
252 cmsInfoManufacturer);
253 m_profileModel = getInformationFromProfile(rgbProfileHandle, //
254 cmsInfoModel);
255 m_profileName = getInformationFromProfile(rgbProfileHandle, //
256 cmsInfoDescription);
257 m_profilePcsColorModel = cmsGetPCS(rgbProfileHandle);
258
259 {
260 // Create an ICC v4 profile object for the CielabD50 color space.
261 cmsHPROFILE cielabD50ProfileHandle = cmsCreateLab4Profile(
262 // nullptr means: Default white point (D50)
263 // TODO Does this make sense? sRGB, for example, has
264 // D65 as whitepoint…
265 nullptr);
266
267 // Create the transforms.
268 // We use the flag cmsFLAGS_NOCACHE which disables the 1-pixel-cache
269 // which is normally used in the transforms. We do this because
270 // transforms that use the 1-pixel-cache are not thread-safe. And
271 // disabling it should not have negative impacts as we usually work
272 // with gradients, so anyway it is not likely to have two consecutive
273 // pixels with the same color, which is the only situation where the
274 // 1-pixel-cache makes processing faster.
275 constexpr auto flags = cmsFLAGS_NOCACHE;
276 m_transformCielabD50ToRgbHandle = cmsCreateTransform(
277 // Create a transform function and get a handle to this function:
278 cielabD50ProfileHandle, // input profile handle
279 TYPE_Lab_DBL, // input buffer format
280 rgbProfileHandle, // output profile handle
281 TYPE_RGB_DBL, // output buffer format
282 renderingIntent,
283 flags);
284 m_transformCielabD50ToRgb16Handle = cmsCreateTransform(
285 // Create a transform function and get a handle to this function:
286 cielabD50ProfileHandle, // input profile handle
287 TYPE_Lab_DBL, // input buffer format
288 rgbProfileHandle, // output profile handle
289 TYPE_RGB_16, // output buffer format
290 renderingIntent,
291 flags);
292 m_transformRgbToCielabD50Handle = cmsCreateTransform(
293 // Create a transform function and get a handle to this function:
294 rgbProfileHandle, // input profile handle
295 TYPE_RGB_DBL, // input buffer format
296 cielabD50ProfileHandle, // output profile handle
297 TYPE_Lab_DBL, // output buffer format
298 renderingIntent,
299 flags);
300 // It is mandatory to close the profiles to prevent memory leaks:
301 cmsCloseProfile(cielabD50ProfileHandle);
302 }
303
304 // After having closed the profiles, we can now return
305 // (if appropriate) without having memory leaks:
306 if ((m_transformCielabD50ToRgbHandle == nullptr) //
307 || (m_transformCielabD50ToRgb16Handle == nullptr) //
308 || (m_transformRgbToCielabD50Handle == nullptr) //
309 ) {
310 return false;
311 }
312
313 // Maximum chroma:
314 // TODO Detect an appropriate value for m_profileMaximumCielchD50Chroma.
315
316 // Find blackpoint and whitepoint.
317 // For CielabD50 make sure that: 0 <= blackbpoint < whitepoint <= 100
318 LchDouble candidate;
319 candidate.c = 0;
320 candidate.h = 0;
321 candidate.l = 0;
322 while (!q_pointer->isCielchD50InGamut(candidate)) {
323 candidate.l += gamutPrecisionCielab;
324 if (candidate.l >= 100) {
325 return false;
326 }
327 }
328 m_cielabD50BlackpointL = candidate.l;
329 candidate.l = 100;
330 while (!q_pointer->isCielchD50InGamut(candidate)) {
331 candidate.l -= gamutPrecisionCielab;
332 if (candidate.l <= m_cielabD50BlackpointL) {
333 return false;
334 }
335 }
336 m_cielabD50WhitepointL = candidate.l;
337 // For Oklab make sure that: 0 <= blackbpoint < whitepoint <= 1
338 candidate.l = 0;
339 while (!q_pointer->isOklchInGamut(candidate)) {
340 candidate.l += gamutPrecisionOklab;
341 if (candidate.l >= 1) {
342 return false;
343 }
344 }
345 m_oklabBlackpointL = candidate.l;
346 candidate.l = 1;
347 while (!q_pointer->isOklchInGamut(candidate)) {
348 candidate.l -= gamutPrecisionOklab;
349 if (candidate.l <= m_oklabBlackpointL) {
350 return false;
351 }
352 }
353 m_oklabWhitepointL = candidate.l;
354
355 // Now, calculate the properties who’s calculation depends on a fully
356 // initialized object.
357 m_profileMaximumCielchD50Chroma = detectMaximumCielchD50Chroma();
358 m_profileMaximumOklchChroma = detectMaximumOklchChroma();
359
360 return true;
361}
362
363/** @brief Destructor */
364RgbColorSpace::~RgbColorSpace() noexcept
365{
366 RgbColorSpacePrivate::deleteTransform( //
367 &d_pointer->m_transformCielabD50ToRgb16Handle);
368 RgbColorSpacePrivate::deleteTransform( //
369 &d_pointer->m_transformCielabD50ToRgbHandle);
370 RgbColorSpacePrivate::deleteTransform( //
371 &d_pointer->m_transformRgbToCielabD50Handle);
372}
373
374/** @brief Constructor
375 *
376 * @param backLink Pointer to the object from which <em>this</em> object
377 * is the private implementation. */
378RgbColorSpacePrivate::RgbColorSpacePrivate(RgbColorSpace *backLink)
379 : q_pointer(backLink)
380{
381}
382
383/** @brief Convenience function for deleting LittleCMS transforms
384 *
385 * <tt>cmsDeleteTransform()</tt> is not comfortable. Calling it on a
386 * <tt>nullptr</tt> crashes. If called on a valid handle, it does not
387 * reset the handle to <tt>nullptr</tt>. Calling it again on the now
388 * invalid handle crashes. This convenience function can be used instead
389 * of <tt>cmsDeleteTransform()</tt>: It provides some more comfort,
390 * by adding support for <tt>nullptr</tt> checks.
391 *
392 * @param transformHandle handle of the transform
393 *
394 * @post If the handle is <tt>nullptr</tt>, nothing happens. Otherwise,
395 * <tt>cmsDeleteTransform()</tt> is called, and afterwards the handle is set
396 * to <tt>nullptr</tt>. */
397void RgbColorSpacePrivate::deleteTransform(cmsHTRANSFORM *transformHandle)
398{
399 if ((*transformHandle) != nullptr) {
400 cmsDeleteTransform(*transformHandle);
401 (*transformHandle) = nullptr;
402 }
403}
404
405// No documentation here (documentation of properties
406// and its getters are in the header)
407QString RgbColorSpace::profileAbsoluteFilePath() const
408{
409 return d_pointer->m_profileAbsoluteFilePath;
410}
411
412// No documentation here (documentation of properties
413// and its getters are in the header)
414cmsProfileClassSignature RgbColorSpace::profileClass() const
415{
416 return d_pointer->m_profileClass;
417}
418
419// No documentation here (documentation of properties
420// and its getters are in the header)
421cmsColorSpaceSignature RgbColorSpace::profileColorModel() const
422{
423 return d_pointer->m_profileColorModel;
424}
425
426// No documentation here (documentation of properties
427// and its getters are in the header)
428QString RgbColorSpace::profileCopyright() const
429{
430 return d_pointer->m_profileCopyright;
431}
432
433// No documentation here (documentation of properties
434// and its getters are in the header)
435QDateTime RgbColorSpace::profileCreationDateTime() const
436{
437 return d_pointer->m_profileCreationDateTime;
438}
439
440// No documentation here (documentation of properties
441// and its getters are in the header)
442qint64 RgbColorSpace::profileFileSize() const
443{
444 return d_pointer->m_profileFileSize;
445}
446
447// No documentation here (documentation of properties
448// and its getters are in the header)
449bool RgbColorSpace::profileHasClut() const
450{
451 return d_pointer->m_profileHasClut;
452}
453
454// No documentation here (documentation of properties
455// and its getters are in the header)
456bool RgbColorSpace::profileHasMatrixShaper() const
457{
458 return d_pointer->m_profileHasMatrixShaper;
459}
460
461// No documentation here (documentation of properties
462// and its getters are in the header)
463QVersionNumber RgbColorSpace::profileIccVersion() const
464{
465 return d_pointer->m_profileIccVersion;
466}
467
468// No documentation here (documentation of properties
469// and its getters are in the header)
470QString RgbColorSpace::profileManufacturer() const
471{
472 return d_pointer->m_profileManufacturer;
473}
474
475// No documentation here (documentation of properties
476// and its getters are in the header)
477double RgbColorSpace::profileMaximumCielchD50Chroma() const
478{
479 return d_pointer->m_profileMaximumCielchD50Chroma;
480}
481
482// No documentation here (documentation of properties
483// and its getters are in the header)
484double RgbColorSpace::profileMaximumOklchChroma() const
485{
486 return d_pointer->m_profileMaximumOklchChroma;
487}
488
489// No documentation here (documentation of properties
490// and its getters are in the header)
491QString RgbColorSpace::profileModel() const
492{
493 return d_pointer->m_profileModel;
494}
495
496// No documentation here (documentation of properties
497// and its getters are in the header)
498QString RgbColorSpace::profileName() const
499{
500 return d_pointer->m_profileName;
501}
502
503// No documentation here (documentation of properties
504// and its getters are in the header)
505cmsColorSpaceSignature RgbColorSpace::profilePcsColorModel() const
506{
507 return d_pointer->m_profilePcsColorModel;
508}
509
510/** @brief Get information from an ICC profile via LittleCMS
511 *
512 * @param profileHandle handle to the ICC profile in which will be searched
513 * @param infoType the type of information that is searched
514 * @returns A QString with the information. It searches the
515 * information in the current locale (language code and country code as
516 * provided currently by <tt>QLocale</tt>). If the information is not
517 * available in this locale, LittleCMS silently falls back to another available
518 * localization. Note that the returned <tt>QString</tt> might be empty if the
519 * requested information is not available in the ICC profile. */
520QString RgbColorSpacePrivate::getInformationFromProfile(cmsHPROFILE profileHandle, cmsInfoType infoType)
521{
522 QByteArray languageCode;
524 // Update languageCode and countryCode to the actual locale (if possible)
525 const QStringList list = QLocale().name().split(QStringLiteral(u"_"));
526 // The list of locale codes should be ASCII only.
527 // Therefore QString::toUtf8() should return ASCII-only valid results.
528 // (We do not know what character encoding LittleCMS expects,
529 // but ASCII seems a safe choice.)
530 if (list.count() == 2) {
531 languageCode = list.at(0).toUtf8();
532 countryCode = list.at(1).toUtf8();
533 }
534 // Fallback for missing (empty) values to the default value recommended
535 // by LittleCMS documentation: “en” and “US”.
536 if (languageCode.size() != 2) {
537 // Encoding of C++ string literals is UTF8 (we have static_assert
538 // for this):
539 languageCode = QByteArrayLiteral("en");
540 }
541 if (countryCode.size() != 2) {
542 // Encoding of C++ string literals is UTF8 (we have a static_assert
543 // for this):
544 countryCode = QByteArrayLiteral("US");
545 }
546 // NOTE Since LittleCMS ≥ 2.16, cmsNoLanguage and cmsNoCountry could be
547 // used instead of "en" and "US" and would return simply the first language
548 // in the profile, but that seems less predictable and less reliably than
549 // "en" and "US".
550 //
551 // NOTE Do only v4 profiles provide internationalization, while v2 profiles
552 // don’t? This seems to be implied in LittleCMS documentation:
553 //
554 // “Since 2.16, a special setting for the lenguage and country allows
555 // to access the unicode variant on V2 profiles.
556 //
557 // For the language and country:
558 //
559 // cmsV2Unicode
560 //
561 // Many V2 profiles have this field empty or filled with bogus values.
562 // Previous versions of Little CMS were ignoring it, but with
563 // this additional setting, correct V2 profiles with two variants
564 // can be honored now. By default, the ASCII variant is returned on
565 // V2 profiles unless you specify this special setting. If you decide
566 // to use it, check the result for empty strings and if this is the
567 // case, repeat reading by using the normal path.”
568 //
569 // So maybe v2 profiles have just one ASCII and one Unicode string, and
570 // that’s all? If so, our approach seems fine: Our locale will be honored
571 // on v4 profiles, and it will be ignored on v2 profiles because we do not
572 // use cmsV2Unicode. This seems a wise choice, because otherwise we would
573 // need different code paths for v2 and v4 profiles, which would be even
574 // even more complex than the current code, and still potentially return
575 // “bogus values” (as LittleCMS the documentation states), so the result
576 // would be worse than the current code.
577
578 // Calculate the expected maximum size of the return value that we have
579 // to provide for cmsGetProfileInfo later on in order to return an
580 // actual value.
581 const cmsUInt32Number resultLength = cmsGetProfileInfo(
582 // Profile in which we search:
583 profileHandle,
584 // The type of information we search:
585 infoType,
586 // The preferred language in which we want to get the information:
587 languageCode.constData(),
588 // The preferred country for which we want to get the information:
590 // Do not actually provide the information,
591 // just return the required buffer size:
592 nullptr,
593 // Do not actually provide the information,
594 // just return the required buffer size:
595 0);
596 // For the actual buffer size, increment by 1. This helps us to
597 // guarantee a null-terminated string later on.
598 const cmsUInt32Number bufferLength = resultLength + 1;
599
600 // NOTE According to the documentation, it seems that cmsGetProfileInfo()
601 // calculates the buffer length in bytes and not in wchar_t. However,
602 // the documentation (as of LittleCMS 2.9) is not clear about the
603 // used encoding, and the buffer type must be wchar_t anyway, and
604 // wchar_t might have different sizes (either 16 bit or 32 bit) on
605 // different systems, and LittleCMS’ treatment of this situation is
606 // not well documented. Therefore, we interpret the buffer length
607 // as number of necessary wchart_t, which creates a greater buffer,
608 // which might possibly be waste of space, but it’s just a little bit
609 // of text, so that’s not so much space that is wasted finally.
610
611 // TODO For security reasons (you never know what surprise a foreign ICC
612 // file might have for us), it would be better to have a maximum
613 // length for the buffer, so that insane big buffer will not be
614 // actually created, and instead an empty string is returned.
615
616 // Allocate the buffer
617 wchar_t *buffer = new wchar_t[bufferLength];
618 // Initialize the buffer with 0
619 for (cmsUInt32Number i = 0; i < bufferLength; ++i) {
620 *(buffer + i) = 0;
621 }
622
623 // Write the actual information to the buffer
624 cmsGetProfileInfo(
625 // profile in which we search
626 profileHandle,
627 // the type of information we search
628 infoType,
629 // the preferred language in which we want to get the information
630 languageCode.constData(),
631 // the preferred country for which we want to get the information
633 // the buffer into which the requested information will be written
634 buffer,
635 // the buffer size as previously calculated by cmsGetProfileInfo
636 resultLength);
637 // Make absolutely sure the buffer is null-terminated by marking its last
638 // element (the one that was the +1 "extra" element) as null.
639 *(buffer + (bufferLength - 1)) = 0;
640
641 // Create a QString() from the from the buffer
642 //
643 // cmsGetProfileInfo returns often strings that are smaller than the
644 // previously calculated buffer size. But we had initialized the buffer
645 // with null, so actually we get a null-terminated string even if LittleCMS
646 // would not provide the final null. So we read only up to the first null
647 // value.
648 //
649 // LittleCMS returns wchar_t. This type might have different sizes:
650 // Depending on the operating system either 16 bit or 32 bit.
651 // LittleCMS does not specify the encoding in its documentation for
652 // cmsGetProfileInfo() as of LittleCMS 2.9. It only says “Strings are
653 // returned as wide chars.” So this is likely either UTF-16 or UTF-32.
654 // According to github.com/mm2/Little-CMS/issues/180#issue-421837278
655 // it is even UTF-16 when the size of wchar_t is 32 bit! And according
656 // to github.com/mm2/Little-CMS/issues/180#issuecomment-1007490587
657 // in LittleCMS versions after 2.13 it might be UTF-32 when the size
658 // of wchar_t is 32 bit. So the behaviour of LittleCMS changes between
659 // various versions. Conclusion: It’s either UTF-16 or UTF-32, but we
660 // never know which it is and have to be prepared for all possible
661 // combinations between UTF-16/UTF-32 and a wchar_t size of
662 // 16 bit/32 bit.
663 //
664 // QString::fromWCharArray can create a QString from this data. It
665 // accepts arrays of wchar_t. As Qt’s documentation of
666 // QString::fromWCharArray() says:
667 //
668 // “If wchar is 4 bytes, the string is interpreted as UCS-4,
669 // if wchar is 2 bytes it is interpreted as UTF-16.”
670 //
671 // However, apparently this is not exact: When wchar is 4 bytes,
672 // surrogate pairs in the code unit array are interpreted like UTF-16:
673 // The surrogate pair is recognized as such, which is not strictly
674 // UTF-32 conform, but enhances the compatibility. Single surrogates
675 // cannot be interpreted correctly, but there will be no crash:
676 // QString::fromWCharArray will continue to read, also the part
677 // after the first UTF error. So QString::fromWCharArray is quite
678 // error-tolerant, which is great as we do not exactly know the
679 // encoding of the buffer that LittleCMS returns. However, this is
680 // undocumented behaviour of QString::fromWCharArray which means
681 // it could change over time. Therefore, in the unit tests of this
682 // class, we test if QString::fromWCharArray actually behaves as we want.
683 //
684 // NOTE Instead of cmsGetProfileInfo(), we could also use
685 // cmsGetProfileInfoUTF8() which returns directly an UTF-8 encoded
686 // string. We were no longer required to guess the encoding, but we
687 // would have a return value in a well-defined encoding. However,
688 // this would also require LittleCMS ≥ 2.16, and we would still
689 // need the buffer.
690 const QString result = QString::fromWCharArray(
691 // Convert to string with these parameters:
692 buffer, // read from this buffer
693 -1 // read until the first null element
694 );
695
696 // Free allocated memory of the buffer
697 delete[] buffer;
698
699 // Return
700 return result;
701}
702
703/** @brief Get ICC version from profile via LittleCMS
704 *
705 * @param profileHandle handle to the ICC profile
706 * @returns The version number of the ICC format used in the profile. */
707QVersionNumber RgbColorSpacePrivate::getIccVersionFromProfile(cmsHPROFILE profileHandle)
708{
709 // cmsGetProfileVersion returns a floating point number. Apparently
710 // the digits before the decimal separator are the major version,
711 // and the digits after the decimal separator are the minor version.
712 // So, the version number strings “2.1” (major version 2, minor version 1)
713 // and “2.10” (major version 2, minor version 10) both get the same
714 // representation as floating point number 2.1 because floating
715 // point numbers do not have memory about how many trailing zeros
716 // exist. So we have to assume minor versions higher than 9 are not
717 // supported by cmsGetProfileVersion anyway. A positive side effect
718 // of this assumption is that is makes the conversion to QVersionNumber
719 // easier: We use a fixed width of exactly one digit for the
720 // part after the decimal separator. This makes also sure that
721 // the floating point number 2 is interpreted as “2.0” (and not
722 // simply as “2”).
723
724 // QString::number() ignores the locale and uses always a “.”
725 // as separator, which is exactly what we need to create
726 // a QVersionNumber from.
728 cmsGetProfileVersion(profileHandle), // floating point
729 'f', // use normal rendering format (no exponents)
730 1 // number of digits after the decimal point
731 );
732 return QVersionNumber::fromString(versionString);
733}
734
735/** @brief Date and time of creation of a profile via LittleCMS
736 *
737 * @param profileHandle handle to the ICC profile
738 * @returns Date and time of creation of the profile, if available. An invalid
739 * date and time otherwise. */
740QDateTime RgbColorSpacePrivate::getCreationDateTimeFromProfile(cmsHPROFILE profileHandle)
741{
742 tm myDateTime; // The type “tm” as defined in C (time.h), as LittleCMS expects.
743 const bool success = cmsGetHeaderCreationDateTime(profileHandle, &myDateTime);
744 if (!success) {
745 // Return invalid QDateTime object
746 return QDateTime();
747 }
748 const QDate myDate(myDateTime.tm_year + 1900, // tm_year means: years since 1900
749 myDateTime.tm_mon + 1, // tm_mon ranges fromm 0 to 11
750 myDateTime.tm_mday // tm_mday ranges from 1 to 31
751 );
752 // “tm” allows seconds higher than 59: It allows up to 60 seconds: The
753 // “supplement” second is for leap seconds. However, QTime does not
754 // accept seconds beyond 59. Therefore, this has to be corrected:
755 const QTime myTime(myDateTime.tm_hour, //
756 myDateTime.tm_min, //
757 qBound(0, myDateTime.tm_sec, 59));
758 return QDateTime(
759 // Date:
760 myDate,
761 // Time:
762 myTime,
763 // Assuming UTC for the QDateTime because it’s the only choice
764 // that will not change arbitrary.
765 Qt::TimeSpec::UTC);
766}
767
768/** @brief Reduces the chroma until the color fits into the gamut.
769 *
770 * It always preserves the hue. It preservers the lightness whenever
771 * possible.
772 *
773 * @note In some cases with very curvy color spaces, the nearest in-gamut
774 * color (with the same lightness and hue) might be at <em>higher</em>
775 * chroma. As this function always <em>reduces</em> the chroma,
776 * in this case the result is not the nearest in-gamut color.
777 *
778 * @param cielchD50color The color that will be adapted.
779 *
780 * @returns An @ref isCielchD50InGamut color. */
781PerceptualColor::LchDouble RgbColorSpace::reduceCielchD50ChromaToFitIntoGamut(const PerceptualColor::LchDouble &cielchD50color) const
782{
783 LchDouble referenceColor = cielchD50color;
784
785 // Normalize the LCH coordinates
786 normalizePolar360(referenceColor.c, referenceColor.h);
787
788 // Bound to valid range:
789 referenceColor.c = qMin<decltype(referenceColor.c)>( //
790 referenceColor.c, //
791 profileMaximumCielchD50Chroma());
792 referenceColor.l = qBound(d_pointer->m_cielabD50BlackpointL, //
793 referenceColor.l, //
794 d_pointer->m_cielabD50WhitepointL);
795
796 // Test special case: If we are yet in-gamut…
797 if (isCielchD50InGamut(referenceColor)) {
798 return referenceColor;
799 }
800
801 // Now we know: We are out-of-gamut.
802 LchDouble temp;
803
804 // Create an in-gamut point on the gray axis:
805 LchDouble lowerChroma{referenceColor.l, 0, referenceColor.h};
806 if (!isCielchD50InGamut(lowerChroma)) {
807 // This is quite strange because every point between the blackpoint
808 // and the whitepoint on the gray axis should be in-gamut on
809 // normally shaped gamuts. But as we never know, we need a fallback,
810 // which is guaranteed to be in-gamut:
811 referenceColor.l = d_pointer->m_cielabD50BlackpointL;
812 lowerChroma.l = d_pointer->m_cielabD50BlackpointL;
813 }
814 // TODO Decide which one of the algorithms provides with the “if constexpr”
815 // will be used (and remove the other one).
816 constexpr bool quickApproximate = true;
817 if constexpr (quickApproximate) {
818 // Do a quick-approximate search:
819 LchDouble upperChroma{referenceColor};
820 // Now we know for sure that lowerChroma is in-gamut
821 // and upperChroma is out-of-gamut…
822 temp = upperChroma;
823 while (upperChroma.c - lowerChroma.c > gamutPrecisionCielab) {
824 // Our test candidate is half the way between lowerChroma
825 // and upperChroma:
826 temp.c = ((lowerChroma.c + upperChroma.c) / 2);
827 if (isCielchD50InGamut(temp)) {
828 lowerChroma = temp;
829 } else {
830 upperChroma = temp;
831 }
832 }
833 return lowerChroma;
834
835 } else {
836 // Do a slow-thorough search:
837 temp = referenceColor;
838 while (temp.c > 0) {
839 if (isCielchD50InGamut(temp)) {
840 break;
841 } else {
842 temp.c -= gamutPrecisionCielab;
843 }
844 }
845 if (temp.c < 0) {
846 temp.c = 0;
847 }
848 return temp;
849 }
850}
851
852/** @brief Reduces the chroma until the color fits into the gamut.
853 *
854 * It always preserves the hue. It preservers the lightness whenever
855 * possible.
856 *
857 * @note In some cases with very curvy color spaces, the nearest in-gamut
858 * color (with the same lightness and hue) might be at <em>higher</em>
859 * chroma. As this function always <em>reduces</em> the chroma,
860 * in this case the result is not the nearest in-gamut color.
861 *
862 * @param oklchColor The color that will be adapted.
863 *
864 * @returns An @ref isOklchInGamut color. */
865PerceptualColor::LchDouble RgbColorSpace::reduceOklchChromaToFitIntoGamut(const PerceptualColor::LchDouble &oklchColor) const
866{
867 LchDouble referenceColor = oklchColor;
868
869 // Normalize the LCH coordinates
870 normalizePolar360(referenceColor.c, referenceColor.h);
871
872 // Bound to valid range:
873 referenceColor.c = qMin<decltype(referenceColor.c)>( //
874 referenceColor.c, //
875 profileMaximumOklchChroma());
876 referenceColor.l = qBound(d_pointer->m_oklabBlackpointL,
877 referenceColor.l, //
878 d_pointer->m_oklabWhitepointL);
879
880 // Test special case: If we are yet in-gamut…
881 if (isOklchInGamut(referenceColor)) {
882 return referenceColor;
883 }
884
885 // Now we know: We are out-of-gamut.
886 LchDouble temp;
887
888 // Create an in-gamut point on the gray axis:
889 LchDouble lowerChroma{referenceColor.l, 0, referenceColor.h};
890 if (!isOklchInGamut(lowerChroma)) {
891 // This is quite strange because every point between the blackpoint
892 // and the whitepoint on the gray axis should be in-gamut on
893 // normally shaped gamuts. But as we never know, we need a fallback,
894 // which is guaranteed to be in-gamut:
895 referenceColor.l = d_pointer->m_oklabBlackpointL;
896 lowerChroma.l = d_pointer->m_oklabBlackpointL;
897 }
898 // TODO Decide which one of the algorithms provides with the “if constexpr”
899 // will be used (and remove the other one).
900 constexpr bool quickApproximate = true;
901 if constexpr (quickApproximate) {
902 // Do a quick-approximate search:
903 LchDouble upperChroma{referenceColor};
904 // Now we know for sure that lowerChroma is in-gamut
905 // and upperChroma is out-of-gamut…
906 temp = upperChroma;
907 while (upperChroma.c - lowerChroma.c > gamutPrecisionOklab) {
908 // Our test candidate is half the way between lowerChroma
909 // and upperChroma:
910 temp.c = ((lowerChroma.c + upperChroma.c) / 2);
911 if (isOklchInGamut(temp)) {
912 lowerChroma = temp;
913 } else {
914 upperChroma = temp;
915 }
916 }
917 return lowerChroma;
918
919 } else {
920 // Do a slow-thorough search:
921 temp = referenceColor;
922 while (temp.c > 0) {
923 if (isOklchInGamut(temp)) {
924 break;
925 } else {
926 temp.c -= gamutPrecisionOklab;
927 }
928 }
929 if (temp.c < 0) {
930 temp.c = 0;
931 }
932 return temp;
933 }
934}
935
936/** @brief Conversion to CIELab.
937 *
938 * @param rgbColor The original color.
939 * @returns The corresponding (opaque) CIELab color.
940 *
941 * @note By definition, each RGB color in a given color space is an in-gamut
942 * color in this very same color space. Nevertheless, because of rounding
943 * errors, when converting colors that are near to the outer hull of the
944 * gamut/color space, than @ref isCielabD50InGamut() might return <tt>false</tt> for
945 * a return value of <em>this</em> function. */
946cmsCIELab RgbColorSpace::toCielabD50(const QRgba64 rgbColor) const
947{
948 constexpr qreal maximum = //
949 std::numeric_limits<decltype(rgbColor.red())>::max();
950 const double my_rgb[]{rgbColor.red() / maximum, //
951 rgbColor.green() / maximum, //
952 rgbColor.blue() / maximum};
953 cmsCIELab cielabD50;
954 cmsDoTransform(d_pointer->m_transformRgbToCielabD50Handle, // handle to transform
955 &my_rgb, // input
956 &cielabD50, // output
957 1 // convert exactly 1 value
958 );
959 if (cielabD50.L < 0) {
960 // Workaround for https://github.com/mm2/Little-CMS/issues/395
961 cielabD50.L = 0;
962 }
963 return cielabD50;
964}
965
966/** @brief Conversion to CIELCh-D50.
967 *
968 * @param rgbColor The original color.
969 * @returns The corresponding (opaque) CIELCh-D50 color.
970 *
971 * @note By definition, each RGB color in a given color space is an in-gamut
972 * color in this very same color space. Nevertheless, because of rounding
973 * errors, when converting colors that are near to the outer hull of the
974 * gamut/color space, than @ref isCielchD50InGamut() might return <tt>false</tt> for
975 * a return value of <em>this</em> function. */
976PerceptualColor::LchDouble RgbColorSpace::toCielchD50Double(const QRgba64 rgbColor) const
977{
978 constexpr qreal maximum = //
979 std::numeric_limits<decltype(rgbColor.red())>::max();
980 const double my_rgb[]{rgbColor.red() / maximum, //
981 rgbColor.green() / maximum, //
982 rgbColor.blue() / maximum};
983 cmsCIELab cielabD50;
984 cmsDoTransform(d_pointer->m_transformRgbToCielabD50Handle, // handle to transform
985 &my_rgb, // input
986 &cielabD50, // output
987 1 // convert exactly 1 value
988 );
989 if (cielabD50.L < 0) {
990 // Workaround for https://github.com/mm2/Little-CMS/issues/395
991 cielabD50.L = 0;
992 }
993 cmsCIELCh cielchD50;
994 cmsLab2LCh(&cielchD50, // output
995 &cielabD50 // input
996 );
997 return LchDouble{cielchD50.L, cielchD50.C, cielchD50.h};
998}
999
1000/** @brief Conversion to QRgb.
1001 *
1002 * @param lch The original color.
1003 *
1004 * @returns If the original color is in-gamut, the corresponding
1005 * (opaque) in-range RGB value. If the original color is out-of-gamut,
1006 * a more or less similar (opaque) in-range RGB value.
1007 *
1008 * @note There is no guarantee <em>which</em> specific algorithm is used
1009 * to fit out-of-gamut colors into the gamut.
1010 *
1011 * @sa @ref fromCielabD50ToQRgbOrTransparent */
1012QRgb RgbColorSpace::fromCielchD50ToQRgbBound(const LchDouble &lch) const
1013{
1014 const cmsCIELCh myCmsCieLch = toCmsLch(lch);
1015 cmsCIELab lab; // uses cmsFloat64Number internally
1016 cmsLCh2Lab(&lab, // output
1017 &myCmsCieLch // input
1018 );
1019 cmsUInt16Number rgb_int[3];
1020 cmsDoTransform(d_pointer->m_transformCielabD50ToRgb16Handle, // transform
1021 &lab, // input
1022 rgb_int, // output
1023 1 // number of values to convert
1024 );
1025 constexpr qreal channelMaximumQReal = //
1026 std::numeric_limits<cmsUInt16Number>::max();
1027 constexpr quint8 rgbMaximum = 255;
1028 return qRgb(qRound(rgb_int[0] / channelMaximumQReal * rgbMaximum), //
1029 qRound(rgb_int[1] / channelMaximumQReal * rgbMaximum), //
1030 qRound(rgb_int[2] / channelMaximumQReal * rgbMaximum));
1031}
1032
1033/** @brief Check if a color is within the gamut.
1034 * @param lch the color
1035 * @returns <tt>true</tt> if the color is in the gamut.
1036 * <tt>false</tt> otherwise. */
1037bool RgbColorSpace::isCielchD50InGamut(const LchDouble &lch) const
1038{
1039 if (!isInRange<decltype(lch.l)>(0, lch.l, 100)) {
1040 return false;
1041 }
1042 if (!isInRange<decltype(lch.l)>( //
1043 (-1) * d_pointer->m_profileMaximumCielchD50Chroma, //
1044 lch.c, //
1045 d_pointer->m_profileMaximumCielchD50Chroma //
1046 )) {
1047 return false;
1048 }
1049 cmsCIELab lab; // uses cmsFloat64Number internally
1050 const cmsCIELCh myCmsCieLch = toCmsLch(lch);
1051 cmsLCh2Lab(&lab, &myCmsCieLch);
1052 return qAlpha(fromCielabD50ToQRgbOrTransparent(lab)) != 0;
1053}
1054
1055/** @brief Check if a color is within the gamut.
1056 * @param lch the color
1057 * @returns <tt>true</tt> if the color is in the gamut.
1058 * <tt>false</tt> otherwise. */
1059bool RgbColorSpace::isOklchInGamut(const LchDouble &lch) const
1060{
1061 if (!isInRange<decltype(lch.l)>(0, lch.l, 1)) {
1062 return false;
1063 }
1064 if (!isInRange<decltype(lch.l)>( //
1065 (-1) * d_pointer->m_profileMaximumOklchChroma, //
1066 lch.c, //
1067 d_pointer->m_profileMaximumOklchChroma //
1068 )) {
1069 return false;
1070 }
1071 const auto oklab = AbsoluteColor::fromPolarToCartesian(GenericColor(lch));
1072 const auto xyzD65 = AbsoluteColor::fromOklabToXyzD65(oklab);
1073 const auto xyzD50 = AbsoluteColor::fromXyzD65ToXyzD50(xyzD65);
1074 const auto cielabD50 = AbsoluteColor::fromXyzD50ToCielabD50(xyzD50);
1075 const auto cielabD50cms = cielabD50.reinterpretAsLabToCmscielab();
1076 const auto rgb = fromCielabD50ToQRgbOrTransparent(cielabD50cms);
1077 return (qAlpha(rgb) != 0);
1078}
1079
1080/** @brief Check if a color is within the gamut.
1081 * @param lab the color
1082 * @returns <tt>true</tt> if the color is in the gamut.
1083 * <tt>false</tt> otherwise. */
1084bool RgbColorSpace::isCielabD50InGamut(const cmsCIELab &lab) const
1085{
1086 if (!isInRange<decltype(lab.L)>(0, lab.L, 100)) {
1087 return false;
1088 }
1089 const auto chromaSquare = lab.a * lab.a + lab.b * lab.b;
1090 const auto maximumChromaSquare = qPow(d_pointer->m_profileMaximumCielchD50Chroma, 2);
1091 if (chromaSquare > maximumChromaSquare) {
1092 return false;
1093 }
1094 return qAlpha(fromCielabD50ToQRgbOrTransparent(lab)) != 0;
1095}
1096
1097/** @brief Conversion to QRgb.
1098 *
1099 * @pre
1100 * - Input Lightness: 0 ≤ lightness ≤ 100
1101 * @pre
1102 * - Input Chroma: - @ref RgbColorSpace::profileMaximumCielchD50Chroma ≤ chroma ≤
1103 * @ref RgbColorSpace::profileMaximumCielchD50Chroma
1104 *
1105 * @param lab the original color
1106 *
1107 * @returns The corresponding opaque color if the original color is in-gamut.
1108 * A transparent color otherwise.
1109 *
1110 * @sa @ref fromCielchD50ToQRgbBound */
1111QRgb RgbColorSpace::fromCielabD50ToQRgbOrTransparent(const cmsCIELab &lab) const
1112{
1113 constexpr QRgb transparentValue = 0;
1114 static_assert(qAlpha(transparentValue) == 0, //
1115 "The alpha value of a transparent QRgb must be 0.");
1116
1117 double rgb[3];
1118 cmsDoTransform(
1119 // Parameters:
1120 d_pointer->m_transformCielabD50ToRgbHandle, // handle to transform function
1121 &lab, // input
1122 &rgb, // output
1123 1 // convert exactly 1 value
1124 );
1125
1126 // Detect if valid:
1127 const bool colorIsValid = //
1128 isInRange<double>(0, rgb[0], 1) //
1129 && isInRange<double>(0, rgb[1], 1) //
1130 && isInRange<double>(0, rgb[2], 1);
1131 if (!colorIsValid) {
1132 return transparentValue;
1133 }
1134
1135 // Detect deviation:
1136 cmsCIELab roundtripCielabD50;
1137 cmsDoTransform(
1138 // Parameters:
1139 d_pointer->m_transformRgbToCielabD50Handle, // handle to transform function
1140 &rgb, // input
1141 &roundtripCielabD50, // output
1142 1 // convert exactly 1 value
1143 );
1144 const qreal actualDeviationSquare = //
1145 qPow(lab.L - roundtripCielabD50.L, 2) //
1146 + qPow(lab.a - roundtripCielabD50.a, 2) //
1147 + qPow(lab.b - roundtripCielabD50.b, 2);
1148 constexpr auto cielabDeviationLimitSquare = //
1149 RgbColorSpacePrivate::cielabDeviationLimit //
1150 * RgbColorSpacePrivate::cielabDeviationLimit;
1151 const bool actualDeviationIsOkay = //
1152 actualDeviationSquare <= cielabDeviationLimitSquare;
1153
1154 // If deviation is too big, return a transparent color.
1155 if (!actualDeviationIsOkay) {
1156 return transparentValue;
1157 }
1158
1159 // If in-gamut, return an opaque color.
1160 QColor temp = QColor::fromRgbF(static_cast<QColorFloatType>(rgb[0]), //
1161 static_cast<QColorFloatType>(rgb[1]), //
1162 static_cast<QColorFloatType>(rgb[2]));
1163 return temp.rgb();
1164}
1165
1166/** @brief Conversion to RGB.
1167 *
1168 * @param lch The original color.
1169 *
1170 * @returns If the original color is in-gamut, it returns the corresponding
1171 * in-range RGB color. If the original color is out-of-gamut, it returns an
1172 * RGB value which might be in-range or out-of range. The RGB value range
1173 * is [0, 1]. */
1174PerceptualColor::GenericColor RgbColorSpace::fromCielchD50ToRgb1(const PerceptualColor::LchDouble &lch) const
1175{
1176 const cmsCIELCh myCmsCieLch = toCmsLch(lch);
1177 cmsCIELab lab; // uses cmsFloat64Number internally
1178 cmsLCh2Lab(&lab, // output
1179 &myCmsCieLch // input
1180 );
1181 double rgb[3];
1182 cmsDoTransform(
1183 // Parameters:
1184 d_pointer->m_transformCielabD50ToRgbHandle, // handle to transform function
1185 &lab, // input
1186 &rgb, // output
1187 1 // convert exactly 1 value
1188 );
1189 return GenericColor(rgb[0], rgb[1], rgb[2]);
1190}
1191
1192/** @brief Calculation of @ref RgbColorSpace::profileMaximumCielchD50Chroma
1193 *
1194 * @returns Calculation of @ref RgbColorSpace::profileMaximumCielchD50Chroma */
1195double RgbColorSpacePrivate::detectMaximumCielchD50Chroma() const
1196{
1197 // Make sure chromaDetectionHuePrecision is big enough to make a difference
1198 // when being added to floating point variable “hue” used in loop later.
1199 static_assert(0. + chromaDetectionHuePrecision > 0.);
1200 static_assert(360. + chromaDetectionHuePrecision > 360.);
1201
1202 // Implementation
1203 double result = 0;
1204 double hue = 0;
1205 while (hue < 360) {
1206 const auto qColorHue = static_cast<QColorFloatType>(hue / 360.);
1207 const auto color = QColor::fromHsvF(qColorHue, 1, 1).rgba64();
1208 result = qMax(result, q_pointer->toCielchD50Double(color).c);
1209 hue += chromaDetectionHuePrecision;
1210 }
1211 result = result * chromaDetectionIncrementFactor + cielabDeviationLimit;
1212 return std::min<double>(result, CielchD50Values::maximumChroma);
1213}
1214
1215/** @brief Calculation of @ref RgbColorSpace::profileMaximumOklchChroma
1216 *
1217 * @returns Calculation of @ref RgbColorSpace::profileMaximumOklchChroma */
1218double RgbColorSpacePrivate::detectMaximumOklchChroma() const
1219{
1220 // Make sure chromaDetectionHuePrecision is big enough to make a difference
1221 // when being added to floating point variable “hue” used in loop later.
1222 static_assert(0. + chromaDetectionHuePrecision > 0.);
1223 static_assert(360. + chromaDetectionHuePrecision > 360.);
1224
1225 double chromaSquare = 0;
1226 double hue = 0;
1227 while (hue < 360) {
1228 const auto qColorHue = static_cast<QColorFloatType>(hue / 360.);
1229 const auto rgbColor = QColor::fromHsvF(qColorHue, 1, 1).rgba64();
1230 const auto cielabD50Color = q_pointer->toCielabD50(rgbColor);
1231 const auto cielabD50 = GenericColor(cielabD50Color);
1232 const auto xyzD50 = AbsoluteColor::fromCielabD50ToXyzD50(cielabD50);
1233 const auto xyzD65 = AbsoluteColor::fromXyzD50ToXyzD65(xyzD50);
1234 const auto oklab = AbsoluteColor::fromXyzD65ToOklab(xyzD65);
1235 chromaSquare = qMax( //
1236 chromaSquare, //
1237 oklab.second * oklab.second + oklab.third * oklab.third);
1238 hue += chromaDetectionHuePrecision;
1239 }
1240 const auto result = qSqrt(chromaSquare) * chromaDetectionIncrementFactor //
1241 + oklabDeviationLimit;
1242 return std::min<double>(result, OklchValues::maximumChroma);
1243}
1244
1245/** @brief Gets the rendering intents supported by the LittleCMS library.
1246 *
1247 * @returns The rendering intents supported by the LittleCMS library.
1248 *
1249 * @note Do not use this function. Instead, use @ref intentList. */
1250QMap<cmsUInt32Number, QString> RgbColorSpacePrivate::getIntentList()
1251{
1252 // TODO xxx Actually use this (for translation, for example), or remove it…
1254 const cmsUInt32Number intentCount = //
1255 cmsGetSupportedIntents(0, nullptr, nullptr);
1256 cmsUInt32Number *codeArray = new cmsUInt32Number[intentCount];
1257 char **descriptionArray = new char *[intentCount];
1258 cmsGetSupportedIntents(intentCount, codeArray, descriptionArray);
1259 for (cmsUInt32Number i = 0; i < intentCount; ++i) {
1260 result.insert(codeArray[i], QString::fromUtf8(descriptionArray[i]));
1261 }
1262 delete[] codeArray;
1263 delete[] descriptionArray;
1264 return result;
1265}
1266
1267} // namespace PerceptualColor
KGUIADDONS_EXPORT qreal hue(const QColor &)
KCOREADDONS_EXPORT QString versionString()
QStringView countryCode(QStringView coachNumber)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
The namespace of this library.
const char * constData() const const
qsizetype size() const const
QColor fromHsvF(float h, float s, float v, float a)
QColor fromRgbF(float r, float g, float b, float a)
QRgb rgb() const const
QRgba64 rgba64() const const
QCoreApplication * instance()
const_reference at(qsizetype i) const const
qsizetype count() const const
QString name() const const
iterator insert(const Key &key, const T &value)
quint16 blue() const const
quint16 green() const const
quint16 red() const const
QString fromUtf8(QByteArrayView str)
QString fromWCharArray(const wchar_t *string, qsizetype size)
QString number(double n, char format, int precision)
qsizetype size() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
const_pointer constData() const const
qsizetype size() const const
QVersionNumber fromString(QAnyStringView string, qsizetype *suffixIndex)
A LCH color (Oklch, CielchD50, CielchD65…)
Definition lchdouble.h:50
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:20:36 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.