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

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