IFF: Fix halfbride detection, 1-bitplane colors and PBM line size calculation. It also ignore ZBuffer flag on Maya images (like Photoshop does) amd adds CMYK palette support.

This commit is contained in:
Mirco Miranda
2025-08-01 15:29:52 +02:00
parent cd39c36621
commit c2a1d4b401
7 changed files with 290 additions and 75 deletions

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -9,6 +9,7 @@
#include "packbits_p.h"
#include <QBuffer>
#include <QColor>
#include <QDebug>
#include <QLoggingCategory>
@ -16,6 +17,14 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN)
#define RECURSION_PROTECTION 10
static QString dataToString(const IFFChunk *chunk)
{
if (chunk == nullptr || !chunk->isValid()) {
return {};
}
return QString::fromUtf8(chunk->data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed();
}
IFFChunk::~IFFChunk()
{
@ -258,6 +267,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk *
chunk = QSharedPointer<IFFChunk>(new CAMGChunk());
} else if (cid == CMAP_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new CMAPChunk());
} else if (cid == CMYK_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new CMYKChunk());
} else if (cid == COPY_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new COPYChunk());
} else if (cid == DATE_CHUNK) {
@ -274,6 +285,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk *
chunk = QSharedPointer<IFFChunk>(new FVERChunk());
} else if (cid == HIST_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new HISTChunk());
} else if (cid == ICCN_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new ICCNChunk());
} else if (cid == ICCP_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new ICCPChunk());
} else if (cid == NAME_CHUNK) {
@ -478,21 +491,87 @@ bool CMAPChunk::isValid() const
return chunkId() == CMAPChunk::defaultChunkId();
}
qint32 CMAPChunk::count() const
{
if (!isValid()) {
return 0;
}
return bytes() / 3;
}
QList<QRgb> CMAPChunk::palette(bool halfbride) const
{
auto p = innerPalette();
if (!halfbride) {
return p;
}
auto tmp = p;
for(auto &&v : tmp) {
p << qRgb(qRed(v) / 2, qGreen(v) / 2, qBlue(v) / 2);
}
return p;
}
bool CMAPChunk::innerReadStructure(QIODevice *d)
{
return cacheData(d);
}
QList<QRgb> CMAPChunk::palette() const
QList<QRgb> CMAPChunk::innerPalette() const
{
QList<QRgb> l;
auto &&d = data();
for (quint32 i = 0, n = bytes() / 3; i < n; ++i) {
l << qRgb(d.at(i * 3), d.at(i * 3 + 1), d.at(i * 3 + 2));
for (qint32 i = 0, n = count(); i < n; ++i) {
auto i3 = i * 3;
l << qRgb(d.at(i3), d.at(i3 + 1), d.at(i3 + 2));
}
return l;
}
/* ******************
* *** CMYK Chunk ***
* ****************** */
CMYKChunk::~CMYKChunk()
{
}
CMYKChunk::CMYKChunk() : CMAPChunk()
{
}
bool CMYKChunk::isValid() const
{
return chunkId() == CMYKChunk::defaultChunkId();
}
qint32 CMYKChunk::count() const
{
if (!isValid()) {
return 0;
}
return bytes() / 4;
}
QList<QRgb> CMYKChunk::innerPalette() const
{
QList<QRgb> l;
auto &&d = data();
for (qint32 i = 0, n = count(); i < n; ++i) {
auto i4 = i * 4;
auto C = quint8(d.at(i4)) / 255.;
auto M = quint8(d.at(i4 + 1)) / 255.;
auto Y = quint8(d.at(i4 + 2)) / 255.;
auto K = quint8(d.at(i4 + 3)) / 255.;
l << QColor::fromCmykF(C, M, Y, K).toRgb().rgb();
}
return l;
}
/* ******************
* *** CAMG Chunk ***
* ****************** */
@ -630,20 +709,33 @@ bool BODYChunk::resetStrideRead(QIODevice *d) const
return seek(d);
}
CAMGChunk::ModeIds BODYChunk::safeModeId(const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap)
{
if (camg) {
return camg->modeId();
}
if (header == nullptr) {
return CAMGChunk::ModeIds();
}
if (cmap && cmap->count() == (1 << (header->bitplanes() - 1))) {
return CAMGChunk::ModeIds(CAMGChunk::ModeId::HalfBrite);
}
if (header->bitplanes() == 6) {
// If no CAMG chunk is present, and image is 6 planes deep,
// assume HAM and you'll probably be right.
return CAMGChunk::ModeIds(CAMGChunk::ModeId::Ham);
}
return CAMGChunk::ModeIds();
}
quint32 BODYChunk::strideSize(const BMHDChunk *header, bool isPbm) const
{
auto rs = header->rowLen() * header->bitplanes();
if (!isPbm) {
return rs;
return header->rowLen() * header->bitplanes();
}
// I found two versions of PBM: one uses ILBM calculation, the other uses width-based.
// As it is a proprietary extension, one of them was probably generated incorrectly.
if (header->compression() == BMHDChunk::Compression::Uncompressed) {
if (rs * header->height() != bytes())
rs = header->width() * header->bitplanes() / 8;
}
auto rs = header->width() * header->bitplanes() / 8;
if (rs & 1)
++rs;
return rs;
}
@ -655,21 +747,12 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he
auto rowLen = qint32(header->rowLen());
auto bitplanes = header->bitplanes();
auto modeId = CAMGChunk::ModeIds();
if (camg) {
modeId = camg->modeId();
}
auto modeId = BODYChunk::safeModeId(header, camg, cmap);
QByteArray ba;
switch (bitplanes) {
case 1: // bitmap
ba = QByteArray((7 + header->width() * bitplanes) / 8, char());
for (qint32 i = 0, n = std::min(planes.size(), ba.size()); i < n; ++i) {
ba[i] = ~planes.at(i);
}
break;
case 2: // gray, indexed and rgb Ham mode
case 1: // gray, indexed and rgb Ham mode
case 2:
case 3:
case 4:
case 5:
@ -741,14 +824,16 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he
}
}
} else if ((modeId & CAMGChunk::ModeId::HalfBrite) && (cmap)) {
// From A Quick Introduction to IFF.txt:
//
// In HALFBRITE mode, the Amiga interprets the bit in the
// last plane as HALFBRITE modification. The bits in the other planes are
// treated as normal color register numbers (RGB values for each color register
// is specified in the CMAP chunk). If the bit in the last plane is set (1),
// then that pixel is displayed at half brightness. This can provide up to 64
// absolute colors.
ba = QByteArray(rowLen * 8 * 3, char());
auto pal = cmap->palette();
ba = QByteArray(rowLen * 8, char());
auto palSize = cmap->count();
for (qint32 i = 0, cnt = 0; i < rowLen; ++i) {
for (qint32 j = 0; j < 8; ++j, ++cnt) {
quint8 idx = 0, ctl = 0;
@ -760,12 +845,8 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he
else
ctl = 1;
}
if (idx < pal.size()) {
auto cnt3 = cnt * 3;
auto div = ctl ? 2 : 1;
ba[cnt3] = qRed(pal.at(idx)) / div;
ba[cnt3 + 1] = qGreen(pal.at(idx)) / div;
ba[cnt3 + 2] = qBlue(pal.at(idx)) / div;
if (idx < palSize) {
ba[cnt] = ctl ? idx + palSize : idx;
} else {
qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave: palette index" << idx << "is out of range";
}
@ -968,30 +1049,28 @@ QImage::Format FORMChunk::format() const
if (auto &&h = headers.first()) {
auto cmaps = IFFChunk::searchT<CMAPChunk>(chunks());
auto camgs = IFFChunk::searchT<CAMGChunk>(chunks());
auto modeId = CAMGChunk::ModeIds();
if (!camgs.isEmpty()) {
modeId = camgs.first()->modeId();
} else if (h->bitplanes() == 6) {
// If no CAMG chunk is present, and image is 6 planes deep,
// assume HAM and you'll probably be right.
modeId = CAMGChunk::ModeIds(CAMGChunk::ModeId::Ham);
if (cmaps.isEmpty()) {
auto cmyks = IFFChunk::searchT<CMYKChunk>(chunks());
for (auto &&cmyk : cmyks)
cmaps.append(cmyk);
}
auto camgs = IFFChunk::searchT<CAMGChunk>(chunks());
auto modeId = BODYChunk::safeModeId(h, camgs.isEmpty() ? nullptr : camgs.first(), cmaps.isEmpty() ? nullptr : cmaps.first());
if (h->bitplanes() == 24) {
return QImage::Format_RGB888;
}
if (h->bitplanes() == 32) {
return QImage::Format_RGBA8888;
}
if (h->bitplanes() >= 2 && h->bitplanes() <= 8) {
if (h->bitplanes() >= 1 && h->bitplanes() <= 8) {
if (!IFFChunk::search(SHAM_CHUNK, chunks()).isEmpty() || !IFFChunk::search(CTBL_CHUNK, chunks()).isEmpty()) {
// Images with the SHAM or 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(): SHAM/CTBL chunk is not supported";
return QImage::Format_Invalid;
}
if (modeId & (CAMGChunk::ModeId::Ham | CAMGChunk::ModeId::HalfBrite)) {
if (modeId & CAMGChunk::ModeId::Ham) {
return QImage::Format_RGB888;
}
@ -1001,9 +1080,6 @@ QImage::Format FORMChunk::format() const
return QImage::Format_Grayscale8;
}
if (h->bitplanes() == 1) {
return QImage::Format_Mono;
}
}
return QImage::Format_Invalid;
@ -1168,10 +1244,10 @@ qint32 TBHDChunk::bpc() const
qint32 TBHDChunk::channels() const
{
if (flags() == TBHDChunk::Flag::RgbA) {
if ((flags() & TBHDChunk::Flag::RgbA) == TBHDChunk::Flag::RgbA) {
return 4;
}
if (flags() == TBHDChunk::Flag::Rgb) {
if ((flags() & TBHDChunk::Flag::Rgb) == TBHDChunk::Flag::Rgb) {
return 3;
}
return 0;
@ -1188,12 +1264,12 @@ quint16 TBHDChunk::tiles() const
QImage::Format TBHDChunk::format() const
{
// Support for RGBA and RGB only for now.
if (flags() == TBHDChunk::Flag::RgbA) {
if ((flags() & TBHDChunk::Flag::RgbA) == TBHDChunk::Flag::RgbA) {
if (bpc() == 2)
return QImage::Format_RGBA64;
else if (bpc() == 1)
return QImage::Format_RGBA8888;
} else if (flags() == TBHDChunk::Flag::Rgb) {
} else if ((flags() & TBHDChunk::Flag::Rgb) == TBHDChunk::Flag::Rgb) {
if (bpc() == 2)
return QImage::Format_RGBX64;
else if (bpc() == 1)
@ -1516,12 +1592,12 @@ ANNOChunk::ANNOChunk()
bool ANNOChunk::isValid() const
{
return chunkId() == AUTHChunk::defaultChunkId();
return chunkId() == ANNOChunk::defaultChunkId();
}
QString ANNOChunk::value() const
{
return QString::fromLatin1(data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed();
return dataToString(this);
}
bool ANNOChunk::innerReadStructure(QIODevice *d)
@ -1550,7 +1626,7 @@ bool AUTHChunk::isValid() const
QString AUTHChunk::value() const
{
return QString::fromLatin1(data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed();
return dataToString(this);
}
bool AUTHChunk::innerReadStructure(QIODevice *d)
@ -1580,7 +1656,7 @@ bool COPYChunk::isValid() const
QString COPYChunk::value() const
{
return QString::fromLatin1(data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed();
return dataToString(this);
}
bool COPYChunk::innerReadStructure(QIODevice *d)
@ -1610,6 +1686,9 @@ bool DATEChunk::isValid() const
QDateTime DATEChunk::value() const
{
if (!isValid()) {
return {};
}
return QDateTime::fromString(QString::fromLatin1(data()), Qt::TextDate);
}
@ -1643,6 +1722,9 @@ bool EXIFChunk::isValid() const
MicroExif EXIFChunk::value() const
{
if (!isValid()) {
return {};
}
return MicroExif::fromByteArray(data().mid(6));
}
@ -1652,6 +1734,36 @@ bool EXIFChunk::innerReadStructure(QIODevice *d)
}
/* ******************
* *** ICCN Chunk ***
* ****************** */
ICCNChunk::~ICCNChunk()
{
}
ICCNChunk::ICCNChunk()
{
}
bool ICCNChunk::isValid() const
{
return chunkId() == ICCNChunk::defaultChunkId();
}
QString ICCNChunk::value() const
{
return dataToString(this);
}
bool ICCNChunk::innerReadStructure(QIODevice *d)
{
return cacheData(d);
}
/* ******************
* *** ICCP Chunk ***
* ****************** */
@ -1673,6 +1785,9 @@ bool ICCPChunk::isValid() const
QColorSpace ICCPChunk::value() const
{
if (!isValid()) {
return {};
}
return QColorSpace::fromIccProfile(data());
}
@ -1726,6 +1841,9 @@ bool HISTChunk::isValid() const
QString HISTChunk::value() const
{
if (!isValid()) {
return {};
}
return QString::fromLatin1(data());
}
@ -1756,7 +1874,7 @@ bool NAMEChunk::isValid() const
QString NAMEChunk::value() const
{
return QString::fromLatin1(data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed();
return dataToString(this);
}
bool NAMEChunk::innerReadStructure(QIODevice *d)
@ -1786,6 +1904,9 @@ bool VERSChunk::isValid() const
QString VERSChunk::value() const
{
if (!isValid()) {
return {};
}
return QString::fromLatin1(data());
}
@ -1816,11 +1937,10 @@ bool XMP0Chunk::isValid() const
QString XMP0Chunk::value() const
{
return QString::fromUtf8(data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed();
return dataToString(this);
}
bool XMP0Chunk::innerReadStructure(QIODevice *d)
{
return cacheData(d);
}

View File

@ -49,6 +49,7 @@
#define BODY_CHUNK QByteArray("BODY")
#define CAMG_CHUNK QByteArray("CAMG")
#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 CTBL_CHUNK QByteArray("CTBL") // undocumented
@ -64,6 +65,7 @@
#define COPY_CHUNK QByteArray("(c) ")
#define DATE_CHUNK QByteArray("DATE")
#define EXIF_CHUNK QByteArray("EXIF") // https://aminet.net/package/docs/misc/IFF-metadata
#define ICCN_CHUNK QByteArray("ICCN") // https://aminet.net/package/docs/misc/IFF-metadata
#define ICCP_CHUNK QByteArray("ICCP") // https://aminet.net/package/docs/misc/IFF-metadata
#define FVER_CHUNK QByteArray("FVER")
#define HIST_CHUNK QByteArray("HIST")
@ -467,14 +469,64 @@ public:
virtual bool isValid() const override;
QList<QRgb> palette() const;
/*!
* \brief count
* \return The number of color in the palette.
*/
virtual qint32 count() const;
/*!
* \brief palette
* \param halfbride When True, the new palette values are appended using the halfbride method.
* \return The color palette.
* \note If \a halfbride is true, the returned palette size is count() * 2.
*/
QList<QRgb> palette(bool halfbride = false) const;
CHUNKID_DEFINE(CMAP_CHUNK)
protected:
virtual QList<QRgb> innerPalette() const;
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The CMYKChunk class
*
* This chunk would allow color specification in terms of Cyan,
* Magenta, Yellow, and Black as opposed to the current CMAP which uses RGB.
* The format would be the same as the CMAP chunk with the exception that this
* chunk uses four color components as opposed to three. The number of colors
* contained within would be chunk length/4. This chunk would be used in addition
* to the CMAP chunk.
*/
class CMYKChunk : public CMAPChunk
{
public:
virtual ~CMYKChunk() override;
CMYKChunk();
CMYKChunk(const CMYKChunk& other) = default;
CMYKChunk& operator =(const CMYKChunk& other) = default;
virtual bool isValid() const override;
/*!
* \brief count
* \return The number of color in the palette.
*/
virtual qint32 count() const override;
CHUNKID_DEFINE(CMYK_CHUNK)
protected:
/*!
* \brief palette
* \return The CMYK color palette converted to RGB one.
*/
virtual QList<QRgb> innerPalette() const override;
};
/*!
* \brief The CAMGChunk class
*/
@ -589,6 +641,14 @@ public:
*/
virtual bool resetStrideRead(QIODevice *d) const;
/*!
* \brief safeModeId
* \param header The header.
* \param camg The CAMG chunk.
* \return The most likely ModeId if not explicitly specified.
*/
static CAMGChunk::ModeIds safeModeId(const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap = nullptr);
protected:
/*!
* \brief strideSize
@ -959,6 +1019,28 @@ protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The NAMEChunk class
*/
class ICCNChunk : public IFFChunk
{
public:
virtual ~ICCNChunk() override;
ICCNChunk();
ICCNChunk(const ICCNChunk& other) = default;
ICCNChunk& operator =(const ICCNChunk& other) = default;
virtual bool isValid() const override;
QString value() const;
CHUNKID_DEFINE(ICCN_CHUNK)
protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The ICCPChunk class
*/

View File

@ -15,7 +15,7 @@
#include <QPainter>
#ifdef QT_DEBUG
Q_LOGGING_CATEGORY(LOG_IFFPLUGIN, "kf.imageformats.plugins.iff", QtInfoMsg)
Q_LOGGING_CATEGORY(LOG_IFFPLUGIN, "kf.imageformats.plugins.iff", QtDebugMsg)
#else
Q_LOGGING_CATEGORY(LOG_IFFPLUGIN, "kf.imageformats.plugins.iff", QtWarningMsg)
#endif
@ -184,6 +184,12 @@ void addMetadata(QImage& img, const IFFChunk *form)
if (!iccps.isEmpty()) {
auto cs = iccps.first()->value();
if (cs.isValid()) {
auto iccns = IFFChunk::searchT<ICCNChunk>(form);
if (!iccns.isEmpty()) {
auto desc = iccns.first()->value();
if (!desc.isEmpty())
cs.setDescription(desc);
}
img.setColorSpace(cs);
}
}
@ -223,13 +229,30 @@ bool IFFHandler::readStandardImage(QImage *image)
}
// set color table
auto cmaps = IFFChunk::searchT<CMAPChunk>(form);
if (img.format() == QImage::Format_Indexed8) {
if (!cmaps.isEmpty())
if (auto &&cmap = cmaps.first())
img.setColorTable(cmap->palette());
const CAMGChunk *camg = nullptr;
auto camgs = IFFChunk::searchT<CAMGChunk>(form);
if (!camgs.isEmpty()) {
camg = camgs.first();
}
const CMAPChunk *cmap = nullptr;
auto cmaps = IFFChunk::searchT<CMAPChunk>(form);
if (cmaps.isEmpty()) {
auto cmyks = IFFChunk::searchT<CMYKChunk>(form);
for (auto &&cmyk : cmyks)
cmaps.append(cmyk);
}
if (!cmaps.isEmpty()) {
cmap = cmaps.first();
}
if (img.format() == QImage::Format_Indexed8) {
if (cmap) {
auto halfbride = BODYChunk::safeModeId(header, camg, cmap) & CAMGChunk::ModeId::HalfBrite ? true : false;
img.setColorTable(cmap->palette(halfbride));
}
}
// reading image data
auto bodies = IFFChunk::searchT<BODYChunk>(form);
if (bodies.isEmpty()) {
auto abits = IFFChunk::searchT<ABITChunk>(form);
@ -239,16 +262,6 @@ bool IFFHandler::readStandardImage(QImage *image)
if (bodies.isEmpty()) {
img.fill(0);
} else {
const CAMGChunk *camg = nullptr;
auto camgs = IFFChunk::searchT<CAMGChunk>(form);
if (!camgs.isEmpty()) {
camg = camgs.first();
}
const CMAPChunk *cmap = nullptr;
if (!cmaps.isEmpty())
cmap = cmaps.first();
auto &&body = bodies.first();
if (!body->resetStrideRead(device())) {
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image data";