From c2a1d4b4018608d7ebd6eecef99e2a2a0628f07e Mon Sep 17 00:00:00 2001 From: Mirco Miranda Date: Fri, 1 Aug 2025 15:29:52 +0200 Subject: [PATCH] IFF: Fix halfbride detection, 1-bitplane colors and PBM line size calculation. It also ignore ZBuffer flag on Maya images (like Photoshop does) amd adds CMYK palette support. --- autotests/read/iff/ps_testcard_cmyk.iff | Bin 0 -> 15880 bytes autotests/read/iff/ps_testcard_cmyk.png | Bin 0 -> 3337 bytes .../read/iff/testcard_indexed1_amiga.iff | Bin 0 -> 2248 bytes .../read/iff/testcard_indexed1_amiga.png | Bin 0 -> 2242 bytes src/imageformats/chunks.cpp | 236 +++++++++++++----- src/imageformats/chunks_p.h | 84 ++++++- src/imageformats/iff.cpp | 45 ++-- 7 files changed, 290 insertions(+), 75 deletions(-) create mode 100644 autotests/read/iff/ps_testcard_cmyk.iff create mode 100644 autotests/read/iff/ps_testcard_cmyk.png create mode 100644 autotests/read/iff/testcard_indexed1_amiga.iff create mode 100644 autotests/read/iff/testcard_indexed1_amiga.png diff --git a/autotests/read/iff/ps_testcard_cmyk.iff b/autotests/read/iff/ps_testcard_cmyk.iff new file mode 100644 index 0000000000000000000000000000000000000000..39d21016401eb8c5f0b9f38f4dd44ce4cabb0846 GIT binary patch literal 15880 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 literal 0 HcmV?d00001 diff --git a/autotests/read/iff/testcard_indexed1_amiga.iff b/autotests/read/iff/testcard_indexed1_amiga.iff new file mode 100644 index 0000000000000000000000000000000000000000..ad76ba3405d36a0fe6495afda2365d66e6b2756c GIT binary patch literal 2248 zcmZXWUrbY19LIm>-rlxWqpd-GSeA6oZme!`W{-O*1r(Xg+j>tcB?h02fjJ4vVq6>* zL^k&@eDrXMWf2U`w#2sR00LpdWwnhXIS@Uq*3#OP!9$E~Nw^>+4!}Vr0Vyf_oZT z(c!a2P>!qlm#_q?<8QUJ;-3;|PD08eEPx3P)=_xSS>JhJi2_Z$5(*uEYo8VPLeoNg zIh3Wq$(zGMTu-O+B`AZ!xW6RW1>FiXV;Y0hNF~@C#^9Pk0k~fRs22Ws{6VQxIh@Oz z)cnR!rW8_)vq=*qsH~M^8zpnFV1?T6l`#CH6|NO2>uv0=6>f;7Vkpiwb1Qt`pKC4& zHO0r{W%OEu)8ci=5~#Ao0v0KbX&a)_qi{gJfsov7I>&5%w+ zxjv@Ao2XeW&DMQH96#xdZ$b^~j5HCA>o)ibHLoKzh^))$%-AW*;%g)kBCam5Ddh73 z=HoAe2eyqDlnnVeq?zM!K=k%>5^Q92NtQCr7HTc(nod2Dvcp1Au7hivEGhkncz_xT zRMk0TIgNSqa1xk@MN>M|UXdjY6X)0h5%1A88~W&%DhL~{D*y~(kFHIamVs;JQ9H&9 zHaGTISC4ra_o!j}Z%O4WCSSW1^NOUw?y-xU4t1Mkc|avE0~C)kiuSeWr4Lt-=F$8! zI-Eu?7mkthT0qoZ)NNtR{vxGXLCq;$S%r!kE7cL$;MvgLqq7FgzRyZEL}PRn98%6L z4j-#I2b3x%F*iOd&66>a2sW`?&ZBxLxehMUz}VNBQ*x1Euh)8ZKS$e7LGnZbc5{gr%=_qDAFNP8PKJ5t?R

