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#include "qicc_p.h"
5
6#include <qbuffer.h>
7#include <qbytearray.h>
8#include <qvarlengtharray.h>
9#include <qhash.h>
10#include <qdatastream.h>
11#include <qendian.h>
12#include <qloggingcategory.h>
13#include <qstring.h>
14
15#include "qcolorclut_p.h"
16#include "qcolormatrix_p.h"
17#include "qcolorspace_p.h"
18#include "qcolortrc_p.h"
19
20#include <array>
21
22QT_BEGIN_NAMESPACE
23Q_LOGGING_CATEGORY(lcIcc, "qt.gui.icc", QtWarningMsg)
24
25namespace QIcc {
26
27struct ICCProfileHeader
28{
29 quint32_be profileSize;
30
31 quint32_be preferredCmmType;
32
33 quint32_be profileVersion;
34 quint32_be profileClass;
35 quint32_be inputColorSpace;
36 quint32_be pcs;
37 quint32_be datetime[3];
38 quint32_be signature;
39 quint32_be platformSignature;
40 quint32_be flags;
41 quint32_be deviceManufacturer;
42 quint32_be deviceModel;
43 quint32_be deviceAttributes[2];
44
45 quint32_be renderingIntent;
46 qint32_be illuminantXyz[3];
47
48 quint32_be creatorSignature;
49 quint32_be profileId[4];
50
51 quint32_be reserved[7];
52
53// Technically after the header, but easier to include here:
54 quint32_be tagCount;
55};
56
57constexpr quint32 IccTag(uchar a, uchar b, uchar c, uchar d)
58{
59 return (a << 24) | (b << 16) | (c << 8) | d;
60}
61
62enum class ColorSpaceType : quint32 {
63 Rgb = IccTag(a: 'R', b: 'G', c: 'B', d: ' '),
64 Gray = IccTag(a: 'G', b: 'R', c: 'A', d: 'Y'),
65 Cmyk = IccTag(a: 'C', b: 'M', c: 'Y', d: 'K'),
66};
67
68enum class ProfileClass : quint32 {
69 Input = IccTag(a: 's', b: 'c', c: 'n', d: 'r'),
70 Display = IccTag(a: 'm', b: 'n', c: 't', d: 'r'),
71 Output = IccTag(a: 'p', b: 'r', c: 't', d: 'r'),
72 ColorSpace = IccTag(a: 's', b: 'p', c: 'a', d: 'c'),
73 // Not supported:
74 DeviceLink = IccTag(a: 'l', b: 'i', c: 'n', d: 'k'),
75 Abstract = IccTag(a: 'a', b: 'b', c: 's', d: 't'),
76 NamedColor = IccTag(a: 'n', b: 'm', c: 'c', d: 'l'),
77};
78
79enum class Tag : quint32 {
80 acsp = IccTag(a: 'a', b: 'c', c: 's', d: 'p'),
81 Lab_ = IccTag(a: 'L', b: 'a', c: 'b', d: ' '),
82 RGB_ = IccTag(a: 'R', b: 'G', c: 'B', d: ' '),
83 XYZ_ = IccTag(a: 'X', b: 'Y', c: 'Z', d: ' '),
84 rXYZ = IccTag(a: 'r', b: 'X', c: 'Y', d: 'Z'),
85 gXYZ = IccTag(a: 'g', b: 'X', c: 'Y', d: 'Z'),
86 bXYZ = IccTag(a: 'b', b: 'X', c: 'Y', d: 'Z'),
87 rTRC = IccTag(a: 'r', b: 'T', c: 'R', d: 'C'),
88 gTRC = IccTag(a: 'g', b: 'T', c: 'R', d: 'C'),
89 bTRC = IccTag(a: 'b', b: 'T', c: 'R', d: 'C'),
90 kTRC = IccTag(a: 'k', b: 'T', c: 'R', d: 'C'),
91 A2B0 = IccTag(a: 'A', b: '2', c: 'B', d: '0'),
92 A2B1 = IccTag(a: 'A', b: '2', c: 'B', d: '1'),
93 A2B2 = IccTag(a: 'A', b: '2', c: 'B', d: '2'),
94 B2A0 = IccTag(a: 'B', b: '2', c: 'A', d: '0'),
95 B2A1 = IccTag(a: 'B', b: '2', c: 'A', d: '1'),
96 B2A2 = IccTag(a: 'B', b: '2', c: 'A', d: '2'),
97 B2D0 = IccTag(a: 'B', b: '2', c: 'D', d: '0'),
98 B2D1 = IccTag(a: 'B', b: '2', c: 'D', d: '1'),
99 B2D2 = IccTag(a: 'B', b: '2', c: 'D', d: '2'),
100 B2D3 = IccTag(a: 'B', b: '2', c: 'D', d: '3'),
101 D2B0 = IccTag(a: 'D', b: '2', c: 'B', d: '0'),
102 D2B1 = IccTag(a: 'D', b: '2', c: 'B', d: '1'),
103 D2B2 = IccTag(a: 'D', b: '2', c: 'B', d: '2'),
104 D2B3 = IccTag(a: 'D', b: '2', c: 'B', d: '3'),
105 desc = IccTag(a: 'd', b: 'e', c: 's', d: 'c'),
106 text = IccTag(a: 't', b: 'e', c: 'x', d: 't'),
107 cprt = IccTag(a: 'c', b: 'p', c: 'r', d: 't'),
108 curv = IccTag(a: 'c', b: 'u', c: 'r', d: 'v'),
109 para = IccTag(a: 'p', b: 'a', c: 'r', d: 'a'),
110 wtpt = IccTag(a: 'w', b: 't', c: 'p', d: 't'),
111 bkpt = IccTag(a: 'b', b: 'k', c: 'p', d: 't'),
112 mft1 = IccTag(a: 'm', b: 'f', c: 't', d: '1'),
113 mft2 = IccTag(a: 'm', b: 'f', c: 't', d: '2'),
114 mluc = IccTag(a: 'm', b: 'l', c: 'u', d: 'c'),
115 mpet = IccTag(a: 'm', b: 'p', c: 'e', d: 't'),
116 mAB_ = IccTag(a: 'm', b: 'A', c: 'B', d: ' '),
117 mBA_ = IccTag(a: 'm', b: 'B', c: 'A', d: ' '),
118 chad = IccTag(a: 'c', b: 'h', c: 'a', d: 'd'),
119 cicp = IccTag(a: 'c', b: 'i', c: 'c', d: 'p'),
120 gamt = IccTag(a: 'g', b: 'a', c: 'm', d: 't'),
121 sf32 = IccTag(a: 's', b: 'f', c: '3', d: '2'),
122
123 // Apple extensions for ICCv2:
124 aarg = IccTag(a: 'a', b: 'a', c: 'r', d: 'g'),
125 aagg = IccTag(a: 'a', b: 'a', c: 'g', d: 'g'),
126 aabg = IccTag(a: 'a', b: 'a', c: 'b', d: 'g'),
127};
128
129} // namespace QIcc
130
131namespace QIcc {
132
133struct TagTableEntry
134{
135 quint32_be signature;
136 quint32_be offset;
137 quint32_be size;
138};
139
140struct GenericTagData {
141 quint32_be type;
142 quint32_be null;
143};
144
145struct XYZTagData : GenericTagData {
146 qint32_be fixedX;
147 qint32_be fixedY;
148 qint32_be fixedZ;
149};
150
151struct CurvTagData : GenericTagData {
152 quint32_be valueCount;
153 // followed by curv values: quint16_be[]
154};
155
156struct ParaTagData : GenericTagData {
157 quint16_be curveType;
158 quint16_be null2;
159 // followed by parameter values: quint32_be[1-7];
160};
161
162struct DescTagData : GenericTagData {
163 quint32_be asciiDescriptionLength;
164 // followed by ascii description: char[]
165 // .. we ignore the rest
166};
167
168struct MlucTagRecord {
169 quint16_be languageCode;
170 quint16_be countryCode;
171 quint32_be size;
172 quint32_be offset;
173};
174
175struct MlucTagData : GenericTagData {
176 quint32_be recordCount;
177 quint32_be recordSize; // = sizeof(MlucTagRecord)
178 MlucTagRecord records[1];
179};
180
181struct Lut8TagData : GenericTagData {
182 quint8 inputChannels;
183 quint8 outputChannels;
184 quint8 clutGridPoints;
185 quint8 padding;
186 qint32_be e1;
187 qint32_be e2;
188 qint32_be e3;
189 qint32_be e4;
190 qint32_be e5;
191 qint32_be e6;
192 qint32_be e7;
193 qint32_be e8;
194 qint32_be e9;
195 // followed by parameter values: quint8[inputChannels * 256];
196 // followed by parameter values: quint8[outputChannels * clutGridPoints^inputChannels];
197 // followed by parameter values: quint8[outputChannels * 256];
198};
199
200struct Lut16TagData : GenericTagData {
201 quint8 inputChannels;
202 quint8 outputChannels;
203 quint8 clutGridPoints;
204 quint8 padding;
205 qint32_be e1;
206 qint32_be e2;
207 qint32_be e3;
208 qint32_be e4;
209 qint32_be e5;
210 qint32_be e6;
211 qint32_be e7;
212 qint32_be e8;
213 qint32_be e9;
214 quint16_be inputTableEntries;
215 quint16_be outputTableEntries;
216 // followed by parameter values: quint16_be[inputChannels * inputTableEntries];
217 // followed by parameter values: quint16_be[outputChannels * clutGridPoints^inputChannels];
218 // followed by parameter values: quint16_be[outputChannels * outputTableEntries];
219};
220
221// For both mAB and mBA
222struct mABTagData : GenericTagData {
223 quint8 inputChannels;
224 quint8 outputChannels;
225 quint8 padding[2];
226 quint32_be bCurvesOffset;
227 quint32_be matrixOffset;
228 quint32_be mCurvesOffset;
229 quint32_be clutOffset;
230 quint32_be aCurvesOffset;
231 // followed by embedded data for the offsets above
232};
233
234struct mpetTagData : GenericTagData {
235 quint16_be inputChannels;
236 quint16_be outputChannels;
237 quint32_be processingElements;
238 // element offset table
239 // element data
240};
241
242struct Sf32TagData : GenericTagData {
243 quint32_be value[9];
244};
245
246struct CicpTagData : GenericTagData {
247 quint8 colorPrimaries;
248 quint8 transferCharacteristics;
249 quint8 matrixCoefficients;
250 quint8 videoFullRangeFlag;
251};
252
253struct MatrixElement {
254 qint32_be e0;
255 qint32_be e1;
256 qint32_be e2;
257 qint32_be e3;
258 qint32_be e4;
259 qint32_be e5;
260 qint32_be e6;
261 qint32_be e7;
262 qint32_be e8;
263 qint32_be e9;
264 qint32_be e10;
265 qint32_be e11;
266};
267
268static int toFixedS1516(float x)
269{
270 if (x < float(SHRT_MIN))
271 return INT_MIN;
272 if (x > float(SHRT_MAX))
273 return INT_MAX;
274 return qRound(f: x * 65536.0f);
275}
276
277static float fromFixedS1516(int x)
278{
279 return x * (1.0f / 65536.0f);
280}
281
282static bool isValidIccProfile(const ICCProfileHeader &header)
283{
284 if (header.signature != uint(Tag::acsp)) {
285 qCWarning(lcIcc, "Failed ICC signature test");
286 return false;
287 }
288
289 // Don't overflow 32bit integers:
290 if (header.tagCount >= (INT32_MAX - sizeof(ICCProfileHeader)) / sizeof(TagTableEntry)) {
291 qCWarning(lcIcc, "Failed tag count sanity");
292 return false;
293 }
294 if (header.profileSize - sizeof(ICCProfileHeader) < header.tagCount * sizeof(TagTableEntry)) {
295 qCWarning(lcIcc, "Failed basic size sanity");
296 return false;
297 }
298
299 if (header.profileClass != uint(ProfileClass::Input)
300 && header.profileClass != uint(ProfileClass::Display)
301 && header.profileClass != uint(ProfileClass::Output)
302 && header.profileClass != uint(ProfileClass::ColorSpace)) {
303 qCInfo(lcIcc, "Unsupported ICC profile class 0x%x", quint32(header.profileClass));
304 return false;
305 }
306 if (header.inputColorSpace != uint(ColorSpaceType::Rgb)
307 && header.inputColorSpace != uint(ColorSpaceType::Gray)
308 && header.inputColorSpace != uint(ColorSpaceType::Cmyk)) {
309 qCInfo(lcIcc, "Unsupported ICC input color space 0x%x", quint32(header.inputColorSpace));
310 return false;
311 }
312 if (header.pcs != uint(Tag::XYZ_) && header.pcs != uint(Tag::Lab_)) {
313 qCInfo(lcIcc, "Invalid ICC profile connection space 0x%x", quint32(header.pcs));
314 return false;
315 }
316
317 QColorVector illuminant;
318 illuminant.x = fromFixedS1516(x: header.illuminantXyz[0]);
319 illuminant.y = fromFixedS1516(x: header.illuminantXyz[1]);
320 illuminant.z = fromFixedS1516(x: header.illuminantXyz[2]);
321 if (illuminant != QColorVector::D50()) {
322 qCWarning(lcIcc, "Invalid ICC illuminant");
323 return false;
324 }
325
326 return true;
327}
328
329static int writeColorTrc(QDataStream &stream, const QColorTrc &trc)
330{
331 if (trc.isIdentity()) {
332 stream << uint(Tag::curv) << uint(0);
333 stream << uint(0);
334 return 12;
335 }
336
337 if (trc.m_type == QColorTrc::Type::ParameterizedFunction) {
338 const QColorTransferFunction &fun = trc.m_fun;
339 stream << uint(Tag::para) << uint(0);
340 if (fun.isGamma()) {
341 stream << ushort(0) << ushort(0);
342 stream << toFixedS1516(x: fun.m_g);
343 return 12 + 4;
344 }
345 bool type3 = qFuzzyIsNull(f: fun.m_e) && qFuzzyIsNull(f: fun.m_f);
346 stream << ushort(type3 ? 3 : 4) << ushort(0);
347 stream << toFixedS1516(x: fun.m_g);
348 stream << toFixedS1516(x: fun.m_a);
349 stream << toFixedS1516(x: fun.m_b);
350 stream << toFixedS1516(x: fun.m_c);
351 stream << toFixedS1516(x: fun.m_d);
352 if (type3)
353 return 12 + 5 * 4;
354 stream << toFixedS1516(x: fun.m_e);
355 stream << toFixedS1516(x: fun.m_f);
356 return 12 + 7 * 4;
357 }
358 if (trc.m_type != QColorTrc::Type::Table) {
359 stream << uint(Tag::curv) << uint(0);
360 stream << uint(16);
361 for (uint i = 0; i < 16; ++i) {
362 stream << ushort(qBound(min: 0, val: qRound(f: trc.apply(x: i / 15.f) * 65535.f), max: 65535));
363 }
364 return 12 + 16 * 2;
365 }
366
367 Q_ASSERT(trc.m_type == QColorTrc::Type::Table);
368 stream << uint(Tag::curv) << uint(0);
369 stream << uint(trc.m_table.m_tableSize);
370 if (!trc.m_table.m_table16.isEmpty()) {
371 for (uint i = 0; i < trc.m_table.m_tableSize; ++i) {
372 stream << ushort(trc.m_table.m_table16[i]);
373 }
374 } else {
375 for (uint i = 0; i < trc.m_table.m_tableSize; ++i) {
376 stream << ushort(trc.m_table.m_table8[i] * 257U);
377 }
378 }
379 if (trc.m_table.m_tableSize & 1) {
380 stream << ushort(0);
381 return 12 + 2 * trc.m_table.m_tableSize + 2;
382 }
383 return 12 + 2 * trc.m_table.m_tableSize;
384}
385
386// very simple version for small values (<=4) of exp.
387static constexpr qsizetype intPow(qsizetype x, qsizetype exp)
388{
389 return (exp <= 1) ? x : x * intPow(x, exp: exp - 1);
390}
391
392struct ElementCombo
393{
394 const QColorMatrix *inMatrix = nullptr;
395 const QColorSpacePrivate::TransferElement *inTable = nullptr;
396 const QColorCLUT *clut = nullptr;
397 const QColorSpacePrivate::TransferElement *midTable = nullptr;
398 const QColorMatrix *midMatrix = nullptr;
399 const QColorVector *midOffset = nullptr;
400 const QColorSpacePrivate::TransferElement *outTable = nullptr;
401};
402
403static void visitElement(ElementCombo &combo, const QColorSpacePrivate::TransferElement &element, int number,
404 const QList<QColorSpacePrivate::Element> &list)
405{
406 if (number == 0)
407 combo.inTable = &element;
408 else if (number == list.size() - 1)
409 combo.outTable = &element;
410 else if (number == 1 && combo.inMatrix)
411 combo.inTable = &element;
412 else
413 combo.midTable = &element;
414}
415
416static void visitElement(ElementCombo &combo, const QColorMatrix &element, int number,
417 const QList<QColorSpacePrivate::Element> &)
418{
419 if (number == 0)
420 combo.inMatrix = &element;
421 else
422 combo.midMatrix = &element;
423}
424
425static void visitElement(ElementCombo &combo, const QColorVector &element, int,
426 const QList<QColorSpacePrivate::Element> &)
427{
428 combo.midOffset = &element;
429}
430
431static void visitElement(ElementCombo &combo, const QColorCLUT &element, int,
432 const QList<QColorSpacePrivate::Element> &)
433{
434 combo.clut = &element;
435}
436
437static bool isTableTrc(const QColorSpacePrivate::TransferElement *transfer)
438{
439 int i = 0;
440 while (i < 4 && transfer->trc[i].isValid()) {
441 if (transfer->trc[i].m_type != QColorTrc::Type::Table)
442 return false;
443 i++;
444 }
445 return i > 0;
446}
447
448static bool isTableTrcSingleSize(const QColorSpacePrivate::TransferElement *transfer)
449{
450 Q_ASSERT(transfer->trc[0].m_type == QColorTrc::Type::Table);
451 int i = 1;
452 const uint32_t size = transfer->trc[0].table().m_tableSize;
453 while (i < 4 && transfer->trc[i].isValid()) {
454 if (transfer->trc[i].table().m_tableSize != size)
455 return false;
456 i++;
457 }
458 return true;
459}
460
461static int writeMab(QDataStream &stream, const QList<QColorSpacePrivate::Element> &abList, bool isAb, bool pcsLab, bool isCmyk)
462{
463 int number = 0;
464 ElementCombo combo;
465 for (auto &&element : abList)
466 std::visit(visitor: [&](auto &&elm) { visitElement(combo, elm, number++, abList); }, variants: element);
467
468 Q_ASSERT(!(combo.inMatrix && combo.midMatrix));
469
470 // qWarning() << Q_FUNC_INFO << bool(combo.inMatrix) << bool(combo.inTable) << bool(combo.clut) << bool(combo.midTable) << bool(combo.midMatrix) << bool(combo.midOffset) << bool(combo.outTable);
471 bool lut16 = true;
472 if (combo.midMatrix || combo.midTable || combo.midOffset)
473 lut16 = false;
474 if (combo.clut && (combo.clut->gridPointsX != combo.clut->gridPointsY ||
475 combo.clut->gridPointsX != combo.clut->gridPointsZ ||
476 (combo.clut->gridPointsW > 1 && combo.clut->gridPointsX != combo.clut->gridPointsW)))
477 lut16 = false;
478 if (lut16 && combo.inTable)
479 lut16 = isTableTrc(transfer: combo.inTable) && isTableTrcSingleSize(transfer: combo.inTable);
480 if (lut16 && combo.outTable)
481 lut16 = isTableTrc(transfer: combo.outTable) && isTableTrcSingleSize(transfer: combo.outTable);
482
483 if (!lut16) {
484 if (combo.inMatrix)
485 qSwap(value1&: combo.inMatrix, value2&: combo.midMatrix);
486 if (isAb)
487 stream << uint(Tag::mAB_) << uint(0);
488 else
489 stream << uint(Tag::mBA_) << uint(0);
490 } else {
491 stream << uint(Tag::mft2) << uint(0);
492 }
493
494 const int inChannels = (isCmyk && isAb) ? 4 : 3;
495 const int outChannels = (isCmyk && !isAb) ? 4 : 3;
496 stream << uchar(inChannels) << uchar(outChannels);
497 qsizetype gridPointsLut16 = 0;
498 if (lut16 && combo.clut)
499 gridPointsLut16 = combo.clut->gridPointsX;
500 if (lut16)
501 stream << uchar(gridPointsLut16) << uchar(0);
502 else
503 stream << quint16(0);
504 if (lut16) {
505 if (combo.inMatrix) {
506 stream << toFixedS1516(x: combo.inMatrix->r.x);
507 stream << toFixedS1516(x: combo.inMatrix->g.x);
508 stream << toFixedS1516(x: combo.inMatrix->b.x);
509 stream << toFixedS1516(x: combo.inMatrix->r.y);
510 stream << toFixedS1516(x: combo.inMatrix->g.y);
511 stream << toFixedS1516(x: combo.inMatrix->b.y);
512 stream << toFixedS1516(x: combo.inMatrix->r.z);
513 stream << toFixedS1516(x: combo.inMatrix->g.z);
514 stream << toFixedS1516(x: combo.inMatrix->b.z);
515 } else {
516 stream << toFixedS1516(x: 1.0f);
517 stream << toFixedS1516(x: 0.0f);
518 stream << toFixedS1516(x: 0.0f);
519 stream << toFixedS1516(x: 0.0f);
520 stream << toFixedS1516(x: 1.0f);
521 stream << toFixedS1516(x: 0.0f);
522 stream << toFixedS1516(x: 0.0f);
523 stream << toFixedS1516(x: 0.0f);
524 stream << toFixedS1516(x: 1.0f);
525 }
526 int inputEntries = 0, outputEntries = 0;
527 if (combo.inTable)
528 inputEntries = combo.inTable->trc[0].table().m_tableSize;
529 else
530 inputEntries = 2;
531 if (combo.outTable)
532 outputEntries = combo.outTable->trc[0].table().m_tableSize;
533 else
534 outputEntries = 2;
535 stream << quint16(inputEntries);
536 stream << quint16(outputEntries);
537 auto writeTable = [&](const QColorSpacePrivate::TransferElement *table, int entries, int channels) {
538 if (table) {
539 for (int j = 0; j < channels; ++j) {
540 if (!table->trc[j].table().m_table16.isEmpty()) {
541 for (int i = 0; i < entries; ++i)
542 stream << table->trc[j].table().m_table16[i];
543 } else {
544 for (int i = 0; i < entries; ++i)
545 stream << quint16(table->trc[j].table().m_table8[i] * 257);
546 }
547 }
548 } else {
549 for (int j = 0; j < channels; ++j)
550 stream << quint16(0) << quint16(65535);
551 }
552 };
553
554 writeTable(combo.inTable, inputEntries, inChannels);
555
556 if (combo.clut) {
557 if (isAb && pcsLab) {
558 for (const QColorVector &v : combo.clut->table) {
559 stream << quint16(v.x * 65280.0f + 0.5f);
560 stream << quint16(v.y * 65280.0f + 0.5f);
561 stream << quint16(v.z * 65280.0f + 0.5f);
562 }
563 } else {
564 if (outChannels == 4) {
565 for (const QColorVector &v : combo.clut->table) {
566 stream << quint16(v.x * 65535.0f + 0.5f);
567 stream << quint16(v.y * 65535.0f + 0.5f);
568 stream << quint16(v.z * 65535.0f + 0.5f);
569 stream << quint16(v.w * 65535.0f + 0.5f);
570 }
571 } else {
572 for (const QColorVector &v : combo.clut->table) {
573 stream << quint16(v.x * 65535.0f + 0.5f);
574 stream << quint16(v.y * 65535.0f + 0.5f);
575 stream << quint16(v.z * 65535.0f + 0.5f);
576 }
577 }
578 }
579 }
580
581 writeTable(combo.outTable, outputEntries, outChannels);
582
583 qsizetype offset = sizeof(Lut16TagData) + 2 * inChannels * inputEntries
584 + 2 * outChannels * outputEntries
585 + 2 * outChannels * intPow(x: gridPointsLut16, exp: inChannels);
586 if (offset & 0x2) {
587 stream << quint16(0);
588 offset += 2;
589 }
590 return offset;
591 } else {
592 // mAB/mBA tag:
593 if (isAb) {
594 if (!combo.clut && combo.inTable && combo.midMatrix && !combo.midTable)
595 std::swap(a&: combo.inTable, b&: combo.midTable);
596 } else {
597 if (!combo.clut && combo.outTable && combo.midMatrix && !combo.midTable)
598 std::swap(a&: combo.outTable, b&: combo.midTable);
599 }
600 quint32 offset = sizeof(mABTagData);
601 QBuffer buffer2;
602 buffer2.open(openMode: QIODevice::WriteOnly);
603 QDataStream stream2(&buffer2);
604 quint32 bOffset = offset;
605 quint32 matrixOffset = 0;
606 quint32 mOffset = 0;
607 quint32 clutOffset = 0;
608 quint32 aOffset = 0;
609 // Tags must start on 4 byte offsets, but sampled curves might have sizes 2-byte aligned
610 auto alignTag = [&]() {
611 if (offset & 0x2) {
612 stream2 << quint16(0);
613 offset += 2;
614 }
615 };
616
617 const QColorSpacePrivate::TransferElement *aCurve, *bCurve;
618 int aChannels;
619 if (isAb) {
620 aCurve = combo.inTable;
621 aChannels = inChannels;
622 bCurve = combo.outTable;
623 Q_ASSERT(outChannels == 3);
624 } else {
625 aCurve = combo.outTable;
626 aChannels = outChannels;
627 bCurve = combo.inTable;
628 Q_ASSERT(inChannels == 3);
629 }
630 if (bCurve) {
631 offset += writeColorTrc(stream&: stream2, trc: bCurve->trc[0]);
632 alignTag();
633 offset += writeColorTrc(stream&: stream2, trc: bCurve->trc[1]);
634 alignTag();
635 offset += writeColorTrc(stream&: stream2, trc: bCurve->trc[2]);
636 alignTag();
637 } else {
638 stream2 << uint(Tag::curv) << uint(0) << uint(0);
639 stream2 << uint(Tag::curv) << uint(0) << uint(0);
640 stream2 << uint(Tag::curv) << uint(0) << uint(0);
641 offset += 12 * 3;
642 }
643 if (combo.midMatrix || combo.midOffset || combo.midTable) {
644 matrixOffset = offset;
645 if (combo.midMatrix) {
646 stream2 << toFixedS1516(x: combo.midMatrix->r.x);
647 stream2 << toFixedS1516(x: combo.midMatrix->g.x);
648 stream2 << toFixedS1516(x: combo.midMatrix->b.x);
649 stream2 << toFixedS1516(x: combo.midMatrix->r.y);
650 stream2 << toFixedS1516(x: combo.midMatrix->g.y);
651 stream2 << toFixedS1516(x: combo.midMatrix->b.y);
652 stream2 << toFixedS1516(x: combo.midMatrix->r.z);
653 stream2 << toFixedS1516(x: combo.midMatrix->g.z);
654 stream2 << toFixedS1516(x: combo.midMatrix->b.z);
655 } else {
656 stream2 << toFixedS1516(x: 1.0f);
657 stream2 << toFixedS1516(x: 0.0f);
658 stream2 << toFixedS1516(x: 0.0f);
659 stream2 << toFixedS1516(x: 0.0f);
660 stream2 << toFixedS1516(x: 1.0f);
661 stream2 << toFixedS1516(x: 0.0f);
662 stream2 << toFixedS1516(x: 0.0f);
663 stream2 << toFixedS1516(x: 0.0f);
664 stream2 << toFixedS1516(x: 1.0f);
665 }
666 if (combo.midOffset) {
667 stream2 << toFixedS1516(x: combo.midOffset->x);
668 stream2 << toFixedS1516(x: combo.midOffset->y);
669 stream2 << toFixedS1516(x: combo.midOffset->z);
670 } else {
671 stream2 << toFixedS1516(x: 0.0f);
672 stream2 << toFixedS1516(x: 0.0f);
673 stream2 << toFixedS1516(x: 0.0f);
674 }
675 offset += 12 * 4;
676 mOffset = offset;
677 if (combo.midTable) {
678 offset += writeColorTrc(stream&: stream2, trc: combo.midTable->trc[0]);
679 alignTag();
680 offset += writeColorTrc(stream&: stream2, trc: combo.midTable->trc[1]);
681 alignTag();
682 offset += writeColorTrc(stream&: stream2, trc: combo.midTable->trc[2]);
683 alignTag();
684 } else {
685 stream2 << uint(Tag::curv) << uint(0) << uint(0);
686 stream2 << uint(Tag::curv) << uint(0) << uint(0);
687 stream2 << uint(Tag::curv) << uint(0) << uint(0);
688 offset += 12 * 3;
689 }
690 }
691 if (combo.clut || aCurve) {
692 clutOffset = offset;
693 if (combo.clut) {
694 stream2 << uchar(combo.clut->gridPointsX);
695 stream2 << uchar(combo.clut->gridPointsY);
696 stream2 << uchar(combo.clut->gridPointsZ);
697 if (inChannels == 4)
698 stream2 << uchar(combo.clut->gridPointsW);
699 else
700 stream2 << uchar(0);
701 for (int i = 0; i < 12; ++i)
702 stream2 << uchar(0);
703 stream2 << uchar(2) << uchar(0) << uchar(0) << uchar(0);
704 offset += 20;
705 if (outChannels == 4) {
706 for (const QColorVector &v : combo.clut->table) {
707 stream2 << quint16(v.x * 65535.0f + 0.5f);
708 stream2 << quint16(v.y * 65535.0f + 0.5f);
709 stream2 << quint16(v.z * 65535.0f + 0.5f);
710 stream2 << quint16(v.w * 65535.0f + 0.5f);
711 }
712 } else {
713 for (const QColorVector &v : combo.clut->table) {
714 stream2 << quint16(v.x * 65535.0f + 0.5f);
715 stream2 << quint16(v.y * 65535.0f + 0.5f);
716 stream2 << quint16(v.z * 65535.0f + 0.5f);
717 }
718 }
719 offset += 2 * outChannels * combo.clut->table.size();
720 alignTag();
721 } else {
722 for (int i = 0; i < 16; ++i)
723 stream2 << uchar(0);
724 stream2 << uchar(1) << uchar(0) << uchar(0) << uchar(0);
725 offset += 20;
726 }
727 aOffset = offset;
728 if (aCurve) {
729 offset += writeColorTrc(stream&: stream2, trc: aCurve->trc[0]);
730 alignTag();
731 offset += writeColorTrc(stream&: stream2, trc: aCurve->trc[1]);
732 alignTag();
733 offset += writeColorTrc(stream&: stream2, trc: aCurve->trc[2]);
734 alignTag();
735 if (aChannels == 4) {
736 offset += writeColorTrc(stream&: stream2, trc: aCurve->trc[3]);
737 alignTag();
738 }
739 } else {
740 stream2 << uint(Tag::curv) << uint(0) << uint(0);
741 stream2 << uint(Tag::curv) << uint(0) << uint(0);
742 stream2 << uint(Tag::curv) << uint(0) << uint(0);
743 if (aChannels == 4)
744 stream2 << uint(Tag::curv) << uint(0) << uint(0);
745 offset += 12 * aChannels;
746 }
747 }
748 buffer2.close();
749 QByteArray tagData = buffer2.buffer();
750 stream << quint32(bOffset);
751 stream << quint32(matrixOffset);
752 stream << quint32(mOffset);
753 stream << quint32(clutOffset);
754 stream << quint32(aOffset);
755 stream.writeRawData(tagData.data(), len: tagData.size());
756
757 return int(sizeof(mABTagData) + tagData.size());
758 }
759}
760
761QByteArray toIccProfile(const QColorSpace &space)
762{
763 if (!space.isValid())
764 return QByteArray();
765
766 const QColorSpacePrivate *spaceDPtr = QColorSpacePrivate::get(colorSpace: space);
767 if (!spaceDPtr->iccProfile.isEmpty())
768 return spaceDPtr->iccProfile;
769
770 int fixedLengthTagCount = 5;
771 if (!spaceDPtr->isThreeComponentMatrix())
772 fixedLengthTagCount = 2;
773 else if (spaceDPtr->colorModel == QColorSpace::ColorModel::Gray)
774 fixedLengthTagCount = 2;
775 bool writeChad = false;
776 bool writeB2a = true;
777 bool writeCicp = false;
778 if (spaceDPtr->isThreeComponentMatrix() && !spaceDPtr->chad.isIdentity()) {
779 writeChad = true;
780 fixedLengthTagCount++;
781 }
782 int varLengthTagCount = 4;
783 if (!spaceDPtr->isThreeComponentMatrix())
784 varLengthTagCount = 3;
785 else if (spaceDPtr->colorModel == QColorSpace::ColorModel::Gray)
786 varLengthTagCount = 2;
787
788 if (!space.isValidTarget()) {
789 writeB2a = false;
790 Q_ASSERT(!spaceDPtr->isThreeComponentMatrix());
791 varLengthTagCount--;
792 }
793 switch (spaceDPtr->transferFunction) {
794 case QColorSpace::TransferFunction::St2084:
795 case QColorSpace::TransferFunction::Hlg:
796 writeCicp = true;
797 fixedLengthTagCount++;
798 break;
799 default:
800 break;
801 }
802
803 const int tagCount = fixedLengthTagCount + varLengthTagCount;
804 const uint profileDataOffset = 128 + 4 + 12 * tagCount;
805 uint variableTagTableOffsets = 128 + 4 + 12 * fixedLengthTagCount;
806
807 uint currentOffset = 0;
808 uint rTrcOffset = 0;
809 uint gTrcOffset = 0;
810 uint bTrcOffset = 0;
811 uint kTrcOffset = 0;
812 uint rTrcSize = 0;
813 uint gTrcSize = 0;
814 uint bTrcSize = 0;
815 uint kTrcSize = 0;
816 uint descOffset = 0;
817 uint descSize = 0;
818 uint mA2bOffset = 0;
819 uint mB2aOffset = 0;
820 uint mA2bSize = 0;
821 uint mB2aSize = 0;
822
823 QBuffer buffer;
824 buffer.open(openMode: QIODevice::WriteOnly);
825 QDataStream stream(&buffer);
826
827 // Profile header:
828 stream << uint(0); // Size, we will update this later
829 stream << uint(0);
830 stream << uint(0x04400000); // Version 4.4
831 stream << uint(ProfileClass::Display);
832 switch (spaceDPtr->colorModel) {
833 case QColorSpace::ColorModel::Rgb:
834 stream << uint(ColorSpaceType::Rgb);
835 break;
836 case QColorSpace::ColorModel::Gray:
837 stream << uint(ColorSpaceType::Gray);
838 break;
839 case QColorSpace::ColorModel::Cmyk:
840 stream << uint(ColorSpaceType::Cmyk);
841 break;
842 case QColorSpace::ColorModel::Undefined:
843 Q_UNREACHABLE();
844 }
845 stream << (spaceDPtr->isPcsLab ? uint(Tag::Lab_) : uint(Tag::XYZ_));
846 stream << uint(0) << uint(0) << uint(0);
847 stream << uint(Tag::acsp);
848 stream << uint(0) << uint(0) << uint(0);
849 stream << uint(0) << uint(0) << uint(0);
850 stream << uint(0); // Rendering intent
851 stream << uint(0x0000f6d6); // D50 X
852 stream << uint(0x00010000); // D50 Y
853 stream << uint(0x0000d32d); // D50 Z
854 stream << IccTag(a: 'Q',b: 't', QT_VERSION_MAJOR, QT_VERSION_MINOR);
855 stream << uint(0) << uint(0) << uint(0) << uint(0);
856 stream << uint(0) << uint(0) << uint(0) << uint(0) << uint(0) << uint(0) << uint(0);
857
858 currentOffset = profileDataOffset;
859 if (spaceDPtr->isThreeComponentMatrix()) {
860 // Tag table:
861 stream << uint(tagCount);
862 if (spaceDPtr->colorModel == QColorSpace::ColorModel::Rgb) {
863 stream << uint(Tag::rXYZ) << uint(currentOffset + 00) << uint(20);
864 stream << uint(Tag::gXYZ) << uint(currentOffset + 20) << uint(20);
865 stream << uint(Tag::bXYZ) << uint(currentOffset + 40) << uint(20);
866 currentOffset += 20 + 20 + 20;
867 }
868 stream << uint(Tag::wtpt) << uint(currentOffset + 00) << uint(20);
869 stream << uint(Tag::cprt) << uint(currentOffset + 20) << uint(34);
870 currentOffset += 20 + 34 + 2;
871 if (writeChad) {
872 stream << uint(Tag::chad) << uint(currentOffset) << uint(44);
873 currentOffset += 44;
874 }
875 if (writeCicp) {
876 stream << uint(Tag::cicp) << uint(currentOffset) << uint(12);
877 currentOffset += 12;
878 }
879 // From here the offset and size will be updated later:
880 if (spaceDPtr->colorModel == QColorSpace::ColorModel::Rgb) {
881 stream << uint(Tag::rTRC) << uint(0) << uint(0);
882 stream << uint(Tag::gTRC) << uint(0) << uint(0);
883 stream << uint(Tag::bTRC) << uint(0) << uint(0);
884 } else {
885 stream << uint(Tag::kTRC) << uint(0) << uint(0);
886 }
887 stream << uint(Tag::desc) << uint(0) << uint(0);
888
889 // Tag data:
890 if (spaceDPtr->colorModel == QColorSpace::ColorModel::Rgb) {
891 stream << uint(Tag::XYZ_) << uint(0);
892 stream << toFixedS1516(x: spaceDPtr->toXyz.r.x);
893 stream << toFixedS1516(x: spaceDPtr->toXyz.r.y);
894 stream << toFixedS1516(x: spaceDPtr->toXyz.r.z);
895 stream << uint(Tag::XYZ_) << uint(0);
896 stream << toFixedS1516(x: spaceDPtr->toXyz.g.x);
897 stream << toFixedS1516(x: spaceDPtr->toXyz.g.y);
898 stream << toFixedS1516(x: spaceDPtr->toXyz.g.z);
899 stream << uint(Tag::XYZ_) << uint(0);
900 stream << toFixedS1516(x: spaceDPtr->toXyz.b.x);
901 stream << toFixedS1516(x: spaceDPtr->toXyz.b.y);
902 stream << toFixedS1516(x: spaceDPtr->toXyz.b.z);
903 }
904 stream << uint(Tag::XYZ_) << uint(0);
905 stream << toFixedS1516(x: spaceDPtr->whitePoint.x);
906 stream << toFixedS1516(x: spaceDPtr->whitePoint.y);
907 stream << toFixedS1516(x: spaceDPtr->whitePoint.z);
908 stream << uint(Tag::mluc) << uint(0);
909 stream << uint(1) << uint(12);
910 stream << uchar('e') << uchar('n') << uchar('U') << uchar('S');
911 stream << uint(6) << uint(28);
912 stream << ushort('N') << ushort('/') << ushort('A');
913 stream << ushort(0); // 4-byte alignment
914 if (writeChad) {
915 const QColorMatrix &chad = spaceDPtr->chad;
916 stream << uint(Tag::sf32) << uint(0);
917 stream << toFixedS1516(x: chad.r.x);
918 stream << toFixedS1516(x: chad.g.x);
919 stream << toFixedS1516(x: chad.b.x);
920 stream << toFixedS1516(x: chad.r.y);
921 stream << toFixedS1516(x: chad.g.y);
922 stream << toFixedS1516(x: chad.b.y);
923 stream << toFixedS1516(x: chad.r.z);
924 stream << toFixedS1516(x: chad.g.z);
925 stream << toFixedS1516(x: chad.b.z);
926 }
927 if (writeCicp) {
928 stream << uint(Tag::cicp) << uint(0);
929 stream << uchar(2); // Unspecified primaries
930 if (spaceDPtr->transferFunction == QColorSpace::TransferFunction::St2084)
931 stream << uchar(16);
932 else if (spaceDPtr->transferFunction == QColorSpace::TransferFunction::Hlg)
933 stream << uchar(18);
934 else
935 Q_UNREACHABLE();
936 stream << uchar(0); // Only for YCbCr, otherwise 0
937 stream << uchar(1); // Video full range
938 }
939
940 // From now on the data is variable sized:
941 if (spaceDPtr->colorModel == QColorSpace::ColorModel::Rgb) {
942 rTrcOffset = currentOffset;
943 rTrcSize = writeColorTrc(stream, trc: spaceDPtr->trc[0]);
944 currentOffset += rTrcSize;
945 if (spaceDPtr->trc[0] == spaceDPtr->trc[1]) {
946 gTrcOffset = rTrcOffset;
947 gTrcSize = rTrcSize;
948 } else {
949 gTrcOffset = currentOffset;
950 gTrcSize = writeColorTrc(stream, trc: spaceDPtr->trc[1]);
951 currentOffset += gTrcSize;
952 }
953 if (spaceDPtr->trc[0] == spaceDPtr->trc[2]) {
954 bTrcOffset = rTrcOffset;
955 bTrcSize = rTrcSize;
956 } else {
957 bTrcOffset = currentOffset;
958 bTrcSize = writeColorTrc(stream, trc: spaceDPtr->trc[2]);
959 currentOffset += bTrcSize;
960 }
961 } else {
962 Q_ASSERT(spaceDPtr->colorModel == QColorSpace::ColorModel::Gray);
963 kTrcOffset = currentOffset;
964 kTrcSize = writeColorTrc(stream, trc: spaceDPtr->trc[0]);
965 currentOffset += kTrcSize;
966 }
967 } else {
968 // Tag table:
969 stream << uint(tagCount);
970 stream << uint(Tag::wtpt) << uint(profileDataOffset + 00) << uint(20);
971 stream << uint(Tag::cprt) << uint(profileDataOffset + 20) << uint(34);
972 currentOffset += 20 + 34 + 2;
973 // From here the offset and size will be updated later:
974 stream << uint(Tag::A2B0) << uint(0) << uint(0);
975 if (writeB2a)
976 stream << uint(Tag::B2A0) << uint(0) << uint(0);
977 stream << uint(Tag::desc) << uint(0) << uint(0);
978
979 // Fixed tag data
980 stream << uint(Tag::XYZ_) << uint(0);
981 stream << toFixedS1516(x: spaceDPtr->whitePoint.x);
982 stream << toFixedS1516(x: spaceDPtr->whitePoint.y);
983 stream << toFixedS1516(x: spaceDPtr->whitePoint.z);
984 stream << uint(Tag::mluc) << uint(0);
985 stream << uint(1) << uint(12);
986 stream << uchar('e') << uchar('n') << uchar('U') << uchar('S');
987 stream << uint(6) << uint(28);
988 stream << ushort('N') << ushort('/') << ushort('A');
989 stream << ushort(0); // 4-byte alignment
990
991 // From now on the data is variable sized:
992 mA2bOffset = currentOffset;
993 mA2bSize = writeMab(stream, abList: spaceDPtr->mAB, isAb: true, pcsLab: spaceDPtr->isPcsLab, isCmyk: spaceDPtr->colorModel == QColorSpace::ColorModel::Cmyk);
994 currentOffset += mA2bSize;
995 if (writeB2a) {
996 mB2aOffset = currentOffset;
997 mB2aSize = writeMab(stream, abList: spaceDPtr->mBA, isAb: false, pcsLab: spaceDPtr->isPcsLab, isCmyk: spaceDPtr->colorModel == QColorSpace::ColorModel::Cmyk);
998 currentOffset += mB2aSize;
999 }
1000 }
1001
1002 // Writing description
1003 descOffset = currentOffset;
1004 const QString description = space.description();
1005 stream << uint(Tag::mluc) << uint(0);
1006 stream << uint(1) << uint(12);
1007 stream << uchar('e') << uchar('n') << uchar('U') << uchar('S');
1008 stream << uint(description.size() * 2) << uint(28);
1009 for (QChar ch : description)
1010 stream << ushort(ch.unicode());
1011 descSize = 28 + description.size() * 2;
1012 if (description.size() & 1) {
1013 stream << ushort(0);
1014 currentOffset += 2;
1015 }
1016 currentOffset += descSize;
1017
1018 buffer.close();
1019 QByteArray iccProfile = buffer.buffer();
1020 // Now write final size
1021 *(quint32_be *)iccProfile.data() = iccProfile.size();
1022 // And the final indices and sizes of variable size tags:
1023 if (spaceDPtr->isThreeComponentMatrix()) {
1024 if (spaceDPtr->colorModel == QColorSpace::ColorModel::Rgb) {
1025 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 4) = rTrcOffset;
1026 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 8) = rTrcSize;
1027 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 12 + 4) = gTrcOffset;
1028 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 12 + 8) = gTrcSize;
1029 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 2 * 12 + 4) = bTrcOffset;
1030 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 2 * 12 + 8) = bTrcSize;
1031 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 3 * 12 + 4) = descOffset;
1032 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 3 * 12 + 8) = descSize;
1033 } else {
1034 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 4) = kTrcOffset;
1035 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 8) = kTrcSize;
1036 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 12 + 4) = descOffset;
1037 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 12 + 8) = descSize;
1038 }
1039 } else {
1040 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 4) = mA2bOffset;
1041 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 8) = mA2bSize;
1042 variableTagTableOffsets += 12;
1043 if (writeB2a) {
1044 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 4) = mB2aOffset;
1045 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 8) = mB2aSize;
1046 variableTagTableOffsets += 12;
1047 }
1048 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 4) = descOffset;
1049 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 8) = descSize;
1050 }
1051
1052#if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS)
1053 const ICCProfileHeader *iccHeader = (const ICCProfileHeader *)iccProfile.constData();
1054 Q_ASSERT(qsizetype(iccHeader->profileSize) == qsizetype(iccProfile.size()));
1055 Q_ASSERT(isValidIccProfile(*iccHeader));
1056#endif
1057
1058 return iccProfile;
1059}
1060
1061struct TagEntry {
1062 quint32 offset;
1063 quint32 size;
1064};
1065
1066static bool parseXyzData(const QByteArray &data, const TagEntry &tagEntry, QColorVector &colorVector)
1067{
1068 if (tagEntry.size < sizeof(XYZTagData)) {
1069 qCWarning(lcIcc) << "Undersized XYZ tag";
1070 return false;
1071 }
1072 const XYZTagData xyz = qFromUnaligned<XYZTagData>(src: data.constData() + tagEntry.offset);
1073 if (xyz.type != quint32(Tag::XYZ_)) {
1074 qCWarning(lcIcc) << "Bad XYZ content type";
1075 return false;
1076 }
1077 const float x = fromFixedS1516(x: xyz.fixedX);
1078 const float y = fromFixedS1516(x: xyz.fixedY);
1079 const float z = fromFixedS1516(x: xyz.fixedZ);
1080
1081 colorVector = QColorVector(x, y, z);
1082 return true;
1083}
1084
1085static quint32 parseTRC(const QByteArrayView &tagData, QColorTrc &gamma, QColorTransferTable::Type type = QColorTransferTable::TwoWay)
1086{
1087 if (tagData.size() < 12)
1088 return 0;
1089 const GenericTagData trcData = qFromUnaligned<GenericTagData>(src: tagData.constData());
1090 if (trcData.type == quint32(Tag::curv)) {
1091 Q_STATIC_ASSERT(sizeof(CurvTagData) == 12);
1092 const CurvTagData curv = qFromUnaligned<CurvTagData>(src: tagData.constData());
1093 if (curv.valueCount > (1 << 16)) {
1094 qCWarning(lcIcc) << "Invalid count in curv table";
1095 return 0;
1096 }
1097 if (tagData.size() < qsizetype(12 + 2 * curv.valueCount)) {
1098 qCWarning(lcIcc) << "Truncated curv table";
1099 return 0;
1100 }
1101 const auto valueOffset = sizeof(CurvTagData);
1102 if (curv.valueCount == 0) {
1103 gamma.m_type = QColorTrc::Type::ParameterizedFunction;
1104 gamma.m_fun = QColorTransferFunction(); // Linear
1105 } else if (curv.valueCount == 1) {
1106 const quint16 v = qFromBigEndian<quint16>(src: tagData.constData() + valueOffset);
1107 gamma.m_type = QColorTrc::Type::ParameterizedFunction;
1108 gamma.m_fun = QColorTransferFunction::fromGamma(gamma: v * (1.0f / 256.0f));
1109 } else {
1110 QList<quint16> tabl;
1111 tabl.resize(size: curv.valueCount);
1112 static_assert(sizeof(GenericTagData) == 2 * sizeof(quint32_be),
1113 "GenericTagData has padding. The following code is a subject to UB.");
1114 qFromBigEndian<quint16>(source: tagData.constData() + valueOffset, count: curv.valueCount, dest: tabl.data());
1115 QColorTransferTable table(curv.valueCount, tabl, type);
1116 QColorTransferFunction curve;
1117 if (!table.checkValidity()) {
1118 qCWarning(lcIcc) << "Invalid curv table";
1119 return 0;
1120 } else if (!table.asColorTransferFunction(transferFn: &curve)) {
1121 gamma.m_type = QColorTrc::Type::Table;
1122 gamma.m_table = table;
1123 } else {
1124 qCDebug(lcIcc) << "Detected curv table as function";
1125 gamma.m_type = QColorTrc::Type::ParameterizedFunction;
1126 gamma.m_fun = curve;
1127 }
1128 }
1129 return 12 + 2 * curv.valueCount;
1130 }
1131 if (trcData.type == quint32(Tag::para)) {
1132 Q_STATIC_ASSERT(sizeof(ParaTagData) == 12);
1133 const ParaTagData para = qFromUnaligned<ParaTagData>(src: tagData.constData());
1134 const auto parametersOffset = sizeof(ParaTagData);
1135 quint32 parameters[7];
1136 switch (para.curveType) {
1137 case 0: {
1138 if (tagData.size() < 12 + 1 * 4)
1139 return 0;
1140 qFromBigEndian<quint32>(source: tagData.constData() + parametersOffset, count: 1, dest: parameters);
1141 float g = fromFixedS1516(x: parameters[0]);
1142 gamma.m_type = QColorTrc::Type::ParameterizedFunction;
1143 gamma.m_fun = QColorTransferFunction::fromGamma(gamma: g);
1144 return 12 + 1 * 4;
1145 }
1146 case 1: {
1147 if (tagData.size() < 12 + 3 * 4)
1148 return 0;
1149 qFromBigEndian<quint32>(source: tagData.constData() + parametersOffset, count: 3, dest: parameters);
1150 if (parameters[1] == 0)
1151 return 0;
1152 float g = fromFixedS1516(x: parameters[0]);
1153 float a = fromFixedS1516(x: parameters[1]);
1154 float b = fromFixedS1516(x: parameters[2]);
1155 float d = -b / a;
1156 gamma.m_type = QColorTrc::Type::ParameterizedFunction;
1157 gamma.m_fun = QColorTransferFunction(a, b, 0.0f, d, 0.0f, 0.0f, g);
1158 return 12 + 3 * 4;
1159 }
1160 case 2: {
1161 if (tagData.size() < 12 + 4 * 4)
1162 return 0;
1163 qFromBigEndian<quint32>(source: tagData.constData() + parametersOffset, count: 4, dest: parameters);
1164 if (parameters[1] == 0)
1165 return 0;
1166 float g = fromFixedS1516(x: parameters[0]);
1167 float a = fromFixedS1516(x: parameters[1]);
1168 float b = fromFixedS1516(x: parameters[2]);
1169 float c = fromFixedS1516(x: parameters[3]);
1170 float d = -b / a;
1171 gamma.m_type = QColorTrc::Type::ParameterizedFunction;
1172 gamma.m_fun = QColorTransferFunction(a, b, 0.0f, d, c, c, g);
1173 return 12 + 4 * 4;
1174 }
1175 case 3: {
1176 if (tagData.size() < 12 + 5 * 4)
1177 return 0;
1178 qFromBigEndian<quint32>(source: tagData.constData() + parametersOffset, count: 5, dest: parameters);
1179 float g = fromFixedS1516(x: parameters[0]);
1180 float a = fromFixedS1516(x: parameters[1]);
1181 float b = fromFixedS1516(x: parameters[2]);
1182 float c = fromFixedS1516(x: parameters[3]);
1183 float d = fromFixedS1516(x: parameters[4]);
1184 gamma.m_type = QColorTrc::Type::ParameterizedFunction;
1185 gamma.m_fun = QColorTransferFunction(a, b, c, d, 0.0f, 0.0f, g);
1186 return 12 + 5 * 4;
1187 }
1188 case 4: {
1189 if (tagData.size() < 12 + 7 * 4)
1190 return 0;
1191 qFromBigEndian<quint32>(source: tagData.constData() + parametersOffset, count: 7, dest: parameters);
1192 float g = fromFixedS1516(x: parameters[0]);
1193 float a = fromFixedS1516(x: parameters[1]);
1194 float b = fromFixedS1516(x: parameters[2]);
1195 float c = fromFixedS1516(x: parameters[3]);
1196 float d = fromFixedS1516(x: parameters[4]);
1197 float e = fromFixedS1516(x: parameters[5]);
1198 float f = fromFixedS1516(x: parameters[6]);
1199 gamma.m_type = QColorTrc::Type::ParameterizedFunction;
1200 gamma.m_fun = QColorTransferFunction(a, b, c, d, e, f, g);
1201 return 12 + 7 * 4;
1202 }
1203 default:
1204 qCWarning(lcIcc) << "Unknown para type" << uint(para.curveType);
1205 return 0;
1206 }
1207 return true;
1208 }
1209 qCWarning(lcIcc) << "Invalid TRC data type" << Qt::hex << trcData.type;
1210 return 0;
1211}
1212
1213template<typename T>
1214static void parseCLUT(const T *tableData, const float f, QColorCLUT *clut, uchar outputChannels)
1215{
1216 if (outputChannels == 4) {
1217 for (qsizetype index = 0; index < clut->table.size(); ++index) {
1218 QColorVector v(tableData[index * 4 + 0] * f,
1219 tableData[index * 4 + 1] * f,
1220 tableData[index * 4 + 2] * f,
1221 tableData[index * 4 + 3] * f);
1222 clut->table[index] = v;
1223 };
1224 } else {
1225 for (qsizetype index = 0; index < clut->table.size(); ++index) {
1226 QColorVector v(tableData[index * 3 + 0] * f,
1227 tableData[index * 3 + 1] * f,
1228 tableData[index * 3 + 2] * f);
1229 clut->table[index] = v;
1230 };
1231 }
1232}
1233
1234// Parses lut8 and lut16 type elements
1235template<typename T>
1236static bool parseLutData(const QByteArray &data, const TagEntry &tagEntry, QColorSpacePrivate *colorSpacePrivate, bool isAb)
1237{
1238 if (tagEntry.size < sizeof(T)) {
1239 qCWarning(lcIcc) << "Undersized lut8/lut16 tag";
1240 return false;
1241 }
1242 if (qsizetype(tagEntry.size) > data.size()) {
1243 qCWarning(lcIcc) << "Truncated lut8/lut16 tag";
1244 return false;
1245 }
1246 using S = std::conditional_t<std::is_same_v<T, Lut8TagData>, uint8_t, uint16_t>;
1247 const T lut = qFromUnaligned<T>(data.constData() + tagEntry.offset);
1248 int inputTableEntries, outputTableEntries, precision;
1249 if constexpr (std::is_same_v<T, Lut8TagData>) {
1250 Q_ASSERT(lut.type == quint32(Tag::mft1));
1251 if (!colorSpacePrivate->isPcsLab && isAb) {
1252 qCWarning(lcIcc) << "Lut8 can not output XYZ values";
1253 return false;
1254 }
1255 inputTableEntries = 256;
1256 outputTableEntries = 256;
1257 precision = 1;
1258 } else {
1259 Q_ASSERT(lut.type == quint32(Tag::mft2));
1260 inputTableEntries = lut.inputTableEntries;
1261 outputTableEntries = lut.outputTableEntries;
1262 if (inputTableEntries < 2 || inputTableEntries > 4096)
1263 return false;
1264 if (outputTableEntries < 2 || outputTableEntries > 4096)
1265 return false;
1266 precision = 2;
1267 }
1268
1269 bool inTableIsLinear = true, outTableIsLinear = true;
1270 QColorSpacePrivate::TransferElement inTableElement;
1271 QColorSpacePrivate::TransferElement outTableElement;
1272 QColorCLUT clutElement;
1273 QColorMatrix matrixElement;
1274
1275 matrixElement.r.x = fromFixedS1516(lut.e1);
1276 matrixElement.g.x = fromFixedS1516(lut.e2);
1277 matrixElement.b.x = fromFixedS1516(lut.e3);
1278 matrixElement.r.y = fromFixedS1516(lut.e4);
1279 matrixElement.g.y = fromFixedS1516(lut.e5);
1280 matrixElement.b.y = fromFixedS1516(lut.e6);
1281 matrixElement.r.z = fromFixedS1516(lut.e7);
1282 matrixElement.g.z = fromFixedS1516(lut.e8);
1283 matrixElement.b.z = fromFixedS1516(lut.e9);
1284 if (!colorSpacePrivate->isPcsLab && !isAb && !matrixElement.isValid()) {
1285 qCWarning(lcIcc) << "Invalid matrix values in lut8/lut16";
1286 return false;
1287 }
1288
1289 const int inputChannels = (isAb && colorSpacePrivate->colorModel == QColorSpace::ColorModel::Cmyk) ? 4 : 3;
1290 const int outputChannels = (!isAb && colorSpacePrivate->colorModel == QColorSpace::ColorModel::Cmyk) ? 4 : 3;
1291
1292 if (lut.inputChannels != inputChannels) {
1293 qCWarning(lcIcc) << "Unsupported lut8/lut16 input channel count" << lut.inputChannels;
1294 return false;
1295 }
1296
1297 if (lut.outputChannels != outputChannels) {
1298 qCWarning(lcIcc) << "Unsupported lut8/lut16 output channel count" << lut.outputChannels;
1299 return false;
1300 }
1301
1302 const qsizetype clutTableSize = intPow(lut.clutGridPoints, inputChannels);
1303 if (tagEntry.size < (sizeof(T) + precision * inputChannels * inputTableEntries
1304 + precision * outputChannels * outputTableEntries
1305 + precision * outputChannels * clutTableSize)) {
1306 qCWarning(lcIcc) << "Undersized lut8/lut16 tag, no room for tables";
1307 return false;
1308 }
1309 if (colorSpacePrivate->colorModel == QColorSpace::ColorModel::Cmyk && clutTableSize == 0) {
1310 qCWarning(lcIcc) << "Cmyk conversion must have a CLUT";
1311 return false;
1312 }
1313
1314 const uint8_t *tableData = reinterpret_cast<const uint8_t *>(data.constData() + tagEntry.offset + sizeof(T));
1315
1316 for (int j = 0; j < inputChannels; ++j) {
1317 QList<S> input(inputTableEntries);
1318 qFromBigEndian<S>(tableData, inputTableEntries, input.data());
1319 QColorTransferTable table(inputTableEntries, input, QColorTransferTable::OneWay);
1320 if (!table.checkValidity()) {
1321 qCWarning(lcIcc) << "Bad input table in lut8/lut16";
1322 return false;
1323 }
1324 if (!table.isIdentity())
1325 inTableIsLinear = false;
1326 inTableElement.trc[j] = std::move(table);
1327 tableData += inputTableEntries * precision;
1328 }
1329
1330 clutElement.table.resize(size: clutTableSize);
1331 clutElement.gridPointsX = clutElement.gridPointsY = clutElement.gridPointsZ = lut.clutGridPoints;
1332 if (inputChannels == 4)
1333 clutElement.gridPointsW = lut.clutGridPoints;
1334
1335 if constexpr (std::is_same_v<T, Lut8TagData>) {
1336 parseCLUT(tableData, f: 1.f / 255.f, clut: &clutElement, outputChannels);
1337 } else {
1338 float f = 1.0f / 65535.f;
1339 if (colorSpacePrivate->isPcsLab && isAb) // Legacy lut16 conversion to Lab
1340 f = 1.0f / 65280.f;
1341 QList<S> clutTable(clutTableSize * outputChannels);
1342 qFromBigEndian<S>(tableData, clutTable.size(), clutTable.data());
1343 parseCLUT(clutTable.constData(), f, &clutElement, outputChannels);
1344 }
1345 tableData += clutTableSize * outputChannels * precision;
1346
1347 for (int j = 0; j < outputChannels; ++j) {
1348 QList<S> output(outputTableEntries);
1349 qFromBigEndian<S>(tableData, outputTableEntries, output.data());
1350 QColorTransferTable table(outputTableEntries, output, QColorTransferTable::OneWay);
1351 if (!table.checkValidity()) {
1352 qCWarning(lcIcc) << "Bad output table in lut8/lut16";
1353 return false;
1354 }
1355 if (!table.isIdentity())
1356 outTableIsLinear = false;
1357 outTableElement.trc[j] = std::move(table);
1358 tableData += outputTableEntries * precision;
1359 }
1360
1361 if (isAb) {
1362 if (!inTableIsLinear)
1363 colorSpacePrivate->mAB.append(t: inTableElement);
1364 if (!clutElement.isEmpty())
1365 colorSpacePrivate->mAB.append(t: clutElement);
1366 if (!outTableIsLinear || colorSpacePrivate->mAB.isEmpty())
1367 colorSpacePrivate->mAB.append(t: outTableElement);
1368 } else {
1369 // The matrix is only to be applied if the input color-space is XYZ
1370 if (!colorSpacePrivate->isPcsLab && !matrixElement.isIdentity())
1371 colorSpacePrivate->mBA.append(t: matrixElement);
1372 if (!inTableIsLinear)
1373 colorSpacePrivate->mBA.append(t: inTableElement);
1374 if (!clutElement.isEmpty())
1375 colorSpacePrivate->mBA.append(t: clutElement);
1376 if (!outTableIsLinear || colorSpacePrivate->mBA.isEmpty())
1377 colorSpacePrivate->mBA.append(t: outTableElement);
1378 }
1379 return true;
1380}
1381
1382// Parses mAB and mBA type elements
1383static bool parseMabData(const QByteArray &data, const TagEntry &tagEntry, QColorSpacePrivate *colorSpacePrivate, bool isAb)
1384{
1385 if (tagEntry.size < sizeof(mABTagData)) {
1386 qCWarning(lcIcc) << "Undersized mAB/mBA tag";
1387 return false;
1388 }
1389 if (qsizetype(tagEntry.size) > data.size()) {
1390 qCWarning(lcIcc) << "Truncated mAB/mBA tag";
1391 return false;
1392 }
1393 const mABTagData mab = qFromUnaligned<mABTagData>(src: data.constData() + tagEntry.offset);
1394 if ((mab.type != quint32(Tag::mAB_) && isAb) || (mab.type != quint32(Tag::mBA_) && !isAb)){
1395 qCWarning(lcIcc) << "Bad mAB/mBA content type";
1396 return false;
1397 }
1398
1399 const int inputChannels = (isAb && colorSpacePrivate->colorModel == QColorSpace::ColorModel::Cmyk) ? 4 : 3;
1400 const int outputChannels = (!isAb && colorSpacePrivate->colorModel == QColorSpace::ColorModel::Cmyk) ? 4 : 3;
1401
1402 if (mab.inputChannels != inputChannels) {
1403 qCWarning(lcIcc) << "Unsupported mAB/mBA input channel count" << mab.inputChannels;
1404 return false;
1405 }
1406
1407 if (mab.outputChannels != outputChannels) {
1408 qCWarning(lcIcc) << "Unsupported mAB/mBA output channel count" << mab.outputChannels;
1409 return false;
1410 }
1411
1412 // These combinations are legal: B, M + Matrix + B, A + Clut + B, A + Clut + M + Matrix + B
1413 if (!mab.bCurvesOffset) {
1414 qCWarning(lcIcc) << "Illegal mAB/mBA without B table";
1415 return false;
1416 }
1417 if (((bool)mab.matrixOffset != (bool)mab.mCurvesOffset) ||
1418 ((bool)mab.aCurvesOffset != (bool)mab.clutOffset)) {
1419 qCWarning(lcIcc) << "Illegal mAB/mBA element combination";
1420 return false;
1421 }
1422
1423 if (mab.aCurvesOffset > (tagEntry.size - 3 * sizeof(GenericTagData)) ||
1424 mab.bCurvesOffset > (tagEntry.size - 3 * sizeof(GenericTagData)) ||
1425 mab.mCurvesOffset > (tagEntry.size - 3 * sizeof(GenericTagData)) ||
1426 mab.matrixOffset > (tagEntry.size - 4 * 12) ||
1427 mab.clutOffset > (tagEntry.size - 20)) {
1428 qCWarning(lcIcc) << "Illegal mAB/mBA element offset";
1429 return false;
1430 }
1431
1432 QColorSpacePrivate::TransferElement bTableElement;
1433 QColorSpacePrivate::TransferElement aTableElement;
1434 QColorCLUT clutElement;
1435 QColorSpacePrivate::TransferElement mTableElement;
1436 QColorMatrix matrixElement;
1437 QColorVector offsetElement;
1438
1439 auto parseCurves = [&data, &tagEntry] (uint curvesOffset, QColorTrc *table, int channels) {
1440 for (int i = 0; i < channels; ++i) {
1441 if (qsizetype(tagEntry.offset + curvesOffset + 12) > data.size() || curvesOffset + 12 > tagEntry.size) {
1442 qCWarning(lcIcc) << "Space missing for channel curves in mAB/mBA";
1443 return false;
1444 }
1445 auto size = parseTRC(tagData: QByteArrayView(data).sliced(pos: tagEntry.offset + curvesOffset, n: tagEntry.size - curvesOffset), gamma&: table[i], type: QColorTransferTable::OneWay);
1446 if (!size)
1447 return false;
1448 if (size & 2) size += 2; // possible padding
1449 curvesOffset += size;
1450 }
1451 return true;
1452 };
1453
1454 bool bCurvesAreLinear = true, aCurvesAreLinear = true, mCurvesAreLinear = true;
1455
1456 // B Curves
1457 if (!parseCurves(mab.bCurvesOffset, bTableElement.trc, isAb ? outputChannels : inputChannels)) {
1458 qCWarning(lcIcc) << "Invalid B curves";
1459 return false;
1460 } else {
1461 bCurvesAreLinear = bTableElement.trc[0].isIdentity() && bTableElement.trc[1].isIdentity() && bTableElement.trc[2].isIdentity();
1462 }
1463
1464 // A Curves
1465 if (mab.aCurvesOffset) {
1466 if (!parseCurves(mab.aCurvesOffset, aTableElement.trc, isAb ? inputChannels : outputChannels)) {
1467 qCWarning(lcIcc) << "Invalid A curves";
1468 return false;
1469 } else {
1470 aCurvesAreLinear = aTableElement.trc[0].isIdentity() && aTableElement.trc[1].isIdentity() && aTableElement.trc[2].isIdentity();
1471 }
1472 }
1473
1474 // M Curves
1475 if (mab.mCurvesOffset) {
1476 if (!parseCurves(mab.mCurvesOffset, mTableElement.trc, 3)) {
1477 qCWarning(lcIcc) << "Invalid M curves";
1478 return false;
1479 } else {
1480 mCurvesAreLinear = mTableElement.trc[0].isIdentity() && mTableElement.trc[1].isIdentity() && mTableElement.trc[2].isIdentity();
1481 }
1482 }
1483
1484 // Matrix
1485 if (mab.matrixOffset) {
1486 const MatrixElement matrix = qFromUnaligned<MatrixElement>(src: data.constData() + tagEntry.offset + mab.matrixOffset);
1487 matrixElement.r.x = fromFixedS1516(x: matrix.e0);
1488 matrixElement.g.x = fromFixedS1516(x: matrix.e1);
1489 matrixElement.b.x = fromFixedS1516(x: matrix.e2);
1490 matrixElement.r.y = fromFixedS1516(x: matrix.e3);
1491 matrixElement.g.y = fromFixedS1516(x: matrix.e4);
1492 matrixElement.b.y = fromFixedS1516(x: matrix.e5);
1493 matrixElement.r.z = fromFixedS1516(x: matrix.e6);
1494 matrixElement.g.z = fromFixedS1516(x: matrix.e7);
1495 matrixElement.b.z = fromFixedS1516(x: matrix.e8);
1496 offsetElement.x = fromFixedS1516(x: matrix.e9);
1497 offsetElement.y = fromFixedS1516(x: matrix.e10);
1498 offsetElement.z = fromFixedS1516(x: matrix.e11);
1499 if (!matrixElement.isValid() || !offsetElement.isValid()) {
1500 qCWarning(lcIcc) << "Invalid matrix values in mAB/mBA element";
1501 return false;
1502 }
1503 }
1504
1505 // CLUT
1506 if (mab.clutOffset) {
1507 clutElement.gridPointsX = uint8_t(data[tagEntry.offset + mab.clutOffset]);
1508 clutElement.gridPointsY = uint8_t(data[tagEntry.offset + mab.clutOffset + 1]);
1509 clutElement.gridPointsZ = uint8_t(data[tagEntry.offset + mab.clutOffset + 2]);
1510 clutElement.gridPointsW = std::max(a: uint8_t(data[tagEntry.offset + mab.clutOffset + 3]), b: uint8_t(1));
1511 const uchar precision = data[tagEntry.offset + mab.clutOffset + 16];
1512 if (precision > 2 || precision < 1) {
1513 qCWarning(lcIcc) << "Invalid mAB/mBA element CLUT precision";
1514 return false;
1515 }
1516 if (clutElement.gridPointsX < 2 || clutElement.gridPointsY < 2 || clutElement.gridPointsZ < 2) {
1517 qCWarning(lcIcc) << "Empty CLUT";
1518 return false;
1519 }
1520 const qsizetype clutTableSize = clutElement.gridPointsX * clutElement.gridPointsY * clutElement.gridPointsZ * clutElement.gridPointsW;
1521 if ((mab.clutOffset + 20 + clutTableSize * outputChannels * precision) > tagEntry.size) {
1522 qCWarning(lcIcc) << "CLUT oversized for tag";
1523 return false;
1524 }
1525
1526 clutElement.table.resize(size: clutTableSize);
1527 if (precision == 2) {
1528 QList<uint16_t> clutTable(clutTableSize * outputChannels);
1529 qFromBigEndian<uint16_t>(source: data.constData() + tagEntry.offset + mab.clutOffset + 20, count: clutTable.size(), dest: clutTable.data());
1530 parseCLUT(tableData: clutTable.constData(), f: (1.f/65535.f), clut: &clutElement, outputChannels);
1531 } else {
1532 const uint8_t *clutTable = reinterpret_cast<const uint8_t *>(data.constData() + tagEntry.offset + mab.clutOffset + 20);
1533 parseCLUT(tableData: clutTable, f: (1.f/255.f), clut: &clutElement, outputChannels);
1534 }
1535 } else if (colorSpacePrivate->colorModel == QColorSpace::ColorModel::Cmyk) {
1536 qCWarning(lcIcc) << "Cmyk conversion must have a CLUT";
1537 return false;
1538 }
1539
1540 if (isAb) {
1541 if (mab.aCurvesOffset) {
1542 if (!aCurvesAreLinear)
1543 colorSpacePrivate->mAB.append(t: std::move(aTableElement));
1544 if (!clutElement.isEmpty())
1545 colorSpacePrivate->mAB.append(t: std::move(clutElement));
1546 }
1547 if (mab.mCurvesOffset && outputChannels == 3) {
1548 if (!mCurvesAreLinear)
1549 colorSpacePrivate->mAB.append(t: std::move(mTableElement));
1550 if (!matrixElement.isIdentity())
1551 colorSpacePrivate->mAB.append(t: std::move(matrixElement));
1552 if (!offsetElement.isNull())
1553 colorSpacePrivate->mAB.append(t: std::move(offsetElement));
1554 }
1555 if (!bCurvesAreLinear|| colorSpacePrivate->mAB.isEmpty())
1556 colorSpacePrivate->mAB.append(t: std::move(bTableElement));
1557 } else {
1558 if (!bCurvesAreLinear)
1559 colorSpacePrivate->mBA.append(t: std::move(bTableElement));
1560 if (mab.mCurvesOffset && inputChannels == 3) {
1561 if (!matrixElement.isIdentity())
1562 colorSpacePrivate->mBA.append(t: std::move(matrixElement));
1563 if (!offsetElement.isNull())
1564 colorSpacePrivate->mBA.append(t: std::move(offsetElement));
1565 if (!mCurvesAreLinear)
1566 colorSpacePrivate->mBA.append(t: std::move(mTableElement));
1567 }
1568 if (mab.aCurvesOffset) {
1569 if (!clutElement.isEmpty())
1570 colorSpacePrivate->mBA.append(t: std::move(clutElement));
1571 if (!aCurvesAreLinear)
1572 colorSpacePrivate->mBA.append(t: std::move(aTableElement));
1573 }
1574 if (colorSpacePrivate->mBA.isEmpty()) // Ensure non-empty to indicate valid empty transform
1575 colorSpacePrivate->mBA.append(t: std::move(bTableElement));
1576 }
1577
1578 return true;
1579}
1580
1581static bool parseA2B(const QByteArray &data, const TagEntry &tagEntry, QColorSpacePrivate *privat, bool isAb)
1582{
1583 const GenericTagData a2bData = qFromUnaligned<GenericTagData>(src: data.constData() + tagEntry.offset);
1584 if (a2bData.type == quint32(Tag::mft1))
1585 return parseLutData<Lut8TagData>(data, tagEntry, colorSpacePrivate: privat, isAb);
1586 else if (a2bData.type == quint32(Tag::mft2))
1587 return parseLutData<Lut16TagData>(data, tagEntry, colorSpacePrivate: privat, isAb);
1588 else if (a2bData.type == quint32(Tag::mAB_) || a2bData.type == quint32(Tag::mBA_))
1589 return parseMabData(data, tagEntry, colorSpacePrivate: privat, isAb);
1590
1591 qCWarning(lcIcc) << "fromIccProfile: Unknown A2B/B2A data type";
1592 return false;
1593}
1594
1595static bool parseDesc(const QByteArray &data, const TagEntry &tagEntry, QString &descName)
1596{
1597 const GenericTagData tag = qFromUnaligned<GenericTagData>(src: data.constData() + tagEntry.offset);
1598
1599 // Either 'desc' (ICCv2) or 'mluc' (ICCv4)
1600 if (tag.type == quint32(Tag::desc)) {
1601 if (tagEntry.size < sizeof(DescTagData))
1602 return false;
1603 Q_STATIC_ASSERT(sizeof(DescTagData) == 12);
1604 const DescTagData desc = qFromUnaligned<DescTagData>(src: data.constData() + tagEntry.offset);
1605 const quint32 len = desc.asciiDescriptionLength;
1606 if (len < 1)
1607 return false;
1608 if (tagEntry.size - 12 < len)
1609 return false;
1610 const char *asciiDescription = data.constData() + tagEntry.offset + sizeof(DescTagData);
1611 if (asciiDescription[len - 1] != '\0')
1612 return false;
1613 descName = QString::fromLatin1(str: asciiDescription, size: len - 1);
1614 return true;
1615 }
1616 if (tag.type != quint32(Tag::mluc))
1617 return false;
1618
1619 if (tagEntry.size < sizeof(MlucTagData))
1620 return false;
1621 const MlucTagData mluc = qFromUnaligned<MlucTagData>(src: data.constData() + tagEntry.offset);
1622 if (mluc.recordCount < 1)
1623 return false;
1624 if (mluc.recordSize != 12)
1625 return false;
1626 // We just use the primary record regardless of language or country.
1627 const quint32 stringOffset = mluc.records[0].offset;
1628 const quint32 stringSize = mluc.records[0].size;
1629 if (tagEntry.size < stringOffset || tagEntry.size - stringOffset < stringSize )
1630 return false;
1631 if ((stringSize | stringOffset) & 1)
1632 return false;
1633 quint32 stringLen = stringSize / 2;
1634 QVarLengthArray<char16_t> utf16hostendian(stringLen);
1635 qFromBigEndian<char16_t>(source: data.constData() + tagEntry.offset + stringOffset, count: stringLen,
1636 dest: utf16hostendian.data());
1637 // The given length shouldn't include 0-termination, but might.
1638 if (stringLen > 1 && utf16hostendian[stringLen - 1] == 0)
1639 --stringLen;
1640 descName = QString::fromUtf16(utf16hostendian.data(), size: stringLen);
1641 return true;
1642}
1643
1644static bool parseRgbMatrix(const QByteArray &data, const QHash<Tag, TagEntry> &tagIndex, QColorSpacePrivate *colorspaceDPtr)
1645{
1646 // Parse XYZ tags
1647 if (!parseXyzData(data, tagEntry: tagIndex[Tag::rXYZ], colorVector&: colorspaceDPtr->toXyz.r))
1648 return false;
1649 if (!parseXyzData(data, tagEntry: tagIndex[Tag::gXYZ], colorVector&: colorspaceDPtr->toXyz.g))
1650 return false;
1651 if (!parseXyzData(data, tagEntry: tagIndex[Tag::bXYZ], colorVector&: colorspaceDPtr->toXyz.b))
1652 return false;
1653 if (!parseXyzData(data, tagEntry: tagIndex[Tag::wtpt], colorVector&: colorspaceDPtr->whitePoint))
1654 return false;
1655 if (!colorspaceDPtr->toXyz.isValid() || !colorspaceDPtr->whitePoint.isValid() || colorspaceDPtr->whitePoint.isNull()) {
1656 qCWarning(lcIcc) << "Invalid XYZ values in RGB matrix";
1657 return false;
1658 }
1659
1660 colorspaceDPtr->primaries = QColorSpace::Primaries::Custom;
1661 if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromSRgb()) {
1662 qCDebug(lcIcc) << "fromIccProfile: sRGB primaries detected";
1663 colorspaceDPtr->primaries = QColorSpace::Primaries::SRgb;
1664 } else if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromAdobeRgb()) {
1665 qCDebug(lcIcc) << "fromIccProfile: Adobe RGB primaries detected";
1666 colorspaceDPtr->primaries = QColorSpace::Primaries::AdobeRgb;
1667 } else if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromDciP3D65()) {
1668 qCDebug(lcIcc) << "fromIccProfile: DCI-P3 D65 primaries detected";
1669 colorspaceDPtr->primaries = QColorSpace::Primaries::DciP3D65;
1670 } else if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromProPhotoRgb()) {
1671 qCDebug(lcIcc) << "fromIccProfile: ProPhoto RGB primaries detected";
1672 colorspaceDPtr->primaries = QColorSpace::Primaries::ProPhotoRgb;
1673 } else if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromBt2020()) {
1674 qCDebug(lcIcc) << "fromIccProfile: BT.2020 primaries detected";
1675 colorspaceDPtr->primaries = QColorSpace::Primaries::Bt2020;
1676 }
1677 return true;
1678}
1679
1680static bool parseGrayMatrix(const QByteArray &data, const QHash<Tag, TagEntry> &tagIndex, QColorSpacePrivate *colorspaceDPtr)
1681{
1682 QColorVector whitePoint;
1683 if (!parseXyzData(data, tagEntry: tagIndex[Tag::wtpt], colorVector&: whitePoint))
1684 return false;
1685 if (!whitePoint.isValid() || !qFuzzyCompare(p1: whitePoint.y, p2: 1.0f) || (1.0f + whitePoint.z + whitePoint.x) == 0.0f) {
1686 qCWarning(lcIcc) << "fromIccProfile: Invalid ICC profile - gray white-point not normalized";
1687 return false;
1688 }
1689 colorspaceDPtr->primaries = QColorSpace::Primaries::Custom;
1690 colorspaceDPtr->whitePoint = whitePoint;
1691 return true;
1692}
1693
1694static bool parseChad(const QByteArray &data, const TagEntry &tagEntry, QColorSpacePrivate *colorspaceDPtr)
1695{
1696 if (tagEntry.size < sizeof(Sf32TagData) || qsizetype(tagEntry.size) > data.size())
1697 return false;
1698 const Sf32TagData chadtag = qFromUnaligned<Sf32TagData>(src: data.constData() + tagEntry.offset);
1699 if (chadtag.type != uint32_t(Tag::sf32)) {
1700 qCWarning(lcIcc, "fromIccProfile: bad chad data type");
1701 return false;
1702 }
1703 QColorMatrix chad;
1704 chad.r.x = fromFixedS1516(x: chadtag.value[0]);
1705 chad.g.x = fromFixedS1516(x: chadtag.value[1]);
1706 chad.b.x = fromFixedS1516(x: chadtag.value[2]);
1707 chad.r.y = fromFixedS1516(x: chadtag.value[3]);
1708 chad.g.y = fromFixedS1516(x: chadtag.value[4]);
1709 chad.b.y = fromFixedS1516(x: chadtag.value[5]);
1710 chad.r.z = fromFixedS1516(x: chadtag.value[6]);
1711 chad.g.z = fromFixedS1516(x: chadtag.value[7]);
1712 chad.b.z = fromFixedS1516(x: chadtag.value[8]);
1713
1714 if (!chad.isValid()) {
1715 qCWarning(lcIcc, "fromIccProfile: invalid chad matrix");
1716 return false;
1717 }
1718 colorspaceDPtr->chad = chad;
1719 return true;
1720}
1721
1722static bool parseTRCs(const QByteArray &data, const QHash<Tag, TagEntry> &tagIndex, QColorSpacePrivate *colorspaceDPtr, bool isColorSpaceTypeGray)
1723{
1724 TagEntry rTrc;
1725 TagEntry gTrc;
1726 TagEntry bTrc;
1727 if (isColorSpaceTypeGray) {
1728 rTrc = tagIndex[Tag::kTRC];
1729 gTrc = tagIndex[Tag::kTRC];
1730 bTrc = tagIndex[Tag::kTRC];
1731 } else if (tagIndex.contains(key: Tag::aarg) && tagIndex.contains(key: Tag::aagg) && tagIndex.contains(key: Tag::aabg)) {
1732 // Apple extension for parametric version of TRCs in ICCv2:
1733 rTrc = tagIndex[Tag::aarg];
1734 gTrc = tagIndex[Tag::aagg];
1735 bTrc = tagIndex[Tag::aabg];
1736 } else {
1737 rTrc = tagIndex[Tag::rTRC];
1738 gTrc = tagIndex[Tag::gTRC];
1739 bTrc = tagIndex[Tag::bTRC];
1740 }
1741
1742 QColorTrc rCurve;
1743 QColorTrc gCurve;
1744 QColorTrc bCurve;
1745 if (!parseTRC(tagData: QByteArrayView(data).sliced(pos: rTrc.offset, n: rTrc.size), gamma&: rCurve, type: QColorTransferTable::TwoWay)) {
1746 qCWarning(lcIcc) << "fromIccProfile: Invalid rTRC";
1747 return false;
1748 }
1749 if (!parseTRC(tagData: QByteArrayView(data).sliced(pos: gTrc.offset, n: gTrc.size), gamma&: gCurve, type: QColorTransferTable::TwoWay)) {
1750 qCWarning(lcIcc) << "fromIccProfile: Invalid gTRC";
1751 return false;
1752 }
1753 if (!parseTRC(tagData: QByteArrayView(data).sliced(pos: bTrc.offset, n: bTrc.size), gamma&: bCurve, type: QColorTransferTable::TwoWay)) {
1754 qCWarning(lcIcc) << "fromIccProfile: Invalid bTRC";
1755 return false;
1756 }
1757 if (rCurve == gCurve && gCurve == bCurve) {
1758 if (rCurve.isIdentity()) {
1759 qCDebug(lcIcc) << "fromIccProfile: Linear gamma detected";
1760 colorspaceDPtr->trc[0] = QColorTransferFunction();
1761 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Linear;
1762 colorspaceDPtr->gamma = 1.0f;
1763 } else if (rCurve.m_type == QColorTrc::Type::ParameterizedFunction && rCurve.m_fun.isGamma()) {
1764 qCDebug(lcIcc) << "fromIccProfile: Simple gamma detected";
1765 colorspaceDPtr->trc[0] = QColorTransferFunction::fromGamma(gamma: rCurve.m_fun.m_g);
1766 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Gamma;
1767 colorspaceDPtr->gamma = rCurve.m_fun.m_g;
1768 } else if (rCurve.m_type == QColorTrc::Type::ParameterizedFunction && rCurve.m_fun.isSRgb()) {
1769 qCDebug(lcIcc) << "fromIccProfile: sRGB gamma detected";
1770 colorspaceDPtr->trc[0] = QColorTransferFunction::fromSRgb();
1771 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::SRgb;
1772 } else {
1773 colorspaceDPtr->trc[0] = rCurve;
1774 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Custom;
1775 }
1776 colorspaceDPtr->trc[1] = colorspaceDPtr->trc[0];
1777 colorspaceDPtr->trc[2] = colorspaceDPtr->trc[0];
1778 } else {
1779 colorspaceDPtr->trc[0] = rCurve;
1780 colorspaceDPtr->trc[1] = gCurve;
1781 colorspaceDPtr->trc[2] = bCurve;
1782 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Custom;
1783 }
1784 return true;
1785}
1786
1787static bool parseCicp(const QByteArray &data, const TagEntry &tagEntry, QColorSpacePrivate *colorspaceDPtr)
1788{
1789 if (colorspaceDPtr->isPcsLab)
1790 return false;
1791 if (tagEntry.size < sizeof(CicpTagData) || qsizetype(tagEntry.size) > data.size())
1792 return false;
1793 const CicpTagData cicp = qFromUnaligned<CicpTagData>(src: data.constData() + tagEntry.offset);
1794 if (cicp.type != uint32_t(Tag::cicp)) {
1795 qCWarning(lcIcc, "fromIccProfile: bad cicp data type");
1796 return false;
1797 }
1798 switch (cicp.transferCharacteristics) {
1799 case 4:
1800 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Gamma;
1801 colorspaceDPtr->gamma = 2.2f;
1802 break;
1803 case 5:
1804 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Gamma;
1805 colorspaceDPtr->gamma = 2.8f;
1806 break;
1807 case 8:
1808 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Linear;
1809 break;
1810 case 13:
1811 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::SRgb;
1812 break;
1813 case 1:
1814 case 6:
1815 case 14:
1816 case 15:
1817 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Bt2020;
1818 break;
1819 case 16:
1820 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::St2084;
1821 break;
1822 case 18:
1823 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Hlg;
1824 break;
1825 default:
1826 return false;
1827 }
1828 switch (cicp.colorPrimaries) {
1829 case 1:
1830 colorspaceDPtr->primaries = QColorSpace::Primaries::SRgb;
1831 break;
1832 case 9:
1833 colorspaceDPtr->primaries = QColorSpace::Primaries::Bt2020;
1834 break;
1835 case 12:
1836 colorspaceDPtr->primaries = QColorSpace::Primaries::DciP3D65;
1837 break;
1838 default:
1839 return false;
1840 }
1841 return true;
1842}
1843
1844bool fromIccProfile(const QByteArray &data, QColorSpace *colorSpace)
1845{
1846 if (data.size() < qsizetype(sizeof(ICCProfileHeader))) {
1847 qCWarning(lcIcc) << "fromIccProfile: failed size sanity 1";
1848 return false;
1849 }
1850 const ICCProfileHeader header = qFromUnaligned<ICCProfileHeader>(src: data.constData());
1851 if (!isValidIccProfile(header))
1852 return false; // if failed we already printing a warning
1853 if (qsizetype(header.profileSize) > data.size() || qsizetype(header.profileSize) < qsizetype(sizeof(ICCProfileHeader))) {
1854 qCWarning(lcIcc) << "fromIccProfile: failed size sanity 2";
1855 return false;
1856 }
1857
1858 const qsizetype offsetToData = sizeof(ICCProfileHeader) + header.tagCount * sizeof(TagTableEntry);
1859 Q_ASSERT(offsetToData > 0);
1860 if (offsetToData > data.size()) {
1861 qCWarning(lcIcc) << "fromIccProfile: failed index size sanity";
1862 return false;
1863 }
1864
1865 QHash<Tag, TagEntry> tagIndex;
1866 for (uint i = 0; i < header.tagCount; ++i) {
1867 // Read tag index
1868 const qsizetype tableOffset = sizeof(ICCProfileHeader) + i * sizeof(TagTableEntry);
1869 const TagTableEntry tagTable = qFromUnaligned<TagTableEntry>(src: data.constData()
1870 + tableOffset);
1871
1872 // Sanity check tag sizes and offsets:
1873 if (qsizetype(tagTable.offset) < offsetToData) {
1874 qCWarning(lcIcc) << "fromIccProfile: failed tag offset sanity 1";
1875 return false;
1876 }
1877 // Checked separately from (+ size) to handle overflow.
1878 if (tagTable.offset > header.profileSize) {
1879 qCWarning(lcIcc) << "fromIccProfile: failed tag offset sanity 2";
1880 return false;
1881 }
1882 if (tagTable.size < 8) {
1883 qCWarning(lcIcc) << "fromIccProfile: failed minimal tag size sanity";
1884 return false;
1885 }
1886 if (tagTable.size > header.profileSize - tagTable.offset) {
1887 qCWarning(lcIcc) << "fromIccProfile: failed tag offset + size sanity";
1888 return false;
1889 }
1890 if (tagTable.offset & 0x03) {
1891 qCWarning(lcIcc) << "fromIccProfile: invalid tag offset alignment";
1892 return false;
1893 }
1894// printf("'%4s' %d %d\n", (const char *)&tagTable.signature,
1895// quint32(tagTable.offset),
1896// quint32(tagTable.size));
1897 tagIndex.insert(key: Tag(quint32(tagTable.signature)), value: { .offset: tagTable.offset, .size: tagTable.size });
1898 }
1899
1900 bool threeComponentMatrix = true;
1901
1902 if (header.inputColorSpace == uint(ColorSpaceType::Rgb)) {
1903 // Check the profile is three-component matrix based:
1904 if (!tagIndex.contains(key: Tag::rXYZ) || !tagIndex.contains(key: Tag::gXYZ) || !tagIndex.contains(key: Tag::bXYZ) ||
1905 !tagIndex.contains(key: Tag::rTRC) || !tagIndex.contains(key: Tag::gTRC) || !tagIndex.contains(key: Tag::bTRC) ||
1906 !tagIndex.contains(key: Tag::wtpt) || header.pcs == uint(Tag::Lab_)) {
1907 threeComponentMatrix = false;
1908 // Check if the profile is valid n-LUT based:
1909 if (!tagIndex.contains(key: Tag::A2B0)) {
1910 qCWarning(lcIcc) << "fromIccProfile: Invalid ICC profile - neither valid three component nor n-LUT";
1911 return false;
1912 }
1913 }
1914 } else if (header.inputColorSpace == uint(ColorSpaceType::Gray)) {
1915 if (!tagIndex.contains(key: Tag::kTRC) || !tagIndex.contains(key: Tag::wtpt)) {
1916 qCWarning(lcIcc) << "fromIccProfile: Invalid ICC profile - not valid gray scale based";
1917 return false;
1918 }
1919 } else if (header.inputColorSpace == uint(ColorSpaceType::Cmyk)) {
1920 threeComponentMatrix = false;
1921 if (!tagIndex.contains(key: Tag::A2B0)) {
1922 qCWarning(lcIcc) << "fromIccProfile: Invalid ICC profile - CMYK, not n-LUT";
1923 return false;
1924 }
1925 } else {
1926 Q_UNREACHABLE();
1927 }
1928
1929 colorSpace->detach();
1930 QColorSpacePrivate *colorspaceDPtr = QColorSpacePrivate::get(colorSpace&: *colorSpace);
1931
1932 colorspaceDPtr->isPcsLab = (header.pcs == uint(Tag::Lab_));
1933 if (tagIndex.contains(key: Tag::cicp) && header.inputColorSpace == uint(ColorSpaceType::Rgb)) {
1934 // Let cicp override nLut profiles if we fully recognize it.
1935 if (parseCicp(data, tagEntry: tagIndex[Tag::cicp], colorspaceDPtr))
1936 threeComponentMatrix = true;
1937 if (colorspaceDPtr->primaries != QColorSpace::Primaries::Custom)
1938 colorspaceDPtr->setToXyzMatrix();
1939 if (colorspaceDPtr->transferFunction != QColorSpace::TransferFunction::Custom)
1940 colorspaceDPtr->setTransferFunction();
1941 }
1942
1943 if (threeComponentMatrix) {
1944 colorspaceDPtr->transformModel = QColorSpace::TransformModel::ThreeComponentMatrix;
1945
1946 if (header.inputColorSpace == uint(ColorSpaceType::Rgb)) {
1947 if (colorspaceDPtr->primaries == QColorSpace::Primaries::Custom && !parseRgbMatrix(data, tagIndex, colorspaceDPtr))
1948 return false;
1949 colorspaceDPtr->colorModel = QColorSpace::ColorModel::Rgb;
1950 } else if (header.inputColorSpace == uint(ColorSpaceType::Gray)) {
1951 if (!parseGrayMatrix(data, tagIndex, colorspaceDPtr))
1952 return false;
1953 colorspaceDPtr->colorModel = QColorSpace::ColorModel::Gray;
1954 } else {
1955 Q_UNREACHABLE();
1956 }
1957 if (auto it = tagIndex.constFind(key: Tag::chad); it != tagIndex.constEnd()) {
1958 if (!parseChad(data, tagEntry: it.value(), colorspaceDPtr))
1959 return false;
1960 } else {
1961 colorspaceDPtr->chad = QColorMatrix::chromaticAdaptation(whitePoint: colorspaceDPtr->whitePoint);
1962 }
1963 if (colorspaceDPtr->colorModel == QColorSpace::ColorModel::Gray)
1964 colorspaceDPtr->toXyz = colorspaceDPtr->chad;
1965
1966 // Reset the matrix to our canonical values:
1967 if (colorspaceDPtr->primaries != QColorSpace::Primaries::Custom)
1968 colorspaceDPtr->setToXyzMatrix();
1969
1970 if (colorspaceDPtr->transferFunction == QColorSpace::TransferFunction::Custom &&
1971 !parseTRCs(data, tagIndex, colorspaceDPtr, isColorSpaceTypeGray: header.inputColorSpace == uint(ColorSpaceType::Gray)))
1972 return false;
1973 } else {
1974 colorspaceDPtr->transformModel = QColorSpace::TransformModel::ElementListProcessing;
1975 if (header.inputColorSpace == uint(ColorSpaceType::Cmyk))
1976 colorspaceDPtr->colorModel = QColorSpace::ColorModel::Cmyk;
1977 else
1978 colorspaceDPtr->colorModel = QColorSpace::ColorModel::Rgb;
1979
1980 // Only parse the default perceptual transform for now
1981 if (!parseA2B(data, tagEntry: tagIndex[Tag::A2B0], privat: colorspaceDPtr, isAb: true))
1982 return false;
1983 if (auto it = tagIndex.constFind(key: Tag::B2A0); it != tagIndex.constEnd()) {
1984 if (!parseA2B(data, tagEntry: it.value(), privat: colorspaceDPtr, isAb: false))
1985 return false;
1986 }
1987
1988 if (auto it = tagIndex.constFind(key: Tag::wtpt); it != tagIndex.constEnd()) {
1989 if (!parseXyzData(data, tagEntry: it.value(), colorVector&: colorspaceDPtr->whitePoint))
1990 return false;
1991 }
1992 if (auto it = tagIndex.constFind(key: Tag::chad); it != tagIndex.constEnd()) {
1993 if (!parseChad(data, tagEntry: it.value(), colorspaceDPtr))
1994 return false;
1995 } else if (!colorspaceDPtr->whitePoint.isNull()) {
1996 colorspaceDPtr->chad = QColorMatrix::chromaticAdaptation(whitePoint: colorspaceDPtr->whitePoint);
1997 }
1998 }
1999
2000 if (auto it = tagIndex.constFind(key: Tag::desc); it != tagIndex.constEnd()) {
2001 if (!parseDesc(data, tagEntry: it.value(), descName&: colorspaceDPtr->description))
2002 qCWarning(lcIcc) << "fromIccProfile: Failed to parse description";
2003 else
2004 qCDebug(lcIcc) << "fromIccProfile: Description" << colorspaceDPtr->description;
2005 }
2006
2007 colorspaceDPtr->identifyColorSpace();
2008 if (colorspaceDPtr->namedColorSpace)
2009 qCDebug(lcIcc) << "fromIccProfile: Named colorspace detected: " << QColorSpace::NamedColorSpace(colorspaceDPtr->namedColorSpace);
2010
2011 colorspaceDPtr->iccProfile = data;
2012
2013 Q_ASSERT(colorspaceDPtr->isValid());
2014 return true;
2015}
2016
2017} // namespace QIcc
2018
2019QT_END_NAMESPACE
2020

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

source code of qtbase/src/gui/painting/qicc.cpp