66 case QSSGExrReader::Compression::None:
68 case QSSGExrReader::Compression::Rle:
70 case QSSGExrReader::Compression::Zips:
72 case QSSGExrReader::Compression::Zip:
74 case QSSGExrReader::Compression::Piz:
76 case QSSGExrReader::Compression::Pxr24:
78 case QSSGExrReader::Compression::B44:
80 case QSSGExrReader::Compression::B44A:
82 case QSSGExrReader::Compression::DwaA:
84 case QSSGExrReader::Compression::DwaB:
241class QSSGExrReader::Private
249 quint32 versionFlags = 0;
251 bool longNames =
false;
255 Compression compression = Compression::None;
256 LineOrder lineOrder = LineOrder::IncreasingY;
257 QList<Channel> channels;
259 qint32 tileWidth = 0;
260 qint32 tileHeight = 0;
261 LevelMode levelMode = LevelMode::OneLevel;
262 bool roundUp =
false;
266 qint64 bytesPerPixel = 0;
267 QVarLengthArray<qint64, 8> channelByteOffset;
268 QList<QSSGExr::PizChannel> pizChannels;
270 QList<quint64> offsets;
274 std::vector<
float> band;
276 bool setError(
const QString &message)
283 bool parseHeaderAttributes(ByteCursor &cursor);
285 bool readOffsetTable(ByteCursor &cursor);
291 std::vector<
float> row;
292 std::unique_ptr<QSSGExr::PizScratch> piz;
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);
299 bool decodeScanlines(
const ChannelMap &map, Flags flags, RowSink sink);
300 bool decodeTiles(
const ChannelMap &map, Flags flags, RowSink sink);
302 qint64 destinationRow(qint64 imageRow, Flags flags)
const
304 return flags.testFlag(FlipVertically) ? height - 1 - imageRow : imageRow;
308bool QSSGExrReader::Private::parseHeaderAttributes(ByteCursor &cursor)
310 const int nameLimit = longNames ? LongNameLimit : ShortNameLimit;
312 bool haveChannels =
false;
313 bool haveCompression =
false;
314 bool haveDataWindow =
false;
315 bool haveDisplayWindow =
false;
316 bool haveTiles =
false;
318 int attributeCount = 0;
322 if (!cursor.readName(&name, nameLimit))
327 if (attributeCount++ >= limits.maxAttributes)
328 return setError(QStringLiteral(
"header has too many attributes"));
331 if (!cursor.readName(&type, nameLimit))
334 return setError(QStringLiteral(
"attribute '%1' has no type").arg(QString::fromLatin1(name)));
337 if (!cursor.readI32(&rawSize))
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))
345 ByteCursor value = cursor.sub(size);
347 if (name ==
"channels" && type ==
"chlist") {
350 QByteArray channelName;
351 if (!value.readName(&channelName, nameLimit))
353 if (channelName.isEmpty())
355 if (channels.size() >= limits.maxChannels)
356 return setError(QStringLiteral(
"image has too many channels"));
358 qint32 pixelType = 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)) {
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 "
371 .arg(QString::fromLatin1(channelName)));
375 channel.name = channelName;
376 channel.type = PixelType(pixelType);
379 if (!channels.isEmpty() && !(channels.last().name < channel.name))
380 return setError(QStringLiteral(
"channel list is not sorted"));
382 channels.append(channel);
385 }
else if (name ==
"compression" && type ==
"compression") {
387 if (!value.readU8(&c))
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)) {
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)));
403 const QRect rect(QPoint(xMin, yMin), QSize(
int(w),
int(h)));
404 if (name ==
"dataWindow") {
406 haveDataWindow =
true;
408 displayWindow = rect;
409 haveDisplayWindow =
true;
411 }
else if (name ==
"lineOrder" && type ==
"lineOrder") {
413 if (!value.readU8(&o))
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;
421 if (!value.readU32(&xSize) || !value.readU32(&ySize) || !value.readU8(&mode))
423 if (xSize < 1 || ySize < 1 || xSize > quint32(limits.maxWidth) || ySize > quint32(limits.maxHeight)) {
424 return setError(QStringLiteral(
"implausible tile size"));
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));
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;
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"));
444 if (!cursor.skip(size))
449 return setError(QStringLiteral(
"header has no channel list"));
450 if (!haveCompression)
451 return setError(QStringLiteral(
"header has no compression attribute"));
453 return setError(QStringLiteral(
"header has no data window"));
454 if (tiled && !haveTiles)
455 return setError(QStringLiteral(
"tiled image has no tile description"));
458 if (!haveDisplayWindow)
459 displayWindow = dataWindow;
464bool QSSGExrReader::Private::validate()
466 if (channels.isEmpty())
467 return setError(QStringLiteral(
"image has no channels"));
469 if (linesPerChunk(compression) == 0) {
470 return setError(QStringLiteral(
"%1 compression is not supported").arg(QString::fromLatin1(compressionName(compression))));
473 width = dataWindow.width();
474 height = dataWindow.height();
477 if (qMulOverflow(width, height, &pixels) || pixels > limits.maxPixels)
478 return setError(QStringLiteral(
"image is implausibly large"));
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()];
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);
496 if (qMulOverflow(pixels, qint64(4 *
sizeof(
float)), &decoded) || decoded > limits.maxDecodedBytes) {
497 return setError(QStringLiteral(
"image needs more memory than the limit allows"));
503bool QSSGExrReader::Private::readOffsetTable(ByteCursor &cursor)
505 qint64 chunkCount = 0;
508 const bool mip = levelMode != LevelMode::OneLevel;
509 const qint64 numXLevels = levelMode == LevelMode::OneLevel
511 : (roundUp ? ceilLog2(levelMode == LevelMode::RipMap ? width : qMax(width, height))
512 : floorLog2(levelMode == LevelMode::RipMap ? width : qMax(width, height)))
514 const qint64 numYLevels = levelMode == LevelMode::RipMap ? (roundUp ? ceilLog2(height) : floorLog2(height)) + 1 : numXLevels;
516 tilesX = (levelSize(width, 0, roundUp) + tileWidth - 1) / tileWidth;
517 tilesY = (levelSize(height, 0, roundUp) + tileHeight - 1) / tileHeight;
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"));
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"));
540 const qint64 lpc = linesPerChunk(compression);
541 chunkCount = (height + lpc - 1) / lpc;
545 return setError(QStringLiteral(
"image has no chunks"));
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"));
552 const qint64 wanted = tiled ? tilesX * tilesY : chunkCount;
554 const qint64 minChunkHeader = tiled ? 20 : 8;
555 const quint64 maxOffset = quint64(qMax(cursor.size() - minChunkHeader, qint64(0)));
557 offsets.resize(wanted);
558 for (qint64 i = 0; i < wanted; ++i) {
560 if (!cursor.readU64(&offset))
562 if (offset == 0 || offset > maxOffset)
563 return setError(QStringLiteral(
"chunk offset %1 is out of range").arg(i));
570bool QSSGExrReader::Private::decodeChunk(QByteArrayView payload, qint64 expected, qint64 tw, qint64 th, ChunkScratch *s, QByteArrayView *decoded, QString *err)
572 QByteArray *out = &s->chunk;
573 QByteArray *scratch = &s->scratch;
574 if (payload.size() == expected) {
580 out->resize(expected);
581 *decoded = QByteArrayView(out->constData(), expected);
582 QSpan<
char> outSpan(out->data(), expected);
584 switch (compression) {
585 case Compression::None:
586 *err = QStringLiteral(
"uncompressed chunk has the wrong size");
588 case Compression::Rle:
589 scratch->resize(expected);
590 if (!QSSGExr::rleDecode(payload, QSpan<
char>(scratch->data(), expected), err))
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))
598 return QSSGExr::undoPredictorAndDeinterleave(outSpan, QSpan<
char>(scratch->data(), expected), err);
599 case Compression::Piz:
601 s->piz = std::make_unique<QSSGExr::PizScratch>();
602 return QSSGExr::pizDecode(payload, outSpan, pizChannels, tw, th, bytesPerPixel, s->piz.get(), err);
607 *err = QStringLiteral(
"%1 compression is not supported").arg(QString::fromLatin1(compressionName(compression)));
612void QSSGExrReader::Private::scatter(QByteArrayView chunk, qint64 r, qint64 tw, qint64 rowStride,
const ChannelMap &map,
float *row)
614 const char *rowBase = chunk.data() + r * rowStride;
616 for (
int k = 0; k < 4; ++k) {
617 const qint32 source = map.source[k];
619 const float v = map.constantValue[k];
620 for (qint64 x = 0; x < tw; ++x)
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;
637 memcpy(
static_cast<
void *>(&h), &bits,
sizeof(h));
638 row[x * 4 + k] =
float(h);
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;
649 memcpy(&v, &bits,
sizeof(v));
656bool QSSGExrReader::Private::decodeScanlineChunk(qsizetype i,
const ChannelMap &map, Flags flags, RowSink sink, ChunkScratch *s, QString *err)
658 const qint64 lpc = linesPerChunk(compression);
660 ByteCursor cursor(data, err);
661 if (!cursor.seek(qint64(offsets[i])))
666 if (!cursor.readI32(&y) || !cursor.readI32(&rawSize))
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);
674 if (blockRow % lpc != 0) {
675 *err = QStringLiteral(
"chunk %1 does not start on a block boundary").arg(i);
678 if (blockRow / lpc >= offsets.size()) {
679 *err = QStringLiteral(
"chunk %1 is outside the data window").arg(i);
683 const qint64 numLines = qMin(lpc, height - blockRow);
685 if (qMulOverflow(numLines * width, bytesPerPixel, &expected) || expected > limits.maxChunkBytes) {
686 *err = QStringLiteral(
"chunk %1 is implausibly large").arg(i);
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);
696 QByteArrayView decoded;
697 if (!decodeChunk(cursor.view(size), expected, width, numLines, s, &decoded, err))
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());
709bool QSSGExrReader::Private::decodeScanlines(
const ChannelMap &map, Flags flags, RowSink sink)
711 const qint64 lpc = linesPerChunk(compression);
714 QBitArray seen(offsets.size());
715 for (qsizetype i = 0; i < offsets.size(); ++i) {
716 ByteCursor cursor(data, &error);
718 if (!cursor.seek(qint64(offsets[i])) || !cursor.readI32(&y))
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));
732 if (seen.count(
false) != 0)
733 return setError(QStringLiteral(
"image is missing %1 row blocks").arg(seen.count(
false)));
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));
743 QAtomicInt failed(0);
745 const auto worker = [&](
int slot) {
747 const int i = next.fetchAndAddOrdered(1);
748 if (i >= offsets.size() || failed.loadRelaxed())
750 if (!decodeScanlineChunk(i, map, flags, sink, &scratches[size_t(slot)], &errors[size_t(slot)])) {
751 failed.storeRelaxed(1);
758 QThreadPool *pool = QThreadPool::globalInstance();
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] {
767 helper->setAutoDelete(
false);
768 pool->start(helper.get());
769 helpers.push_back(std::move(helper));
773 for (
const auto &h : helpers) {
774 if (!pool->tryTake(h.get()))
777 finished.acquire(started);
779 if (failed.loadRelaxed()) {
780 for (
const QString &e : errors) {
784 return setError(QStringLiteral(
"failed to decode a chunk"));
790 ChunkScratch scratch;
791 for (qsizetype i = 0; i < offsets.size(); ++i) {
793 if (!decodeScanlineChunk(i, map, flags, sink, &scratch, &err))
794 return setError(err);
799bool QSSGExrReader::Private::decodeTiles(
const ChannelMap &map, Flags flags, RowSink sink)
801 QBitArray seen(offsets.size());
804 const qint64 bandHeight = qMin(qint64(tileHeight), height);
805 band.assign(size_t(width * bandHeight * 4), 0.0f);
807 qint64 bandTiles = 0;
809 for (qsizetype i = 0; i < offsets.size(); ++i) {
810 ByteCursor cursor(data, &error);
811 if (!cursor.seek(qint64(offsets[i])))
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)) {
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));
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));
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));
838 if (qMulOverflow(th * tw, bytesPerPixel, &expected) || expected > limits.maxChunkBytes) {
839 return setError(QStringLiteral(
"tile %1 is implausibly large").arg(i));
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));
846 QByteArrayView decoded;
847 if (!decodeChunk(cursor.view(size), expected, tw, th, &cs, &decoded, &error))
851 if (bandY >= 0 && bandTiles != tilesX) {
852 return setError(QStringLiteral(
"tile row %1 is incomplete: tiles are not grouped "
856 std::fill(band.begin(), band.end(), 0.0f);
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);
868 if (seen.count(
false) != 0)
869 return setError(QStringLiteral(
"image is missing %1 tiles").arg(seen.count(
false)));
895bool QSSGExrReader::parseHeader(
const QByteArray &data)
897 const Limits limits = d->limits;
898 d = std::make_unique<Private>();
902 ByteCursor cursor(data, &d->error);
905 if (!cursor.readU32(&magic))
907 if (magic != ExrMagic)
908 return d->setError(QStringLiteral(
"not an OpenEXR file"));
910 quint32 versionField = 0;
911 if (!cursor.readU32(&versionField))
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"));
925 d->tiled = d->versionFlags & FlagTiled;
926 d->longNames = d->versionFlags & FlagLongNames;
928 if (!d->parseHeaderAttributes(cursor))
932 if (!d->readOffsetTable(cursor))
998bool QSSGExrReader::decode(
const ChannelMap &map, Flags flags, RowSink sink)
1001 return d->setError(QStringLiteral(
"no valid header"));
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 "
1011 .arg(QString::fromLatin1(d->channels[map.source[k]].name)));
1015 const bool ok = d->tiled ? d->decodeTiles(map, flags, sink) : d->decodeScanlines(map, flags, sink);
1018 d->band.shrink_to_fit();
1023bool QSSGExrReader::decodeToRgba32F(
const ChannelMap &map, Flags flags,
float *dst, qsizetype dstFloatCount)
1026 return d->setError(QStringLiteral(
"no valid header"));
1030 const qint64 needed = d->width * d->height * 4;
1031 if (!dst || dstFloatCount < needed)
1032 return d->setError(QStringLiteral(
"destination buffer is too small"));
1035 memset(dst, 0, size_t(needed) *
sizeof(
float));
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));
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;
1056 if (reader.channels().size() == 1) {
1058 map.source[0] = map.source[1] = map.source[2] = map.source[3] = 0;
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");