1 | // Copyright (C) 2024 The Qt Company Ltd. |
---|---|
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #ifndef QCOLORMATRIX_H |
5 | #define QCOLORMATRIX_H |
6 | |
7 | // |
8 | // W A R N I N G |
9 | // ------------- |
10 | // |
11 | // This file is not part of the Qt API. It exists purely as an |
12 | // implementation detail. This header file may change from version to |
13 | // version without notice, or even be removed. |
14 | // |
15 | // We mean it. |
16 | // |
17 | |
18 | #include <QtGui/qtguiglobal.h> |
19 | #include <QtCore/qpoint.h> |
20 | #include <QtCore/private/qglobal_p.h> |
21 | #include <QtCore/private/qsimd_p.h> |
22 | #include <cmath> |
23 | |
24 | QT_BEGIN_NAMESPACE |
25 | |
26 | // An abstract 3 value color |
27 | class QColorVector |
28 | { |
29 | public: |
30 | QColorVector() = default; |
31 | constexpr QColorVector(float x, float y, float z, float w = 0.0f) noexcept : x(x), y(y), z(z), w(w) { } |
32 | static constexpr QColorVector fromXYChromaticity(QPointF chr) |
33 | { return {float(chr.x() / chr.y()), 1.0f, float((1.0f - chr.x() - chr.y()) / chr.y())}; } |
34 | float x = 0.0f; // X, x, L, or red/cyan |
35 | float y = 0.0f; // Y, y, a, or green/magenta |
36 | float z = 0.0f; // Z, Y, b, or blue/yellow |
37 | float w = 0.0f; // unused, or black |
38 | |
39 | constexpr bool isNull() const noexcept |
40 | { |
41 | return !x && !y && !z && !w; |
42 | } |
43 | bool isValid() const noexcept |
44 | { |
45 | return std::isfinite(x: x) && std::isfinite(x: y) && std::isfinite(x: z); |
46 | } |
47 | |
48 | static constexpr bool isValidChromaticity(const QPointF &chr) |
49 | { |
50 | if (chr.x() < qreal(0.0) || chr.x() > qreal(1.0)) |
51 | return false; |
52 | if (chr.y() <= qreal(0.0) || chr.y() > qreal(1.0)) |
53 | return false; |
54 | if (chr.x() + chr.y() > qreal(1.0)) |
55 | return false; |
56 | return true; |
57 | } |
58 | |
59 | constexpr QColorVector operator*(float f) const { return QColorVector(x * f, y * f, z * f, w * f); } |
60 | constexpr QColorVector operator+(const QColorVector &v) const { return QColorVector(x + v.x, y + v.y, z + v.z, w + v.w); } |
61 | constexpr QColorVector operator-(const QColorVector &v) const { return QColorVector(x - v.x, y - v.y, z - v.z, w - v.w); } |
62 | void operator+=(const QColorVector &v) { x += v.x; y += v.y; z += v.z; w += v.w; } |
63 | |
64 | QPointF toChromaticity() const |
65 | { |
66 | if (isNull()) |
67 | return QPointF(); |
68 | float mag = 1.0f / (x + y + z); |
69 | return QPointF(x * mag, y * mag); |
70 | } |
71 | |
72 | // Common whitepoints: |
73 | static constexpr QPointF D50Chromaticity() { return QPointF(0.34567, 0.35850); } |
74 | static constexpr QPointF D65Chromaticity() { return QPointF(0.31271, 0.32902); } |
75 | static constexpr QColorVector D50() { return fromXYChromaticity(chr: D50Chromaticity()); } |
76 | static constexpr QColorVector D65() { return fromXYChromaticity(chr: D65Chromaticity()); } |
77 | |
78 | QColorVector xyzToLab() const |
79 | { |
80 | constexpr QColorVector ref = D50(); |
81 | constexpr float eps = 0.008856f; |
82 | constexpr float kap = 903.3f; |
83 | #if defined(__SSE2__) |
84 | const __m128 iref = _mm_setr_ps(z: 1.f / ref.x, y: 1.f / ref.y, x: 1.f / ref.z, w: 0.f); |
85 | __m128 v = _mm_loadu_ps(p: &x); |
86 | v = _mm_mul_ps(a: v, b: iref); |
87 | |
88 | const __m128 f3 = _mm_set1_ps(w: 3.f); |
89 | __m128 est = _mm_add_ps(a: _mm_set1_ps(w: 0.25f), b: _mm_mul_ps(a: v, b: _mm_set1_ps(w: 0.75f))); // float est = 0.25f + (x * 0.75f); |
90 | __m128 estsq = _mm_mul_ps(a: est, b: est); |
91 | est = _mm_sub_ps(a: est, b: _mm_mul_ps(a: _mm_sub_ps(a: _mm_mul_ps(a: estsq, b: est), b: v), |
92 | b: _mm_rcp_ps(a: _mm_mul_ps(a: estsq, b: f3)))); // est -= ((est * est * est) - x) / (3.f * (est * est)); |
93 | estsq = _mm_mul_ps(a: est, b: est); |
94 | est = _mm_sub_ps(a: est, b: _mm_mul_ps(a: _mm_sub_ps(a: _mm_mul_ps(a: estsq, b: est), b: v), |
95 | b: _mm_rcp_ps(a: _mm_mul_ps(a: estsq, b: f3)))); // est -= ((est * est * est) - x) / (3.f * (est * est)); |
96 | estsq = _mm_mul_ps(a: est, b: est); |
97 | est = _mm_sub_ps(a: est, b: _mm_mul_ps(a: _mm_sub_ps(a: _mm_mul_ps(a: estsq, b: est), b: v), |
98 | b: _mm_rcp_ps(a: _mm_mul_ps(a: estsq, b: f3)))); // est -= ((est * est * est) - x) / (3.f * (est * est)); |
99 | estsq = _mm_mul_ps(a: est, b: est); |
100 | est = _mm_sub_ps(a: est, b: _mm_mul_ps(a: _mm_sub_ps(a: _mm_mul_ps(a: estsq, b: est), b: v), |
101 | b: _mm_rcp_ps(a: _mm_mul_ps(a: estsq, b: f3)))); // est -= ((est * est * est) - x) / (3.f * (est * est)); |
102 | |
103 | __m128 kapmul = _mm_mul_ps(a: _mm_add_ps(a: _mm_mul_ps(a: v, b: _mm_set1_ps(w: kap)), b: _mm_set1_ps(w: 16.f)), |
104 | b: _mm_set1_ps(w: 1.f / 116.f)); // f_ = (kap * f_ + 16.f) * (1.f / 116.f); |
105 | __m128 cmpgt = _mm_cmpgt_ps(a: v, b: _mm_set1_ps(w: eps)); // if (f_ > eps) |
106 | #if defined(__SSE4_1__) |
107 | v = _mm_blendv_ps(kapmul, est, cmpgt); // if (..) f_ =.. else f_ =.. |
108 | #else |
109 | v = _mm_or_ps(a: _mm_and_ps(a: cmpgt, b: est), b: _mm_andnot_ps(a: cmpgt, b: kapmul)); |
110 | #endif |
111 | alignas(16) float out[4]; |
112 | _mm_store_ps(p: out, a: v); |
113 | const float L = 116.f * out[1] - 16.f; |
114 | const float a = 500.f * (out[0] - out[1]); |
115 | const float b = 200.f * (out[1] - out[2]); |
116 | #else |
117 | float xr = x * (1.f / ref.x); |
118 | float yr = y * (1.f / ref.y); |
119 | float zr = z * (1.f / ref.z); |
120 | |
121 | float fx, fy, fz; |
122 | if (xr > eps) |
123 | fx = fastCbrt(xr); |
124 | else |
125 | fx = (kap * xr + 16.f) * (1.f / 116.f); |
126 | if (yr > eps) |
127 | fy = fastCbrt(yr); |
128 | else |
129 | fy = (kap * yr + 16.f) * (1.f / 116.f); |
130 | if (zr > eps) |
131 | fz = fastCbrt(zr); |
132 | else |
133 | fz = (kap * zr + 16.f) * (1.f / 116.f); |
134 | |
135 | const float L = 116.f * fy - 16.f; |
136 | const float a = 500.f * (fx - fy); |
137 | const float b = 200.f * (fy - fz); |
138 | #endif |
139 | // We output Lab values that has been scaled to 0.0->1.0 values, see also labToXyz. |
140 | return QColorVector(L * (1.f / 100.f), (a + 128.f) * (1.f / 255.f), (b + 128.f) * (1.f / 255.f)); |
141 | } |
142 | |
143 | QColorVector labToXyz() const |
144 | { |
145 | constexpr QColorVector ref = D50(); |
146 | constexpr float eps = 0.008856f; |
147 | constexpr float kap = 903.3f; |
148 | // This transform has been guessed from the ICC spec, but it is not stated |
149 | // anywhere to be the one to use to map to and from 0.0->1.0 values: |
150 | const float L = x * 100.f; |
151 | const float a = (y * 255.f) - 128.f; |
152 | const float b = (z * 255.f) - 128.f; |
153 | // From here is official Lab->XYZ conversion: |
154 | float fy = (L + 16.f) * (1.f / 116.f); |
155 | float fx = fy + (a * (1.f / 500.f)); |
156 | float fz = fy - (b * (1.f / 200.f)); |
157 | |
158 | float xr, yr, zr; |
159 | if (fx * fx * fx > eps) |
160 | xr = fx * fx * fx; |
161 | else |
162 | xr = (116.f * fx - 16) * (1.f / kap); |
163 | if (L > (kap * eps)) |
164 | yr = fy * fy * fy; |
165 | else |
166 | yr = L * (1.f / kap); |
167 | if (fz * fz * fz > eps) |
168 | zr = fz * fz * fz; |
169 | else |
170 | zr = (116.f * fz - 16) * (1.f / kap); |
171 | |
172 | xr = xr * ref.x; |
173 | yr = yr * ref.y; |
174 | zr = zr * ref.z; |
175 | return QColorVector(xr, yr, zr); |
176 | } |
177 | friend inline bool comparesEqual(const QColorVector &lhs, const QColorVector &rhs) noexcept; |
178 | Q_DECLARE_EQUALITY_COMPARABLE(QColorVector); |
179 | |
180 | private: |
181 | static float fastCbrt(float x) |
182 | { |
183 | // This gives us cube root within the precision we need. |
184 | float est = 0.25f + (x * 0.75f); // guessing a cube-root of numbers between 0.01 and 1. |
185 | est -= ((est * est * est) - x) / (3.f * (est * est)); |
186 | est -= ((est * est * est) - x) / (3.f * (est * est)); |
187 | est -= ((est * est * est) - x) / (3.f * (est * est)); |
188 | est -= ((est * est * est) - x) / (3.f * (est * est)); |
189 | // Q_ASSERT(qAbs(est - std::cbrt(x)) < 0.0001f); |
190 | return est; |
191 | } |
192 | }; |
193 | |
194 | inline bool comparesEqual(const QColorVector &v1, const QColorVector &v2) noexcept |
195 | { |
196 | return (std::abs(x: v1.x - v2.x) < (1.0f / 2048.0f)) |
197 | && (std::abs(x: v1.y - v2.y) < (1.0f / 2048.0f)) |
198 | && (std::abs(x: v1.z - v2.z) < (1.0f / 2048.0f)) |
199 | && (std::abs(x: v1.w - v2.w) < (1.0f / 2048.0f)); |
200 | } |
201 | |
202 | // A matrix mapping 3 value colors. |
203 | // Not using QTransform because only floats are needed and performance is critical. |
204 | class QColorMatrix |
205 | { |
206 | public: |
207 | // We are storing the matrix transposed as that is more convenient: |
208 | QColorVector r; |
209 | QColorVector g; |
210 | QColorVector b; |
211 | |
212 | constexpr bool isNull() const |
213 | { |
214 | return r.isNull() && g.isNull() && b.isNull(); |
215 | } |
216 | constexpr float determinant() const |
217 | { |
218 | return r.x * (b.z * g.y - g.z * b.y) - |
219 | r.y * (b.z * g.x - g.z * b.x) + |
220 | r.z * (b.y * g.x - g.y * b.x); |
221 | } |
222 | bool isValid() const |
223 | { |
224 | // A color matrix must be invertible |
225 | return std::isnormal(x: determinant()); |
226 | } |
227 | bool isIdentity() const noexcept |
228 | { |
229 | return *this == identity(); |
230 | } |
231 | |
232 | QColorMatrix inverted() const |
233 | { |
234 | float det = determinant(); |
235 | det = 1.0f / det; |
236 | QColorMatrix inv; |
237 | inv.r.x = (g.y * b.z - b.y * g.z) * det; |
238 | inv.r.y = (b.y * r.z - r.y * b.z) * det; |
239 | inv.r.z = (r.y * g.z - g.y * r.z) * det; |
240 | inv.g.x = (b.x * g.z - g.x * b.z) * det; |
241 | inv.g.y = (r.x * b.z - b.x * r.z) * det; |
242 | inv.g.z = (g.x * r.z - r.x * g.z) * det; |
243 | inv.b.x = (g.x * b.y - b.x * g.y) * det; |
244 | inv.b.y = (b.x * r.y - r.x * b.y) * det; |
245 | inv.b.z = (r.x * g.y - g.x * r.y) * det; |
246 | return inv; |
247 | } |
248 | friend inline constexpr QColorMatrix operator*(const QColorMatrix &a, const QColorMatrix &o) |
249 | { |
250 | QColorMatrix comb; |
251 | comb.r.x = a.r.x * o.r.x + a.g.x * o.r.y + a.b.x * o.r.z; |
252 | comb.g.x = a.r.x * o.g.x + a.g.x * o.g.y + a.b.x * o.g.z; |
253 | comb.b.x = a.r.x * o.b.x + a.g.x * o.b.y + a.b.x * o.b.z; |
254 | |
255 | comb.r.y = a.r.y * o.r.x + a.g.y * o.r.y + a.b.y * o.r.z; |
256 | comb.g.y = a.r.y * o.g.x + a.g.y * o.g.y + a.b.y * o.g.z; |
257 | comb.b.y = a.r.y * o.b.x + a.g.y * o.b.y + a.b.y * o.b.z; |
258 | |
259 | comb.r.z = a.r.z * o.r.x + a.g.z * o.r.y + a.b.z * o.r.z; |
260 | comb.g.z = a.r.z * o.g.x + a.g.z * o.g.y + a.b.z * o.g.z; |
261 | comb.b.z = a.r.z * o.b.x + a.g.z * o.b.y + a.b.z * o.b.z; |
262 | return comb; |
263 | |
264 | } |
265 | QColorVector map(const QColorVector &c) const |
266 | { |
267 | return QColorVector { c.x * r.x + c.y * g.x + c.z * b.x, |
268 | c.x * r.y + c.y * g.y + c.z * b.y, |
269 | c.x * r.z + c.y * g.z + c.z * b.z }; |
270 | } |
271 | QColorMatrix transposed() const |
272 | { |
273 | return QColorMatrix { .r: { r.x, g.x, b.x }, |
274 | .g: { r.y, g.y, b.y }, |
275 | .b: { r.z, g.z, b.z } }; |
276 | } |
277 | |
278 | static QColorMatrix identity() |
279 | { |
280 | return { .r: { 1.0f, 0.0f, 0.0f }, .g: { 0.0f, 1.0f, 0.0f }, .b: { 0.0f, 0.0f, 1.0f } }; |
281 | } |
282 | static QColorMatrix fromScale(QColorVector v) |
283 | { |
284 | return QColorMatrix { .r: { v.x, 0.0f, 0.0f }, |
285 | .g: { 0.0f, v.y, 0.0f }, |
286 | .b: { 0.0f, 0.0f, v.z } }; |
287 | } |
288 | static QColorMatrix chromaticAdaptation(const QColorVector &whitePoint) |
289 | { |
290 | constexpr QColorVector whitePointD50 = QColorVector::D50(); |
291 | if (whitePoint != whitePointD50) { |
292 | // A chromatic adaptation to map a white point to XYZ D50. |
293 | |
294 | // The Bradford method chromatic adaptation matrix: |
295 | const QColorMatrix abrad = { .r: { 0.8951f, -0.7502f, 0.0389f }, |
296 | .g: { 0.2664f, 1.7135f, -0.0685f }, |
297 | .b: { -0.1614f, 0.0367f, 1.0296f } }; |
298 | const QColorMatrix abradinv = { .r: { 0.9869929f, 0.4323053f, -0.0085287f }, |
299 | .g: { -0.1470543f, 0.5183603f, 0.0400428f }, |
300 | .b: { 0.1599627f, 0.0492912f, 0.9684867f } }; |
301 | |
302 | const QColorVector srcCone = abrad.map(c: whitePoint); |
303 | if (srcCone.x && srcCone.y && srcCone.z) { |
304 | const QColorVector dstCone = abrad.map(c: whitePointD50); |
305 | const QColorMatrix wToD50 = { .r: { dstCone.x / srcCone.x, 0, 0 }, |
306 | .g: { 0, dstCone.y / srcCone.y, 0 }, |
307 | .b: { 0, 0, dstCone.z / srcCone.z } }; |
308 | return abradinv * (wToD50 * abrad); |
309 | } |
310 | } |
311 | return QColorMatrix::identity(); |
312 | } |
313 | |
314 | // These are used to recognize matrices from ICC profiles: |
315 | static QColorMatrix toXyzFromSRgb() |
316 | { |
317 | return QColorMatrix { .r: { 0.4360217452f, 0.2224751115f, 0.0139281144f }, |
318 | .g: { 0.3851087987f, 0.7169067264f, 0.0971015394f }, |
319 | .b: { 0.1430812478f, 0.0606181994f, 0.7141585946f } }; |
320 | } |
321 | static QColorMatrix toXyzFromAdobeRgb() |
322 | { |
323 | return QColorMatrix { .r: { 0.6097189188f, 0.3111021519f, 0.0194766335f }, |
324 | .g: { 0.2052682191f, 0.6256770492f, 0.0608891509f }, |
325 | .b: { 0.1492247432f, 0.0632209629f, 0.7448224425f } }; |
326 | } |
327 | static QColorMatrix toXyzFromDciP3D65() |
328 | { |
329 | return QColorMatrix { .r: { 0.5150973201f, 0.2411795557f, -0.0010491034f }, |
330 | .g: { 0.2919696569f, 0.6922441125f, 0.0418830328f }, |
331 | .b: { 0.1571449190f, 0.0665764511f, 0.7843542695f } }; |
332 | } |
333 | static QColorMatrix toXyzFromProPhotoRgb() |
334 | { |
335 | return QColorMatrix { .r: { 0.7976672649f, 0.2880374491f, 0.0000000000f }, |
336 | .g: { 0.1351922452f, 0.7118769884f, 0.0000000000f }, |
337 | .b: { 0.0313525312f, 0.0000856627f, 0.8251883388f } }; |
338 | } |
339 | static QColorMatrix toXyzFromBt2020() |
340 | { |
341 | return QColorMatrix { .r: { 0.673447f, 0.279037f, -0.00192261f }, |
342 | .g: { 0.165665f, 0.675339f, 0.0299835f }, |
343 | .b: { 0.125092f, 0.0456238f, 0.797134f } }; |
344 | } |
345 | friend inline bool comparesEqual(const QColorMatrix &lhs, const QColorMatrix &rhs) noexcept; |
346 | Q_DECLARE_EQUALITY_COMPARABLE(QColorMatrix); |
347 | }; |
348 | |
349 | inline bool comparesEqual(const QColorMatrix &m1, const QColorMatrix &m2) noexcept |
350 | { |
351 | return (m1.r == m2.r) && (m1.g == m2.g) && (m1.b == m2.b); |
352 | } |
353 | |
354 | QT_END_NAMESPACE |
355 | |
356 | #endif // QCOLORMATRIX_P_H |
357 |
Definitions
- QColorVector
- QColorVector
- QColorVector
- fromXYChromaticity
- isNull
- isValid
- isValidChromaticity
- operator*
- operator+
- operator-
- operator+=
- toChromaticity
- D50Chromaticity
- D65Chromaticity
- D50
- D65
- xyzToLab
- labToXyz
- fastCbrt
- comparesEqual
- QColorMatrix
- isNull
- determinant
- isValid
- isIdentity
- inverted
- operator*
- map
- transposed
- identity
- fromScale
- chromaticAdaptation
- toXyzFromSRgb
- toXyzFromAdobeRgb
- toXyzFromDciP3D65
- toXyzFromProPhotoRgb
- toXyzFromBt2020
Learn to use CMake with our Intro Training
Find out more