V{S<@3ED^!Vu!OEQF zi=N`19_*0EL^?nnh8`h`&5&wd2!DDRMJX+QLS(>(sFhUvIjMvg5lKY59)S~9|K%7Q z)CT*@Q(C?{T@NqM4h{_3`=)|Zw{wwuG@jw<)Bv*n{;9rv2FHUFvA-wkun!LO2dC;! z(GU>@1|nw$&kU+l{Zn;S)WM_J?u_MCMLvxWP><+!yNdJ_d1?Q8(K34N%U%H%0eG%H zzZ1D@9)zRi_q6Rfo_q96eI}I4Rq`&*)}x|Y4Dm>Xz;>K0BE~?KXxnM7y4CKX|7h|^ zGQO1>BHtxe`sBORE?2<5+!u?nyxnfU^F~48Z6&GUxx7L9-#DLivt};ktxuj*XDQsM z2L-=6T3(o&s17#d!&Bz}eo5Y(e_Y*XMnYe2i2IW#7kW|E1U8Rts8I(c5nYg}^ zL@49a8x4M9!pzDa$O~u5b`C%hqc3pNJfYRwsBXNzW40_^OVW(WiCp9tTD{Qb6psy0 z_TnzWymr%48Hvclg|d`S+sVs1ahn@?=grZl3%&DM?9oCQUo>}QsaKXKmrhcTn8byI zF_$~CFe&FpvxLxox-r@@H!)%2hA(nF_KID}`1p7c{T(x-6WmAxXjZBYQ#wkt|MAh- z^&JJM5SHU(q`9~`StXFc>j{ZS=*t=&vxtrpQ*CA4Y8r5y&}Blmp$6U+t>`k;z+Y)) zE76I;f&!ZN1=dEqVO4N`Q+SCLDu-9H3R*B(2BtKbz%mFVFrQ9uD_6LZ&>KDvy20uG z;x&#JKfWftLurb~fl(PKo3aelM)}c=-R9Mdh1g I5#ZH-0H{lVtpET3 literal 0 HcmV?d00001 diff --git a/autotests/read/iff/testcard_indexed1_amiga.png b/autotests/read/iff/testcard_indexed1_amiga.png new file mode 100644 index 0000000000000000000000000000000000000000..cf57684ad4b3fd11f279a1d43b1700827db73b24 GIT binary patch literal 2242 zcmV;z2tD_SP)uwwmT`zW^!x9fTVblK;$Am)4C@^XxPG9T`7eIw*PGf~I?Na5yx?6)_gMl47$oF4{ykOlY{!=1G9Z%n*RQ+d_n*N(yfc1WCWeE&VPvng`IvQ(UEgHDLth103$9NIa7PG}Tn0cz~FGkU04B7R0qep#4gWD*A{?NY77dLMBCE{FGcYbb#D!5ypZr z*{i^E5etDoZ&8R?Q8cH*wjOdaO2b?Q-dvpu8y!|U?S)91>V0>aTsWg_MrpZ_0jU#R z3E@KBGSm=KPUHc`xUL8J8Ofh-vN2cEmWDBI>Olqd&z-)d65|2>ZX{E-^}vhLeSy|W z2m(fxWEdQ)y5H&q&4MxHFYP>qx`2A&$%r*s$t;+q4&#csz}Td)R}6KC3cyKKg?e8Y zMn{JTf%)P(fC^>4jr0x!NL-go~tWt^;DE=jdRhGxZ3mh6x>E zoeD)gAVz-fijnP4zJb|~m~wMEjBY)I?>eEcYf@F<^#B+|hdl$6DN(8lydG9EQ17J- zegy)|byc5qGIVs{4F0j3qQy{tFFF|-4D%3F(68tRX(V4GlWw@}8!s0RKB*9u10V4D$rFM9jk+QTL2NySfxp)B~CG{$t86FCVtDkl|E?tOt0IBdCB%5r8#MPyu~G0^Qpfob-$Skdn%hQ45e}8$&P& zP7ZZQes=XRA~sW24<$qBV5c(V=^B``87LPzRAz|`eFpK+(}7V06*jp5>A`B6RqIe0 zWh&I)7nh|lahSIqH9o*uC*plJ(NBmhq+&)LNOZ10IFsgE*9E9?gFw1UlQ)%WV3=6R zcjY4K;n<4)zjCA!2hY_*h&J3D-A`s)Ey~>eUt?1258&aty5G zP>UfG#sK&H>p2~&S7I2u16F*QK?(-~D1^7*3iWV;F88l+mIz$F1L#;!MDXCxpHYHS} z3xX&dl$g()GUy*Jl)z_c@=zUUlm=0a7zz+Apo@-TcokOtfpk!r6ufF6&K+~@Hww;V^$=4c-n|SMeXMo$32J8*^ihuiWk4Yz zI&OgTjGHs(V*=f!TsGp zF$~0Y@s#&9&)szA9~{;S<3Fe3jTbc7GQ2SU1_p7 +#include #include #include @@ -16,6 +17,14 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN) #define RECURSION_PROTECTION 10 +static QString dataToString(const IFFChunk *chunk) +{ + if (chunk == nullptr || !chunk->isValid()) { + return {}; + } + return QString::fromUtf8(chunk->data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed(); +} + IFFChunk::~IFFChunk() { @@ -258,6 +267,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk * chunk = QSharedPointer(new CAMGChunk()); } else if (cid == CMAP_CHUNK) { chunk = QSharedPointer(new CMAPChunk()); + } else if (cid == CMYK_CHUNK) { + chunk = QSharedPointer(new CMYKChunk()); } else if (cid == COPY_CHUNK) { chunk = QSharedPointer(new COPYChunk()); } else if (cid == DATE_CHUNK) { @@ -274,6 +285,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk * chunk = QSharedPointer(new FVERChunk()); } else if (cid == HIST_CHUNK) { chunk = QSharedPointer(new HISTChunk()); + } else if (cid == ICCN_CHUNK) { + chunk = QSharedPointer(new ICCNChunk()); } else if (cid == ICCP_CHUNK) { chunk = QSharedPointer(new ICCPChunk()); } else if (cid == NAME_CHUNK) { @@ -478,21 +491,87 @@ bool CMAPChunk::isValid() const return chunkId() == CMAPChunk::defaultChunkId(); } +qint32 CMAPChunk::count() const +{ + if (!isValid()) { + return 0; + } + return bytes() / 3; +} + +QList CMAPChunk::palette(bool halfbride) const +{ + auto p = innerPalette(); + if (!halfbride) { + return p; + } + auto tmp = p; + for(auto &&v : tmp) { + p << qRgb(qRed(v) / 2, qGreen(v) / 2, qBlue(v) / 2); + } + return p; +} + bool CMAPChunk::innerReadStructure(QIODevice *d) { return cacheData(d); } -QList CMAPChunk::palette() const +QList CMAPChunk::innerPalette() const { QList l; auto &&d = data(); - for (quint32 i = 0, n = bytes() / 3; i < n; ++i) { - l << qRgb(d.at(i * 3), d.at(i * 3 + 1), d.at(i * 3 + 2)); + for (qint32 i = 0, n = count(); i < n; ++i) { + auto i3 = i * 3; + l << qRgb(d.at(i3), d.at(i3 + 1), d.at(i3 + 2)); } return l; } + +/* ****************** + * *** CMYK Chunk *** + * ****************** */ + +CMYKChunk::~CMYKChunk() +{ + +} + +CMYKChunk::CMYKChunk() : CMAPChunk() +{ + +} + +bool CMYKChunk::isValid() const +{ + return chunkId() == CMYKChunk::defaultChunkId(); +} + +qint32 CMYKChunk::count() const +{ + if (!isValid()) { + return 0; + } + return bytes() / 4; +} + +QList CMYKChunk::innerPalette() const +{ + QList l; + auto &&d = data(); + for (qint32 i = 0, n = count(); i < n; ++i) { + auto i4 = i * 4; + auto C = quint8(d.at(i4)) / 255.; + auto M = quint8(d.at(i4 + 1)) / 255.; + auto Y = quint8(d.at(i4 + 2)) / 255.; + auto K = quint8(d.at(i4 + 3)) / 255.; + l << QColor::fromCmykF(C, M, Y, K).toRgb().rgb(); + } + return l; +} + + /* ****************** * *** CAMG Chunk *** * ****************** */ @@ -630,20 +709,33 @@ bool BODYChunk::resetStrideRead(QIODevice *d) const return seek(d); } +CAMGChunk::ModeIds BODYChunk::safeModeId(const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap) +{ + if (camg) { + return camg->modeId(); + } + if (header == nullptr) { + return CAMGChunk::ModeIds(); + } + if (cmap && cmap->count() == (1 << (header->bitplanes() - 1))) { + return CAMGChunk::ModeIds(CAMGChunk::ModeId::HalfBrite); + } + if (header->bitplanes() == 6) { + // If no CAMG chunk is present, and image is 6 planes deep, + // assume HAM and you'll probably be right. + return CAMGChunk::ModeIds(CAMGChunk::ModeId::Ham); + } + return CAMGChunk::ModeIds(); +} + quint32 BODYChunk::strideSize(const BMHDChunk *header, bool isPbm) const { - auto rs = header->rowLen() * header->bitplanes(); if (!isPbm) { - return rs; + return header->rowLen() * header->bitplanes(); } - - // I found two versions of PBM: one uses ILBM calculation, the other uses width-based. - // As it is a proprietary extension, one of them was probably generated incorrectly. - if (header->compression() == BMHDChunk::Compression::Uncompressed) { - if (rs * header->height() != bytes()) - rs = header->width() * header->bitplanes() / 8; - } - + auto rs = header->width() * header->bitplanes() / 8; + if (rs & 1) + ++rs; return rs; } @@ -655,21 +747,12 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he auto rowLen = qint32(header->rowLen()); auto bitplanes = header->bitplanes(); - auto modeId = CAMGChunk::ModeIds(); - if (camg) { - modeId = camg->modeId(); - } + auto modeId = BODYChunk::safeModeId(header, camg, cmap); QByteArray ba; switch (bitplanes) { - case 1: // bitmap - ba = QByteArray((7 + header->width() * bitplanes) / 8, char()); - for (qint32 i = 0, n = std::min(planes.size(), ba.size()); i < n; ++i) { - ba[i] = ~planes.at(i); - } - break; - - case 2: // gray, indexed and rgb Ham mode + case 1: // gray, indexed and rgb Ham mode + case 2: case 3: case 4: case 5: @@ -741,14 +824,16 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he } } } else if ((modeId & CAMGChunk::ModeId::HalfBrite) && (cmap)) { + // From A Quick Introduction to IFF.txt: + // // In HALFBRITE mode, the Amiga interprets the bit in the // last plane as HALFBRITE modification. The bits in the other planes are // treated as normal color register numbers (RGB values for each color register // is specified in the CMAP chunk). If the bit in the last plane is set (1), // then that pixel is displayed at half brightness. This can provide up to 64 // absolute colors. - ba = QByteArray(rowLen * 8 * 3, char()); - auto pal = cmap->palette(); + ba = QByteArray(rowLen * 8, char()); + auto palSize = cmap->count(); for (qint32 i = 0, cnt = 0; i < rowLen; ++i) { for (qint32 j = 0; j < 8; ++j, ++cnt) { quint8 idx = 0, ctl = 0; @@ -760,12 +845,8 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he else ctl = 1; } - if (idx < pal.size()) { - auto cnt3 = cnt * 3; - auto div = ctl ? 2 : 1; - ba[cnt3] = qRed(pal.at(idx)) / div; - ba[cnt3 + 1] = qGreen(pal.at(idx)) / div; - ba[cnt3 + 2] = qBlue(pal.at(idx)) / div; + if (idx < palSize) { + ba[cnt] = ctl ? idx + palSize : idx; } else { qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave: palette index" << idx << "is out of range"; } @@ -968,30 +1049,28 @@ QImage::Format FORMChunk::format() const if (auto &&h = headers.first()) { auto cmaps = IFFChunk::searchT(chunks()); - auto camgs = IFFChunk::searchT(chunks()); - - auto modeId = CAMGChunk::ModeIds(); - if (!camgs.isEmpty()) { - modeId = camgs.first()->modeId(); - } else if (h->bitplanes() == 6) { - // If no CAMG chunk is present, and image is 6 planes deep, - // assume HAM and you'll probably be right. - modeId = CAMGChunk::ModeIds(CAMGChunk::ModeId::Ham); + if (cmaps.isEmpty()) { + auto cmyks = IFFChunk::searchT(chunks()); + for (auto &&cmyk : cmyks) + cmaps.append(cmyk); } + auto camgs = IFFChunk::searchT(chunks()); + auto modeId = BODYChunk::safeModeId(h, camgs.isEmpty() ? nullptr : camgs.first(), cmaps.isEmpty() ? nullptr : cmaps.first()); if (h->bitplanes() == 24) { return QImage::Format_RGB888; } if (h->bitplanes() == 32) { return QImage::Format_RGBA8888; } - if (h->bitplanes() >= 2 && h->bitplanes() <= 8) { + if (h->bitplanes() >= 1 && h->bitplanes() <= 8) { if (!IFFChunk::search(SHAM_CHUNK, chunks()).isEmpty() || !IFFChunk::search(CTBL_CHUNK, chunks()).isEmpty()) { // Images with the SHAM or CTBL chunk do not load correctly: it seems they contains // a color table but I didn't find any specs. + qCDebug(LOG_IFFPLUGIN) << "FORMChunk::format(): SHAM/CTBL chunk is not supported"; return QImage::Format_Invalid; } - if (modeId & (CAMGChunk::ModeId::Ham | CAMGChunk::ModeId::HalfBrite)) { + if (modeId & CAMGChunk::ModeId::Ham) { return QImage::Format_RGB888; } @@ -1001,9 +1080,6 @@ QImage::Format FORMChunk::format() const return QImage::Format_Grayscale8; } - if (h->bitplanes() == 1) { - return QImage::Format_Mono; - } } return QImage::Format_Invalid; @@ -1168,10 +1244,10 @@ qint32 TBHDChunk::bpc() const qint32 TBHDChunk::channels() const { - if (flags() == TBHDChunk::Flag::RgbA) { + if ((flags() & TBHDChunk::Flag::RgbA) == TBHDChunk::Flag::RgbA) { return 4; } - if (flags() == TBHDChunk::Flag::Rgb) { + if ((flags() & TBHDChunk::Flag::Rgb) == TBHDChunk::Flag::Rgb) { return 3; } return 0; @@ -1188,12 +1264,12 @@ quint16 TBHDChunk::tiles() const QImage::Format TBHDChunk::format() const { // Support for RGBA and RGB only for now. - if (flags() == TBHDChunk::Flag::RgbA) { + if ((flags() & TBHDChunk::Flag::RgbA) == TBHDChunk::Flag::RgbA) { if (bpc() == 2) return QImage::Format_RGBA64; else if (bpc() == 1) return QImage::Format_RGBA8888; - } else if (flags() == TBHDChunk::Flag::Rgb) { + } else if ((flags() & TBHDChunk::Flag::Rgb) == TBHDChunk::Flag::Rgb) { if (bpc() == 2) return QImage::Format_RGBX64; else if (bpc() == 1) @@ -1516,12 +1592,12 @@ ANNOChunk::ANNOChunk() bool ANNOChunk::isValid() const { - return chunkId() == AUTHChunk::defaultChunkId(); + return chunkId() == ANNOChunk::defaultChunkId(); } QString ANNOChunk::value() const { - return QString::fromLatin1(data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed(); + return dataToString(this); } bool ANNOChunk::innerReadStructure(QIODevice *d) @@ -1550,7 +1626,7 @@ bool AUTHChunk::isValid() const QString AUTHChunk::value() const { - return QString::fromLatin1(data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed(); + return dataToString(this); } bool AUTHChunk::innerReadStructure(QIODevice *d) @@ -1580,7 +1656,7 @@ bool COPYChunk::isValid() const QString COPYChunk::value() const { - return QString::fromLatin1(data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed(); + return dataToString(this); } bool COPYChunk::innerReadStructure(QIODevice *d) @@ -1610,6 +1686,9 @@ bool DATEChunk::isValid() const QDateTime DATEChunk::value() const { + if (!isValid()) { + return {}; + } return QDateTime::fromString(QString::fromLatin1(data()), Qt::TextDate); } @@ -1643,6 +1722,9 @@ bool EXIFChunk::isValid() const MicroExif EXIFChunk::value() const { + if (!isValid()) { + return {}; + } return MicroExif::fromByteArray(data().mid(6)); } @@ -1652,6 +1734,36 @@ bool EXIFChunk::innerReadStructure(QIODevice *d) } +/* ****************** + * *** ICCN Chunk *** + * ****************** */ + +ICCNChunk::~ICCNChunk() +{ + +} + +ICCNChunk::ICCNChunk() +{ + +} + +bool ICCNChunk::isValid() const +{ + return chunkId() == ICCNChunk::defaultChunkId(); +} + +QString ICCNChunk::value() const +{ + return dataToString(this); +} + +bool ICCNChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + + /* ****************** * *** ICCP Chunk *** * ****************** */ @@ -1673,6 +1785,9 @@ bool ICCPChunk::isValid() const QColorSpace ICCPChunk::value() const { + if (!isValid()) { + return {}; + } return QColorSpace::fromIccProfile(data()); } @@ -1726,6 +1841,9 @@ bool HISTChunk::isValid() const QString HISTChunk::value() const { + if (!isValid()) { + return {}; + } return QString::fromLatin1(data()); } @@ -1756,7 +1874,7 @@ bool NAMEChunk::isValid() const QString NAMEChunk::value() const { - return QString::fromLatin1(data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed(); + return dataToString(this); } bool NAMEChunk::innerReadStructure(QIODevice *d) @@ -1786,6 +1904,9 @@ bool VERSChunk::isValid() const QString VERSChunk::value() const { + if (!isValid()) { + return {}; + } return QString::fromLatin1(data()); } @@ -1816,11 +1937,10 @@ bool XMP0Chunk::isValid() const QString XMP0Chunk::value() const { - return QString::fromUtf8(data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed(); + return dataToString(this); } bool XMP0Chunk::innerReadStructure(QIODevice *d) { return cacheData(d); } - diff --git a/src/imageformats/chunks_p.h b/src/imageformats/chunks_p.h index b58640f..9805c5a 100644 --- a/src/imageformats/chunks_p.h +++ b/src/imageformats/chunks_p.h @@ -49,6 +49,7 @@ #define BODY_CHUNK QByteArray("BODY") #define CAMG_CHUNK QByteArray("CAMG") #define CMAP_CHUNK QByteArray("CMAP") +#define CMYK_CHUNK QByteArray("CMYK") // https://wiki.amigaos.net/wiki/ILBM_IFF_Interleaved_Bitmap#ILBM.CMYK #define DPI__CHUNK QByteArray("DPI ") #define CTBL_CHUNK QByteArray("CTBL") // undocumented @@ -64,6 +65,7 @@ #define COPY_CHUNK QByteArray("(c) ") #define DATE_CHUNK QByteArray("DATE") #define EXIF_CHUNK QByteArray("EXIF") // https://aminet.net/package/docs/misc/IFF-metadata +#define ICCN_CHUNK QByteArray("ICCN") // https://aminet.net/package/docs/misc/IFF-metadata #define ICCP_CHUNK QByteArray("ICCP") // https://aminet.net/package/docs/misc/IFF-metadata #define FVER_CHUNK QByteArray("FVER") #define HIST_CHUNK QByteArray("HIST") @@ -467,14 +469,64 @@ public: virtual bool isValid() const override; - QList palette() const; + /*! + * \brief count + * \return The number of color in the palette. + */ + virtual qint32 count() const; + + /*! + * \brief palette + * \param halfbride When True, the new palette values are appended using the halfbride method. + * \return The color palette. + * \note If \a halfbride is true, the returned palette size is count() * 2. + */ + QList palette(bool halfbride = false) const; CHUNKID_DEFINE(CMAP_CHUNK) protected: + virtual QList innerPalette() const; + virtual bool innerReadStructure(QIODevice *d) override; }; +/*! + * \brief The CMYKChunk class + * + * This chunk would allow color specification in terms of Cyan, + * Magenta, Yellow, and Black as opposed to the current CMAP which uses RGB. + * The format would be the same as the CMAP chunk with the exception that this + * chunk uses four color components as opposed to three. The number of colors + * contained within would be chunk length/4. This chunk would be used in addition + * to the CMAP chunk. + */ +class CMYKChunk : public CMAPChunk +{ +public: + virtual ~CMYKChunk() override; + CMYKChunk(); + CMYKChunk(const CMYKChunk& other) = default; + CMYKChunk& operator =(const CMYKChunk& other) = default; + + virtual bool isValid() const override; + + /*! + * \brief count + * \return The number of color in the palette. + */ + virtual qint32 count() const override; + + CHUNKID_DEFINE(CMYK_CHUNK) + +protected: + /*! + * \brief palette + * \return The CMYK color palette converted to RGB one. + */ + virtual QList innerPalette() const override; +}; + /*! * \brief The CAMGChunk class */ @@ -589,6 +641,14 @@ public: */ virtual bool resetStrideRead(QIODevice *d) const; + /*! + * \brief safeModeId + * \param header The header. + * \param camg The CAMG chunk. + * \return The most likely ModeId if not explicitly specified. + */ + static CAMGChunk::ModeIds safeModeId(const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap = nullptr); + protected: /*! * \brief strideSize @@ -959,6 +1019,28 @@ protected: virtual bool innerReadStructure(QIODevice *d) override; }; + +/*! + * \brief The NAMEChunk class + */ +class ICCNChunk : public IFFChunk +{ +public: + virtual ~ICCNChunk() override; + ICCNChunk(); + ICCNChunk(const ICCNChunk& other) = default; + ICCNChunk& operator =(const ICCNChunk& other) = default; + + virtual bool isValid() const override; + + QString value() const; + + CHUNKID_DEFINE(ICCN_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + /*! * \brief The ICCPChunk class */ diff --git a/src/imageformats/iff.cpp b/src/imageformats/iff.cpp index d62dfe7..00772df 100644 --- a/src/imageformats/iff.cpp +++ b/src/imageformats/iff.cpp @@ -15,7 +15,7 @@ #include #ifdef QT_DEBUG -Q_LOGGING_CATEGORY(LOG_IFFPLUGIN, "kf.imageformats.plugins.iff", QtInfoMsg) +Q_LOGGING_CATEGORY(LOG_IFFPLUGIN, "kf.imageformats.plugins.iff", QtDebugMsg) #else Q_LOGGING_CATEGORY(LOG_IFFPLUGIN, "kf.imageformats.plugins.iff", QtWarningMsg) #endif @@ -184,6 +184,12 @@ void addMetadata(QImage& img, const IFFChunk *form) if (!iccps.isEmpty()) { auto cs = iccps.first()->value(); if (cs.isValid()) { + auto iccns = IFFChunk::searchT(form); + if (!iccns.isEmpty()) { + auto desc = iccns.first()->value(); + if (!desc.isEmpty()) + cs.setDescription(desc); + } img.setColorSpace(cs); } } @@ -223,13 +229,30 @@ bool IFFHandler::readStandardImage(QImage *image) } // set color table - auto cmaps = IFFChunk::searchT(form); - if (img.format() == QImage::Format_Indexed8) { - if (!cmaps.isEmpty()) - if (auto &&cmap = cmaps.first()) - img.setColorTable(cmap->palette()); + const CAMGChunk *camg = nullptr; + auto camgs = IFFChunk::searchT(form); + if (!camgs.isEmpty()) { + camg = camgs.first(); } + const CMAPChunk *cmap = nullptr; + auto cmaps = IFFChunk::searchT(form); + if (cmaps.isEmpty()) { + auto cmyks = IFFChunk::searchT(form); + for (auto &&cmyk : cmyks) + cmaps.append(cmyk); + } + if (!cmaps.isEmpty()) { + cmap = cmaps.first(); + } + if (img.format() == QImage::Format_Indexed8) { + if (cmap) { + auto halfbride = BODYChunk::safeModeId(header, camg, cmap) & CAMGChunk::ModeId::HalfBrite ? true : false; + img.setColorTable(cmap->palette(halfbride)); + } + } + + // reading image data auto bodies = IFFChunk::searchT(form); if (bodies.isEmpty()) { auto abits = IFFChunk::searchT(form); @@ -239,16 +262,6 @@ bool IFFHandler::readStandardImage(QImage *image) if (bodies.isEmpty()) { img.fill(0); } else { - const CAMGChunk *camg = nullptr; - auto camgs = IFFChunk::searchT(form); - if (!camgs.isEmpty()) { - camg = camgs.first(); - } - - const CMAPChunk *cmap = nullptr; - if (!cmaps.isEmpty()) - cmap = cmaps.first(); - auto &&body = bodies.first(); if (!body->resetStrideRead(device())) { qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image data";