diff --git a/README.md b/README.md index f0e6889..cb47e35 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ The following image formats have read-only support: - Animated Windows cursors (ani) - Camera RAW images (arw, cr2, cr3, dcs, dng, ...) - Gimp (xcf) +- Interchange Format Files (IFF) - Krita (kra) - OpenRaster (ora) - Pixar raster (pxr) @@ -222,6 +223,7 @@ plugin ('n/a' means no limit, i.e. the limit depends on the format encoding). - EPS: n/a - HDR: n/a (large image) - HEIF: n/a +- IFF: 65,535 x 65,535 pixels - JP2: 300,000 x 300,000 pixels, in any case no larger than 2 gigapixels - JXL: 262,144 x 262,144 pixels, in any case no larger than 256 megapixels - JXR: n/a, in any case no larger than 4 GB diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index b4232bd..e871752 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -77,6 +77,7 @@ endmacro() # result against the data read from the corresponding png file kimageformats_read_tests( hdr + iff pcx pfm psd diff --git a/autotests/read/iff/blue_noise_rgba16.iff b/autotests/read/iff/blue_noise_rgba16.iff new file mode 100644 index 0000000..edc1c35 Binary files /dev/null and b/autotests/read/iff/blue_noise_rgba16.iff differ diff --git a/autotests/read/iff/blue_noise_rgba16.png b/autotests/read/iff/blue_noise_rgba16.png new file mode 100644 index 0000000..d47e895 Binary files /dev/null and b/autotests/read/iff/blue_noise_rgba16.png differ diff --git a/autotests/read/iff/blue_noise_rgba8.iff b/autotests/read/iff/blue_noise_rgba8.iff new file mode 100644 index 0000000..cd3aa12 Binary files /dev/null and b/autotests/read/iff/blue_noise_rgba8.iff differ diff --git a/autotests/read/iff/blue_noise_rgba8.png b/autotests/read/iff/blue_noise_rgba8.png new file mode 100644 index 0000000..d94382c Binary files /dev/null and b/autotests/read/iff/blue_noise_rgba8.png differ diff --git a/autotests/read/iff/flag_ham6.iff b/autotests/read/iff/flag_ham6.iff new file mode 100644 index 0000000..7898c8d Binary files /dev/null and b/autotests/read/iff/flag_ham6.iff differ diff --git a/autotests/read/iff/flag_ham6.png b/autotests/read/iff/flag_ham6.png new file mode 100644 index 0000000..aa0f42b Binary files /dev/null and b/autotests/read/iff/flag_ham6.png differ diff --git a/autotests/read/iff/ps_testcard_bitmap_amiga.iff b/autotests/read/iff/ps_testcard_bitmap_amiga.iff new file mode 100644 index 0000000..c8d60da Binary files /dev/null and b/autotests/read/iff/ps_testcard_bitmap_amiga.iff differ diff --git a/autotests/read/iff/ps_testcard_bitmap_amiga.png b/autotests/read/iff/ps_testcard_bitmap_amiga.png new file mode 100644 index 0000000..b50e6b2 Binary files /dev/null and b/autotests/read/iff/ps_testcard_bitmap_amiga.png differ diff --git a/autotests/read/iff/ps_testcard_gray_amiga.iff b/autotests/read/iff/ps_testcard_gray_amiga.iff new file mode 100644 index 0000000..6754aca Binary files /dev/null and b/autotests/read/iff/ps_testcard_gray_amiga.iff differ diff --git a/autotests/read/iff/ps_testcard_gray_amiga.png b/autotests/read/iff/ps_testcard_gray_amiga.png new file mode 100644 index 0000000..e85c25b Binary files /dev/null and b/autotests/read/iff/ps_testcard_gray_amiga.png differ diff --git a/autotests/read/iff/ps_testcard_indexed_amiga.iff b/autotests/read/iff/ps_testcard_indexed_amiga.iff new file mode 100644 index 0000000..8edbe69 Binary files /dev/null and b/autotests/read/iff/ps_testcard_indexed_amiga.iff differ diff --git a/autotests/read/iff/ps_testcard_indexed_amiga.png b/autotests/read/iff/ps_testcard_indexed_amiga.png new file mode 100644 index 0000000..963018c Binary files /dev/null and b/autotests/read/iff/ps_testcard_indexed_amiga.png differ diff --git a/autotests/read/iff/ps_testcard_rgb16_maya.iff b/autotests/read/iff/ps_testcard_rgb16_maya.iff new file mode 100644 index 0000000..8898672 Binary files /dev/null and b/autotests/read/iff/ps_testcard_rgb16_maya.iff differ diff --git a/autotests/read/iff/ps_testcard_rgb16_maya.png b/autotests/read/iff/ps_testcard_rgb16_maya.png new file mode 100644 index 0000000..1f83dec Binary files /dev/null and b/autotests/read/iff/ps_testcard_rgb16_maya.png differ diff --git a/autotests/read/iff/ps_testcard_rgb_amiga.iff b/autotests/read/iff/ps_testcard_rgb_amiga.iff new file mode 100644 index 0000000..ccc1b67 Binary files /dev/null and b/autotests/read/iff/ps_testcard_rgb_amiga.iff differ diff --git a/autotests/read/iff/ps_testcard_rgb_amiga.png b/autotests/read/iff/ps_testcard_rgb_amiga.png new file mode 100644 index 0000000..8d2bd1c Binary files /dev/null and b/autotests/read/iff/ps_testcard_rgb_amiga.png differ diff --git a/autotests/read/iff/ps_testcard_rgb_maya.iff b/autotests/read/iff/ps_testcard_rgb_maya.iff new file mode 100644 index 0000000..909670a Binary files /dev/null and b/autotests/read/iff/ps_testcard_rgb_maya.iff differ diff --git a/autotests/read/iff/ps_testcard_rgb_maya.png b/autotests/read/iff/ps_testcard_rgb_maya.png new file mode 100644 index 0000000..6e4ad51 Binary files /dev/null and b/autotests/read/iff/ps_testcard_rgb_maya.png differ diff --git a/autotests/read/iff/ps_testcard_rgba16_maya.iff b/autotests/read/iff/ps_testcard_rgba16_maya.iff new file mode 100644 index 0000000..c6956bb Binary files /dev/null and b/autotests/read/iff/ps_testcard_rgba16_maya.iff differ diff --git a/autotests/read/iff/ps_testcard_rgba16_maya.png b/autotests/read/iff/ps_testcard_rgba16_maya.png new file mode 100644 index 0000000..95ca29b Binary files /dev/null and b/autotests/read/iff/ps_testcard_rgba16_maya.png differ diff --git a/autotests/read/iff/ps_testcard_rgba_maya.iff b/autotests/read/iff/ps_testcard_rgba_maya.iff new file mode 100644 index 0000000..ff88265 Binary files /dev/null and b/autotests/read/iff/ps_testcard_rgba_maya.iff differ diff --git a/autotests/read/iff/ps_testcard_rgba_maya.png b/autotests/read/iff/ps_testcard_rgba_maya.png new file mode 100644 index 0000000..1da0760 Binary files /dev/null and b/autotests/read/iff/ps_testcard_rgba_maya.png differ diff --git a/autotests/read/iff/testcard_indexed2_amiga.iff b/autotests/read/iff/testcard_indexed2_amiga.iff new file mode 100644 index 0000000..d687b72 Binary files /dev/null and b/autotests/read/iff/testcard_indexed2_amiga.iff differ diff --git a/autotests/read/iff/testcard_indexed2_amiga.png b/autotests/read/iff/testcard_indexed2_amiga.png new file mode 100644 index 0000000..22942ae Binary files /dev/null and b/autotests/read/iff/testcard_indexed2_amiga.png differ diff --git a/autotests/read/iff/testcard_indexed3_amiga.iff b/autotests/read/iff/testcard_indexed3_amiga.iff new file mode 100644 index 0000000..4b44c3c Binary files /dev/null and b/autotests/read/iff/testcard_indexed3_amiga.iff differ diff --git a/autotests/read/iff/testcard_indexed3_amiga.png b/autotests/read/iff/testcard_indexed3_amiga.png new file mode 100644 index 0000000..3bf89c5 Binary files /dev/null and b/autotests/read/iff/testcard_indexed3_amiga.png differ diff --git a/autotests/read/iff/testcard_indexed4_amiga.iff b/autotests/read/iff/testcard_indexed4_amiga.iff new file mode 100644 index 0000000..c629004 Binary files /dev/null and b/autotests/read/iff/testcard_indexed4_amiga.iff differ diff --git a/autotests/read/iff/testcard_indexed4_amiga.png b/autotests/read/iff/testcard_indexed4_amiga.png new file mode 100644 index 0000000..55ee9ec Binary files /dev/null and b/autotests/read/iff/testcard_indexed4_amiga.png differ diff --git a/src/imageformats/CMakeLists.txt b/src/imageformats/CMakeLists.txt index 3d1b0e7..b898aa9 100644 --- a/src/imageformats/CMakeLists.txt +++ b/src/imageformats/CMakeLists.txt @@ -84,6 +84,10 @@ endif() ################################## +kimageformats_add_plugin(kimg_iff SOURCES iff.cpp chunks.cpp) + +################################## + if (LibJXL_FOUND AND LibJXLThreads_FOUND AND LibJXLCMS_FOUND) kimageformats_add_plugin(kimg_jxl SOURCES jxl.cpp microexif.cpp) target_link_libraries(kimg_jxl PRIVATE PkgConfig::LibJXL PkgConfig::LibJXLThreads PkgConfig::LibJXLCMS) diff --git a/src/imageformats/chunks.cpp b/src/imageformats/chunks.cpp new file mode 100644 index 0000000..4446fe6 --- /dev/null +++ b/src/imageformats/chunks.cpp @@ -0,0 +1,1414 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2025 Mirco Miranda + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "chunks_p.h" +#include "packbits_p.h" + +#include + +IFFChunk::~IFFChunk() +{ + +} + +IFFChunk::IFFChunk() + : _chunkId{0} + , _size{0} + , _align{2} + , _dataPos{0} +{ +} + +bool IFFChunk::operator ==(const IFFChunk &other) const +{ + if (chunkId() != other.chunkId()) { + return false; + } + return _size == other._size && _dataPos == other._dataPos; +} + +bool IFFChunk::isValid() const +{ + auto cid = chunkId(); + for (auto &&c : cid) { + if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == ' '))) + return false; + } + return true; +} + +qint32 IFFChunk::alignBytes() const +{ + return _align; +} + +bool IFFChunk::readStructure(QIODevice *d) +{ + auto ok = readInfo(d); + ok = ok && innerReadStructure(d); + if (ok) { + auto pos = _dataPos + _size; + if (auto align = pos % alignBytes()) + pos += alignBytes() - align; + ok = d->seek(pos); + } + return ok; +} + +QByteArray IFFChunk::chunkId() const +{ + return QByteArray(_chunkId, 4); +} + +quint32 IFFChunk::bytes() const +{ + return _size; +} + +const QByteArray &IFFChunk::data() const +{ + return _data; +} + +const IFFChunk::ChunkList &IFFChunk::chunks() const +{ + return _chunks; +} + +quint8 IFFChunk::chunkVersion(const QByteArray &cid) +{ + if (cid.size() != 4) { + return 0; + } + if (cid.at(3) >= char('2') && cid.at(3) <= char('9')) { + return quint8(cid.at(3) - char('0')); + } + return 1; +} + +bool IFFChunk::isChunkType(const QByteArray &cid) const +{ + if (chunkId() == cid) { + return true; + } + if (chunkId().startsWith(cid.left(3)) && IFFChunk::chunkVersion(cid) > 1) { + return true; + } + return false; +} + +bool IFFChunk::readInfo(QIODevice *d) +{ + if (d == nullptr || d->read(_chunkId, 4) != 4) { + return false; + } + if (!IFFChunk::isValid()) { + return false; + } + auto sz = d->read(4); + if (sz.size() != 4) { + return false; + } + _size = ui32(sz.at(3), sz.at(2), sz.at(1), sz.at(0)); + _dataPos = d->pos(); + return true; +} + +QByteArray IFFChunk::readRawData(QIODevice *d, qint64 relPos, qint64 size) const +{ + if (!seek(d, relPos)) + return{}; + 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) + return false; + return d->seek(_dataPos + relPos); +} + +bool IFFChunk::innerReadStructure(QIODevice *) +{ + return true; +} + +IFFChunk::ChunkList IFFChunk::search(const QByteArray &cid, const QSharedPointer &chunk) +{ + return search(cid, ChunkList() << chunk); +} + +IFFChunk::ChunkList IFFChunk::search(const QByteArray &cid, const ChunkList &chunks) +{ + IFFChunk::ChunkList list; + for (auto &&chunk : chunks) { + if (chunk->chunkId() == cid) + list << chunk; + list << IFFChunk::search(cid, chunk->_chunks); + } + return list; +} + +bool IFFChunk::cacheData(QIODevice *d) +{ + if (bytes() > 8 * 1024 * 1024) + return false; + _data = readRawData(d); + return _data.size() == _size; +} + +void IFFChunk::setChunks(const ChunkList &chunks) +{ + _chunks = chunks; +} + +IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, qint32 alignBytes) +{ + auto tmp = false; + if (ok == nullptr) { + ok = &tmp; + } + *ok = false; + + if (d == nullptr) { + return {}; + } + + IFFChunk::ChunkList list; + for (; !d->atEnd();) { + 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()); + } 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 == DATE_CHUNK) { + chunk = QSharedPointer(new DATEChunk()); + } else if (cid == FVER_CHUNK) { + chunk = QSharedPointer(new FVERChunk()); + } else if (cid == HIST_CHUNK) { + chunk = QSharedPointer(new HISTChunk()); + } else if (cid == VERS_CHUNK) { + chunk = QSharedPointer(new VERSChunk()); + } else { // unknown chunk + chunk = QSharedPointer(new IFFChunk()); + qInfo() << "IFFChunk::innerFromDevice: unkwnown chunk" << cid; + } + + // change the alignment to the one of main chunk (required for unknown Maya IFF chunks) + if (chunk->isChunkType(CAT__CHUNK) + || chunk->isChunkType(FILL_CHUNK) + || chunk->isChunkType(FORM_CHUNK) + || chunk->isChunkType(LIST_CHUNK) + || chunk->isChunkType(PROP_CHUNK)) { + alignBytes = chunk->alignBytes(); + } else { + chunk->setAlignBytes(alignBytes); + } + + if (!chunk->readStructure(d)) { + *ok = false; + return {}; + } + + list << chunk; + } + + *ok = true; + return list; +} + +IFFChunk::ChunkList IFFChunk::fromDevice(QIODevice *d, bool *ok) +{ + return innerFromDevice(d, ok, 2); +} + + +/* ****************** + * *** BMHD Chunk *** + * ****************** */ + +BMHDChunk::~BMHDChunk() +{ + +} + +BMHDChunk::BMHDChunk() : IFFChunk() +{ +} + +bool BMHDChunk::isValid() const +{ + if (bytes() < 20) { + return false; + } + return chunkId() == BMHDChunk::defaultChunkId(); +} + +bool BMHDChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + +qint32 BMHDChunk::width() const +{ + if (!isValid()) { + return 0; + } + return qint32(ui16(data().at(1), data().at(0))); +} + +qint32 BMHDChunk::height() const +{ + if (!isValid()) { + return 0; + } + return qint32(ui16(data().at(3), data().at(2))); +} + +QSize BMHDChunk::size() const +{ + return QSize(width(), height()); +} + +qint32 BMHDChunk::left() const +{ + if (!isValid()) { + return 0; + } + return qint32(ui16(data().at(5), data().at(4))); +} + +qint32 BMHDChunk::top() const +{ + if (!isValid()) { + return 0; + } + return qint32(ui16(data().at(7), data().at(6))); +} + +quint8 BMHDChunk::bitplanes() const +{ + if (!isValid()) { + return 0; + } + return quint8(data().at(8)); +} + +quint8 BMHDChunk::masking() const +{ + if (!isValid()) { + return 0; + } + return quint8(data().at(9)); +} + +BMHDChunk::Compression BMHDChunk::compression() const +{ + if (!isValid()) { + return BMHDChunk::Compression::Uncompressed; + } + return BMHDChunk::Compression(data().at(10)); + +} + +quint8 BMHDChunk::padding() const +{ + if (!isValid()) { + return 0; + } + return quint8(data().at(11)); +} + +qint16 BMHDChunk::transparency() const +{ + if (!isValid()) { + return 0; + } + return i16(data().at(13), data().at(12)); +} + +quint8 BMHDChunk::xAspectRatio() const +{ + if (!isValid()) { + return 0; + } + return quint8(data().at(14)); +} + +quint8 BMHDChunk::yAspectRatio() const +{ + if (!isValid()) { + return 0; + } + return quint8(data().at(15)); +} + +quint16 BMHDChunk::pageWidth() const +{ + if (!isValid()) { + return 0; + } + return ui16(data().at(17), data().at(16)); +} + +quint16 BMHDChunk::pageHeight() const +{ + if (!isValid()) { + return 0; + } + return ui16(data().at(19), data().at(18)); +} + +quint32 BMHDChunk::rowLen() const +{ + return ((quint32(width()) + 15) / 16) * 2; +} + +/* ****************** + * *** CMAP Chunk *** + * ****************** */ + +CMAPChunk::~CMAPChunk() +{ + +} + +CMAPChunk::CMAPChunk() : IFFChunk() +{ +} + +bool CMAPChunk::isValid() const +{ + return chunkId() == CMAPChunk::defaultChunkId(); +} + +bool CMAPChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + +QList CMAPChunk::palette() 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)); + } + return l; +} + +/* ****************** + * *** CAMG Chunk *** + * ****************** */ + +CAMGChunk::~CAMGChunk() +{ + +} + +CAMGChunk::CAMGChunk() : IFFChunk() +{ +} + +bool CAMGChunk::isValid() const +{ + if (bytes() != 4) { + return false; + } + return chunkId() == CAMGChunk::defaultChunkId(); +} + +CAMGChunk::ModeIds CAMGChunk::modeId() const +{ + if (!isValid()) { + return CAMGChunk::ModeIds(); + } + return CAMGChunk::ModeIds(ui32(data().at(3), data().at(2), data().at(1), data().at(0))); +} + +bool CAMGChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + +/* ****************** + * *** DPI Chunk *** + * ****************** */ + +DPIChunk::~DPIChunk() +{ + +} + +DPIChunk::DPIChunk() : IFFChunk() +{ +} + +bool DPIChunk::isValid() const +{ + if (dpiX() == 0 || dpiY() == 0) { + return false; + } + return chunkId() == DPIChunk::defaultChunkId(); +} + +quint16 DPIChunk::dpiX() const +{ + if (bytes() < 4) { + return 0; + } + return i16(data().at(1), data().at(0)); +} + +quint16 DPIChunk::dpiY() const +{ + if (bytes() < 4) { + return 0; + } + return i16(data().at(3), data().at(2)); +} + +qint32 DPIChunk::dotsPerMeterX() const +{ + return qRound(dpiX() / 25.4 * 1000); +} + +qint32 DPIChunk::dotsPerMeterY() const +{ + return qRound(dpiY() / 25.4 * 1000); +} + +bool DPIChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + +/* ****************** + * *** BODY Chunk *** + * ****************** */ + +BODYChunk::~BODYChunk() +{ + +} + +BODYChunk::BODYChunk() : IFFChunk() +{ +} + +bool BODYChunk::isValid() const +{ + return chunkId() == BODYChunk::defaultChunkId(); +} + +QByteArray BODYChunk::strideRead(QIODevice *d, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap) const +{ + if (!isValid() || header == nullptr) { + return {}; + } + + auto readSize = header->rowLen() * header->bitplanes(); + for(;!d->atEnd() && _readBuffer.size() < readSize;) { + QByteArray buf(readSize, char()); + qint64 rr = -1; + if (header->compression() == BMHDChunk::Compression::Rle) { + // WARNING: The online spec says it's the same as TIFF but that's + // 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()); + } + if (rr != readSize) + return {}; + _readBuffer.append(buf.data(), rr); + } + + auto planes = _readBuffer.left(readSize); + _readBuffer.remove(0, readSize); + return BODYChunk::deinterleave(planes, header, camg, cmap); +} + +bool BODYChunk::resetStrideRead(QIODevice *d) const +{ + _readBuffer.clear(); + return seek(d); +} + +QByteArray BODYChunk::deinterleave(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap) +{ + auto rowLen = qint32(header->rowLen()); + auto bitplanes = header->bitplanes(); + if (planes.size() != rowLen * bitplanes) { + return {}; + } + + auto modeId = CAMGChunk::ModeIds(); + if (camg) { + modeId = camg->modeId(); + } + + 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 3: + case 4: + case 5: + case 6: + case 7: + case 8: + if (modeId == CAMGChunk::ModeId::Ham && cmap && bitplanes == 6) { + // From A Quick Introduction to IFF.txt: + // + // Amiga HAM (Hold and Modify) mode lets the Amiga display all 4096 RGB values. + // In HAM mode, the bits in the two last planes describe an R G or B + // modification to the color of the previous pixel on the line to create the + // color of the current pixel. So a 6-plane HAM picture has 4 planes for + // specifying absolute color pixels giving up to 16 absolute colors which would + // be specified in the ILBM CMAP chunk. The bits in the last two planes are + // color modification bits which cause the Amiga, in HAM mode, to take the RGB + // value of the previous pixel (Hold and), substitute the 4 bits in planes 0-3 + // for the previous color's R G or B component (Modify) and display the result + // for the current pixel. If the first pixel of a scan line is a modification + // pixel, it modifies the RGB value of the border color (register 0). The color + // modification bits in the last two planes (planes 4 and 5) are interpreted as + // follows: + // 00 - no modification. Use planes 0-3 as normal color register index + // 10 - hold previous, replacing Blue component with bits from planes 0-3 + // 01 - hold previous, replacing Red component with bits from planes 0-3 + // 11 - hold previous. replacing Green component with bits from planes 0-3 + ba = QByteArray(rowLen * 8 * 3, char()); + auto pal = cmap->palette(); + quint8 prev[3] = {}; + 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 < 4) { + idx |= 1 << k; + } else { + ctl |= 1 << (bitplanes - k - 1); + } + } + switch (ctl) { + case 1: // red + prev[0] = idx | (idx << 4); + break; + case 2: // blue + prev[2] = idx | (idx << 4); + break; + case 3: // green + prev[1] = idx | (idx << 4); + break; + default: + if (idx < pal.size()) { + prev[0] = qRed(pal.at(idx)); + prev[1] = qGreen(pal.at(idx)); + prev[2] = qBlue(pal.at(idx)); + } else { + qWarning() << "BODYChunk::deinterleave: palette index" << idx << "is out of range"; + } + break; + } + auto cnt3 = cnt * 3; + ba[cnt3] = char(prev[0]); + ba[cnt3 + 1] = char(prev[1]); + ba[cnt3 + 2] = char(prev[2]); + } + } + } else if (modeId == CAMGChunk::ModeIds()) { + // From A Quick Introduction to IFF.txt: + // + // If the ILBM is not HAM or HALFBRITE, then after parsing and uncompacting if + // necessary, you will have N planes of pixel data. Color register used for + // each pixel is specified by looking at each pixel thru the planes. I.e., + // if you have 5 planes, and the bit for a particular pixel is set in planes + // 0 and 3: + // + // PLANE 4 3 2 1 0 + // PIXEL 0 1 0 0 1 + // + // then that pixel uses color register binary 01001 = 9 + ba = QByteArray(rowLen * 8, char()); + for (qint32 i = 0; i < rowLen; ++i) { + for (qint32 k = 0, i8 = i * 8; k < bitplanes; ++k) { + auto v = planes.at(k * rowLen + i); + if (v & (1 << 7)) + ba[i8] |= 1 << k; + if (v & (1 << 6)) + ba[i8 + 1] |= 1 << k; + if (v & (1 << 5)) + ba[i8 + 2] |= 1 << k; + if (v & (1 << 4)) + ba[i8 + 3] |= 1 << k; + if (v & (1 << 3)) + ba[i8 + 4] |= 1 << k; + if (v & (1 << 2)) + ba[i8 + 5] |= 1 << k; + if (v & (1 << 1)) + ba[i8 + 6] |= 1 << k; + if (v & 1) + ba[i8 + 7] |= 1 << k; + } + } + } + break; + + case 24: // rgb + // From A Quick Introduction to IFF.txt: + // + // If a deep ILBM (like 12 or 24 planes), there should be no CMAP + // and instead the BODY planes are interpreted as the bits of RGB + // in the order R0...Rn G0...Gn B0...Bn + // + // NOTE: This code does not support 12-planes images + ba = QByteArray(rowLen * bitplanes, char()); + for (qint32 i = 0, cnt = 0, p = bitplanes / 8; i < rowLen; ++i) { + for (qint32 j = 0; j < 8; ++j) + for (qint32 k = 0; k < p; ++k, ++cnt) { + auto k8 = k * 8; + auto msk = (1 << (7 - j)); + if (planes.at(k8 * rowLen + i) & msk) + ba[cnt] |= 0x01; + if (planes.at((1 + k8) * rowLen + i) & msk) + ba[cnt] |= 0x02; + if (planes.at((2 + k8) * rowLen + i) & msk) + ba[cnt] |= 0x04; + if (planes.at((3 + k8) * rowLen + i) & msk) + ba[cnt] |= 0x08; + if (planes.at((4 + k8) * rowLen + i) & msk) + ba[cnt] |= 0x10; + if (planes.at((5 + k8) * rowLen + i) & msk) + ba[cnt] |= 0x20; + if (planes.at((6 + k8) * rowLen + i) & msk) + ba[cnt] |= 0x40; + if (planes.at((7 + k8) * rowLen + i) & msk) + ba[cnt] |= 0x80; + } + } + break; + } + return ba; +} + +/* ****************** + * *** FORM Chunk *** + * ****************** */ + +FORMChunk::~FORMChunk() +{ + +} + +FORMChunk::FORMChunk() : IFFChunk() +{ +} + +bool FORMChunk::isValid() const +{ + return chunkId() == FORMChunk::defaultChunkId(); +} + +bool FORMChunk::isSupported() const +{ + return format() != QImage::Format_Invalid; +} + +bool FORMChunk::innerReadStructure(QIODevice *d) +{ + if (bytes() < 4) { + return false; + } + _type = d->read(4); + auto ok = true; + if (_type == QByteArray("ILBM")) { + setChunks(IFFChunk::innerFromDevice(d, &ok, alignBytes())); + } + return ok; +} + +QByteArray FORMChunk::formType() const +{ + return _type; +} + +QImage::Format FORMChunk::format() const +{ + auto headers = IFFChunk::searchT(chunks()); + if (headers.isEmpty()) { + return QImage::Format_Invalid; + } + + 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 (h->bitplanes() == 24) { + return QImage::Format_RGB888; + } + 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)) + 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 (h->bitplanes() == 1) { + return QImage::Format_Mono; + } + } + + return QImage::Format_Invalid; +} + +QSize FORMChunk::size() const +{ + auto headers = IFFChunk::searchT(chunks()); + if (headers.isEmpty()) { + return {}; + } + return headers.first()->size(); +} + +/* ****************** + * *** FOR4 Chunk *** + * ****************** */ + +FOR4Chunk::~FOR4Chunk() +{ + +} + +FOR4Chunk::FOR4Chunk() : IFFChunk() +{ + +} + +bool FOR4Chunk::isValid() const +{ + return chunkId() == FOR4Chunk::defaultChunkId(); +} + +qint32 FOR4Chunk::alignBytes() const +{ + return 4; +} + +bool FOR4Chunk::isSupported() const +{ + return format() != QImage::Format_Invalid; +} + +bool FOR4Chunk::innerReadStructure(QIODevice *d) +{ + if (bytes() < 4) { + return false; + } + _type = d->read(4); + auto ok = true; + if (_type == QByteArray("CIMG")) { + setChunks(IFFChunk::innerFromDevice(d, &ok, alignBytes())); + } else if (_type == QByteArray("TBMP")) { + setChunks(IFFChunk::innerFromDevice(d, &ok, alignBytes())); + } + return ok; +} + +QByteArray FOR4Chunk::formType() const +{ + return _type; +} + +QImage::Format FOR4Chunk::format() const +{ + auto headers = IFFChunk::searchT(chunks()); + if (headers.isEmpty()) { + return QImage::Format_Invalid; + } + return headers.first()->format(); +} + +QSize FOR4Chunk::size() const +{ + auto headers = IFFChunk::searchT(chunks()); + if (headers.isEmpty()) { + return {}; + } + return headers.first()->size(); +} + + +/* ****************** + * *** TBHD Chunk *** + * ****************** */ + +TBHDChunk::~TBHDChunk() +{ + +} + +TBHDChunk::TBHDChunk() +{ + +} + +bool TBHDChunk::isValid() const +{ + if (bytes() != 24 && bytes() != 32) { + return false; + } + return chunkId() == TBHDChunk::defaultChunkId(); +} + +qint32 TBHDChunk::alignBytes() const +{ + return 4; +} + +qint32 TBHDChunk::width() const +{ + if (!isValid()) { + return 0; + } + return i32(data().at(3), data().at(2), data().at(1), data().at(0)); +} + +qint32 TBHDChunk::height() const +{ + if (!isValid()) { + return 0; + } + return i32(data().at(7), data().at(6), data().at(5), data().at(4)); +} + +QSize TBHDChunk::size() const +{ + return QSize(width(), height()); +} + +qint32 TBHDChunk::left() const +{ + if (bytes() != 32) { + return 0; + } + return i32(data().at(27), data().at(26), data().at(25), data().at(24)); +} + +qint32 TBHDChunk::top() const +{ + if (bytes() != 32) { + return 0; + } + return i32(data().at(31), data().at(30), data().at(29), data().at(28)); +} + +TBHDChunk::Flags TBHDChunk::flags() const +{ + if (!isValid()) { + return TBHDChunk::Flags(); + } + return TBHDChunk::Flags(ui32(data().at(15), data().at(14), data().at(13), data().at(12))); +} + +qint32 TBHDChunk::bpc() const +{ + if (!isValid()) { + return 0; + } + return ui16(data().at(17), data().at(16)) ? 2 : 1; +} + +qint32 TBHDChunk::channels() const +{ + if (flags() == TBHDChunk::Flag::RgbA) { + return 4; + } + if (flags() == TBHDChunk::Flag::Rgb) { + return 3; + } + return 0; +} + +quint16 TBHDChunk::tiles() const +{ + if (!isValid()) { + return 0; + } + return ui16(data().at(19), data().at(18)); +} + +QImage::Format TBHDChunk::format() const +{ + // Support for RGBA and RGB only for now. + if (flags() == TBHDChunk::Flag::RgbA) { + if (bpc() == 2) + return QImage::Format_RGBA64; + else if (bpc() == 1) + return QImage::Format_RGBA8888; + } else if (flags() == TBHDChunk::Flag::Rgb) { + if (bpc() == 2) + return QImage::Format_RGBX64; + else if (bpc() == 1) + return QImage::Format_RGB888; + } + + return QImage::Format_Invalid; +} + +TBHDChunk::Compression TBHDChunk::compression() const +{ + if (!isValid()) { + return TBHDChunk::Compression::Uncompressed; + } + return TBHDChunk::Compression(ui32(data().at(23), data().at(22), data().at(21), data().at(20))); +} + +bool TBHDChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + +/* ****************** + * *** RGBA Chunk *** + * ****************** */ + +RGBAChunk::~RGBAChunk() +{ +} + +RGBAChunk::RGBAChunk() +{ + +} + +bool RGBAChunk::isValid() const +{ + if (bytes() < 8) { + return false; + } + return chunkId() == RGBAChunk::defaultChunkId(); +} + +qint32 RGBAChunk::alignBytes() const +{ + return 4; +} + +bool RGBAChunk::isTileCompressed(const TBHDChunk *header) const +{ + if (!isValid() || header == nullptr) { + return false; + } + return qint64(header->channels()) * size().width() * size().height() * header->bpc() > qint64(bytes() - 8); +} + +QPoint RGBAChunk::pos() const +{ + return _pos; +} + +QSize RGBAChunk::size() const +{ + return _size; +} + +// Maya version of IFF uses a slightly different algorithm for RLE compression. +qint64 rleMayaDecompress(QIODevice *input, char *output, qint64 olen) +{ + qint64 j = 0; + for (qint64 rr = 0, available = olen; j < olen; available = olen - j) { + char n; + + // check the output buffer space for the next run + if (available < 128) { + if (input->peek(&n, 1) != 1) { // end of data (or error) + break; + } + rr = qint64(n & 0x7f) + 1; + if (rr > available) + break; + } + + // decompress + if (input->read(&n, 1) != 1) { // end of data (or error) + break; + } + + rr = qint64(n & 0x7f) + 1; + if ((n & 0x80) == 0) { + auto read = input->read(output + j, rr); + if (rr != read) { + return -1; + } + } else { + char b; + if (input->read(&b, 1) != 1) { + break; + } + std::memset(output + j, b, size_t(rr)); + } + + j += rr; + } + return j; +} + +QByteArray RGBAChunk::readStride(QIODevice *d, const TBHDChunk *header) const +{ + auto readSize = size().width(); + if (readSize == 0) { + return {}; + } + + // detect if the tile is compressed (8 is the size of 4 uint16 before the tile data). + auto compressed = isTileCompressed(header); + 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 (rr != buf.size()) { + return {}; + } + _readBuffer.append(buf.data(), rr); + } + + auto buff = _readBuffer.left(readSize); + _readBuffer.remove(0, readSize); + + return buff; +} + +QImage RGBAChunk::compressedTile(QIODevice *d, const TBHDChunk *header) const +{ + QImage img(size(), header->format()); + auto bpc = header->bpc(); + + if (bpc == 1) { + for (auto c = 0, cs = header->channels(); c < cs; ++c) { + for (auto y = 0, h = img.height(); y < h; ++y) { + auto ba = readStride(d, header); + if (ba.isEmpty()) { + return {}; + } + auto scl = reinterpret_cast(img.scanLine(y)); + for (auto x = 0, w = std::min(int(ba.size()), img.width()); x < w; ++x) { + scl[x * cs + cs - c - 1] = ba.at(x); + } + } + } + } else if (bpc == 2) { + auto cs = header->channels(); + if (cs < 4) { // alpha on 64-bit images must be 0xFF + std::memset(img.bits(), 0xFF, img.sizeInBytes()); + } + for (auto c = 0, cc = header->channels() * header->bpc(); c < cc; ++c) { +#if Q_BYTE_ORDER == Q_BIG_ENDIAN + auto c_bcp = c / cs; +#else + auto c_bcp = 1 - c / cs; +#endif + auto c_cs = (cs - 1 - c % cs) * bpc + c_bcp; + for (auto y = 0, h = img.height(); y < h; ++y) { + auto ba = readStride(d, header); + if (ba.isEmpty()) { + return {}; + } + auto scl = reinterpret_cast(img.scanLine(y)); + for (auto x = 0, w = std::min(int(ba.size()), img.width()); x < w; ++x) { + scl[x * 4 * bpc + c_cs] = ba.at(x); // * 4 -> Qt RGB 64-bit formats are always 4 channels + } + } + } + } + + return img; +} + +QImage RGBAChunk::uncompressedTile(QIODevice *d, const TBHDChunk *header) const +{ + QImage img(size(), header->format()); + auto bpc = header->bpc(); + + 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); + if (ba.isEmpty()) { + return {}; + } + auto scl = reinterpret_cast(img.scanLine(y)); + for (auto c = 0; c < cs; ++c) { + for (auto x = 0, w = std::min(int(ba.size() / cs), img.width()); x < w; ++x) { + auto xcs = x * cs; + scl[xcs + cs - c - 1] = ba.at(xcs + c); + } + } + } + } 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); + if (ba.isEmpty()) { + return {}; + } + auto scl = reinterpret_cast(img.scanLine(y)); + auto src = reinterpret_cast(ba.data()); + for (auto c = 0; c < cs; ++c) { + for (auto x = 0, w = std::min(int(ba.size() / cs / bpc), img.width()); x < w; ++x) { + auto xcs = x * cs; + auto xcs4 = x * 4; +#if Q_BYTE_ORDER == Q_BIG_ENDIAN + scl[xcs4 + cs - c - 1] = src[xcs + c]; +#else + scl[xcs4 + cs - c - 1] = (src[xcs + c] >> 8) | (src[xcs + c] << 8); +#endif + } + } + } + } + + return img; +} + +QImage RGBAChunk::tile(QIODevice *d, const TBHDChunk *header) const +{ + if (!isValid() || header == nullptr) { + return {}; + } + if (!seek(d, 8)) { + return {}; + } + + if (isTileCompressed(header)) { + return compressedTile(d, header); + } + + return uncompressedTile(d, header); +} + +bool RGBAChunk::innerReadStructure(QIODevice *d) +{ + auto ba = d->read(8); + if (ba.size() != 8) { + return false; + } + + auto x0 = ui16(ba.at(1), ba.at(0)); + auto y0 = ui16(ba.at(3), ba.at(2)); + auto x1 = ui16(ba.at(5), ba.at(4)); + auto y1 = ui16(ba.at(7), ba.at(6)); + if (x0 > x1 || y0 > y1) { + return false; + } + + _pos = QPoint(x0, y0); + _size = QSize(qint32(x1) - x0 + 1, qint32(y1) - y0 + 1); + + return true; +} + +/* ****************** + * *** AUTH Chunk *** + * ****************** */ + +AUTHChunk::~AUTHChunk() +{ + +} + +AUTHChunk::AUTHChunk() +{ + +} + +bool AUTHChunk::isValid() const +{ + return chunkId() == AUTHChunk::defaultChunkId(); +} + +QString AUTHChunk::value() const +{ + return QString::fromLatin1(data()); +} + +bool AUTHChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + +/* ****************** + * *** DATE Chunk *** + * ****************** */ + +DATEChunk::~DATEChunk() +{ + +} + +DATEChunk::DATEChunk() +{ + +} + +bool DATEChunk::isValid() const +{ + return chunkId() == DATEChunk::defaultChunkId(); +} + +QDateTime DATEChunk::value() const +{ + return QDateTime::fromString(QString::fromLatin1(data()), Qt::TextDate); +} + +bool DATEChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + +/* ****************** + * *** FVER Chunk *** + * ****************** */ + +FVERChunk::~FVERChunk() +{ + +} + +FVERChunk::FVERChunk() +{ + +} + +bool FVERChunk::isValid() const +{ + return chunkId() == FVERChunk::defaultChunkId(); +} + +bool FVERChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + +/* ****************** + * *** HIST Chunk *** + * ****************** */ + +HISTChunk::~HISTChunk() +{ + +} + +HISTChunk::HISTChunk() +{ + +} + +bool HISTChunk::isValid() const +{ + return chunkId() == HISTChunk::defaultChunkId(); +} + +QString HISTChunk::value() const +{ + return QString::fromLatin1(data()); +} + +bool HISTChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} + +/* ****************** + * *** VERS Chunk *** + * ****************** */ + +VERSChunk::~VERSChunk() +{ + +} + +VERSChunk::VERSChunk() +{ + +} + +bool VERSChunk::isValid() const +{ + return chunkId() == VERSChunk::defaultChunkId(); +} + +QString VERSChunk::value() const +{ + return QString::fromLatin1(data()); +} + +bool VERSChunk::innerReadStructure(QIODevice *d) +{ + return cacheData(d); +} diff --git a/src/imageformats/chunks_p.h b/src/imageformats/chunks_p.h new file mode 100644 index 0000000..862b658 --- /dev/null +++ b/src/imageformats/chunks_p.h @@ -0,0 +1,823 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2025 Mirco Miranda + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +/* + * Format specifications: + * - https://wiki.amigaos.net/wiki/IFF_FORM_and_Chunk_Registry + * - https://www.fileformat.info/format/iff/egff.htm + */ + +#ifndef KIMG_CHUNKS_P_H +#define KIMG_CHUNKS_P_H + +#include +#include +#include +#include +#include +#include +#include + +// Main chunks (Standard) +#define CAT__CHUNK QByteArray("CAT ") +#define FILL_CHUNK QByteArray(" ") +#define FORM_CHUNK QByteArray("FORM") +#define LIST_CHUNK QByteArray("LIST") +#define PROP_CHUNK QByteArray("PROP") + +// Main chuncks (Maya) +#define FOR4_CHUNK QByteArray("FOR4") + +// FORM ILBM IFF +#define BMHD_CHUNK QByteArray("BMHD") +#define BODY_CHUNK QByteArray("BODY") +#define CAMG_CHUNK QByteArray("CAMG") +#define CMAP_CHUNK QByteArray("CMAP") +#define DPI__CHUNK QByteArray("DPI ") +#define SHAM_CHUNK QByteArray("SHAM") // undocumented + +// FOR4 CIMG IFF (Maya) +#define RGBA_CHUNK QByteArray("RGBA") +#define TBHD_CHUNK QByteArray("TBHD") + +// FORx IFF (found on some IFF format specs) +#define AUTH_CHUNK QByteArray("AUTH") +#define DATE_CHUNK QByteArray("DATE") +#define FVER_CHUNK QByteArray("FVER") +#define HIST_CHUNK QByteArray("HIST") +#define VERS_CHUNK QByteArray("VERS") + +#define CHUNKID_DEFINE(a) static QByteArray defaultChunkId() { return a; } + +/*! + * \brief The IFFChunk class + */ +class IFFChunk +{ +public: + using ChunkList = QList>; + + virtual ~IFFChunk(); + + /*! + * \brief IFFChunk + * Creates invalid chunk. + * \sa isValid + */ + IFFChunk(); + + IFFChunk(const IFFChunk& other) = default; + IFFChunk& operator =(const IFFChunk& other) = default; + + bool operator ==(const IFFChunk& other) const; + + /*! + * \brief isValid + * \return True if the chunk is valid, otherwise false. + * \note The default implementation checks that chunkId() contains only valid characters. + */ + virtual bool isValid() const; + + /*! + * \brief alignBytes + * \return The chunk alignment bytes. By default returns bytes set using setAlignBytes(). + */ + virtual qint32 alignBytes() const; + + /*! + * \brief chunkId + * \return The chunk Id of this chunk. + */ + QByteArray chunkId() const; + + /*! + * \brief bytes + * \return The size (in bytes) of the chunck data. + */ + + quint32 bytes() const; + + /*! + * \brief data + * \return The data stored inside the class. If no data present, use readRawData(). + * \sa readRawData + */ + const QByteArray& data() const; + + /*! + * \brief chunks + * \return The chunks inside this chunk. + */ + const ChunkList& chunks() const; + + /*! + * \brief chunkVersion + * \param cid Chunk Id to extract the version from. + * \return The version of the chunk. Zero means no valid chunk data. + */ + static quint8 chunkVersion(const QByteArray& cid); + + /*! + * \brief isChunkType + * Check if the chunkId is of type of cid (any version). + * \param cid Chunk Id to check. + * \return True on success, otherwise false. + */ + bool isChunkType(const QByteArray& cid) const; + + /*! + * \brief readInfo + * Reads chunkID, size and set the data position. + * \param d The device. + * \return True on success, otherwise false. + */ + bool readInfo(QIODevice *d); + + /*! + * \brief readStructure + * Read the internal structure using innerReadStructure() of the Chunk and set device the position to the next chunks. + * \param d The device. + * \return True on success, otherwise false. + */ + bool readStructure(QIODevice *d); + + /*! + * \brief readRawData + * \param d The device. + * \param relPos The position to read relative to the chunk position. + * \param size The size of the data to read (-1 means all chunk). + * \return The data read or empty array on error. + * \note Ignores any data already read and available with data(). + * \sa data + */ + QByteArray readRawData(QIODevice *d, qint64 relPos = 0, qint64 size = -1) const; + + /*! + * \brief seek + * \param d The device. + * \param relPos The position to read relative to the chunk position. + * \return True on success, otherwise false. + */ + bool seek(QIODevice *d, qint64 relPos = 0) const; + + /*! + * \brief fromDevice + * \param d The device. + * \param ok Set to false if errors occurred. + * \return The chunk list found. + */ + static ChunkList fromDevice(QIODevice *d, bool *ok = nullptr); + + /*! + * \brief search + * Search for a chunk in the list of chunks. + * \param cid The chunkId to search. + * \param chunks The list of chunks to search for the requested chunk. + * \return The list of chunks with the given chunkId. + */ + static ChunkList search(const QByteArray &cid, const ChunkList& chunks); + + /*! + * \brief search + */ + static ChunkList search(const QByteArray &cid, const QSharedPointer& chunk); + + /*! + * \brief searchT + * Convenient search function to avoid casts. + * \param chunk The chunk to search for the requested chunk type. + * \return The list of chunks of T type. + */ + template + static QList searchT(const IFFChunk *chunk) { + QList list; + if (chunk == nullptr) + return list; + auto cid = T::defaultChunkId(); + if (chunk->chunkId() == cid) + if (auto c = dynamic_cast(chunk)) + list << c; + auto tmp = chunk->chunks(); + for (auto &&c : tmp) + list << searchT(c.data()); + return list; + } + + /*! + * \brief searchT + * Convenient search function to avoid casts. + * \param chunks The list of chunks to search for the requested chunk. + * \return The list of chunks of T type. + */ + template + static QList searchT(const ChunkList& chunks) { + QList list; + for (auto &&chunk : chunks) + list << searchT(chunk.data()); + return list; + } + + CHUNKID_DEFINE(QByteArray()) + +protected: + /*! + * \brief innerReadStructure + * Reads data structure. Default implementation does nothing. + * \param d The device. + * \return True on success, otherwise false. + */ + virtual bool innerReadStructure(QIODevice *d); + + /*! + * \brief setAlignBytes + * \param bytes + */ + void setAlignBytes(qint32 bytes) + { + _align = bytes; + } + + + /*! + * \brief cacheData + * Read all chunk data and store it on _data. + * \return True on success, otherwise false. + * \warning This function does not load anything if the chunk size is larger than 8MiB. For larger chunks, use direct data access. + */ + bool cacheData(QIODevice *d); + + /*! + * \brief setChunks + * \param chunks + */ + void setChunks(const ChunkList &chunks); + + inline quint16 ui16(quint8 c1, quint8 c2) const { + return (quint16(c2) << 8) | quint16(c1); + } + + inline qint16 i16(quint8 c1, quint8 c2) const { + return qint32(ui16(c1, c2)); + } + + inline quint32 ui32(quint8 c1, quint8 c2, quint8 c3, quint8 c4) const { + return (quint32(c4) << 24) | (quint32(c3) << 16) | (quint32(c2) << 8) | quint32(c1); + } + + inline qint32 i32(quint8 c1, quint8 c2, quint8 c3, quint8 c4) const { + return qint32(ui32(c1, c2, c3, c4)); + } + + static ChunkList innerFromDevice(QIODevice *d, bool *ok, qint32 alignBytes); + +private: + char _chunkId[4]; + + quint32 _size; + + qint32 _align; + + qint64 _dataPos; + + QByteArray _data; + + ChunkList _chunks; + + +}; + +/*! + * \brief The IffBMHD class + * Bitmap Header + */ +class BMHDChunk: public IFFChunk +{ +public: + enum Compression { + Uncompressed = 0, + Rle = 1 + }; + + virtual ~BMHDChunk() override; + + BMHDChunk(); + BMHDChunk(const BMHDChunk& other) = default; + BMHDChunk& operator =(const BMHDChunk& other) = default; + + virtual bool isValid() const override; + + qint32 width() const; + + qint32 height() const; + + QSize size() const; + + qint32 left() const; + + qint32 top() const; + + quint8 bitplanes() const; + + quint8 masking() const; + + Compression compression() const; + + quint8 padding() const; + + qint16 transparency() const; + + quint8 xAspectRatio() const; + + quint8 yAspectRatio() const; + + quint16 pageWidth() const; + + quint16 pageHeight() const; + + quint32 rowLen() const; + + CHUNKID_DEFINE(BMHD_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + +/*! + * \brief The CMAPChunk class + */ +class CMAPChunk : public IFFChunk +{ +public: + virtual ~CMAPChunk() override; + CMAPChunk(); + CMAPChunk(const CMAPChunk& other) = default; + CMAPChunk& operator =(const CMAPChunk& other) = default; + + virtual bool isValid() const override; + + QList palette() const; + + CHUNKID_DEFINE(CMAP_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + +/*! + * \brief The CAMGChunk class + */ +class CAMGChunk : public IFFChunk +{ +public: + enum ModeId { + LoResLace = 0x0004, + HalfBrite = 0x0080, + LoResDpf = 0x0400, + Ham = 0x0800, + HiRes = 0x8000 + }; + + Q_DECLARE_FLAGS(ModeIds, ModeId) + + virtual ~CAMGChunk() override; + CAMGChunk(); + CAMGChunk(const CAMGChunk& other) = default; + CAMGChunk& operator =(const CAMGChunk& other) = default; + + virtual bool isValid() const override; + + ModeIds modeId() const; + + CHUNKID_DEFINE(CAMG_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + +/*! + * \brief The DPIChunk class + */ +class DPIChunk : public IFFChunk +{ +public: + virtual ~DPIChunk() override; + DPIChunk(); + DPIChunk(const DPIChunk& other) = default; + DPIChunk& operator =(const DPIChunk& other) = default; + + virtual bool isValid() const override; + + /*! + * \brief dpiX + * \return The horizontal resolution in DPI. + */ + quint16 dpiX() const; + + /*! + * \brief dpiY + * \return The vertical resolution in DPI. + */ + quint16 dpiY() const; + + /*! + * \brief dotsPerMeterX + * \return X resolution as wanted by QImage. + */ + qint32 dotsPerMeterX() const; + + /*! + * \brief dotsPerMeterY + * \return Y resolution as wanted by QImage. + */ + qint32 dotsPerMeterY() const; + + CHUNKID_DEFINE(DPI__CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + + +/*! + * \brief The BODYChunk class + */ +class BODYChunk : public IFFChunk +{ +public: + virtual ~BODYChunk() override; + BODYChunk(); + BODYChunk(const BODYChunk& other) = default; + BODYChunk& operator =(const BODYChunk& other) = default; + + virtual bool isValid() const override; + + CHUNKID_DEFINE(BODY_CHUNK) + + /*! + * \brief readStride + * \param d The device. + * \param header The bitmap header. + * \param camg The CAMG chunk (optional) + * \param cmap The CMAP chunk (optional) + * \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; + + /*! + * \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 + */ + bool resetStrideRead(QIODevice *d) const; + +private: + static QByteArray deinterleave(const QByteArray &planes, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr); + + mutable QByteArray _readBuffer; +}; + +/*! + * \brief The FORMChunk class + */ +class FORMChunk : public IFFChunk +{ + QByteArray _type; + +public: + virtual ~FORMChunk() override; + FORMChunk(); + FORMChunk(const FORMChunk& other) = default; + FORMChunk& operator =(const FORMChunk& other) = default; + + virtual bool isValid() const override; + + bool isSupported() const; + + QByteArray formType() const; + + QImage::Format format() const; + + QSize size() const; + + CHUNKID_DEFINE(FORM_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + + +/*! + * \brief The FOR4Chunk class + */ +class FOR4Chunk : public IFFChunk +{ + QByteArray _type; + +public: + virtual ~FOR4Chunk() override; + FOR4Chunk(); + FOR4Chunk(const FOR4Chunk& other) = default; + FOR4Chunk& operator =(const FOR4Chunk& other) = default; + + virtual bool isValid() const override; + + virtual qint32 alignBytes() const override; + + bool isSupported() const; + + QByteArray formType() const; + + QImage::Format format() const; + + QSize size() const; + + CHUNKID_DEFINE(FOR4_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + +/*! + * \brief The TBHDChunk class + */ +class TBHDChunk : public IFFChunk +{ +public: + enum Flag { + Rgb = 0x01, + Alpha = 0x02, + ZBuffer = 0x04, + Black = 0x10, + + RgbA = Rgb | Alpha + }; + Q_DECLARE_FLAGS(Flags, Flag) + + enum Compression { + Uncompressed = 0, + Rle = 1 + }; + + virtual ~TBHDChunk() override; + + TBHDChunk(); + TBHDChunk(const TBHDChunk& other) = default; + TBHDChunk& operator =(const TBHDChunk& other) = default; + + virtual bool isValid() const override; + + virtual qint32 alignBytes() const override; + + /*! + * \brief width + * \return Image width in pixels. + */ + qint32 width() const; + + /*! + * \brief height + * \return Image height in pixels. + */ + qint32 height() const; + + /*! + * \brief size + * \return Image size in pixels. + */ + QSize size() const; + + /*! + * \brief left + * \return + */ + qint32 left() const; + + /*! + * \brief top + * \return + */ + qint32 top() const; + + /*! + * \brief flags + * \return Image flags. + */ + Flags flags() const; + + /*! + * \brief bpc + * \return Byte per channel (1 or 2) + */ + qint32 bpc() const; + + /*! + * \brief channels + * \return + */ + qint32 channels() const; + + /*! + * \brief tiles + * \return The number of tiles of the image. + */ + quint16 tiles() const; + + /*! + * \brief compression + * \return The data compression. + */ + Compression compression() const; + + /*! + * \brief format + * \return + */ + QImage::Format format() const; + + CHUNKID_DEFINE(TBHD_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + +/*! + * \brief The RGBAChunk class + */ +class RGBAChunk : public IFFChunk +{ +public: + virtual ~RGBAChunk() override; + RGBAChunk(); + RGBAChunk(const RGBAChunk& other) = default; + RGBAChunk& operator =(const RGBAChunk& other) = default; + + virtual bool isValid() const override; + + virtual qint32 alignBytes() const override; + + /*! + * \brief isTileCompressed + * \param header The image header. + * \return True if the tile is compressed, otherwise false. + */ + bool isTileCompressed(const TBHDChunk *header) const; + + /*! + * \brief pos + * \return The tile position (top-left corner) in the final image. + */ + QPoint pos() const; + + /*! + * \brief size + * \return The tile size in pixels. + */ + QSize size() const; + + /*! + * \brief tile + * Create the tile by reading the data from the device. + * \param d The device. + * \param header The image header. + * \return The image tile. + */ + QImage tile(QIODevice *d, const TBHDChunk *header) const; + + CHUNKID_DEFINE(RGBA_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; + +private: + QImage compressedTile(QIODevice *d, const TBHDChunk *header) const; + + QImage uncompressedTile(QIODevice *d, const TBHDChunk *header) const; + + QByteArray readStride(QIODevice *d, const TBHDChunk *header) const; + +private: + QPoint _pos; + + QSize _size; + + mutable QByteArray _readBuffer; +}; + + +/*! + * \brief The AUTHChunk class + */ +class AUTHChunk : public IFFChunk +{ +public: + virtual ~AUTHChunk() override; + AUTHChunk(); + AUTHChunk(const AUTHChunk& other) = default; + AUTHChunk& operator =(const AUTHChunk& other) = default; + + virtual bool isValid() const override; + + QString value() const; + + CHUNKID_DEFINE(AUTH_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + +/*! + * \brief The DATEChunk class + */ +class DATEChunk : public IFFChunk +{ +public: + virtual ~DATEChunk() override; + DATEChunk(); + DATEChunk(const DATEChunk& other) = default; + DATEChunk& operator =(const DATEChunk& other) = default; + + virtual bool isValid() const override; + + QDateTime value() const; + + CHUNKID_DEFINE(DATE_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + +/*! + * \brief The FVERChunk class + * + * \warning The specifications on wiki.amigaos.net differ from what I see in a file saved in Maya format. I do not interpret the data for now. + */ +class FVERChunk : public IFFChunk +{ +public: + virtual ~FVERChunk() override; + FVERChunk(); + FVERChunk(const FVERChunk& other) = default; + FVERChunk& operator =(const FVERChunk& other) = default; + + virtual bool isValid() const override; + + CHUNKID_DEFINE(FVER_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + + +/*! + * \brief The HISTChunk class + */ +class HISTChunk : public IFFChunk +{ +public: + virtual ~HISTChunk() override; + HISTChunk(); + HISTChunk(const HISTChunk& other) = default; + HISTChunk& operator =(const HISTChunk& other) = default; + + virtual bool isValid() const override; + + QString value() const; + + CHUNKID_DEFINE(HIST_CHUNK) + +protected: + virtual bool innerReadStructure(QIODevice *d) override; +}; + + +/*! + * \brief The VERSChunk class + */ +class VERSChunk : public IFFChunk +{ +public: + virtual ~VERSChunk() override; + VERSChunk(); + VERSChunk(const VERSChunk& other) = default; + VERSChunk& operator =(const VERSChunk& other) = default; + + virtual bool isValid() const override; + + QString value() const; + + CHUNKID_DEFINE(VERS_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 new file mode 100644 index 0000000..2ffed32 --- /dev/null +++ b/src/imageformats/iff.cpp @@ -0,0 +1,362 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2025 Mirco Miranda + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "chunks_p.h" +#include "iff_p.h" +#include "util_p.h" + +#include +#include +#include +#include + +#ifdef QT_DEBUG +Q_LOGGING_CATEGORY(LOG_IFFPLUGIN, "kf.imageformats.plugins.iff", QtInfoMsg) +#else +Q_LOGGING_CATEGORY(LOG_IFFPLUGIN, "kf.imageformats.plugins.iff", QtWarningMsg) +#endif + +class IFFHandlerPrivate +{ +public: + IFFHandlerPrivate() {} + ~IFFHandlerPrivate() {} + + bool readStructure(QIODevice *d) { + if (d == nullptr) { + return {}; + } + + if (!_chunks.isEmpty()) { + return true; + } + + auto ok = false; + auto chunks = IFFChunk::fromDevice(d, &ok); + if (ok) { + _chunks = chunks; + } + return ok; + } + + template + static QList searchForms(const IFFChunk::ChunkList &chunks, bool supportedOnly = true) { + QList list; + auto cid = T::defaultChunkId(); + auto forms = IFFChunk::search(cid, chunks); + for (auto &&form : forms) { + if (auto f = dynamic_cast(form.data())) + if (!supportedOnly || f->isSupported()) + list << f; + } + return list; + } + + template + QList searchForms(bool supportedOnly = true) { + return searchForms(_chunks, supportedOnly); + } + + IFFChunk::ChunkList _chunks; +}; + + +IFFHandler::IFFHandler() + : QImageIOHandler() + , d(new IFFHandlerPrivate) +{ +} + +bool IFFHandler::canRead() const +{ + if (canRead(device())) { + setFormat("iff"); + return true; + } + return false; +} + +bool IFFHandler::canRead(QIODevice *device) +{ + if (!device) { + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::canRead() called with no device"; + return false; + } + + if (device->isSequential()) { + return false; + } + + auto ok = false; + auto pos = device->pos(); + auto chunks = IFFChunk::fromDevice(device, &ok); + if (!device->seek(pos)) { + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::canRead() unable to reset device position"; + } + if (ok) { + auto forms = IFFHandlerPrivate::searchForms(chunks, true); + auto for4s = IFFHandlerPrivate::searchForms(chunks, true); + ok = !forms.isEmpty() || !for4s.isEmpty(); + } + return ok; +} + +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)); + } + } + auto auths = IFFChunk::searchT(form); + if (!auths.isEmpty()) { + auto auth = auths.first()->value(); + if (!auth.isEmpty()) { + img.setText(QStringLiteral(META_KEY_AUTHOR), auth); + } + } + auto vers = IFFChunk::searchT(form); + if (!vers.isEmpty()) { + auto ver = vers.first()->value(); + if (!vers.isEmpty()) { + img.setText(QStringLiteral(META_KEY_SOFTWARE), ver); + } + } +} + +bool IFFHandler::readStandardImage(QImage *image) +{ + auto forms = d->searchForms(); + if (forms.isEmpty()) { + return false; + } + auto &&form = forms.first(); + + // 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::readStandardImage() 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::readStandardImage() error while allocating the 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) { + if (!cmaps.isEmpty()) + if (auto &&cmap = cmaps.first()) + img.setColorTable(cmap->palette()); + } + + auto bodies = IFFChunk::searchT(form); + 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"; + return false; + } + 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); + if (ba.isEmpty()) { + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image scanline"; + return false; + } + memcpy(line, ba.constData(), std::min(img.bytesPerLine(), ba.size())); + } + } + + addMetadata(img, form); + + *image = img; + return true; +} + +bool IFFHandler::readMayaImage(QImage *image) +{ + auto forms = d->searchForms(); + if (forms.isEmpty()) { + return false; + } + auto &&form = forms.first(); + + // 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::readMayaImage() 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::readMayaImage() error while allocating the image"; + return false; + } + + auto &&tiles = IFFChunk::searchT(form); + if ((tiles.size() & 0xFFFF) != header->tiles()) { // Photoshop, on large images saves more than 65535 tiles + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() tile number mismatch: found" << tiles.size() << "while expected" << header->tiles(); + return false; + } + for (auto &&tile : tiles) { + auto tp = tile->pos(); + auto ts = tile->size(); + if (tp.x() < 0 || tp.x() + ts.width() > img.width()) { + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() wrong tile position or size"; + return false; + } + if (tp.y() < 0 || tp.y() + ts.height() > img.height()) { + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() wrong tile position or size"; + return false; + } + // For future releases: it might be a good idea not to use a QPainter + auto ti = tile->tile(device(), header); + if (ti.isNull()) { + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() error while decoding the tile"; + return false; + } + QPainter painter(&img); + painter.setCompositionMode(QPainter::CompositionMode_Source); + painter.drawImage(tp, ti); + } +#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0) + img.mirror(false, true); +#else + img.flip(Qt::Orientation::Vertical); +#endif + addMetadata(img, form); + + *image = img; + return true; +} + +bool IFFHandler::read(QImage *image) +{ + if (!d->readStructure(device())) { + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read() invalid IFF structure"; + return false; + } + + if (readStandardImage(image)) { + return true; + } + + if (readMayaImage(image)) { + return true; + } + + qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read() no supported image found"; + return false; +} + +bool IFFHandler::supportsOption(ImageOption option) const +{ + if (option == QImageIOHandler::Size) { + return true; + } + if (option == QImageIOHandler::ImageFormat) { + return true; + } + return false; +} + +QVariant IFFHandler::option(ImageOption option) const +{ + QVariant v; + + if (option == QImageIOHandler::Size) { + if (d->readStructure(device())) { + auto forms = d->searchForms(); + if (!forms.isEmpty()) + if (auto &&form = forms.first()) + v = QVariant::fromValue(form->size()); + + auto for4s = d->searchForms(); + if (!for4s.isEmpty()) + if (auto &&form = for4s.first()) + v = QVariant::fromValue(form->size()); + } + } + + if (option == QImageIOHandler::ImageFormat) { + if (d->readStructure(device())) { + auto forms = d->searchForms(); + if (!forms.isEmpty()) + if (auto &&form = forms.first()) + v = QVariant::fromValue(form->format()); + + auto for4s = d->searchForms(); + if (!for4s.isEmpty()) + if (auto &&form = for4s.first()) + v = QVariant::fromValue(form->format()); + } + } + + return v; +} + +QImageIOPlugin::Capabilities IFFPlugin::capabilities(QIODevice *device, const QByteArray &format) const +{ + if (format == "iff") { + return Capabilities(CanRead); + } + if (!format.isEmpty()) { + return {}; + } + if (!device->isOpen()) { + return {}; + } + + Capabilities cap; + if (device->isReadable() && IFFHandler::canRead(device)) { + cap |= CanRead; + } + return cap; +} + +QImageIOHandler *IFFPlugin::create(QIODevice *device, const QByteArray &format) const +{ + QImageIOHandler *handler = new IFFHandler; + handler->setDevice(device); + handler->setFormat(format); + return handler; +} + +#include "moc_iff_p.cpp" diff --git a/src/imageformats/iff.json b/src/imageformats/iff.json new file mode 100644 index 0000000..a64b8cb --- /dev/null +++ b/src/imageformats/iff.json @@ -0,0 +1,4 @@ +{ + "Keys": [ "iff" ], + "MimeTypes": [ "application/x-iff" ] +} diff --git a/src/imageformats/iff_p.h b/src/imageformats/iff_p.h new file mode 100644 index 0000000..df36b31 --- /dev/null +++ b/src/imageformats/iff_p.h @@ -0,0 +1,47 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2025 Mirco Miranda + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KIMG_IFF_P_H +#define KIMG_IFF_P_H + +#include +#include + +class IFFHandlerPrivate; +class IFFHandler : public QImageIOHandler +{ +public: + IFFHandler(); + + bool canRead() const override; + bool read(QImage *image) override; + + bool supportsOption(QImageIOHandler::ImageOption option) const override; + QVariant option(QImageIOHandler::ImageOption option) const override; + + static bool canRead(QIODevice *device); + +private: + bool readStandardImage(QImage *image); + + bool readMayaImage(QImage *image); + +private: + const QScopedPointer d; +}; + +class IFFPlugin : public QImageIOPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "iff.json") + +public: + Capabilities capabilities(QIODevice *device, const QByteArray &format) const override; + QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override; +}; + +#endif // KIMG_IFF_P_H diff --git a/src/imageformats/packbits_p.h b/src/imageformats/packbits_p.h new file mode 100644 index 0000000..63a1b55 --- /dev/null +++ b/src/imageformats/packbits_p.h @@ -0,0 +1,111 @@ +/* + Packbits compression used on many legacy formats (IFF, PSD, TIFF). + + SPDX-FileCopyrightText: 2025 Mirco Miranda + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef PACKBITS_P_H +#define PACKBITS_P_H + +#include + +/*! + * \brief packbitsDecompress + * Fast PackBits decompression. + * \param input The compressed input buffer. + * \param ilen The input buffer size. + * \param output The uncompressed target buffer. + * \param olen The target buffer size. + * \param allowN128 If true, -128 is a valid run length size (false for PSD / TIFF, true for IFF) . + * \return The number of valid bytes in the target buffer. + */ +inline qint64 packbitsDecompress(const char *input, qint64 ilen, char *output, qint64 olen, bool allowN128 = false) +{ + qint64 j = 0; + for (qint64 ip = 0, rr = 0, available = olen; j < olen && ip < ilen; available = olen - j) { + signed char n = static_cast(input[ip++]); + if (n == -128 && !allowN128) + continue; + + if (n >= 0) { + rr = qint64(n) + 1; + if (available < rr) { + --ip; + break; + } + + if (ip + rr > ilen) + return -1; + memcpy(output + j, input + ip, size_t(rr)); + ip += rr; + } else if (ip < ilen) { + rr = qint64(1-n); + if (available < rr) { + --ip; + break; + } + memset(output + j, input[ip++], size_t(rr)); + } + + j += rr; + } + return j; +} + +/*! + * \brief packbitsDecompress + * PackBits decompression. + * \param input The input device. + * \param output The uncompressed target buffer. + * \param olen The target buffer size. + * \param allowN128 If true, -128 is a valid run length size (false for PSD / TIFF, true for IFF) . + * \return The number of valid bytes in the target buffer. + */ +inline qint64 packbitsDecompress(QIODevice *input, char *output, qint64 olen, bool allowN128 = false) +{ + qint64 j = 0; + for (qint64 rr = 0, available = olen; j < olen; available = olen - j) { + char n; + + // check the output buffer space for the next run + if (available < 129) { + if (input->peek(&n, 1) != 1) { // end of data (or error) + break; + } + if (static_cast(n) != -128 || allowN128) + if ((static_cast(n) >= 0 ? qint64(n) + 1 : qint64(1 - n)) > available) + break; + } + + // decompress + if (input->read(&n, 1) != 1) { // end of data (or error) + break; + } + + if (static_cast(n) == -128 && !allowN128) { + continue; + } + + if (static_cast(n) >= 0) { + rr = input->read(output + j, qint64(n) + 1); + if (rr == -1) { + return -1; + } + } + else { + char b; + if (input->read(&b, 1) != 1) { + break; + } + rr = qint64(1 - static_cast(n)); + std::memset(output + j, b, size_t(rr)); + } + + j += rr; + } + return j; +} + + +#endif // PACKBITS_P_H diff --git a/src/imageformats/psd.cpp b/src/imageformats/psd.cpp index ab8a6ea..8960bbb 100644 --- a/src/imageformats/psd.cpp +++ b/src/imageformats/psd.cpp @@ -28,6 +28,7 @@ #include "fastmath_p.h" #include "microexif_p.h" +#include "packbits_p.h" #include "psd_p.h" #include "scanlineconverter_p.h" #include "util_p.h" @@ -712,48 +713,6 @@ static bool IsSupported(const PSDHeader &header) return true; } -/*! - * \brief decompress - * Fast PackBits decompression. - * \param input The compressed input buffer. - * \param ilen The input buffer size. - * \param output The uncompressed target buffer. - * \param olen The target buffer size. - * \return The number of valid bytes in the target buffer. - */ -qint64 decompress(const char *input, qint64 ilen, char *output, qint64 olen) -{ - qint64 j = 0; - for (qint64 ip = 0, rr = 0, available = olen; j < olen && ip < ilen; available = olen - j) { - signed char n = static_cast(input[ip++]); - if (n == -128) - continue; - - if (n >= 0) { - rr = qint64(n) + 1; - if (available < rr) { - --ip; - break; - } - - if (ip + rr > ilen) - return -1; - memcpy(output + j, input + ip, size_t(rr)); - ip += rr; - } else if (ip < ilen) { - rr = qint64(1-n); - if (available < rr) { - --ip; - break; - } - memset(output + j, input[ip++], size_t(rr)); - } - - j += rr; - } - return j; -} - /*! * \brief imageFormat * \param header The PSD header. @@ -1102,7 +1061,7 @@ bool readChannel(QByteArray &target, QDataStream &stream, quint32 compressedSize if (stream.readRawData(tmp.data(), tmp.size()) != tmp.size()) { return false; } - if (decompress(tmp.data(), tmp.size(), target.data(), target.size()) < 0) { + if (packbitsDecompress(tmp.data(), tmp.size(), target.data(), target.size()) < 0) { return false; } } else if (stream.readRawData(target.data(), target.size()) != target.size()) {