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

KDE's Doxygen guidelines are available online.