Compare commits

...

9 Commits

Author SHA1 Message Date
21b3b890ec tga: Be less strict about palette
There's images in the wild that claim to not have a palette
but still have one.
2025-07-09 13:17:20 +02:00
aef4bd7e1c Fix unused param in MicroExif::toByteArray() 2025-07-07 08:17:36 +02:00
ea1983a7d1 IFF: Fix possible stack overflow 2025-07-05 09:59:06 +00:00
775b53f1f6 Update version to 6.17.0 2025-07-04 18:28:45 +02:00
02cf1502c0 Update dependency version to 6.16.0 2025-07-04 16:29:08 +02:00
f6c718a789 TGA: add indexed write support
Indexed images are saved as uncompressed TGA with color map.

Closes #30
2025-07-03 07:36:33 +02:00
4f2f2425d3 IFF: read only support to Interchange Format Files
Read-only support for most common Interchange Format Files (IFF). Supports IFF saved by Photoshop for the Amiga and Maya platform and HAM6 coded files.

Closes #29
2025-07-01 21:59:03 +00:00
e6357c22f7 tga: Use Format_Indexed8 for indexed formats and support 32-bit BGRA colormap 2025-06-26 16:00:11 +02:00
094177a01c Update version to 6.16.0 2025-06-06 16:31:26 +02:00
45 changed files with 2921 additions and 65 deletions

View File

@ -1,11 +1,11 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
set(KF_VERSION "6.15.0") # handled by release scripts set(KF_VERSION "6.17.0") # handled by release scripts
set(KF_DEP_VERSION "6.15.0") # handled by release scripts set(KF_DEP_VERSION "6.16.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION}) project(KImageFormats VERSION ${KF_VERSION})
include(FeatureSummary) include(FeatureSummary)
find_package(ECM 6.15.0 NO_MODULE) find_package(ECM 6.16.0 NO_MODULE)
set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules") set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules")
feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES) feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES)

View File

@ -17,6 +17,7 @@ The following image formats have read-only support:
- Animated Windows cursors (ani) - Animated Windows cursors (ani)
- Camera RAW images (arw, cr2, cr3, dcs, dng, ...) - Camera RAW images (arw, cr2, cr3, dcs, dng, ...)
- Gimp (xcf) - Gimp (xcf)
- Interchange Format Files (IFF)
- Krita (kra) - Krita (kra)
- OpenRaster (ora) - OpenRaster (ora)
- Pixar raster (pxr) - 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 - EPS: n/a
- HDR: n/a (large image) - HDR: n/a (large image)
- HEIF: n/a - 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 - 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 - 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 - JXR: n/a, in any case no larger than 4 GB

View File

