mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2026-02-25 08:32:56 -05:00
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:
committed by
Albert Astals Cid
parent
8036b1d032
commit
463da81fad
BIN
autotests/read/iff/aga_pchg_amiga_16cl.iff
Normal file
BIN
autotests/read/iff/aga_pchg_amiga_16cl.iff
Normal file
Binary file not shown.
BIN
autotests/read/iff/aga_pchg_amiga_16cl.png
Normal file
BIN
autotests/read/iff/aga_pchg_amiga_16cl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
autotests/read/iff/aga_pchg_amiga_64cl.iff
Normal file
BIN
autotests/read/iff/aga_pchg_amiga_64cl.iff
Normal file
Binary file not shown.
BIN
autotests/read/iff/aga_pchg_amiga_64cl.png
Normal file
BIN
autotests/read/iff/aga_pchg_amiga_64cl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
BIN
autotests/read/iff/ham5.iff
Normal file
BIN
autotests/read/iff/ham5.iff
Normal file
Binary file not shown.
BIN
autotests/read/iff/ham5.png
Normal file
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
BIN
autotests/read/iff/ham8.iff
Normal file
Binary file not shown.
BIN
autotests/read/iff/ham8.png
Normal file
BIN
autotests/read/iff/ham8.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
BIN
autotests/read/iff/ocs_pchg_amiga_16cl.iff
Normal file
BIN
autotests/read/iff/ocs_pchg_amiga_16cl.iff
Normal file
Binary file not shown.
BIN
autotests/read/iff/ocs_pchg_amiga_16cl.png
Normal file
BIN
autotests/read/iff/ocs_pchg_amiga_16cl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
BIN
autotests/read/iff/ocs_pchg_amiga_64cl.iff
Normal file
BIN
autotests/read/iff/ocs_pchg_amiga_64cl.iff
Normal file
Binary file not shown.
BIN
autotests/read/iff/ocs_pchg_amiga_64cl.png
Normal file
BIN
autotests/read/iff/ocs_pchg_amiga_64cl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
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<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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user