IFF: support for PCHG chunk

Highlights:
- Adds support for a new palette changer chunk. Some test cases attached to #38 .
- Fixes the reading of ILBMs with the mask (test case: [cyclone.iff](/uploads/d8734d2155fd0d21f7b003b37e0d1259/cyclone.iff)).
- Adds support for HAM5 encoding.
- Adds more test cases created using [HAM Converter](http://mrsebe.bplaced.net/blog/wordpress/).
- Adds support for Atari STE RAST chunk outside FORM one (test case: [fish.iff](/uploads/c461cf4b6a1423cec60fbce645d9fd07/fish.iff)).

NOTE: I contacted Sebastiano Vigna, the author of the PCHG chunk specifications, and he provided me with:
- Some images to test the code (but I can't include them in the test cases).
- Permission to use [his code](https://vigna.di.unimi.it/amiga/PCHGLib.zip) without restrictions: Huffman decompression was achieved by converting `FastDecomp.a` via AI.

Closes #38
This commit is contained in:
Mirco Miranda
2025-09-08 17:39:50 +02:00
committed by Albert Astals Cid
parent 8036b1d032
commit 463da81fad
19 changed files with 808 additions and 67 deletions

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
autotests/read/iff/ham5.iff Normal file

Binary file not shown.

BIN
autotests/read/iff/ham5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
autotests/read/iff/ham8.iff Normal file

Binary file not shown.

BIN
autotests/read/iff/ham8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -10,6 +10,7 @@
#include <QBuffer>
#include <QColor>
#include <QDataStream>
#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<IFFChunk>(new ICCPChunk());
} else if (cid == NAME_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new NAMEChunk());
} else if (cid == PCHG_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new PCHGChunk());
} else if (cid == RAST_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new RASTChunk());
} else if (cid == RGBA_CHUNK) {
@ -316,6 +319,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk *
chunk = QSharedPointer<IFFChunk>(new TBHDChunk());
} else if (cid == VERS_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new VERSChunk());
} else if (cid == XBMI_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new XBMIChunk());
} else if (cid == XMP0_CHUNK) {
chunk = QSharedPointer<IFFChunk>(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<PCHGChunk>(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<QRgb> BEAMChunk::palette(qint32 y, qint32 height) const
IPALChunk *BEAMChunk::clone() const
{
return new BEAMChunk(*this);
}
bool BEAMChunk::initialize(const QList<QRgb> &, qint32 height)
{
_height = height;
return true;
}
QList<QRgb> 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<QRgb> SHAMChunk::palette(qint32 y, qint32 height) const
IPALChunk *SHAMChunk::clone() const
{
return new SHAMChunk(*this);
}
QList<QRgb> SHAMChunk::palette(qint32 y) const
{
auto && height = _height;
if (height < 1) {
return {};
}
@ -2426,6 +2512,12 @@ QList<QRgb> SHAMChunk::palette(qint32 y, qint32 height) const
return pal;
}
bool SHAMChunk::initialize(const QList<QRgb> &, 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<QRgb> RASTChunk::palette(qint32 y, qint32 height) const
IPALChunk *RASTChunk::clone() const
{
return new RASTChunk(*this);
}
QList<QRgb> RASTChunk::palette(qint32 y) const
{
auto &&height = _height;
if (height < 1) {
return {};
}
@ -2478,7 +2578,413 @@ QList<QRgb> RASTChunk::palette(qint32 y, qint32 height) const
return pal;
}
bool RASTChunk::initialize(const QList<QRgb> &, 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<QRgb> 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<quint8>(base[byteIndex]);
const quint8 b1 = static_cast<quint8>(base[byteIndex + 1]);
return static_cast<qint16>((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<quint8>(base[byteIndex]);
const quint8 b1 = static_cast<quint8>(base[byteIndex + 1]);
const quint8 b2 = static_cast<quint8>(base[byteIndex + 2]);
const quint8 b3 = static_cast<quint8>(base[byteIndex + 3]);
return (static_cast<quint32>(b0) << 24) |
(static_cast<quint32>(b1) << 16) |
(static_cast<quint32>(b2) << 8) |
static_cast<quint32>(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<char>(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<char>(w & 0xFF);
a3_word = resetA3();
} else {
// Not a leaf: continue scanning
continue;
}
}
}
return out;
}
// !Huffman decompression
bool PCHGChunk::initialize(const QList<QRgb> &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<quint32> 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<quint16, QRgb> 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);
}

View File

@ -17,6 +17,7 @@
#include <QByteArray>
#include <QDateTime>
#include <QHash>
#include <QImage>
#include <QIODevice>
#include <QLoggingCategory>
@ -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<QSharedPointer<IFFChunk>>;
@ -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<QRgb> 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<QRgb> 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<QRgb>& 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
isnt a “color number” since its 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<QRgb> palette(qint32 y, qint32 height) const override;
virtual IPALChunk *clone() const override;
virtual QList<QRgb> palette(qint32 y) const override;
virtual bool initialize(const QList<QRgb>& 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<QRgb> palette(qint32 y, qint32 height) const override;
virtual IPALChunk *clone() const override;
virtual QList<QRgb> palette(qint32 y) const override;
virtual bool initialize(const QList<QRgb>& 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<QRgb> palette(qint32 y, qint32 height) const override;
virtual IPALChunk *clone() const override;
virtual QList<QRgb> palette(qint32 y) const override;
virtual bool initialize(const QList<QRgb>& 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<QRgb> palette(qint32 y) const override;
virtual bool initialize(const QList<QRgb>& cmapPalette, qint32 height) override;
CHUNKID_DEFINE(PCHG_CHUNK)
protected:
virtual bool innerReadStructure(QIODevice *d) override;
private:
QHash<qint32, QHash<quint16, QRgb>> _paletteChanges;
QHash<qint32, QList<QRgb>> _palettes;
};
#endif // KIMG_CHUNKS_P_H

View File

@ -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<IFFChunk>(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<FORMChunk>(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<DPIChunk>(form);
auto xbmis = IFFChunk::searchT<XBMIChunk>(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<const quint8 *>(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<BMHDChunk>(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<IPALChunk> ipal;
if (auto ptr = form->searchIPal()) {
ipal = std::unique_ptr<IPALChunk>(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<BODYChunk>(form);
if (bodies.isEmpty()) {
auto abits = IFFChunk::searchT<ABITChunk>(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<char*>(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<TBHDChunk>(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<RGBAChunk>(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;
}