@ -77,6 +77,7 @@ endmacro()
# result against the data read from the corresponding png file # result against the data read from the corresponding png file
kimageformats_read_tests( kimageformats_read_tests(
hdr hdr
iff
pcx pcx
pfm pfm
psd psd

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -84,6 +84,10 @@ endif()
################################## ##################################
kimageformats_add_plugin(kimg_iff SOURCES iff.cpp chunks.cpp)
##################################
if (LibJXL_FOUND AND LibJXLThreads_FOUND AND LibJXLCMS_FOUND) if (LibJXL_FOUND AND LibJXLThreads_FOUND AND LibJXLCMS_FOUND)
kimageformats_add_plugin(kimg_jxl SOURCES jxl.cpp microexif.cpp) kimageformats_add_plugin(kimg_jxl SOURCES jxl.cpp microexif.cpp)
target_link_libraries(kimg_jxl PRIVATE PkgConfig::LibJXL PkgConfig::LibJXLThreads PkgConfig::LibJXLCMS) target_link_libraries(kimg_jxl PRIVATE PkgConfig::LibJXL PkgConfig::LibJXLThreads PkgConfig::LibJXLCMS)

1436
src/imageformats/chunks.cpp Normal file

File diff suppressed because it is too large Load Diff

833
src/imageformats/chunks_p.h Normal file
View File

@ -0,0 +1,833 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2025 Mirco Miranda <mircomir@outlook.com>
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 <QByteArray>
#include <QDateTime>
#include <QImage>
#include <QIODevice>
#include <QPoint>
#include <QSize>
#include <QSharedPointer>
// 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<QSharedPointer<IFFChunk>>;
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<IFFChunk>& 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 <class T>
static QList<const T*> searchT(const IFFChunk *chunk) {
QList<const T*> list;
if (chunk == nullptr)
return list;
auto cid = T::defaultChunkId();
if (chunk->chunkId() == cid)
if (auto c = dynamic_cast<const T*>(chunk))
list << c;
auto tmp = chunk->chunks();
for (auto &&c : tmp)
list << searchT<T>(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 <class T>
static QList<const T*> searchT(const ChunkList& chunks) {
QList<const T*> list;
for (auto &&chunk : chunks)
list << searchT<T>(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);
/*!
* \brief recursionCounter
* Protection against stack overflow due to broken data.
* \return The current recursion counter.
*/
qint32 recursionCounter() const;
void setRecursionCounter(qint32 cnt);
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, qint32 recursionCnt);
private:
char _chunkId[4];
quint32 _size;
qint32 _align;
qint64 _dataPos;
QByteArray _data;
ChunkList _chunks;
qint32 _recursionCnt;
};
/*!
* \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<QRgb> 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

362
src/imageformats/iff.cpp Normal file
View File

@ -0,0 +1,362 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2025 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "chunks_p.h"
#include "iff_p.h"
#include "util_p.h"
#include <QIODevice>
#include <QImage>
#include <QLoggingCategory>
#include <QPainter>
#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 <class T>
static QList<const T*> searchForms(const IFFChunk::ChunkList &chunks, bool supportedOnly = true) {
QList<const T*> list;
auto cid = T::defaultChunkId();
auto forms = IFFChunk::search(cid, chunks);
for (auto &&form : forms) {
if (auto f = dynamic_cast<const T*>(form.data()))
if (!supportedOnly || f->isSupported())
list << f;
}
return list;
}
template <class T>
QList<const T*> searchForms(bool supportedOnly = true) {
return searchForms<T>(_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<FORMChunk>(chunks, true);
auto for4s = IFFHandlerPrivate::searchForms<FOR4Chunk>(chunks, true);
ok = !forms.isEmpty() || !for4s.isEmpty();
}
return ok;
}
void addMetadata(QImage& img, const IFFChunk *form)
{
auto dates = IFFChunk::searchT<DATEChunk>(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<AUTHChunk>(form);
if (!auths.isEmpty()) {
auto auth = auths.first()->value();
if (!auth.isEmpty()) {
img.setText(QStringLiteral(META_KEY_AUTHOR), auth);
}
}
auto vers = IFFChunk::searchT<VERSChunk>(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<FORMChunk>();
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<BMHDChunk>(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<DPIChunk>(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<CMAPChunk>(form);
if (img.format() == QImage::Format_Indexed8) {
if (!cmaps.isEmpty())
if (auto &&cmap = cmaps.first())
img.setColorTable(cmap->palette());
}
auto bodies = IFFChunk::searchT<BODYChunk>(form);
if (bodies.isEmpty()) {
img.fill(0);
} else {
const CAMGChunk *camg = nullptr;
auto camgs = IFFChunk::searchT<CAMGChunk>(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<char*>(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<FOR4Chunk>();
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<TBHDChunk>(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<RGBAChunk>(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<FORMChunk>();
if (!forms.isEmpty())
if (auto &&form = forms.first())
v = QVariant::fromValue(form->size());
auto for4s = d->searchForms<FOR4Chunk>();
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<FORMChunk>();
if (!forms.isEmpty())
if (auto &&form = forms.first())
v = QVariant::fromValue(form->format());
auto for4s = d->searchForms<FOR4Chunk>();
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"

View File

@ -0,0 +1,4 @@
{
"Keys": [ "iff" ],
"MimeTypes": [ "application/x-iff" ]
}

47
src/imageformats/iff_p.h Normal file
View File

@ -0,0 +1,47 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2025 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KIMG_IFF_P_H
#define KIMG_IFF_P_H
#include <QImageIOPlugin>
#include <QScopedPointer>
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<IFFHandlerPrivate> 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

View File

@ -1072,7 +1072,7 @@ QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder, const
QByteArray ba; QByteArray ba;
{ {
QBuffer buf(&ba); QBuffer buf(&ba);
if (!write(&buf, byteOrder)) if (!write(&buf, byteOrder, version))
return {}; return {};
} }
return ba; return ba;

View File

@ -0,0 +1,111 @@
/*
Packbits compression used on many legacy formats (IFF, PSD, TIFF).
SPDX-FileCopyrightText: 2025 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef PACKBITS_P_H
#define PACKBITS_P_H
#include <QIODevice>
/*!
* \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<signed char>(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<signed char>(n) != -128 || allowN128)
if ((static_cast<signed char>(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<signed char>(n) == -128 && !allowN128) {
continue;
}
if (static_cast<signed char>(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<signed char>(n));
std::memset(output + j, b, size_t(rr));
}
j += rr;
}
return j;
}
#endif // PACKBITS_P_H

View File

@ -28,6 +28,7 @@
#include "fastmath_p.h" #include "fastmath_p.h"
#include "microexif_p.h" #include "microexif_p.h"
#include "packbits_p.h"
#include "psd_p.h" #include "psd_p.h"
#include "scanlineconverter_p.h" #include "scanlineconverter_p.h"
#include "util_p.h" #include "util_p.h"
@ -712,48 +713,6 @@ static bool IsSupported(const PSDHeader &header)
return true; 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<signed char>(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 * \brief imageFormat
* \param header The PSD header. * \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()) { if (stream.readRawData(tmp.data(), tmp.size()) != tmp.size()) {
return false; 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; return false;
} }
} else if (stream.readRawData(target.data(), target.size()) != target.size()) { } else if (stream.readRawData(target.data(), target.size()) != target.size()) {

View File

@ -99,7 +99,12 @@ static bool IsSupported(const TgaHeader &head)
return false; return false;
} }
if (head.image_type == TGA_TYPE_INDEXED || head.image_type == TGA_TYPE_RLE_INDEXED) { if (head.image_type == TGA_TYPE_INDEXED || head.image_type == TGA_TYPE_RLE_INDEXED) {
if (head.colormap_length > 256 || head.colormap_size != 24 || head.colormap_type != 1) { // GIMP saves TGAs with palette size of 257 (but 256 used) so, I need to check the pixel size only.
if (head.pixel_size > 8 || head.colormap_type != 1) {
return false;
}
// colormap_size == 16 would be ARRRRRGG GGGBBBBB but we don't support that.
if (head.colormap_size != 24 && head.colormap_size != 32) {
return false; return false;
} }
} }
@ -114,10 +119,11 @@ static bool IsSupported(const TgaHeader &head)
if (head.pixel_size != 8 && head.pixel_size != 16 && head.pixel_size != 24 && head.pixel_size != 32) { if (head.pixel_size != 8 && head.pixel_size != 16 && head.pixel_size != 24 && head.pixel_size != 32) {
return false; return false;
} }
// If the colormap_type field is set to zero, indicating that no color map exists, then colormap_size, colormap_index and colormap_length should be set to zero. // If the colormap_type field is set to zero, indicating that no color map exists, then colormap_index and colormap_length should be set to zero.
if (head.colormap_type == 0 && (head.colormap_size != 0 || head.colormap_index != 0 || head.colormap_length != 0)) { if (head.colormap_type == 0 && (head.colormap_index != 0 || head.colormap_length != 0)) {
return false; return false;
} }
return true; return true;
} }
@ -189,6 +195,8 @@ static QImage::Format imageFormat(const TgaHeader &head)
if (numAlphaBits == 8) { if (numAlphaBits == 8) {
format = QImage::Format_ARGB32; format = QImage::Format_ARGB32;
} }
} else if (head.image_type == TGA_TYPE_INDEXED || head.image_type == TGA_TYPE_RLE_INDEXED) {
format = QImage::Format_Indexed8;
} else { } else {
format = QImage::Format_RGB32; format = QImage::Format_RGB32;
} }
@ -232,21 +240,40 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
} }
// Read palette. // Read palette.
static const int max_palette_size = 768;
char palette[max_palette_size];
if (info.pal) { if (info.pal) {
// @todo Support palettes in other formats! QList<QRgb> colorTable;
const int palette_size = 3 * tga.colormap_length; #if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
if (palette_size > max_palette_size) { colorTable.resize(tga.colormap_length);
#else
colorTable.resizeForOverwrite(tga.colormap_length);
#endif
if (tga.colormap_size == 32) { // BGRA.
char data[4];
for (QRgb &rgb : colorTable) {
const auto dataRead = s.readRawData(data, 4);
if (dataRead < 4) {
return false; return false;
} }
const int dataRead = s.readRawData(palette, palette_size); // BGRA.
if (dataRead < 0) { rgb = qRgba(data[2], data[1], data[0], data[3]);
}
} else if (tga.colormap_size == 24) { // BGR.
char data[3];
for (QRgb &rgb : colorTable) {
const auto dataRead = s.readRawData(data, 3);
if (dataRead < 3) {
return false; return false;
} }
if (dataRead < max_palette_size) { // BGR.
memset(&palette[dataRead], 0, max_palette_size - dataRead); rgb = qRgb(data[2], data[1], data[0]);
} }
// TODO tga.colormap_size == 16 ARRRRRGG GGGBBBBB
} else {
return false;
}
img.setColorTable(colorTable);
} }
// Allocate image. // Allocate image.
@ -355,14 +382,19 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
uchar *src = image; uchar *src = image;
for (int y = y_start; y != y_end; y += y_step) { for (int y = y_start; y != y_end; y += y_step) {
auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
if (info.pal) { if (info.pal) {
// Paletted. // Paletted.
auto scanline = img.scanLine(y);
for (int x = 0; x < tga.width; x++) { for (int x = 0; x < tga.width; x++) {
uchar idx = *src++; uchar idx = *src++;
scanline[x] = qRgb(palette[3 * idx + 2], palette[3 * idx + 1], palette[3 * idx + 0]); if (Q_UNLIKELY(idx >= tga.colormap_length)) {
valid = false;
break;
}
scanline[x] = idx;
} }
} else if (info.grey) { } else if (info.grey) {
auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
// Greyscale. // Greyscale.
for (int x = 0; x < tga.width; x++) { for (int x = 0; x < tga.width; x++) {
if (tga.pixel_size == 16) { if (tga.pixel_size == 16) {
@ -375,6 +407,7 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
} }
} }
} else { } else {
auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
// True Color. // True Color.
if (tga.pixel_size == 16) { if (tga.pixel_size == 16) {
for (int x = 0; x < tga.width; x++) { for (int x = 0; x < tga.width; x++) {
@ -401,7 +434,7 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
// Free image. // Free image.
free(image); free(image);
return true; return valid;
} }
} // namespace } // namespace
@ -469,6 +502,59 @@ bool TGAHandler::read(QImage *outImage)
} }
bool TGAHandler::write(const QImage &image) bool TGAHandler::write(const QImage &image)
{
if (image.format() == QImage::Format_Indexed8)
return writeIndexed(image);
return writeRGBA(image);
}
bool TGAHandler::writeIndexed(const QImage &image)
{
QDataStream s(device());
s.setByteOrder(QDataStream::LittleEndian);
QImage img(image);
auto ct = img.colorTable();
s << quint8(0); // ID Length
s << quint8(1); // Color Map Type
s << quint8(TGA_TYPE_INDEXED); // Image Type
s << quint16(0); // First Entry Index
s << quint16(ct.size()); // Color Map Length
s << quint8(32); // Color map Entry Size
s << quint16(0); // X-origin of Image
s << quint16(0); // Y-origin of Image
s << quint16(img.width()); // Image Width
s << quint16(img.height()); // Image Height
s << quint8(8); // Pixe Depth
s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT); // Image Descriptor
for (auto &&rgb : ct) {
s << quint8(qBlue(rgb));
s << quint8(qGreen(rgb));
s << quint8(qRed(rgb));
s << quint8(qAlpha(rgb));
}
if (s.status() != QDataStream::Ok) {
return false;
}
for (int y = 0; y < img.height(); y++) {
auto ptr = img.constScanLine(y);
for (int x = 0; x < img.width(); x++) {
s << *(ptr + x);
}
if (s.status() != QDataStream::Ok) {
return false;
}
}
return true;
}
bool TGAHandler::writeRGBA(const QImage &image)
{ {
QDataStream s(device()); QDataStream s(device());
s.setByteOrder(QDataStream::LittleEndian); s.setByteOrder(QDataStream::LittleEndian);
@ -504,6 +590,10 @@ bool TGAHandler::write(const QImage &image)
s << quint8(hasAlpha ? 32 : 24); // depth (24 bit RGB + 8 bit alpha) s << quint8(hasAlpha ? 32 : 24); // depth (24 bit RGB + 8 bit alpha)
s << quint8(hasAlpha ? originTopLeft + alphaChannel8Bits : originTopLeft); // top left image (0x20) + 8 bit alpha (0x8) s << quint8(hasAlpha ? originTopLeft + alphaChannel8Bits : originTopLeft); // top left image (0x20) + 8 bit alpha (0x8)
if (s.status() != QDataStream::Ok) {
return false;
}
for (int y = 0; y < img.height(); y++) { for (int y = 0; y < img.height(); y++) {
auto ptr = reinterpret_cast<const QRgb *>(img.constScanLine(y)); auto ptr = reinterpret_cast<const QRgb *>(img.constScanLine(y));
for (int x = 0; x < img.width(); x++) { for (int x = 0; x < img.width(); x++) {
@ -515,6 +605,9 @@ bool TGAHandler::write(const QImage &image)
s << quint8(qAlpha(color)); s << quint8(qAlpha(color));
} }
} }
if (s.status() != QDataStream::Ok) {
return false;
}
} }
return true; return true;

View File

@ -27,6 +27,10 @@ public:
static bool canRead(QIODevice *device); static bool canRead(QIODevice *device);
private: private:
bool writeIndexed(const QImage &image);
bool writeRGBA(const QImage &image);
const QScopedPointer<TGAHandlerPrivate> d; const QScopedPointer<TGAHandlerPrivate> d;
}; };