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

KDE's Doxygen guidelines are available online.