Perceptual Color

rgbcolorspace.h
1// SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
2// SPDX-License-Identifier: BSD-2-Clause OR MIT
3
4#ifndef RGBCOLORSPACE_H
5#define RGBCOLORSPACE_H
6
7#include "constpropagatinguniquepointer.h"
8#include "genericcolor.h"
9#include <lcms2.h>
10#include <optional>
11#include <qcontainerfwd.h>
12#include <qdatetime.h>
13#include <qglobal.h>
14#include <qmap.h>
15#include <qmetatype.h>
16#include <qobject.h>
17#include <qrgb.h>
18#include <qsharedpointer.h>
19#include <qstring.h>
20#include <qversionnumber.h>
21class QRgba64;
22
23#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
24#include <qtmetamacros.h>
25#else
26#include <qobjectdefs.h>
27#endif
28
29Q_DECLARE_METATYPE(cmsColorSpaceSignature)
30Q_DECLARE_METATYPE(cmsProfileClassSignature)
31
32namespace PerceptualColor
33{
34class RgbColorSpacePrivate;
35
36/** @internal
37 *
38 * @brief Provides access to LittleCMS color management
39 *
40 * This class has no public constructor. Objects can be generated
41 * with the static factory functions.
42 *
43 * @note The maximum accepted Cielch-D50/Cielab-D50 lightness range is
44 * 0 to 100, and the maximum Cielch-D50 chroma is
45 * @ref CielchD50Values::maximumChroma. Values outside of this
46 * range are considered out-of-gamut, even if the profile
47 * itself would accept them.
48 *
49 * @todo Unit tests for @ref RgbColorSpace, especially the to…() functions.
50 *
51 * @todo Unit tests for @ref profileMaximumCielchD50Chroma and
52 * @ref profileMaximumOklchChroma with all profiles that are available
53 * in the testbed.
54 *
55 * @todo Allow also other perceptual color spaces instead of CIELAB:
56 * This might be Oklab or Google’s HCT, CAM16 or DIN99. Attention:
57 * The range of valid values for Oklab is identical for L
58 * (0–1, or 0%–100%), but different for a and b:
59 * https://www.w3.org/TR/css-color-4/#ok-lab says up to 0.5, but
60 * we would have to actually test this. Therefore, also the
61 * @ref profileMaximumCielchD50Chroma would have to be provided for all
62 * these color spaces individually. Anyway, we could also
63 * output the data in a new data type for cylindrical coordinates
64 * (angle [in degree], radius, z), independent of the color
65 * space, which must always be cylindrical anyway as we have
66 * no support for anything else in our widgets.
67 *
68 *
69 * @todo The sRGB colour space object should be implemented as a singleton.
70 * This is possible because it is thread-safe, and therefore it does
71 * not make sense to have more than one object of this class. At the
72 * same time, it is necessary that it implements the common interface
73 * of the colour space objects that are created on-the-fly from ICC
74 * profile files, therefore it cannot be static.
75 * As a consequence, translations within sRGB objects should always
76 * be dynamic instead of being done only once at instantiation time,
77 * because now the instantiation time is out of control of the library
78 * user. (And maybe even for ICC profiles we could provide ALL
79 * translations, be reading ALL possible translations at creating time
80 * and guarding them? Or would this be overkill?)
81 * The singleton pattern has special requirements
82 * for: 1) thread-safety. 2) dynamic libraries. See Wikipedia
83 * for details!
84 *
85 * @todo Is it possible to split this into an interface and into
86 * various implementations (a slow but safe implementation for
87 * all valid ICC files, and a fast optimized implementation for sRGB
88 * only? If so, is it possible to get rid of the dependency from
89 * LittleCMS by implementing sRGB ourselves, and providing ICC support
90 * via an optional header-only header that would link against LittleCMS
91 * without injecting this dependency into our shared library? And
92 * this might be faster! The web page
93 * https://bottosson.github.io/misc/colorpicker/#91a7ee is only
94 * JavaScript and works faster than this library! See
95 * https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ and
96 * http://www.brucelindbloom.com/index.html?Math.html and
97 * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
98 * for implementation details.
99 *
100 * @todo We return double precision values. But doesn’t use LittleCMS
101 * only 16-bit-integer internally? On the other hand: Using double
102 * precision allows to filter out out-of-range values…
103 *
104 * @todo Do not convert QRgba64 to RgbDouble, but use a transform that
105 * reads QRgba64 directly. While the benefit might not be big in
106 * this function, in general it would be good to review for which data
107 * types we provide transforms and minimize conversions.
108 *
109 * @todo In the API of this class, clarify the precision. If more than
110 * 8 bit per channel, we have to switch from QRgb to QRgb64. But
111 * probably all OS APIs only accept 8 bit anyway? Is it worth the
112 * pain just because @ref ColorDialog can return <tt>QColor</tt>
113 * which provides 16 bit support?
114 *
115 * @todo Find more efficient ways of in-gamut detection. Maybe provide
116 * a subclass with optimized algorithms just for sRGB-build-in? */
117class RgbColorSpace : public QObject
118{
120
121public: // enums and flags
122 /**
123 * @brief Enum class representing the possible roles of an ICC profile in
124 * color management transforms.
125 *
126 * This enum class defines the directions in which a profile can be used
127 * for creating color transforms. Each flag represents a specific role that
128 * the profile can play.
129 *
130 * This enum is declared to the meta-object system with <tt>Q_ENUM</tt>.
131 * This happens automatically. You do not need to make any manual calls.
132 *
133 * This type is declared as type to Qt’s type system via
134 * <tt>Q_DECLARE_METATYPE</tt>. Depending on your use case (for
135 * example if you want to use for <em>queued</em> signal-slot connections),
136 * you might consider calling <tt>qRegisterMetaType()</tt> for
137 * this type, once you have a QApplication object.
138 */
139 enum class ProfileRole {
140 Input = 0x01, /**< The profile can be used as input profile. */
141 Output = 0x02, /**< The profile can be used as input profile. */
142 Proof = 0x04 /**< The profile can be used as input profile. */
143 };
144 Q_ENUM(ProfileRole)
145 /**
146 * @brief <tt>Q_DECLARE_FLAGS</tt> for @ref ProfileRole.
147 */
148 Q_DECLARE_FLAGS(ProfileRoles, ProfileRole)
149 /**
150 * @brief Type for property @ref profileRenderingIntentDirections
151 *
152 * This type is declared as type to Qt’s type system via
153 * <tt>Q_DECLARE_METATYPE</tt>. Depending on your use case (for
154 * example if you want to use for <em>queued</em> signal-slot connections),
155 * you might consider calling <tt>qRegisterMetaType()</tt> for
156 * this type, once you have a QApplication object.
157 */
158 using RenderingIntentDirections = QMap<cmsUInt32Number, RgbColorSpace::ProfileRoles>;
159
160private:
161 /** @brief The absolute file path of the profile.
162 *
163 * @note This is empty for build-in profiles.
164 *
165 * @sa READ @ref profileAbsoluteFilePath() const */
166 Q_PROPERTY(QString profileAbsoluteFilePath READ profileAbsoluteFilePath CONSTANT)
167
168 /** @brief The class of the profile.
169 *
170 * This type is declared as type to Qt’s type system via
171 * <tt>Q_DECLARE_METATYPE</tt>. Depending on your use case (for
172 * example if you want to use for <em>queued</em> signal-slot connections),
173 * you might consider calling <tt>qRegisterMetaType()</tt> for
174 * this type, once you have a QApplication object.
175 *
176 * @sa READ @ref profileClass() const */
177 Q_PROPERTY(cmsProfileClassSignature profileClass READ profileClass CONSTANT)
178
179 /** @brief The color model of the color space which is described by
180 * this profile.
181 *
182 * This type is declared as type to Qt’s type system via
183 * <tt>Q_DECLARE_METATYPE</tt>. Depending on your use case (for
184 * example if you want to use for <em>queued</em> signal-slot connections),
185 * you might consider calling <tt>qRegisterMetaType()</tt> for
186 * this type, once you have a QApplication object.
187 *
188 * @sa READ @ref profileColorModel() const */
189 Q_PROPERTY(cmsColorSpaceSignature profileColorModel READ profileColorModel CONSTANT)
190
191 /** @brief The copyright information of the profile.
192 *
193 * If supported by the underlying profile, this property is localized
194 * to the current locale <em>at the moment of the constructor call</em>.
195 *
196 * @note This is empty if the information is not available.
197 *
198 * @sa READ @ref profileCopyright() const */
199 Q_PROPERTY(QString profileCopyright READ profileCopyright CONSTANT)
200
201 /** @brief The date and time of the creation of the profile.
202 *
203 * @note This is null if the information is not available.
204 *
205 * @sa READ @ref profileCreationDateTime() const */
206 Q_PROPERTY(QDateTime profileCreationDateTime READ profileCreationDateTime CONSTANT)
207
208 /** @brief The file size of the profile, measured in byte.
209 *
210 * @note This is <tt>-1</tt> for build-in profiles.
211 *
212 * @sa READ @ref profileFileSize() const */
213 Q_PROPERTY(qint64 profileFileSize READ profileFileSize CONSTANT)
214
215 /** @brief Wether or not the profile has a color lookup table (CLUT).
216 *
217 * @sa READ @ref profileHasClut() const */
218 Q_PROPERTY(bool profileHasClut READ profileHasClut CONSTANT)
219
220 /** @brief Wether or not the profile has a matrix shaper.
221 *
222 * @sa READ @ref profileHasMatrixShaper() const */
223 Q_PROPERTY(bool profileHasMatrixShaper READ profileHasMatrixShaper CONSTANT)
224
225 /**
226 * @brief Available transform directions of rendering intents.
227 *
228 * A mapping of all rendering intents supported by LittleCMS, indicating
229 * whether they are supported by the given profile and specifying
230 * the direction of support.
231 * - key: The code corresponding to the rendering intent. Refer
232 * to @ref lcmsIntentList() for additional details.
233 * - value: The transformation directions available for each rendering
234 * intent.
235 */
236 Q_PROPERTY(RenderingIntentDirections profileRenderingIntentDirections READ profileRenderingIntentDirections CONSTANT)
237
238 /** @brief The ICC version of the profile.
239 *
240 * @sa READ @ref profileIccVersion() const */
241 Q_PROPERTY(QVersionNumber profileIccVersion READ profileIccVersion CONSTANT)
242
243 /** @brief The manufacturer information of the profile.
244 *
245 * If supported by the underlying profile, this property is localized
246 * to the current locale <em>at the moment of the constructor call</em>.
247 *
248 * @note This is empty if the information is not available.
249 *
250 * @sa READ @ref profileManufacturer() const */
251 Q_PROPERTY(QString profileManufacturer READ profileManufacturer CONSTANT)
252
253 /** @brief The maximum CIELch-D50 chroma of the profile.
254 *
255 * This value is equal or slightly bigger than the actual maximum chroma.
256 *
257 * @note This is the result of an auto-detection, which might theoretically
258 * in very rare cases return a value that is smaller than the actual
259 * maximum chroma.
260 *
261 * @sa READ @ref profileMaximumCielchD50Chroma() const */
262 Q_PROPERTY(double profileMaximumCielchD50Chroma READ profileMaximumCielchD50Chroma CONSTANT)
263
264 /** @brief The maximum Oklch chroma of the profile.
265 *
266 * This value is equal or slightly bigger than the actual maximum chroma.
267 *
268 * @note This is the result of an auto-detection, which might theoretically
269 * in very rare cases return a value that is smaller than the actual
270 * maximum chroma.
271 *
272 * @sa READ @ref profileMaximumOklchChroma() const */
273 Q_PROPERTY(double profileMaximumOklchChroma READ profileMaximumOklchChroma CONSTANT)
274
275 /** @brief The model information of the profile.
276 *
277 * If supported by the underlying profile, this property is localized
278 * to the current locale <em>at the moment of the constructor call</em>.
279 *
280 * @note This is empty if the information is not available.
281 *
282 * @sa READ @ref profileModel() const */
283 Q_PROPERTY(QString profileModel READ profileModel CONSTANT)
284
285 /** @brief The name of the profile.
286 *
287 * If supported by the underlying profile, this property is localized
288 * to the current locale <em>at the moment of the constructor call</em>.
289 *
290 * Note that this string might be very long in some profiles. On some
291 * UI elements, maybe it should be elided (truncate it and put “…” at
292 * the end).
293 *
294 * @note This is empty if the information is not available.
295 *
296 * @sa READ @ref profileName() const */
297 Q_PROPERTY(QString profileName READ profileName CONSTANT)
298
299 /** @brief The name of the profile.
300 *
301 * If supported by the underlying profile, this property is localized
302 * to the current locale <em>at the moment of the constructor call</em>.
303 *
304 * This type is declared as type to Qt’s type system via
305 * <tt>Q_DECLARE_METATYPE</tt>. Depending on your use case (for
306 * example if you want to use for <em>queued</em> signal-slot connections),
307 * you might consider calling <tt>qRegisterMetaType()</tt> for
308 * this type, once you have a QApplication object.
309 *
310 * @sa READ @ref profilePcsColorModel() const */
311 Q_PROPERTY(cmsColorSpaceSignature profilePcsColorModel READ profilePcsColorModel CONSTANT)
312
313 /** @brief Value of the tag <tt>bkpt</tt> if present in the ICC profile,
314 *
315 * Is <tt>std::nullopt</tt> if the tag is not present in the ICC profile.
316 *
317 * @sa READ @ref profileTagBlackpoint() const */
318 Q_PROPERTY(std::optional<cmsCIEXYZ> profileTagBlackpoint READ profileTagBlackpoint CONSTANT)
319
320 /** @brief Value of the tag <tt>bXYZ</tt> if present in the ICC profile,
321 *
322 * Is <tt>std::nullopt</tt> if the tag is not present in the ICC profile.
323 *
324 * @sa READ @ref profileTagBluePrimary() const */
325 Q_PROPERTY(std::optional<cmsCIEXYZ> profileTagBluePrimary READ profileTagBluePrimary CONSTANT)
326
327 /** @brief Value of the tag <tt>gXYZ</tt> if present in the ICC profile,
328 *
329 * Is <tt>std::nullopt</tt> if the tag is not present in the ICC profile.
330 *
331 * @sa READ @ref profileTagGreenPrimary() const */
332 Q_PROPERTY(std::optional<cmsCIEXYZ> profileTagGreenPrimary READ profileTagGreenPrimary CONSTANT)
333
334 /** @brief Value of the tag <tt>rXYZ</tt> if present in the ICC profile,
335 *
336 * Is <tt>std::nullopt</tt> if the tag is not present in the ICC profile.
337 *
338 * @sa READ @ref profileTagRedPrimary() const */
339 Q_PROPERTY(std::optional<cmsCIEXYZ> profileTagRedPrimary READ profileTagRedPrimary CONSTANT)
340
341 /** @brief The signatures of all tags actually present in the ICC profile.
342 *
343 * This contains both, “public tags” mentioned in the
344 * <a href="https://www.color.org/icc_specs2.xalter">ICC specification</a>
345 * itself, and “private tags” which should be registered at the
346 * <a href="https://www.color.org/signatures2.xalter">ICC Signature
347 * Registry</a>.
348 *
349 * @sa READ @ref profileTagSignatures() const */
350 Q_PROPERTY(QStringList profileTagSignatures READ profileTagSignatures CONSTANT)
351
352 /** @brief Value of the tag <tt>wtpt</tt> if present in the ICC profile,
353 *
354 * Is <tt>std::nullopt</tt> if the tag is not present in the ICC profile.
355 *
356 * @sa READ @ref profileTagWhitepoint() const */
357 Q_PROPERTY(std::optional<cmsCIEXYZ> profileTagWhitepoint READ profileTagWhitepoint CONSTANT)
358
359public: // Static factory functions
360 [[nodiscard]] Q_INVOKABLE static QSharedPointer<PerceptualColor::RgbColorSpace> tryCreateFromFile(const QString &fileName);
361 [[nodiscard]] Q_INVOKABLE static QSharedPointer<PerceptualColor::RgbColorSpace> createSrgb();
362
363public:
364 virtual ~RgbColorSpace() noexcept override;
365 [[nodiscard]] Q_INVOKABLE virtual bool isCielabD50InGamut(const cmsCIELab &lab) const;
366 [[nodiscard]] Q_INVOKABLE virtual bool isCielchD50InGamut(const PerceptualColor::GenericColor &lch) const;
367 [[nodiscard]] Q_INVOKABLE virtual bool isOklchInGamut(const PerceptualColor::GenericColor &lch) const;
368 [[nodiscard]] Q_INVOKABLE QColor maxChromaColorByCielchD50Hue360(double oklabHue360) const;
369 [[nodiscard]] Q_INVOKABLE QColor maxChromaColorByOklabHue360(double oklabHue360) const;
370 /** @brief Getter for property @ref profileAbsoluteFilePath
371 * @returns the property @ref profileAbsoluteFilePath */
372 [[nodiscard]] QString profileAbsoluteFilePath() const;
373 /** @brief Getter for property @ref profileClass
374 * @returns the property @ref profileClass */
375 [[nodiscard]] cmsProfileClassSignature profileClass() const;
376 /** @brief Getter for property @ref profileColorModel
377 * @returns the property @ref profileColorModel */
378 [[nodiscard]] cmsColorSpaceSignature profileColorModel() const;
379 /** @brief Getter for property @ref profileCopyright
380 * @returns the property @ref profileCopyright */
381 [[nodiscard]] QString profileCopyright() const;
382 /** @brief Getter for property @ref profileCreationDateTime
383 * @returns the property @ref profileCreationDateTime */
384 [[nodiscard]] QDateTime profileCreationDateTime() const;
385 /** @brief Getter for property @ref profileFileSize
386 * @returns the property @ref profileFileSize */
387 [[nodiscard]] qint64 profileFileSize() const;
388 /** @brief Getter for property @ref profileHasClut
389 * @returns the property @ref profileHasClut */
390 [[nodiscard]] bool profileHasClut() const;
391 /** @brief Getter for property @ref profileHasMatrixShaper
392 * @returns the property @ref profileHasMatrixShaper */
393 [[nodiscard]] bool profileHasMatrixShaper() const;
394 /** @brief Getter for property @ref profileIccVersion
395 * @returns the property @ref profileIccVersion */
396 [[nodiscard]] QVersionNumber profileIccVersion() const;
397 /** @brief Getter for property @ref profileRenderingIntentDirections
398 * @returns the property @ref profileRenderingIntentDirections */
399 [[nodiscard]] RenderingIntentDirections profileRenderingIntentDirections() const;
400 /** @brief Getter for property @ref profileManufacturer
401 * @returns the property @ref profileManufacturer */
402 [[nodiscard]] QString profileManufacturer() const;
403 /** @brief Getter for property @ref profileMaximumCielchD50Chroma
404 * @returns the property @ref profileMaximumCielchD50Chroma */
405 [[nodiscard]] double profileMaximumCielchD50Chroma() const;
406 /** @brief Getter for property @ref profileMaximumOklchChroma
407 * @returns the property @ref profileMaximumOklchChroma */
408 [[nodiscard]] double profileMaximumOklchChroma() const;
409 /** @brief Getter for property @ref profileModel
410 * @returns the property @ref profileModel */
411 [[nodiscard]] QString profileModel() const;
412 /** @brief Getter for property @ref profileName
413 * @returns the property @ref profileName */
414 [[nodiscard]] QString profileName() const;
415 /** @brief Getter for property @ref profilePcsColorModel
416 * @returns the property @ref profilePcsColorModel */
417 [[nodiscard]] cmsColorSpaceSignature profilePcsColorModel() const;
418 /** @brief Getter for property @ref profileTagBlackpoint
419 * @returns the property @ref profileTagBlackpoint */
420 [[nodiscard]] std::optional<cmsCIEXYZ> profileTagBlackpoint() const;
421 /** @brief Getter for property @ref profileTagBluePrimary
422 * @returns the property @ref profileTagBluePrimary */
423 [[nodiscard]] std::optional<cmsCIEXYZ> profileTagBluePrimary() const;
424 /** @brief Getter for property @ref profileTagGreenPrimary
425 * @returns the property @ref profileTagGreenPrimary */
426 [[nodiscard]] std::optional<cmsCIEXYZ> profileTagGreenPrimary() const;
427 /** @brief Getter for property @ref profileTagRedPrimary
428 * @returns the property @ref profileTagRedPrimary */
429 [[nodiscard]] std::optional<cmsCIEXYZ> profileTagRedPrimary() const;
430 /** @brief Getter for property @ref profileTagSignatures
431 * @returns the property @ref profileTagSignatures */
432 [[nodiscard]] QStringList profileTagSignatures() const;
433 /** @brief Getter for property @ref profileTagWhitepoint
434 * @returns the property @ref profileTagWhitepoint */
435 [[nodiscard]] std::optional<cmsCIEXYZ> profileTagWhitepoint() const;
436 // The function declaration is kept on a single line to prevent issues
437 // with Doxygen parsing.
438 // clang-format is disabled here to prevent automatic line breaks.
439 // clang-format off
440 [[nodiscard]] Q_INVOKABLE virtual PerceptualColor::GenericColor reduceCielchD50ChromaToFitIntoGamut(const PerceptualColor::GenericColor &cielchD50color) const;
441 // clang-format on
442 [[nodiscard]] Q_INVOKABLE virtual PerceptualColor::GenericColor reduceOklchChromaToFitIntoGamut(const PerceptualColor::GenericColor &oklchColor) const;
443 [[nodiscard]] Q_INVOKABLE virtual cmsCIELab toCielabD50(const QRgba64 rgbColor) const;
444 [[nodiscard]] Q_INVOKABLE virtual PerceptualColor::GenericColor toCielchD50(const QRgba64 rgbColor) const;
445 [[nodiscard]] Q_INVOKABLE static cmsCIELab fromLchToCmsCIELab(const PerceptualColor::GenericColor &lch);
446 [[nodiscard]] Q_INVOKABLE virtual QRgb fromCielchD50ToQRgbBound(const PerceptualColor::GenericColor &cielchD50) const;
447 [[nodiscard]] Q_INVOKABLE virtual QRgb fromCielabD50ToQRgbOrTransparent(const cmsCIELab &lab) const;
448 [[nodiscard]] Q_INVOKABLE virtual PerceptualColor::GenericColor fromCielchD50ToRgb1(const PerceptualColor::GenericColor &lch) const;
449
450private:
451 Q_DISABLE_COPY(RgbColorSpace)
452
453 /** @internal
454 *
455 * @brief Private constructor.
456 *
457 * @param parent The widget’s parent widget. This parameter will be
458 * passed to the base class’s constructor. */
459 explicit RgbColorSpace(QObject *parent = nullptr);
460
461 /** @internal
462 *
463 * @brief Declare the private implementation as friend class.
464 *
465 * This allows the private class to access the protected members and
466 * functions of instances of <em>this</em> class. */
467 friend class RgbColorSpacePrivate;
468 /** @brief Pointer to implementation (pimpl) */
469 ConstPropagatingUniquePointer<RgbColorSpacePrivate> d_pointer;
470
471 /** @internal @brief Only for unit tests. */
472 friend class TestRgbColorSpace;
473};
474
475} // namespace PerceptualColor
476
477Q_DECLARE_METATYPE(PerceptualColor::RgbColorSpace::ProfileRole)
478Q_DECLARE_METATYPE(PerceptualColor::RgbColorSpace::RenderingIntentDirections)
479
480#endif // RGBCOLORSPACE_H
The namespace of this library.
QObject(QObject *parent)
Q_ENUM(...)
Q_INVOKABLEQ_INVOKABLE
Q_OBJECTQ_OBJECT
Q_PROPERTY(...)
QObject * parent() const const
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.