From 480ea8dba08b299ce8d3c51243b7bc19d0c22099 Mon Sep 17 00:00:00 2001 From: Mirco Miranda Date: Thu, 7 Aug 2025 22:17:09 +0200 Subject: [PATCH] IFF: add support for RGBN/RGB8 image data and CAT chunk Add the following features: - RGB8 image data support (test case added) - RGBN image data support ([Clouds.iff](/uploads/9db869350f74421bf1813fa7d4332f4f/Clouds.iff)) - CAT chunk support: you can have more than one image for file (test case added) - Image transformation support via EXIF data [RGBN/RGB8](https://wiki.amigaos.net/wiki/RGBN_and_RGB8_IFF_Image_Data) files are used in Impulse's Turbo Silver and Imagine. Closes #34 --- README.md | 27 ++ autotests/read/iff/cat_ilbm.iff | Bin 0 -> 98396 bytes autotests/read/iff/cat_ilbm.iff.json | 5 + autotests/read/iff/sv5_testcard_rgb_rgb8.iff | Bin 0 -> 15572 bytes .../read/iff/sv5_testcard_rgb_rgb8.iff.json | 5 + src/imageformats/chunks.cpp | 307 +++++++++++++++--- src/imageformats/chunks_p.h | 108 +++++- src/imageformats/iff.cpp | 146 ++++++--- src/imageformats/iff_p.h | 5 + 9 files changed, 510 insertions(+), 93 deletions(-) create mode 100644 autotests/read/iff/cat_ilbm.iff create mode 100644 autotests/read/iff/cat_ilbm.iff.json create mode 100644 autotests/read/iff/sv5_testcard_rgb_rgb8.iff create mode 100644 autotests/read/iff/sv5_testcard_rgb_rgb8.iff.json diff --git a/README.md b/README.md index d81b5c1..f65819c 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,33 @@ plugin: - `HDR_HALF_QUALITY`: on read, a 16-bit float image is returned instead of a 32-bit float one. + +### The IFF plugin + +Interchange File Format is a chunk-based format. Since the original 1985 +version, various extensions have been created over time. + +The plugin supports the following image data: +- FORM ILBM (Interleaved Bitmap): Electronic Arts’ IFF standard for + Interchange File Format (EA IFF 1985). ILBM is a format to handle raster + images, specifically an InterLeaved bitplane BitMap image with color map. + It supports from 1 to 8-bit indexed images with HAM, Halfbride, and normal + encoding. It also supports interleaved 24-bit RGB and 32-bit RGBA + extension without color map. +- FORM ILBM 64: ILBM extension to support 48-bit RGB and 64-bit RGBA encoding. +- FORM ACBM (Amiga Contiguous BitMap): It supports uncompressed ACBMs by + converting them to ILBMs at runtime. +- FORM RGBN / RGB8: It supports 13-bit and 25-bit RGB images with compression + type 4. +- FORM PBM: PBM is a chunky version of IFF pictures. It supports 8-bit images + with color map only. +- FOR4 CIMG (Maya Image File Format): It supports 24/48-bit RGB and 32/64-bit + RGBA images. + +The plugin does not load images with non-standard SHAM/CTBL chunks due to the +lack of clear specifications. + + ### The JP2 plugin **This plugin can be disabled by setting `KIMAGEFORMATS_JP2` to `OFF` diff --git a/autotests/read/iff/cat_ilbm.iff b/autotests/read/iff/cat_ilbm.iff new file mode 100644 index 0000000000000000000000000000000000000000..d063275ff815cd7be1beb866ed042a12ee770a5c GIT binary patch literal 98396 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 literal 0 HcmV?d00001 diff --git a/autotests/read/iff/cat_ilbm.iff.json b/autotests/read/iff/cat_ilbm.iff.json new file mode 100644 index 0000000..8ce845a --- /dev/null +++ b/autotests/read/iff/cat_ilbm.iff.json @@ -0,0 +1,5 @@ +[ + { + "fileName" : "ps_testcard_rgb_maya.png" + } +] diff --git a/autotests/read/iff/sv5_testcard_rgb_rgb8.iff b/autotests/read/iff/sv5_testcard_rgb_rgb8.iff new file mode 100644 index 0000000000000000000000000000000000000000..75524c189fd0338a9d09c184ae2983f75a3e72c0 GIT binary patch literal 15572 zcmds836K=k8J@qVyJu#1X4#c@)7P!8n~+g(InJ{-ZrnIJdGaJ}+_*9B_VM=b?YG~i;^JbOK7D%JV|Vn;fYy*l zye}@!HaW!ZY8shy*I|@hPmbX>M zdw1>HC47Rhw6fxU@WBVPXU`tQRJpA%%8R9bSI3LohyEFh%82PdT<{-C#9tZL9n*x* z4FB_Ii08-ol*c%^^$^C8;{&A>MoELecgzz)QLDUL;b;x-~z z9epIH2dOxi7a-60c<4Ru7<1>&CA8&s!gI=tvvA=;9EliQ;o!^5H5&W96)+C%tFOL_8sq%=^A#1tyLj

2ME2c1~r9tvX`3G_`refo3+mehYj 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 { return {}; } - return QString::fromUtf8(chunk->data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed(); + auto dt = chunk->data(); + for (; dt.endsWith(char()); dt = dt.removeLast()); + return QString::fromUtf8(dt).trimmed(); } IFFChunk::~IFFChunk() @@ -268,6 +270,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk * chunk = QSharedPointer(new BODYChunk()); } else if (cid == CAMG_CHUNK) { chunk = QSharedPointer(new CAMGChunk()); + } else if (cid == CAT__CHUNK) { + chunk = QSharedPointer(new CATChunk()); } else if (cid == CMAP_CHUNK) { chunk = QSharedPointer(new CMAPChunk()); } else if (cid == CMYK_CHUNK) { @@ -509,7 +513,7 @@ QList CMAPChunk::palette(bool halfbride) const return p; } auto tmp = p; - for(auto &&v : tmp) { + for (auto &&v : tmp) { p << qRgb(qRed(v) / 2, qGreen(v) / 2, qBlue(v) / 2); } return p; @@ -679,31 +683,150 @@ bool BODYChunk::isValid() const return chunkId() == BODYChunk::defaultChunkId(); } -QByteArray BODYChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, bool isPbm) const +// For each RGB value, a LONG-word (32 bits) is written: +// with the 24 RGB bits in the MSB positions; the "genlock" +// bit next, and then a 7 bit repeat count. +// +// See also: https://wiki.amigaos.net/wiki/RGBN_and_RGB8_IFF_Image_Data +inline qint64 rgb8Decompress(QIODevice *input, char *output, qint64 olen) +{ + qint64 j = 0; + for (qint64 available = olen; j < olen; available = olen - j) { + auto pos = input->pos(); + auto ba4 = input->read(4); + if (ba4.size() != 4) { + break; + } + auto cnt = qint32(ba4.at(3) & 0x7F); + if (cnt * 3 > available) { + if (!input->seek(pos)) + return -1; + break; + } + for (qint32 i = 0; i < cnt; ++i) { + output[j++] = ba4.at(0); + output[j++] = ba4.at(1); + output[j++] = ba4.at(2); + } + } + return j; +} + +// For each RGB value, a WORD (16-bits) is written: with the +// 12 RGB bits in the MSB (most significant bit) positions; +// the "genlock" bit next; and then a 3 bit repeat count. +// If the repeat count is greater than 7, the 3-bit count is +// zero, and a BYTE repeat count follows. If the repeat count +// is greater than 255, the BYTE count is zero, and a WORD +// repeat count follows. Repeat counts greater than 65536 are +// not supported. +// +// See also: https://wiki.amigaos.net/wiki/RGBN_and_RGB8_IFF_Image_Data +inline qint32 rgbnCount(QIODevice *input, quint8 &R, quint8& G, quint8 &B) +{ + auto ba2 = input->read(2); + if (ba2.size() != 2) + return 0; + + R = ba2.at(0) & 0xF0; + R = R | (R >> 4); + + G = ba2.at(0) & 0x0F; + G = G | (G << 4); + + B = ba2.at(1) & 0xF0; + B = B | (B >> 4); + + auto cnt = ba2.at(1) & 7; + if (cnt == 0) { + auto ba1 = input->read(1); + if (ba1.size() != 1) + return 0; + cnt = quint8(ba1.at(0)); + } + if (cnt == 0) { + auto baw = input->read(2); + if (baw.size() != 2) + return 0; + cnt = qint32(quint8(baw.at(0))) << 8 | quint8(baw.at(1)); + } + + return cnt; +} + +inline qint64 rgbNDecompress(QIODevice *input, char *output, qint64 olen) +{ + qint64 j = 0; + for (qint64 available = olen; j < olen; available = olen - j) { + quint8 R = 0, G = 0, B = 0; + auto pos = input->pos(); + auto cnt = rgbnCount(input, R, G, B); + if (cnt * 3 > available || cnt == 0) { + if (!input->seek(pos)) + return -1; + break; + } + for (qint32 i = 0; i < cnt; ++i) { + output[j++] = R; + output[j++] = G; + output[j++] = B; + } + } + return j; +} + +QByteArray BODYChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, const QByteArray& formType) const { if (!isValid() || header == nullptr || d == nullptr) { return {}; } - auto readSize = strideSize(header, isPbm); - for(;!d->atEnd() && _readBuffer.size() < readSize;) { - QByteArray buf(readSize, char()); + auto isRgbN = formType == RGBN_FORM_TYPE; + auto isRgb8 = formType == RGB8_FORM_TYPE; + auto isPbm = formType == PBM__FORM_TYPE; + auto lineCompressed = isRgbN || isRgb8 ? false : true; + auto readSize = strideSize(header, formType); + auto bufSize = readSize; + if (isRgbN) { + bufSize = std::max(quint32(65536 * 3), readSize); + } + if (isRgb8) { + bufSize = std::max(quint32(127 * 3), readSize); + } + for (auto nextPos = nextChunkPos(); !d->atEnd() && d->pos() < nextPos && _readBuffer.size() < readSize;) { + QByteArray buf(bufSize, char()); qint64 rr = -1; if (header->compression() == BMHDChunk::Compression::Rle) { // WARNING: The online spec says it's the same as TIFF but that's // not accurate: the RLE -128 code is not a noop. rr = packbitsDecompress(d, buf.data(), buf.size(), true); + } else if (header->compression() == BMHDChunk::Compression::RgbN8) { + if (isRgb8) + rr = rgb8Decompress(d, buf.data(), buf.size()); + else if (isRgbN) + rr = rgbNDecompress(d, buf.data(), buf.size()); } else if (header->compression() == BMHDChunk::Compression::Uncompressed) { rr = d->read(buf.data(), buf.size()); // never seen + } else { + qCDebug(LOG_IFFPLUGIN) << "BODYChunk::strideRead: unknown compression" << header->compression(); } - if (rr != readSize) + if ((rr != readSize && lineCompressed) || (rr < 1)) return {}; _readBuffer.append(buf.data(), rr); } auto planes = _readBuffer.left(readSize); _readBuffer.remove(0, readSize); - return deinterleave(planes, header, camg, cmap, isPbm); + if (isPbm) { + return pbm(planes, header, camg, cmap); + } + if (isRgb8) { + return rgb8(planes, header, camg, cmap); + } + if (isRgbN) { + return rgbN(planes, header, camg, cmap); + } + return deinterleave(planes, header, camg, cmap); } bool BODYChunk::resetStrideRead(QIODevice *d) const @@ -731,20 +854,56 @@ CAMGChunk::ModeIds BODYChunk::safeModeId(const BMHDChunk *header, const CAMGChun return CAMGChunk::ModeIds(); } -quint32 BODYChunk::strideSize(const BMHDChunk *header, bool isPbm) const +quint32 BODYChunk::strideSize(const BMHDChunk *header, const QByteArray& formType) const { - if (!isPbm) { - return header->rowLen() * header->bitplanes(); + // RGB8 / RGBN + if (formType == RGB8_FORM_TYPE || formType == RGBN_FORM_TYPE) { + return header->width() * 3; } - auto rs = header->width() * header->bitplanes() / 8; - if (rs & 1) - ++rs; - return rs; + + // PBM + if (formType == PBM__FORM_TYPE) { + auto rs = header->width() * header->bitplanes() / 8; + if (rs & 1) + ++rs; + return rs; + } + + // ILBM + return header->rowLen() * header->bitplanes(); } -QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, bool isPbm) const +QByteArray BODYChunk::pbm(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *, const CMAPChunk *) const { - if (planes.size() != strideSize(header, isPbm)) { + if (planes.size() != strideSize(header, PBM__FORM_TYPE)) { + return {}; + } + if (header->bitplanes() == 8) { + // The data are contiguous. + return planes; + } + return {}; +} + +QByteArray BODYChunk::rgb8(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *, const CMAPChunk *) const +{ + if (planes.size() != strideSize(header, RGB8_FORM_TYPE)) { + return {}; + } + return planes; +} + +QByteArray BODYChunk::rgbN(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *, const CMAPChunk *) const +{ + if (planes.size() != strideSize(header, RGBN_FORM_TYPE)) { + return {}; + } + return planes; +} + +QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap) const +{ + if (planes.size() != strideSize(header, ILBM_FORM_TYPE)) { return {}; } @@ -762,10 +921,7 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he case 6: case 7: case 8: - if (isPbm && bitplanes == 8) { - // The data are contiguous. - ba = planes; - } else if ((modeId & CAMGChunk::ModeId::Ham) && (cmap) && (bitplanes >= 5 && bitplanes <= 8)) { + if ((modeId & CAMGChunk::ModeId::Ham) && (cmap) && (bitplanes >= 5 && bitplanes <= 8)) { // From A Quick Introduction to IFF.txt: // // Amiga HAM (Hold and Modify) mode lets the Amiga display all 4096 RGB values. @@ -895,11 +1051,6 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he case 24: // rgb case 32: // rgba (SView5 extension) - if (isPbm) { - // PBM cannot be a 24/32-bits image - break; - } - // From A Quick Introduction to IFF.txt: // // If a deep ILBM (like 12 or 24 planes), there should be no CMAP @@ -935,11 +1086,6 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he case 48: // rgb (SView5 extension) case 64: // rgba (SView5 extension) - if (isPbm) { - // PBM cannot be a 48/64-bits image - break; - } - // From https://aminet.net/package/docs/misc/ILBM64: // // Previously, the IFF-ILBM fileformat has been @@ -1020,17 +1166,17 @@ bool ABITChunk::isValid() const return chunkId() == ABITChunk::defaultChunkId(); } -QByteArray ABITChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, bool isPbm) const +QByteArray ABITChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, const QByteArray& formType) const { if (!isValid() || header == nullptr || d == nullptr) { return {}; } - if (header->compression() != BMHDChunk::Compression::Uncompressed || isPbm) { + if (header->compression() != BMHDChunk::Compression::Uncompressed || formType != ACBM_FORM_TYPE) { return {}; } // convert ABIT data to an ILBM line on the fly - auto ilbmLine = QByteArray(strideSize(header, isPbm), char()); + auto ilbmLine = QByteArray(strideSize(header, formType), char()); auto rowSize = header->rowLen(); auto height = header->height(); if (_y >= height) { @@ -1054,7 +1200,7 @@ QByteArray ABITChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CA if (!buf.open(QBuffer::ReadOnly)) { return {}; } - return BODYChunk::strideRead(&buf, header, camg, cmap, isPbm); + return BODYChunk::strideRead(&buf, header, camg, cmap, ILBM_FORM_TYPE); } bool ABITChunk::resetStrideRead(QIODevice *d) const @@ -1063,6 +1209,33 @@ bool ABITChunk::resetStrideRead(QIODevice *d) const return BODYChunk::resetStrideRead(d); } + +/* ********************** + * *** FORM Interface *** + * ********************** */ + +IFOR_Chunk::~IFOR_Chunk() +{ + +} + +IFOR_Chunk::IFOR_Chunk() : IFFChunk() +{ + +} + +QImageIOHandler::Transformation IFOR_Chunk::transformation() const +{ + auto exifs = IFFChunk::searchT(chunks()); + if (!exifs.isEmpty()) { + auto exif = exifs.first()->value(); + if (!exif.isEmpty()) + return exif.transformation(); + } + return QImageIOHandler::Transformation::TransformationNone; +} + + /* ****************** * *** FORM Chunk *** * ****************** */ @@ -1072,7 +1245,7 @@ FORMChunk::~FORMChunk() } -FORMChunk::FORMChunk() : IFFChunk() +FORMChunk::FORMChunk() : IFOR_Chunk() { } @@ -1093,12 +1266,18 @@ bool FORMChunk::innerReadStructure(QIODevice *d) } _type = d->read(4); auto ok = true; + + // NOTE: add new supported type to CATChunk as well. if (_type == ILBM_FORM_TYPE) { setChunks(IFFChunk::innerFromDevice(d, &ok, this)); } else if (_type == PBM__FORM_TYPE) { setChunks(IFFChunk::innerFromDevice(d, &ok, this)); } else if (_type == ACBM_FORM_TYPE) { setChunks(IFFChunk::innerFromDevice(d, &ok, this)); + } else if (_type == RGB8_FORM_TYPE) { + setChunks(IFFChunk::innerFromDevice(d, &ok, this)); + } else if (_type == RGBN_FORM_TYPE) { + setChunks(IFFChunk::innerFromDevice(d, &ok, this)); } return ok; } @@ -1124,7 +1303,10 @@ QImage::Format FORMChunk::format() const } auto camgs = IFFChunk::searchT(chunks()); auto modeId = BODYChunk::safeModeId(h, camgs.isEmpty() ? nullptr : camgs.first(), cmaps.isEmpty() ? nullptr : cmaps.first()); - if (h->bitplanes() == 24) { + if (h->bitplanes() == 13) { + return QImage::Format_RGB888; // NOTE: with a little work you could use Format_RGB444 + } + if (h->bitplanes() == 24 || h->bitplanes() == 25) { return QImage::Format_RGB888; } if (h->bitplanes() == 48) { @@ -1154,6 +1336,7 @@ QImage::Format FORMChunk::format() const return QImage::Format_Grayscale8; } + qCDebug(LOG_IFFPLUGIN) << "FORMChunk::format: Unsupported" << h->bitplanes() << "bitplanes"; } return QImage::Format_Invalid; @@ -1177,7 +1360,7 @@ FOR4Chunk::~FOR4Chunk() } -FOR4Chunk::FOR4Chunk() : IFFChunk() +FOR4Chunk::FOR4Chunk() : IFOR_Chunk() { } @@ -1235,6 +1418,52 @@ QSize FOR4Chunk::size() const return headers.first()->size(); } +/* ****************** + * *** CAT Chunk *** + * ****************** */ + +CATChunk::~CATChunk() +{ + +} + +CATChunk::CATChunk() : IFFChunk() +{ + +} + +bool CATChunk::isValid() const +{ + return chunkId() == CATChunk::defaultChunkId(); +} + +QByteArray CATChunk::catType() const +{ + return _type; +} + +bool CATChunk::innerReadStructure(QIODevice *d) +{ + if (bytes() < 4) { + return false; + } + _type = d->read(4); + auto ok = true; + + // supports the image formats of FORMChunk. + if (_type == ILBM_FORM_TYPE) { + setChunks(IFFChunk::innerFromDevice(d, &ok, this)); + } else if (_type == PBM__FORM_TYPE) { + setChunks(IFFChunk::innerFromDevice(d, &ok, this)); + } else if (_type == ACBM_FORM_TYPE) { + setChunks(IFFChunk::innerFromDevice(d, &ok, this)); + } else if (_type == RGB8_FORM_TYPE) { + setChunks(IFFChunk::innerFromDevice(d, &ok, this)); + } else if (_type == RGBN_FORM_TYPE) { + setChunks(IFFChunk::innerFromDevice(d, &ok, this)); + } + return ok; +} /* ****************** * *** TBHD Chunk *** @@ -1477,7 +1706,7 @@ QByteArray RGBAChunk::readStride(QIODevice *d, const TBHDChunk *header) const } // compressed - for(;!d->atEnd() && _readBuffer.size() < readSize;) { + for (auto nextPos = nextChunkPos(); !d->atEnd() && d->pos() < nextPos && _readBuffer.size() < readSize;) { QByteArray buf(readSize * size().height(), char()); qint64 rr = -1; if (header->compression() == TBHDChunk::Compression::Rle) { diff --git a/src/imageformats/chunks_p.h b/src/imageformats/chunks_p.h index f8cb533..be4ec4a 100644 --- a/src/imageformats/chunks_p.h +++ b/src/imageformats/chunks_p.h @@ -79,6 +79,9 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN) #define ACBM_FORM_TYPE QByteArray("ACBM") #define ILBM_FORM_TYPE QByteArray("ILBM") #define PBM__FORM_TYPE QByteArray("PBM ") +#define RGB8_FORM_TYPE QByteArray("RGB8") +#define RGBN_FORM_TYPE QByteArray("RGBN") + #define CIMG_FOR4_TYPE QByteArray("CIMG") #define TBMP_FOR4_TYPE QByteArray("TBMP") @@ -345,7 +348,8 @@ class BMHDChunk: public IFFChunk public: enum Compression { Uncompressed = 0, /**< Image data are uncompressed. */ - Rle = 1 /**< Image data are RLE compressed. */ + Rle = 1, /**< Image data are RLE compressed. */ + RgbN8 = 4 /**< RGB8/RGBN compresson. */ }; enum Masking { None = 0, /**< Designates an opaque rectangular image. */ @@ -626,11 +630,11 @@ public: * \param header The bitmap header. * \param camg The CAMG chunk (optional) * \param cmap The CMAP chunk (optional) - * \param isPbm Set to true if the formType() == "PBM " + * \param formType The type of the current form chunk. * \return The scanline as requested for QImage. * \warning Call resetStrideRead() once before this one. */ - virtual QByteArray strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr, bool isPbm = false) const; + virtual QByteArray strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr, const QByteArray& formType = ILBM_FORM_TYPE) const; /*! * \brief resetStrideRead @@ -655,12 +659,18 @@ public: protected: /*! * \brief strideSize - * \param isPbm Set true if the image is PBM. + * \param formType The type of the current form chunk. * \return The size of data to have to decode an image row. */ - quint32 strideSize(const BMHDChunk *header, bool isPbm) const; + quint32 strideSize(const BMHDChunk *header, const QByteArray& formType) const; - QByteArray deinterleave(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr, bool isPbm = false) const; + QByteArray deinterleave(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr) const; + + QByteArray pbm(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr) const; + + QByteArray rgb8(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr) const; + + QByteArray rgbN(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr) const; private: mutable QByteArray _readBuffer; @@ -682,7 +692,7 @@ public: CHUNKID_DEFINE(ABIT_CHUNK) - virtual QByteArray strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr, bool isPbm = false) const override; + virtual QByteArray strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr, const QByteArray& formType = ACBM_FORM_TYPE) const override; virtual bool resetStrideRead(QIODevice *d) const override; @@ -690,11 +700,52 @@ private: mutable qint32 _y; }; +/*! + * \brief The IFOR_Chunk class + * Interface for FORM chunks. + */ +class IFOR_Chunk : public IFFChunk +{ +public: + virtual ~IFOR_Chunk() override; + IFOR_Chunk(); + + /*! + * \brief isSupported + * \return True if the form is supported by the plugin. + */ + virtual bool isSupported() const = 0; + + /*! + * \brief formType + * \return The type of image data contained in the form. + */ + virtual QByteArray formType() const = 0; + + /*! + * \brief format + * \return The Qt image format the form is converted to. + */ + virtual QImage::Format format() const = 0; + + /*! + * \brief transformation + * \return The image transformation. + * \note The Default implentation returns the trasformation of EXIF chunk (if any). + */ + virtual QImageIOHandler::Transformation transformation() const; + + /*! + * \brief size + * \return The image size in pixels. + */ + virtual QSize size() const = 0; +}; /*! * \brief The FORMChunk class */ -class FORMChunk : public IFFChunk +class FORMChunk : public IFOR_Chunk { QByteArray _type; @@ -706,13 +757,13 @@ public: virtual bool isValid() const override; - bool isSupported() const; + virtual bool isSupported() const override; - QByteArray formType() const; + virtual QByteArray formType() const override; - QImage::Format format() const; + virtual QImage::Format format() const override; - QSize size() const; + virtual QSize size() const override; CHUNKID_DEFINE(FORM_CHUNK) @@ -724,7 +775,7 @@ protected: /*! * \brief The FOR4Chunk class */ -class FOR4Chunk : public IFFChunk +class FOR4Chunk : public IFOR_Chunk { QByteArray _type; @@ -738,13 +789,13 @@ public: virtual qint32 alignBytes() const override; - bool isSupported() const; + virtual bool isSupported() const override; - QByteArray formType() const; + virtual QByteArray formType() const override; - QImage::Format format() const; + virtual QImage::Format format() const override; - QSize size() const; + virtual QSize size() const override; CHUNKID_DEFINE(FOR4_CHUNK) @@ -752,6 +803,29 @@ protected: virtual bool innerReadStructure(QIODevice *d) override; }; +/*! + * \brief The CATChunk class + */ +class CATChunk : public IFFChunk +{ + QByteArray _type; + +public: + virtual ~CATChunk() override; + CATChunk(); + CATChunk(const CATChunk& other) = default; + CATChunk& operator =(const CATChunk& other) = default; + + virtual bool isValid() const override; + + QByteArray catType() const; + + CHUNKID_DEFINE(CAT__CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + /*! * \brief The TBHDChunk class */ diff --git a/src/imageformats/iff.cpp b/src/imageformats/iff.cpp index 2eb8de0..881f619 100644 --- a/src/imageformats/iff.cpp +++ b/src/imageformats/iff.cpp @@ -16,28 +16,38 @@ class IFFHandlerPrivate { public: - IFFHandlerPrivate() {} - ~IFFHandlerPrivate() {} + IFFHandlerPrivate() + : m_imageNumber(0) + , m_imageCount(0) + { - bool readStructure(QIODevice *d) { + } + ~IFFHandlerPrivate() + { + + } + + bool readStructure(QIODevice *d) + { if (d == nullptr) { return {}; } - if (!_chunks.isEmpty()) { + if (!m_chunks.isEmpty()) { return true; } auto ok = false; auto chunks = IFFChunk::fromDevice(d, &ok); if (ok) { - _chunks = chunks; + m_chunks = chunks; } return ok; } template - static QList searchForms(const IFFChunk::ChunkList &chunks, bool supportedOnly = true) { + static QList searchForms(const IFFChunk::ChunkList &chunks, bool supportedOnly = true) + { QList list; auto cid = T::defaultChunkId(); auto forms = IFFChunk::search(cid, chunks); @@ -50,11 +60,25 @@ public: } template - QList searchForms(bool supportedOnly = true) { - return searchForms(_chunks, supportedOnly); + QList searchForms(bool supportedOnly = true) + { + return searchForms(m_chunks, supportedOnly); } - IFFChunk::ChunkList _chunks; + IFFChunk::ChunkList m_chunks; + + /*! + * \brief m_imageNumber + * Value set by QImageReader::jumpToImage() or QImageReader::jumpToNextImage(). + * The number of view selected in a multiview image. + */ + qint32 m_imageNumber; + + /*! + * \brief m_imageCount + * The total number of views (cache value) + */ + mutable qint32 m_imageCount; }; @@ -62,6 +86,7 @@ IFFHandler::IFFHandler() : QImageIOHandler() , d(new IFFHandlerPrivate) { + } bool IFFHandler::canRead() const @@ -204,7 +229,8 @@ bool IFFHandler::readStandardImage(QImage *image) if (forms.isEmpty()) { return false; } - auto &&form = forms.first(); + auto cin = qBound(0, currentImageNumber(), int(forms.size() - 1)); + auto &&form = forms.at(cin); // show the first one (I don't have a sample with many images) auto headers = IFFChunk::searchT(form); @@ -260,10 +286,9 @@ bool IFFHandler::readStandardImage(QImage *image) qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image data"; return false; } - auto isPbm = form->formType() == PBM__FORM_TYPE; for (auto y = 0, h = img.height(); y < h; ++y) { auto line = reinterpret_cast(img.scanLine(y)); - auto ba = body->strideRead(device(), header, camg, cmap, isPbm); + auto ba = body->strideRead(device(), header, camg, cmap, form->formType()); if (ba.isEmpty()) { qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image scanline"; return false; @@ -285,7 +310,8 @@ bool IFFHandler::readMayaImage(QImage *image) if (forms.isEmpty()) { return false; } - auto &&form = forms.first(); + auto cin = qBound(0, currentImageNumber(), int(forms.size() - 1)); + auto &&form = forms.at(cin); // show the first one (I don't have a sample with many images) auto headers = IFFChunk::searchT(form); @@ -366,42 +392,88 @@ bool IFFHandler::supportsOption(ImageOption option) const if (option == QImageIOHandler::ImageFormat) { return true; } + if (option == QImageIOHandler::ImageTransformation) { + return true; + } return false; } QVariant IFFHandler::option(ImageOption option) const { - QVariant v; + if (!supportsOption(option)) { + return {}; + } + + const IFOR_Chunk *form = nullptr; + if (d->readStructure(device())) { + auto forms = d->searchForms(); + auto for4s = d->searchForms(); + auto cin = currentImageNumber(); + if (!forms.isEmpty()) + form = cin < forms.size() ? forms.at(cin) : forms.first(); + else if (!for4s.isEmpty()) + form = cin < for4s.size() ? for4s.at(cin) : for4s.first(); + } + if (form == nullptr) { + return {}; + } if (option == QImageIOHandler::Size) { - if (d->readStructure(device())) { - auto forms = d->searchForms(); - if (!forms.isEmpty()) - if (auto &&form = forms.first()) - v = QVariant::fromValue(form->size()); - - auto for4s = d->searchForms(); - if (!for4s.isEmpty()) - if (auto &&form = for4s.first()) - v = QVariant::fromValue(form->size()); - } + return QVariant::fromValue(form->size()); } if (option == QImageIOHandler::ImageFormat) { - if (d->readStructure(device())) { - auto forms = d->searchForms(); - if (!forms.isEmpty()) - if (auto &&form = forms.first()) - v = QVariant::fromValue(form->format()); - - auto for4s = d->searchForms(); - if (!for4s.isEmpty()) - if (auto &&form = for4s.first()) - v = QVariant::fromValue(form->format()); - } + return QVariant::fromValue(form->format()); } - return v; + if (option == QImageIOHandler::ImageTransformation) { + return QVariant::fromValue(form->transformation()); + } + + return {}; +} + +bool IFFHandler::jumpToNextImage() +{ + return jumpToImage(d->m_imageNumber + 1); +} + +bool IFFHandler::jumpToImage(int imageNumber) +{ + if (imageNumber < 0 || imageNumber >= imageCount()) { + return false; + } + d->m_imageNumber = imageNumber; + return true; +} + +int IFFHandler::imageCount() const +{ + // NOTE: image count is cached for performance reason + auto &&count = d->m_imageCount; + if (count > 0) { + return count; + } + + count = QImageIOHandler::imageCount(); + if (!d->readStructure(device())) { + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::imageCount() invalid IFF structure"; + return count; + } + + auto forms = d->searchForms(); + auto for4s = d->searchForms(); + if (!forms.isEmpty()) + count = forms.size(); + else if (!for4s.isEmpty()) + count = for4s.size(); + + return count; +} + +int IFFHandler::currentImageNumber() const +{ + return d->m_imageNumber; } QImageIOPlugin::Capabilities IFFPlugin::capabilities(QIODevice *device, const QByteArray &format) const diff --git a/src/imageformats/iff_p.h b/src/imageformats/iff_p.h index df36b31..114844b 100644 --- a/src/imageformats/iff_p.h +++ b/src/imageformats/iff_p.h @@ -23,6 +23,11 @@ public: bool supportsOption(QImageIOHandler::ImageOption option) const override; QVariant option(QImageIOHandler::ImageOption option) const override; + bool jumpToNextImage() override; + bool jumpToImage(int imageNumber) override; + int imageCount() const override; + int currentImageNumber() const override; + static bool canRead(QIODevice *device); private: