1/****************************************************************************
2**
3** Copyright (C) 2020 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the QtGui module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qicc_p.h"
41
42#include <qbuffer.h>
43#include <qbytearray.h>
44#include <qdatastream.h>
45#include <qendian.h>
46#include <qloggingcategory.h>
47#include <qstring.h>
48
49#include "qcolorspace_p.h"
50#include "qcolortrc_p.h"
51
52#include <array>
53
54QT_BEGIN_NAMESPACE
55Q_LOGGING_CATEGORY(lcIcc, "qt.gui.icc")
56
57struct ICCProfileHeader
58{
59 quint32_be profileSize;
60
61 quint32_be preferredCmmType;
62
63 quint32_be profileVersion;
64 quint32_be profileClass;
65 quint32_be inputColorSpace;
66 quint32_be pcs;
67 quint32_be datetime[3];
68 quint32_be signature;
69 quint32_be platformSignature;
70 quint32_be flags;
71 quint32_be deviceManufacturer;
72 quint32_be deviceModel;
73 quint32_be deviceAttributes[2];
74
75 quint32_be renderingIntent;
76 qint32_be illuminantXyz[3];
77
78 quint32_be creatorSignature;
79 quint32_be profileId[4];
80
81 quint32_be reserved[7];
82
83// Technically after the header, but easier to include here:
84 quint32_be tagCount;
85};
86
87constexpr quint32 IccTag(uchar a, uchar b, uchar c, uchar d)
88{
89 return (a << 24) | (b << 16) | (c << 8) | d;
90}
91
92enum class ColorSpaceType : quint32 {
93 Rgb = IccTag('R', 'G', 'B', ' '),
94 Gray = IccTag('G', 'R', 'A', 'Y'),
95};
96
97enum class ProfileClass : quint32 {
98 Input = IccTag('s', 'c', 'r', 'n'),
99 Display = IccTag('m', 'n', 't', 'r'),
100 // Not supported:
101 Output = IccTag('p', 'r', 't', 'r'),
102 ColorSpace = IccTag('s', 'p', 'a', 'c'),
103};
104
105enum class Tag : quint32 {
106 acsp = IccTag('a', 'c', 's', 'p'),
107 RGB_ = IccTag('R', 'G', 'B', ' '),
108 XYZ_ = IccTag('X', 'Y', 'Z', ' '),
109 rXYZ = IccTag('r', 'X', 'Y', 'Z'),
110 gXYZ = IccTag('g', 'X', 'Y', 'Z'),
111 bXYZ = IccTag('b', 'X', 'Y', 'Z'),
112 rTRC = IccTag('r', 'T', 'R', 'C'),
113 gTRC = IccTag('g', 'T', 'R', 'C'),
114 bTRC = IccTag('b', 'T', 'R', 'C'),
115 kTRC = IccTag('k', 'T', 'R', 'C'),
116 A2B0 = IccTag('A', '2', 'B', '0'),
117 A2B1 = IccTag('A', '2', 'B', '1'),
118 B2A0 = IccTag('B', '2', 'A', '0'),
119 B2A1 = IccTag('B', '2', 'A', '1'),
120 desc = IccTag('d', 'e', 's', 'c'),
121 text = IccTag('t', 'e', 'x', 't'),
122 cprt = IccTag('c', 'p', 'r', 't'),
123 curv = IccTag('c', 'u', 'r', 'v'),
124 para = IccTag('p', 'a', 'r', 'a'),
125 wtpt = IccTag('w', 't', 'p', 't'),
126 bkpt = IccTag('b', 'k', 'p', 't'),
127 mft1 = IccTag('m', 'f', 't', '1'),
128 mft2 = IccTag('m', 'f', 't', '2'),
129 mluc = IccTag('m', 'l', 'u', 'c'),
130 mAB_ = IccTag('m', 'A', 'B', ' '),
131 mBA_ = IccTag('m', 'B', 'A', ' '),
132 chad = IccTag('c', 'h', 'a', 'd'),
133 sf32 = IccTag('s', 'f', '3', '2'),
134
135 // Apple extensions for ICCv2:
136 aarg = IccTag('a', 'a', 'r', 'g'),
137 aagg = IccTag('a', 'a', 'g', 'g'),
138 aabg = IccTag('a', 'a', 'b', 'g'),
139};
140
141inline size_t qHash(const Tag &key, size_t seed = 0)
142{
143 return qHash(quint32(key), seed);
144}
145
146namespace QIcc {
147
148struct TagTableEntry
149{
150 quint32_be signature;
151 quint32_be offset;
152 quint32_be size;
153};
154
155struct GenericTagData {
156 quint32_be type;
157 quint32_be null;
158};
159
160struct XYZTagData : GenericTagData {
161 qint32_be fixedX;
162 qint32_be fixedY;
163 qint32_be fixedZ;
164};
165
166struct CurvTagData : GenericTagData {
167 quint32_be valueCount;
168 quint16_be value[1];
169};
170
171struct ParaTagData : GenericTagData {
172 quint16_be curveType;
173 quint16_be null2;
174 quint32_be parameter[1];
175};
176
177struct DescTagData : GenericTagData {
178 quint32_be asciiDescriptionLength;
179 char asciiDescription[1];
180 // .. we ignore the rest
181};
182
183struct MlucTagRecord {
184 quint16_be languageCode;
185 quint16_be countryCode;
186 quint32_be size;
187 quint32_be offset;
188};
189
190struct MlucTagData : GenericTagData {
191 quint32_be recordCount;
192 quint32_be recordSize; // = sizeof(MlucTagRecord)
193 MlucTagRecord records[1];
194};
195
196// For both mAB and mBA
197struct mABTagData : GenericTagData {
198 quint8 inputChannels;
199 quint8 outputChannels;
200 quint8 padding[2];
201 quint32_be bCurvesOffset;
202 quint32_be matrixOffset;
203 quint32_be mCurvesOffset;
204 quint32_be clutOffset;
205 quint32_be aCurvesOffset;
206};
207
208struct Sf32TagData : GenericTagData {
209 quint32_be value[1];
210};
211
212static int toFixedS1516(float x)
213{
214 return int(x * 65536.0f + 0.5f);
215}
216
217static float fromFixedS1516(int x)
218{
219 return x * (1.0f / 65536.0f);
220}
221
222static bool isValidIccProfile(const ICCProfileHeader &header)
223{
224 if (header.signature != uint(Tag::acsp)) {
225 qCWarning(lcIcc, "Failed ICC signature test");
226 return false;
227 }
228
229 // Don't overflow 32bit integers:
230 if (header.tagCount >= (INT32_MAX - sizeof(ICCProfileHeader)) / sizeof(TagTableEntry)) {
231 qCWarning(lcIcc, "Failed tag count sanity");
232 return false;
233 }
234 if (header.profileSize - sizeof(ICCProfileHeader) < header.tagCount * sizeof(TagTableEntry)) {
235 qCWarning(lcIcc, "Failed basic size sanity");
236 return false;
237 }
238
239 if (header.profileClass != uint(ProfileClass::Input)
240 && header.profileClass != uint(ProfileClass::Display)) {
241 qCWarning(lcIcc, "Unsupported ICC profile class %x", quint32(header.profileClass));
242 return false;
243 }
244 if (header.inputColorSpace != uint(ColorSpaceType::Rgb)
245 && header.inputColorSpace != uint(ColorSpaceType::Gray)) {
246 qCWarning(lcIcc, "Unsupported ICC input color space %x", quint32(header.inputColorSpace));
247 return false;
248 }
249 if (header.pcs != 0x58595a20 /* 'XYZ '*/) {
250 // ### support PCSLAB
251 qCWarning(lcIcc, "Unsupported ICC profile connection space %x", quint32(header.pcs));
252 return false;
253 }
254
255 QColorVector illuminant;
256 illuminant.x = fromFixedS1516(header.illuminantXyz[0]);
257 illuminant.y = fromFixedS1516(header.illuminantXyz[1]);
258 illuminant.z = fromFixedS1516(header.illuminantXyz[2]);
259 if (illuminant != QColorVector::D50()) {
260 qCWarning(lcIcc, "Invalid ICC illuminant");
261 return false;
262 }
263
264 return true;
265}
266
267static int writeColorTrc(QDataStream &stream, const QColorTrc &trc)
268{
269 if (trc.isLinear()) {
270 stream << uint(Tag::curv) << uint(0);
271 stream << uint(0);
272 return 12;
273 }
274
275 if (trc.m_type == QColorTrc::Type::Function) {
276 const QColorTransferFunction &fun = trc.m_fun;
277 stream << uint(Tag::para) << uint(0);
278 if (fun.isGamma()) {
279 stream << ushort(0) << ushort(0);
280 stream << toFixedS1516(fun.m_g);
281 return 12 + 4;
282 }
283 bool type3 = qFuzzyIsNull(fun.m_e) && qFuzzyIsNull(fun.m_f);
284 stream << ushort(type3 ? 3 : 4) << ushort(0);
285 stream << toFixedS1516(fun.m_g);
286 stream << toFixedS1516(fun.m_a);
287 stream << toFixedS1516(fun.m_b);
288 stream << toFixedS1516(fun.m_c);
289 stream << toFixedS1516(fun.m_d);
290 if (type3)
291 return 12 + 5 * 4;
292 stream << toFixedS1516(fun.m_e);
293 stream << toFixedS1516(fun.m_f);
294 return 12 + 7 * 4;
295 }
296
297 Q_ASSERT(trc.m_type == QColorTrc::Type::Table);
298 stream << uint(Tag::curv) << uint(0);
299 stream << uint(trc.m_table.m_tableSize);
300 if (!trc.m_table.m_table16.isEmpty()) {
301 for (uint i = 0; i < trc.m_table.m_tableSize; ++i) {
302 stream << ushort(trc.m_table.m_table16[i]);
303 }
304 } else {
305 for (uint i = 0; i < trc.m_table.m_tableSize; ++i) {
306 stream << ushort(trc.m_table.m_table8[i] * 257U);
307 }
308 }
309 return 12 + 2 * trc.m_table.m_tableSize;
310}
311
312QByteArray toIccProfile(const QColorSpace &space)
313{
314 if (!space.isValid())
315 return QByteArray();
316
317 const QColorSpacePrivate *spaceDPtr = QColorSpacePrivate::get(space);
318
319 constexpr int tagCount = 9;
320 constexpr uint profileDataOffset = 128 + 4 + 12 * tagCount;
321 constexpr uint variableTagTableOffsets = 128 + 4 + 12 * 5;
322 uint currentOffset = 0;
323 uint rTrcOffset, gTrcOffset, bTrcOffset;
324 uint rTrcSize, gTrcSize, bTrcSize;
325 uint descOffset, descSize;
326
327 QBuffer buffer;
328 buffer.open(QIODevice::WriteOnly);
329 QDataStream stream(&buffer);
330
331 // Profile header:
332 stream << uint(0); // Size, we will update this later
333 stream << uint(0);
334 stream << uint(0x02400000); // Version 2.4 (note we use 'para' from version 4)
335 stream << uint(ProfileClass::Display);
336 stream << uint(Tag::RGB_);
337 stream << uint(Tag::XYZ_);
338 stream << uint(0) << uint(0) << uint(0);
339 stream << uint(Tag::acsp);
340 stream << uint(0) << uint(0) << uint(0);
341 stream << uint(0) << uint(0) << uint(0);
342 stream << uint(1); // Rendering intent
343 stream << uint(0x0000f6d6); // D50 X
344 stream << uint(0x00010000); // D50 Y
345 stream << uint(0x0000d32d); // D50 Z
346 stream << IccTag('Q','t', QT_VERSION_MAJOR, QT_VERSION_MINOR);
347 stream << uint(0) << uint(0) << uint(0) << uint(0);
348 stream << uint(0) << uint(0) << uint(0) << uint(0) << uint(0) << uint(0) << uint(0);
349
350 // Tag table:
351 stream << uint(tagCount);
352 stream << uint(Tag::rXYZ) << uint(profileDataOffset + 00) << uint(20);
353 stream << uint(Tag::gXYZ) << uint(profileDataOffset + 20) << uint(20);
354 stream << uint(Tag::bXYZ) << uint(profileDataOffset + 40) << uint(20);
355 stream << uint(Tag::wtpt) << uint(profileDataOffset + 60) << uint(20);
356 stream << uint(Tag::cprt) << uint(profileDataOffset + 80) << uint(12);
357 // From here the offset and size will be updated later:
358 stream << uint(Tag::rTRC) << uint(0) << uint(0);
359 stream << uint(Tag::gTRC) << uint(0) << uint(0);
360 stream << uint(Tag::bTRC) << uint(0) << uint(0);
361 stream << uint(Tag::desc) << uint(0) << uint(0);
362 // TODO: consider adding 'chad' tag (required in ICC >=4 when we have non-D50 whitepoint)
363 currentOffset = profileDataOffset;
364
365 // Tag data:
366 stream << uint(Tag::XYZ_) << uint(0);
367 stream << toFixedS1516(spaceDPtr->toXyz.r.x);
368 stream << toFixedS1516(spaceDPtr->toXyz.r.y);
369 stream << toFixedS1516(spaceDPtr->toXyz.r.z);
370 stream << uint(Tag::XYZ_) << uint(0);
371 stream << toFixedS1516(spaceDPtr->toXyz.g.x);
372 stream << toFixedS1516(spaceDPtr->toXyz.g.y);
373 stream << toFixedS1516(spaceDPtr->toXyz.g.z);
374 stream << uint(Tag::XYZ_) << uint(0);
375 stream << toFixedS1516(spaceDPtr->toXyz.b.x);
376 stream << toFixedS1516(spaceDPtr->toXyz.b.y);
377 stream << toFixedS1516(spaceDPtr->toXyz.b.z);
378 stream << uint(Tag::XYZ_) << uint(0);
379 stream << toFixedS1516(spaceDPtr->whitePoint.x);
380 stream << toFixedS1516(spaceDPtr->whitePoint.y);
381 stream << toFixedS1516(spaceDPtr->whitePoint.z);
382 stream << uint(Tag::text) << uint(0);
383 stream << uint(IccTag('N', '/', 'A', '\0'));
384 currentOffset += 92;
385
386 // From now on the data is variable sized:
387 rTrcOffset = currentOffset;
388 rTrcSize = writeColorTrc(stream, spaceDPtr->trc[0]);
389 currentOffset += rTrcSize;
390 if (spaceDPtr->trc[0] == spaceDPtr->trc[1]) {
391 gTrcOffset = rTrcOffset;
392 gTrcSize = rTrcSize;
393 } else {
394 gTrcOffset = currentOffset;
395 gTrcSize = writeColorTrc(stream, spaceDPtr->trc[1]);
396 currentOffset += gTrcSize;
397 }
398 if (spaceDPtr->trc[0] == spaceDPtr->trc[2]) {
399 bTrcOffset = rTrcOffset;
400 bTrcSize = rTrcSize;
401 } else {
402 bTrcOffset = currentOffset;
403 bTrcSize = writeColorTrc(stream, spaceDPtr->trc[2]);
404 currentOffset += bTrcSize;
405 }
406
407 descOffset = currentOffset;
408 QByteArray description = spaceDPtr->description.toUtf8();
409 stream << uint(Tag::desc) << uint(0);
410 stream << uint(description.size() + 1);
411 stream.writeRawData(description.constData(), description.size() + 1);
412 stream << uint(0) << uint(0);
413 stream << ushort(0) << uchar(0);
414 QByteArray macdesc(67, '\0');
415 stream.writeRawData(macdesc.constData(), 67);
416 descSize = 90 + description.size() + 1;
417 currentOffset += descSize;
418
419 buffer.close();
420 QByteArray iccProfile = buffer.buffer();
421 // Now write final size
422 *(quint32_be *)iccProfile.data() = iccProfile.size();
423 // And the final indices and sizes of variable size tags:
424 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 4) = rTrcOffset;
425 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 8) = rTrcSize;
426 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 12 + 4) = gTrcOffset;
427 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 12 + 8) = gTrcSize;
428 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 2 * 12 + 4) = bTrcOffset;
429 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 2 * 12 + 8) = bTrcSize;
430 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 3 * 12 + 4) = descOffset;
431 *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 3 * 12 + 8) = descSize;
432
433#if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS)
434 const ICCProfileHeader *iccHeader = (const ICCProfileHeader *)iccProfile.constData();
435 Q_ASSERT(qsizetype(iccHeader->profileSize) == qsizetype(iccProfile.size()));
436 Q_ASSERT(isValidIccProfile(*iccHeader));
437#endif
438
439 return iccProfile;
440}
441
442struct TagEntry {
443 quint32 offset;
444 quint32 size;
445};
446
447bool parseXyzData(const QByteArray &data, const TagEntry &tagEntry, QColorVector &colorVector)
448{
449 if (tagEntry.size < sizeof(XYZTagData)) {
450 qCWarning(lcIcc) << "Undersized XYZ tag";
451 return false;
452 }
453 const XYZTagData xyz = qFromUnaligned<XYZTagData>(data.constData() + tagEntry.offset);
454 if (xyz.type != quint32(Tag::XYZ_)) {
455 qCWarning(lcIcc) << "Bad XYZ content type";
456 return false;
457 }
458 const float x = fromFixedS1516(xyz.fixedX);
459 const float y = fromFixedS1516(xyz.fixedY);
460 const float z = fromFixedS1516(xyz.fixedZ);
461
462 colorVector = QColorVector(x, y, z);
463 return true;
464}
465
466bool parseTRC(const QByteArray &data, const TagEntry &tagEntry, QColorTrc &gamma)
467{
468 const GenericTagData trcData = qFromUnaligned<GenericTagData>(data.constData()
469 + tagEntry.offset);
470 if (trcData.type == quint32(Tag::curv)) {
471 const CurvTagData curv = qFromUnaligned<CurvTagData>(data.constData() + tagEntry.offset);
472 if (curv.valueCount > (1 << 16))
473 return false;
474 if (tagEntry.size - 12 < 2 * curv.valueCount)
475 return false;
476 if (curv.valueCount == 0) {
477 gamma.m_type = QColorTrc::Type::Function;
478 gamma.m_fun = QColorTransferFunction(); // Linear
479 } else if (curv.valueCount == 1) {
480 float g = curv.value[0] * (1.0f / 256.0f);
481 gamma.m_type = QColorTrc::Type::Function;
482 gamma.m_fun = QColorTransferFunction::fromGamma(g);
483 } else {
484 QList<quint16> tabl;
485 tabl.resize(curv.valueCount);
486 static_assert(sizeof(GenericTagData) == 2 * sizeof(quint32_be),
487 "GenericTagData has padding. The following code is a subject to UB.");
488 const auto offset = tagEntry.offset + sizeof(GenericTagData) + sizeof(quint32_be);
489 qFromBigEndian<quint16>(data.constData() + offset, curv.valueCount, tabl.data());
490 QColorTransferTable table = QColorTransferTable(curv.valueCount, std::move(tabl));
491 QColorTransferFunction curve;
492 if (!table.checkValidity()) {
493 qCWarning(lcIcc) << "Invalid curv table";
494 return false;
495 } else if (!table.asColorTransferFunction(&curve)) {
496 gamma.m_type = QColorTrc::Type::Table;
497 gamma.m_table = table;
498 } else {
499 qCDebug(lcIcc) << "Detected curv table as function";
500 gamma.m_type = QColorTrc::Type::Function;
501 gamma.m_fun = curve;
502 }
503 }
504 return true;
505 }
506 if (trcData.type == quint32(Tag::para)) {
507 if (tagEntry.size < sizeof(ParaTagData))
508 return false;
509 static_assert(sizeof(GenericTagData) == 2 * sizeof(quint32_be),
510 "GenericTagData has padding. The following code is a subject to UB.");
511 const ParaTagData para = qFromUnaligned<ParaTagData>(data.constData() + tagEntry.offset);
512 // re-read first parameter for consistency:
513 const auto parametersOffset = tagEntry.offset + sizeof(GenericTagData)
514 + 2 * sizeof(quint16_be);
515 switch (para.curveType) {
516 case 0: {
517 float g = fromFixedS1516(para.parameter[0]);
518 gamma.m_type = QColorTrc::Type::Function;
519 gamma.m_fun = QColorTransferFunction::fromGamma(g);
520 break;
521 }
522 case 1: {
523 if (tagEntry.size < sizeof(ParaTagData) + 2 * 4)
524 return false;
525 std::array<quint32_be, 3> parameters =
526 qFromUnaligned<decltype(parameters)>(data.constData() + parametersOffset);
527 float g = fromFixedS1516(parameters[0]);
528 float a = fromFixedS1516(parameters[1]);
529 float b = fromFixedS1516(parameters[2]);
530 float d = -b / a;
531 gamma.m_type = QColorTrc::Type::Function;
532 gamma.m_fun = QColorTransferFunction(a, b, 0.0f, d, 0.0f, 0.0f, g);
533 break;
534 }
535 case 2: {
536 if (tagEntry.size < sizeof(ParaTagData) + 3 * 4)
537 return false;
538 std::array<quint32_be, 4> parameters =
539 qFromUnaligned<decltype(parameters)>(data.constData() + parametersOffset);
540 float g = fromFixedS1516(parameters[0]);
541 float a = fromFixedS1516(parameters[1]);
542 float b = fromFixedS1516(parameters[2]);
543 float c = fromFixedS1516(parameters[3]);
544 float d = -b / a;
545 gamma.m_type = QColorTrc::Type::Function;
546 gamma.m_fun = QColorTransferFunction(a, b, 0.0f, d, c, c, g);
547 break;
548 }
549 case 3: {
550 if (tagEntry.size < sizeof(ParaTagData) + 4 * 4)
551 return false;
552 std::array<quint32_be, 5> parameters =
553 qFromUnaligned<decltype(parameters)>(data.constData() + parametersOffset);
554 float g = fromFixedS1516(parameters[0]);
555 float a = fromFixedS1516(parameters[1]);
556 float b = fromFixedS1516(parameters[2]);
557 float c = fromFixedS1516(parameters[3]);
558 float d = fromFixedS1516(parameters[4]);
559 gamma.m_type = QColorTrc::Type::Function;
560 gamma.m_fun = QColorTransferFunction(a, b, c, d, 0.0f, 0.0f, g);
561 break;
562 }
563 case 4: {
564 if (tagEntry.size < sizeof(ParaTagData) + 6 * 4)
565 return false;
566 std::array<quint32_be, 7> parameters =
567 qFromUnaligned<decltype(parameters)>(data.constData() + parametersOffset);
568 float g = fromFixedS1516(parameters[0]);
569 float a = fromFixedS1516(parameters[1]);
570 float b = fromFixedS1516(parameters[2]);
571 float c = fromFixedS1516(parameters[3]);
572 float d = fromFixedS1516(parameters[4]);
573 float e = fromFixedS1516(parameters[5]);
574 float f = fromFixedS1516(parameters[6]);
575 gamma.m_type = QColorTrc::Type::Function;
576 gamma.m_fun = QColorTransferFunction(a, b, c, d, e, f, g);
577 break;
578 }
579 default:
580 qCWarning(lcIcc) << "Unknown para type" << uint(para.curveType);
581 return false;
582 }
583 return true;
584 }
585 qCWarning(lcIcc) << "Invalid TRC data type";
586 return false;
587}
588
589bool parseDesc(const QByteArray &data, const TagEntry &tagEntry, QString &descName)
590{
591 const GenericTagData tag = qFromUnaligned<GenericTagData>(data.constData() + tagEntry.offset);
592
593 // Either 'desc' (ICCv2) or 'mluc' (ICCv4)
594 if (tag.type == quint32(Tag::desc)) {
595 if (tagEntry.size < sizeof(DescTagData))
596 return false;
597 const DescTagData desc = qFromUnaligned<DescTagData>(data.constData() + tagEntry.offset);
598 const quint32 len = desc.asciiDescriptionLength;
599 if (len < 1)
600 return false;
601 if (tagEntry.size - 12 < len)
602 return false;
603 static_assert(sizeof(GenericTagData) == 2 * sizeof(quint32_be),
604 "GenericTagData has padding. The following code is a subject to UB.");
605 const char *asciiDescription = data.constData() + tagEntry.offset + sizeof(GenericTagData)
606 + sizeof(quint32_be);
607 if (asciiDescription[len - 1] != '\0')
608 return false;
609 descName = QString::fromLatin1(asciiDescription, len - 1);
610 return true;
611 }
612 if (tag.type != quint32(Tag::mluc))
613 return false;
614
615 if (tagEntry.size < sizeof(MlucTagData))
616 return false;
617 const MlucTagData mluc = qFromUnaligned<MlucTagData>(data.constData() + tagEntry.offset);
618 if (mluc.recordCount < 1)
619 return false;
620 if (mluc.recordSize < 12)
621 return false;
622 // We just use the primary record regardless of language or country.
623 const quint32 stringOffset = mluc.records[0].offset;
624 const quint32 stringSize = mluc.records[0].size;
625 if (tagEntry.size < stringOffset || tagEntry.size - stringOffset < stringSize )
626 return false;
627 if ((stringSize | stringOffset) & 1)
628 return false;
629 quint32 stringLen = stringSize / 2;
630 QVarLengthArray<char16_t> utf16hostendian(stringLen);
631 qFromBigEndian<char16_t>(data.constData() + tagEntry.offset + stringOffset, stringLen,
632 utf16hostendian.data());
633 // The given length shouldn't include 0-termination, but might.
634 if (stringLen > 1 && utf16hostendian[stringLen - 1] == 0)
635 --stringLen;
636 descName = QString::fromUtf16(utf16hostendian.data(), stringLen);
637 return true;
638}
639
640bool fromIccProfile(const QByteArray &data, QColorSpace *colorSpace)
641{
642 if (data.size() < qsizetype(sizeof(ICCProfileHeader))) {
643 qCWarning(lcIcc) << "fromIccProfile: failed size sanity 1";
644 return false;
645 }
646 const ICCProfileHeader header = qFromUnaligned<ICCProfileHeader>(data.constData());
647 if (!isValidIccProfile(header))
648 return false; // if failed we already printing a warning
649 if (qsizetype(header.profileSize) > data.size()) {
650 qCWarning(lcIcc) << "fromIccProfile: failed size sanity 2";
651 return false;
652 }
653
654 const qsizetype offsetToData = sizeof(ICCProfileHeader) + header.tagCount * sizeof(TagTableEntry);
655 Q_ASSERT(offsetToData > 0);
656 if (offsetToData > data.size()) {
657 qCWarning(lcIcc) << "fromIccProfile: failed index size sanity";
658 return false;
659 }
660
661 QHash<Tag, TagEntry> tagIndex;
662 for (uint i = 0; i < header.tagCount; ++i) {
663 // Read tag index
664 const qsizetype tableOffset = sizeof(ICCProfileHeader) + i * sizeof(TagTableEntry);
665 const TagTableEntry tagTable = qFromUnaligned<TagTableEntry>(data.constData()
666 + tableOffset);
667
668 // Sanity check tag sizes and offsets:
669 if (qsizetype(tagTable.offset) < offsetToData) {
670 qCWarning(lcIcc) << "fromIccProfile: failed tag offset sanity 1";
671 return false;
672 }
673 // Checked separately from (+ size) to handle overflow.
674 if (tagTable.offset > header.profileSize) {
675 qCWarning(lcIcc) << "fromIccProfile: failed tag offset sanity 2";
676 return false;
677 }
678 if (tagTable.size < 12) {
679 qCWarning(lcIcc) << "fromIccProfile: failed minimal tag size sanity";
680 return false;
681 }
682 if (tagTable.size > header.profileSize - tagTable.offset) {
683 qCWarning(lcIcc) << "fromIccProfile: failed tag offset + size sanity";
684 return false;
685 }
686 if (tagTable.offset & 0x03) {
687 qCWarning(lcIcc) << "fromIccProfile: invalid tag offset alignment";
688 return false;
689 }
690// printf("'%4s' %d %d\n", (const char *)&tagTable.signature,
691// quint32(tagTable.offset),
692// quint32(tagTable.size));
693 tagIndex.insert(Tag(quint32(tagTable.signature)), { tagTable.offset, tagTable.size });
694 }
695
696 // Check the profile is three-component matrix based (what we currently support):
697 if (header.inputColorSpace == uint(ColorSpaceType::Rgb)) {
698 if (!tagIndex.contains(Tag::rXYZ) || !tagIndex.contains(Tag::gXYZ) || !tagIndex.contains(Tag::bXYZ) ||
699 !tagIndex.contains(Tag::rTRC) || !tagIndex.contains(Tag::gTRC) || !tagIndex.contains(Tag::bTRC) ||
700 !tagIndex.contains(Tag::wtpt)) {
701 qCWarning(lcIcc) << "fromIccProfile: Unsupported ICC profile - not three component matrix based";
702 return false;
703 }
704 } else {
705 Q_ASSERT(header.inputColorSpace == uint(ColorSpaceType::Gray));
706 if (!tagIndex.contains(Tag::kTRC) || !tagIndex.contains(Tag::wtpt)) {
707 qCWarning(lcIcc) << "fromIccProfile: Invalid ICC profile - not valid gray scale based";
708 return false;
709 }
710 }
711
712 colorSpace->detach();
713 QColorSpacePrivate *colorspaceDPtr = QColorSpacePrivate::get(*colorSpace);
714
715 if (header.inputColorSpace == uint(ColorSpaceType::Rgb)) {
716 // Parse XYZ tags
717 if (!parseXyzData(data, tagIndex[Tag::rXYZ], colorspaceDPtr->toXyz.r))
718 return false;
719 if (!parseXyzData(data, tagIndex[Tag::gXYZ], colorspaceDPtr->toXyz.g))
720 return false;
721 if (!parseXyzData(data, tagIndex[Tag::bXYZ], colorspaceDPtr->toXyz.b))
722 return false;
723 if (!parseXyzData(data, tagIndex[Tag::wtpt], colorspaceDPtr->whitePoint))
724 return false;
725
726 colorspaceDPtr->primaries = QColorSpace::Primaries::Custom;
727 if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromSRgb()) {
728 qCDebug(lcIcc) << "fromIccProfile: sRGB primaries detected";
729 colorspaceDPtr->primaries = QColorSpace::Primaries::SRgb;
730 } else if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromAdobeRgb()) {
731 qCDebug(lcIcc) << "fromIccProfile: Adobe RGB primaries detected";
732 colorspaceDPtr->primaries = QColorSpace::Primaries::AdobeRgb;
733 } else if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromDciP3D65()) {
734 qCDebug(lcIcc) << "fromIccProfile: DCI-P3 D65 primaries detected";
735 colorspaceDPtr->primaries = QColorSpace::Primaries::DciP3D65;
736 }
737 if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromProPhotoRgb()) {
738 qCDebug(lcIcc) << "fromIccProfile: ProPhoto RGB primaries detected";
739 colorspaceDPtr->primaries = QColorSpace::Primaries::ProPhotoRgb;
740 }
741 } else {
742 // We will use sRGB primaries and fit to match the given white-point if
743 // it doesn't match sRGB's.
744 QColorVector whitePoint;
745 if (!parseXyzData(data, tagIndex[Tag::wtpt], whitePoint))
746 return false;
747 if (!qFuzzyCompare(whitePoint.y, 1.0f) || (1.0f + whitePoint.z - whitePoint.x) == 0.0f) {
748 qCWarning(lcIcc) << "fromIccProfile: Invalid ICC profile - gray white-point not normalized";
749 return false;
750 }
751 if (whitePoint == QColorVector::D65()) {
752 colorspaceDPtr->primaries = QColorSpace::Primaries::SRgb;
753 } else {
754 colorspaceDPtr->primaries = QColorSpace::Primaries::Custom;
755 // Calculate chromaticity from xyz (assuming y == 1.0f).
756 float y = 1.0f / (1.0f + whitePoint.z - whitePoint.x);
757 float x = whitePoint.x * y;
758 QColorSpacePrimaries primaries(QColorSpace::Primaries::SRgb);
759 primaries.whitePoint = QPointF(x,y);
760 if (!primaries.areValid()) {
761 qCWarning(lcIcc) << "fromIccProfile: Invalid ICC profile - invalid white-point";
762 return false;
763 }
764 colorspaceDPtr->toXyz = primaries.toXyzMatrix();
765 }
766 }
767 // Reset the matrix to our canonical values:
768 if (colorspaceDPtr->primaries != QColorSpace::Primaries::Custom)
769 colorspaceDPtr->setToXyzMatrix();
770
771 // Parse TRC tags
772 TagEntry rTrc;
773 TagEntry gTrc;
774 TagEntry bTrc;
775 if (header.inputColorSpace == uint(ColorSpaceType::Gray)) {
776 rTrc = tagIndex[Tag::kTRC];
777 gTrc = tagIndex[Tag::kTRC];
778 bTrc = tagIndex[Tag::kTRC];
779 } else if (tagIndex.contains(Tag::aarg) && tagIndex.contains(Tag::aagg) && tagIndex.contains(Tag::aabg)) {
780 // Apple extension for parametric version of TRCs in ICCv2:
781 rTrc = tagIndex[Tag::aarg];
782 gTrc = tagIndex[Tag::aagg];
783 bTrc = tagIndex[Tag::aabg];
784 } else {
785 rTrc = tagIndex[Tag::rTRC];
786 gTrc = tagIndex[Tag::gTRC];
787 bTrc = tagIndex[Tag::bTRC];
788 }
789
790 QColorTrc rCurve;
791 QColorTrc gCurve;
792 QColorTrc bCurve;
793 if (!parseTRC(data, rTrc, rCurve)) {
794 qCWarning(lcIcc) << "fromIccProfile: Invalid rTRC";
795 return false;
796 }
797 if (!parseTRC(data, gTrc, gCurve)) {
798 qCWarning(lcIcc) << "fromIccProfile: Invalid gTRC";
799 return false;
800 }
801 if (!parseTRC(data, bTrc, bCurve)) {
802 qCWarning(lcIcc) << "fromIccProfile: Invalid bTRC";
803 return false;
804 }
805 if (rCurve == gCurve && gCurve == bCurve && rCurve.m_type == QColorTrc::Type::Function) {
806 if (rCurve.m_fun.isLinear()) {
807 qCDebug(lcIcc) << "fromIccProfile: Linear gamma detected";
808 colorspaceDPtr->trc[0] = QColorTransferFunction();
809 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Linear;
810 colorspaceDPtr->gamma = 1.0f;
811 } else if (rCurve.m_fun.isGamma()) {
812 qCDebug(lcIcc) << "fromIccProfile: Simple gamma detected";
813 colorspaceDPtr->trc[0] = QColorTransferFunction::fromGamma(rCurve.m_fun.m_g);
814 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Gamma;
815 colorspaceDPtr->gamma = rCurve.m_fun.m_g;
816 } else if (rCurve.m_fun.isSRgb()) {
817 qCDebug(lcIcc) << "fromIccProfile: sRGB gamma detected";
818 colorspaceDPtr->trc[0] = QColorTransferFunction::fromSRgb();
819 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::SRgb;
820 } else {
821 colorspaceDPtr->trc[0] = rCurve;
822 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Custom;
823 }
824
825 colorspaceDPtr->trc[1] = colorspaceDPtr->trc[0];
826 colorspaceDPtr->trc[2] = colorspaceDPtr->trc[0];
827 } else {
828 colorspaceDPtr->trc[0] = rCurve;
829 colorspaceDPtr->trc[1] = gCurve;
830 colorspaceDPtr->trc[2] = bCurve;
831 colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Custom;
832 }
833
834 if (tagIndex.contains(Tag::desc)) {
835 if (!parseDesc(data, tagIndex[Tag::desc], colorspaceDPtr->description))
836 qCWarning(lcIcc) << "fromIccProfile: Failed to parse description";
837 else
838 qCDebug(lcIcc) << "fromIccProfile: Description" << colorspaceDPtr->description;
839 }
840
841 colorspaceDPtr->identifyColorSpace();
842 if (colorspaceDPtr->namedColorSpace)
843 qCDebug(lcIcc) << "fromIccProfile: Named colorspace detected: " << QColorSpace::NamedColorSpace(colorspaceDPtr->namedColorSpace);
844
845 colorspaceDPtr->iccProfile = data;
846
847 return true;
848}
849
850} // namespace QIcc
851
852QT_END_NAMESPACE
853