diff --git a/autotests/read/iff/meta_rgba.iff b/autotests/read/iff/meta_rgba.iff new file mode 100644 index 0000000..c458503 Binary files /dev/null and b/autotests/read/iff/meta_rgba.iff differ diff --git a/autotests/read/iff/meta_rgba.iff.json b/autotests/read/iff/meta_rgba.iff.json new file mode 100644 index 0000000..c7eba5b --- /dev/null +++ b/autotests/read/iff/meta_rgba.iff.json @@ -0,0 +1,31 @@ +[ + { + "fileName" : "metadata.png", + "metadata" : [ + { + "key" : "Author", + "value" : "KDE Project" + }, + { + "key" : "Copyright", + "value" : "@2025 KDE Project" + }, + { + "key" : "CreationDate", + "value" : "2025-01-14T10:34:51" + }, + { + "key" : "Description", + "value" : "TV broadcast test image." + }, + { + "key" : "Title", + "value" : "Test Card" + } + ], + "resolution" : { + "dotsPerMeterX" : 2835, + "dotsPerMeterY" : 2835 + } + } +] diff --git a/autotests/read/iff/meta_rgba.png b/autotests/read/iff/meta_rgba.png new file mode 100644 index 0000000..23ad25a Binary files /dev/null and b/autotests/read/iff/meta_rgba.png differ diff --git a/autotests/read/iff/testcard_pbm.iff b/autotests/read/iff/testcard_pbm.iff new file mode 100644 index 0000000..89199f7 Binary files /dev/null and b/autotests/read/iff/testcard_pbm.iff differ diff --git a/autotests/read/iff/testcard_pbm.png b/autotests/read/iff/testcard_pbm.png new file mode 100644 index 0000000..e97488f Binary files /dev/null and b/autotests/read/iff/testcard_pbm.png differ diff --git a/autotests/read/iff/testcard_rlepbm.iff b/autotests/read/iff/testcard_rlepbm.iff new file mode 100644 index 0000000..9d62bd6 Binary files /dev/null and b/autotests/read/iff/testcard_rlepbm.iff differ diff --git a/autotests/read/iff/testcard_rlepbm.png b/autotests/read/iff/testcard_rlepbm.png new file mode 100644 index 0000000..626622c Binary files /dev/null and b/autotests/read/iff/testcard_rlepbm.png differ diff --git a/src/imageformats/CMakeLists.txt b/src/imageformats/CMakeLists.txt index b898aa9..2a29795 100644 --- a/src/imageformats/CMakeLists.txt +++ b/src/imageformats/CMakeLists.txt @@ -84,7 +84,7 @@ endif() ################################## -kimageformats_add_plugin(kimg_iff SOURCES iff.cpp chunks.cpp) +kimageformats_add_plugin(kimg_iff SOURCES iff.cpp chunks.cpp microexif.cpp) ################################## diff --git a/src/imageformats/chunks.cpp b/src/imageformats/chunks.cpp index 6344ca9..b4bb0a0 100644 --- a/src/imageformats/chunks.cpp +++ b/src/imageformats/chunks.cpp @@ -37,8 +37,19 @@ bool IFFChunk::operator ==(const IFFChunk &other) const bool IFFChunk::isValid() const { auto cid = chunkId(); + if (cid.isEmpty()) { + return false; + } + // A “type ID”, “property name”, “FORM type”, or any other IFF + // identifier is a 32-bit value: the concatenation of four ASCII + // characters in the range “ ” (SP, hex 20) through “~” (hex 7E). + // Spaces (hex 20) should not precede printing characters; + // trailing spaces are OK. Control characters are forbidden. + if (cid.at(0) == ' ') { + return false; + } for (auto &&c : cid) { - if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == ' '))) + if (c < ' ' || c > '~') return false; } return true; @@ -58,10 +69,7 @@ bool IFFChunk::readStructure(QIODevice *d) ok = ok && innerReadStructure(d); } if (ok) { - auto pos = _dataPos + _size; - if (auto align = pos % alignBytes()) - pos += alignBytes() - align; - ok = pos < d->pos() ? false : d->seek(pos); + ok = d->seek(nextChunkPos()); } return ok; } @@ -127,18 +135,21 @@ bool IFFChunk::readInfo(QIODevice *d) QByteArray IFFChunk::readRawData(QIODevice *d, qint64 relPos, qint64 size) const { - if (!seek(d, relPos)) + if (!seek(d, relPos)) { return{}; - if (size == -1) + } + if (size == -1) { size = _size; + } auto read = std::min(size, _size - relPos); return d->read(read); } bool IFFChunk::seek(QIODevice *d, qint64 relPos) const { - if (d == nullptr) + if (d == nullptr) { return false; + } return d->seek(_dataPos + relPos); } @@ -147,6 +158,19 @@ bool IFFChunk::innerReadStructure(QIODevice *) return true; } +void IFFChunk::setAlignBytes(qint32 bytes) +{ + _align = bytes; +} + +qint64 IFFChunk::nextChunkPos() const +{ + auto pos = _dataPos + _size; + if (auto align = pos % alignBytes()) + pos += alignBytes() - align; + return pos; +} + IFFChunk::ChunkList IFFChunk::search(const QByteArray &cid, const QSharedPointer &chunk) { return search(cid, ChunkList() << chunk); @@ -165,8 +189,9 @@ IFFChunk::ChunkList IFFChunk::search(const QByteArray &cid, const ChunkList &chu bool IFFChunk::cacheData(QIODevice *d) { - if (bytes() > 8 * 1024 * 1024) + if (bytes() > 8 * 1024 * 1024) { return false; + } _data = readRawData(d); return _data.size() == _size; } @@ -186,7 +211,7 @@ void IFFChunk::setRecursionCounter(qint32 cnt) _recursionCnt = cnt; } -IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, qint32 alignBytes, qint32 recursionCnt) +IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk *parent) { auto tmp = false; if (ok == nullptr) { @@ -198,42 +223,63 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, qint32 ali return {}; } + auto alignBytes = qint32(2); + auto recursionCnt = qint32(); + auto nextChunkPos = qint64(); + if (parent) { + alignBytes = parent->alignBytes(); + recursionCnt = parent->recursionCounter(); + nextChunkPos = parent->nextChunkPos(); + } + if (recursionCnt > RECURSION_PROTECTION) { return {}; } IFFChunk::ChunkList list; - for (; !d->atEnd();) { + for (; !d->atEnd() && (nextChunkPos == 0 || d->pos() < nextChunkPos);) { auto cid = d->peek(4); QSharedPointer chunk; - if (cid == FORM_CHUNK) { - chunk = QSharedPointer(new FORMChunk()); - } else if (cid == CAMG_CHUNK) { - chunk = QSharedPointer(new CAMGChunk()); - } else if (cid == CMAP_CHUNK) { - chunk = QSharedPointer(new CMAPChunk()); + if (cid == ANNO_CHUNK) { + chunk = QSharedPointer(new ANNOChunk()); + } else if (cid == AUTH_CHUNK) { + chunk = QSharedPointer(new AUTHChunk()); } else if (cid == BMHD_CHUNK) { chunk = QSharedPointer(new BMHDChunk()); } else if (cid == BODY_CHUNK) { chunk = QSharedPointer(new BODYChunk()); - } else if (cid == DPI__CHUNK) { - chunk = QSharedPointer(new DPIChunk()); - } else if (cid == FOR4_CHUNK) { - chunk = QSharedPointer(new FOR4Chunk()); - } else if (cid == TBHD_CHUNK) { - chunk = QSharedPointer(new TBHDChunk()); - } else if (cid == RGBA_CHUNK) { - chunk = QSharedPointer(new RGBAChunk()); - } else if (cid == AUTH_CHUNK) { - chunk = QSharedPointer(new AUTHChunk()); + } else if (cid == CAMG_CHUNK) { + chunk = QSharedPointer(new CAMGChunk()); + } else if (cid == CMAP_CHUNK) { + chunk = QSharedPointer(new CMAPChunk()); + } else if (cid == COPY_CHUNK) { + chunk = QSharedPointer(new COPYChunk()); } else if (cid == DATE_CHUNK) { chunk = QSharedPointer(new DATEChunk()); + } else if (cid == DPI__CHUNK) { + chunk = QSharedPointer(new DPIChunk()); + } else if (cid == EXIF_CHUNK) { + chunk = QSharedPointer(new EXIFChunk()); + } else if (cid == FOR4_CHUNK) { + chunk = QSharedPointer(new FOR4Chunk()); + } else if (cid == FORM_CHUNK) { + chunk = QSharedPointer(new FORMChunk()); } else if (cid == FVER_CHUNK) { chunk = QSharedPointer(new FVERChunk()); } else if (cid == HIST_CHUNK) { chunk = QSharedPointer(new HISTChunk()); + } else if (cid == ICCP_CHUNK) { + chunk = QSharedPointer(new ICCPChunk()); + } else if (cid == NAME_CHUNK) { + chunk = QSharedPointer(new NAMEChunk()); + } else if (cid == RGBA_CHUNK) { + chunk = QSharedPointer(new RGBAChunk()); + } else if (cid == TBHD_CHUNK) { + chunk = QSharedPointer(new TBHDChunk()); } else if (cid == VERS_CHUNK) { chunk = QSharedPointer(new VERSChunk()); + } else if (cid == XMP0_CHUNK) { + chunk = QSharedPointer(new XMP0Chunk()); } else { // unknown chunk chunk = QSharedPointer(new IFFChunk()); qInfo() << "IFFChunk::innerFromDevice: unkwnown chunk" << cid; @@ -256,6 +302,12 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, qint32 ali return {}; } + // skip any non-IFF data at the end of the file. + // NOTE: there should be no more chunks after the first (root) + if (nextChunkPos == 0) { + nextChunkPos = chunk->nextChunkPos(); + } + list << chunk; } @@ -265,7 +317,7 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, qint32 ali IFFChunk::ChunkList IFFChunk::fromDevice(QIODevice *d, bool *ok) { - return innerFromDevice(d, ok, 2, 0); + return innerFromDevice(d, ok, nullptr); } @@ -340,12 +392,12 @@ quint8 BMHDChunk::bitplanes() const return quint8(data().at(8)); } -quint8 BMHDChunk::masking() const +BMHDChunk::Masking BMHDChunk::masking() const { if (!isValid()) { - return 0; + return BMHDChunk::Masking::None; } - return quint8(data().at(9)); + return BMHDChunk::Masking(quint8(data().at(9))); } BMHDChunk::Compression BMHDChunk::compression() const @@ -357,14 +409,6 @@ BMHDChunk::Compression BMHDChunk::compression() const } -quint8 BMHDChunk::padding() const -{ - if (!isValid()) { - return 0; - } - return quint8(data().at(11)); -} - qint16 BMHDChunk::transparency() const { if (!isValid()) { @@ -547,13 +591,13 @@ bool BODYChunk::isValid() const return chunkId() == BODYChunk::defaultChunkId(); } -QByteArray BODYChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap) const +QByteArray BODYChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, bool isPbm) const { if (!isValid() || header == nullptr) { return {}; } - auto readSize = header->rowLen() * header->bitplanes(); + auto readSize = strideSize(header, isPbm); for(;!d->atEnd() && _readBuffer.size() < readSize;) { QByteArray buf(readSize, char()); qint64 rr = -1; @@ -562,7 +606,7 @@ QByteArray BODYChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CA // not accurate: the RLE -128 code is not a noop. rr = packbitsDecompress(d, buf.data(), buf.size(), true); } else if (header->compression() == BMHDChunk::Compression::Uncompressed) { - rr = d->read(buf.data(), buf.size()); + rr = d->read(buf.data(), buf.size()); // never seen } if (rr != readSize) return {}; @@ -571,7 +615,7 @@ QByteArray BODYChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CA auto planes = _readBuffer.left(readSize); _readBuffer.remove(0, readSize); - return BODYChunk::deinterleave(planes, header, camg, cmap); + return deinterleave(planes, header, camg, cmap, isPbm); } bool BODYChunk::resetStrideRead(QIODevice *d) const @@ -580,14 +624,31 @@ bool BODYChunk::resetStrideRead(QIODevice *d) const return seek(d); } -QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap) +quint32 BODYChunk::strideSize(const BMHDChunk *header, bool isPbm) const { - auto rowLen = qint32(header->rowLen()); - auto bitplanes = header->bitplanes(); - if (planes.size() != rowLen * bitplanes) { + auto rs = header->rowLen() * header->bitplanes(); + if (!isPbm) { + return rs; + } + + // 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; + } + + return rs; +} + +QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, bool isPbm) const +{ + if (planes.size() != strideSize(header, isPbm)) { return {}; } + auto rowLen = qint32(header->rowLen()); + auto bitplanes = header->bitplanes(); auto modeId = CAMGChunk::ModeIds(); if (camg) { modeId = camg->modeId(); @@ -609,7 +670,10 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he case 6: case 7: case 8: - if (modeId == CAMGChunk::ModeId::Ham && cmap && bitplanes == 6) { + if (isPbm && bitplanes == 8) { + // The data are contiguous. + ba = planes; + } else if ((modeId & CAMGChunk::ModeId::Ham) && (cmap) && (bitplanes >= 5 && bitplanes <= 8)) { // From A Quick Introduction to IFF.txt: // // Amiga HAM (Hold and Modify) mode lets the Amiga display all 4096 RGB values. @@ -631,6 +695,7 @@ 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(); + auto max = (1 << (bitplanes - 2)) - 1; quint8 prev[3] = {}; for (qint32 i = 0, cnt = 0; i < rowLen; ++i) { for (qint32 j = 0; j < 8; ++j, ++cnt) { @@ -638,21 +703,20 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he for (qint32 k = 0, msk = (1 << (7 - j)); k < bitplanes; ++k) { if ((planes.at(k * rowLen + i) & msk) == 0) continue; - if (k < 4) { + if (k < bitplanes - 2) idx |= 1 << k; - } else { + else ctl |= 1 << (bitplanes - k - 1); - } } switch (ctl) { case 1: // red - prev[0] = idx | (idx << 4); + prev[0] = idx * 255 / max; break; case 2: // blue - prev[2] = idx | (idx << 4); + prev[2] = idx * 255 / max; break; case 3: // green - prev[1] = idx | (idx << 4); + prev[1] = idx * 255 / max; break; default: if (idx < pal.size()) { @@ -670,7 +734,38 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he ba[cnt3 + 2] = char(prev[2]); } } - } else if (modeId == CAMGChunk::ModeIds()) { + } else if ((modeId & CAMGChunk::ModeId::HalfBrite) && (cmap)) { + // 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(); + for (qint32 i = 0, cnt = 0; i < rowLen; ++i) { + for (qint32 j = 0; j < 8; ++j, ++cnt) { + quint8 idx = 0, ctl = 0; + for (qint32 k = 0, msk = (1 << (7 - j)); k < bitplanes; ++k) { + if ((planes.at(k * rowLen + i) & msk) == 0) + continue; + if (k < bitplanes - 1) + idx |= 1 << k; + 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; + } else { + qWarning() << "BODYChunk::deinterleave: palette index" << idx << "is out of range"; + } + } + } + } else { // From A Quick Introduction to IFF.txt: // // If the ILBM is not HAM or HALFBRITE, then after parsing and uncompacting if @@ -709,6 +804,12 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *he break; case 24: // rgb + case 32: // rgba + if (isPbm) { + // TODO: no testcase found + break; + } + // From A Quick Introduction to IFF.txt: // // If a deep ILBM (like 12 or 24 planes), there should be no CMAP @@ -775,8 +876,10 @@ bool FORMChunk::innerReadStructure(QIODevice *d) } _type = d->read(4); auto ok = true; - if (_type == QByteArray("ILBM")) { - setChunks(IFFChunk::innerFromDevice(d, &ok, alignBytes(), recursionCounter())); + if (_type == ILBM_FORM_TYPE) { + setChunks(IFFChunk::innerFromDevice(d, &ok, this)); + } else if (_type == PBM__FORM_TYPE) { + setChunks(IFFChunk::innerFromDevice(d, &ok, this)); } return ok; } @@ -805,25 +908,28 @@ QImage::Format FORMChunk::format() const // assume HAM and you'll probably be right. modeId = CAMGChunk::ModeIds(CAMGChunk::ModeId::Ham); } - if (h->bitplanes() == 24) { return QImage::Format_RGB888; } + if (h->bitplanes() == 32) { + return QImage::Format_RGBA8888; + } if (h->bitplanes() >= 2 && h->bitplanes() <= 8) { - // Currently supported modes: HAM6 and No HAM/HALFBRITE. - if (modeId != CAMGChunk::ModeIds() && (modeId != CAMGChunk::ModeId::Ham || h->bitplanes() != 6)) + 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. return QImage::Format_Invalid; - - if (modeId & CAMGChunk::ModeId::Ham) { - if (IFFChunk::search(SHAM_CHUNK, chunks()).isEmpty()) - return QImage::Format_RGB888; - else // Images with the SHAM chunk do not load correctly. - return QImage::Format_Invalid; - } else if (!cmaps.isEmpty()) { - return QImage::Format_Indexed8; - } else { - return QImage::Format_Grayscale8; } + + if (modeId & (CAMGChunk::ModeId::Ham | CAMGChunk::ModeId::HalfBrite)) { + return QImage::Format_RGB888; + } + + if (!cmaps.isEmpty()) { + return QImage::Format_Indexed8; + } + + return QImage::Format_Grayscale8; } if (h->bitplanes() == 1) { return QImage::Format_Mono; @@ -878,10 +984,10 @@ bool FOR4Chunk::innerReadStructure(QIODevice *d) } _type = d->read(4); auto ok = true; - if (_type == QByteArray("CIMG")) { - setChunks(IFFChunk::innerFromDevice(d, &ok, alignBytes(), recursionCounter())); - } else if (_type == QByteArray("TBMP")) { - setChunks(IFFChunk::innerFromDevice(d, &ok, alignBytes(), recursionCounter())); + if (_type == CIMG_FOR4_TYPE) { + setChunks(IFFChunk::innerFromDevice(d, &ok, this)); + } else if (_type == TBMP_FOR4_TYPE) { + setChunks(IFFChunk::innerFromDevice(d, &ok, this)); } return ok; } @@ -1076,16 +1182,20 @@ bool RGBAChunk::isTileCompressed(const TBHDChunk *header) const QPoint RGBAChunk::pos() const { - return _pos; + return _posPx; } QSize RGBAChunk::size() const { - return _size; + return _sizePx; } // Maya version of IFF uses a slightly different algorithm for RLE compression. -qint64 rleMayaDecompress(QIODevice *input, char *output, qint64 olen) +// To understand how it works I saved images with regular patterns from Photoshop +// and then checked the data. It is basically the same as packbits except for how +// the length is extracted: I don't know if it's a standard variant or not, so +// I'm keeping it private. +inline qint64 rleMayaDecompress(QIODevice *input, char *output, qint64 olen) { qint64 j = 0; for (qint64 rr = 0, available = olen; j < olen; available = olen - j) { @@ -1096,7 +1206,7 @@ qint64 rleMayaDecompress(QIODevice *input, char *output, qint64 olen) if (input->peek(&n, 1) != 1) { // end of data (or error) break; } - rr = qint64(n & 0x7f) + 1; + rr = qint64(n & 0x7F) + 1; if (rr > available) break; } @@ -1106,7 +1216,7 @@ qint64 rleMayaDecompress(QIODevice *input, char *output, qint64 olen) break; } - rr = qint64(n & 0x7f) + 1; + rr = qint64(n & 0x7F) + 1; if ((n & 0x80) == 0) { auto read = input->read(output + j, rr); if (rr != read) { @@ -1132,20 +1242,26 @@ QByteArray RGBAChunk::readStride(QIODevice *d, const TBHDChunk *header) const return {}; } - // detect if the tile is compressed (8 is the size of 4 uint16 before the tile data). - auto compressed = isTileCompressed(header); + // It seems that tiles are compressed independently only if there is space savings. + // The compression method specified in the header is only to indicate the type of + // compression if used. + if (!isTileCompressed(header)) { + // when not compressed, the line contains all channels + readSize *= header->bpc() * header->channels(); + QByteArray buf(readSize, char()); + auto rr = d->read(buf.data(), buf.size()); + if (rr != buf.size()) { + return {}; + } + return buf; + } + + // compressed for(;!d->atEnd() && _readBuffer.size() < readSize;) { QByteArray buf(readSize * size().height(), char()); qint64 rr = -1; - if (compressed) { - // It seems that tiles are compressed independently only if there is space savings. - // The compression method specified in the header is only to indicate the type of - // compression if used. - if (header->compression() == TBHDChunk::Compression::Rle) { - rr = rleMayaDecompress(d, buf.data(), buf.size()); - } - } else { - rr = d->read(buf.data(), buf.size()); + if (header->compression() == TBHDChunk::Compression::Rle) { + rr = rleMayaDecompress(d, buf.data(), buf.size()); } if (rr != buf.size()) { return {}; @@ -1159,6 +1275,18 @@ QByteArray RGBAChunk::readStride(QIODevice *d, const TBHDChunk *header) const return buff; } +/*! + * \brief compressedTile + * + * The compressed tile contains compressed data per channel. + * + * If 16 bit, high and low bytes are treated separately (so I have + * channels * 2 compressed data blocks). First the high ones, then the low + * ones (or vice versa): for the reconstruction I went by trial and error :) + * \param d The device + * \param header The header. + * \return The tile as Qt image. + */ QImage RGBAChunk::compressedTile(QIODevice *d, const TBHDChunk *header) const { QImage img(size(), header->format()); @@ -1184,7 +1312,7 @@ QImage RGBAChunk::compressedTile(QIODevice *d, const TBHDChunk *header) const } for (auto c = 0, cc = header->channels() * header->bpc(); c < cc; ++c) { #if Q_BYTE_ORDER == Q_BIG_ENDIAN - auto c_bcp = c / cs; + auto c_bcp = c / cs; // Not tried #else auto c_bcp = 1 - c / cs; #endif @@ -1205,6 +1333,15 @@ QImage RGBAChunk::compressedTile(QIODevice *d, const TBHDChunk *header) const return img; } +/*! + * \brief RGBAChunk::uncompressedTile + * + * The uncompressed tile scanline contains the data in + * B0 G0 R0 A0 B1 G1 R1 A1... Bn Gn Rn An format. + * \param d The device + * \param header The header. + * \return The tile as Qt image. + */ QImage RGBAChunk::uncompressedTile(QIODevice *d, const TBHDChunk *header) const { QImage img(size(), header->format()); @@ -1212,10 +1349,8 @@ QImage RGBAChunk::uncompressedTile(QIODevice *d, const TBHDChunk *header) const if (bpc == 1) { auto cs = header->channels(); - auto lineSize = img.width() * bpc * cs; - for (auto y = 0, h = img.height(); y < h; ++y) { - auto ba = d->read(lineSize); + auto ba = readStride(d, header); if (ba.isEmpty()) { return {}; } @@ -1229,13 +1364,12 @@ QImage RGBAChunk::uncompressedTile(QIODevice *d, const TBHDChunk *header) const } } else if (bpc == 2) { auto cs = header->channels(); - auto lineSize = img.width() * bpc * cs; if (cs < 4) { // alpha on 64-bit images must be 0xFF std::memset(img.bits(), 0xFF, img.sizeInBytes()); } for (auto y = 0, h = img.height(); y < h; ++y) { - auto ba = d->read(lineSize); + auto ba = readStride(d, header); if (ba.isEmpty()) { return {}; } @@ -1246,7 +1380,7 @@ QImage RGBAChunk::uncompressedTile(QIODevice *d, const TBHDChunk *header) const auto xcs = x * cs; auto xcs4 = x * 4; #if Q_BYTE_ORDER == Q_BIG_ENDIAN - scl[xcs4 + cs - c - 1] = src[xcs + c]; + scl[xcs4 + cs - c - 1] = src[xcs + c]; // Not tried #else scl[xcs4 + cs - c - 1] = (src[xcs + c] >> 8) | (src[xcs + c] << 8); #endif @@ -1289,12 +1423,42 @@ bool RGBAChunk::innerReadStructure(QIODevice *d) return false; } - _pos = QPoint(x0, y0); - _size = QSize(qint32(x1) - x0 + 1, qint32(y1) - y0 + 1); + _posPx = QPoint(x0, y0); + _sizePx = QSize(qint32(x1) - x0 + 1, qint32(y1) - y0 + 1); return true; } + +/* ****************** + * *** ANNO Chunk *** + * ****************** */ + +ANNOChunk::~ANNOChunk() +{ + +} + +ANNOChunk::ANNOChunk() +{ + +} + +bool ANNOChunk::isValid() const +{ + return chunkId() == AUTHChunk::defaultChunkId(); +} + +QString ANNOChunk::value() const +{ + return QString::fromLatin1(data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed(); +} + +bool ANNOChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + /* ****************** * *** AUTH Chunk *** * ****************** */ @@ -1316,7 +1480,7 @@ bool AUTHChunk::isValid() const QString AUTHChunk::value() const { - return QString::fromLatin1(data()); + return QString::fromLatin1(data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed(); } bool AUTHChunk::innerReadStructure(QIODevice *d) @@ -1324,6 +1488,37 @@ bool AUTHChunk::innerReadStructure(QIODevice *d) return cacheData(d); } + +/* ****************** + * *** COPY Chunk *** + * ****************** */ + +COPYChunk::~COPYChunk() +{ + +} + +COPYChunk::COPYChunk() +{ + +} + +bool COPYChunk::isValid() const +{ + return chunkId() == COPYChunk::defaultChunkId(); +} + +QString COPYChunk::value() const +{ + return QString::fromLatin1(data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed(); +} + +bool COPYChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + + /* ****************** * *** DATE Chunk *** * ****************** */ @@ -1353,6 +1548,69 @@ bool DATEChunk::innerReadStructure(QIODevice *d) return cacheData(d); } + +/* ****************** + * *** EXIF Chunk *** + * ****************** */ + +EXIFChunk::~EXIFChunk() +{ + +} + +EXIFChunk::EXIFChunk() +{ + +} + +bool EXIFChunk::isValid() const +{ + if (!data().startsWith(QByteArray("Exif\0\0"))) { + return false; + } + return chunkId() == EXIFChunk::defaultChunkId(); +} + +MicroExif EXIFChunk::value() const +{ + return MicroExif::fromByteArray(data().mid(6)); +} + +bool EXIFChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + + +/* ****************** + * *** ICCP Chunk *** + * ****************** */ + +ICCPChunk::~ICCPChunk() +{ + +} + +ICCPChunk::ICCPChunk() +{ + +} + +bool ICCPChunk::isValid() const +{ + return chunkId() == ICCPChunk::defaultChunkId(); +} + +QColorSpace ICCPChunk::value() const +{ + return QColorSpace::fromIccProfile(data()); +} + +bool ICCPChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + /* ****************** * *** FVER Chunk *** * ****************** */ @@ -1406,6 +1664,37 @@ bool HISTChunk::innerReadStructure(QIODevice *d) return cacheData(d); } + +/* ****************** + * *** NAME Chunk *** + * ****************** */ + +NAMEChunk::~NAMEChunk() +{ + +} + +NAMEChunk::NAMEChunk() +{ + +} + +bool NAMEChunk::isValid() const +{ + return chunkId() == NAMEChunk::defaultChunkId(); +} + +QString NAMEChunk::value() const +{ + return QString::fromLatin1(data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed(); +} + +bool NAMEChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + + /* ****************** * *** VERS Chunk *** * ****************** */ @@ -1434,3 +1723,34 @@ bool VERSChunk::innerReadStructure(QIODevice *d) { return cacheData(d); } + + +/* ****************** + * *** XMP0 Chunk *** + * ****************** */ + +XMP0Chunk::~XMP0Chunk() +{ + +} + +XMP0Chunk::XMP0Chunk() +{ + +} + +bool XMP0Chunk::isValid() const +{ + return chunkId() == XMP0Chunk::defaultChunkId(); +} + +QString XMP0Chunk::value() const +{ + return QString::fromUtf8(data()).replace(QStringLiteral("\0"), QStringLiteral(" ")).trimmed(); +} + +bool XMP0Chunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + diff --git a/src/imageformats/chunks_p.h b/src/imageformats/chunks_p.h index c77deaa..73b1aa1 100644 --- a/src/imageformats/chunks_p.h +++ b/src/imageformats/chunks_p.h @@ -9,6 +9,7 @@ * Format specifications: * - https://wiki.amigaos.net/wiki/IFF_FORM_and_Chunk_Registry * - https://www.fileformat.info/format/iff/egff.htm + * - https://download.autodesk.com/us/maya/2010help/index.html (Developer resources -> File formats -> Maya IFF) */ #ifndef KIMG_CHUNKS_P_H @@ -22,6 +23,8 @@ #include #include +#include "microexif_p.h" + // Main chunks (Standard) #define CAT__CHUNK QByteArray("CAT ") #define FILL_CHUNK QByteArray(" ") @@ -30,7 +33,15 @@ #define PROP_CHUNK QByteArray("PROP") // Main chuncks (Maya) +#define CAT4_CHUNK QByteArray("CAT4") // 4 byte alignment #define FOR4_CHUNK QByteArray("FOR4") +#define LIS4_CHUNK QByteArray("LIS4") +#define PRO4_CHUNK QByteArray("PRO4") + +#define CAT8_CHUNK QByteArray("CAT8") // 8 byte alignment (never seen) +#define FOR8_CHUNK QByteArray("FOR8") +#define LIS8_CHUNK QByteArray("LIS8") +#define PRO8_CHUNK QByteArray("PRO8") // FORM ILBM IFF #define BMHD_CHUNK QByteArray("BMHD") @@ -38,6 +49,8 @@ #define CAMG_CHUNK QByteArray("CAMG") #define CMAP_CHUNK QByteArray("CMAP") #define DPI__CHUNK QByteArray("DPI ") + +#define CTBL_CHUNK QByteArray("CTBL") // undocumented #define SHAM_CHUNK QByteArray("SHAM") // undocumented // FOR4 CIMG IFF (Maya) @@ -45,11 +58,22 @@ #define TBHD_CHUNK QByteArray("TBHD") // FORx IFF (found on some IFF format specs) +#define ANNO_CHUNK QByteArray("ANNO") #define AUTH_CHUNK QByteArray("AUTH") +#define COPY_CHUNK QByteArray("(c) ") #define DATE_CHUNK QByteArray("DATE") +#define EXIF_CHUNK QByteArray("EXIF") // 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") +#define NAME_CHUNK QByteArray("NAME") #define VERS_CHUNK QByteArray("VERS") +#define XMP0_CHUNK QByteArray("XMP0") // https://aminet.net/package/docs/misc/IFF-metadata + +#define ILBM_FORM_TYPE QByteArray("ILBM") +#define PBM__FORM_TYPE QByteArray("PBM ") +#define CIMG_FOR4_TYPE QByteArray("CIMG") +#define TBMP_FOR4_TYPE QByteArray("TBMP") #define CHUNKID_DEFINE(a) static QByteArray defaultChunkId() { return a; } @@ -195,15 +219,18 @@ public: template static QList searchT(const IFFChunk *chunk) { QList list; - if (chunk == nullptr) + if (chunk == nullptr) { return list; + } auto cid = T::defaultChunkId(); - if (chunk->chunkId() == cid) + if (chunk->chunkId() == cid) { if (auto c = dynamic_cast(chunk)) list << c; + } auto tmp = chunk->chunks(); - for (auto &&c : tmp) + for (auto &&c : tmp) { list << searchT(c.data()); + } return list; } @@ -216,8 +243,9 @@ public: template static QList searchT(const ChunkList& chunks) { QList list; - for (auto &&chunk : chunks) + for (auto &&chunk : chunks) { list << searchT(chunk.data()); + } return list; } @@ -236,11 +264,14 @@ protected: * \brief setAlignBytes * \param bytes */ - void setAlignBytes(qint32 bytes) - { - _align = bytes; - } + void setAlignBytes(qint32 bytes); + /*! + * \brief nextChunkPos + * Calculates the position of the next chunk. The position is already aligned. + * \return The position of the next chunk from the beginning of the stream. + */ + qint64 nextChunkPos() const; /*! * \brief cacheData @@ -280,7 +311,7 @@ protected: return qint32(ui32(c1, c2, c3, c4)); } - static ChunkList innerFromDevice(QIODevice *d, bool *ok, qint32 alignBytes, qint32 recursionCnt); + static ChunkList innerFromDevice(QIODevice *d, bool *ok, IFFChunk *parent = nullptr); private: char _chunkId[4]; @@ -296,20 +327,31 @@ private: ChunkList _chunks; qint32 _recursionCnt; - - }; /*! - * \brief The IffBMHD class + * \brief The BMHDChunk class * Bitmap Header */ class BMHDChunk: public IFFChunk { public: enum Compression { - Uncompressed = 0, - Rle = 1 + Uncompressed = 0, /**< Image data are uncompressed. */ + Rle = 1 /**< Image data are RLE compressed. */ + }; + enum Masking { + None = 0, /**< Designates an opaque rectangular image. */ + HasMask = 1, /**< A mask plane is interleaved with the bitplanes in the BODY chunk. */ + 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 + by the source bitmap rather than the possibly deeper destination + bitmap. Note that having a transparent color implies ignoring + 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. */ }; virtual ~BMHDChunk() override; @@ -320,34 +362,88 @@ public: 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 left + * \return The left position of the image. + */ qint32 left() const; + /*! + * \brief top + * \return The top position of the image. + */ qint32 top() const; + /*! + * \brief bitplanes + * \return The number of bit planes. + */ quint8 bitplanes() const; - quint8 masking() const; + /*! + * \brief masking + * \return Kind of masking is to be used for this image. + */ + Masking masking() const; + /*! + * \brief compression + * \return The type of compression used. + */ Compression compression() const; - quint8 padding() const; - + /*! + * \brief transparency + * \return Transparent "color number". + */ qint16 transparency() const; + /*! + * \brief xAspectRatio + * \return X pixel aspect. + */ quint8 xAspectRatio() const; + /*! + * \brief yAspectRatio + * \return Y pixel aspect. + */ quint8 yAspectRatio() const; + /*! + * \brief pageWidth + * \return Source "page" width in pixels. + */ quint16 pageWidth() const; + /*! + * \brief pageHeight + * \return Source "page" height in pixels. + */ quint16 pageHeight() const; + /*! + * \brief rowLen + * \return The row len of a plane. + */ quint32 rowLen() const; CHUNKID_DEFINE(BMHD_CHUNK) @@ -473,10 +569,11 @@ public: * \param header The bitmap header. * \param camg The CAMG chunk (optional) * \param cmap The CMAP chunk (optional) + * \param isPbm Set to true if the formType() == "PBM " * \return The scanline as requested for QImage. * \warning Call resetStrideRead() once before this one. */ - QByteArray strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr) const; + QByteArray strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr, bool isPbm = false) const; /*! * \brief resetStrideRead @@ -490,7 +587,14 @@ public: bool resetStrideRead(QIODevice *d) const; private: - static QByteArray deinterleave(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr); + /*! + * \brief strideSize + * \param isPbm Set true if the image is PBM. + * \return The size of data to have to decode an image row. + */ + quint32 strideSize(const BMHDChunk *header, bool isPbm) const; + + QByteArray deinterleave(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr, bool isPbm = false) const; mutable QByteArray _readBuffer; }; @@ -563,12 +667,11 @@ class TBHDChunk : public IFFChunk { public: enum Flag { - Rgb = 0x01, - Alpha = 0x02, - ZBuffer = 0x04, - Black = 0x10, + Rgb = 0x01, /**< RGB image */ + Alpha = 0x02, /**< Image contains alpha channel */ + ZBuffer = 0x04, /**< If the image has a z-buffer, it is described by ZBUF blocks with the same structure as the RGBA blocks, RLE encoded. */ - RgbA = Rgb | Alpha + RgbA = Rgb | Alpha /**< RGBA image */ }; Q_DECLARE_FLAGS(Flags, Flag) @@ -715,13 +818,33 @@ private: QByteArray readStride(QIODevice *d, const TBHDChunk *header) const; private: - QPoint _pos; + QPoint _posPx; - QSize _size; + QSize _sizePx; mutable QByteArray _readBuffer; }; +/*! + * \brief The ANNOChunk class + */ +class ANNOChunk : public IFFChunk +{ +public: + virtual ~ANNOChunk() override; + ANNOChunk(); + ANNOChunk(const ANNOChunk& other) = default; + ANNOChunk& operator =(const ANNOChunk& other) = default; + + virtual bool isValid() const override; + + QString value() const; + + CHUNKID_DEFINE(ANNO_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; /*! * \brief The AUTHChunk class @@ -744,6 +867,27 @@ protected: virtual bool innerReadStructure(QIODevice *d) override; }; +/*! + * \brief The COPYChunk class + */ +class COPYChunk : public IFFChunk +{ +public: + virtual ~COPYChunk() override; + COPYChunk(); + COPYChunk(const COPYChunk& other) = default; + COPYChunk& operator =(const COPYChunk& other) = default; + + virtual bool isValid() const override; + + QString value() const; + + CHUNKID_DEFINE(COPY_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + /*! * \brief The DATEChunk class */ @@ -765,6 +909,49 @@ protected: virtual bool innerReadStructure(QIODevice *d) override; }; +/*! + * \brief The EXIFChunk class + */ +class EXIFChunk : public IFFChunk +{ +public: + virtual ~EXIFChunk() override; + EXIFChunk(); + EXIFChunk(const EXIFChunk& other) = default; + EXIFChunk& operator =(const EXIFChunk& other) = default; + + virtual bool isValid() const override; + + MicroExif value() const; + + CHUNKID_DEFINE(EXIF_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + +/*! + * \brief The ICCPChunk class + */ +class ICCPChunk : public IFFChunk +{ +public: + virtual ~ICCPChunk() override; + ICCPChunk(); + ICCPChunk(const ICCPChunk& other) = default; + ICCPChunk& operator =(const ICCPChunk& other) = default; + + virtual bool isValid() const override; + + QColorSpace value() const; + + CHUNKID_DEFINE(ICCP_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + + /*! * \brief The FVERChunk class * @@ -809,6 +996,27 @@ protected: }; +/*! + * \brief The NAMEChunk class + */ +class NAMEChunk : public IFFChunk +{ +public: + virtual ~NAMEChunk() override; + NAMEChunk(); + NAMEChunk(const NAMEChunk& other) = default; + NAMEChunk& operator =(const NAMEChunk& other) = default; + + virtual bool isValid() const override; + + QString value() const; + + CHUNKID_DEFINE(NAME_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + /*! * \brief The VERSChunk class */ @@ -830,4 +1038,26 @@ protected: virtual bool innerReadStructure(QIODevice *d) override; }; + +/*! + * \brief The XMP0Chunk class + */ +class XMP0Chunk : public IFFChunk +{ +public: + virtual ~XMP0Chunk() override; + XMP0Chunk(); + XMP0Chunk(const XMP0Chunk& other) = default; + XMP0Chunk& operator =(const XMP0Chunk& other) = default; + + virtual bool isValid() const override; + + QString value() const; + + CHUNKID_DEFINE(XMP0_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 2ffed32..ff6186d 100644 --- a/src/imageformats/iff.cpp +++ b/src/imageformats/iff.cpp @@ -107,11 +107,12 @@ bool IFFHandler::canRead(QIODevice *device) void addMetadata(QImage& img, const IFFChunk *form) { - auto dates = IFFChunk::searchT(form); - if (!dates.isEmpty()) { - auto dt = dates.first()->value(); - if (dt.isValid()) { - img.setText(QStringLiteral(META_KEY_CREATIONDATE), dt.toString(Qt::ISODate)); + // standard IFF metadata + auto annos = IFFChunk::searchT(form); + if (!annos.isEmpty()) { + auto anno = annos.first()->value(); + if (!anno.isEmpty()) { + img.setText(QStringLiteral(META_KEY_DESCRIPTION), anno); } } auto auths = IFFChunk::searchT(form); @@ -121,6 +122,29 @@ void addMetadata(QImage& img, const IFFChunk *form) img.setText(QStringLiteral(META_KEY_AUTHOR), auth); } } + auto dates = IFFChunk::searchT(form); + if (!dates.isEmpty()) { + auto dt = dates.first()->value(); + if (dt.isValid()) { + img.setText(QStringLiteral(META_KEY_CREATIONDATE), dt.toString(Qt::ISODate)); + } + } + auto copys = IFFChunk::searchT(form); + if (!copys.isEmpty()) { + auto cp = copys.first()->value(); + if (!cp.isEmpty()) { + img.setText(QStringLiteral(META_KEY_COPYRIGHT), cp); + } + } + auto names = IFFChunk::searchT(form); + if (!names.isEmpty()) { + auto name = names.first()->value(); + if (!name.isEmpty()) { + img.setText(QStringLiteral(META_KEY_TITLE), name); + } + } + + // software info auto vers = IFFChunk::searchT(form); if (!vers.isEmpty()) { auto ver = vers.first()->value(); @@ -128,6 +152,40 @@ void addMetadata(QImage& img, const IFFChunk *form) img.setText(QStringLiteral(META_KEY_SOFTWARE), ver); } } + + // SView5 metadata + auto exifs = IFFChunk::searchT(form); + if (!exifs.isEmpty()) { + auto exif = exifs.first()->value(); + exif.updateImageMetadata(img, false); + exif.updateImageResolution(img); + } + + auto xmp0s = IFFChunk::searchT(form); + if (!xmp0s.isEmpty()) { + auto xmp = xmp0s.first()->value(); + if (!xmp.isEmpty()) { + img.setText(QStringLiteral(META_KEY_XMP_ADOBE), xmp); + } + } + + auto iccps = IFFChunk::searchT(form); + if (!iccps.isEmpty()) { + auto cs = iccps.first()->value(); + if (cs.isValid()) { + img.setColorSpace(cs); + } + } + + // resolution -> leave after set of EXIF chunk + auto dpis = IFFChunk::searchT(form); + if (!dpis.isEmpty()) { + auto &&dpi = dpis.first(); + if (dpi->isValid()) { + img.setDotsPerMeterX(dpi->dotsPerMeterX()); + img.setDotsPerMeterY(dpi->dotsPerMeterY()); + } + } } bool IFFHandler::readStandardImage(QImage *image) @@ -153,16 +211,6 @@ bool IFFHandler::readStandardImage(QImage *image) return false; } - // resolution - auto dpis = IFFChunk::searchT(form); - if (!dpis.isEmpty()) { - auto &&dpi = dpis.first(); - if (dpi->isValid()) { - img.setDotsPerMeterX(dpi->dotsPerMeterX()); - img.setDotsPerMeterY(dpi->dotsPerMeterY()); - } - } - // set color table auto cmaps = IFFChunk::searchT(form); if (img.format() == QImage::Format_Indexed8) { @@ -190,9 +238,10 @@ bool IFFHandler::readStandardImage(QImage *image) qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image data"; return false; } + auto isPbm = form->formType() == PBM__FORM_TYPE; 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); + auto ba = body->strideRead(device(), header, camg, cmap, isPbm); if (ba.isEmpty()) { qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image scanline"; return false; @@ -201,6 +250,7 @@ bool IFFHandler::readStandardImage(QImage *image) } } + // set metadata (including image resolution) addMetadata(img, form); *image = img;