From 8224c0099d5b10d2108250a6bd7b2a6711f15877 Mon Sep 17 00:00:00 2001 From: Mirco Miranda Date: Thu, 8 Jan 2026 17:49:01 +0100 Subject: [PATCH] Add support for CD-I IFF images --- README.md | 2 + autotests/read/iff/cdi_cl7.iff | Bin 0 -> 570 bytes autotests/read/iff/cdi_cl7.png | Bin 0 -> 1186 bytes src/imageformats/chunks.cpp | 473 ++++++++++++++++++++++++++++++++- src/imageformats/chunks_p.h | 278 ++++++++++++++++++- src/imageformats/iff.cpp | 88 +++++- src/imageformats/iff_p.h | 2 + 7 files changed, 822 insertions(+), 21 deletions(-) create mode 100644 autotests/read/iff/cdi_cl7.iff create mode 100644 autotests/read/iff/cdi_cl7.png diff --git a/README.md b/README.md index bbe1e9a..574daac 100644 --- a/README.md +++ b/README.md @@ -357,6 +357,8 @@ The plugin supports the following image data: type 4. - FORM PBM: PBM is a chunky version of IFF pictures. It supports 8-bit images with color map only. +- FORM IMAG (Compact Disc-Interactive): It supports CLut4, CLut8 and DYuv + formats. - FOR4 CIMG (Maya Image File Format): It supports 24/48-bit RGB and 32/64-bit RGBA images. diff --git a/autotests/read/iff/cdi_cl7.iff b/autotests/read/iff/cdi_cl7.iff new file mode 100644 index 0000000000000000000000000000000000000000..18259a1f57f6ce1c0e0172160f9ddc351ec5db9d GIT binary patch literal 570 zcmajbF=*RB6b9hel7`TfqQ!8V(RkThv}h_Zu3DLh7GW|7yjX(-0E!2%5y9*Txw5Y8eB29qwY=hR?mhko3_f5&@oc*~X}u-ZJW5LbiV z3P{;tSz)okqDb>*@O}}xBm^NS>znk4pI5%`ds*#ew&!_lzGO+o?lNYI42U~hxN+&aBPX?; z+0qGb9p~DPa_eSdS(>H3HiOJG&EZ@yV%rFEg9V0R=&rAGve1u+>cy%`<#8m-9f{pu zxD#pzqUpD-ukE2tbeHt))?n0vj?gs)4rD+8)&(~Prwvvc%m{`A3<^4hz6kFHkTmEJ zxDbHqAQEVufbhKN{j&c>N2A}i`{Bp{ literal 0 HcmV?d00001 diff --git a/autotests/read/iff/cdi_cl7.png b/autotests/read/iff/cdi_cl7.png new file mode 100644 index 0000000000000000000000000000000000000000..64cbee50d7defaa52abd0b465d0992af26abb998 GIT binary patch literal 1186 zcmc(e|5MU;7{K3N-hnQWmUyTw;aW4)@(Xnv>ZRhB`baSS$eFRREopH?DhY}^&c3*h z^So$H#cOlKWl1$1lsY+Y9oiQ{P87m|V@oNWMKTpL7uKxLKJG8rZ+qSI<(}uB=bq>8 zxu>a+vnMPxIurm92JOv-G5^k*;@4qTQ`H!OxnN;7I~#zWBM;T`B>;l1h;niYrSKjm zrL0B&AIjJPDo1a0kvw9618)I31>jZf?=67AGpKh%1$jc|>M)#>5tO-9- zRGg9ccF`CWt=X7aytR%$4#`<+@RB;+w(<_$FFR%SpTfP z22aMBJCDi2qvBdF=AZw-*kyR&EY^1|%pOUuczVp~9eDk;`i4Re;MTQLhrmyLJ z#(7RlUzBCxX4O5%S@x}mTbyxe{Tj%EA`NXE-4ZVH#H~N(?|nP>i^@5|?Up28Yd&-H z&aQz#|NPWl>ERDMR&y?W(lCTRZg}Y(n(S?~!+gkgcWLy_xtc=8?Z~qu``?`M*7O}e zv3|=i6+79Xa!3q_O z!5|iB$&gY9K{SR`%V4DJL^|UY?KH8PDp17=kH_-GWLQBi5RGy4>0m0(new ICCNChunk()); } else if (cid == ICCP_CHUNK) { chunk = QSharedPointer(new ICCPChunk()); + } else if (cid == IDAT_CHUNK) { + chunk = QSharedPointer(new IDATChunk()); + } else if (cid == IHDR_CHUNK) { + chunk = QSharedPointer(new IHDRChunk()); + } else if (cid == IPAR_CHUNK) { + chunk = QSharedPointer(new IPARChunk()); } else if (cid == NAME_CHUNK) { chunk = QSharedPointer(new NAMEChunk()); } else if (cid == PCHG_CHUNK) { chunk = QSharedPointer(new PCHGChunk()); + } else if (cid == PLTE_CHUNK) { + chunk = QSharedPointer(new PLTEChunk()); } else if (cid == RAST_CHUNK) { chunk = QSharedPointer(new RASTChunk()); } else if (cid == RGBA_CHUNK) { @@ -1406,16 +1414,13 @@ bool FORMChunk::innerReadStructure(QIODevice *d) setChunks(IFFChunk::innerFromDevice(d, &ok, this)); } else if (_type == RGBN_FORM_TYPE) { setChunks(IFFChunk::innerFromDevice(d, &ok, this)); + } else if (_type == IMAG_FORM_TYPE) { + setChunks(IFFChunk::innerFromDevice(d, &ok, this)); } return ok; } -QByteArray FORMChunk::formType() const -{ - return _type; -} - -QImage::Format FORMChunk::format() const +QImage::Format FORMChunk::iffFormat() const { auto headers = IFFChunk::searchT(chunks()); if (headers.isEmpty()) { @@ -1464,13 +1469,67 @@ QImage::Format FORMChunk::format() const return QImage::Format_Invalid; } +QImage::Format FORMChunk::cdiFormat() const +{ + auto headers = IFFChunk::searchT(chunks()); + if (headers.isEmpty()) { + return QImage::Format_Invalid; + } + + if (auto &&h = headers.first()) { + if (h->model() == IHDRChunk::Rgb555 && h->depth() == 16) { // no test case + return QImage::Format_RGB555; + } + + if (h->depth() == 4) { + if (h->model() == IHDRChunk::CLut4 || h->model() == IHDRChunk::CLut3) { // CLut3: no test case + return QImage::Format_Indexed8; + } + } + + if (h->depth() == 8) { + if (h->model() == IHDRChunk::CLut8 || h->model() == IHDRChunk::CLut7) { // CLut7: no test case + return QImage::Format_Indexed8; + } + if (h->model() == IHDRChunk::Rgb888) { // no test case + return FORMAT_RGB_8BIT; + } + if (h->model() == IHDRChunk::DYuv && h->yuvKind() == IHDRChunk::One) { + return FORMAT_RGB_8BIT; + } + } + } + + return QImage::Format_Invalid; +} + +QByteArray FORMChunk::formType() const +{ + return _type; +} + +QImage::Format FORMChunk::format() const +{ + if (formType() == IMAG_FORM_TYPE) { + return cdiFormat(); + } + return iffFormat(); +} + QSize FORMChunk::size() const { - auto headers = IFFChunk::searchT(chunks()); - if (headers.isEmpty()) { - return {}; + if (formType() == IMAG_FORM_TYPE) { + auto ihdrs = IFFChunk::searchT(chunks()); + if (!ihdrs.isEmpty()) { + return ihdrs.first()->size(); + } + } else { + auto bmhds = IFFChunk::searchT(chunks()); + if (!bmhds.isEmpty()) { + return bmhds.first()->size(); + } } - return headers.first()->size(); + return {}; } /* ****************** @@ -1583,6 +1642,8 @@ bool CATChunk::innerReadStructure(QIODevice *d) setChunks(IFFChunk::innerFromDevice(d, &ok, this)); } else if (_type == RGBN_FORM_TYPE) { setChunks(IFFChunk::innerFromDevice(d, &ok, this)); + } else if (_type == IMAG_FORM_TYPE) { + setChunks(IFFChunk::innerFromDevice(d, &ok, this)); } return ok; } @@ -2371,6 +2432,398 @@ bool XMP0Chunk::innerReadStructure(QIODevice *d) } +/* ****************** + * *** IHDR Chunk *** + * ****************** */ + +IHDRChunk::~IHDRChunk() +{ + +} + +IHDRChunk::IHDRChunk() +{ + +} + +bool IHDRChunk::isValid() const +{ + if (dataBytes() < 14) { + return false; + } + return chunkId() == IHDRChunk::defaultChunkId(); +} + +qint32 IHDRChunk::width() const +{ + if (!isValid()) { + return 0; + } + return qint32(ui16(data().at(1), data().at(0))); +} + +qint32 IHDRChunk::height() const +{ + if (!isValid()) { + return 0; + } + return qint32(ui16(data().at(5), data().at(4))); +} + +QSize IHDRChunk::size() const +{ + return QSize(width(), height()); +} + +qint32 IHDRChunk::lineSize() const +{ + if (!isValid()) { + return 0; + } + return qint32(ui16(data().at(3), data().at(2))); +} + +quint16 IHDRChunk::depth() const +{ + if (!isValid()) { + return 0; + } + return qint32(ui16(data().at(9), data().at(8))); +} + +IHDRChunk::Model IHDRChunk::model() const +{ + if (!isValid()) { + return IHDRChunk::Model::Invalid; + } + return IHDRChunk::Model(ui16(data().at(7), data().at(6))); +} + +IHDRChunk::DYuvKind IHDRChunk::yuvKind() const +{ + if (!isValid()) { + return IHDRChunk::DYuvKind::One; + } + return IHDRChunk::DYuvKind(data().at(10)); +} + +IHDRChunk::Yuv IHDRChunk::yuvStart() const +{ + if (!isValid()) { + return{}; + } + return(Yuv(data().at(11), data().at(12), data().at(13))); +} + +bool IHDRChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + + +/* ****************** + * *** IPAR Chunk *** + * ****************** */ + +IPARChunk::~IPARChunk() +{ + +} + +IPARChunk::IPARChunk() +{ + +} + +bool IPARChunk::isValid() const +{ + if (dataBytes() < 22) { + return false; + } + return chunkId() == IPARChunk::defaultChunkId(); +} + +qint32 IPARChunk::xOffset() const +{ + if (!isValid()) { + return 0; + } + return qint32(ui16(data().at(1), data().at(0))); +} + +qint32 IPARChunk::yOffset() const +{ + if (!isValid()) { + return 0; + } + return qint32(ui16(data().at(3), data().at(2))); +} + +double IPARChunk::aspectRatio() const +{ + if (!isValid()) { + return 1; + } + if (auto xr = ui16(data().at(5), data().at(4))) { + auto yr = double(ui16(data().at(7), data().at(6))); + return yr / xr; + } + return 1; +} + +qint32 IPARChunk::xPage() const +{ + if (!isValid()) { + return 0; + } + return qint32(ui16(data().at(9), data().at(8))); +} + +qint32 IPARChunk::yPage() const +{ + if (!isValid()) { + return 0; + } + return qint32(ui16(data().at(11), data().at(10))); +} + +qint32 IPARChunk::xGrub() const +{ + if (!isValid()) { + return 0; + } + return qint32(ui16(data().at(13), data().at(12))); +} + +qint32 IPARChunk::yGrub() const +{ + if (!isValid()) { + return 0; + } + return qint32(ui16(data().at(15), data().at(14))); +} + +IPARChunk::Rgb IPARChunk::mask() const +{ + if (!isValid()) { + return {}; + } + return Rgb(data().at(16), data().at(17), data().at(18)); +} + +IPARChunk::Rgb IPARChunk::transparency() const +{ + if (!isValid()) { + return {}; + } + return Rgb(data().at(19), data().at(20), data().at(21)); +} + +bool IPARChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + + +/* ****************** + * *** PLTE Chunk *** + * ****************** */ + +PLTEChunk::~PLTEChunk() +{ + +} + +PLTEChunk::PLTEChunk() +{ + +} + +bool PLTEChunk::isValid() const +{ + if (dataBytes() < 4) { + return false; + } + if (dataBytes() - 4 < total() * 3) { + return false; + } + return chunkId() == PLTEChunk::defaultChunkId(); +} + +qint32 PLTEChunk::count() const +{ + if (!isValid()) { + return 0; + } + return total() - count(); +} + +qint32 PLTEChunk::offset() const +{ + return qint32(ui16(data().at(1), data().at(0))); +} + +qint32 PLTEChunk::total() const +{ + return qint32(ui16(data().at(3), data().at(2))); +} + +QList PLTEChunk::innerPalette() const +{ + if (!isValid()) { + return{}; + } + QList l; + auto &&d = data(); + for (qint32 i = offset(), n = total(); i < n; ++i) { + auto i3 = 4 + i * 3; + l << qRgb(d.at(i3), d.at(i3 + 1), d.at(i3 + 2)); + } + return l; +} + + +/* ****************** + * *** IDAT Chunk *** + * ****************** */ + +IDATChunk::~IDATChunk() +{ + +} + +IDATChunk::IDATChunk() +{ + +} + +bool IDATChunk::isValid() const +{ + return chunkId() == IDATChunk::defaultChunkId(); +} + +/*! + * Converts a YUV pixel to RGB. + */ +inline IPARChunk::Rgb yuvToRgb(IHDRChunk::Yuv yuv) { + IPARChunk::Rgb rgb; + + // Green Book Cap. V Par. 4.4.2.2 + const auto b = yuv.y + (yuv.u - 128.) * 1.733; + const auto r = yuv.y + (yuv.v - 128.) * 1.371; + const auto g = (yuv.y - 0.299 * r - 0.114 * b) / 0.587; + + rgb.r = quint8(std::clamp(r + 0.5, 0., 255.)); + rgb.g = quint8(std::clamp(g + 0.5, 0., 255.)); + rgb.b = quint8(std::clamp(b + 0.5, 0., 255.)); + + return rgb; +} + + +QByteArray IDATChunk::strideRead(QIODevice *d, qint32 y, const IHDRChunk *header, const IPARChunk *params) const +{ + Q_UNUSED(y) + Q_UNUSED(params) + if (!isValid() || header == nullptr || d == nullptr) { + return {}; + } + + auto read = strideSize(header); + for (auto nextPos = nextChunkPos(); !d->atEnd() && d->pos() < nextPos;) { + auto rr = d->read(read); + + if (header->model() == IHDRChunk::CLut4) { + if (rr.size() < header->width() / 2) { + return {}; + } + QByteArray tmp(header->width(), char()); + for (auto x = 0, w = header->width(); x < w; ++x) { + auto i8 = quint8(rr.at(x / 2)); + tmp[x] = x & 1 ? i8 & 0xF : (i8 >> 4) & 0xF; + } + rr = tmp; + } + + if (header->model() == IHDRChunk::Rgb555) { + for(qint32 x = 0, w = rr.size() - 1; x < w; x += 2) { + std::swap(rr[x], rr[x + 1]); + } + } + + if (header->model() == IHDRChunk::DYuv) { + if (rr.size() < header->width()) { + return {}; + } + + // delta table: Green Book Cap. V Par. 3.4.1.3 + // NOTE 1: using the wrong delta table creates visible artifacts on the image. + // NOTE 2: using { 0, 1, 4, 9, 16, 27, 44, 79, -128, -79, -44, -27, -16, -9, -4, -1 } + // table gives the same result (when assigned to an uint8). + static const qint32 deltaTable[16] = { + 0, 1, 4, 9, 16, 27, 44, 79, 128, 177, 212, 229, 240, 247, 252, 255 + }; + + auto yuv = header->yuvStart(); + QByteArray tmp(header->width() * 3, char()); + for (auto x = 0, w = header->width() - 1; x < w; x += 2) { + // nibble order from Green Book Cap. V Par. 6.5.1.1 + // NOTE: using the wrong nibble order creates visible artifacts on the image. + const auto du = deltaTable[(rr.at(x) >> 4) & 0x0F]; + const auto d1 = deltaTable[rr.at(x) & 0x0F]; + const auto dv = deltaTable[(rr.at(x + 1) >> 4) & 0x0F]; + const auto d2 = deltaTable[rr.at(x + 1) & 0x0F]; + + // pixel 1 + yuv.y = d1 + yuv.y; + yuv.u = du + yuv.u; + yuv.v = dv + yuv.v; + auto rgb = yuvToRgb(yuv); + tmp[x * 3] = rgb.r; + tmp[x * 3 + 1] = rgb.g; + tmp[x * 3 + 2] = rgb.b; + + // pixel 2 + yuv.y = d2 + yuv.y; + rgb = yuvToRgb(yuv); + tmp[(x + 1) * 3] = rgb.r; + tmp[(x + 1) * 3 + 1] = rgb.g; + tmp[(x + 1) * 3 + 2] = rgb.b; + } + rr = tmp; + } + + return rr; + } + + return {}; +} + +bool IDATChunk::resetStrideRead(QIODevice *d) const +{ + return seek(d); +} + +quint32 IDATChunk::strideSize(const IHDRChunk *header) const +{ + if (header == nullptr) { + return 0; + } + + auto rs = (header->width() * header->depth() + 7) / 8; + + // No padding bytes are inserted in the data. + if (header->model() == IHDRChunk::Rgb888) { + return rs; + } + + // The first pixel of each scan line must begin in a longword boundary. + if (auto mod = rs % 4) + rs += (4 - mod); + return rs; +} + + /* ****************** * *** BEAM Chunk *** * ****************** */ diff --git a/src/imageformats/chunks_p.h b/src/imageformats/chunks_p.h index bb9d485..55391ad 100644 --- a/src/imageformats/chunks_p.h +++ b/src/imageformats/chunks_p.h @@ -55,6 +55,10 @@ 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 IDAT_CHUNK QByteArray("IDAT") +#define IHDR_CHUNK QByteArray("IHDR") +#define IPAR_CHUNK QByteArray("IPAR") +#define PLTE_CHUNK QByteArray("PLTE") #define XBMI_CHUNK QByteArray("XBMI") // Different palette for scanline @@ -82,11 +86,13 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN) #define VERS_CHUNK QByteArray("VERS") #define XMP0_CHUNK QByteArray("XMP0") // https://aminet.net/package/docs/misc/IFF-metadata +// FORM types #define ACBM_FORM_TYPE QByteArray("ACBM") #define ILBM_FORM_TYPE QByteArray("ILBM") #define PBM__FORM_TYPE QByteArray("PBM ") #define RGB8_FORM_TYPE QByteArray("RGB8") #define RGBN_FORM_TYPE QByteArray("RGBN") +#define IMAG_FORM_TYPE QByteArray("IMAG") #define CIMG_FOR4_TYPE QByteArray("CIMG") #define TBMP_FOR4_TYPE QByteArray("TBMP") @@ -756,10 +762,11 @@ public: /*! * \brief readStride * \param d The device. - * \param header The bitmap header. * \param y The current scanline. + * \param header The bitmap header. * \param camg The CAMG chunk (optional) * \param cmap The CMAP chunk (optional) + * \param ipal The per-line palette 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. @@ -776,8 +783,6 @@ public: * \brief resetStrideRead * Reset the stride read set the position at the beginning of the data and reset all buffers. * \param d The device. - * \param header The BMHDChunk chunk (mandatory) - * \param camg The CAMG chunk (optional) * \return True on success, otherwise false. * \sa strideRead * \note Must be called once before strideRead(). @@ -788,6 +793,7 @@ public: * \brief safeModeId * \param header The header. * \param camg The CAMG chunk. + * \param cmap The CMAP chunk. * \return The most likely ModeId if not explicitly specified. */ static CAMGChunk::ModeIds safeModeId(const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap = nullptr); @@ -921,6 +927,11 @@ public: protected: virtual bool innerReadStructure(QIODevice *d) override; + +private: + QImage::Format iffFormat() const; + + QImage::Format cdiFormat() const; }; @@ -1400,6 +1411,267 @@ protected: virtual bool innerReadStructure(QIODevice *d) override; }; +/*! + * *** I-CD IFF CHUNKS *** + */ + +/*! + * \brief The IHDRChunk class + * Image Header + */ +class IHDRChunk: public IFFChunk +{ +public: + enum Model { + Invalid = 0, /**< Invalid model. */ + Rgb888 = 1, /**< red, green, blue - 8 bits per color. */ + Rgb555 = 2, /**< Green Book absolute RGB. */ + DYuv = 3, /**< Green Book Delta YUV. */ + CLut8 = 4, /**< Green Book 8 bit CLUT. */ + CLut7 = 5, /**< Green Book 7 bit CLUT. */ + CLut4 = 6, /**< Green Book 4 bit CLUT. */ + CLut3 = 7, /**< Green Book 3 bit CLUT. */ + Rle7 = 8, /**< Green Book runlength coded 7 bit CLUT. */ + Rle3 = 9, /**< Green Book runlength coded 3 bit CLUT. */ + PaletteOnly = 10 /**< color palette only. */ + }; + + enum DYuvKind { + One = 0, + Each = 1 + }; + + struct Yuv { + Yuv(quint8 y0 = 0, quint8 u0 = 0, quint8 v0 = 0) : y(y0), u(u0), v(v0) {} + quint8 y; + quint8 u; + quint8 v; + }; + + virtual ~IHDRChunk() override; + + IHDRChunk(); + IHDRChunk(const IHDRChunk& other) = default; + IHDRChunk& operator =(const IHDRChunk& other) = default; + + virtual bool isValid() const override; + + /*! + * \brief width + * \return Width of the bitmap in pixels. + */ + qint32 width() const; + + /*! + * \brief height + * \return Height of the bitmap in pixels. + */ + qint32 height() const; + + /*! + * \brief size + * \return Size in pixels. + */ + QSize size() const; + + /*! + * \brief lineSize + * Physical width of image (number of bytes in each scan line, including any data required at + * the end of each scan line for padding [see description of each model’s IDAT chunk for padding + * rules]) This field is not used when model() = Rle7 or Rle3. + * When model() = Rgb555, this field defines the size of one scan line of the upper + * or lower portion of the pixel data, but not the size of them both together. + */ + qint32 lineSize() const; + + /*! + * \brief model + * Image model (coding method) + */ + Model model() const; + + /*! + * \brief depth + * Physical size of pixel (number of bits per pixel used in storing image data) When + * model() = Rle7 or Rle3, this value only represents the size of a + * single pixel; the size of a run of pixels is indeterminate. + */ + quint16 depth() const; + + /*! + * \brief yuvKind + * if model() = DYuv, indicates whether there is one DYUV start value for all + * scan lines (in yuvStart()), or whether each scan line has its own start value in the + * YUVS chunk which follows. + */ + DYuvKind yuvKind() const; + + /*! + * \brief yuvStart + * Start values for DYUV image if model() = DYuv and dYuvKind() = One + */ + Yuv yuvStart() const; + + CHUNKID_DEFINE(IHDR_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + + +/*! + * \brief The IHDRChunk class + */ +class IPARChunk: public IFFChunk +{ +public: + struct Rgb { + Rgb(quint8 r0 = 0, quint8 g0 = 0, quint8 b0 = 0) : r(r0), g(g0), b(b0) {} + quint8 r; + quint8 g; + quint8 b; + }; + + virtual ~IPARChunk() override; + + IPARChunk(); + IPARChunk(const IPARChunk& other) = default; + IPARChunk& operator =(const IPARChunk& other) = default; + + virtual bool isValid() const override; + + /*! + * \brief xOffset + * X offset of origin in source image [0 < xOffset() < xPage()] + */ + qint32 xOffset() const; + + /*! + * \brief yOffset + * \returnX offset of origin in source image [0 < yOffset() < yPage()] + */ + qint32 yOffset() const; + + /*! + * \brief aspectRatio + * Aspect ratio of pixels in source image. + */ + double aspectRatio() const; + + /*! + * \brief xPage + * X size of source image. + */ + qint32 xPage() const; + + /*! + * \brief yPage + * Y size of source image. + */ + qint32 yPage() const; + + /*! + * \brief xGrub + * X location of hot spot within image. + */ + qint32 xGrub() const; + + /*! + * \brief yGrub + * Y location of hot spot within image. + */ + qint32 yGrub() const; + + /*! + * \brief transparency + * Transparent color. + */ + Rgb transparency() const; + + /*! + * \brief mask + * Mask color. + */ + Rgb mask() const; + + CHUNKID_DEFINE(IPAR_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + + +/*! + * \brief The PLTEChunk class + */ +class PLTEChunk : public CMAPChunk +{ +public: + virtual ~PLTEChunk() override; + PLTEChunk(); + PLTEChunk(const PLTEChunk& other) = default; + PLTEChunk& operator =(const PLTEChunk& other) = default; + + virtual bool isValid() const override; + + /*! + * \brief count + * \return The number of color in the palette. + */ + virtual qint32 count() const override; + + CHUNKID_DEFINE(PLTE_CHUNK) + +protected: + qint32 offset() const; + + qint32 total() const; + + virtual QList innerPalette() const override; +}; + +/*! + * \brief The IDATChunk class + */ +class IDATChunk : public IFFChunk +{ +public: + virtual ~IDATChunk() override; + IDATChunk(); + IDATChunk(const IDATChunk& other) = default; + IDATChunk& operator =(const IDATChunk& other) = default; + + virtual bool isValid() const override; + + CHUNKID_DEFINE(IDAT_CHUNK) + + /*! + * \brief readStride + * \param d The device. + * \param y The current scanline. + * \param header The bitmap header. + * \param params The additional parameters (optional) + * \return The scanline as requested for QImage. + * \warning Call resetStrideRead() once before this one. + */ + QByteArray strideRead(QIODevice *d, + qint32 y, + const IHDRChunk *header, + const IPARChunk *params = nullptr) const; + + /*! + * \brief resetStrideRead + * Reset the stride read set the position at the beginning of the data and reset all buffers. + * \param d The device. + * \return True on success, otherwise false. + * \sa strideRead + * \note Must be called once before strideRead(). + */ + bool resetStrideRead(QIODevice *d) const; + +protected: + quint32 strideSize(const IHDRChunk *header) const; +}; /*! * *** UNDOCUMENTED CHUNKS *** diff --git a/src/imageformats/iff.cpp b/src/imageformats/iff.cpp index 0f24366..422908c 100644 --- a/src/imageformats/iff.cpp +++ b/src/imageformats/iff.cpp @@ -262,14 +262,21 @@ static void addMetadata(QImage &img, const IFOR_Chunk *form) // 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); + if (form->formType() == IMAG_FORM_TYPE) { + auto params = IFFChunk::searchT(form); + if (!params.isEmpty()) { + img.setDotsPerMeterY(img.dotsPerMeterY() * params.first()->aspectRatio()); + } + } else { + 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); + } } } } @@ -475,6 +482,67 @@ bool IFFHandler::readMayaImage(QImage *image) return true; } +bool IFFHandler::readCDIImage(QImage *image) +{ + auto forms = d->searchForms(); + if (forms.isEmpty()) { + return false; + } + auto cin = qBound(0, currentImageNumber(), int(forms.size() - 1)); + auto &&form = forms.at(cin); + + // show the first one (I don't have a sample with many images) + auto headers = IFFChunk::searchT(form); + if (headers.isEmpty()) { + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readCDIImage(): no supported image found"; + return false; + } + + // create the image + auto &&header = headers.first(); + auto img = imageAlloc(header->size(), form->format()); + if (img.isNull()) { + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readCDIImage(): error while allocating the image"; + return false; + } + + // set the palette + if (img.format() == QImage::Format_Indexed8) { + auto pltes = IFFChunk::searchT(form); + if (!pltes.isEmpty()) { + img.setColorTable(pltes.first()->palette()); + } + } + + // decoding the image + auto bodies = IFFChunk::searchT(form); + if (bodies.isEmpty()) { + img.fill(0); + } else { + auto &&body = bodies.first(); + if (!body->resetStrideRead(device())) { + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readCDIImage(): error while reading image data"; + return false; + } + auto pars = IFFChunk::searchT(form); + for (auto y = 0, h = img.height(); y < h; ++y) { + auto line = reinterpret_cast(img.scanLine(y)); + auto ba = body->strideRead(device(), y, header, pars.isEmpty() ? nullptr : pars.first()); + if (ba.isEmpty()) { + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readCDIImage(): error while reading image scanline"; + return false; + } + memcpy(line, ba.constData(), std::min(img.bytesPerLine(), ba.size())); + } + } + + // set metadata (including image resolution) + addMetadata(img, form); + + *image = img; + return true; +} + bool IFFHandler::read(QImage *image) { if (!d->readStructure(device())) { @@ -490,6 +558,10 @@ bool IFFHandler::read(QImage *image) return true; } + if (readCDIImage(image)) { + return true; + } + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read(): no supported image found"; return false; } diff --git a/src/imageformats/iff_p.h b/src/imageformats/iff_p.h index 114844b..76cde44 100644 --- a/src/imageformats/iff_p.h +++ b/src/imageformats/iff_p.h @@ -35,6 +35,8 @@ private: bool readMayaImage(QImage *image); + bool readCDIImage(QImage *image); + private: const QScopedPointer d; };