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