IFF: add support for a different palette per line

This commit is contained in:
Mirco Miranda
2025-08-12 08:28:31 +02:00
parent 68cc915132
commit f63b082c85
5 changed files with 466 additions and 51 deletions

View File

@ -268,6 +268,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk *
chunk = QSharedPointer<IFFChunk>(new ANNOChunk());
} else if (cid == AUTH_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new AUTHChunk());
} else if (cid == BEAM_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new BEAMChunk());
} else if (cid == BMHD_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new BMHDChunk());
} else if (cid == BODY_CHUNK) {
@ -282,6 +284,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk *
chunk = QSharedPointer<IFFChunk>(new CMYKChunk());
} else if (cid == COPY_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new COPYChunk());
} else if (cid == CTBL_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new CTBLChunk());
} else if (cid == DATE_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new DATEChunk());
} else if (cid == DPI__CHUNK) {
@ -302,8 +306,12 @@ 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 == RAST_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new RASTChunk());
} else if (cid == RGBA_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new RGBAChunk());
} else if (cid == SHAM_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new SHAMChunk());
} else if (cid == TBHD_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new TBHDChunk());
} else if (cid == VERS_CHUNK) {
@ -312,7 +320,7 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk *
chunk = QSharedPointer<IFFChunk>(new XMP0Chunk());
} else { // unknown chunk
chunk = QSharedPointer<IFFChunk>(new IFFChunk());
qCDebug(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)
@ -779,7 +787,7 @@ inline qint64 rgbNDecompress(QIODevice *input, char *output, qint64 olen)
return j;
}
QByteArray BODYChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, const QByteArray& formType) const
QByteArray BODYChunk::strideRead(QIODevice *d, qint32 y, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, const IPALChunk *ipal, const QByteArray& formType) const
{
if (!isValid() || header == nullptr || d == nullptr) {
return {};
@ -812,7 +820,7 @@ QByteArray BODYChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CA
} 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();
qCDebug(LOG_IFFPLUGIN) << "BODYChunk::strideRead(): unknown compression" << header->compression();
}
if ((rr != readSize && lineCompressed) || (rr < 1))
return {};
@ -822,15 +830,15 @@ QByteArray BODYChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CA
auto planes = _readBuffer.left(readSize);
_readBuffer.remove(0, readSize);
if (isPbm) {
return pbm(planes, header, camg, cmap);
return pbm(planes, y, header, camg, cmap, ipal);
}
if (isRgb8) {
return rgb8(planes, header, camg, cmap);
return rgb8(planes, y, header, camg, cmap, ipal);
}
if (isRgbN) {
return rgbN(planes, header, camg, cmap);
return rgbN(planes, y, header, camg, cmap, ipal);
}
return deinterleave(planes, header, camg, cmap);
return deinterleave(planes, y, header, camg, cmap, ipal);
}
bool BODYChunk::resetStrideRead(QIODevice *d) const
@ -879,7 +887,7 @@ quint32 BODYChunk::strideSize(const BMHDChunk *header, const QByteArray& formTyp
return header->rowLen() * header->bitplanes();
}
QByteArray BODYChunk::pbm(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *, const CMAPChunk *) const
QByteArray BODYChunk::pbm(const QByteArray &planes, qint32, const BMHDChunk *header, const CAMGChunk *, const CMAPChunk *, const IPALChunk *) const
{
if (planes.size() != strideSize(header, PBM__FORM_TYPE)) {
return {};
@ -891,7 +899,7 @@ QByteArray BODYChunk::pbm(const QByteArray &planes, const BMHDChunk *header, con
return {};
}
QByteArray BODYChunk::rgb8(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *, const CMAPChunk *) const
QByteArray BODYChunk::rgb8(const QByteArray &planes, qint32, const BMHDChunk *header, const CAMGChunk *, const CMAPChunk *, const IPALChunk *) const
{
if (planes.size() != strideSize(header, RGB8_FORM_TYPE)) {
return {};
@ -899,7 +907,7 @@ QByteArray BODYChunk::rgb8(const QByteArray &planes, const BMHDChunk *header, co
return planes;
}
QByteArray BODYChunk::rgbN(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *, const CMAPChunk *) const
QByteArray BODYChunk::rgbN(const QByteArray &planes, qint32, const BMHDChunk *header, const CAMGChunk *, const CMAPChunk *, const IPALChunk *) const
{
if (planes.size() != strideSize(header, RGBN_FORM_TYPE)) {
return {};
@ -907,7 +915,7 @@ QByteArray BODYChunk::rgbN(const QByteArray &planes, const BMHDChunk *header, co
return planes;
}
QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap) const
QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, const IPALChunk *ipal) const
{
if (planes.size() != strideSize(header, ILBM_FORM_TYPE)) {
return {};
@ -950,6 +958,11 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he
// 11 - hold previous. replacing Green component with bits from planes 0-3
ba = QByteArray(rowLen * 8 * 3, char());
auto pal = cmap->palette();
if (ipal) {
auto tmp = ipal->palette(y, header->height());
if (tmp.size() == pal.size())
pal = tmp;
}
auto max = (1 << (bitplanes - 2)) - 1;
quint8 prev[3] = {};
for (qint32 i = 0, cnt = 0; i < rowLen; ++i) {
@ -979,7 +992,7 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he
prev[1] = qGreen(pal.at(idx));
prev[2] = qBlue(pal.at(idx));
} else {
qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave: palette index" << idx << "is out of range";
qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave(): palette index" << idx << "is out of range";
}
break;
}
@ -1015,7 +1028,7 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he
if (idx < palSize) {
ba[cnt] = ctl ? idx + palSize : idx;
} else {
qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave: palette index" << idx << "is out of range";
qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave(): palette index" << idx << "is out of range";
}
}
}
@ -1174,7 +1187,7 @@ bool ABITChunk::isValid() const
return chunkId() == ABITChunk::defaultChunkId();
}
QByteArray ABITChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, const QByteArray& formType) const
QByteArray ABITChunk::strideRead(QIODevice *d, qint32 y, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, const IPALChunk *ipal, const QByteArray& formType) const
{
if (!isValid() || header == nullptr || d == nullptr) {
return {};
@ -1187,11 +1200,11 @@ QByteArray ABITChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CA
auto ilbmLine = QByteArray(strideSize(header, formType), char());
auto rowSize = header->rowLen();
auto height = header->height();
if (_y >= 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))
if (!seek(d, qint64(plane) * rowSize * height + y * rowSize))
return {};
auto offset = qint64(plane) * rowSize;
if (offset + rowSize > ilbmLine.size())
@ -1199,8 +1212,6 @@ QByteArray ABITChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CA
if (d->read(ilbmLine.data() + offset, rowSize) != rowSize)
return {};
}
// next line on the next run
++_y;
// decode the ILBM line
QBuffer buf;
@ -1208,12 +1219,11 @@ QByteArray ABITChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CA
if (!buf.open(QBuffer::ReadOnly)) {
return {};
}
return BODYChunk::strideRead(&buf, header, camg, cmap, ILBM_FORM_TYPE);
return BODYChunk::strideRead(&buf, y, header, camg, cmap, ipal, ILBM_FORM_TYPE);
}
bool ABITChunk::resetStrideRead(QIODevice *d) const
{
_y = 0;
return BODYChunk::resetStrideRead(d);
}
@ -1243,6 +1253,41 @@ QImageIOHandler::Transformation IFOR_Chunk::transformation() const
return QImageIOHandler::Transformation::TransformationNone;
}
QImage::Format IFOR_Chunk::optionformat() const
{
auto fmt = this->format();
if (fmt == QImage::Format_Indexed8) {
if (searchIPal())
fmt = FORMAT_RGB_8BIT;
}
return fmt;
}
const IPALChunk *IFOR_Chunk::searchIPal() const
{
const IPALChunk *ipal = nullptr;
auto beam = IFFChunk::searchT<BEAMChunk>(this);
if (!beam.isEmpty()) {
ipal = beam.first();
}
auto ctbl = IFFChunk::searchT<CTBLChunk>(this);
if (!ctbl.isEmpty()) {
ipal = ctbl.first();
}
auto sham = IFFChunk::searchT<SHAMChunk>(this);
if (!sham.isEmpty()) {
ipal = sham.first();
}
auto rast = IFFChunk::searchT<RASTChunk>(this);
if (!rast.isEmpty()) {
ipal = rast.first();
}
if (ipal && ipal->isValid()) {
return ipal;
}
return nullptr;
}
/* ******************
* *** FORM Chunk ***
@ -1312,10 +1357,10 @@ QImage::Format FORMChunk::format() const
auto camgs = IFFChunk::searchT<CAMGChunk>(chunks());
auto modeId = BODYChunk::safeModeId(h, camgs.isEmpty() ? nullptr : camgs.first(), cmaps.isEmpty() ? nullptr : cmaps.first());
if (h->bitplanes() == 13) {
return QImage::Format_RGB888; // NOTE: with a little work you could use Format_RGB444
return FORMAT_RGB_8BIT; // NOTE: with a little work you could use Format_RGB444
}
if (h->bitplanes() == 24 || h->bitplanes() == 25) {
return QImage::Format_RGB888;
return FORMAT_RGB_8BIT;
}
if (h->bitplanes() == 48) {
return QImage::Format_RGBX64;
@ -1327,19 +1372,14 @@ QImage::Format FORMChunk::format() const
return QImage::Format_RGBA64;
}
if (h->bitplanes() >= 1 && h->bitplanes() <= 8) {
if (!IFFChunk::search(SHAM_CHUNK, chunks()).isEmpty()
|| !IFFChunk::search(RAST_CHUNK, chunks()).isEmpty()
|| !IFFChunk::search(CTBL_CHUNK, chunks()).isEmpty()
|| !IFFChunk::search(BEAM_CHUNK, chunks()).isEmpty()) {
// Images with the SHAM, RAST or BEAM/CTBL chunk do not load correctly:
// it seems they contains a color table but I didn't find any specs.
qCDebug(LOG_IFFPLUGIN) << "FORMChunk::format(): BEAM/CTBL/RAST/SHAM chunk is not supported";
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 QImage::Format_RGB888;
return FORMAT_RGB_8BIT;
}
if (!cmaps.isEmpty()) {
@ -1348,7 +1388,7 @@ QImage::Format FORMChunk::format() const
return QImage::Format_Grayscale8;
}
qCDebug(LOG_IFFPLUGIN) << "FORMChunk::format: Unsupported" << h->bitplanes() << "bitplanes";
qCDebug(LOG_IFFPLUGIN) << "FORMChunk::format(): Unsupported" << h->bitplanes() << "bitplanes";
}
return QImage::Format_Invalid;
@ -1588,7 +1628,7 @@ QImage::Format TBHDChunk::format() const
if (bpc() == 2)
return QImage::Format_RGBX64;
else if (bpc() == 1)
return QImage::Format_RGB888;
return FORMAT_RGB_8BIT;
}
return QImage::Format_Invalid;
@ -2259,3 +2299,186 @@ bool XMP0Chunk::innerReadStructure(QIODevice *d)
{
return cacheData(d);
}
/* ******************
* *** BEAM Chunk ***
* ****************** */
BEAMChunk::~BEAMChunk()
{
}
BEAMChunk::BEAMChunk() : IPALChunk()
{
}
bool BEAMChunk::isValid() const
{
return chunkId() == BEAMChunk::defaultChunkId();
}
QList<QRgb> BEAMChunk::palette(qint32 y, qint32 height) const
{
if (height < 1) {
return {};
}
auto bpp = bytes() / height;
if (bytes() != height * bpp) {
return {};
}
auto col = qint32(bpp / 2);
auto &&dt = data();
QList<QRgb> pal;
for (auto c = 0; c < col; ++c) {
// 2 bytes per color (0x0R 0xGB)
auto idx = bpp * y + c * 2;
auto r = quint8(dt[idx] & 0x0F);
auto g = quint8(dt[idx + 1] & 0xF0);
auto b = quint8(dt[idx + 1] & 0x0F);
pal << qRgb(r | (r << 4), (g >> 4) | g, b | (b << 4));
}
return pal;
}
bool BEAMChunk::innerReadStructure(QIODevice *d)
{
return cacheData(d);
}
/* ******************
* *** CTBL Chunk ***
* ****************** */
CTBLChunk::~CTBLChunk()
{
}
CTBLChunk::CTBLChunk() : BEAMChunk()
{
}
bool CTBLChunk::isValid() const
{
return chunkId() == CTBLChunk::defaultChunkId();
}
/* ******************
* *** SHAM Chunk ***
* ****************** */
SHAMChunk::~SHAMChunk()
{
}
SHAMChunk::SHAMChunk() : IPALChunk()
{
}
bool SHAMChunk::isValid() const
{
if (bytes() < 2) {
return false;
}
auto &&dt = data();
if (dt[0] != 0 && dt[1] != 0) {
// In all the sham test cases I have them at zero...
// if they are different from zero I suppose they should
// be interpreted differently from what was done.
return false;
}
return chunkId() == SHAMChunk::defaultChunkId();
}
QList<QRgb> SHAMChunk::palette(qint32 y, qint32 height) const
{
if (height < 1) {
return {};
}
auto bpp = 32; // always 32 bytes per palette (16 colors)
auto div = 0;
if (bytes() == quint32(height * bpp + 2)) {
div = 1;
} else if (bytes() == quint32(height / 2 * bpp + 2)) {
div = 2;
}
if (div == 0) {
return {};
}
auto &&dt = data();
QList<QRgb> pal;
for (auto c = 0, col = bpp / 2, idx0 = y / div * bpp + 2; c < col; ++c) {
// 2 bytes per color (0x0R 0xGB)
auto idx = idx0 + c * 2;
auto r = quint8(dt[idx] & 0x0F);
auto g = quint8(dt[idx + 1] & 0xF0);
auto b = quint8(dt[idx + 1] & 0x0F);
pal << qRgb(r | (r << 4), (g >> 4) | g, b | (b << 4));
}
return pal;
}
bool SHAMChunk::innerReadStructure(QIODevice *d)
{
return cacheData(d);
}
/* ******************
* *** RAST Chunk ***
* ****************** */
RASTChunk::~RASTChunk()
{
}
RASTChunk::RASTChunk() : IPALChunk()
{
}
bool RASTChunk::isValid() const
{
return chunkId() == RASTChunk::defaultChunkId();
}
QList<QRgb> RASTChunk::palette(qint32 y, qint32 height) const
{
if (height < 1) {
return {};
}
auto bpp = bytes() / height;
if (bytes() != height * bpp) {
return {};
}
auto col = qint32(bpp / 2 - 1);
auto &&dt = data();
QList<QRgb> pal;
for (auto c = 0; c < col; ++c) {
auto idx = bpp * y + 2 + c * 2;
// The Atari ST uses 3 bits per color (512 colors) while the Atari STE
// uses 4 bits per color (4096 colors). This strange encoding with the
// least significant bit set as MSB is, I believe, to ensure hardware
// compatibility between the two machines.
#define H1L(a) ((quint8(a) & 0x7) << 1) | ((quint8(a) >> 3) & 1)
auto r = H1L(dt[idx]);
auto g = H1L(dt[idx + 1] >> 4);
auto b = H1L(dt[idx + 1]);
#undef H1L
pal << qRgb(r | (r << 4), (g << 4) | g, b | (b << 4));
}
return pal;
}
bool RASTChunk::innerReadStructure(QIODevice *d)
{
return cacheData(d);
}