diff --git a/autotests/read/iff/aga_pchg_amiga_16cl.iff b/autotests/read/iff/aga_pchg_amiga_16cl.iff new file mode 100644 index 0000000..235cf39 Binary files /dev/null and b/autotests/read/iff/aga_pchg_amiga_16cl.iff differ diff --git a/autotests/read/iff/aga_pchg_amiga_16cl.png b/autotests/read/iff/aga_pchg_amiga_16cl.png new file mode 100644 index 0000000..ab426d2 Binary files /dev/null and b/autotests/read/iff/aga_pchg_amiga_16cl.png differ diff --git a/autotests/read/iff/aga_pchg_amiga_64cl.iff b/autotests/read/iff/aga_pchg_amiga_64cl.iff new file mode 100644 index 0000000..2e6d001 Binary files /dev/null and b/autotests/read/iff/aga_pchg_amiga_64cl.iff differ diff --git a/autotests/read/iff/aga_pchg_amiga_64cl.png b/autotests/read/iff/aga_pchg_amiga_64cl.png new file mode 100644 index 0000000..dd1e258 Binary files /dev/null and b/autotests/read/iff/aga_pchg_amiga_64cl.png differ diff --git a/autotests/read/iff/ham5.iff b/autotests/read/iff/ham5.iff new file mode 100644 index 0000000..4d60108 Binary files /dev/null and b/autotests/read/iff/ham5.iff differ diff --git a/autotests/read/iff/ham5.png b/autotests/read/iff/ham5.png new file mode 100644 index 0000000..e19160f Binary files /dev/null and b/autotests/read/iff/ham5.png differ diff --git a/autotests/read/iff/ham8.iff b/autotests/read/iff/ham8.iff new file mode 100644 index 0000000..c750c3a Binary files /dev/null and b/autotests/read/iff/ham8.iff differ diff --git a/autotests/read/iff/ham8.png b/autotests/read/iff/ham8.png new file mode 100644 index 0000000..5853e7d Binary files /dev/null and b/autotests/read/iff/ham8.png differ diff --git a/autotests/read/iff/ocs_pchg_amiga_16cl.iff b/autotests/read/iff/ocs_pchg_amiga_16cl.iff new file mode 100644 index 0000000..b857b47 Binary files /dev/null and b/autotests/read/iff/ocs_pchg_amiga_16cl.iff differ diff --git a/autotests/read/iff/ocs_pchg_amiga_16cl.png b/autotests/read/iff/ocs_pchg_amiga_16cl.png new file mode 100644 index 0000000..f4a98c7 Binary files /dev/null and b/autotests/read/iff/ocs_pchg_amiga_16cl.png differ diff --git a/autotests/read/iff/ocs_pchg_amiga_64cl.iff b/autotests/read/iff/ocs_pchg_amiga_64cl.iff new file mode 100644 index 0000000..611ee4a Binary files /dev/null and b/autotests/read/iff/ocs_pchg_amiga_64cl.iff differ diff --git a/autotests/read/iff/ocs_pchg_amiga_64cl.png b/autotests/read/iff/ocs_pchg_amiga_64cl.png new file mode 100644 index 0000000..742ac1c Binary files /dev/null and b/autotests/read/iff/ocs_pchg_amiga_64cl.png differ diff --git a/autotests/read/iff/ps_testcard_rgb16_sview5.iff b/autotests/read/iff/sv5_testcard_rgb16.iff similarity index 100% rename from autotests/read/iff/ps_testcard_rgb16_sview5.iff rename to autotests/read/iff/sv5_testcard_rgb16.iff diff --git a/autotests/read/iff/ps_testcard_rgb16_sview5.iff.json b/autotests/read/iff/sv5_testcard_rgb16.iff.json similarity index 100% rename from autotests/read/iff/ps_testcard_rgb16_sview5.iff.json rename to autotests/read/iff/sv5_testcard_rgb16.iff.json diff --git a/autotests/read/iff/ps_testcard_rgba16_sview5.iff b/autotests/read/iff/sv5_testcard_rgba16.iff similarity index 100% rename from autotests/read/iff/ps_testcard_rgba16_sview5.iff rename to autotests/read/iff/sv5_testcard_rgba16.iff diff --git a/autotests/read/iff/ps_testcard_rgba16_sview5.iff.json b/autotests/read/iff/sv5_testcard_rgba16.iff.json similarity index 100% rename from autotests/read/iff/ps_testcard_rgba16_sview5.iff.json rename to autotests/read/iff/sv5_testcard_rgba16.iff.json diff --git a/src/imageformats/chunks.cpp b/src/imageformats/chunks.cpp index d36e263..c3d7f44 100644 --- a/src/imageformats/chunks.cpp +++ b/src/imageformats/chunks.cpp @@ -10,6 +10,7 @@ #include #include +#include #ifdef QT_DEBUG Q_LOGGING_CATEGORY(LOG_IFFPLUGIN, "kf.imageformats.plugins.iff", QtDebugMsg) @@ -306,6 +307,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk * chunk = QSharedPointer(new ICCPChunk()); } else if (cid == NAME_CHUNK) { chunk = QSharedPointer(new NAMEChunk()); + } else if (cid == PCHG_CHUNK) { + chunk = QSharedPointer(new PCHGChunk()); } else if (cid == RAST_CHUNK) { chunk = QSharedPointer(new RASTChunk()); } else if (cid == RGBA_CHUNK) { @@ -316,6 +319,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk * chunk = QSharedPointer(new TBHDChunk()); } else if (cid == VERS_CHUNK) { chunk = QSharedPointer(new VERSChunk()); + } else if (cid == XBMI_CHUNK) { + chunk = QSharedPointer(new XBMIChunk()); } else if (cid == XMP0_CHUNK) { chunk = QSharedPointer(new XMP0Chunk()); } else { // unknown chunk @@ -677,6 +682,51 @@ bool DPIChunk::innerReadStructure(QIODevice *d) return cacheData(d); } +/* ****************** + * *** XBMI Chunk *** + * ****************** */ + +XBMIChunk::~XBMIChunk() +{ + +} + +XBMIChunk::XBMIChunk() : DPIChunk() +{ +} + +bool XBMIChunk::isValid() const +{ + if (dpiX() == 0 || dpiY() == 0) { + return false; + } + return chunkId() == XBMIChunk::defaultChunkId(); +} + +quint16 XBMIChunk::dpiX() const +{ + if (bytes() < 6) { + return 0; + } + return i16(data().at(3), data().at(2)); +} + +quint16 XBMIChunk::dpiY() const +{ + if (bytes() < 6) { + return 0; + } + return i16(data().at(5), data().at(4)); +} + +XBMIChunk::PictureType XBMIChunk::pictureType() const +{ + if (bytes() < 6) { + return PictureType(-1); + } + return PictureType(i16(data().at(1), data().at(0))); +} + /* ****************** * *** BODY Chunk *** * ****************** */ @@ -884,7 +934,10 @@ quint32 BODYChunk::strideSize(const BMHDChunk *header, const QByteArray& formTyp } // ILBM - return header->rowLen() * header->bitplanes(); + auto sz = header->rowLen() * header->bitplanes(); + if (header->masking() == BMHDChunk::Masking::HasMask) + sz += header->rowLen(); + return sz; } QByteArray BODYChunk::pbm(const QByteArray &planes, qint32, const BMHDChunk *header, const CAMGChunk *, const CMAPChunk *, const IPALChunk *) const @@ -959,11 +1012,18 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH ba = QByteArray(rowLen * 8 * 3, char()); auto pal = cmap->palette(); if (ipal) { - auto tmp = ipal->palette(y, header->height()); + auto tmp = ipal->palette(y); if (tmp.size() == pal.size()) pal = tmp; } - auto max = (1 << (bitplanes - 2)) - 1; + // HAM 6: 2 control bits+4 bits of data, 16-color palette + // + // HAM 8: 2 control bits+6 bits of data, 64-color palette + // + // HAM 5: 1 control bit (and 1 hardwired to zero)+4 bits of data + // (red and green modify operations are unavailable) + auto ctlbits = bitplanes > 5 ? 2 : 1; + auto max = (1 << (bitplanes - ctlbits)) - 1; quint8 prev[3] = {}; for (qint32 i = 0, cnt = 0; i < rowLen; ++i) { for (qint32 j = 0; j < 8; ++j, ++cnt) { @@ -971,11 +1031,14 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH for (qint32 k = 0, msk = (1 << (7 - j)); k < bitplanes; ++k) { if ((planes.at(k * rowLen + i) & msk) == 0) continue; - if (k < bitplanes - 2) + if (k < bitplanes - ctlbits) idx |= 1 << k; else ctl |= 1 << (bitplanes - k - 1); } + if (ctl && ctlbits == 1) { + ctl <<= 1; // HAM 5 has only 1 control bit and the LSB is always 0 + } switch (ctl) { case 1: // red prev[0] = idx * 255 / max; @@ -1049,22 +1112,23 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH for (qint32 i = 0; i < rowLen; ++i) { for (qint32 k = 0, i8 = i * 8; k < bitplanes; ++k) { auto v = planes.at(k * rowLen + i); + auto msk = 1 << k; if (v & (1 << 7)) - ba[i8] |= 1 << k; + ba[i8] |= msk; if (v & (1 << 6)) - ba[i8 + 1] |= 1 << k; + ba[i8 + 1] |= msk; if (v & (1 << 5)) - ba[i8 + 2] |= 1 << k; + ba[i8 + 2] |= msk; if (v & (1 << 4)) - ba[i8 + 3] |= 1 << k; + ba[i8 + 3] |= msk; if (v & (1 << 3)) - ba[i8 + 4] |= 1 << k; + ba[i8 + 4] |= msk; if (v & (1 << 2)) - ba[i8 + 5] |= 1 << k; + ba[i8 + 5] |= msk; if (v & (1 << 1)) - ba[i8 + 6] |= 1 << k; + ba[i8 + 6] |= msk; if (v & 1) - ba[i8 + 7] |= 1 << k; + ba[i8 + 7] |= msk; } } } @@ -1257,8 +1321,9 @@ QImage::Format IFOR_Chunk::optionformat() const { auto fmt = this->format(); if (fmt == QImage::Format_Indexed8) { - if (searchIPal()) - fmt = FORMAT_RGB_8BIT; + if (auto ipal = searchIPal()) { + fmt = ipal->hasAlpha() ? FORMAT_RGBA_8BIT : FORMAT_RGB_8BIT; + } } return fmt; } @@ -1282,6 +1347,10 @@ const IPALChunk *IFOR_Chunk::searchIPal() const if (!rast.isEmpty()) { ipal = rast.first(); } + auto pchg = IFFChunk::searchT(this); + if (!pchg.isEmpty()) { + ipal = pchg.first(); + } if (ipal && ipal->isValid()) { return ipal; } @@ -1372,11 +1441,6 @@ QImage::Format FORMChunk::format() const return QImage::Format_RGBA64; } if (h->bitplanes() >= 1 && h->bitplanes() <= 8) { - if (!IFFChunk::search(PCHG_CHUNK, chunks()).isEmpty()) { - qCDebug(LOG_IFFPLUGIN) << "FORMChunk::format(): PCHG chunk is not supported"; - return QImage::Format_Invalid; - } - if (h->bitplanes() >= BITPLANES_HAM_MIN && h->bitplanes() <= BITPLANES_HAM_MAX) { if (modeId & CAMGChunk::ModeId::Ham) return FORMAT_RGB_8BIT; @@ -2310,7 +2374,9 @@ BEAMChunk::~BEAMChunk() } -BEAMChunk::BEAMChunk() : IPALChunk() +BEAMChunk::BEAMChunk() + : IPALChunk() + , _height() { } @@ -2320,8 +2386,20 @@ bool BEAMChunk::isValid() const return chunkId() == BEAMChunk::defaultChunkId(); } -QList BEAMChunk::palette(qint32 y, qint32 height) const +IPALChunk *BEAMChunk::clone() const { + return new BEAMChunk(*this); +} + +bool BEAMChunk::initialize(const QList &, qint32 height) +{ + _height = height; + return true; +} + +QList BEAMChunk::palette(qint32 y) const +{ + auto &&height = _height; if (height < 1) { return {}; } @@ -2378,7 +2456,9 @@ SHAMChunk::~SHAMChunk() } -SHAMChunk::SHAMChunk() : IPALChunk() +SHAMChunk::SHAMChunk() + : IPALChunk() + , _height() { } @@ -2398,8 +2478,14 @@ bool SHAMChunk::isValid() const return chunkId() == SHAMChunk::defaultChunkId(); } -QList SHAMChunk::palette(qint32 y, qint32 height) const +IPALChunk *SHAMChunk::clone() const { + return new SHAMChunk(*this); +} + +QList SHAMChunk::palette(qint32 y) const +{ + auto && height = _height; if (height < 1) { return {}; } @@ -2426,6 +2512,12 @@ QList SHAMChunk::palette(qint32 y, qint32 height) const return pal; } +bool SHAMChunk::initialize(const QList &, qint32 height) +{ + _height = height; + return true; +} + bool SHAMChunk::innerReadStructure(QIODevice *d) { return cacheData(d); @@ -2440,7 +2532,9 @@ RASTChunk::~RASTChunk() } -RASTChunk::RASTChunk() : IPALChunk() +RASTChunk::RASTChunk() + : IPALChunk() + , _height() { } @@ -2450,8 +2544,14 @@ bool RASTChunk::isValid() const return chunkId() == RASTChunk::defaultChunkId(); } -QList RASTChunk::palette(qint32 y, qint32 height) const +IPALChunk *RASTChunk::clone() const { + return new RASTChunk(*this); +} + +QList RASTChunk::palette(qint32 y) const +{ + auto &&height = _height; if (height < 1) { return {}; } @@ -2478,7 +2578,413 @@ QList RASTChunk::palette(qint32 y, qint32 height) const return pal; } +bool RASTChunk::initialize(const QList &, qint32 height) +{ + _height = height; + return true; +} + bool RASTChunk::innerReadStructure(QIODevice *d) { return cacheData(d); } + +/* ****************** + * *** PCHG Chunk *** + * ****************** */ + +PCHGChunk::~PCHGChunk() +{ +} + +PCHGChunk::PCHGChunk() : IPALChunk() +{ + +} + +PCHGChunk::Compression PCHGChunk::compression() const +{ + if (!isValid()) { + return Compression::Uncompressed; + } + return Compression(ui16(data(), 0)); +} + +PCHGChunk::Flags PCHGChunk::flags() const +{ + if (!isValid()) { + return Flags(Flag::None); + } + return Flags(ui16(data(), 2)); +} + +qint16 PCHGChunk::startLine() const +{ + if (!isValid()) { + return 0; + } + return i16(data(), 4); +} + +quint16 PCHGChunk::lineCount() const +{ + if (!isValid()) { + return 0; + } + return ui16(data(), 6); +} + +quint16 PCHGChunk::changedLines() const +{ + if (!isValid()) { + return 0; + } + return ui16(data(), 8); +} + +quint16 PCHGChunk::minReg() const +{ + if (!isValid()) { + return 0; + } + return ui16(data(), 10); +} + +quint16 PCHGChunk::maxReg() const +{ + if (!isValid()) { + return 0; + } + return ui16(data(), 12); +} + +quint16 PCHGChunk::maxChanges() const +{ + if (!isValid()) { + return 0; + } + return ui16(data(), 14); +} + +quint32 PCHGChunk::totalChanges() const +{ + if (!isValid()) { + return 0; + } + return ui32(data(), 16); +} + +bool PCHGChunk::hasAlpha() const +{ + return (flags() & PCHGChunk::Flag::UseAlpha) ? true : false; +} + +bool PCHGChunk::isValid() const +{ + if (bytes() < 20) { + return false; + } + return chunkId() == PCHGChunk::defaultChunkId(); +} + +IPALChunk *PCHGChunk::clone() const +{ + return new PCHGChunk(*this); +} + +QList PCHGChunk::palette(qint32 y) const +{ + return _palettes.value(y); +} + +// ---------------------------------------------------------------------------- +// PCHG_FastDecomp reimplementation (Amiga 68k -> portable C++/Qt) +// ---------------------------------------------------------------------------- +// This mirrors the original 68k routine semantics: +// - The Huffman tree is stored as a sequence of signed 16-bit words (big-endian) +// and TreeCode points to the *last word* of that sequence. +// - Bits are consumed MSB-first from 32-bit big-endian longwords of the source. +// - Navigation rules (matching the assembly): +// bit=1: read w = *(a3). If w < 0 then a3 += w (byte-wise) and continue; +// else emit (w & 0xFF) and reset a3 to TreeCode (last word). +// bit=0: predecrement a3 by 2; read w = *a3. If w < 0: continue; +// else if (w & 0x0100) emit (w & 0xFF) and reset a3; else continue. +// - Stop after writing exactly OriginalSize bytes. +// +// This function expects a single QByteArray laid out as: +// [ tree (treeSize bytes, even) | compressed bitstream (... bytes) ] +// +// On any error, logs with qCCritical(LOG_IFFPLUGIN) and returns {}. +// Comments are in English as requested. +// ---------------------------------------------------------------------------- +// +// NOTE: Sebastiano Vigna, the author of the PCHG specification and the ASM +// decompression code for the Motorola 68K, gave us permission to use his +// code and recommended that we convert it with AI. + +// Read a big-endian 16-bit signed word from a byte buffer +static inline qint16 read_be16(const char* base, int byteIndex, int size) +{ + if (byteIndex + 1 >= size) + return 0; // caller must bounds-check; we keep silent here + const quint8 b0 = static_cast(base[byteIndex]); + const quint8 b1 = static_cast(base[byteIndex + 1]); + return static_cast((b0 << 8) | b1); +} + +// Read a big-endian 32-bit unsigned long from a byte buffer +static inline quint32 read_be32(const char* base, int byteIndex, int size) +{ + if (byteIndex + 3 >= size) + return 0; // caller must bounds-check + const quint8 b0 = static_cast(base[byteIndex]); + const quint8 b1 = static_cast(base[byteIndex + 1]); + const quint8 b2 = static_cast(base[byteIndex + 2]); + const quint8 b3 = static_cast(base[byteIndex + 3]); + return (static_cast(b0) << 24) | + (static_cast(b1) << 16) | + (static_cast(b2) << 8) | + static_cast(b3); +} + +// Core decompressor (tree + compressed stream in one QByteArray) +static QByteArray pchgFastDecomp(const QByteArray& input, int treeSize, int originalSize) +{ + // Basic validation + if (treeSize <= 0 || (treeSize & 1)) { + qCCritical(LOG_IFFPLUGIN) << "Invalid treeSize (must be positive and even)" << treeSize; + return {}; + } + if (input.size() < treeSize) { + qCCritical(LOG_IFFPLUGIN) << "Input too small for treeSize" << input.size() << treeSize; + return {}; + } + if (originalSize < 0) { + qCCritical(LOG_IFFPLUGIN) << "Invalid originalSize" << originalSize; + return {}; + } + + const char* data = input.constData(); + const int totalSize = input.size(); + + // Tree view (big-endian words) + const int treeBytes = treeSize; + const int treeWords = treeBytes / 2; + if (treeWords <= 0) { + qCCritical(LOG_IFFPLUGIN) << "Tree has zero words"; + return {}; + } + + // Compressed stream + const int srcBase = treeBytes; // offset where bitstream starts + const int srcSize = totalSize - srcBase; + if (srcSize <= 0 && originalSize > 0) { + qCCritical(LOG_IFFPLUGIN) << "No compressed payload present"; + return {}; + } + + QByteArray out; + out.resize(originalSize); + char* outPtr = out.data(); + + // Emulate a3 pointer to words: + // a2 points to the *last word* => word index (0..treeWords-1) + auto resetA3 = [&]() { + return treeWords - 1; // last word index + }; + int a3_word = resetA3(); + + // Bit reader: loads 32b big-endian and shifts MSB-first + quint32 bitbuf = 0; + int bits = 0; // remaining bits in bitbuf + int srcPos = 0; // byte offset relative to srcBase + + auto refill = [&]() -> bool { + if (srcPos + 4 > srcSize) { + qCCritical(LOG_IFFPLUGIN) << "Compressed stream underflow while refilling bit buffer" + << "srcPos=" << srcPos << "srcSize=" << srcSize; + return false; + } + bitbuf = read_be32(data + srcBase, srcPos, srcSize); + bits = 32; + srcPos += 4; + return true; + }; + + int produced = 0; + + // Main decode loop: produce exactly originalSize bytes + while (produced < originalSize) { + if (bits == 0) { + if (!refill()) { + // Not enough bits to complete output + return {}; + } + } + + const bool bit1 = (bitbuf & 0x80000000u) != 0u; // MSB before shift + bitbuf <<= 1; + --bits; + + if (bit1) { + // Case bit == 1 --> w = *(a3) + if (a3_word < 0 || a3_word >= treeWords) { + qCCritical(LOG_IFFPLUGIN) << "a3 out of bounds (bit=1)" << a3_word; + return {}; + } + const int byteIndex = a3_word * 2; + const qint16 w = read_be16(data, byteIndex, treeBytes); + + if (w < 0) { + // a3 += w (w is a signed byte offset, must be even) + if (w & 1) { + qCCritical(LOG_IFFPLUGIN) << "Misaligned tree offset (odd)" << w; + return {}; + } + const int deltaWords = w / 2; // arithmetic division, w is even in valid streams + const int next = a3_word + deltaWords; + if (next < 0 || next >= treeWords) { + qCCritical(LOG_IFFPLUGIN) << "a3 out of bounds after offset" << next; + return {}; + } + a3_word = next; + } else { + // Leaf: emit low 8 bits, reset a3 + outPtr[produced++] = static_cast(w & 0xFF); + a3_word = resetA3(); + } + } else { + // Case bit == 0 --> w = *--a3 (predecrement) + --a3_word; + if (a3_word < 0) { + qCCritical(LOG_IFFPLUGIN) << "a3 underflow on predecrement"; + return {}; + } + const int byteIndex = a3_word * 2; + const qint16 w = read_be16(data, byteIndex, treeBytes); + + if (w < 0) { + // Internal node: continue with current a3 + continue; + } + + // Non-negative: check bit #8; if set -> leaf + if ((w & 0x0100) != 0) { + outPtr[produced++] = static_cast(w & 0xFF); + a3_word = resetA3(); + } else { + // Not a leaf: continue scanning + continue; + } + } + } + + return out; +} + +// !Huffman decompression + +bool PCHGChunk::initialize(const QList &cmapPalette, qint32 height) +{ + auto dt = data().mid(20); + if (compression() == PCHGChunk::Compression::Huffman) { + QDataStream ds(dt); + ds.setByteOrder(QDataStream::BigEndian); + + quint32 infoSize; + ds >> infoSize; + quint32 origSize; + ds >> origSize; + + dt = pchgFastDecomp(dt.mid(8), infoSize, origSize); + } + if (dt.isEmpty()) { + return false; + } + + QDataStream ds(dt); + ds.setByteOrder(QDataStream::BigEndian); + + // read the masks + auto lcnt = lineCount(); + auto nlw = (lcnt + 31) / 32; // number of LWORD containing the bit mask + QList masks; + for (auto i = 0; i < nlw; ++i) { + quint32 mask; + ds >> mask; + masks << mask; + } + if (ds.status() != QDataStream::Ok) { + return false; + } + + // read the palettes + auto changesLoaded = qint64(); + auto startY = startLine(); + auto last = cmapPalette; + auto flgs = flags(); + for (auto i = 0; i < lcnt; ++i) { + auto mask = masks.at(i / 32); + if (((mask >> (31 - i % 32)) & 1) == 0) { + _palettes.insert(i + startY, last); + continue; // no palette change for this line + } + + QHash hash; + if (flgs & PCHGChunk::Flag::F12Bit) { + quint8 c16; + ds >> c16; + quint8 c32; + ds >> c32; + for (auto j = 0; j < int(c16); ++j) { + quint16 tmp; + ds >> tmp; + hash.insert(((tmp >> 12) & 0xF), qRgb(((tmp >> 8) & 0xF) * 17, ((tmp >> 4) & 0xF) * 17, ((tmp & 0xF) * 17))); + } + for (auto j = 0; j < int(c32); ++j) { + quint16 tmp; + ds >> tmp; + hash.insert((((tmp >> 12) & 0xF) + 16), qRgb(((tmp >> 8) & 0xF) * 17, ((tmp >> 4) & 0xF) * 17, ((tmp & 0xF) * 17))); + } + } else if (flgs & PCHGChunk::Flag::F32Bit) { // NOTE: missing test case (not tested) + quint16 cnt; + ds >> cnt; + for (auto j = 0; j < int(cnt); ++j) { + quint16 reg; + ds >> reg; + quint8 alpha; + ds >> alpha; + quint8 red; + ds >> red; + quint8 blue; + ds >> blue; + quint8 green; + ds >> green; + hash.insert(reg, qRgba(red, green, blue, flgs & PCHGChunk::Flag::UseAlpha ? alpha : 0xFF)); + } + } + + if (ds.status() != QDataStream::Ok) { + return false; + } + + for (auto i = qsizetype(), n = last.size(); i < n; ++i) { + if (hash.contains(i)) + last[i] = hash.value(i); + } + + _palettes.insert(i + startY, last); + changesLoaded += hash.size(); + } + + if (changesLoaded != qint64(totalChanges())) { + qCDebug(LOG_IFFPLUGIN) << "PCHGChunk::innerReadStructure(): palette changes count mismatch!"; + } + + return true; +} + +bool PCHGChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} diff --git a/src/imageformats/chunks_p.h b/src/imageformats/chunks_p.h index 6560ee0..eb80312 100644 --- a/src/imageformats/chunks_p.h +++ b/src/imageformats/chunks_p.h @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -54,6 +55,7 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN) #define CMAP_CHUNK QByteArray("CMAP") #define CMYK_CHUNK QByteArray("CMYK") // https://wiki.amigaos.net/wiki/ILBM_IFF_Interleaved_Bitmap#ILBM.CMYK #define DPI__CHUNK QByteArray("DPI ") +#define XBMI_CHUNK QByteArray("XBMI") // Different palette for scanline #define BEAM_CHUNK QByteArray("BEAM") @@ -91,17 +93,20 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN) #define CHUNKID_DEFINE(a) static QByteArray defaultChunkId() { return a; } -// The 8-bit RGB format must be one. If you change it here, you have also to use the same +// The 8-bit RGB format must be consistent. If you change it here, you have also to use the same // when converting an image with BEAM/CTBL/SHAM chunks otherwise the option(QImageIOHandler::ImageFormat) // could returns a wrong value. // Warning: Changing it requires changing the algorithms. Se, don't touch! :) -#define FORMAT_RGB_8BIT QImage::Format_RGB888 +#define FORMAT_RGB_8BIT QImage::Format_RGB888 // default one + +#define FORMAT_RGBA_8BIT QImage::Format_RGBA8888 // used by PCHG chunk /*! * \brief The IFFChunk class */ class IFFChunk { + friend class IFFHandlerPrivate; public: using ChunkList = QList>; @@ -318,18 +323,30 @@ protected: inline quint16 ui16(quint8 c1, quint8 c2) const { return (quint16(c2) << 8) | quint16(c1); } + inline quint16 ui16(const QByteArray &data, qint32 pos) const { + return ui16(data.at(pos + 1), data.at(pos)); + } inline qint16 i16(quint8 c1, quint8 c2) const { return qint32(ui16(c1, c2)); } + inline qint16 i16(const QByteArray &data, qint32 pos) const { + return i16(data.at(pos + 1), data.at(pos)); + } inline quint32 ui32(quint8 c1, quint8 c2, quint8 c3, quint8 c4) const { return (quint32(c4) << 24) | (quint32(c3) << 16) | (quint32(c2) << 8) | quint32(c1); } + inline quint32 ui32(const QByteArray &data, qint32 pos) const { + return ui32(data.at(pos + 3), data.at(pos + 2), data.at(pos + 1), data.at(pos)); + } inline qint32 i32(quint8 c1, quint8 c2, quint8 c3, quint8 c4) const { return qint32(ui32(c1, c2, c3, c4)); } + inline qint32 i32(const QByteArray &data, qint32 pos) const { + return i32(data.at(pos + 3), data.at(pos + 2), data.at(pos + 1), data.at(pos)); + } static ChunkList innerFromDevice(QIODevice *d, bool *ok, IFFChunk *parent = nullptr); @@ -358,7 +375,36 @@ class IPALChunk : public IFFChunk public: virtual ~IPALChunk() override {} IPALChunk() : IFFChunk() {} - virtual QList palette(qint32 y, qint32 height) const = 0; + IPALChunk(const IPALChunk& other) = default; + IPALChunk& operator =(const IPALChunk& other) = default; + + /*! + * \brief hasAlpha + * \return True it the palette supports the alpha channel. + */ + virtual bool hasAlpha() const { return false; } + + /*! + * \brief clone + * \return A new instance of the class with all data. + */ + virtual IPALChunk *clone() const = 0; + + /*! + * \brief palette + * \param y The scanline. + * \return The modified palette. + */ + virtual QList palette(qint32 y) const = 0; + + /*! + * \brief initialize + * Initialize the palette changer. + * \param cmapPalette The palette as stored in the CMAP chunk. + * \param height The image height. + * \return True on success, otherwise false. + */ + virtual bool initialize(const QList& cmapPalette, qint32 height) = 0; }; @@ -376,7 +422,17 @@ public: }; enum Masking { None = 0, /**< Designates an opaque rectangular image. */ - HasMask = 1, /**< A mask plane is interleaved with the bitplanes in the BODY chunk. */ + HasMask = 1, /**< A "mask" is an optional "plane" of data the same size (w, h) as a bitplane. + It tells how to "cut out" part of the image when painting it onto another + image. "One" bits in the mask mean "copy the corresponding pixel to the + destination". "Zero" mask bits mean "leave this destination pixel alone". In + other words, "zero" bits designate transparent pixels. + The rows of the different bitplanes and mask are interleaved in the file. + This localizes all the information pertinent to each scan line. It + makes it much easier to transform the data while reading it to adjust the + image size or depth. It also makes it possible to scroll a big image by + swapping rows directly from the file without the need for random-access to + all the bitplanes. */ HasTransparentColor = 2, /**< Pixels in the source planes matching transparentColor are to be considered “transparent”. (Actually, transparentColor isn’t a “color number” since it’s matched with numbers formed @@ -385,7 +441,7 @@ public: one of the color registers. */ Lasso = 3 /**< The reader may construct a mask by lassoing the image as in MacPaint. To do this, put a 1 pixel border of transparentColor around the image rectangle. - Then do a seed fill from this border. Filled pixels are to be transparent. */ + Then do a seed fill from this border. Filled pixels are to be transparent. */ }; virtual ~BMHDChunk() override; @@ -605,13 +661,13 @@ public: * \brief dpiX * \return The horizontal resolution in DPI. */ - quint16 dpiX() const; + virtual quint16 dpiX() const; /*! * \brief dpiY * \return The vertical resolution in DPI. */ - quint16 dpiY() const; + virtual quint16 dpiY() const; /*! * \brief dotsPerMeterX @@ -631,6 +687,50 @@ protected: virtual bool innerReadStructure(QIODevice *d) override; }; +/*! + * \brief The XBMIChunk class + */ +class XBMIChunk : public DPIChunk +{ +public: + enum PictureType : quint16 { + Indexed = 0, + Grayscale = 1, + Rgb = 2, + RgbA = 3, + Cmyk = 4, + CmykA = 5, + Bitmap = 6 + }; + + virtual ~XBMIChunk() override; + XBMIChunk(); + XBMIChunk(const XBMIChunk& other) = default; + XBMIChunk& operator =(const XBMIChunk& other) = default; + + virtual bool isValid() const override; + + /*! + * \brief dpiX + * \return The horizontal resolution in DPI. + */ + virtual quint16 dpiX() const override; + + /*! + * \brief dpiY + * \return The vertical resolution in DPI. + */ + virtual quint16 dpiY() const override; + + /*! + * \brief pictureType + * \return The picture type + */ + PictureType pictureType() const; + + CHUNKID_DEFINE(XBMI_CHUNK) +}; + /*! * \brief The BODYChunk class @@ -1312,12 +1412,19 @@ public: virtual bool isValid() const override; - virtual QList palette(qint32 y, qint32 height) const override; + virtual IPALChunk *clone() const override; + + virtual QList palette(qint32 y) const override; + + virtual bool initialize(const QList& cmapPalette, qint32 height) override; CHUNKID_DEFINE(BEAM_CHUNK) protected: virtual bool innerReadStructure(QIODevice *d) override; + +private: + qint32 _height; }; /*! @@ -1349,12 +1456,19 @@ public: virtual bool isValid() const override; - virtual QList palette(qint32 y, qint32 height) const override; + virtual IPALChunk *clone() const override; + + virtual QList palette(qint32 y) const override; + + virtual bool initialize(const QList& cmapPalette, qint32 height) override; CHUNKID_DEFINE(SHAM_CHUNK) protected: virtual bool innerReadStructure(QIODevice *d) override; + +private: + qint32 _height; }; /*! @@ -1373,13 +1487,82 @@ public: virtual bool isValid() const override; - virtual QList palette(qint32 y, qint32 height) const override; + virtual IPALChunk *clone() const override; + + virtual QList palette(qint32 y) const override; + + virtual bool initialize(const QList& cmapPalette, qint32 height) override; CHUNKID_DEFINE(RAST_CHUNK) protected: virtual bool innerReadStructure(QIODevice *d) override; + +private: + qint32 _height; }; +/*! + * \brief The PCHGChunk class + */ +class PCHGChunk : public IPALChunk +{ +public: + enum Compression { + Uncompressed, + Huffman + }; + + enum Flag { + None = 0x00, + F12Bit = 0x01, + F32Bit = 0x02, + UseAlpha = 0x04 + }; + Q_DECLARE_FLAGS(Flags, Flag) + + virtual ~PCHGChunk() override; + PCHGChunk(); + PCHGChunk(const PCHGChunk& other) = default; + PCHGChunk& operator =(const PCHGChunk& other) = default; + + Compression compression() const; + + Flags flags() const; + + qint16 startLine() const; + + quint16 lineCount() const; + + quint16 changedLines() const; + + quint16 minReg() const; + + quint16 maxReg() const; + + quint16 maxChanges() const; + + quint32 totalChanges() const; + + virtual bool hasAlpha() const override; + + virtual bool isValid() const override; + + virtual IPALChunk *clone() const override; + + virtual QList palette(qint32 y) const override; + + virtual bool initialize(const QList& cmapPalette, qint32 height) override; + + CHUNKID_DEFINE(PCHG_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; + +private: + QHash> _paletteChanges; + + QHash> _palettes; +}; #endif // KIMG_CHUNKS_P_H diff --git a/src/imageformats/iff.cpp b/src/imageformats/iff.cpp index 2c39d4f..2becc7b 100644 --- a/src/imageformats/iff.cpp +++ b/src/imageformats/iff.cpp @@ -27,6 +27,37 @@ public: } + /*! + * \brief atariSTERast + * On Atari STE images, the RAST chunk can be found outside + * the FORM one so, I check if this is the case. + * \param chunks The chunk list. + */ + void atariSTERast(QIODevice *d, IFFChunk::ChunkList &chunks) + { + if (chunks.size() != 1 || d->isSequential()) { + return; + } + auto &&c = chunks.first(); + if (c->chunkId() != FORMChunk::defaultChunkId()) { + return; + } + + // The RAST chunk is not aligned so I have to temporary change the + // position and the alignment to read it successfully. + auto pos = d->pos(); + auto align = c->alignBytes(); + c->setAlignBytes(1); + d->seek(c->nextChunkPos()); + c->setAlignBytes(align); + if (d->peek(4) == RAST_CHUNK) { + auto rast = QSharedPointer(new RASTChunk()); + if (rast->readStructure(d) && rast->isValid()) + chunks.first()->_chunks.append(rast); + } + d->seek(pos); + } + bool readStructure(QIODevice *d) { if (d == nullptr) { @@ -40,6 +71,7 @@ public: auto ok = false; auto chunks = IFFChunk::fromDevice(d, &ok); if (ok) { + atariSTERast(d, chunks); m_chunks = chunks; } return ok; @@ -101,7 +133,7 @@ bool IFFHandler::canRead() const bool IFFHandler::canRead(QIODevice *device) { if (!device) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::canRead() called with no device"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::canRead(): called with no device"; return false; } @@ -124,7 +156,7 @@ bool IFFHandler::canRead(QIODevice *device) auto pos = device->pos(); auto chunks = IFFChunk::fromDevice(device, &ok); if (!device->seek(pos)) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::canRead() unable to reset device position"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::canRead(): unable to reset device position"; } if (ok) { auto forms = IFFHandlerPrivate::searchForms(chunks, true); @@ -214,14 +246,18 @@ static void addMetadata(QImage &img, const IFOR_Chunk *form) } // resolution -> leave after set of EXIF chunk + const DPIChunk *dpi = nullptr; auto dpis = IFFChunk::searchT(form); + auto xbmis = IFFChunk::searchT(form); if (!dpis.isEmpty()) { - auto &&dpi = dpis.first(); - if (dpi->isValid()) { - img.setDotsPerMeterX(dpi->dotsPerMeterX()); - img.setDotsPerMeterY(dpi->dotsPerMeterY()); - resChanged = true; - } + dpi = dpis.first(); + } else if (!xbmis.isEmpty()) { + dpi = xbmis.first(); // never seen + } + if (dpi && dpi->isValid()) { + img.setDotsPerMeterX(dpi->dotsPerMeterX()); + img.setDotsPerMeterY(dpi->dotsPerMeterY()); + resChanged = true; } // if no explicit resolution was found, apply the aspect ratio to the default one @@ -248,26 +284,30 @@ static void addMetadata(QImage &img, const IFOR_Chunk *form) static QImage convertIPAL(const QImage& img, const IPALChunk *ipal) { if (img.format() != QImage::Format_Indexed8) { - qDebug(LOG_IFFPLUGIN) << "convertIPAL(): the image is not indexed!"; + qCDebug(LOG_IFFPLUGIN) << "convertIPAL(): the image is not indexed!"; return img; } - auto tmp = img.convertToFormat(FORMAT_RGB_8BIT); + auto tmp = img.convertToFormat(ipal->hasAlpha() ? FORMAT_RGBA_8BIT : FORMAT_RGB_8BIT); if (tmp.isNull()) { qCritical(LOG_IFFPLUGIN) << "convertIPAL(): error while converting the image!"; return img; } + auto mul = tmp.hasAlphaChannel() ? 4 : 3; for (auto y = 0, h = img.height(); y < h; ++y) { auto src = reinterpret_cast(img.constScanLine(y)); auto dst = tmp.scanLine(y); - auto lpal = ipal->palette(y, h); + auto lpal = ipal->palette(y); for (auto x = 0, w = img.width(); x < w; ++x) { if (src[x] < lpal.size()) { - auto x3 = x * 3; - dst[x3] = qRed(lpal.at(src[x])); - dst[x3 + 1] = qGreen(lpal.at(src[x])); - dst[x3 + 2] = qBlue(lpal.at(src[x])); + auto xmul = x * mul; + dst[xmul] = qRed(lpal.at(src[x])); + dst[xmul + 1] = qGreen(lpal.at(src[x])); + dst[xmul + 2] = qBlue(lpal.at(src[x])); + if (mul == 4) { + dst[xmul + 3] = qAlpha(lpal.at(src[x])); + } } } } @@ -287,7 +327,7 @@ bool IFFHandler::readStandardImage(QImage *image) // show the first one (I don't have a sample with many images) auto headers = IFFChunk::searchT(form); if (headers.isEmpty()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() no supported image found"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage(): no supported image found"; return false; } @@ -295,7 +335,7 @@ bool IFFHandler::readStandardImage(QImage *image) auto &&header = headers.first(); auto img = imageAlloc(header->size(), form->format()); if (img.isNull()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while allocating the image"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage(): error while allocating the image"; return false; } @@ -324,7 +364,19 @@ bool IFFHandler::readStandardImage(QImage *image) } // reading image data - auto ipal = form->searchIPal(); + std::unique_ptr ipal; + if (auto ptr = form->searchIPal()) { + ipal = std::unique_ptr(ptr->clone()); + } + if (ipal) { + auto pal = img.colorTable(); + if (pal.isEmpty()) + pal = cmap->palette(); + if (!ipal->initialize(pal, img.height())) { + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage(): unable to initialize palette changer"; + return false; + } + } auto bodies = IFFChunk::searchT(form); if (bodies.isEmpty()) { auto abits = IFFChunk::searchT(form); @@ -336,23 +388,23 @@ bool IFFHandler::readStandardImage(QImage *image) } else { auto &&body = bodies.first(); if (!body->resetStrideRead(device())) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image data"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage(): error while reading image data"; return false; } for (auto y = 0, h = img.height(); y < h; ++y) { auto line = reinterpret_cast(img.scanLine(y)); - auto ba = body->strideRead(device(), y, header, camg, cmap, ipal, form->formType()); + auto ba = body->strideRead(device(), y, header, camg, cmap, ipal.get(), form->formType()); if (ba.isEmpty()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image scanline"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage(): error while reading image scanline"; return false; } memcpy(line, ba.constData(), std::min(img.bytesPerLine(), ba.size())); } } - // BEAM / CTBL conversion (if not already done) + // BEAM / CTBL, SHAM, RAST, PCHG conversion (if not already done) if (ipal && img.format() == QImage::Format_Indexed8) { - img = convertIPAL(img, ipal); + img = convertIPAL(img, ipal.get()); } // set metadata (including image resolution) @@ -374,7 +426,7 @@ bool IFFHandler::readMayaImage(QImage *image) // show the first one (I don't have a sample with many images) auto headers = IFFChunk::searchT(form); if (headers.isEmpty()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() no supported image found"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage(): no supported image found"; return false; } @@ -382,30 +434,30 @@ bool IFFHandler::readMayaImage(QImage *image) auto &&header = headers.first(); auto img = imageAlloc(header->size(), form->format()); if (img.isNull()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() error while allocating the image"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage(): error while allocating the image"; return false; } auto &&tiles = IFFChunk::searchT(form); if ((tiles.size() & 0xFFFF) != header->tiles()) { // Photoshop, on large images saves more than 65535 tiles - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() tile number mismatch: found" << tiles.size() << "while expected" << header->tiles(); + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage(): tile number mismatch: found" << tiles.size() << "while expected" << header->tiles(); return false; } for (auto &&tile : tiles) { auto tp = tile->pos(); auto ts = tile->size(); if (tp.x() < 0 || tp.x() + ts.width() > img.width()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() wrong tile position or size"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage(): wrong tile position or size"; return false; } if (tp.y() < 0 || tp.y() + ts.height() > img.height()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() wrong tile position or size"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage(): wrong tile position or size"; return false; } // For future releases: it might be a good idea not to use a QPainter auto ti = tile->tile(device(), header); if (ti.isNull()) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() error while decoding the tile"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage(): error while decoding the tile"; return false; } QPainter painter(&img); @@ -426,7 +478,7 @@ bool IFFHandler::readMayaImage(QImage *image) bool IFFHandler::read(QImage *image) { if (!d->readStructure(device())) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read() invalid IFF structure"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read(): invalid IFF structure"; return false; } @@ -438,7 +490,7 @@ bool IFFHandler::read(QImage *image) return true; } - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read() no supported image found"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read(): no supported image found"; return false; } @@ -515,7 +567,7 @@ int IFFHandler::imageCount() const count = QImageIOHandler::imageCount(); if (!d->readStructure(device())) { - qCWarning(LOG_IFFPLUGIN) << "IFFHandler::imageCount() invalid IFF structure"; + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::imageCount(): invalid IFF structure"; return count; }