Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qssgexrreader.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
5#include <QtQuick3DTextureHelpers/private/qssgexrreader_p.h>
6#include <QtQuick3DTextureHelpers/private/qssgexrcodecs_p.h>
7
8#include <QtCore/qbitarray.h>
9#include <QtCore/qendian.h>
10#include <QtCore/qfloat16.h>
11#include <QtCore/qnumeric.h>
12#include <QtCore/qvarlengtharray.h>
13
14#if QT_CONFIG(thread)
15#include <QtCore/qatomic.h>
16#include <QtCore/qrunnable.h>
17#include <QtCore/qsemaphore.h>
18#include <QtCore/qthread.h>
19#include <QtCore/qthreadpool.h>
20#endif
21
22#include <algorithm>
23#include <cstring>
24#include <limits>
25#include <memory>
26#include <vector>
27
28QT_BEGIN_NAMESPACE
29
30namespace {
31
32constexpr quint32 ExrMagic = 0x01312f76;
33constexpr int ExrVersion = 2;
34
35constexpr quint32 FlagTiled = 0x0200;
36constexpr quint32 FlagLongNames = 0x0400;
37constexpr quint32 FlagNonImage = 0x0800; // deep data
38constexpr quint32 FlagMultiPart = 0x1000;
39constexpr quint32 FlagAll = FlagTiled | FlagLongNames | FlagNonImage | FlagMultiPart;
40
41constexpr int ShortNameLimit = 31;
42constexpr int LongNameLimit = 255;
43
44constexpr float MaxFiniteHalf = 65504.0f;
45constexpr float MaxFiniteFloat = std::numeric_limits<float>::max();
46
47qint64 linesPerChunk(QSSGExrReader::Compression c)
48{
49 switch (c) {
50 case QSSGExrReader::Compression::None:
51 case QSSGExrReader::Compression::Rle:
52 case QSSGExrReader::Compression::Zips:
53 return 1;
54 case QSSGExrReader::Compression::Zip:
55 return 16;
56 case QSSGExrReader::Compression::Piz:
57 return 32;
58 default:
59 return 0;
60 }
61}
62
63const char *compressionName(QSSGExrReader::Compression c)
64{
65 switch (c) {
66 case QSSGExrReader::Compression::None:
67 return "NONE";
68 case QSSGExrReader::Compression::Rle:
69 return "RLE";
70 case QSSGExrReader::Compression::Zips:
71 return "ZIPS";
72 case QSSGExrReader::Compression::Zip:
73 return "ZIP";
74 case QSSGExrReader::Compression::Piz:
75 return "PIZ";
76 case QSSGExrReader::Compression::Pxr24:
77 return "PXR24";
78 case QSSGExrReader::Compression::B44:
79 return "B44";
80 case QSSGExrReader::Compression::B44A:
81 return "B44A";
82 case QSSGExrReader::Compression::DwaA:
83 return "DWAA";
84 case QSSGExrReader::Compression::DwaB:
85 return "DWAB";
86 }
87 return "unknown";
88}
89
90qint64 bytesPerSample(QSSGExrReader::PixelType t)
91{
92 return t == QSSGExrReader::PixelType::Half ? 2 : 4;
93}
94
96{
97public:
98 ByteCursor(QByteArrayView data, QString *error) : m_data(data), m_error(error) { }
99
100 bool seek(qint64 pos)
101 {
102 if (pos < 0 || pos > m_data.size())
103 return fail(QStringLiteral("seek past the end of the file"));
104 m_pos = pos;
105 return true;
106 }
107
108 qint64 pos() const { return m_pos; }
109 qint64 remaining() const { return m_data.size() - m_pos; }
110 qint64 size() const { return m_data.size(); }
111
112 bool require(qint64 n)
113 {
114 if (n < 0 || n > remaining())
115 return fail(QStringLiteral("unexpected end of file"));
116 return true;
117 }
118
119 bool skip(qint64 n)
120 {
121 if (!require(n))
122 return false;
123 m_pos += n;
124 return true;
125 }
126
127 bool readU8(quint8 *out)
128 {
129 if (!require(1))
130 return false;
131 *out = quint8(m_data.at(m_pos));
132 m_pos += 1;
133 return true;
134 }
135
136 bool readI32(qint32 *out)
137 {
138 if (!require(4))
139 return false;
140 *out = qFromLittleEndian<qint32>(m_data.data() + m_pos);
141 m_pos += 4;
142 return true;
143 }
144
145 bool readU32(quint32 *out)
146 {
147 if (!require(4))
148 return false;
149 *out = qFromLittleEndian<quint32>(m_data.data() + m_pos);
150 m_pos += 4;
151 return true;
152 }
153
154 bool readU64(quint64 *out)
155 {
156 if (!require(8))
157 return false;
158 *out = qFromLittleEndian<quint64>(m_data.data() + m_pos);
159 m_pos += 8;
160 return true;
161 }
162
163 bool readFloat(float *out)
164 {
165 if (!require(4))
166 return false;
167 *out = qFromLittleEndian<float>(m_data.data() + m_pos);
168 m_pos += 4;
169 return true;
170 }
171
172 bool readName(QByteArray *out, int maxLen)
173 {
174 out->clear();
175 for (int i = 0; i <= maxLen; ++i) {
176 quint8 c = 0;
177 if (!readU8(&c))
178 return false;
179 if (c == 0)
180 return true;
181 out->append(char(c));
182 }
183 return fail(QStringLiteral("attribute name is longer than %1 bytes").arg(maxLen));
184 }
185
186 ByteCursor sub(qint64 n) const { return ByteCursor(view(n), m_error); }
187
188 QByteArrayView view(qint64 n) const
189 {
190 Q_ASSERT(n >= 0 && n <= remaining());
191 return m_data.sliced(m_pos, qBound(qint64(0), n, remaining()));
192 }
193
194 bool fail(const QString &message)
195 {
196 if (m_error->isEmpty())
197 *m_error = message;
198 return false;
199 }
200
201private:
202 QByteArrayView m_data;
203 QString *m_error;
204 qint64 m_pos = 0;
205};
206
208{
209 qint64 y = 0;
210 while (x > 1) {
211 y += 1;
212 x >>= 1;
213 }
214 return y;
215}
216
218{
219 qint64 y = 0;
220 qint64 r = 0;
221 while (x > 1) {
222 if (x & 1)
223 r = 1;
224 y += 1;
225 x >>= 1;
226 }
227 return y + r;
228}
229
230qint64 levelSize(qint64 top, qint64 level, bool roundUp)
231{
232 const qint64 b = qint64(1) << level;
233 qint64 s = top / b;
234 if (roundUp && s * b < top)
235 s += 1;
236 return qMax(s, qint64(1));
237}
238
239} // namespace
240
241class QSSGExrReader::Private
242{
243public:
244 QByteArray data;
245 QString error;
246 Limits limits;
247 bool valid = false;
248
249 quint32 versionFlags = 0;
250 bool tiled = false;
251 bool longNames = false;
252
253 QRect dataWindow;
254 QRect displayWindow;
255 Compression compression = Compression::None;
256 LineOrder lineOrder = LineOrder::IncreasingY;
257 QList<Channel> channels;
258
259 qint32 tileWidth = 0;
260 qint32 tileHeight = 0;
261 LevelMode levelMode = LevelMode::OneLevel;
262 bool roundUp = false;
263
264 qint64 width = 0;
265 qint64 height = 0;
266 qint64 bytesPerPixel = 0;
267 QVarLengthArray<qint64, 8> channelByteOffset;
268 QList<QSSGExr::PizChannel> pizChannels;
269
270 QList<quint64> offsets;
271 qint64 tilesX = 0;
272 qint64 tilesY = 0;
273
274 std::vector<float> band;
275
276 bool setError(const QString &message)
277 {
278 if (error.isEmpty())
279 error = message;
280 return false;
281 }
282
283 bool parseHeaderAttributes(ByteCursor &cursor);
284 bool validate();
285 bool readOffsetTable(ByteCursor &cursor);
286
287 struct ChunkScratch
288 {
289 QByteArray chunk;
290 QByteArray scratch;
291 std::vector<float> row;
292 std::unique_ptr<QSSGExr::PizScratch> piz;
293 };
294
295 bool decodeChunk(QByteArrayView payload, qint64 expected, qint64 tw, qint64 th, ChunkScratch *s, QByteArrayView *decoded, QString *err);
296 bool decodeScanlineChunk(qsizetype i, const ChannelMap &map, Flags flags, RowSink sink, ChunkScratch *s, QString *err);
297 void scatter(QByteArrayView chunk, qint64 r, qint64 tw, qint64 rowStride, const ChannelMap &map, float *row);
298
299 bool decodeScanlines(const ChannelMap &map, Flags flags, RowSink sink);
300 bool decodeTiles(const ChannelMap &map, Flags flags, RowSink sink);
301
302 qint64 destinationRow(qint64 imageRow, Flags flags) const
303 {
304 return flags.testFlag(FlipVertically) ? height - 1 - imageRow : imageRow;
305 }
306};
307
308bool QSSGExrReader::Private::parseHeaderAttributes(ByteCursor &cursor)
309{
310 const int nameLimit = longNames ? LongNameLimit : ShortNameLimit;
311
312 bool haveChannels = false;
313 bool haveCompression = false;
314 bool haveDataWindow = false;
315 bool haveDisplayWindow = false;
316 bool haveTiles = false;
317
318 int attributeCount = 0;
319
320 for (;;) {
321 QByteArray name;
322 if (!cursor.readName(&name, nameLimit))
323 return false;
324 if (name.isEmpty())
325 break;
326
327 if (attributeCount++ >= limits.maxAttributes)
328 return setError(QStringLiteral("header has too many attributes"));
329
330 QByteArray type;
331 if (!cursor.readName(&type, nameLimit))
332 return false;
333 if (type.isEmpty())
334 return setError(QStringLiteral("attribute '%1' has no type").arg(QString::fromLatin1(name)));
335
336 qint32 rawSize = 0;
337 if (!cursor.readI32(&rawSize))
338 return false;
339 const qint64 size = rawSize;
340 if (size < 0 || size > limits.maxAttributeBytes)
341 return setError(QStringLiteral("attribute '%1' has an implausible size").arg(QString::fromLatin1(name)));
342 if (!cursor.require(size))
343 return false;
344
345 ByteCursor value = cursor.sub(size);
346
347 if (name == "channels" && type == "chlist") {
348 channels.clear();
349 for (;;) {
350 QByteArray channelName;
351 if (!value.readName(&channelName, nameLimit))
352 return false;
353 if (channelName.isEmpty())
354 break;
355 if (channels.size() >= limits.maxChannels)
356 return setError(QStringLiteral("image has too many channels"));
357
358 qint32 pixelType = 0;
359 quint8 pLinear = 0;
360 qint32 xSampling = 0;
361 qint32 ySampling = 0;
362 if (!value.readI32(&pixelType) || !value.readU8(&pLinear) || !value.skip(3)
363 || !value.readI32(&xSampling) || !value.readI32(&ySampling)) {
364 return false;
365 }
366 if (pixelType < 0 || pixelType > 2)
367 return setError(QStringLiteral("channel '%1' has an unknown pixel type").arg(QString::fromLatin1(channelName)));
368 if (xSampling != 1 || ySampling != 1) {
369 return setError(QStringLiteral("channel '%1' is subsampled, which is not "
370 "supported")
371 .arg(QString::fromLatin1(channelName)));
372 }
373
374 Channel channel;
375 channel.name = channelName;
376 channel.type = PixelType(pixelType);
377
378 // Also rules out duplicate names.
379 if (!channels.isEmpty() && !(channels.last().name < channel.name))
380 return setError(QStringLiteral("channel list is not sorted"));
381
382 channels.append(channel);
383 }
384 haveChannels = true;
385 } else if (name == "compression" && type == "compression") {
386 quint8 c = 0;
387 if (!value.readU8(&c))
388 return false;
389 if (c > quint8(Compression::DwaB))
390 return setError(QStringLiteral("unknown compression id %1").arg(c));
391 compression = Compression(c);
392 haveCompression = true;
393 } else if ((name == "dataWindow" || name == "displayWindow") && type == "box2i") {
394 qint32 xMin = 0, yMin = 0, xMax = 0, yMax = 0;
395 if (!value.readI32(&xMin) || !value.readI32(&yMin) || !value.readI32(&xMax) || !value.readI32(&yMax)) {
396 return false;
397 }
398 const qint64 w = qint64(xMax) - qint64(xMin) + 1;
399 const qint64 h = qint64(yMax) - qint64(yMin) + 1;
400 if (w < 1 || h < 1 || w > limits.maxWidth || h > limits.maxHeight) {
401 return setError(QStringLiteral("%1 is empty or implausibly large").arg(QString::fromLatin1(name)));
402 }
403 const QRect rect(QPoint(xMin, yMin), QSize(int(w), int(h)));
404 if (name == "dataWindow") {
405 dataWindow = rect;
406 haveDataWindow = true;
407 } else {
408 displayWindow = rect;
409 haveDisplayWindow = true;
410 }
411 } else if (name == "lineOrder" && type == "lineOrder") {
412 quint8 o = 0;
413 if (!value.readU8(&o))
414 return false;
415 if (o > quint8(LineOrder::RandomY))
416 return setError(QStringLiteral("unsupported line order %1").arg(o));
417 lineOrder = LineOrder(o);
418 } else if (name == "tiles" && type == "tiledesc") {
419 quint32 xSize = 0, ySize = 0;
420 quint8 mode = 0;
421 if (!value.readU32(&xSize) || !value.readU32(&ySize) || !value.readU8(&mode))
422 return false;
423 if (xSize < 1 || ySize < 1 || xSize > quint32(limits.maxWidth) || ySize > quint32(limits.maxHeight)) {
424 return setError(QStringLiteral("implausible tile size"));
425 }
426 const quint8 level = mode & 0x0f;
427 const quint8 rounding = (mode >> 4) & 0x0f;
428 if (level > quint8(LevelMode::RipMap))
429 return setError(QStringLiteral("unknown tile level mode %1").arg(level));
430 if (rounding > 1)
431 return setError(QStringLiteral("unknown tile rounding mode %1").arg(rounding));
432 tileWidth = qint32(xSize);
433 tileHeight = qint32(ySize);
434 levelMode = LevelMode(level);
435 roundUp = rounding == 1;
436 haveTiles = true;
437 } else if (name == "type" && type == "string") {
438 const QByteArray kind = value.view(size).toByteArray();
439 if (kind == "deepscanline" || kind == "deeptile") {
440 return setError(QStringLiteral("deep images are not supported"));
441 }
442 }
443
444 if (!cursor.skip(size))
445 return false;
446 }
447
448 if (!haveChannels)
449 return setError(QStringLiteral("header has no channel list"));
450 if (!haveCompression)
451 return setError(QStringLiteral("header has no compression attribute"));
452 if (!haveDataWindow)
453 return setError(QStringLiteral("header has no data window"));
454 if (tiled && !haveTiles)
455 return setError(QStringLiteral("tiled image has no tile description"));
456
457 // Required by the specification, but real writers omit them.
458 if (!haveDisplayWindow)
459 displayWindow = dataWindow;
460
461 return true;
462}
463
464bool QSSGExrReader::Private::validate()
465{
466 if (channels.isEmpty())
467 return setError(QStringLiteral("image has no channels"));
468
469 if (linesPerChunk(compression) == 0) {
470 return setError(QStringLiteral("%1 compression is not supported").arg(QString::fromLatin1(compressionName(compression))));
471 }
472
473 width = dataWindow.width();
474 height = dataWindow.height();
475
476 qint64 pixels = 0;
477 if (qMulOverflow(width, height, &pixels) || pixels > limits.maxPixels)
478 return setError(QStringLiteral("image is implausibly large"));
479
480 channelByteOffset.resize(channels.size() + 1);
481 channelByteOffset[0] = 0;
482 for (qsizetype c = 0; c < channels.size(); ++c)
483 channelByteOffset[c + 1] = channelByteOffset[c] + bytesPerSample(channels[c].type);
484 bytesPerPixel = channelByteOffset[channels.size()];
485
486 pizChannels.clear();
487 pizChannels.reserve(channels.size());
488 for (qsizetype c = 0; c < channels.size(); ++c) {
489 QSSGExr::PizChannel pc;
490 pc.wordsPerSample = bytesPerSample(channels[c].type) / 2;
491 pc.byteOffsetInPixel = channelByteOffset[c];
492 pizChannels.append(pc);
493 }
494
495 qint64 decoded = 0;
496 if (qMulOverflow(pixels, qint64(4 * sizeof(float)), &decoded) || decoded > limits.maxDecodedBytes) {
497 return setError(QStringLiteral("image needs more memory than the limit allows"));
498 }
499
500 return true;
501}
502
503bool QSSGExrReader::Private::readOffsetTable(ByteCursor &cursor)
504{
505 qint64 chunkCount = 0;
506
507 if (tiled) {
508 const bool mip = levelMode != LevelMode::OneLevel;
509 const qint64 numXLevels = levelMode == LevelMode::OneLevel
510 ? 1
511 : (roundUp ? ceilLog2(levelMode == LevelMode::RipMap ? width : qMax(width, height))
512 : floorLog2(levelMode == LevelMode::RipMap ? width : qMax(width, height)))
513 + 1;
514 const qint64 numYLevels = levelMode == LevelMode::RipMap ? (roundUp ? ceilLog2(height) : floorLog2(height)) + 1 : numXLevels;
515
516 tilesX = (levelSize(width, 0, roundUp) + tileWidth - 1) / tileWidth;
517 tilesY = (levelSize(height, 0, roundUp) + tileHeight - 1) / tileHeight;
518
519 // Every level, or a chunk offset could point into the table.
520 if (!mip) {
521 chunkCount = tilesX * tilesY;
522 } else if (levelMode == LevelMode::MipMap) {
523 for (qint64 l = 0; l < numXLevels; ++l) {
524 const qint64 tx = (levelSize(width, l, roundUp) + tileWidth - 1) / tileWidth;
525 const qint64 ty = (levelSize(height, l, roundUp) + tileHeight - 1) / tileHeight;
526 if (qAddOverflow(chunkCount, tx * ty, &chunkCount))
527 return setError(QStringLiteral("tile count is implausibly large"));
528 }
529 } else {
530 for (qint64 ly = 0; ly < numYLevels; ++ly) {
531 const qint64 ty = (levelSize(height, ly, roundUp) + tileHeight - 1) / tileHeight;
532 for (qint64 lx = 0; lx < numXLevels; ++lx) {
533 const qint64 tx = (levelSize(width, lx, roundUp) + tileWidth - 1) / tileWidth;
534 if (qAddOverflow(chunkCount, tx * ty, &chunkCount))
535 return setError(QStringLiteral("tile count is implausibly large"));
536 }
537 }
538 }
539 } else {
540 const qint64 lpc = linesPerChunk(compression);
541 chunkCount = (height + lpc - 1) / lpc;
542 }
543
544 if (chunkCount < 1)
545 return setError(QStringLiteral("image has no chunks"));
546
547 qint64 tableBytes = 0;
548 if (qMulOverflow(chunkCount, qint64(8), &tableBytes) || tableBytes > cursor.remaining())
549 return setError(QStringLiteral("chunk offset table extends past the end of the file"));
550
551 // Level (0,0) comes first.
552 const qint64 wanted = tiled ? tilesX * tilesY : chunkCount;
553
554 const qint64 minChunkHeader = tiled ? 20 : 8;
555 const quint64 maxOffset = quint64(qMax(cursor.size() - minChunkHeader, qint64(0)));
556
557 offsets.resize(wanted);
558 for (qint64 i = 0; i < wanted; ++i) {
559 quint64 offset = 0;
560 if (!cursor.readU64(&offset))
561 return false;
562 if (offset == 0 || offset > maxOffset)
563 return setError(QStringLiteral("chunk offset %1 is out of range").arg(i));
564 offsets[i] = offset;
565 }
566
567 return true;
568}
569
570bool QSSGExrReader::Private::decodeChunk(QByteArrayView payload, qint64 expected, qint64 tw, qint64 th, ChunkScratch *s, QByteArrayView *decoded, QString *err)
571{
572 QByteArray *out = &s->chunk;
573 QByteArray *scratch = &s->scratch;
574 if (payload.size() == expected) {
575 // A block that did not shrink is stored raw.
576 *decoded = payload;
577 return true;
578 }
579
580 out->resize(expected);
581 *decoded = QByteArrayView(out->constData(), expected);
582 QSpan<char> outSpan(out->data(), expected);
583
584 switch (compression) {
585 case Compression::None:
586 *err = QStringLiteral("uncompressed chunk has the wrong size");
587 return false;
588 case Compression::Rle:
589 scratch->resize(expected);
590 if (!QSSGExr::rleDecode(payload, QSpan<char>(scratch->data(), expected), err))
591 return false;
592 return QSSGExr::undoPredictorAndDeinterleave(outSpan, QSpan<char>(scratch->data(), expected), err);
593 case Compression::Zips:
594 case Compression::Zip:
595 scratch->resize(expected);
596 if (!QSSGExr::inflateExact(payload, QSpan<char>(scratch->data(), expected), err))
597 return false;
598 return QSSGExr::undoPredictorAndDeinterleave(outSpan, QSpan<char>(scratch->data(), expected), err);
599 case Compression::Piz:
600 if (!s->piz)
601 s->piz = std::make_unique<QSSGExr::PizScratch>();
602 return QSSGExr::pizDecode(payload, outSpan, pizChannels, tw, th, bytesPerPixel, s->piz.get(), err);
603 default:
604 break;
605 }
606
607 *err = QStringLiteral("%1 compression is not supported").arg(QString::fromLatin1(compressionName(compression)));
608 return false;
609}
610
611// 'row' points at the first component of the column this chunk starts at.
612void QSSGExrReader::Private::scatter(QByteArrayView chunk, qint64 r, qint64 tw, qint64 rowStride, const ChannelMap &map, float *row)
613{
614 const char *rowBase = chunk.data() + r * rowStride;
615
616 for (int k = 0; k < 4; ++k) {
617 const qint32 source = map.source[k];
618 if (source < 0) {
619 const float v = map.constantValue[k];
620 for (qint64 x = 0; x < tw; ++x)
621 row[x * 4 + k] = v;
622 continue;
623 }
624
625 // Checks the exponent bits, since qIsFinite() is out of line and too slow here.
626 const char *src = rowBase + channelByteOffset[source] * tw;
627 if (channels[source].type == PixelType::Half) {
628 for (qint64 x = 0; x < tw; ++x) {
629 const quint16 bits = qFromLittleEndian<quint16>(src + x * 2);
630 if ((bits & 0x7c00) == 0x7c00) {
631 const float clamped = (bits & 0x8000) ? -MaxFiniteHalf : MaxFiniteHalf;
632 row[x * 4 + k] = (bits & 0x03ff) ? 0.0f : clamped;
633 continue;
634 }
635 // GCC rejects memcpy() into a class type without the cast.
636 qfloat16 h;
637 memcpy(static_cast<void *>(&h), &bits, sizeof(h));
638 row[x * 4 + k] = float(h);
639 }
640 } else {
641 for (qint64 x = 0; x < tw; ++x) {
642 const quint32 bits = qFromLittleEndian<quint32>(src + x * 4);
643 if ((bits & 0x7f800000u) == 0x7f800000u) {
644 const float clamped = (bits & 0x80000000u) ? -MaxFiniteFloat : MaxFiniteFloat;
645 row[x * 4 + k] = (bits & 0x007fffffu) ? 0.0f : clamped;
646 continue;
647 }
648 float v;
649 memcpy(&v, &bits, sizeof(v));
650 row[x * 4 + k] = v;
651 }
652 }
653 }
654}
655
656bool QSSGExrReader::Private::decodeScanlineChunk(qsizetype i, const ChannelMap &map, Flags flags, RowSink sink, ChunkScratch *s, QString *err)
657{
658 const qint64 lpc = linesPerChunk(compression);
659
660 ByteCursor cursor(data, err);
661 if (!cursor.seek(qint64(offsets[i])))
662 return false;
663
664 qint32 y = 0;
665 qint32 rawSize = 0;
666 if (!cursor.readI32(&y) || !cursor.readI32(&rawSize))
667 return false;
668
669 const qint64 blockRow = qint64(y) - dataWindow.top();
670 if (blockRow < 0 || blockRow >= height) {
671 *err = QStringLiteral("chunk %1 is outside the data window").arg(i);
672 return false;
673 }
674 if (blockRow % lpc != 0) {
675 *err = QStringLiteral("chunk %1 does not start on a block boundary").arg(i);
676 return false;
677 }
678 if (blockRow / lpc >= offsets.size()) {
679 *err = QStringLiteral("chunk %1 is outside the data window").arg(i);
680 return false;
681 }
682
683 const qint64 numLines = qMin(lpc, height - blockRow);
684 qint64 expected = 0;
685 if (qMulOverflow(numLines * width, bytesPerPixel, &expected) || expected > limits.maxChunkBytes) {
686 *err = QStringLiteral("chunk %1 is implausibly large").arg(i);
687 return false;
688 }
689
690 const qint64 size = rawSize;
691 if (size < 1 || size > expected || size > cursor.remaining()) {
692 *err = QStringLiteral("chunk %1 has an implausible size").arg(i);
693 return false;
694 }
695
696 QByteArrayView decoded;
697 if (!decodeChunk(cursor.view(size), expected, width, numLines, s, &decoded, err))
698 return false;
699
700 if (s->row.size() != size_t(width * 4))
701 s->row.assign(size_t(width * 4), 0.0f);
702 for (qint64 r = 0; r < numLines; ++r) {
703 scatter(decoded, r, width, width * bytesPerPixel, map, s->row.data());
704 sink(qint32(destinationRow(blockRow + r, flags)), s->row.data());
705 }
706 return true;
707}
708
709bool QSSGExrReader::Private::decodeScanlines(const ChannelMap &map, Flags flags, RowSink sink)
710{
711 const qint64 lpc = linesPerChunk(compression);
712
713 // Checked up front, so that the workers share no state.
714 QBitArray seen(offsets.size());
715 for (qsizetype i = 0; i < offsets.size(); ++i) {
716 ByteCursor cursor(data, &error);
717 qint32 y = 0;
718 if (!cursor.seek(qint64(offsets[i])) || !cursor.readI32(&y))
719 return false;
720 const qint64 blockRow = qint64(y) - dataWindow.top();
721 if (blockRow < 0 || blockRow >= height)
722 return setError(QStringLiteral("chunk %1 is outside the data window").arg(i));
723 if (blockRow % lpc != 0)
724 return setError(QStringLiteral("chunk %1 does not start on a block boundary").arg(i));
725 const qint64 block = blockRow / lpc;
726 if (block >= offsets.size())
727 return setError(QStringLiteral("chunk %1 is outside the data window").arg(i));
728 if (seen.testBit(block))
729 return setError(QStringLiteral("row block %1 appears more than once").arg(block));
730 seen.setBit(block);
731 }
732 if (seen.count(false) != 0)
733 return setError(QStringLiteral("image is missing %1 row blocks").arg(seen.count(false)));
734
735#if QT_CONFIG(thread)
736 if (offsets.size() > 1) {
737 const int threads = qMin<int>(QThread::idealThreadCount(), int(offsets.size()));
738 std::vector<ChunkScratch> scratches;
739 std::vector<QString> errors;
740 scratches.resize(size_t(threads));
741 errors.resize(size_t(threads));
742 QAtomicInt next(0);
743 QAtomicInt failed(0);
744
745 const auto worker = [&](int slot) {
746 for (;;) {
747 const int i = next.fetchAndAddOrdered(1);
748 if (i >= offsets.size() || failed.loadRelaxed())
749 return;
750 if (!decodeScanlineChunk(i, map, flags, sink, &scratches[size_t(slot)], &errors[size_t(slot)])) {
751 failed.storeRelaxed(1);
752 return;
753 }
754 }
755 };
756
757 // Take back helpers that never started, or a saturated pool deadlocks.
758 QThreadPool *pool = QThreadPool::globalInstance();
759 QSemaphore finished;
760 std::vector<std::unique_ptr<QRunnable>> helpers;
761 helpers.reserve(size_t(threads - 1));
762 for (int t = 1; t < threads; ++t) {
763 std::unique_ptr<QRunnable> helper(QRunnable::create([&worker, &finished, t] {
764 worker(t);
765 finished.release();
766 }));
767 helper->setAutoDelete(false);
768 pool->start(helper.get());
769 helpers.push_back(std::move(helper));
770 }
771 worker(0);
772 int started = 0;
773 for (const auto &h : helpers) {
774 if (!pool->tryTake(h.get()))
775 ++started;
776 }
777 finished.acquire(started);
778
779 if (failed.loadRelaxed()) {
780 for (const QString &e : errors) {
781 if (!e.isEmpty())
782 return setError(e);
783 }
784 return setError(QStringLiteral("failed to decode a chunk"));
785 }
786 return true;
787 }
788#endif // QT_CONFIG(thread)
789
790 ChunkScratch scratch;
791 for (qsizetype i = 0; i < offsets.size(); ++i) {
792 QString err;
793 if (!decodeScanlineChunk(i, map, flags, sink, &scratch, &err))
794 return setError(err);
795 }
796 return true;
797}
798
799bool QSSGExrReader::Private::decodeTiles(const ChannelMap &map, Flags flags, RowSink sink)
800{
801 QBitArray seen(offsets.size());
802 ChunkScratch cs;
803
804 const qint64 bandHeight = qMin(qint64(tileHeight), height);
805 band.assign(size_t(width * bandHeight * 4), 0.0f);
806 qint64 bandY = -1;
807 qint64 bandTiles = 0;
808
809 for (qsizetype i = 0; i < offsets.size(); ++i) {
810 ByteCursor cursor(data, &error);
811 if (!cursor.seek(qint64(offsets[i])))
812 return false;
813
814 qint32 tileX = 0, tileY = 0, levelX = 0, levelY = 0, rawSize = 0;
815 if (!cursor.readI32(&tileX) || !cursor.readI32(&tileY) || !cursor.readI32(&levelX) || !cursor.readI32(&levelY)
816 || !cursor.readI32(&rawSize)) {
817 return false;
818 }
819
820 if (levelX != 0 || levelY != 0)
821 return setError(QStringLiteral("tile %1 is not in resolution level 0").arg(i));
822 if (tileX < 0 || tileX >= tilesX || tileY < 0 || tileY >= tilesY)
823 return setError(QStringLiteral("tile %1 is out of range").arg(i));
824
825 const qint64 index = tileY * tilesX + tileX;
826 if (seen.testBit(index))
827 return setError(QStringLiteral("tile %1,%2 appears more than once").arg(tileX).arg(tileY));
828 seen.setBit(index);
829
830 const qint64 x0 = qint64(tileX) * tileWidth;
831 const qint64 y0 = qint64(tileY) * tileHeight;
832 const qint64 tw = qMin(qint64(tileWidth), width - x0);
833 const qint64 th = qMin(qint64(tileHeight), height - y0);
834 if (tw < 1 || th < 1)
835 return setError(QStringLiteral("tile %1 is empty").arg(i));
836
837 qint64 expected = 0;
838 if (qMulOverflow(th * tw, bytesPerPixel, &expected) || expected > limits.maxChunkBytes) {
839 return setError(QStringLiteral("tile %1 is implausibly large").arg(i));
840 }
841
842 const qint64 size = rawSize;
843 if (size < 1 || size > expected || size > cursor.remaining())
844 return setError(QStringLiteral("tile %1 has an implausible size").arg(i));
845
846 QByteArrayView decoded;
847 if (!decodeChunk(cursor.view(size), expected, tw, th, &cs, &decoded, &error))
848 return false;
849
850 if (bandY != y0) {
851 if (bandY >= 0 && bandTiles != tilesX) {
852 return setError(QStringLiteral("tile row %1 is incomplete: tiles are not grouped "
853 "by tile row")
854 .arg(bandY));
855 }
856 std::fill(band.begin(), band.end(), 0.0f);
857 bandY = y0;
858 bandTiles = 0;
859 }
860 for (qint64 r = 0; r < th; ++r)
861 scatter(decoded, r, tw, tw * bytesPerPixel, map, band.data() + (r * width + x0) * 4);
862 if (++bandTiles == tilesX) {
863 for (qint64 r = 0; r < th; ++r)
864 sink(qint32(destinationRow(y0 + r, flags)), band.data() + r * width * 4);
865 }
866 }
867
868 if (seen.count(false) != 0)
869 return setError(QStringLiteral("image is missing %1 tiles").arg(seen.count(false)));
870
871 return true;
872}
873
874QSSGExrReader::QSSGExrReader() : d(std::make_unique<Private>()) { }
875
876QSSGExrReader::~QSSGExrReader() = default;
877
878bool QSSGExrReader::isExr(QByteArrayView data)
879{
880 if (data.size() < 4)
881 return false;
882 return qFromLittleEndian<quint32>(data.data()) == ExrMagic;
883}
884
885void QSSGExrReader::setLimits(const Limits &limits)
886{
887 d->limits = limits;
888}
889
890const QSSGExrReader::Limits &QSSGExrReader::limits() const
891{
892 return d->limits;
893}
894
895bool QSSGExrReader::parseHeader(const QByteArray &data)
896{
897 const Limits limits = d->limits;
898 d = std::make_unique<Private>();
899 d->limits = limits;
900 d->data = data;
901
902 ByteCursor cursor(data, &d->error);
903
904 quint32 magic = 0;
905 if (!cursor.readU32(&magic))
906 return false;
907 if (magic != ExrMagic)
908 return d->setError(QStringLiteral("not an OpenEXR file"));
909
910 quint32 versionField = 0;
911 if (!cursor.readU32(&versionField))
912 return false;
913
914 const quint32 version = versionField & 0xff;
915 d->versionFlags = versionField & ~0xffu;
916 if (version != ExrVersion)
917 return d->setError(QStringLiteral("unsupported OpenEXR version %1").arg(version));
918 if (d->versionFlags & ~FlagAll)
919 return d->setError(QStringLiteral("unknown flags in the version field"));
920 if (d->versionFlags & FlagMultiPart)
921 return d->setError(QStringLiteral("multi part files are not supported"));
922 if (d->versionFlags & FlagNonImage)
923 return d->setError(QStringLiteral("deep images are not supported"));
924
925 d->tiled = d->versionFlags & FlagTiled;
926 d->longNames = d->versionFlags & FlagLongNames;
927
928 if (!d->parseHeaderAttributes(cursor))
929 return false;
930 if (!d->validate())
931 return false;
932 if (!d->readOffsetTable(cursor))
933 return false;
934
935 d->valid = true;
936 return true;
937}
938
939bool QSSGExrReader::isValid() const
940{
941 return d->valid;
942}
943
944QSize QSSGExrReader::size() const
945{
946 return d->dataWindow.size();
947}
948
949QRect QSSGExrReader::dataWindow() const
950{
951 return d->dataWindow;
952}
953
954QRect QSSGExrReader::displayWindow() const
955{
956 return d->displayWindow;
957}
958
959QSSGExrReader::Compression QSSGExrReader::compression() const
960{
961 return d->compression;
962}
963
964QSSGExrReader::LineOrder QSSGExrReader::lineOrder() const
965{
966 return d->lineOrder;
967}
968
969const QList<QSSGExrReader::Channel> &QSSGExrReader::channels() const
970{
971 return d->channels;
972}
973
974qint32 QSSGExrReader::indexOfChannel(QByteArrayView name) const
975{
976 for (qsizetype i = 0; i < d->channels.size(); ++i) {
977 if (d->channels[i].name == name)
978 return qint32(i);
979 }
980 return -1;
981}
982
983bool QSSGExrReader::isTiled() const
984{
985 return d->tiled;
986}
987
988QSize QSSGExrReader::tileSize() const
989{
990 return d->tiled ? QSize(d->tileWidth, d->tileHeight) : QSize();
991}
992
993QSSGExrReader::LevelMode QSSGExrReader::levelMode() const
994{
995 return d->levelMode;
996}
997
998bool QSSGExrReader::decode(const ChannelMap &map, Flags flags, RowSink sink)
999{
1000 if (!d->valid)
1001 return d->setError(QStringLiteral("no valid header"));
1002
1003 d->error.clear();
1004
1005 for (int k = 0; k < 4; ++k) {
1006 if (map.source[k] >= d->channels.size())
1007 return d->setError(QStringLiteral("channel map refers to a channel that is absent"));
1008 if (map.source[k] >= 0 && d->channels[map.source[k]].type == PixelType::UInt) {
1009 return d->setError(QStringLiteral("channel '%1' is an integer channel, which is not "
1010 "supported")
1011 .arg(QString::fromLatin1(d->channels[map.source[k]].name)));
1012 }
1013 }
1014
1015 const bool ok = d->tiled ? d->decodeTiles(map, flags, sink) : d->decodeScanlines(map, flags, sink);
1016
1017 d->band.clear();
1018 d->band.shrink_to_fit();
1019
1020 return ok;
1021}
1022
1023bool QSSGExrReader::decodeToRgba32F(const ChannelMap &map, Flags flags, float *dst, qsizetype dstFloatCount)
1024{
1025 if (!d->valid)
1026 return d->setError(QStringLiteral("no valid header"));
1027
1028 d->error.clear();
1029
1030 const qint64 needed = d->width * d->height * 4;
1031 if (!dst || dstFloatCount < needed)
1032 return d->setError(QStringLiteral("destination buffer is too small"));
1033
1034 // Never hand uninitialised memory to a texture.
1035 memset(dst, 0, size_t(needed) * sizeof(float));
1036
1037 const qint64 rowFloats = d->width * 4;
1038 return decode(map, flags, [&](qint32 dstRow, const float *rgba) {
1039 memcpy(dst + dstRow * rowFloats, rgba, size_t(rowFloats) * sizeof(float));
1040 });
1041}
1042
1043QString QSSGExrReader::errorMessage() const
1044{
1045 return d->error;
1046}
1047
1049{
1050 QSSGExrReader::ChannelMap map;
1051 map.constantValue[0] = 0.0f;
1052 map.constantValue[1] = 0.0f;
1053 map.constantValue[2] = 0.0f;
1054 map.constantValue[3] = 1.0f;
1055
1056 if (reader.channels().size() == 1) {
1057 // Alpha too, the baseline images depend on it.
1058 map.source[0] = map.source[1] = map.source[2] = map.source[3] = 0;
1059 return map;
1060 }
1061
1062 map.source[0] = reader.indexOfChannel("R");
1063 map.source[1] = reader.indexOfChannel("G");
1064 map.source[2] = reader.indexOfChannel("B");
1065 map.source[3] = reader.indexOfChannel("A"); // absent means opaque
1066
1067 return map;
1068}
1069
1070QT_END_NAMESPACE
QByteArrayView view(qint64 n) const
bool readName(QByteArray *out, int maxLen)
bool fail(const QString &message)
ByteCursor sub(qint64 n) const
ByteCursor(QByteArrayView data, QString *error)
constexpr float MaxFiniteHalf
constexpr quint32 ExrMagic
constexpr int ExrVersion
constexpr quint32 FlagAll
constexpr quint32 FlagNonImage
qint64 floorLog2(qint64 x)
constexpr quint32 FlagTiled
const char * compressionName(QSSGExrReader::Compression c)
qint64 levelSize(qint64 top, qint64 level, bool roundUp)
qint64 linesPerChunk(QSSGExrReader::Compression c)
constexpr quint32 FlagLongNames
qint64 bytesPerSample(QSSGExrReader::PixelType t)
constexpr int LongNameLimit
qint64 ceilLog2(qint64 x)
constexpr quint32 FlagMultiPart
constexpr int ShortNameLimit
constexpr float MaxFiniteFloat
QSSGExrReader::ChannelMap qssgExrRgbaChannelMap(const QSSGExrReader &reader)