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 0000000..d063275 Binary files /dev/null and b/autotests/read/iff/cat_ilbm.iff differ 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 0000000..75524c1 Binary files /dev/null and b/autotests/read/iff/sv5_testcard_rgb_rgb8.iff differ diff --git a/autotests/read/iff/sv5_testcard_rgb_rgb8.iff.json b/autotests/read/iff/sv5_testcard_rgb_rgb8.iff.json new file mode 100644 index 0000000..8ce845a --- /dev/null +++ b/autotests/read/iff/sv5_testcard_rgb_rgb8.iff.json @@ -0,0 +1,5 @@ +[ + { + "fileName" : "ps_testcard_rgb_maya.png" + } +] diff --git a/src/imageformats/chunks.cpp b/src/imageformats/chunks.cpp index 0d30e58..d94d273 100644 --- a/src/imageformats/chunks.cpp +++ b/src/imageformats/chunks.cpp @@ -25,7 +25,9 @@ static QString dataToString(const IFFChunk *chunk) if (chunk == nullptr || !chunk->isValid()) { 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: