Compare commits

..

13 Commits

Author SHA1 Message Date
a9ccdce598 IFF: Fix possible stack overflow
(cherry picked from commit ea1983a7d1)
2025-07-11 15:13:32 +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
7420f47c17 Update dependency version to 6.15.0 2025-06-06 15:19:44 +02:00
888bca7387 cmake: add cmake config
That way, other projects can declare to have a runtime dependency on KImageFormats
2025-05-21 17:39:44 +02:00
e3aefd2aa1 JXR: Restore device position after reading options 2025-05-14 02:50:03 +02:00
aa8134ee0d README.md: minor fixes and improvements 2025-05-06 07:33:27 +02:00
9f09473aa0 It compiles fine without kf_6_13 deprecated methods 2025-05-04 21:31:49 +02:00
62eb1d28cb Add README about autotests 2025-05-03 11:01:46 +02:00
15bece40ec Update version to 6.15.0 2025-05-02 17:10:02 +02:00
47 changed files with 3223 additions and 68 deletions

View File

@ -1,11 +1,11 @@
cmake_minimum_required(VERSION 3.16)
set(KF_VERSION "6.14.0") # handled by release scripts
set(KF_DEP_VERSION "6.14.0") # handled by release scripts
set(KF_VERSION "6.16.0") # handled by release scripts
set(KF_DEP_VERSION "6.16.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION})
include(FeatureSummary)
find_package(ECM 6.14.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")
feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES)
@ -100,7 +100,7 @@ add_feature_info(LibJXR LibJXR_FOUND "required for the QImage plugin for JPEG XR
ecm_set_disabled_deprecation_versions(
QT 6.9.0
KF 6.12.0
KF 6.13.0
)
add_subdirectory(src)
@ -109,6 +109,28 @@ if (BUILD_TESTING)
add_subdirectory(tests)
endif()
set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF6ImageFormats")
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/KF6ImageFormatsConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY AnyNewerVersion
)
configure_package_config_file(
"${CMAKE_CURRENT_SOURCE_DIR}/KF6ImageFormatsConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/KF6ImageFormatsConfig.cmake"
INSTALL_DESTINATION "${CMAKECONFIG_INSTALL_DIR}"
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/KF6ImageFormatsConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/KF6ImageFormatsConfigVersion.cmake"
DESTINATION "${CMAKECONFIG_INSTALL_DIR}"
COMPONENT Devel
)
include(ECMFeatureSummary)
ecm_feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)

View File

@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2025 Xaver Hugl <xaver.hugl@gmail.com>
# SPDX-License-Identifier: BSD-2-Clause
@PACKAGE_INIT@
# empty, because this is plugins for Qt instead of a library to link against

View File

@ -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)
@ -53,8 +54,10 @@ willing to sign the Qt Project contributor agreement, it may be better to
submit the plugin directly to the Qt Project.
To be accepted, contributions must:
- Contain the test images needed to verify that the changes work correctly
- Pass the tests successfully
- Contain the test images needed to verify that the changes work correctly.
- Pass the tests successfully.
For more info about tests, see also [Autotests README](autotests/README.md).
## Duplicated Plugins
@ -220,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

View File

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

253
autotests/README.md Normal file
View File

@ -0,0 +1,253 @@
# Autotests
Automated testing for plugins to allow
[`QImage`](https://doc.qt.io/qt-6/qimage.html) to support
extra file formats.
## Introduction
The testing part is essential for the correct functioning of the plugins.
There are generic read/write tests and specific tests for the plugins that
require them.
## Read tests
The generic reading tests are contained in the `read` folder. Inside the
`read` folder, there are one or more folders for each plugin to be tested.
A plugin can support multiple image file types, and if you need different
parameters for each type, you need to create a folder for each type supported
by the plugin (see e.g. HEIF plugin). If all formats supported by the plugin
do not require different parameters, a single folder approach is simpler (see
e.g. PSD plugin).
The reading tests are mainly based on comparing the image read by the plugin
with a template in a known and working format. For this reason, the template
formats chosen are those distributed by the Qt project: PNG in the first
instance and TIFF for image formats not supported by PNG (e.g. CMYK images).
Some image options such as `QImageIOHandler::Size`,
`QImageIOHandler::ImageFormat` and `QImageIOHandler::ImageTransformation` are
also checked. If supported by the plugin, the resulting image is checked to
see if it is compatible with the option's specification.
Optionally, for each image, you can also create a JSON file to modify the test
behavior and/or verify data other than image pixels, such as metadata.
Finally, two tests are run for each test case:
- On a random access device: this test must not fail.
- On a sequential access device: a plugin may not support sequential operation.
In this case, the test is skipped. However, if an image is returned, the test
must succeed.
### The readtest command
To start a test, run the `readtest` command with the format to test as an
argument. The format is one of those supported by plugins and a folder with
the name of the format must be present inside the `read` folder.
Depending on the format, you can specify the following additional options.
- `--help`: Displays help on commandline options.
- `--fuzz <max>`: The fuzziness. Used to add some deviation in ARGB data
(nornally used on lossy codec).
- `--perceptive-fuzz`: Used to scale dynamically the fuzziness based on
the alpha channel value. This is useful on images with pre-multiplied and
small alphas. Qt can use different roundings based on optimizations resulting
in very different RGB values. Since the alpha is small visually there is no
difference (so it is not considered an error).
- `--skip-optional-tests`: Used to skip the optional test such as metadata
and resolution tests.
Note that some tests may fail if the correct options are not used. The correct
options for each test are defined in [CMakeLists.txt](CMakeLists.txt).
See also [Add a test to CMakeLists.txt](#add-a-test-to-cmakeliststxt).
### Test image nomenclature
Each test consists of the image to test, the verification image(s) (template)
and, optionally, the additional JSON file.
To be a test, the names of these files must be the same (except for
the extension). A test for a JXL image would be, for example, composed like
this:
- `testRGB.jxl`: The image to test.
- `testRGB.jxl.json`: The test behavior modifier (note that it must contain
the double extension).
- `testRGB.png`: How the image should look (template). The template name
can be different if specified in the JSON file.
Although there is no precise rule for the name of a test, it is good to have
a name that is explanatory.
### JSON behavior file
The behavior file was initially introduced to solve compatibility issues
between different versions of Qt supported by the framework. It was later
extended to also check image metadata.
The JSON file consists of an array of JSON objects. The objects in the array
are iterated sequentially and the first object that matches the requirements
is used for testing (successes are ignored).
Supported values for JSON objects:
- `comment`: Type string. A string shown by the test when a condition occurs.
- `description`: Type string. A description of the object. Not used by the
test.
- `disableAutoTransform`: Type boolean. By default, tests are run with
autotransform enabled (i.e. rotation is applied if the plugin supports it).
Set to `true` to disable autotransform.
- `filename`: Type string. Name of the template file to use. E.g.
"testRGB_Qt_6_2.png".
- `fuzziness`: Type integer. Set the fuzziness only if not already set on the
command line. The value set on the command line wins over the one in the JSON
file.
- `maxQtVersion`: Type string. Maximum Qt version this object is compatible
with (if not set means all). E.g. "6.2.99".
- `metadata`: Type Array. An array of key/value objects (string type)
containing the image metadata as returned by `QImage::text`.
- `minQtVersion`: Type string. Minimum Qt version this object is compatible
with (if not set means all). E.g. "6.2.0".
- `perceptiveFuzziness` Type boolean. Set the perceptive fuzziness only if not
already set on the command line. The value set on the command line wins over
the one in the JSON file.
- `resolution`: Type object. An object with the `dotsPerMeterX` and
`dotsPerMeterY` (integer) values of the image.
- `seeAlso`: Type string. More info about the object. Normally used to point
to bug reports. Not used by the test.
- `unsupportedFormat`: Type `boolean`. When true, the test is skipped.
Some examples:
- Example 1: [Runs only on Qt without alpha bug on float formats](read/jxl/testcard_rgba_fp16.jxl.json)
- Example 2: [Rotation disabled](read/jxl/orientation6_notranfs.jxl.json)
- Example 3: [Metadata](read/psd/metadata.psd.json)
- Example 4: [Check Qt version, resolution and metadata](read/psd/mch-16bits.psd.json)
- Example 5: [Fuzziness setting](read/xcf/birthday16.xcf.json)
These are just a few examples. More examples can be found in the test folders.
## Write tests
The generic writing tests are contained in the `write/basic` and
`write/format` folders. Similar to the read tests, they verify the written
(and then reread) image with a template.
The write test is composed of several phases:
- Basic test: Uses the `write/basic` folder and checks the most common images
and, optionally, metadata and resolution via a JSON properties file.
- Format test: Uses the `write/format` folder and checks that all QImage image
formats are written correctly.
- Dimensional test: Uses the `write/format` folder and check images of
different sizes (odd numbers, prime numbers, etc.) to verify internal
alignments.
- Null device test: Verify that there are no crashes if the device is null.
### The writetest command
To start a test, run the `writetest` command with the format to test as an
argument. The format is one of those supported by plugins, a folder with
the name of the format must be present inside the `write/format` folder and
may need a template image in `write/basic`.
Depending on the format, you can specify the following additional options.
- `--help`: Displays help on commandline options.
- `--create-format-templates`: Create template images for all formats
supported by the QImage in `write/format`. Command to simplify the creation of
format test images when adding a new plugin or modifying an old one. This
command is not intended to be used from the CMakeLists file as it must be used
manually and the generated images must be verified one by one.
- `--fuzz <max>`: The fuzziness. Used to add some deviation in ARGB data
(nornally used on lossy codec).
- `--lossless`: Check that reading back the data gives the same image.
- `--no-data-check`: Don't check that write data is exactly the same.
- `--skip-optional-tests`: Skip optional data tests (metadata, resolution,
etc...).
Note that some tests may fail if the correct options are not used. The correct
options for each test are defined in [CMakeLists.txt](CMakeLists.txt).
See also [Add a test to CMakeLists.txt](#add-a-test-to-cmakeliststxt).
### JSON properties file
The properties file must be located in `write/basic` and must have the name
of the file format (e.g. jxl.json). It is a JSON object composed of the
following values:
- `format`: Type string. The format tested.
- `metadata`: Type Array. An array of key/value objects (string type)
containing the image metadata as returned by `QImage::text`.
- `resolution`: Type object. An object with the `dotsPerMeterX` and `
dotsPerMeterY` (integer) values of the image.
[This is an example](write/basic/jxl.json) of property file.
## Custom tests
If the generic read/write tests do not meet the requirements of a plugin,
it is possible to write a custom test.
In general it makes sense to write a dedicated test for a format if and
only if you are testing unique features not present in other plugins.
### The PIC test
The PIC test is generated using Qt Test class. For more information
see [Qt Test](https://doc.qt.io/qt-6/qttest-index.html).
### The ANI test
The ANI test is generated using Qt Test class. For more information
see [Qt Test](https://doc.qt.io/qt-6/qttest-index.html).
## Add a test to CMakeLists.txt
To add a test to CMake use the `kimageformats_read_tests` and
`kimageformats_write_tests` functions. For example, to add the read
tests for the PSD you just write `kimageformats_read_tests(psd)`.
It is also possible to pass command line arguments to the test by
appropriately composing the string passed to the test functions.
For boolean parameters you need to add a string starting with '-'
after the image format. For example, to pass `--skip-optional-tests`
to the PSD plugin write `kimageformats_read_tests(psd-skipoptional)`.
To add a fuzziness of 4, you must first set it as follows:
`kimageformats_read_tests(FUZZ 4 psd-skipoptional)`.
The possible modifiers for `kimageformats_read_tests` are as follows:
- `-skipoptional`: Add the `--skip-optional-tests` command line parameter.
The possible modifiers for `kimageformats_write_tests` are as follows:
- `-lossless`: Add the `--lossless` command line parameter.
- `-nodatacheck`: Add the `--no-data-check` command line parameter.
- `-skipoptional`: Add the `--skip-optional-tests` command line parameter.
To set multiple parameters, you can enter multiple modifiers. For example:
```
kimageformats_write_tests(FUZZ 1
hej2-nodatacheck-lossless
)
```
## OSS-Fuzz
Plugins are also tested with [OSS-Fuzz](https://google.github.io/oss-fuzz/)
project to identify possible security issues. When adding a new plugin it is
also necessary to add it to the test in the [kimageformats
project](https://github.com/google/oss-fuzz/tree/master/projects/kimageformats)
on OSS-Fuzz.
## TODO
List of tests not implemented or only partially implemented.
### Color Profiles Test
Many plugins support color profiles via [`QColorSpace`](https://doc.qt.io/qt-6/qcolorspace.html).
Checking for correct color management is increasingly necessary especially now
that monitors are HDR and other than RGB color spaces have been added to Qt.
Furthermore, lossy plugins often have different color management behaviors
depending on how the image is saved.
### Animations Test
Few plugins support animations. There are currently no plans for
implementation.

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)
kimageformats_add_plugin(kimg_jxl SOURCES jxl.cpp microexif.cpp)
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

@ -243,6 +243,24 @@ public:
return true;
}
/*!
* \brief initForReadingAndRollBack
* Initialize the device for reading and rollback the device to start position.
* \param device The source device.
* \return True on success, otherwise false.
*/
bool initForReadingAndRollBack(QIODevice *device)
{
if (device) {
device->startTransaction();
}
auto ok = initForReading(device);
if (device) {
device->rollbackTransaction();
}
return ok;
}
/*!
* \brief jxrFormat
* \return The JXR format.
@ -1153,7 +1171,7 @@ QVariant JXRHandler::option(ImageOption option) const
QVariant v;
if (option == QImageIOHandler::Size) {
if (d->initForReading(device())) {
if (d->initForReadingAndRollBack(device())) {
auto size = d->imageSize();
if (size.isValid()) {
v = QVariant::fromValue(size);
@ -1162,7 +1180,7 @@ QVariant JXRHandler::option(ImageOption option) const
}
if (option == QImageIOHandler::ImageFormat) {
if (d->initForReading(device())) {
if (d->initForReadingAndRollBack(device())) {
v = QVariant::fromValue(d->imageFormat());
}
}
@ -1173,7 +1191,7 @@ QVariant JXRHandler::option(ImageOption option) const
if (option == QImageIOHandler::ImageTransformation) {
// ignore result: I might want to read the value set in writing
d->initForReading(device());
d->initForReadingAndRollBack(device());
v = int(d->transformation());
}

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 "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<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
* \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()) {

View File

@ -99,7 +99,12 @@ static bool IsSupported(const TgaHeader &head)
return false;
}
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;
}
}
@ -189,6 +194,8 @@ static QImage::Format imageFormat(const TgaHeader &head)
if (numAlphaBits == 8) {
format = QImage::Format_ARGB32;
}
} else if (head.image_type == TGA_TYPE_INDEXED || head.image_type == TGA_TYPE_RLE_INDEXED) {
format = QImage::Format_Indexed8;
} else {
format = QImage::Format_RGB32;
}
@ -232,21 +239,40 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
}
// Read palette.
static const int max_palette_size = 768;
char palette[max_palette_size];
if (info.pal) {
// @todo Support palettes in other formats!
const int palette_size = 3 * tga.colormap_length;
if (palette_size > max_palette_size) {
QList<QRgb> colorTable;
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
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;
}
// BGRA.
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;
}
// BGR.
rgb = qRgb(data[2], data[1], data[0]);
}
// TODO tga.colormap_size == 16 ARRRRRGG GGGBBBBB
} else {
return false;
}
const int dataRead = s.readRawData(palette, palette_size);
if (dataRead < 0) {
return false;
}
if (dataRead < max_palette_size) {
memset(&palette[dataRead], 0, max_palette_size - dataRead);
}
img.setColorTable(colorTable);
}
// Allocate image.
@ -355,14 +381,19 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
uchar *src = image;
for (int y = y_start; y != y_end; y += y_step) {
auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
if (info.pal) {
// Paletted.
auto scanline = img.scanLine(y);
for (int x = 0; x < tga.width; x++) {
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) {
auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
// Greyscale.
for (int x = 0; x < tga.width; x++) {
if (tga.pixel_size == 16) {
@ -375,6 +406,7 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
}
}
} else {
auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
// True Color.
if (tga.pixel_size == 16) {
for (int x = 0; x < tga.width; x++) {
@ -401,7 +433,7 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
// Free image.
free(image);
return true;
return valid;
}
} // namespace
@ -469,6 +501,59 @@ bool TGAHandler::read(QImage *outImage)
}
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());
s.setByteOrder(QDataStream::LittleEndian);
@ -504,6 +589,10 @@ bool TGAHandler::write(const QImage &image)
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)
if (s.status() != QDataStream::Ok) {
return false;
}
for (int y = 0; y < img.height(); y++) {
auto ptr = reinterpret_cast<const QRgb *>(img.constScanLine(y));
for (int x = 0; x < img.width(); x++) {
@ -515,6 +604,9 @@ bool TGAHandler::write(const QImage &image)
s << quint8(qAlpha(color));
}
}
if (s.status() != QDataStream::Ok) {
return false;
}
}
return true;

View File

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