Compare commits
13 Commits
v6.14.0-rc
...
v6.16.0
Author | SHA1 | Date | |
---|---|---|---|
a9ccdce598 | |||
02cf1502c0 | |||
f6c718a789 | |||
4f2f2425d3 | |||
e6357c22f7 | |||
094177a01c | |||
7420f47c17 | |||
888bca7387 | |||
e3aefd2aa1 | |||
aa8134ee0d | |||
9f09473aa0 | |||
62eb1d28cb | |||
15bece40ec |
@ -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)
|
||||
|
||||
|
5
KF6ImageFormatsConfig.cmake.in
Normal 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
|
@ -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
|
||||
|
@ -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
@ -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.
|
BIN
autotests/read/iff/blue_noise_rgba16.iff
Normal file
BIN
autotests/read/iff/blue_noise_rgba16.png
Normal file
After Width: | Height: | Size: 103 KiB |
BIN
autotests/read/iff/blue_noise_rgba8.iff
Normal file
BIN
autotests/read/iff/blue_noise_rgba8.png
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
autotests/read/iff/flag_ham6.iff
Normal file
BIN
autotests/read/iff/flag_ham6.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
autotests/read/iff/ps_testcard_bitmap_amiga.iff
Normal file
BIN
autotests/read/iff/ps_testcard_bitmap_amiga.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
autotests/read/iff/ps_testcard_gray_amiga.iff
Normal file
BIN
autotests/read/iff/ps_testcard_gray_amiga.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
autotests/read/iff/ps_testcard_indexed_amiga.iff
Normal file
BIN
autotests/read/iff/ps_testcard_indexed_amiga.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
autotests/read/iff/ps_testcard_rgb16_maya.iff
Normal file
BIN
autotests/read/iff/ps_testcard_rgb16_maya.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
autotests/read/iff/ps_testcard_rgb_amiga.iff
Normal file
BIN
autotests/read/iff/ps_testcard_rgb_amiga.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
autotests/read/iff/ps_testcard_rgb_maya.iff
Normal file
BIN
autotests/read/iff/ps_testcard_rgb_maya.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
autotests/read/iff/ps_testcard_rgba16_maya.iff
Normal file
BIN
autotests/read/iff/ps_testcard_rgba16_maya.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
autotests/read/iff/ps_testcard_rgba_maya.iff
Normal file
BIN
autotests/read/iff/ps_testcard_rgba_maya.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
autotests/read/iff/testcard_indexed2_amiga.iff
Normal file
BIN
autotests/read/iff/testcard_indexed2_amiga.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
autotests/read/iff/testcard_indexed3_amiga.iff
Normal file
BIN
autotests/read/iff/testcard_indexed3_amiga.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
autotests/read/iff/testcard_indexed4_amiga.iff
Normal file
BIN
autotests/read/iff/testcard_indexed4_amiga.png
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
autotests/read/tga/colormapped.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
autotests/read/tga/colormapped.tga
Normal file
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 10 KiB |
@ -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
833
src/imageformats/chunks_p.h
Normal 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
@ -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"
|
4
src/imageformats/iff.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Keys": [ "iff" ],
|
||||
"MimeTypes": [ "application/x-iff" ]
|
||||
}
|
47
src/imageformats/iff_p.h
Normal 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
|
@ -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());
|
||||
}
|
||||
|
||||
|
111
src/imageformats/packbits_p.h
Normal 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
|
@ -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()) {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|