diff --git a/src/imageformats/chunks.cpp b/src/imageformats/chunks.cpp index a7b0be3..d36e263 100644 --- a/src/imageformats/chunks.cpp +++ b/src/imageformats/chunks.cpp @@ -268,6 +268,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk * chunk = QSharedPointer(new ANNOChunk()); } else if (cid == AUTH_CHUNK) { chunk = QSharedPointer(new AUTHChunk()); + } else if (cid == BEAM_CHUNK) { + chunk = QSharedPointer(new BEAMChunk()); } else if (cid == BMHD_CHUNK) { chunk = QSharedPointer(new BMHDChunk()); } else if (cid == BODY_CHUNK) { @@ -282,6 +284,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk * chunk = QSharedPointer(new CMYKChunk()); } else if (cid == COPY_CHUNK) { chunk = QSharedPointer(new COPYChunk()); + } else if (cid == CTBL_CHUNK) { + chunk = QSharedPointer(new CTBLChunk()); } else if (cid == DATE_CHUNK) { chunk = QSharedPointer(new DATEChunk()); } else if (cid == DPI__CHUNK) { @@ -302,8 +306,12 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk * chunk = QSharedPointer(new ICCPChunk()); } else if (cid == NAME_CHUNK) { chunk = QSharedPointer(new NAMEChunk()); + } else if (cid == RAST_CHUNK) { + chunk = QSharedPointer(new RASTChunk()); } else if (cid == RGBA_CHUNK) { chunk = QSharedPointer(new RGBAChunk()); + } else if (cid == SHAM_CHUNK) { + chunk = QSharedPointer(new SHAMChunk()); } else if (cid == TBHD_CHUNK) { chunk = QSharedPointer(new TBHDChunk()); } else if (cid == VERS_CHUNK) { @@ -312,7 +320,7 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk * chunk = QSharedPointer(new XMP0Chunk()); } else { // unknown chunk chunk = QSharedPointer(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(this); + if (!beam.isEmpty()) { + ipal = beam.first(); + } + auto ctbl = IFFChunk::searchT(this); + if (!ctbl.isEmpty()) { + ipal = ctbl.first(); + } + auto sham = IFFChunk::searchT(this); + if (!sham.isEmpty()) { + ipal = sham.first(); + } + auto rast = IFFChunk::searchT(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(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 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 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 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 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 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 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); +} diff --git a/src/imageformats/chunks_p.h b/src/imageformats/chunks_p.h index aea8196..6560ee0 100644 --- a/src/imageformats/chunks_p.h +++ b/src/imageformats/chunks_p.h @@ -55,10 +55,12 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN) #define CMYK_CHUNK QByteArray("CMYK") // https://wiki.amigaos.net/wiki/ILBM_IFF_Interleaved_Bitmap#ILBM.CMYK #define DPI__CHUNK QByteArray("DPI ") -#define BEAM_CHUNK QByteArray("BEAM") // undocumented (should be the same as CTBL) -#define CTBL_CHUNK QByteArray("CTBL") // undocumented -#define RAST_CHUNK QByteArray("RAST") // undocumented (Atari STE) -#define SHAM_CHUNK QByteArray("SHAM") // undocumented +// Different palette for scanline +#define BEAM_CHUNK QByteArray("BEAM") +#define CTBL_CHUNK QByteArray("CTBL") // same as BEAM +#define PCHG_CHUNK QByteArray("PCHG") // encoded in a unknown way (to be investigated) +#define RAST_CHUNK QByteArray("RAST") // Atari ST(E) +#define SHAM_CHUNK QByteArray("SHAM") // FOR4 CIMG IFF (Maya) #define RGBA_CHUNK QByteArray("RGBA") @@ -89,6 +91,12 @@ 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 +// 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 + /*! * \brief The IFFChunk class */ @@ -341,6 +349,19 @@ private: qint32 _recursionCnt; }; +/*! + * \brief The IPALChunk class + * Interface for additional per-line palette. + */ +class IPALChunk : public IFFChunk +{ +public: + virtual ~IPALChunk() override {} + IPALChunk() : IFFChunk() {} + virtual QList palette(qint32 y, qint32 height) const = 0; +}; + + /*! * \brief The BMHDChunk class * Bitmap Header @@ -630,13 +651,20 @@ public: * \brief readStride * \param d The device. * \param header The bitmap header. + * \param y The current scanline. * \param camg The CAMG chunk (optional) * \param cmap The CMAP chunk (optional) * \param formType The type of the current form chunk. * \return The scanline as requested for QImage. * \warning Call resetStrideRead() once before this one. */ - virtual QByteArray strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr, const QByteArray& formType = ILBM_FORM_TYPE) const; + virtual QByteArray strideRead(QIODevice *d, + qint32 y, + const BMHDChunk *header, + const CAMGChunk *camg = nullptr, + const CMAPChunk *cmap = nullptr, + const IPALChunk *ipal = nullptr, + const QByteArray& formType = ILBM_FORM_TYPE) const; /*! * \brief resetStrideRead @@ -666,13 +694,13 @@ protected: */ quint32 strideSize(const BMHDChunk *header, const QByteArray& formType) const; - QByteArray deinterleave(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr) const; + QByteArray deinterleave(const QByteArray &planes, qint32 y, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr, const IPALChunk *ipal = nullptr) const; - QByteArray pbm(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr) const; + QByteArray pbm(const QByteArray &planes, qint32 y, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr, const IPALChunk *ipal = nullptr) const; - QByteArray rgb8(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr) const; + QByteArray rgb8(const QByteArray &planes, qint32 y, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr, const IPALChunk *ipal = nullptr) const; - QByteArray rgbN(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr) const; + QByteArray rgbN(const QByteArray &planes, qint32 y, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr, const IPALChunk *ipal = nullptr) const; private: mutable QByteArray _readBuffer; @@ -694,12 +722,15 @@ public: CHUNKID_DEFINE(ABIT_CHUNK) - virtual QByteArray strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr, const QByteArray& formType = ACBM_FORM_TYPE) const override; + virtual QByteArray strideRead(QIODevice *d, + qint32 y, + const BMHDChunk *header, + const CAMGChunk *camg = nullptr, + const CMAPChunk *cmap = nullptr, + const IPALChunk *ipal = nullptr, + const QByteArray& formType = ACBM_FORM_TYPE) const override; virtual bool resetStrideRead(QIODevice *d) const override; - -private: - mutable qint32 _y; }; /*! @@ -742,6 +773,19 @@ public: * \return The image size in pixels. */ virtual QSize size() const = 0; + + /*! + * \brief optionformat + * \return The format retuned by the plugin after all conversions. + */ + QImage::Format optionformat() const; + + /*! + * \brief searchIPal + * Search the palett per line chunk. + * \return The per line palette (BEAM, CTBL, SHAM, etc....). + */ + const IPALChunk *searchIPal() const; }; /*! @@ -1250,4 +1294,92 @@ protected: virtual bool innerReadStructure(QIODevice *d) override; }; + +/*! + * *** UNDOCUMENTED CHUNKS *** + */ + +/*! + * \brief The BEAMChunk class + */ +class BEAMChunk : public IPALChunk +{ +public: + virtual ~BEAMChunk() override; + BEAMChunk(); + BEAMChunk(const BEAMChunk& other) = default; + BEAMChunk& operator =(const BEAMChunk& other) = default; + + virtual bool isValid() const override; + + virtual QList palette(qint32 y, qint32 height) const override; + + CHUNKID_DEFINE(BEAM_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + +/*! + * \brief The CTBLChunk class + */ +class CTBLChunk : public BEAMChunk +{ +public: + virtual ~CTBLChunk() override; + CTBLChunk(); + CTBLChunk(const CTBLChunk& other) = default; + CTBLChunk& operator =(const CTBLChunk& other) = default; + + virtual bool isValid() const override; + + CHUNKID_DEFINE(CTBL_CHUNK) +}; + +/*! + * \brief The SHAMChunk class + */ +class SHAMChunk : public IPALChunk +{ +public: + virtual ~SHAMChunk() override; + SHAMChunk(); + SHAMChunk(const SHAMChunk& other) = default; + SHAMChunk& operator =(const SHAMChunk& other) = default; + + virtual bool isValid() const override; + + virtual QList palette(qint32 y, qint32 height) const override; + + CHUNKID_DEFINE(SHAM_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + +/*! + * \brief The RASTChunk class + * \note I found an Atari STE image with the RAST chunk outside + * the form chunk (Fish.neo.iff). To support it the IFF parser + * should be changed so, this kind of IFFs are shown wrong. + */ +class RASTChunk : public IPALChunk +{ +public: + virtual ~RASTChunk() override; + RASTChunk(); + RASTChunk(const RASTChunk& other) = default; + RASTChunk& operator =(const RASTChunk& other) = default; + + virtual bool isValid() const override; + + virtual QList palette(qint32 y, qint32 height) const override; + + CHUNKID_DEFINE(RAST_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + + #endif // KIMG_CHUNKS_P_H diff --git a/src/imageformats/iff.cpp b/src/imageformats/iff.cpp index 881f619..2c39d4f 100644 --- a/src/imageformats/iff.cpp +++ b/src/imageformats/iff.cpp @@ -134,7 +134,7 @@ bool IFFHandler::canRead(QIODevice *device) return ok; } -void addMetadata(QImage& img, const IFFChunk *form) +static void addMetadata(QImage &img, const IFOR_Chunk *form) { // standard IFF metadata auto annos = IFFChunk::searchT(form); @@ -183,11 +183,12 @@ void addMetadata(QImage& img, const IFFChunk *form) } // SView5 metadata + auto resChanged = false; auto exifs = IFFChunk::searchT(form); if (!exifs.isEmpty()) { auto exif = exifs.first()->value(); exif.updateImageMetadata(img, false); - exif.updateImageResolution(img); + resChanged = exif.updateImageResolution(img); } auto xmp0s = IFFChunk::searchT(form); @@ -219,8 +220,59 @@ void addMetadata(QImage& img, const IFFChunk *form) if (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 + if (!resChanged) { + auto headers = IFFChunk::searchT(form); + if (!headers.isEmpty()) { + auto xr = headers.first()->xAspectRatio(); + auto yr = headers.first()->yAspectRatio(); + if (xr > 0 && yr > 0 && xr > yr) { + img.setDotsPerMeterX(img.dotsPerMeterX() * yr / xr); + } else if (xr > 0 && yr > 0 && xr < yr) { + img.setDotsPerMeterY(img.dotsPerMeterY() * xr / yr); + } + } + } +} + +/*! + * \brief convertIPAL + * \param img The source image. + * \param ipal The per line palette. + * \return The new image converted or \a img if no conversion is needed or possible. + */ +static QImage convertIPAL(const QImage& img, const IPALChunk *ipal) +{ + if (img.format() != QImage::Format_Indexed8) { + qDebug(LOG_IFFPLUGIN) << "convertIPAL(): the image is not indexed!"; + return img; + } + + auto tmp = img.convertToFormat(FORMAT_RGB_8BIT); + if (tmp.isNull()) { + qCritical(LOG_IFFPLUGIN) << "convertIPAL(): error while converting the image!"; + return img; + } + + for (auto y = 0, h = img.height(); y < h; ++y) { + auto src = reinterpret_cast(img.constScanLine(y)); + auto dst = tmp.scanLine(y); + auto lpal = ipal->palette(y, h); + 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])); + } + } + } + + return tmp; } bool IFFHandler::readStandardImage(QImage *image) @@ -272,6 +324,7 @@ bool IFFHandler::readStandardImage(QImage *image) } // reading image data + auto ipal = form->searchIPal(); auto bodies = IFFChunk::searchT(form); if (bodies.isEmpty()) { auto abits = IFFChunk::searchT(form); @@ -288,7 +341,7 @@ bool IFFHandler::readStandardImage(QImage *image) } for (auto y = 0, h = img.height(); y < h; ++y) { auto line = reinterpret_cast(img.scanLine(y)); - auto ba = body->strideRead(device(), header, camg, cmap, form->formType()); + auto ba = body->strideRead(device(), y, header, camg, cmap, ipal, form->formType()); if (ba.isEmpty()) { qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image scanline"; return false; @@ -297,6 +350,11 @@ bool IFFHandler::readStandardImage(QImage *image) } } + // BEAM / CTBL conversion (if not already done) + if (ipal && img.format() == QImage::Format_Indexed8) { + img = convertIPAL(img, ipal); + } + // set metadata (including image resolution) addMetadata(img, form); @@ -423,7 +481,7 @@ QVariant IFFHandler::option(ImageOption option) const } if (option == QImageIOHandler::ImageFormat) { - return QVariant::fromValue(form->format()); + return QVariant::fromValue(form->optionformat()); } if (option == QImageIOHandler::ImageTransformation) { diff --git a/src/imageformats/microexif.cpp b/src/imageformats/microexif.cpp index 4b1a401..876ea47 100644 --- a/src/imageformats/microexif.cpp +++ b/src/imageformats/microexif.cpp @@ -1193,12 +1193,13 @@ void MicroExif::updateImageMetadata(QImage &targetImage, bool replaceExisting) c } } -void MicroExif::updateImageResolution(QImage &targetImage) +bool MicroExif::updateImageResolution(QImage &targetImage) { if (horizontalResolution() > 0) targetImage.setDotsPerMeterX(qRound(horizontalResolution() / 25.4 * 1000)); if (verticalResolution() > 0) targetImage.setDotsPerMeterY(qRound(verticalResolution() / 25.4 * 1000)); + return (horizontalResolution() > 0) || (verticalResolution() > 0); } MicroExif MicroExif::fromByteArray(const QByteArray &ba, bool searchHeader) diff --git a/src/imageformats/microexif_p.h b/src/imageformats/microexif_p.h index 6e89b02..fdfcc70 100644 --- a/src/imageformats/microexif_p.h +++ b/src/imageformats/microexif_p.h @@ -339,8 +339,9 @@ public: * \brief updateImageResolution * Helper to set the EXIF resolution to the image. Resolution is set only if valid. * \param targetImage The image to set resolution on. + * \return True if either the x-resolution or the y-resolution has been changed, otherwise false. */ - void updateImageResolution(QImage &targetImage); + bool updateImageResolution(QImage &targetImage); /*! * \brief fromByteArray