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

KDE's Doxygen guidelines are available online.