From fd864e6f55fdc5cdca89d75f6348f2005d8ac7c1 Mon Sep 17 00:00:00 2001 From: Mirco Miranda Date: Thu, 24 Jul 2025 07:37:58 +0200 Subject: [PATCH] IFF: add support for uncompressed ACBM type --- autotests/read/iff/testcard_acbm.iff | Bin 0 -> 13380 bytes autotests/read/iff/testcard_acbm.png | Bin 0 -> 3296 bytes src/imageformats/chunks.cpp | 73 +++++++++++++++++++++++++-- src/imageformats/chunks_p.h | 37 ++++++++++++-- src/imageformats/iff.cpp | 16 ++++++ 5 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 autotests/read/iff/testcard_acbm.iff create mode 100644 autotests/read/iff/testcard_acbm.png diff --git a/autotests/read/iff/testcard_acbm.iff b/autotests/read/iff/testcard_acbm.iff new file mode 100644 index 0000000000000000000000000000000000000000..d3098f2331651068ddf0afdea956ad0eaad18dbb GIT binary patch literal 13380 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 #include #include @@ -243,7 +244,9 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk * for (; !d->atEnd() && (nextChunkPos == 0 || d->pos() < nextChunkPos);) { auto cid = d->peek(4); QSharedPointer chunk; - if (cid == ANNO_CHUNK) { + if (cid == ABIT_CHUNK) { + chunk = QSharedPointer(new ABITChunk()); + } else if (cid == ANNO_CHUNK) { chunk = QSharedPointer(new ANNOChunk()); } else if (cid == AUTH_CHUNK) { chunk = QSharedPointer(new AUTHChunk()); @@ -285,7 +288,7 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk * chunk = QSharedPointer(new XMP0Chunk()); } else { // unknown chunk chunk = QSharedPointer(new IFFChunk()); - qCInfo(LOG_IFFPLUGIN) << "IFFChunk::innerFromDevice: unknown chunk" << cid; + qCDebug(LOG_IFFPLUGIN) << "IFFChunk::innerFromDevice: unknown chunk" << cid; } // change the alignment to the one of main chunk (required for unknown Maya IFF chunks) @@ -596,7 +599,7 @@ bool BODYChunk::isValid() const QByteArray BODYChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, bool isPbm) const { - if (!isValid() || header == nullptr) { + if (!isValid() || header == nullptr || d == nullptr) { return {}; } @@ -849,6 +852,68 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he return ba; } +/* ****************** + * *** ABIT Chunk *** + * ****************** */ + +ABITChunk::~ABITChunk() +{ + +} + +ABITChunk::ABITChunk() : BODYChunk() +{ + +} + +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 +{ + if (!isValid() || header == nullptr || d == nullptr) { + return {}; + } + if (header->compression() != BMHDChunk::Compression::Uncompressed || isPbm) { + return {}; + } + + // convert ABIT data to an ILBM line on the fly + auto ilbmLine = QByteArray(strideSize(header, isPbm), char()); + auto rowSize = header->rowLen(); + auto height = header->height(); + if (_y >= height) { + return {}; + } + for (qint32 plane = 0, planes = qint32(header->bitplanes()); plane < planes; ++plane) { + if (!seek(d, qint64(plane) * rowSize * height + _y * rowSize)) + return {}; + auto offset = qint64(plane) * rowSize; + if (offset + rowSize > ilbmLine.size()) + return {}; + if (d->read(ilbmLine.data() + offset, rowSize) != rowSize) + return {}; + } + // next line on the next run + ++_y; + + // decode the ILBM line + QBuffer buf; + buf.setData(ilbmLine); + if (!buf.open(QBuffer::ReadOnly)) { + return {}; + } + return BODYChunk::strideRead(&buf, header, camg, cmap, isPbm); +} + +bool ABITChunk::resetStrideRead(QIODevice *d) const +{ + _y = 0; + return BODYChunk::resetStrideRead(d); +} + /* ****************** * *** FORM Chunk *** * ****************** */ @@ -883,6 +948,8 @@ bool FORMChunk::innerReadStructure(QIODevice *d) 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)); } return ok; } diff --git a/src/imageformats/chunks_p.h b/src/imageformats/chunks_p.h index 73b1aa1..b58640f 100644 --- a/src/imageformats/chunks_p.h +++ b/src/imageformats/chunks_p.h @@ -44,6 +44,7 @@ #define PRO8_CHUNK QByteArray("PRO8") // FORM ILBM IFF +#define ABIT_CHUNK QByteArray("ABIT") #define BMHD_CHUNK QByteArray("BMHD") #define BODY_CHUNK QByteArray("BODY") #define CAMG_CHUNK QByteArray("CAMG") @@ -70,6 +71,7 @@ #define VERS_CHUNK QByteArray("VERS") #define XMP0_CHUNK QByteArray("XMP0") // https://aminet.net/package/docs/misc/IFF-metadata +#define ACBM_FORM_TYPE QByteArray("ACBM") #define ILBM_FORM_TYPE QByteArray("ILBM") #define PBM__FORM_TYPE QByteArray("PBM ") #define CIMG_FOR4_TYPE QByteArray("CIMG") @@ -573,20 +575,21 @@ public: * \return The scanline as requested for QImage. * \warning Call resetStrideRead() once before this one. */ - 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, bool isPbm = false) const; /*! * \brief resetStrideRead * Reset the stride read set the position at the beginning of the data and reset all buffers. - * \param d The device + * \param d The device. * \param header The BMHDChunk chunk (mandatory) * \param camg The CAMG chunk (optional) * \return True on success, otherwise false. * \sa strideRead + * \note Must be called once before strideRead(). */ - bool resetStrideRead(QIODevice *d) const; + virtual bool resetStrideRead(QIODevice *d) const; -private: +protected: /*! * \brief strideSize * \param isPbm Set true if the image is PBM. @@ -596,9 +599,35 @@ private: QByteArray deinterleave(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr, bool isPbm = false) const; +private: mutable QByteArray _readBuffer; }; + +/*! + * \brief The ABITChunk class + */ +class ABITChunk : public BODYChunk +{ +public: + virtual ~ABITChunk() override; + ABITChunk(); + ABITChunk(const ABITChunk& other) = default; + ABITChunk& operator =(const ABITChunk& other) = default; + + virtual bool isValid() const override; + + 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 bool resetStrideRead(QIODevice *d) const override; + +private: + mutable qint32 _y; +}; + + /*! * \brief The FORMChunk class */ diff --git a/src/imageformats/iff.cpp b/src/imageformats/iff.cpp index 48ac874..d62dfe7 100644 --- a/src/imageformats/iff.cpp +++ b/src/imageformats/iff.cpp @@ -91,6 +91,17 @@ bool IFFHandler::canRead(QIODevice *device) return false; } + // I avoid parsing obviously incorrect files + auto cid = device->peek(4); + if (cid != CAT__CHUNK && + cid != FORM_CHUNK && + cid != LIST_CHUNK && + cid != CAT4_CHUNK && + cid != FOR4_CHUNK && + cid != LIS4_CHUNK) { + return false; + } + auto ok = false; auto pos = device->pos(); auto chunks = IFFChunk::fromDevice(device, &ok); @@ -220,6 +231,11 @@ bool IFFHandler::readStandardImage(QImage *image) } auto bodies = IFFChunk::searchT(form); + if (bodies.isEmpty()) { + auto abits = IFFChunk::searchT(form); + for (auto &&abit : abits) + bodies.append(abit); + } if (bodies.isEmpty()) { img.fill(0); } else {