Compare commits

..

34 Commits

Author SHA1 Message Date
799ac37660 Update dependency version to 6.8.0 2024-11-02 16:13:04 +01:00
0378bd67e1 TGA: Fixed GrayA image loading error
Gray TGA images with alpha were loading incorrectly and tests did not detect the error since the BW(A).TGA images were actually RGB(A) images.
2024-10-24 15:07:44 +00:00
7d696a81d2 exr: Fix read/write with openexr 3.3
It really wants to have a filename

Also it uses seek and tell a lot so sequential devices are for now not
supported

BUGS: 494571
2024-10-12 01:47:22 +02:00
b5d5abe0ea JXL improvements
Highlights of the patch:
- Supersede MR !249
- Added FP16 and FP32 images support thus preserving HDR values (read / write, required libjxl 0.9+).
- Added Gray8 and Gray16 support (read / write).
- Indexed images are saved as Gray8 when palette is gray scale.
- Binary images are saved as Gray8 (does JXL natively support binary images?).
- Simplified writing process by partially removing the use of additional buffers.
- Added XMP metadata support by decoding/encoding Boxes.
- Changed maximum image size in pixels in accordance with JXL feature level 5 (still limited to 256 megapixels).

Compatibility: 
- Older versions of this plugin load FP images correctly as UINT16 (obviously losing HDR info).
- HDR images saved with this patch are also loaded correctly by Gimp and Photoshop.
- Grayscale images saved with this patch are also loaded correctly by Gimp and Photoshop.

Compilation modifiers for cmake file:
- `JXL_HDR_PRESERVATION_DISABLED`: disable the FP support (behaves like previous versions).
- `JXL_DECODE_BOXES_DISABLED`: disable metadata reading (behaves like previous versions).
2024-10-11 12:42:42 +00:00
3f4690d1e9 Update version to 6.8.0 2024-10-11 13:35:32 +02:00
ac1006cc66 JXR: Fixed image reading on sequential devices
The following changes are done:
- Fixed an error when copying image from sequential device
- Return error when the image exceed 4GB size on writing*
- Enabled JXR tests

(*) Note that when writing an image larger than 4GiB, the JXRLib does not give any error but the resulting saved image id wrong.
2024-10-09 21:34:32 +00:00
97120b2537 Simplified read/verify header process
Where possible, QIODevice::peek has been used instead of transactions or instead of using ungetchar() for sequential access devices and seek() for random access devices.

Furthermore:
- RAS format gained the ability of read on sequential devices.
- Removed unused code in XCF (still related to ungetchar and sequential devices).
- These changes should prevent errors like the ones fixed by MR !258
2024-10-06 17:26:25 +00:00
fee0165bef Update dependency version to 6.7.0 2024-10-04 16:57:57 +02:00
ae641f7e94 Fix endianness bug in PCX reader on big endian architectures
When reading from a sequential device, the peekHeader() method in
the PCX readers reads the header its defined little endian into
arch-specific endianness for multibyte types.

Being a "peek" method, it then it tries to push back the bytes into
the device after reading for its next use, but it doesn’t convert
multibyte types correctly from arch-specifice endianness to the
initial little endian format.

Subsequent reading of the data from the device will thus lead to
incorrect values for multibyte types on the next use.

This patch reuses the same technique as the TGA reader to read the
whole header as bytes before deserializing it, so that the bytes
can be pushed back into the sequential device in the same order.
2024-09-22 01:31:13 +02:00
46f7b90ce6 Fixed read of BGR32 and RGB555 formats 2024-09-16 17:16:28 +02:00
f7c8eaa140 FIxed comparison of unsigned expression
Fix of [Issue 9](https://invent.kde.org/frameworks/kimageformats/-/issues/9)

Same of MR !253 to solve the `Fix of [Issue 9](https://invent.kde.org/frameworks/kimageformats/-/issues/9)` when rebasing.
2024-09-15 15:00:11 +00:00
36bfee8ae3 raw: Getting the image size does not need unpacking
According to the libraw documentation, the sizes are available directly
after open_datastream.
2024-09-13 19:17:31 +02:00
e2aaf89ec5 Update version to 6.7.0 2024-09-06 14:21:04 +02:00
989a5c70d6 Update version to 6.6.0 2024-09-06 13:28:36 +02:00
8588c053b6 XCF: fix crash 2024-08-27 21:52:16 +00:00
145dedf360 README update
Added some information about plugins and their behaviors (when they do particular things).
2024-08-26 15:18:57 +00:00
2405a09e36 RGB: added options support
- Added support for `Size` and `Format` options and slightly improved format detection from canRead().
- Removed conversion to ARGB32 on load (improved performace with RGBA images).
- Added result checks on writing.

With this MR, all plugins have minimal support for options.
2024-08-25 21:00:08 +00:00
d02dcb064b PCX: added options support
- Added support for ```Size``` and ```Format``` options and slightly improved format detection from canRead().
- Added PCXHEADER::isValid() method to consolidate header consistency checks in one place.
2024-08-17 06:40:29 +00:00
0590c6b49d Update version to 6.6.0 2024-08-09 12:58:58 +02:00
eb46f0f421 Fix crash on malformed files
Co-authored-by: Mirco Miranda <mircomir@gmail.com>
2024-08-05 22:15:06 +00:00
8c23e74ef6 Update dependency version to 6.5.0 2024-08-02 12:56:31 +02:00
219d9cb2c2 JXL: added ImageTransformation option
Let Qt rotate the image when the ImageAutotransform option is set to true.

In tests it also solves the image size control with the value returned by the options with certain rotations.
2024-07-30 22:46:52 +00:00
51921e8ee5 xcf: Fix crash on malformed files 2024-07-29 20:53:13 +02:00
23e9fec869 pcx: Fix crash in broken files 2024-07-26 16:53:32 +02:00
4478bc8d2b xcf: Fix crash on broken files 2024-07-25 00:04:38 +02:00
acd6b3970c pcx: fix crash on invalid files 2024-07-23 00:22:08 +02:00
638fdfcbdd pcx: fix crash on invalid files
Added new PCX testfile for readtest.
2024-07-22 22:13:14 +00:00
a497ab789b exr: added some usefull attributes 2024-07-21 04:52:18 +00:00
3590a43fc5 pcx: Read 16 color images that are 4bpp and 1 plane
We had code for 1bpp and 4 planes but gimp is generating this format
2024-07-19 21:30:59 +00:00
f5a6de7280 Full range HDR support
EXR, HDR, JXR and PFM formats support High Dynamic Range images (FP values grater than 1).

In summary, here is the list of changes:

    EXR, HDR, JXR and PFM: When working with FP formats, the clamp between 0 and 1 ​​is no longer done.
    EXR: Removed old SDR code and conversions. Due to the lack of a QImage Gray FP format, Gray images are output as RGB FP (recently added code for Qt 6.8 has been removed).
    PFM: Due to the lack of a QImage Gray FP format, Gray images are output as RGB FP.
    HDR: Added rotation and exposure support.

With this patch, EXR, JXR, HDR, PFM behave like Qt's TIFF plugin when working with FP images.
2024-07-17 22:24:57 +02:00
4c0f49295b Prepare gitlab for files that are coming int the next commit 2024-07-17 22:24:33 +02:00
e9da5edb9a avif: check return values
Some libavif calls did not return values in the older versions.
Situation changed meanwhile;
we can check the return values for error conditions now.
2024-07-15 17:46:21 +02:00
e10f5aa9a5 raw: Fix compiler warning with macro redefinition in Windows
This fixes the following compiler warning with mingw-w64 toolchain in Windows.

raw.cpp:436:9: warning: "DEFAULT_QUALITY" redefined
  436 | #define DEFAULT_QUALITY (C_IQ(3) | C_OC(1) | C_CW(1) | C_AW(1) | C_BT(1) | C_HS(0) | C_FLAGS(1))
      |         ^~~~~~~~~~~~~~~
wingdi.h:1142:9: note: this is the location of the previous definition
 1142 | #define DEFAULT_QUALITY 0
      |         ^~~~~~~~~~~~~~~

DEFAULT_QUALITY macro is used with CreateFontW API.
https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createfontw
2024-07-14 04:14:18 +00:00
14020a23d5 Update version to 6.5.0 2024-07-12 13:27:10 +02:00
69 changed files with 1928 additions and 830 deletions

12
.gitattributes vendored
View File

@ -1 +1,13 @@
autotests/read/raw/RAW_KODAK_C330_FORMAT_NONE_YRGB.raw binary
autotests/read/hdr/orientation1.hdr binary
autotests/read/hdr/orientation2.hdr binary
autotests/read/hdr/orientation3.hdr binary
autotests/read/hdr/orientation4.hdr binary
autotests/read/hdr/orientation5.hdr binary
autotests/read/hdr/orientation6.hdr binary
autotests/read/hdr/orientation7.hdr binary
autotests/read/hdr/orientation8.hdr binary
autotests/read/hdr/fake_earth.hdr binary
autotests/read/hdr/rgb.hdr binary
autotests/read/hdr/rgb-landscape.hdr binary
autotests/read/hdr/rgb-portrait.hdr binary

View File

@ -7,3 +7,4 @@ Dependencies:
Options:
test-before-installing: True
require-passing-tests-on: [ 'Linux', 'FreeBSD', 'Windows' ]
cmake-options: "-DKIMAGEFORMATS_JXR=ON"

View File

@ -1,11 +1,11 @@
cmake_minimum_required(VERSION 3.16)
set(KF_VERSION "6.4.0") # handled by release scripts
set(KF_DEP_VERSION "6.4.0") # handled by release scripts
set(KF_VERSION "6.8.0") # handled by release scripts
set(KF_DEP_VERSION "6.8.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION})
include(FeatureSummary)
find_package(ECM 6.4.0 NO_MODULE)
find_package(ECM 6.8.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)

165
README.md
View File

@ -1,6 +1,6 @@
# KImageFormats
Plugins to allow QImage to support extra file formats.
Plugins to allow `QImage` to support extra file formats.
## Introduction
@ -28,9 +28,9 @@ The following image formats have read and write support:
- AV1 Image File Format (avif)
- Encapsulated PostScript (eps)
- High Efficiency Image File Format (heif). Can be enabled with the KIMAGEFORMATS_HEIF build option.
- High Efficiency Image File Format (heif)
- JPEG XL (jxl)
- JPEG XR (jxr). Can be enabled with the KIMAGEFORMATS_JXR build option.
- JPEG XR (jxr)
- OpenEXR (exr)
- Personal Computer Exchange (pcx)
- Quite OK Image format (qoi)
@ -40,7 +40,7 @@ The following image formats have read and write support:
## Contributing
See the QImageIOPlugin documentation for information on how to write a
See the [`QImageIOPlugin`](https://doc.qt.io/qt-6/qimageioplugin.html) documentation for information on how to write a
new plugin.
The main difference between this framework and the qimageformats module
@ -68,3 +68,160 @@ This framework is licensed under the
The CMake code in this framework is licensed under the
[BSD license](http://opensource.org/licenses/BSD-3-Clause).
## Plugin status
The current implementation of a plugin may not be complete or may have limitations
of various kinds. Typically the limitations are on maximum size and color depth.
The various plugins are also limited by the formats natively supported by Qt.
For example, native support for CMYK images is only available since Qt 6.8.
### HDR images
HDR images are supported via floating point image formats from EXR, HDR, JXL,
JXR, PFM and PSD plugins.
It is important to note that in the past these plugins stripped away HDR
information, returning SDR images.
HDR images return R, G and B values outside the range 0.0 - 1.0.
While Qt painters handles HDR data correctly, some older programs may display
strange artifacts if they do not use a tone mapping operator (or at least a
clamp). This is not a plugin issue.
### Metadata
Metadata support is implemented in all formats that support it. In particular,
in addition to the classic `"Description"`, `"Author"`, `"Copyright"`, etc... where
possible, XMP data is supported via the `"XML:com.adobe.xmp"` key.
Please note that only the most common metadata is supported.
### ICC profile support
ICC support is fully implemented in all formats that support it. When saving,
some formats convert the image using color profiles according to
specifications. In particular, HDR formats almost always convert to linear
RGB.
### Maximum image size
Where possible, plugins support large images. By convention, many of the
large image plugins are limited to a maximum of 300,000 x 300,000 pixels.
Anyway, all plugins are also limited by the
`QImageIOReader::allocationLimit()`. Below are the maximum sizes for each
plugin ('n/a' means no limit, i.e. the limit depends on the format encoding).
- ANI: n/a
- AVIF: 32,768 x 32,768 pixels, in any case no larger than 256 megapixels
- EXR: 300,000 x 300,000 pixels
- HDR: n/a (large image)
- HEIF: n/a
- 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
- PCX: 65,535 x 65,535 pixels
- PFM: n/a (large image)
- PIC: 65,535 x 65,535 pixels
- PSD: 300,000 x 300,000 pixels
- PXR: 65,535 x 65,535 pixels
- QOI: 300,000 x 300,000 pixels
- RAS: n/a (large image)
- RAW: n/a (depends on the RAW format loaded)
- RGB: 65,535 x 65,535 pixels
- TGA: 65,535 x 65,535 pixels
- XCF: 300,000 x 300,000 pixels
### Sequential and random access devices
All plugins work fine on random access devices while only some work on
sequential access devices.
Some plugins, such as PSD, allow reading RGB images on sequential access
devices, but cannot do the same for Lab files.
**Important: some plugins use `QIODevice` transactions and/or
`QIODevice::ungetChar()`. Therefore, the device used to read the image must not
have any active transactions.**
### Memory usage
Qt has added many image formats over time. In older plugins, to support new
formats, `QImage` conversion functions have been used, causing memory
consumption proportional to the size of the image to be saved.
Normally this is not a source of problems because the affected plugins
are limited to maximum images of 2GiB or less.
On plugins for formats that support large images, progressive conversion has
been used or the maximum size of the image that can be saved has been limited.
### Non-RGB formats
PSD plugin loads CMYK, Lab and Multichannel images and converts them to RGB
without using the ICC profile.
JXR and PSD plugins natively support 4-channel CMYK images when compiled
with Qt 6.8+.
### The HEIF plugin
**This plugin is disabled by default. It can be enabled with the
`KIMAGEFORMATS_HEIF` build option in the cmake file.**
### The EXR plugin
The following defines can be defined in cmake to modify the behavior of the plugin:
- `EXR_CONVERT_TO_SRGB`: the linear data is converted to sRGB on read to accommodate programs that do not support color profiles.
- `EXR_DISABLE_XMP_ATTRIBUTE`: disables the stores XMP values in a non-standard attribute named "xmp". Note that Gimp reads the "xmp" attribute and Darktable writes it as well.
### The HDR plugin
The following defines can be defined in cmake to modify the behavior of the plugin:
- `HDR_HALF_QUALITY`: on read, a 16-bit float image is returned instead of a 32-bit float one.
### The JXL plugin
**The current version of the plugin limits the image size to 256 megapixels
according to feature level 5 of the JXL stream encoding.**
The following defines can be defined in cmake to modify the behavior of the plugin:
- `JXL_HDR_PRESERVATION_DISABLED`: disable floating point images (both read and write) by converting them to UINT16 images. Any HDR data is lost. Note that FP images are always disabled when compiling with libJXL less than v0.9.
- `JXL_DECODE_BOXES_DISABLED`: disable reading of metadata (e.g. XMP).
### The JXR plugin
**This plugin is disabled by default. It can be enabled with the
`KIMAGEFORMATS_JXR` build option in the cmake file.**
The following defines can be defined in cmake to modify the behavior of the plugin:
- `JXR_DENY_FLOAT_IMAGE`: disables the use of float images and consequently any HDR data will be lost.
- `JXR_DISABLE_DEPTH_CONVERSION`: remove the neeeds of additional memory by disabling the conversion between different color depths (e.g. RGBA64bpp to RGBA32bpp) at the cost of reduced compatibility.
- `JXR_DISABLE_BGRA_HACK`: Windows displays and opens JXR files correctly out of the box. Unfortunately it doesn't seem to open (P)RGBA @32bpp files as it only wants (P)BGRA32bpp files (a format not supported by Qt). Only for this format an hack is activated to guarantee total compatibility of the plugin with Windows.
- `JXR_ENABLE_ADVANCED_METADATA`: enable metadata support (e.g. XMP). Some distributions use an incomplete JXR library that does not allow reading metadata, causing compilation errors.
### The PSD plugin
PSD support has the following limitations:
- Only images saved by Photoshop using compatibility mode enabled (Photoshop default) can be decoded.
- Multichannel images are treated as CMY/CMYK and are only loaded if they have 3 or more channels.
- Duotone images are treated as grayscale images.
- Extra channels other than alpha are discarded.
The following defines can be defined in cmake to modify the behavior of the plugin:
- `PSD_FAST_LAB_CONVERSION`: the LAB image is converted to linear sRGB instead of sRGB which significantly increases performance.
- `PSD_NATIVE_CMYK_SUPPORT_DISABLED`: disable native support for CMYK images when compiled with Qt 6.8+
### The RAW plugin
Loading RAW images always requires a conversion. To allow the user to
choose how to convert the image, it was chosen to use the quality parameter
to act on the converter. The quality parameter can be used with values from
0 to 100 (0 = fast, 100 = maximum quality) or by setting flags to
selectively change the conversion (see also [raw_p.h](./src/imageformats/raw_p.h)).
The default setting tries to balance quality and conversion speed.
### The XCF plugin
XCF support has the following limitations:
- XCF format up to [version 12](https://testing.developer.gimp.org/core/standards/xcf/#version-history) (no support for GIMP 3).
- The returned image is always 8-bit.
- Cannot read zlib compressed files.
- The rendered image may be slightly different (colors/transparencies) than in GIMP.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
[
{
"fileName" : "orientation_all.png"
}
]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
[
{
"fileName" : "orientation_all.png"
}
]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
[
{
"fileName" : "orientation_all.png"
}
]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
[
{
"fileName" : "orientation_all.png"
}
]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
[
{
"fileName" : "orientation_all.png"
}
]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
[
{
"fileName" : "orientation_all.png"
}
]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
[
{
"fileName" : "orientation_all.png"
}
]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
[
{
"fileName" : "orientation_all.png"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

View File

@ -0,0 +1,19 @@
[
{
"minQtVersion" : "6.5.7",
"maxQtVersion" : "6.5.99",
"disableAutoTransform": true,
"fileName" : "orientation6_notranfs.png",
"comment" : "Test with automatic transformation disabled."
},
{
"minQtVersion" : "6.7.3",
"disableAutoTransform": true,
"fileName" : "orientation6_notranfs.png",
"comment" : "Test with automatic transformation disabled."
},
{
"unsupportedFormat" : true,
"comment" : "It is not possible to disable the transformation with the current version of the plugin."
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 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: 5.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: 11 KiB

Binary file not shown.

View File

@ -0,0 +1,21 @@
[
{
"minQtVersion" : "6.2.11",
"maxQtVersion" : "6.2.99",
"fileName" : "testcard_rgba_fp16.png"
},
{
"minQtVersion" : "6.5.5",
"maxQtVersion" : "6.5.99",
"fileName" : "testcard_rgba_fp16.png"
},
{
"minQtVersion" : "6.6.2",
"fileName" : "testcard_rgba_fp16.png"
},
{
"unsupportedFormat" : true,
"comment" : "Skipped due to QTBUG-120614.",
"seeAlso" : "https://bugreports.qt.io/browse/QTBUG-120614"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

View File

@ -0,0 +1,21 @@
[
{
"minQtVersion" : "6.2.11",
"maxQtVersion" : "6.2.99",
"fileName" : "testcard_rgba_fp32.png"
},
{
"minQtVersion" : "6.5.5",
"maxQtVersion" : "6.5.99",
"fileName" : "testcard_rgba_fp32.png"
},
{
"minQtVersion" : "6.6.2",
"fileName" : "testcard_rgba_fp32.png"
},
{
"unsupportedFormat" : true,
"comment" : "Skipped due to QTBUG-120614.",
"seeAlso" : "https://bugreports.qt.io/browse/QTBUG-120614"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 485 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 737 B

View File

@ -136,7 +136,7 @@ public:
}
if (reader->supportsOption(QImageIOHandler::ImageTransformation)) {
m_transformations = reader->transformation();
if (m_transformations < 0 || m_transformations > 7)
if (int(m_transformations) < 0 || int(m_transformations) > 7)
ok = false;
}
return ok;
@ -177,7 +177,8 @@ public:
{
bool ok = true;
if (!m_size.isEmpty()) {
ok = ok && (m_size == image.size());
// Size option return the size without transformation (tested with Qt TIFF plugin).
ok = ok && (m_size == image.size() || m_size == image.size().transposed());
}
if (m_format != QImage::Format_Invalid) {
ok = ok && (m_format == image.format());
@ -266,10 +267,13 @@ int main(int argc, char **argv)
continue;
}
bool skipTest = false;
QFileInfo expFileInfo = timg.compareImage(skipTest);
if (skipTest) {
QTextStream(stdout) << "SKIP : " << fi.fileName() << ": image format not supported by current Qt version!\n";
TemplateImage::TestFlags flags = TemplateImage::None;
QString comment;
QFileInfo expFileInfo = timg.compareImage(flags, comment);
if ((flags & TemplateImage::SkipTest) == TemplateImage::SkipTest) {
if(comment.isEmpty())
comment = QStringLiteral("image format not supported by current Qt version!");
QTextStream(stdout) << "SKIP : " << fi.fileName() << QStringLiteral(": %1\n").arg(comment);
++skipped;
continue;
}
@ -290,7 +294,7 @@ int main(int argc, char **argv)
QImage expImage;
// inputImage is auto-rotated to final orientation
inputReader.setAutoTransform(true);
inputReader.setAutoTransform((flags & TemplateImage::DisableAutotransform) != TemplateImage::DisableAutotransform);
if (!expReader.read(&expImage)) {
QTextStream(stdout) << "ERROR: " << fi.fileName() << ": could not load " << expfilename << ": " << expReader.errorString() << "\n";

View File

@ -28,10 +28,10 @@ bool TemplateImage::isTemplate() const
return false;
}
QFileInfo TemplateImage::compareImage(bool &skipTest) const
QFileInfo TemplateImage::compareImage(TestFlags &flags, QString& comment) const
{
auto fi = jsonImage(skipTest);
if (skipTest) {
auto fi = jsonImage(flags, comment);
if ((flags & TestFlag::SkipTest) == TestFlag::SkipTest) {
return {};
}
if (fi.exists()) {
@ -58,8 +58,9 @@ QFileInfo TemplateImage::legacyImage() const
return {};
}
QFileInfo TemplateImage::jsonImage(bool &skipTest) const
QFileInfo TemplateImage::jsonImage(TestFlags &flags, QString& comment) const
{
flags = TestFlag::None;
auto fi = QFileInfo(QStringLiteral("%1.json").arg(m_fi.filePath()));
if (!fi.exists()) {
return {};
@ -86,6 +87,10 @@ QFileInfo TemplateImage::jsonImage(bool &skipTest) const
auto maxQt = QVersionNumber::fromString(obj.value("maxQtVersion").toString());
auto name = obj.value("fileName").toString();
auto unsupportedFormat = obj.value("unsupportedFormat").toBool();
comment = obj.value("comment").toString();
if(obj.value("disableAutoTransform").toBool())
flags |= TestFlag::DisableAutotransform;
// filter
if (name.isEmpty() && !unsupportedFormat)
@ -95,7 +100,7 @@ QFileInfo TemplateImage::jsonImage(bool &skipTest) const
if (!maxQt.isNull() && currentQt > maxQt)
continue;
if (unsupportedFormat) {
skipTest = true;
flags |= TestFlag::SkipTest;
break;
}
return QFileInfo(QStringLiteral("%1/%2").arg(fi.path(), name));

View File

@ -16,6 +16,13 @@
class TemplateImage
{
public:
enum TestFlag {
None = 0x0,
SkipTest = 0x1,
DisableAutotransform = 0x2
};
Q_DECLARE_FLAGS(TestFlags, TestFlag)
/*!
* \brief TemplateImage
* \param fi The image to test.
@ -42,10 +49,10 @@ public:
/*!
* \brief compareImage
* \param skipTest True if the test should be skipped (e.g. image format not supported by current Qt version).
* \param flags Flags for modifying test behavior (e.g. image format not supported by current Qt version).
* \return The template image to use for the comparison.
*/
QFileInfo compareImage(bool &skipTest) const;
QFileInfo compareImage(TestFlags &flags, QString& comment) const;
/*!
* \brief suffixes
@ -62,13 +69,15 @@ private:
/*!
* \brief jsonImage
* \param skipTest True if the test should be skipped (not supported).
* \param flags Flags for modifying test behavior.
* \return The template image read from the corresponding JSON.
*/
QFileInfo jsonImage(bool &skipTest) const;
QFileInfo jsonImage(TestFlags &flags, QString& comment) const;
private:
QFileInfo m_fi;
};
Q_DECLARE_OPERATORS_FOR_FLAGS(TemplateImage::TestFlags)
#endif // TEMPLATEIMAGE_H

View File

@ -619,7 +619,15 @@ bool QAVIFHandler::write(const QImage &image)
QImage tmpgrayimage = image.convertToFormat(tmpformat);
avif = avifImageCreate(tmpgrayimage.width(), tmpgrayimage.height(), save_depth, AVIF_PIXEL_FORMAT_YUV400);
#if AVIF_VERSION >= 110000
res = avifImageAllocatePlanes(avif, AVIF_PLANES_YUV);
if (res != AVIF_RESULT_OK) {
qWarning("ERROR in avifImageAllocatePlanes: %s", avifResultToString(res));
return false;
}
#else
avifImageAllocatePlanes(avif, AVIF_PLANES_YUV);
#endif
if (tmpgrayimage.colorSpace().isValid()) {
avif->colorPrimaries = (avifColorPrimaries)1;
@ -806,7 +814,15 @@ bool QAVIFHandler::write(const QImage &image)
avif->transferCharacteristics = transfer_to_save;
if (iccprofile.size() > 0) {
#if AVIF_VERSION >= 1000000
res = avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());
if (res != AVIF_RESULT_OK) {
qWarning("ERROR in avifImageSetProfileICC: %s", avifResultToString(res));
return false;
}
#else
avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());
#endif
}
avifRGBImage rgb;
@ -971,6 +987,8 @@ bool QAVIFHandler::jumpToNextImage()
return false;
}
avifResult decodeResult;
if (m_decoder->imageIndex >= 0) {
if (m_decoder->imageCount < 2) {
m_parseState = ParseAvifSuccess;
@ -978,11 +996,16 @@ bool QAVIFHandler::jumpToNextImage()
}
if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { // start from beginning
avifDecoderReset(m_decoder);
decodeResult = avifDecoderReset(m_decoder);
if (decodeResult != AVIF_RESULT_OK) {
qWarning("ERROR in avifDecoderReset: %s", avifResultToString(decodeResult));
m_parseState = ParseAvifError;
return false;
}
}
}
avifResult decodeResult = avifDecoderNextImage(m_decoder);
decodeResult = avifDecoderNextImage(m_decoder);
if (decodeResult != AVIF_RESULT_OK) {
qWarning("ERROR: Failed to decode Next image in sequence: %s", avifResultToString(decodeResult));

View File

@ -7,20 +7,11 @@
SPDX-License-Identifier: LGPL-2.0-or-later
*/
/* *** EXR_USE_LEGACY_CONVERSIONS ***
* If defined, the result image is an 8-bit RGB(A) converted
* without icc profiles. Otherwise, a 16-bit images is generated.
* NOTE: The use of legacy conversions are discouraged due to
* imprecise image result.
*/
//#define EXR_USE_LEGACY_CONVERSIONS // default commented -> you should define it in your cmake file
/* *** EXR_CONVERT_TO_SRGB ***
* If defined, the linear data is converted to sRGB on read to accommodate
* programs that do not support color profiles.
* Otherwise the data are kept as is and it is the display program that
* must convert to the monitor profile.
* NOTE: If EXR_USE_LEGACY_CONVERSIONS is active, this is ignored.
*/
//#define EXR_CONVERT_TO_SRGB // default: commented -> you should define it in your cmake file
@ -92,23 +83,11 @@
#include <QThread>
#include <QTimeZone>
// Allow the code to works on all QT versions supported by KDE
// project (Qt 5.15 and Qt 6.x) to easy backports fixes.
#if !defined(EXR_USE_LEGACY_CONVERSIONS)
// If uncommented, the image is rendered in a float16 format, the result is very precise
#define EXR_USE_QT6_FLOAT_IMAGE // default uncommented
#endif
// Qt 6.8 allow to create and use Gray profile, so we can load a Gray image as Grayscale format instead RGB one.
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
#define EXR_GRAY_SUPPORT_ENABLED
#endif
class K_IStream : public Imf::IStream
{
public:
K_IStream(QIODevice *dev, const QByteArray &fileName)
: IStream(fileName.data())
K_IStream(QIODevice *dev)
: IStream("K_IStream")
, m_dev(dev)
{
}
@ -166,8 +145,8 @@ void K_IStream::clear()
class K_OStream : public Imf::OStream
{
public:
K_OStream(QIODevice *dev, const QByteArray &fileName)
: OStream(fileName.data())
K_OStream(QIODevice *dev)
: OStream("K_OStream")
, m_dev(dev)
{
}
@ -214,22 +193,6 @@ void K_OStream::seekg(Imf::Int64 pos)
m_dev->seek(pos);
}
#ifdef EXR_USE_LEGACY_CONVERSIONS
// source: https://openexr.com/en/latest/ReadingAndWritingImageFiles.html
inline unsigned char gamma(float x)
{
x = std::pow(5.5555f * std::max(0.f, x), 0.4545f) * 84.66f;
return (unsigned char)qBound(0.f, x, 255.f);
}
inline QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel)
{
return qRgba(gamma(float(imagePixel.r)),
gamma(float(imagePixel.g)),
gamma(float(imagePixel.b)),
(unsigned char)(qBound(0.f, imagePixel.a * 255.f, 255.f) + 0.5f));
}
#endif
EXRHandler::EXRHandler()
: m_compressionRatio(-1)
, m_quality(-1)
@ -253,18 +216,7 @@ bool EXRHandler::canRead() const
static QImage::Format imageFormat(const Imf::RgbaInputFile &file)
{
auto isRgba = file.channels() & Imf::RgbaChannels::WRITE_A;
#ifdef EXR_GRAY_SUPPORT_ENABLED
auto isGray = file.channels() & Imf::RgbaChannels::WRITE_Y;
#else
auto isGray = false;
#endif
#if defined(EXR_USE_LEGACY_CONVERSIONS)
return (isRgba ? QImage::Format_ARGB32 : QImage::Format_RGB32);
#elif defined(EXR_USE_QT6_FLOAT_IMAGE)
return (isRgba ? QImage::Format_RGBA16FPx4 : isGray ? QImage::Format_Grayscale16 : QImage::Format_RGBX16FPx4);
#else
return (isRgba ? QImage::Format_RGBA64 : QImage::Format_RGBX64);
#endif
return (isRgba ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4);
}
/*!
@ -345,23 +297,27 @@ static void readMetadata(const Imf::Header &header, QImage &image)
image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromStdString(xmp->value()));
}
/* TODO: OpenEXR 3.2 metadata
*
* New Optional Standard Attributes:
* - Support automated editorial workflow:
* reelName, imageCounter, ascFramingDecisionList
*
* - Support forensics (“which other shots used that camera and lens before the camera firmware was updated?”):
* cameraMake, cameraModel, cameraSerialNumber, cameraFirmware, cameraUuid, cameraLabel, lensMake, lensModel,
* lensSerialNumber, lensFirmware, cameraColorBalance
*
* -Support pickup shots (reproduce critical camera settings):
* shutterAngle, cameraCCTSetting, cameraTintSetting
*
* - Support metadata-driven match move:
* sensorCenterOffset, sensorOverallDimensions, sensorPhotositePitch, sensorAcquisitionRectanglenominalFocalLength,
* effectiveFocalLength, pinholeFocalLength, entrancePupilOffset, tStop(complementing existing 'aperture')
*/
// camera metadata
if (auto manufacturer = header.findTypedAttribute<Imf::StringAttribute>("cameraMake")) {
image.setText(QStringLiteral(META_KEY_MANUFACTURER), QString::fromStdString(manufacturer->value()));
}
if (auto model = header.findTypedAttribute<Imf::StringAttribute>("cameraModel")) {
image.setText(QStringLiteral(META_KEY_MODEL), QString::fromStdString(model->value()));
}
if (auto serial = header.findTypedAttribute<Imf::StringAttribute>("cameraSerialNumber")) {
image.setText(QStringLiteral(META_KEY_SERIALNUMBER), QString::fromStdString(serial->value()));
}
// lens metadata
if (auto manufacturer = header.findTypedAttribute<Imf::StringAttribute>("lensMake")) {
image.setText(QStringLiteral(META_KEY_LENS_MANUFACTURER), QString::fromStdString(manufacturer->value()));
}
if (auto model = header.findTypedAttribute<Imf::StringAttribute>("lensModel")) {
image.setText(QStringLiteral(META_KEY_LENS_MODEL), QString::fromStdString(model->value()));
}
if (auto serial = header.findTypedAttribute<Imf::StringAttribute>("lensSerialNumber")) {
image.setText(QStringLiteral(META_KEY_LENS_SERIALNUMBER), QString::fromStdString(serial->value()));
}
}
/*!
@ -371,8 +327,6 @@ static void readMetadata(const Imf::Header &header, QImage &image)
static void readColorSpace(const Imf::Header &header, QImage &image)
{
// final color operations
#ifndef EXR_USE_LEGACY_CONVERSIONS
QColorSpace cs;
if (auto chroma = header.findTypedAttribute<Imf::ChromaticitiesAttribute>("chromaticities")) {
auto &&v = chroma->value();
@ -383,24 +337,13 @@ static void readColorSpace(const Imf::Header &header, QImage &image)
QColorSpace::TransferFunction::Linear);
}
if (!cs.isValid()) {
#ifdef EXR_GRAY_SUPPORT_ENABLED
if (image.format() == QImage::Format_Grayscale16 || image.format() == QImage::Format_Grayscale8) {
cs = QColorSpace(QPointF(0.31271, 0.32902), QColorSpace::TransferFunction::Linear);
cs.setDescription(QStringLiteral("Gray Linear build-in"));
} else {
cs = QColorSpace(QColorSpace::SRgbLinear);
}
#else
cs = QColorSpace(QColorSpace::SRgbLinear);
#endif
}
image.setColorSpace(cs);
#ifdef EXR_CONVERT_TO_SRGB
image.convertToColorSpace(QColorSpace(QColorSpace::SRgb));
#endif
#endif // !EXR_USE_LEGACY_CONVERSIONS
}
bool EXRHandler::read(QImage *outImage)
@ -417,7 +360,7 @@ bool EXRHandler::read(QImage *outImage)
}
}
K_IStream istr(d, QByteArray());
K_IStream istr(d);
Imf::RgbaInputFile file(istr);
auto &&header = file.header();
@ -451,7 +394,6 @@ bool EXRHandler::read(QImage *outImage)
pixels.resizeErase(EXR_LINES_PER_BLOCK, width);
bool isRgba = image.hasAlphaChannel();
// somehow copy pixels into image
for (int y = 0, n = 0; y < height; y += n) {
auto my = dw.min.y + y;
if (my > dw.max.y) { // paranoia check
@ -462,37 +404,14 @@ bool EXRHandler::read(QImage *outImage)
file.readPixels(my, std::min(my + EXR_LINES_PER_BLOCK - 1, dw.max.y));
for (n = 0; n < std::min(EXR_LINES_PER_BLOCK, height - y); ++n) {
if (image.format() == QImage::Format_Grayscale16) { // grayscale image
auto scanLine = reinterpret_cast<quint16 *>(image.scanLine(y + n));
for (int x = 0; x < width; ++x) {
*(scanLine + x) = quint16(qBound(0.f, float(pixels[n][x].r) * 65535.f + 0.5f, 65535.f));
}
continue;
}
#if defined(EXR_USE_LEGACY_CONVERSIONS)
Q_UNUSED(isRgba)
auto scanLine = reinterpret_cast<QRgb *>(image.scanLine(y + n));
for (int x = 0; x < width; ++x) {
*(scanLine + x) = RgbaToQrgba(pixels[n][x]);
}
#elif defined(EXR_USE_QT6_FLOAT_IMAGE)
auto scanLine = reinterpret_cast<qfloat16 *>(image.scanLine(y + n));
for (int x = 0; x < width; ++x) {
auto xcs = x * 4;
*(scanLine + xcs) = qfloat16(qBound(0.f, float(pixels[n][x].r), 1.f));
*(scanLine + xcs + 1) = qfloat16(qBound(0.f, float(pixels[n][x].g), 1.f));
*(scanLine + xcs + 2) = qfloat16(qBound(0.f, float(pixels[n][x].b), 1.f));
*(scanLine + xcs + 3) = qfloat16(isRgba ? qBound(0.f, float(pixels[n][x].a), 1.f) : 1.f);
*(scanLine + xcs) = qfloat16(float(pixels[n][x].r));
*(scanLine + xcs + 1) = qfloat16(float(pixels[n][x].g));
*(scanLine + xcs + 2) = qfloat16(float(pixels[n][x].b));
*(scanLine + xcs + 3) = qfloat16(isRgba ? std::clamp(float(pixels[n][x].a), 0.f, 1.f) : 1.f);
}
#else
auto scanLine = reinterpret_cast<QRgba64 *>(image.scanLine(y + n));
for (int x = 0; x < width; ++x) {
*(scanLine + x) = QRgba64::fromRgba64(quint16(qBound(0.f, float(pixels[n][x].r) * 65535.f + 0.5f, 65535.f)),
quint16(qBound(0.f, float(pixels[n][x].g) * 65535.f + 0.5f, 65535.f)),
quint16(qBound(0.f, float(pixels[n][x].b) * 65535.f + 0.5f, 65535.f)),
isRgba ? quint16(qBound(0.f, float(pixels[n][x].a) * 65535.f + 0.5f, 65535.f)) : quint16(65535));
}
#endif
}
}
@ -588,6 +507,26 @@ static void setMetadata(const QImage &image, Imf::Header &header)
header.insert("xmp", Imf::StringAttribute(text.toStdString()));
}
#endif
if (!key.compare(QStringLiteral(META_KEY_MANUFACTURER), Qt::CaseInsensitive)) {
header.insert("cameraMake", Imf::StringAttribute(text.toStdString()));
}
if (!key.compare(QStringLiteral(META_KEY_MODEL), Qt::CaseInsensitive)) {
header.insert("cameraModel", Imf::StringAttribute(text.toStdString()));
}
if (!key.compare(QStringLiteral(META_KEY_SERIALNUMBER), Qt::CaseInsensitive)) {
header.insert("cameraSerialNumber", Imf::StringAttribute(text.toStdString()));
}
if (!key.compare(QStringLiteral(META_KEY_LENS_MANUFACTURER), Qt::CaseInsensitive)) {
header.insert("lensMake", Imf::StringAttribute(text.toStdString()));
}
if (!key.compare(QStringLiteral(META_KEY_LENS_MODEL), Qt::CaseInsensitive)) {
header.insert("lensModel", Imf::StringAttribute(text.toStdString()));
}
if (!key.compare(QStringLiteral(META_KEY_LENS_SERIALNUMBER), Qt::CaseInsensitive)) {
header.insert("lensSerialNumber", Imf::StringAttribute(text.toStdString()));
}
}
if (dateTime.isValid()) {
header.insert("capDate", Imf::StringAttribute(dateTime.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")).toStdString()));
@ -604,8 +543,6 @@ static void setMetadata(const QImage &image, Imf::Header &header)
// If a file doesnt have a chromaticities attribute, display software should assume that the
// files primaries and the white point match Rec. ITU-R BT.709-3.
// header.insert("chromaticities", Imf::ChromaticitiesAttribute(Imf::Chromaticities()));
// TODO: EXR 3.2 attributes (see readMetadata())
}
bool EXRHandler::write(const QImage &image)
@ -646,7 +583,7 @@ bool EXRHandler::write(const QImage &image)
setMetadata(image, header);
// write the EXR
K_OStream ostr(device(), QByteArray());
K_OStream ostr(device());
auto channelsType = image.hasAlphaChannel() ? Imf::RgbaChannels::WRITE_RGBA : Imf::RgbaChannels::WRITE_RGB;
if (image.format() == QImage::Format_Mono ||
image.format() == QImage::Format_MonoLSB ||
@ -659,26 +596,12 @@ bool EXRHandler::write(const QImage &image)
pixels.resizeErase(EXR_LINES_PER_BLOCK, width);
// convert the image and write into the stream
#if defined(EXR_USE_QT6_FLOAT_IMAGE)
auto convFormat = image.hasAlphaChannel() ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4;
#else
auto convFormat = image.hasAlphaChannel() ? QImage::Format_RGBA64 : QImage::Format_RGBX64;
#endif
ScanLineConverter slc(convFormat);
#ifdef EXR_GRAY_SUPPORT_ENABLED
if (channelsType == Imf::RgbaChannels::WRITE_Y) {
slc.setDefaultSourceColorSpace(QColorSpace(QColorSpace(QColorSpace::SRgb).whitePoint(), QColorSpace::TransferFunction::SRgb)); // Creates a custom grayscale color space
} else {
slc.setDefaultSourceColorSpace(QColorSpace(QColorSpace::SRgb));
}
#else
slc.setDefaultSourceColorSpace(QColorSpace(QColorSpace::SRgb));
#endif
slc.setTargetColorSpace(QColorSpace(QColorSpace::SRgbLinear));
for (int y = 0, n = 0; y < height; y += n) {
for (n = 0; n < std::min(EXR_LINES_PER_BLOCK, height - y); ++n) {
#if defined(EXR_USE_QT6_FLOAT_IMAGE)
auto scanLine = reinterpret_cast<const qfloat16 *>(slc.convertedScanLine(image, y + n));
if (scanLine == nullptr) {
return false;
@ -690,18 +613,6 @@ bool EXRHandler::write(const QImage &image)
pixels[n][x].b = float(*(scanLine + xcs + 2));
pixels[n][x].a = float(*(scanLine + xcs + 3));
}
#else
auto scanLine = reinterpret_cast<const QRgba64 *>(slc.convertedScanLine(image, y + n));
if (scanLine == nullptr) {
return false;
}
for (int x = 0; x < width; ++x) {
pixels[n][x].r = float((scanLine + x)->red() / 65535.f);
pixels[n][x].g = float((scanLine + x)->green() / 65535.f);
pixels[n][x].b = float((scanLine + x)->blue() / 65535.f);
pixels[n][x].a = float((scanLine + x)->alpha() / 65535.f);
}
#endif
}
file.setFrameBuffer(&pixels[0][0] - qint64(y) * width, 1, width);
file.writePixels(n);
@ -762,7 +673,7 @@ QVariant EXRHandler::option(ImageOption option) const
d->seek(m_startPos);
}
try {
K_IStream istr(d, QByteArray());
K_IStream istr(d);
Imf::RgbaInputFile file(istr);
if (m_imageNumber > -1) { // set the image to read
auto views = viewList(file.header());
@ -787,7 +698,7 @@ QVariant EXRHandler::option(ImageOption option) const
d->seek(m_startPos);
}
try {
K_IStream istr(d, QByteArray());
K_IStream istr(d);
Imf::RgbaInputFile file(istr);
v = QVariant::fromValue(imageFormat(file));
} catch (const std::exception &) {
@ -836,7 +747,7 @@ int EXRHandler::imageCount() const
d->startTransaction();
try {
K_IStream istr(d, QByteArray());
K_IStream istr(d);
Imf::RgbaInputFile file(istr);
auto views = viewList(file.header());
if (!views.isEmpty()) {
@ -863,6 +774,13 @@ bool EXRHandler::canRead(QIODevice *device)
return false;
}
#if OPENEXR_VERSION_MAJOR == 3 && OPENEXR_VERSION_MINOR > 2
// openexpr >= 3.3 uses seek and tell extensively
if (device->isSequential()) {
return false;
}
#endif
const QByteArray head = device->peek(4);
return Imf::isImfMagic(head.data());

View File

@ -27,12 +27,194 @@ typedef unsigned char uchar;
Q_LOGGING_CATEGORY(HDRPLUGIN, "kf.imageformats.plugins.hdr", QtWarningMsg)
namespace // Private.
{
#define MAXLINE 1024
#define MINELEN 8 // minimum scanline length for encoding
#define MAXELEN 0x7fff // maximum scanline length for encoding
class Header
{
public:
Header()
{
m_colorSpace = QColorSpace(QColorSpace::SRgbLinear);
m_transformation = QImageIOHandler::TransformationNone;
}
Header(const Header&) = default;
Header& operator=(const Header&) = default;
bool isValid() const { return width() > 0 && height() > 0; }
qint32 width() const { return(m_size.width()); }
qint32 height() const { return(m_size.height()); }
QString software() const { return(m_software); }
QImageIOHandler::Transformations transformation() const { return(m_transformation); }
/*!
* \brief colorSpace
*
* The color space for the image.
*
* The CIE (x,y) chromaticity coordinates of the three (RGB)
* primaries and the white point used to standardize the picture's
* color system. This is used mainly by the ra_xyze program to
* convert between color systems. If no PRIMARIES line
* appears, we assume the standard primaries defined in
* src/common/color.h, namely "0.640 0.330 0.290
* 0.600 0.150 0.060 0.333 0.333" for red, green, blue
* and white, respectively.
*/
QColorSpace colorSpace() const { return(m_colorSpace); }
/*!
* \brief exposure
*
* A single floating point number indicating a multiplier that has
* been applied to all the pixels in the file. EXPOSURE values are
* cumulative, so the original pixel values (i.e., radiances in
* watts/steradian/m^2) must be derived by taking the values in the
* file and dividing by all the EXPOSURE settings multiplied
* together. No EXPOSURE setting implies that no exposure
* changes have taken place.
*/
float exposure() const {
float mul = 1;
for (auto&& v : m_exposure)
mul *= v;
return mul;
}
QImageIOHandler::Transformations m_transformation;
QColorSpace m_colorSpace;
QString m_software;
QSize m_size;
QList<float> m_exposure;
};
class HDRHandlerPrivate
{
public:
HDRHandlerPrivate()
{
}
~HDRHandlerPrivate()
{
}
const Header& header(QIODevice *device)
{
auto&& h = m_header;
if (h.isValid()) {
return h;
}
h = readHeader(device);
return h;
}
static Header readHeader(QIODevice *device)
{
Header h;
int len;
QByteArray line(MAXLINE + 1, Qt::Uninitialized);
QByteArray format;
// Parse header
do {
len = device->readLine(line.data(), MAXLINE);
if (line.startsWith("FORMAT=")) {
format = line.mid(7, len - 7).trimmed();
}
if (line.startsWith("SOFTWARE=")) {
h.m_software = QString::fromUtf8(line.mid(9, len - 9)).trimmed();
}
if (line.startsWith("EXPOSURE=")) {
auto ok = false;
auto ex = QLocale::c().toFloat(QString::fromLatin1(line.mid(9, len - 9)).trimmed(), &ok);
if (ok)
h.m_exposure << ex;
}
if (line.startsWith("PRIMARIES=")) {
auto list = line.mid(10, len - 10).trimmed().split(' ');
QList<double> primaries;
for (auto&& v : list) {
auto ok = false;
auto d = QLocale::c().toDouble(QString::fromLatin1(v), &ok);
if (ok)
primaries << d;
}
if (primaries.size() == 8) {
auto cs = QColorSpace(QPointF(primaries.at(6), primaries.at(7)),
QPointF(primaries.at(0), primaries.at(1)),
QPointF(primaries.at(2), primaries.at(3)),
QPointF(primaries.at(4), primaries.at(5)),
QColorSpace::TransferFunction::Linear);
cs.setDescription(QStringLiteral("Embedded RGB"));
if (cs.isValid())
h.m_colorSpace = cs;
}
}
} while ((len > 0) && (line[0] != '\n'));
if (format != "32-bit_rle_rgbe") {
qCDebug(HDRPLUGIN) << "Unknown HDR format:" << format;
return h;
}
len = device->readLine(line.data(), MAXLINE);
line.resize(len);
/*
* Handle flipping and rotation, as per the spec below.
* The single resolution line consists of 4 values, a X and Y label each followed by a numerical
* integer value. The X and Y are immediately preceded by a sign which can be used to indicate
* flipping, the order of the X and Y indicate rotation. The standard coordinate system for
* Radiance images would have the following resolution string -Y N +X N. This indicates that the
* vertical axis runs down the file and the X axis is to the right (imagining the image as a
* rectangular block of data). A -X would indicate a horizontal flip of the image. A +Y would
* indicate a vertical flip. If the X value appears before the Y value then that indicates that
* the image is stored in column order rather than row order, that is, it is rotated by 90 degrees.
* The reader can convince themselves that the 8 combinations cover all the possible image orientations
* and rotations.
*/
QRegularExpression resolutionRegExp(QStringLiteral("([+\\-][XY])\\s+([0-9]+)\\s+([+\\-][XY])\\s+([0-9]+)\n"));
QRegularExpressionMatch match = resolutionRegExp.match(QString::fromLatin1(line));
if (!match.hasMatch()) {
qCDebug(HDRPLUGIN) << "Invalid HDR file, the first line after the header didn't have the expected format:" << line;
return h;
}
auto c0 = match.captured(1);
auto c1 = match.captured(3);
if (c0.at(1) == u'Y') {
if (c0.at(0) == u'-' && c1.at(0) == u'+')
h.m_transformation = QImageIOHandler::TransformationNone;
if (c0.at(0) == u'-' && c1.at(0) == u'-')
h.m_transformation = QImageIOHandler::TransformationMirror;
if (c0.at(0) == u'+' && c1.at(0) == u'+')
h.m_transformation = QImageIOHandler::TransformationFlip;
if (c0.at(0) == u'+' && c1.at(0) == u'-')
h.m_transformation = QImageIOHandler::TransformationRotate180;
}
else {
if (c0.at(0) == u'-' && c1.at(0) == u'+')
h.m_transformation = QImageIOHandler::TransformationRotate90;
if (c0.at(0) == u'-' && c1.at(0) == u'-')
h.m_transformation = QImageIOHandler::TransformationMirrorAndRotate90;
if (c0.at(0) == u'+' && c1.at(0) == u'+')
h.m_transformation = QImageIOHandler::TransformationFlipAndRotate90;
if (c0.at(0) == u'+' && c1.at(0) == u'-')
h.m_transformation = QImageIOHandler::TransformationRotate270;
}
h.m_size = QSize(match.captured(4).toInt(), match.captured(2).toInt());
return h;
}
private:
Header m_header;
};
// read an old style line from the hdr image file
// if 'first' is true the first byte is already read
static bool Read_Old_Line(uchar *image, int width, QDataStream &s)
@ -76,9 +258,10 @@ static bool Read_Old_Line(uchar *image, int width, QDataStream &s)
}
template<class float_T>
void RGBE_To_QRgbLine(uchar *image, float_T *scanline, int width)
void RGBE_To_QRgbLine(uchar *image, float_T *scanline, const Header& h)
{
for (int j = 0; j < width; j++) {
auto exposure = h.exposure();
for (int j = 0, width = h.width(); j < width; j++) {
// v = ldexp(1.0, int(image[3]) - 128);
float v;
int e = qBound(-31, int(image[3]) - 128, 31);
@ -90,9 +273,13 @@ void RGBE_To_QRgbLine(uchar *image, float_T *scanline, int width)
auto j4 = j * 4;
auto vn = v / 255.0f;
scanline[j4] = float_T(std::min(float(image[0]) * vn, 1.0f));
scanline[j4 + 1] = float_T(std::min(float(image[1]) * vn, 1.0f));
scanline[j4 + 2] = float_T(std::min(float(image[2]) * vn, 1.0f));
if (exposure > 0) {
vn /= exposure;
}
scanline[j4] = float_T(float(image[0]) * vn);
scanline[j4 + 1] = float_T(float(image[1]) * vn);
scanline[j4 + 2] = float_T(float(image[2]) * vn);
scanline[j4 + 3] = float_T(1.0f);
image += 4;
}
@ -108,11 +295,14 @@ QImage::Format imageFormat()
}
// Load the HDR image.
static bool LoadHDR(QDataStream &s, const int width, const int height, QImage &img)
static bool LoadHDR(QDataStream &s, const Header& h, QImage &img)
{
uchar val;
uchar code;
const int width = h.width();
const int height = h.height();
// Create dst image.
img = imageAlloc(width, height, imageFormat());
if (img.isNull()) {
@ -134,7 +324,7 @@ static bool LoadHDR(QDataStream &s, const int width, const int height, QImage &i
// determine scanline type
if ((width < MINELEN) || (MAXELEN < width)) {
Read_Old_Line(image, width, s);
RGBE_To_QRgbLine(image, scanline, width);
RGBE_To_QRgbLine(image, scanline, h);
continue;
}
@ -147,7 +337,7 @@ static bool LoadHDR(QDataStream &s, const int width, const int height, QImage &i
if (val != 2) {
s.device()->ungetChar(val);
Read_Old_Line(image, width, s);
RGBE_To_QRgbLine(image, scanline, width);
RGBE_To_QRgbLine(image, scanline, h);
continue;
}
@ -162,7 +352,7 @@ static bool LoadHDR(QDataStream &s, const int width, const int height, QImage &i
if ((image[1] != 2) || (image[2] & 128)) {
image[0] = 2;
Read_Old_Line(image + 4, width - 1, s);
RGBE_To_QRgbLine(image, scanline, width);
RGBE_To_QRgbLine(image, scanline, h);
continue;
}
@ -204,84 +394,34 @@ static bool LoadHDR(QDataStream &s, const int width, const int height, QImage &i
}
}
}
RGBE_To_QRgbLine(image, scanline, width);
RGBE_To_QRgbLine(image, scanline, h);
}
return true;
}
static QSize readHeaderSize(QIODevice *device)
{
int len;
QByteArray line(MAXLINE + 1, Qt::Uninitialized);
QByteArray format;
// Parse header
do {
len = device->readLine(line.data(), MAXLINE);
if (line.startsWith("FORMAT=")) {
format = line.mid(7, len - 7 - 1 /*\n*/);
}
} while ((len > 0) && (line[0] != '\n'));
if (format != "32-bit_rle_rgbe") {
qCDebug(HDRPLUGIN) << "Unknown HDR format:" << format;
return QSize();
}
len = device->readLine(line.data(), MAXLINE);
line.resize(len);
/*
TODO: handle flipping and rotation, as per the spec below
The single resolution line consists of 4 values, a X and Y label each followed by a numerical
integer value. The X and Y are immediately preceded by a sign which can be used to indicate
flipping, the order of the X and Y indicate rotation. The standard coordinate system for
Radiance images would have the following resolution string -Y N +X N. This indicates that the
vertical axis runs down the file and the X axis is to the right (imagining the image as a
rectangular block of data). A -X would indicate a horizontal flip of the image. A +Y would
indicate a vertical flip. If the X value appears before the Y value then that indicates that
the image is stored in column order rather than row order, that is, it is rotated by 90 degrees.
The reader can convince themselves that the 8 combinations cover all the possible image orientations
and rotations.
*/
QRegularExpression resolutionRegExp(QStringLiteral("([+\\-][XY]) ([0-9]+) ([+\\-][XY]) ([0-9]+)\n"));
QRegularExpressionMatch match = resolutionRegExp.match(QString::fromLatin1(line));
if (!match.hasMatch()) {
qCDebug(HDRPLUGIN) << "Invalid HDR file, the first line after the header didn't have the expected format:" << line;
return QSize();
}
if ((match.captured(1).at(1) != u'Y') || (match.captured(3).at(1) != u'X')) {
qCDebug(HDRPLUGIN) << "Unsupported image orientation in HDR file.";
return QSize();
}
return QSize(match.captured(4).toInt(), match.captured(2).toInt());
}
} // namespace
bool HDRHandler::read(QImage *outImage)
{
QDataStream s(device());
m_imageSize = readHeaderSize(s.device());
if (!m_imageSize.isValid()) {
const Header& h = d->header(s.device());
if (!h.isValid()) {
return false;
}
QImage img;
if (!LoadHDR(s, m_imageSize.width(), m_imageSize.height(), img)) {
if (!LoadHDR(s, h, img)) {
// qDebug() << "Error loading HDR file.";
return false;
}
// The images read by Gimp and Photoshop (including those of the tests) are interpreted with linear color space.
// By setting the linear color space, programs that support profiles display HDR files as in GIMP and Photoshop.
img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
img.setColorSpace(h.colorSpace());
// Metadata
if (!h.software().isEmpty()) {
img.setText(QStringLiteral(META_KEY_SOFTWARE), h.software());
}
*outImage = img;
return true;
@ -295,6 +435,9 @@ bool HDRHandler::supportsOption(ImageOption option) const
if (option == QImageIOHandler::ImageFormat) {
return true;
}
if (option == QImageIOHandler::ImageTransformation) {
return true;
}
return false;
}
@ -303,15 +446,10 @@ QVariant HDRHandler::option(ImageOption option) const
QVariant v;
if (option == QImageIOHandler::Size) {
if (!m_imageSize.isEmpty()) {
v = QVariant::fromValue(m_imageSize);
} else if (auto d = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
auto size = readHeaderSize(d);
d->rollbackTransaction();
if (size.isValid()) {
v = QVariant::fromValue(size);
if (auto dev = device()) {
auto&& h = d->header(dev);
if (h.isValid()) {
v = QVariant::fromValue(h.m_size);
}
}
}
@ -320,10 +458,21 @@ QVariant HDRHandler::option(ImageOption option) const
v = QVariant::fromValue(imageFormat());
}
if (option == QImageIOHandler::ImageTransformation) {
if (auto dev = device()) {
auto&& h = d->header(dev);
if (h.isValid()) {
v = QVariant::fromValue(h.transformation());
}
}
}
return v;
}
HDRHandler::HDRHandler()
: QImageIOHandler()
, d(new HDRHandlerPrivate)
{
}
@ -350,9 +499,9 @@ bool HDRHandler::canRead(QIODevice *device)
// allow to load offical test cases: https://radsite.lbl.gov/radiance/framed.html
device->startTransaction();
QSize size = readHeaderSize(device);
auto h = HDRHandlerPrivate::readHeader(device);
device->rollbackTransaction();
if (size.isValid()) {
if (h.isValid()) {
return true;
}

View File

@ -9,7 +9,9 @@
#define KIMG_HDR_P_H
#include <QImageIOPlugin>
#include <QScopedPointer>
class HDRHandlerPrivate;
class HDRHandler : public QImageIOHandler
{
public:
@ -24,11 +26,7 @@ public:
static bool canRead(QIODevice *device);
private:
/*!
* \brief m_imageSize
* Image size cache used by option()
*/
QSize m_imageSize;
const QScopedPointer<HDRHandlerPrivate> d;
};
class HDRPlugin : public QImageIOPlugin

View File

@ -16,11 +16,47 @@
#include <jxl/thread_parallel_runner.h>
#include <string.h>
// Avoid rotation on buggy Qts (see also https://bugreports.qt.io/browse/QTBUG-126575)
#if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 7) && QT_VERSION < QT_VERSION_CHECK(6, 6, 0)) || (QT_VERSION >= QT_VERSION_CHECK(6, 7, 3))
#ifndef JXL_QT_AUTOTRANSFORM
#define JXL_QT_AUTOTRANSFORM
#endif
#endif
#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
#ifndef JXL_HDR_PRESERVATION_DISABLED
// Define JXL_HDR_PRESERVATION_DISABLED to disable HDR preservation
// (HDR images are saved as UINT16).
#define JXL_HDR_PRESERVATION_DISABLED
#endif
#endif
#ifndef JXL_DECODE_BOXES_DISABLED
// Decode Boxes in order to read optional metadata (XMP, Exif, etc...).
// Define JXL_DECODE_BOXES_DISABLED to disable Boxes decoding.
// #define JXL_DECODE_BOXES_DISABLED
#endif
#define FEATURE_LEVEL_5_WIDTH 262144
#define FEATURE_LEVEL_5_HEIGHT 262144
#define FEATURE_LEVEL_5_PIXELS 268435456
#if QT_POINTER_SIZE < 8
#define MAX_IMAGE_WIDTH 32767
#define MAX_IMAGE_HEIGHT 32767
#define MAX_IMAGE_PIXELS FEATURE_LEVEL_5_PIXELS
#else // JXL code stream level 5
#define MAX_IMAGE_WIDTH FEATURE_LEVEL_5_WIDTH
#define MAX_IMAGE_HEIGHT FEATURE_LEVEL_5_HEIGHT
#define MAX_IMAGE_PIXELS FEATURE_LEVEL_5_PIXELS
#endif
QJpegXLHandler::QJpegXLHandler()
: m_parseState(ParseJpegXLNotParsed)
, m_quality(90)
, m_currentimage_index(0)
, m_previousimage_index(-1)
, m_transformations(QImageIOHandler::TransformationNone)
, m_decoder(nullptr)
, m_runner(nullptr)
, m_next_image_delay(0)
@ -129,6 +165,11 @@ bool QJpegXLHandler::ensureDecoder()
return false;
}
#ifdef JXL_QT_AUTOTRANSFORM
// Let Qt handle the orientation.
JxlDecoderSetKeepOrientation(m_decoder, true);
#endif
int num_worker_threads = QThread::idealThreadCount();
if (!m_runner && num_worker_threads >= 4) {
/* use half of the threads because plug-in is usually used in environment
@ -151,23 +192,18 @@ bool QJpegXLHandler::ensureDecoder()
}
JxlDecoderCloseInput(m_decoder);
#ifndef JXL_DECODE_BOXES_DISABLED
JxlDecoderStatus status = JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME | JXL_DEC_BOX);
#else
JxlDecoderStatus status = JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME);
#endif
if (status == JXL_DEC_ERROR) {
qWarning("ERROR: JxlDecoderSubscribeEvents failed");
m_parseState = ParseJpegXLError;
return false;
}
status = JxlDecoderProcessInput(m_decoder);
if (status == JXL_DEC_ERROR) {
qWarning("ERROR: JXL decoding failed");
m_parseState = ParseJpegXLError;
return false;
}
if (status == JXL_DEC_NEED_MORE_INPUT) {
qWarning("ERROR: JXL data incomplete");
m_parseState = ParseJpegXLError;
if (!decodeBoxes()) {
return false;
}
@ -184,32 +220,12 @@ bool QJpegXLHandler::ensureDecoder()
return false;
}
if (m_basicinfo.xsize > 65535 || m_basicinfo.ysize > 65535) {
if (m_basicinfo.xsize > MAX_IMAGE_WIDTH || m_basicinfo.ysize > MAX_IMAGE_HEIGHT) {
qWarning("JXL image (%dx%d) is too large", m_basicinfo.xsize, m_basicinfo.ysize);
m_parseState = ParseJpegXLError;
return false;
}
if (sizeof(void *) <= 4) {
/* On 32bit systems, there is limited address space.
* We skip imagess bigger than 8192 x 8192 pixels.
* If we don't do it, abort() in libjxl may close whole application */
if (m_basicinfo.xsize > ((8192 * 8192) / m_basicinfo.ysize)) {
qWarning("JXL image (%dx%d) is too large for 32bit build of the plug-in", m_basicinfo.xsize, m_basicinfo.ysize);
m_parseState = ParseJpegXLError;
return false;
}
} else {
/* On 64bit systems
* We skip images bigger than 16384 x 16384 pixels.
* It is an artificial limit not to use extreme amount of memory */
if (m_basicinfo.xsize > ((16384 * 16384) / m_basicinfo.ysize)) {
qWarning("JXL image (%dx%d) is bigger than security limit 256 megapixels", m_basicinfo.xsize, m_basicinfo.ysize);
m_parseState = ParseJpegXLError;
return false;
}
}
m_parseState = ParseJpegXLBasicInfoParsed;
return true;
}
@ -243,27 +259,50 @@ bool QJpegXLHandler::countALLFrames()
m_input_pixel_format.endianness = JXL_NATIVE_ENDIAN;
m_input_pixel_format.align = 0;
m_input_pixel_format.num_channels = 4;
m_input_pixel_format.num_channels = m_basicinfo.num_color_channels == 1 ? 1 : 4;
if (m_basicinfo.bits_per_sample > 8) { // high bit depth
m_input_pixel_format.data_type = JXL_TYPE_UINT16;
m_buffer_size = 8 * (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize;
m_input_image_format = QImage::Format_RGBA64;
#ifdef JXL_HDR_PRESERVATION_DISABLED
bool is_fp = false;
#else
bool is_fp = m_basicinfo.exponent_bits_per_sample > 0 && m_basicinfo.num_color_channels == 3;
#endif
m_input_pixel_format.data_type = is_fp ? JXL_TYPE_FLOAT16 : JXL_TYPE_UINT16;
m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels * 2;
if (loadalpha) {
m_target_image_format = QImage::Format_RGBA64;
if (m_basicinfo.num_color_channels == 1) {
m_input_pixel_format.data_type = JXL_TYPE_UINT16;
m_input_image_format = m_target_image_format = QImage::Format_Grayscale16;
m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels * 2;
} else if (m_basicinfo.bits_per_sample > 16 && is_fp) {
m_input_pixel_format.data_type = JXL_TYPE_FLOAT;
m_input_image_format = QImage::Format_RGBA32FPx4;
m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels * 4;
if (loadalpha)
m_target_image_format = QImage::Format_RGBA32FPx4;
else
m_target_image_format = QImage::Format_RGBX32FPx4;
} else {
m_target_image_format = QImage::Format_RGBX64;
m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels * 2;
m_input_image_format = is_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64;
if (loadalpha)
m_target_image_format = is_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64;
else
m_target_image_format = is_fp ? QImage::Format_RGBX16FPx4 : QImage::Format_RGBX64;
}
} else { // 8bit depth
m_input_pixel_format.data_type = JXL_TYPE_UINT8;
m_buffer_size = 4 * (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize;
m_input_image_format = QImage::Format_RGBA8888;
m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels;
if (loadalpha) {
m_target_image_format = QImage::Format_ARGB32;
if (m_basicinfo.num_color_channels == 1) {
m_input_image_format = m_target_image_format = QImage::Format_Grayscale8;
} else {
m_target_image_format = QImage::Format_RGB32;
m_input_image_format = QImage::Format_RGBA8888;
if (loadalpha) {
m_target_image_format = QImage::Format_ARGB32;
} else {
m_target_image_format = QImage::Format_RGB32;
}
}
}
@ -363,6 +402,12 @@ bool QJpegXLHandler::countALLFrames()
m_framedelays[0] = 0;
}
#ifndef JXL_DECODE_BOXES_DISABLED
if (!decodeBoxes()) {
return false;
}
#endif
if (!rewind()) {
return false;
}
@ -389,6 +434,9 @@ bool QJpegXLHandler::decode_one_frame()
}
m_current_image.setColorSpace(m_colorspace);
if (!m_xmp.isEmpty()) {
m_current_image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromUtf8(m_xmp));
}
if (JxlDecoderSetImageOutBuffer(m_decoder, &m_input_pixel_format, m_current_image.bits(), m_buffer_size) != JXL_DEC_SUCCESS) {
qWarning("ERROR: JxlDecoderSetImageOutBuffer failed");
@ -450,6 +498,30 @@ bool QJpegXLHandler::read(QImage *image)
}
}
template<class T>
void packRGBPixels(QImage &img)
{
// pack pixel data
auto dest_pixels = reinterpret_cast<T *>(img.bits());
for (qint32 y = 0; y < img.height(); y++) {
auto src_pixels = reinterpret_cast<const T *>(img.constScanLine(y));
for (qint32 x = 0; x < img.width(); x++) {
// R
*dest_pixels = *src_pixels;
dest_pixels++;
src_pixels++;
// G
*dest_pixels = *src_pixels;
dest_pixels++;
src_pixels++;
// B
*dest_pixels = *src_pixels;
dest_pixels++;
src_pixels += 2; // skipalpha
}
}
}
bool QJpegXLHandler::write(const QImage &image)
{
if (image.format() == QImage::Format_Invalid) {
@ -457,36 +529,47 @@ bool QJpegXLHandler::write(const QImage &image)
return false;
}
if ((image.width() > 0) && (image.height() > 0)) {
if ((image.width() > 65535) || (image.height() > 65535)) {
qWarning("Image (%dx%d) is too large to save!", image.width(), image.height());
return false;
}
if (sizeof(void *) <= 4) {
if (image.width() > ((8192 * 8192) / image.height())) {
qWarning("Image (%dx%d) is too large save via 32bit build of JXL plug-in", image.width(), image.height());
return false;
}
} else {
if (image.width() > ((16384 * 16384) / image.height())) {
qWarning("Image (%dx%d) will not be saved because it has more than 256 megapixels", image.width(), image.height());
return false;
}
}
} else {
if ((image.width() == 0) || (image.height() == 0)) {
qWarning("Image has zero dimension!");
return false;
}
int save_depth = 8; // 8 or 16
if ((image.width() > MAX_IMAGE_WIDTH) || (image.height() > MAX_IMAGE_HEIGHT)) {
qWarning("Image (%dx%d) is too large to save!", image.width(), image.height());
return false;
}
size_t pixel_count = size_t(image.width()) * image.height();
if (MAX_IMAGE_PIXELS && pixel_count > MAX_IMAGE_PIXELS) {
qWarning("Image (%dx%d) will not be saved because it has more than %d megapixels!", image.width(), image.height(), MAX_IMAGE_PIXELS / 1024 / 1024);
return false;
}
int save_depth = 8; // 8 / 16 / 32
bool save_fp = false;
bool is_gray = false;
// depth detection
switch (image.format()) {
case QImage::Format_RGBX32FPx4:
case QImage::Format_RGBA32FPx4:
case QImage::Format_RGBA32FPx4_Premultiplied:
#ifndef JXL_HDR_PRESERVATION_DISABLED
save_depth = 32;
save_fp = true;
break;
#endif
case QImage::Format_RGBX16FPx4:
case QImage::Format_RGBA16FPx4:
case QImage::Format_RGBA16FPx4_Premultiplied:
#ifndef JXL_HDR_PRESERVATION_DISABLED
save_depth = 16;
save_fp = true;
break;
#endif
case QImage::Format_BGR30:
case QImage::Format_A2BGR30_Premultiplied:
case QImage::Format_RGB30:
case QImage::Format_A2RGB30_Premultiplied:
case QImage::Format_Grayscale16:
case QImage::Format_RGBX64:
case QImage::Format_RGBA64:
case QImage::Format_RGBA64_Premultiplied:
@ -501,6 +584,21 @@ bool QJpegXLHandler::write(const QImage &image)
case QImage::Format_RGBA8888_Premultiplied:
save_depth = 8;
break;
case QImage::Format_Grayscale16:
save_depth = 16;
is_gray = true;
break;
case QImage::Format_Grayscale8:
case QImage::Format_Alpha8:
case QImage::Format_Mono:
case QImage::Format_MonoLSB:
save_depth = 8;
is_gray = true;
break;
case QImage::Format_Indexed8:
save_depth = 8;
is_gray = image.isGrayscale();
break;
default:
if (image.depth() > 32) {
save_depth = 16;
@ -515,6 +613,7 @@ bool QJpegXLHandler::write(const QImage &image)
qWarning("Failed to create Jxl encoder");
return false;
}
JxlEncoderUseBoxes(encoder);
if (m_quality > 100) {
m_quality = 100;
@ -525,28 +624,28 @@ bool QJpegXLHandler::write(const QImage &image)
JxlBasicInfo output_info;
JxlEncoderInitBasicInfo(&output_info);
bool convert_color_profile;
QByteArray iccprofile;
if (image.colorSpace().isValid() && (m_quality < 100)) {
if (image.colorSpace().primaries() != QColorSpace::Primaries::SRgb || image.colorSpace().transferFunction() != QColorSpace::TransferFunction::SRgb) {
convert_color_profile = true;
} else {
convert_color_profile = false;
}
} else { // lossless or no profile or Qt-unsupported ICC profile
convert_color_profile = false;
iccprofile = image.colorSpace().iccProfile();
QColorSpace tmpcs = image.colorSpace();
if (!tmpcs.isValid() || tmpcs.primaries() != QColorSpace::Primaries::SRgb || tmpcs.transferFunction() != QColorSpace::TransferFunction::SRgb || m_quality == 100) {
// no profile or Qt-unsupported ICC profile
iccprofile = tmpcs.iccProfile();
// note: lossless encoding requires uses_original_profile = JXL_TRUE
if (iccprofile.size() > 0 || m_quality == 100) {
output_info.uses_original_profile = JXL_TRUE;
}
}
if (save_depth == 16 && (image.hasAlphaChannel() || output_info.uses_original_profile)) {
// clang-format off
if ( (save_depth > 8 && (image.hasAlphaChannel() || output_info.uses_original_profile))
|| (save_depth > 16)
|| (pixel_count > FEATURE_LEVEL_5_PIXELS)
|| (image.width() > FEATURE_LEVEL_5_WIDTH)
|| (image.height() > FEATURE_LEVEL_5_HEIGHT)) {
output_info.have_container = JXL_TRUE;
JxlEncoderUseContainer(encoder, JXL_TRUE);
JxlEncoderSetCodestreamLevel(encoder, 10);
}
// clang-format on
void *runner = nullptr;
int num_worker_threads = qBound(1, QThread::idealThreadCount(), 64);
@ -568,29 +667,79 @@ bool QJpegXLHandler::write(const QImage &image)
pixel_format.endianness = JXL_NATIVE_ENDIAN;
pixel_format.align = 0;
output_info.orientation = JXL_ORIENT_IDENTITY;
output_info.num_color_channels = 3;
output_info.animation.tps_numerator = 10;
output_info.animation.tps_denominator = 1;
output_info.orientation = JXL_ORIENT_IDENTITY;
if (m_transformations == QImageIOHandler::TransformationMirror) {
output_info.orientation = JXL_ORIENT_FLIP_HORIZONTAL;
} else if (m_transformations == QImageIOHandler::TransformationRotate180) {
output_info.orientation = JXL_ORIENT_ROTATE_180;
} else if (m_transformations == QImageIOHandler::TransformationFlip) {
output_info.orientation = JXL_ORIENT_FLIP_VERTICAL;
} else if (m_transformations == QImageIOHandler::TransformationFlipAndRotate90) {
output_info.orientation = JXL_ORIENT_TRANSPOSE;
} else if (m_transformations == QImageIOHandler::TransformationRotate90) {
output_info.orientation = JXL_ORIENT_ROTATE_90_CW;
} else if (m_transformations == QImageIOHandler::TransformationMirrorAndRotate90) {
output_info.orientation = JXL_ORIENT_ANTI_TRANSPOSE;
} else if (m_transformations == QImageIOHandler::TransformationRotate270) {
output_info.orientation = JXL_ORIENT_ROTATE_90_CCW;
}
if (save_depth > 8) { // 16bit depth
if (save_depth > 8 && is_gray) { // 16bit depth gray
pixel_format.data_type = JXL_TYPE_UINT16;
pixel_format.align = 4;
output_info.num_color_channels = 1;
output_info.bits_per_sample = 16;
tmpformat = QImage::Format_Grayscale16;
pixel_format.num_channels = 1;
} else if (is_gray) { // 8bit depth gray
pixel_format.data_type = JXL_TYPE_UINT8;
pixel_format.align = 4;
output_info.num_color_channels = 1;
output_info.bits_per_sample = 8;
tmpformat = QImage::Format_Grayscale8;
pixel_format.num_channels = 1;
} else if (save_depth > 16) { // 32bit depth rgb
pixel_format.data_type = JXL_TYPE_FLOAT;
output_info.exponent_bits_per_sample = 8;
output_info.num_color_channels = 3;
output_info.bits_per_sample = 32;
if (image.hasAlphaChannel()) {
tmpformat = QImage::Format_RGBA32FPx4;
pixel_format.num_channels = 4;
output_info.alpha_bits = 32;
output_info.alpha_exponent_bits = 8;
output_info.num_extra_channels = 1;
} else {
tmpformat = QImage::Format_RGBX32FPx4;
pixel_format.num_channels = 3;
output_info.alpha_bits = 0;
output_info.num_extra_channels = 0;
}
} else if (save_depth > 8) { // 16bit depth rgb
pixel_format.data_type = save_fp ? JXL_TYPE_FLOAT16 : JXL_TYPE_UINT16;
output_info.exponent_bits_per_sample = save_fp ? 5 : 0;
output_info.num_color_channels = 3;
output_info.bits_per_sample = 16;
if (image.hasAlphaChannel()) {
tmpformat = QImage::Format_RGBA64;
tmpformat = save_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64;
pixel_format.num_channels = 4;
output_info.alpha_bits = 16;
output_info.alpha_exponent_bits = save_fp ? 5 : 0;
output_info.num_extra_channels = 1;
} else {
tmpformat = QImage::Format_RGBX64;
tmpformat = save_fp ? QImage::Format_RGBX16FPx4 : QImage::Format_RGBX64;
pixel_format.num_channels = 3;
output_info.alpha_bits = 0;
output_info.num_extra_channels = 0;
}
} else { // 8bit depth
} else { // 8bit depth rgb
pixel_format.data_type = JXL_TYPE_UINT8;
pixel_format.align = 4;
output_info.num_color_channels = 3;
output_info.bits_per_sample = 8;
if (image.hasAlphaChannel()) {
@ -602,15 +751,13 @@ bool QJpegXLHandler::write(const QImage &image)
tmpformat = QImage::Format_RGB888;
pixel_format.num_channels = 3;
output_info.alpha_bits = 0;
output_info.num_extra_channels = 0;
}
}
const QImage tmpimage =
convert_color_profile ? image.convertToFormat(tmpformat).convertedToColorSpace(QColorSpace(QColorSpace::SRgb)) : image.convertToFormat(tmpformat);
QImage tmpimage = image.convertToFormat(tmpformat);
const size_t xsize = tmpimage.width();
const size_t ysize = tmpimage.height();
const size_t buffer_size = (save_depth > 8) ? (2 * pixel_format.num_channels * xsize * ysize) : (pixel_format.num_channels * xsize * ysize);
if (xsize == 0 || ysize == 0 || tmpimage.isNull()) {
qWarning("Unable to allocate memory for output image");
@ -634,7 +781,22 @@ bool QJpegXLHandler::write(const QImage &image)
return false;
}
if (!convert_color_profile && iccprofile.size() > 0) {
auto xmp_data = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
if (!xmp_data.isEmpty()) {
const char *box_type = "xml ";
status = JxlEncoderAddBox(encoder, box_type, reinterpret_cast<const uint8_t *>(xmp_data.constData()), xmp_data.size(), JXL_FALSE);
if (status != JXL_ENC_SUCCESS) {
qWarning("JxlEncoderAddBox failed!");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
}
JxlEncoderCloseBoxes(encoder); // no more metadata
if (iccprofile.size() > 0) {
status = JxlEncoderSetICCProfile(encoder, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());
if (status != JXL_ENC_SUCCESS) {
qWarning("JxlEncoderSetICCProfile failed!");
@ -665,61 +827,30 @@ bool QJpegXLHandler::write(const QImage &image)
JxlEncoderSetFrameLossless(encoder_options, (m_quality == 100) ? JXL_TRUE : JXL_FALSE);
if (image.hasAlphaChannel() || ((save_depth == 8) && (xsize % 4 == 0))) {
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast<const void *>(tmpimage.constBits()), buffer_size);
} else {
if (save_depth > 8) { // 16bit depth without alpha channel
uint16_t *tmp_buffer = new (std::nothrow) uint16_t[3 * xsize * ysize];
if (!tmp_buffer) {
qWarning("Memory allocation error");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
size_t buffer_size = size_t(tmpimage.bytesPerLine()) * tmpimage.height();
if (!image.hasAlphaChannel() && save_depth > 8 && !is_gray) { // pack pixel on tmpimage
buffer_size = (size_t(save_depth / 8) * pixel_format.num_channels * xsize * ysize);
uint16_t *dest_pixels = tmp_buffer;
for (int y = 0; y < tmpimage.height(); y++) {
const uint16_t *src_pixels = reinterpret_cast<const uint16_t *>(tmpimage.constScanLine(y));
for (int x = 0; x < tmpimage.width(); x++) {
// R
*dest_pixels = *src_pixels;
dest_pixels++;
src_pixels++;
// G
*dest_pixels = *src_pixels;
dest_pixels++;
src_pixels++;
// B
*dest_pixels = *src_pixels;
dest_pixels++;
src_pixels += 2; // skipalpha
}
// detaching image
tmpimage.detach();
if (tmpimage.isNull()) {
qWarning("Memory allocation error");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast<const void *>(tmp_buffer), buffer_size);
delete[] tmp_buffer;
} else { // 8bit depth without alpha channel
uchar *tmp_buffer8 = new (std::nothrow) uchar[3 * xsize * ysize];
if (!tmp_buffer8) {
qWarning("Memory allocation error");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
uchar *dest_pixels8 = tmp_buffer8;
const size_t rowbytes = 3 * xsize;
for (int y = 0; y < tmpimage.height(); y++) {
memcpy(dest_pixels8, tmpimage.constScanLine(y), rowbytes);
dest_pixels8 += rowbytes;
}
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast<const void *>(tmp_buffer8), buffer_size);
delete[] tmp_buffer8;
JxlEncoderDestroy(encoder);
return false;
}
// pack pixel data
if (save_depth > 16 && save_fp)
packRGBPixels<float>(tmpimage);
else if (save_fp)
packRGBPixels<qfloat16>(tmpimage);
else
packRGBPixels<quint16>(tmpimage);
}
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast<const void *>(tmpimage.constBits()), buffer_size);
if (status == JXL_ENC_ERROR) {
qWarning("JxlEncoderAddImageFrame failed!");
@ -777,14 +908,24 @@ bool QJpegXLHandler::write(const QImage &image)
QVariant QJpegXLHandler::option(ImageOption option) const
{
if (!supportsOption(option)) {
return QVariant();
}
if (option == Quality) {
return m_quality;
}
if (!supportsOption(option) || !ensureParsed()) {
if (!ensureParsed()) {
#ifdef JXL_QT_AUTOTRANSFORM
if (option == ImageTransformation) {
return int(m_transformations);
}
#endif
return QVariant();
}
switch (option) {
case Size:
return QSize(m_basicinfo.xsize, m_basicinfo.ysize);
@ -794,9 +935,31 @@ QVariant QJpegXLHandler::option(ImageOption option) const
} else {
return false;
}
#ifdef JXL_QT_AUTOTRANSFORM
case ImageTransformation:
if (m_basicinfo.orientation == JXL_ORIENT_IDENTITY) {
return int(QImageIOHandler::TransformationNone);
} else if (m_basicinfo.orientation == JXL_ORIENT_FLIP_HORIZONTAL) {
return int(QImageIOHandler::TransformationMirror);
} else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_180) {
return int(QImageIOHandler::TransformationRotate180);
} else if (m_basicinfo.orientation == JXL_ORIENT_FLIP_VERTICAL) {
return int(QImageIOHandler::TransformationFlip);
} else if (m_basicinfo.orientation == JXL_ORIENT_TRANSPOSE) {
return int(QImageIOHandler::TransformationFlipAndRotate90);
} else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_90_CW) {
return int(QImageIOHandler::TransformationRotate90);
} else if (m_basicinfo.orientation == JXL_ORIENT_ANTI_TRANSPOSE) {
return int(QImageIOHandler::TransformationMirrorAndRotate90);
} else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_90_CCW) {
return int(QImageIOHandler::TransformationRotate270);
}
#endif
default:
return QVariant();
}
return QVariant();
}
void QJpegXLHandler::setOption(ImageOption option, const QVariant &value)
@ -810,6 +973,14 @@ void QJpegXLHandler::setOption(ImageOption option, const QVariant &value)
m_quality = 90;
}
return;
#ifdef JXL_QT_AUTOTRANSFORM
case ImageTransformation:
if (auto t = value.toInt()) {
if (t > 0 && t < 8)
m_transformations = QImageIOHandler::Transformations(t);
}
break;
#endif
default:
break;
}
@ -818,7 +989,11 @@ void QJpegXLHandler::setOption(ImageOption option, const QVariant &value)
bool QJpegXLHandler::supportsOption(ImageOption option) const
{
return option == Quality || option == Size || option == Animation;
auto supported = option == Quality || option == Size || option == Animation;
#ifdef JXL_QT_AUTOTRANSFORM
supported = supported || option == ImageTransformation;
#endif
return supported;
}
int QJpegXLHandler::imageCount() const
@ -988,6 +1163,37 @@ bool QJpegXLHandler::rewind()
return true;
}
bool QJpegXLHandler::decodeBoxes()
{
JxlDecoderStatus status;
do { // decode metadata
status = JxlDecoderProcessInput(m_decoder);
if (status == JXL_DEC_BOX) {
JxlBoxType type;
JxlDecoderGetBoxType(m_decoder, type, JXL_FALSE);
if (memcmp(type, "xml ", 4) == 0) {
uint64_t size;
if (JxlDecoderGetBoxSizeRaw(m_decoder, &size) == JXL_DEC_SUCCESS) {
m_xmp = QByteArray(size, '\0');
JxlDecoderSetBoxBuffer(m_decoder, reinterpret_cast<uint8_t *>(m_xmp.data()), m_xmp.size());
}
}
}
} while (status == JXL_DEC_BOX);
if (status == JXL_DEC_ERROR) {
qWarning("ERROR: JXL decoding failed");
m_parseState = ParseJpegXLError;
return false;
}
if (status == JXL_DEC_NEED_MORE_INPUT) {
qWarning("ERROR: JXL data incomplete");
m_parseState = ParseJpegXLError;
return false;
}
return true;
}
QImageIOPlugin::Capabilities QJpegXLPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
if (format == "jxl") {

View File

@ -51,6 +51,7 @@ private:
bool countALLFrames();
bool decode_one_frame();
bool rewind();
bool decodeBoxes();
enum ParseJpegXLState {
ParseJpegXLError = -1,
@ -64,6 +65,7 @@ private:
int m_quality;
int m_currentimage_index;
int m_previousimage_index;
QImageIOHandler::Transformations m_transformations;
QByteArray m_rawData;
@ -76,6 +78,7 @@ private:
QImage m_current_image;
QColorSpace m_colorspace;
QByteArray m_xmp;
QImage::Format m_input_image_format;
QImage::Format m_target_image_format;

View File

@ -175,8 +175,8 @@ public:
// 32-bit
if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppBGR)) {
*conversionFormat = GUID_PKPixelFormat32bppRGB;
return QImage::Format_RGBX8888; // Format_RGB32 (?)
*conversionFormat = GUID_PKPixelFormat24bppRGB;
return QImage::Format_RGB888;
};
if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppBGRA)) {
*conversionFormat = GUID_PKPixelFormat32bppRGBA;
@ -436,6 +436,13 @@ public:
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
<< QImage::Format_CMYK8888
#endif
#ifndef JXR_DENY_FLOAT_IMAGE
<< QImage::Format_RGBA16FPx4
<< QImage::Format_RGBX16FPx4
<< QImage::Format_RGBA32FPx4
<< QImage::Format_RGBA32FPx4_Premultiplied
<< QImage::Format_RGBX32FPx4
#endif // JXR_DENY_FLOAT_IMAGE
<< QImage::Format_RGBA64
<< QImage::Format_RGBA64_Premultiplied
<< QImage::Format_RGBA8888
@ -476,7 +483,20 @@ public:
} else {
qi = qi.convertToFormat(alpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888);
}
#ifndef JXR_DENY_FLOAT_IMAGE
// clang-format off
} else if (qi.format() == QImage::Format_RGBA16FPx4 ||
qi.format() == QImage::Format_RGBX16FPx4 ||
qi.format() == QImage::Format_RGBA32FPx4 ||
qi.format() == QImage::Format_RGBA32FPx4_Premultiplied ||
qi.format() == QImage::Format_RGBX32FPx4) {
// clang-format on
auto cs = qi.colorSpace();
if (cs.isValid() && cs.transferFunction() != QColorSpace::TransferFunction::Linear) {
qi = qi.convertedToColorSpace(QColorSpace(QColorSpace::SRgbLinear));
}
}
#endif // JXR_DENY_FLOAT_IMAGE
return qi;
}
@ -625,7 +645,7 @@ private:
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_Mono, GUID_PKPixelFormatBlackWhite)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_Grayscale8, GUID_PKPixelFormat8bppGray)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_Grayscale16, GUID_PKPixelFormat16bppGray)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB555, GUID_PKPixelFormat16bppRGB565)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB555, GUID_PKPixelFormat16bppRGB555)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB16, GUID_PKPixelFormat16bppRGB565)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_BGR888, GUID_PKPixelFormat24bppBGR)
<< std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB888, GUID_PKPixelFormat24bppRGB)
@ -652,9 +672,12 @@ private:
return false;
}
QByteArray buff(32768 * 4, char());
for (; !source->atEnd();) {
for (;;) {
auto read = source->read(buff.data(), buff.size());
if (read < 1) {
if (read == 0) {
break;
}
if (read < 0) {
return false;
}
if (target->write(buff.data(), read) != read) {
@ -759,35 +782,6 @@ private:
}
};
template<class T>
inline T scRGBTosRGB(T f)
{
// convert from linear scRGB to non-linear sRGB
if (f <= T(0)) {
return T(0);
}
if (f <= T(0.0031308f)) {
return qBound(T(0), f * T(12.92f), T(1));
}
if (f < T(1)) {
return qBound(T(0), T(1.055f) * T(pow(f, T(1.0) / T(2.4))) - T(0.055), T(1));
}
return T(1);
}
template<class T>
inline T alpha_scRGBTosRGB(T f)
{
// alpha is converted differently than RGB in scRGB
if (f <= T(0)) {
return T(0);
}
if (f < T(1.0)) {
return T(f);
}
return T(1);
}
bool JXRHandler::read(QImage *outImage)
{
if (!d->initForReading(device())) {
@ -841,13 +835,11 @@ bool JXRHandler::read(QImage *outImage)
} else { // additional buffer needed
qint64 convStrideSize = (img.width() * d->pDecoder->WMP.wmiI.cBitsPerUnit + 7) / 8;
qint64 buffSize = convStrideSize * img.height();
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
qint64 limit = QImageReader::allocationLimit();
if (limit && (buffSize + img.sizeInBytes()) > limit * 1024 * 1024) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to covert due to allocation limit set:" << limit << "MiB";
return false;
}
#endif
QVector<quint8> ba(buffSize);
if (auto err = pConverter->Copy(pConverter, &rect, ba.data(), convStrideSize)) {
PKFormatConverter_Release(&pConverter);
@ -866,34 +858,24 @@ bool JXRHandler::read(QImage *outImage)
d->setTextMetadata(img);
#ifndef JXR_DENY_FLOAT_IMAGE
// JXR float are stored in scRGB -> range -0,5 to 7,5 (source Wikipedia)
if (img.format() == QImage::Format_RGBX16FPx4 || img.format() == QImage::Format_RGBA16FPx4 || img.format() == QImage::Format_RGBA16FPx4_Premultiplied) {
// JXR float are stored in scRGB.
if (img.format() == QImage::Format_RGBX16FPx4 || img.format() == QImage::Format_RGBA16FPx4 || img.format() == QImage::Format_RGBA16FPx4_Premultiplied ||
img.format() == QImage::Format_RGBX32FPx4 || img.format() == QImage::Format_RGBA32FPx4 || img.format() == QImage::Format_RGBA32FPx4_Premultiplied) {
auto hasAlpha = img.hasAlphaChannel();
for (qint32 y = 0, h = img.height(); y < h; ++y) {
qfloat16 *line = reinterpret_cast<qfloat16 *>(img.scanLine(y));
for (int x = 0, w = img.width(); x < w; ++x) {
const auto x4 = x * 4;
line[x4 + 0] = scRGBTosRGB(line[x4 + 0]);
line[x4 + 1] = scRGBTosRGB(line[x4 + 1]);
line[x4 + 2] = scRGBTosRGB(line[x4 + 2]);
line[x4 + 3] = hasAlpha ? alpha_scRGBTosRGB(line[x4 + 3]) : qfloat16(1);
if (img.depth() == 64) {
auto line = reinterpret_cast<qfloat16 *>(img.scanLine(y));
for (int x = 0, w = img.width() * 4; x < w; x += 4)
line[x + 3] = hasAlpha ? std::clamp(line[x + 3], qfloat16(0), qfloat16(1)) : qfloat16(1);
} else {
auto line = reinterpret_cast<float *>(img.scanLine(y));
for (int x = 0, w = img.width() * 4; x < w; x += 4)
line[x + 3] = hasAlpha ? std::clamp(line[x + 3], float(0), float(1)) : float(1);
}
}
img.setColorSpace(QColorSpace(QColorSpace::SRgb));
} else if (img.format() == QImage::Format_RGBX32FPx4 || img.format() == QImage::Format_RGBA32FPx4
|| img.format() == QImage::Format_RGBA32FPx4_Premultiplied) {
auto hasAlpha = img.hasAlphaChannel();
for (qint32 y = 0, h = img.height(); y < h; ++y) {
float *line = reinterpret_cast<float *>(img.scanLine(y));
for (int x = 0, w = img.width(); x < w; ++x) {
const auto x4 = x * 4;
line[x4 + 0] = scRGBTosRGB(line[x4 + 0]);
line[x4 + 1] = scRGBTosRGB(line[x4 + 1]);
line[x4 + 2] = scRGBTosRGB(line[x4 + 2]);
line[x4 + 3] = hasAlpha ? alpha_scRGBTosRGB(line[x4 + 3]) : float(1);
}
if (!img.colorSpace().isValid()) {
img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
}
img.setColorSpace(QColorSpace(QColorSpace::SRgb));
}
#endif
@ -903,6 +885,13 @@ bool JXRHandler::read(QImage *outImage)
bool JXRHandler::write(const QImage &image)
{
// JXR is stored in a TIFF V6 container that is limited to 4GiB. The size
// is limited to 4GB to leave room for IFDs, Metadata, etc...
if (qint64(image.sizeInBytes()) > 4000000000ll) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() image too large: the image cannot exceed 4GB.";
return false;
}
if (!d->initForWriting()) {
return false;
}
@ -922,19 +911,11 @@ bool JXRHandler::write(const QImage &image)
#ifndef JXR_DISABLE_BGRA_HACK
if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppRGBA)) {
jxlfmt = GUID_PKPixelFormat32bppBGRA;
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
qi = qi.rgbSwapped();
#else
qi.rgbSwap();
#endif
}
if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppPRGBA)) {
jxlfmt = GUID_PKPixelFormat32bppPBGRA;
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
qi = qi.rgbSwapped();
#else
qi.rgbSwap();
#endif
}
#endif
@ -968,7 +949,7 @@ bool JXRHandler::write(const QImage &image)
}
// setting metadata (a failure of setting metadata doesn't stop the encoding)
auto cs = image.colorSpace().iccProfile();
auto cs = qi.colorSpace().iccProfile();
if (!cs.isEmpty()) {
if (auto err = d->pEncoder->SetColorContext(d->pEncoder, reinterpret_cast<quint8 *>(cs.data()), cs.size())) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting ICC profile:" << err;
@ -1043,28 +1024,28 @@ QVariant JXRHandler::option(ImageOption option) const
if (d->initForReading(device())) {
switch (d->pDecoder->WMP.oOrientationFromContainer) {
case O_FLIPV:
v = QImageIOHandler::TransformationFlip;
v = int(QImageIOHandler::TransformationFlip);
break;
case O_FLIPH:
v = QImageIOHandler::TransformationMirror;
v = int(QImageIOHandler::TransformationMirror);
break;
case O_FLIPVH:
v = QImageIOHandler::TransformationRotate180;
v = int(QImageIOHandler::TransformationRotate180);
break;
case O_RCW:
v = QImageIOHandler::TransformationRotate90;
v = int(QImageIOHandler::TransformationRotate90);
break;
case O_RCW_FLIPV:
v = QImageIOHandler::TransformationFlipAndRotate90;
v = int(QImageIOHandler::TransformationFlipAndRotate90);
break;
case O_RCW_FLIPH:
v = QImageIOHandler::TransformationMirrorAndRotate90;
v = int(QImageIOHandler::TransformationMirrorAndRotate90);
break;
case O_RCW_FLIPVH:
v = QImageIOHandler::TransformationRotate270;
v = int(QImageIOHandler::TransformationRotate270);
break;
default:
v = QImageIOHandler::TransformationNone;
v = int(QImageIOHandler::TransformationNone);
break;
}
}
@ -1095,11 +1076,6 @@ bool JXRHandler::canRead(QIODevice *device)
return false;
}
// Some tests on sequential devices fail: I reject them for now
if (device->isSequential()) {
return false;
}
// JPEG XR image data is stored in TIFF-like container format (II and 0xBC01 version)
if (device->peek(4) == QByteArray::fromRawData("\x49\x49\xbc\x01", 4)) {
return true;

View File

@ -67,6 +67,40 @@ public:
{
return (Encoding == 1);
}
/*!
* \brief isValid
* Checks if the header data are valid for the PCX.
* \note Put here the header sanity checks.
* \return True if the header is a valid PCX header, otherwise false.
*/
inline bool isValid() const
{
return Manufacturer == 10 && BytesPerLine != 0;
}
/*!
* \brief isSupported
* \return True if the header is valid and the PCX format is supported by the plugin. Otherwise false.
*/
inline bool isSupported() const
{
return isValid() && format() != QImage::Format_Invalid;
}
inline QImage::Format format() const
{
auto fmt = QImage::Format_Invalid;
if (Bpp == 1 && NPlanes == 1) {
fmt = QImage::Format_Mono;
} else if (Bpp == 1 && NPlanes == 4) {
fmt = QImage::Format_Indexed8;
} else if (Bpp == 4 && NPlanes == 1) {
fmt = QImage::Format_Indexed8;
} else if (Bpp == 8 && NPlanes == 1) {
fmt = QImage::Format_Indexed8;
} else if (Bpp == 8 && NPlanes == 3) {
fmt = QImage::Format_RGB32;
}
return fmt;
}
quint8 Manufacturer; // Constant Flag, 10 = ZSoft .pcx
quint8 Version; // Version information·
@ -100,6 +134,8 @@ public:
// found only in PB IV/IV Plus
quint16 VScreenSize; // Vertical screen size in pixels. New field
// found only in PB IV/IV Plus
quint8 unused[54];
};
#pragma pack(pop)
@ -173,9 +209,8 @@ static QDataStream &operator>>(QDataStream &s, PCXHEADER &ph)
ph.VScreenSize = vscreensize;
// Skip the rest of the header
quint8 byte;
for (auto i = 0; i < 54; ++i) {
s >> byte;
for (size_t i = 0, n = sizeof(ph.unused); i < n; ++i) {
s >> ph.unused[i];
}
return s;
@ -213,9 +248,8 @@ static QDataStream &operator<<(QDataStream &s, const PCXHEADER &ph)
s << ph.HScreenSize;
s << ph.VScreenSize;
quint8 byte = 0;
for (int i = 0; i < 54; ++i) {
s << byte;
for (size_t i = 0, n = sizeof(ph.unused); i < n; ++i) {
s << ph.unused[i];
}
return s;
@ -230,6 +264,20 @@ PCXHEADER::PCXHEADER()
s >> *this;
}
bool peekHeader(QIODevice *d, PCXHEADER& h)
{
auto head = d->peek(sizeof(PCXHEADER));
if (size_t(head.size()) < sizeof(PCXHEADER)) {
return false;
}
QDataStream ds(head);
ds.setByteOrder(QDataStream::LittleEndian);
ds >> h;
return ds.status() == QDataStream::Ok && h.isValid();
}
static bool readLine(QDataStream &s, QByteArray &buf, const PCXHEADER &header)
{
quint32 i = 0;
@ -265,7 +313,7 @@ static bool readImage1(QImage &img, QDataStream &s, const PCXHEADER &header)
{
QByteArray buf(header.BytesPerLine, 0);
img = imageAlloc(header.width(), header.height(), QImage::Format_Mono);
img = imageAlloc(header.width(), header.height(), header.format());
img.setColorCount(2);
if (img.isNull()) {
@ -301,13 +349,18 @@ static bool readImage4(QImage &img, QDataStream &s, const PCXHEADER &header)
QByteArray buf(header.BytesPerLine * 4, 0);
QByteArray pixbuf(header.width(), 0);
img = imageAlloc(header.width(), header.height(), QImage::Format_Indexed8);
img = imageAlloc(header.width(), header.height(), header.format());
img.setColorCount(16);
if (img.isNull()) {
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height());
return false;
}
if (header.BytesPerLine < (header.width() + 7) / 8) {
qWarning() << "PCX image has invalid BytesPerLine value";
return false;
}
for (int y = 0; y < header.height(); ++y) {
if (s.atEnd()) {
return false;
@ -344,11 +397,52 @@ static bool readImage4(QImage &img, QDataStream &s, const PCXHEADER &header)
return true;
}
static bool readImage4v2(QImage &img, QDataStream &s, const PCXHEADER &header)
{
QByteArray buf(header.BytesPerLine, 0);
img = imageAlloc(header.width(), header.height(), header.format());
img.setColorCount(16);
if (img.isNull()) {
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height());
return false;
}
for (int y = 0; y < header.height(); ++y) {
if (s.atEnd()) {
return false;
}
if (!readLine(s, buf, header)) {
return false;
}
uchar *p = img.scanLine(y);
if (!p) {
return false;
}
const unsigned int bpl = std::min(header.BytesPerLine, static_cast<quint16>(header.width() / 2));
for (unsigned int x = 0; x < bpl; ++x) {
p[x * 2] = (buf[x] & 240) >> 4;
p[x * 2 + 1] = buf[x] & 15;
}
}
// Read the palette
for (int i = 0; i < 16; ++i) {
img.setColor(i, header.ColorMap.color(i));
}
return (s.status() == QDataStream::Ok);
}
static bool readImage8(QImage &img, QDataStream &s, const PCXHEADER &header)
{
QByteArray buf(header.BytesPerLine, 0);
img = imageAlloc(header.width(), header.height(), QImage::Format_Indexed8);
img = imageAlloc(header.width(), header.height(), header.format());
img.setColorCount(256);
if (img.isNull()) {
@ -411,13 +505,15 @@ static bool readImage24(QImage &img, QDataStream &s, const PCXHEADER &header)
QByteArray g_buf(header.BytesPerLine, 0);
QByteArray b_buf(header.BytesPerLine, 0);
img = imageAlloc(header.width(), header.height(), QImage::Format_RGB32);
img = imageAlloc(header.width(), header.height(), header.format());
if (img.isNull()) {
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height());
return false;
}
const unsigned int bpl = std::min(header.BytesPerLine, static_cast<quint16>(header.width()));
for (int y = 0; y < header.height(); ++y) {
if (s.atEnd()) {
return false;
@ -434,7 +530,8 @@ static bool readImage24(QImage &img, QDataStream &s, const PCXHEADER &header)
}
uint *p = (uint *)img.scanLine(y);
for (int x = 0; x < header.width(); ++x) {
for (unsigned int x = 0; x < bpl; ++x) {
p[x] = qRgb(r_buf[x], g_buf[x], b_buf[x]);
}
}
@ -636,7 +733,18 @@ static bool writeImage24(QImage &img, QDataStream &s, PCXHEADER &header)
return true;
}
class PCXHandlerPrivate
{
public:
PCXHandlerPrivate() {}
~PCXHandlerPrivate() {}
PCXHEADER m_header;
};
PCXHandler::PCXHandler()
: QImageIOHandler()
, d(new PCXHandlerPrivate)
{
}
@ -658,11 +766,14 @@ bool PCXHandler::read(QImage *outImage)
return false;
}
PCXHEADER header;
auto&& header = d->m_header;
s >> header;
if (header.Manufacturer != 10 || header.BytesPerLine == 0 || s.atEnd()) {
if (s.status() != QDataStream::Ok || s.atEnd()) {
return false;
}
if (!header.isSupported()) {
return false;
}
@ -672,6 +783,8 @@ bool PCXHandler::read(QImage *outImage)
ok = readImage1(img, s, header);
} else if (header.Bpp == 1 && header.NPlanes == 4) {
ok = readImage4(img, s, header);
} else if (header.Bpp == 4 && header.NPlanes == 1) {
ok = readImage4v2(img, s, header);
} else if (header.Bpp == 8 && header.NPlanes == 1) {
ok = readImage8(img, s, header);
} else if (header.Bpp == 8 && header.NPlanes == 3) {
@ -730,6 +843,46 @@ bool PCXHandler::write(const QImage &image)
return ok;
}
bool PCXHandler::supportsOption(ImageOption option) const
{
if (option == QImageIOHandler::Size) {
return true;
}
if (option == QImageIOHandler::ImageFormat) {
return true;
}
return false;
}
QVariant PCXHandler::option(ImageOption option) const
{
QVariant v;
if (option == QImageIOHandler::Size) {
auto&& header = d->m_header;
if (header.isSupported()) {
v = QVariant::fromValue(QSize(header.width(), header.height()));
} else if (auto dev = device()) {
if (peekHeader(dev, header) && header.isSupported()) {
v = QVariant::fromValue(QSize(header.width(), header.height()));
}
}
}
if (option == QImageIOHandler::ImageFormat) {
auto&& header = d->m_header;
if (header.isSupported()) {
v = QVariant::fromValue(header.format());
} else if (auto dev = device()) {
if (peekHeader(dev, header) && header.isSupported()) {
v = QVariant::fromValue(header.format());
}
}
}
return v;
}
bool PCXHandler::canRead(QIODevice *device)
{
if (!device) {
@ -737,30 +890,11 @@ bool PCXHandler::canRead(QIODevice *device)
return false;
}
qint64 oldPos = device->pos();
char head[1];
qint64 readBytes = device->read(head, sizeof(head));
if (readBytes != sizeof(head)) {
if (device->isSequential()) {
while (readBytes > 0) {
device->ungetChar(head[readBytes-- - 1]);
}
} else {
device->seek(oldPos);
}
PCXHEADER header;
if (!peekHeader(device, header)) {
return false;
}
if (device->isSequential()) {
while (readBytes > 0) {
device->ungetChar(head[readBytes-- - 1]);
}
} else {
device->seek(oldPos);
}
return qstrncmp(head, "\012", 1) == 0;
return header.isSupported();
}
QImageIOPlugin::Capabilities PCXPlugin::capabilities(QIODevice *device, const QByteArray &format) const

View File

@ -9,7 +9,9 @@
#define KIMG_PCX_P_H
#include <QImageIOPlugin>
#include <QScopedPointer>
class PCXHandlerPrivate;
class PCXHandler : public QImageIOHandler
{
public:
@ -19,7 +21,13 @@ public:
bool read(QImage *image) override;
bool write(const QImage &image) override;
bool supportsOption(QImageIOHandler::ImageOption option) const override;
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
private:
const QScopedPointer<PCXHandlerPrivate> d;
};
class PCXPlugin : public QImageIOPlugin

View File

@ -109,7 +109,7 @@ public:
QImage::Format format() const
{
if (isValid()) {
return isBlackAndWhite() ? QImage::Format_Grayscale16 : QImage::Format_RGBX32FPx4;
return QImage::Format_RGBX32FPx4;
}
return QImage::Format_Invalid;
}
@ -155,7 +155,7 @@ public:
d->rollbackTransaction();
return ok;
}
} ;
};
class PFMHandlerPrivate
{
@ -215,42 +215,26 @@ bool PFMHandler::read(QImage *image)
}
for (auto y = 0, h = img.height(); y < h; ++y) {
float f;
if (header.isBlackAndWhite()) {
auto line = reinterpret_cast<quint16*>(img.scanLine(header.isPhotoshop() ? y : h - y - 1));
for (auto x = 0, w = img.width(); x < w; ++x) {
s >> f;
// QColorSpace does not handle gray linear profile, so I have to convert to non-linear
f = f < 0.0031308f ? (f * 12.92f) : (1.055 * std::pow(f, 1.0 / 2.4) - 0.055);
line[x] = quint16(std::clamp(f, float(0), float(1)) * std::numeric_limits<quint16>::max() + float(0.5));
if (s.status() != QDataStream::Ok) {
qCWarning(LOG_PFMPLUGIN) << "PFMHandler::read() detected corrupted data";
return false;
}
auto bw = header.isBlackAndWhite();
auto line = reinterpret_cast<float *>(img.scanLine(header.isPhotoshop() ? y : h - y - 1));
for (auto x = 0, n = img.width() * 4; x < n; x += 4) {
line[x + 3] = float(1);
s >> line[x];
if (bw) {
line[x + 1] = line[x];
line[x + 2] = line[x];
} else {
s >> line[x + 1];
s >> line[x + 2];
}
} else {
auto line = reinterpret_cast<float*>(img.scanLine(header.isPhotoshop() ? y : h - y - 1));
for (auto x = 0, n = img.width() * 4; x < n; x += 4) {
s >> f;
line[x] = std::clamp(f, float(0), float(1));
s >> f;
line[x + 1] = std::clamp(f, float(0), float(1));
s >> f;
line[x + 2] = std::clamp(f, float(0), float(1));
line[x + 3] = float(1);
if (s.status() != QDataStream::Ok) {
qCWarning(LOG_PFMPLUGIN) << "PFMHandler::read() detected corrupted data";
return false;
}
if (s.status() != QDataStream::Ok) {
qCWarning(LOG_PFMPLUGIN) << "PFMHandler::read() detected corrupted data";
return false;
}
}
}
if (!header.isBlackAndWhite()) {
img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
}
img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
*image = img;
return true;

View File

@ -121,10 +121,8 @@ public:
bool peek(QIODevice *d)
{
d->startTransaction();
auto ok = read(d);
d->rollbackTransaction();
return ok;
m_rawHeader = d->peek(512);
return isValid();
}
bool jumpToImageData(QIODevice *d) const

View File

@ -341,12 +341,8 @@ bool QOIHandler::canRead(QIODevice *device)
return false;
}
device->startTransaction();
QByteArray head = device->read(QOI_HEADER_SIZE);
qsizetype readBytes = head.size();
device->rollbackTransaction();
if (readBytes < QOI_HEADER_SIZE) {
auto head = device->peek(QOI_HEADER_SIZE);
if (head.size() < QOI_HEADER_SIZE) {
return false;
}
@ -430,12 +426,7 @@ QVariant QOIHandler::option(ImageOption option) const
if (IsSupported(header)) {
v = QVariant::fromValue(QSize(header.Width, header.Height));
} else if (auto d = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
auto ba = d->read(sizeof(QoiHeader));
d->rollbackTransaction();
QDataStream s(ba);
QDataStream s(d->peek(sizeof(QoiHeader)));
s.setByteOrder(QDataStream::BigEndian);
s >> header;
if (s.status() == QDataStream::Ok && IsSupported(header)) {
@ -449,12 +440,7 @@ QVariant QOIHandler::option(ImageOption option) const
if (IsSupported(header)) {
v = QVariant::fromValue(imageFormat(header));
} else if (auto d = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
auto ba = d->read(sizeof(QoiHeader));
d->rollbackTransaction();
QDataStream s(ba);
QDataStream s(d->peek(sizeof(QoiHeader)));
s.setByteOrder(QDataStream::BigEndian);
s >> header;
if (s.status() == QDataStream::Ok && IsSupported(header)) {

View File

@ -202,8 +202,6 @@ private:
static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
{
s.device()->seek(RasHeader::SIZE);
// The width of a scan line is always a multiple of 16 bits, padded when necessary.
auto rasLineSize = (qint64(ras.Width) * ras.Depth + 7) / 8;
if (rasLineSize & 1)
@ -368,18 +366,8 @@ bool RASHandler::canRead(QIODevice *device)
return false;
}
if (device->isSequential()) {
// qWarning("Reading ras files from sequential devices not supported");
return false;
}
qint64 oldPos = device->pos();
QByteArray head = device->read(RasHeader::SIZE); // header is exactly 32 bytes, always FIXME
int readBytes = head.size(); // this should always be 32 bytes
device->seek(oldPos);
if (readBytes < RasHeader::SIZE) {
auto head = device->peek(RasHeader::SIZE); // header is exactly 32 bytes, always FIXME
if (head.size() < RasHeader::SIZE) {
return false;
}
@ -411,9 +399,7 @@ bool RASHandler::read(QImage *outImage)
}
QImage img;
bool result = LoadRAS(s, ras, img);
if (result == false) {
if (!LoadRAS(s, ras, img)) {
// qDebug() << "Error loading RAS file.";
return false;
}
@ -443,12 +429,7 @@ QVariant RASHandler::option(ImageOption option) const
v = QVariant::fromValue(QSize(header.Width, header.Height));
}
else if (auto dev = device()) {
// transactions works on both random and sequential devices
dev->startTransaction();
auto ba = dev->read(RasHeader::SIZE);
dev->rollbackTransaction();
QDataStream s(ba);
QDataStream s(dev->peek(RasHeader::SIZE));
s.setByteOrder(QDataStream::BigEndian);
s >> header;
if (s.status() == QDataStream::Ok && IsSupported(header)) {
@ -463,12 +444,7 @@ QVariant RASHandler::option(ImageOption option) const
v = QVariant::fromValue(imageFormat(header));
}
else if (auto dev = device()) {
// transactions works on both random and sequential devices
dev->startTransaction();
auto ba = dev->read(RasHeader::SIZE);
dev->rollbackTransaction();
QDataStream s(ba);
QDataStream s(dev->peek(RasHeader::SIZE));
s.setByteOrder(QDataStream::BigEndian);
s >> header;
if (s.status() == QDataStream::Ok && IsSupported(header)) {

View File

@ -433,7 +433,7 @@ inline void rgbToRgbX(uchar *target, const uchar *source, qint32 targetSize, qin
#define T_FLAGS(a) (((a) >> 31) & 0x1)
// clang-format on
#define DEFAULT_QUALITY (C_IQ(3) | C_OC(1) | C_CW(1) | C_AW(1) | C_BT(1) | C_HS(0) | C_FLAGS(1))
#define DEFAULT_IMAGE_QUALITY (C_IQ(3) | C_OC(1) | C_CW(1) | C_AW(1) | C_BT(1) | C_HS(0) | C_FLAGS(1))
void setParams(QImageIOHandler *handler, LibRaw *rawProcessor)
{
@ -497,7 +497,7 @@ void setParams(QImageIOHandler *handler, LibRaw *rawProcessor)
quality |= C_FLAGS(1);
}
if (quality == -1) {
quality = DEFAULT_QUALITY;
quality = DEFAULT_IMAGE_QUALITY;
}
Q_ASSERT(T_FLAGS(quality));
@ -806,12 +806,10 @@ QVariant RAWHandler::option(ImageOption option) const
rawProcessor->imgdata.rawparams.shot_select = currentImageNumber();
#endif
if (rawProcessor->open_datastream(&stream) == LIBRAW_SUCCESS) {
if (rawProcessor->unpack() == LIBRAW_SUCCESS) {
auto w = libraw_get_iwidth(&rawProcessor->imgdata);
auto h = libraw_get_iheight(&rawProcessor->imgdata);
// flip & 4: taken from LibRaw code
v = (rawProcessor->imgdata.sizes.flip & 4) ? QSize(h, w) : QSize(w, h);
}
auto w = libraw_get_iwidth(&rawProcessor->imgdata);
auto h = libraw_get_iheight(&rawProcessor->imgdata);
// flip & 4: taken from LibRaw code
v = (rawProcessor->imgdata.sizes.flip & 4) ? QSize(h, w) : QSize(w, h);
}
d->rollbackTransaction();
}

View File

@ -22,6 +22,8 @@
#include "rgb_p.h"
#include "util_p.h"
#include <cstring>
#include <QList>
#include <QMap>
@ -72,15 +74,25 @@ private:
uint _offset;
};
class SGIImage
class SGIImagePrivate
{
public:
SGIImage(QIODevice *device);
~SGIImage();
SGIImagePrivate();
~SGIImagePrivate();
bool readImage(QImage &);
bool writeImage(const QImage &);
bool isValid() const;
bool isSupported() const;
bool peekHeader(QIODevice *device);
QSize size() const;
QImage::Format format() const;
void setDevice(QIODevice *device);
private:
enum {
NORMAL,
@ -91,16 +103,19 @@ private:
QIODevice *_dev;
QDataStream _stream;
quint8 _rle;
quint8 _bpc;
quint16 _dim;
quint16 _xsize;
quint16 _ysize;
quint16 _zsize;
quint32 _pixmin;
quint32 _pixmax;
quint16 _magic = 0;
quint8 _rle = 0;
quint8 _bpc = 0;
quint16 _dim = 0;
quint16 _xsize = 0;
quint16 _ysize = 0;
quint16 _zsize = 0;
quint32 _pixmin = 0;
quint32 _pixmax = 0;
char _imagename[80];
quint32 _colormap;
quint32 _colormap = 0;
quint8 _unused[404];
quint32 _unused32 = 0;
quint32 *_starttab;
quint32 *_lengthtab;
@ -112,24 +127,28 @@ private:
bool readData(QImage &);
bool getRow(uchar *dest);
bool readHeader();
void writeHeader();
void writeRle();
void writeVerbatim(const QImage &);
static bool readHeader(QDataStream &ds, SGIImagePrivate *sgi);
bool writeHeader();
bool writeRle();
bool writeVerbatim(const QImage &);
bool scanData(const QImage &);
uint compact(uchar *, uchar *);
uchar intensity(uchar);
};
SGIImage::SGIImage(QIODevice *io)
: _starttab(nullptr)
SGIImagePrivate::SGIImagePrivate()
: _dev(nullptr)
, _starttab(nullptr)
, _lengthtab(nullptr)
{
_dev = io;
_stream.setDevice(_dev);
std::memset(_imagename, 0, sizeof(_imagename));
std::memset(_unused, 0, sizeof(_unused));
}
SGIImage::~SGIImage()
SGIImagePrivate::~SGIImagePrivate()
{
delete[] _starttab;
delete[] _lengthtab;
@ -137,7 +156,13 @@ SGIImage::~SGIImage()
///////////////////////////////////////////////////////////////////////////////
bool SGIImage::getRow(uchar *dest)
void SGIImagePrivate::setDevice(QIODevice *device)
{
_dev = device;
_stream.setDevice(_dev);
}
bool SGIImagePrivate::getRow(uchar *dest)
{
int n;
int i;
@ -180,7 +205,7 @@ bool SGIImage::getRow(uchar *dest)
return i == _xsize;
}
bool SGIImage::readData(QImage &img)
bool SGIImagePrivate::readData(QImage &img)
{
QRgb *c;
quint32 *start = _starttab;
@ -258,66 +283,9 @@ bool SGIImage::readData(QImage &img)
return true;
}
bool SGIImage::readImage(QImage &img)
bool SGIImagePrivate::readImage(QImage &img)
{
qint8 u8;
qint16 u16;
qint32 u32;
// qDebug() << "reading rgb ";
// magic
_stream >> u16;
if (u16 != 0x01da) {
return false;
}
// verbatim/rle
_stream >> _rle;
// qDebug() << (_rle ? "RLE" : "verbatim");
if (_rle > 1) {
return false;
}
// bytes per channel
_stream >> _bpc;
// qDebug() << "bytes per channel: " << int(_bpc);
if (_bpc == 1) {
;
} else if (_bpc == 2) {
// qDebug() << "dropping least significant byte";
} else {
return false;
}
// number of dimensions
_stream >> _dim;
// qDebug() << "dimensions: " << _dim;
if (_dim < 1 || _dim > 3) {
return false;
}
_stream >> _xsize >> _ysize >> _zsize >> _pixmin >> _pixmax >> u32;
// qDebug() << "x: " << _xsize;
// qDebug() << "y: " << _ysize;
// qDebug() << "z: " << _zsize;
// name
_stream.readRawData(_imagename, 80);
_imagename[79] = '\0';
_stream >> _colormap;
// qDebug() << "colormap: " << _colormap;
if (_colormap != NORMAL) {
return false; // only NORMAL supported
}
for (int i = 0; i < 404; i++) {
_stream >> u8;
}
if (_dim == 1) {
// qDebug() << "1-dimensional images aren't supported yet";
if (!readHeader() || !isSupported()) {
return false;
}
@ -325,19 +293,13 @@ bool SGIImage::readImage(QImage &img)
return false;
}
img = imageAlloc(_xsize, _ysize, QImage::Format_RGB32);
img = imageAlloc(size(), format());
if (img.isNull()) {
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(_xsize, _ysize);
return false;
}
if (_zsize == 0) {
return false;
}
if (_zsize == 2 || _zsize == 4) {
img = img.convertToFormat(QImage::Format_ARGB32);
} else if (_zsize > 4) {
if (_zsize > 4) {
// qDebug() << "using first 4 of " << _zsize << " channels";
// Only let this continue if it won't cause a int overflow later
// this is most likely a broken file anyway
@ -435,7 +397,7 @@ QList<const RLEData *> RLEMap::vector()
return v;
}
uchar SGIImage::intensity(uchar c)
uchar SGIImagePrivate::intensity(uchar c)
{
if (c < _pixmin) {
_pixmin = c;
@ -446,7 +408,7 @@ uchar SGIImage::intensity(uchar c)
return c;
}
uint SGIImage::compact(uchar *d, uchar *s)
uint SGIImagePrivate::compact(uchar *d, uchar *s)
{
uchar *dest = d;
uchar *src = s;
@ -489,7 +451,7 @@ uint SGIImage::compact(uchar *d, uchar *s)
return dest - d;
}
bool SGIImage::scanData(const QImage &img)
bool SGIImagePrivate::scanData(const QImage &img)
{
quint32 *start = _starttab;
QByteArray lineguard(_xsize * 2, 0);
@ -575,13 +537,105 @@ bool SGIImage::scanData(const QImage &img)
return true;
}
void SGIImage::writeHeader()
bool SGIImagePrivate::isValid() const
{
_stream << quint16(0x01da);
// File signature/magic number
if (_magic != 0x01da) {
return false;
}
// Compression, 0 = Uncompressed, 1 = RLE compressed
if (_rle > 1) {
return false;
}
// Bytes per pixel, 1 = 8 bit, 2 = 16 bit
if (_bpc != 1 && _bpc != 2) {
return false;
}
// Image dimension, 3 for RGBA image
if (_dim < 1 || _dim > 3) {
return false;
}
// Number channels in the image file, 4 for RGBA image
if (_zsize < 1) {
return false;
}
return true;
}
bool SGIImagePrivate::isSupported() const
{
if (!isValid()) {
return false;
}
if (_colormap != NORMAL) {
return false; // only NORMAL supported
}
if (_dim == 1) {
return false;
}
return true;
}
bool SGIImagePrivate::peekHeader(QIODevice *device)
{
QDataStream ds(device->peek(512));
return SGIImagePrivate::readHeader(ds, this) && isValid();
}
QSize SGIImagePrivate::size() const
{
return QSize(_xsize, _ysize);
}
QImage::Format SGIImagePrivate::format() const
{
if (_zsize == 2 || _zsize == 4) {
return QImage::Format_ARGB32;
}
return QImage::Format_RGB32;
}
bool SGIImagePrivate::readHeader()
{
return readHeader(_stream, this);
}
bool SGIImagePrivate::readHeader(QDataStream &ds, SGIImagePrivate *sgi)
{
// magic
ds >> sgi->_magic;
// verbatim/rle
ds >> sgi->_rle;
// bytes per channel
ds >> sgi->_bpc;
// number of dimensions
ds >> sgi->_dim;
ds >> sgi->_xsize >> sgi->_ysize >> sgi->_zsize >> sgi->_pixmin >> sgi->_pixmax >> sgi->_unused32;
// name
ds.readRawData(sgi->_imagename, 80);
sgi->_imagename[79] = '\0';
ds >> sgi->_colormap;
for (size_t i = 0; i < sizeof(_unused); i++) {
ds >> sgi->_unused[i];
}
return ds.status() == QDataStream::Ok;
}
bool SGIImagePrivate::writeHeader()
{
_stream << _magic;
_stream << _rle << _bpc << _dim;
_stream << _xsize << _ysize << _zsize;
_stream << _pixmin << _pixmax;
_stream << quint32(0);
_stream << _unused32;
for (int i = 0; i < 80; i++) {
_imagename[i] = '\0';
@ -589,16 +643,20 @@ void SGIImage::writeHeader()
_stream.writeRawData(_imagename, 80);
_stream << _colormap;
for (int i = 0; i < 404; i++) {
_stream << quint8(0);
for (size_t i = 0; i < sizeof(_unused); i++) {
_stream << _unused[i];
}
return _stream.status() == QDataStream::Ok;
}
void SGIImage::writeRle()
bool SGIImagePrivate::writeRle()
{
_rle = 1;
// qDebug() << "writing RLE data";
writeHeader();
if (!writeHeader()) {
return false;
}
uint i;
// write start table
@ -615,13 +673,16 @@ void SGIImage::writeRle()
for (i = 0; (int)i < _rlevector.size(); i++) {
const_cast<RLEData *>(_rlevector[i])->write(_stream);
}
return _stream.status() == QDataStream::Ok;
}
void SGIImage::writeVerbatim(const QImage &img)
bool SGIImagePrivate::writeVerbatim(const QImage &img)
{
_rle = 0;
// qDebug() << "writing verbatim data";
writeHeader();
if (!writeHeader()) {
return false;
}
const QRgb *c;
unsigned x;
@ -635,7 +696,7 @@ void SGIImage::writeVerbatim(const QImage &img)
}
if (_zsize == 1) {
return;
return _stream.status() == QDataStream::Ok;
}
if (_zsize != 2) {
@ -654,7 +715,7 @@ void SGIImage::writeVerbatim(const QImage &img)
}
if (_zsize == 3) {
return;
return _stream.status() == QDataStream::Ok;
}
}
@ -664,9 +725,11 @@ void SGIImage::writeVerbatim(const QImage &img)
_stream << quint8(qAlpha(*c++));
}
}
return _stream.status() == QDataStream::Ok;
}
bool SGIImage::writeImage(const QImage &image)
bool SGIImagePrivate::writeImage(const QImage &image)
{
// qDebug() << "writing "; // TODO add filename
QImage img = image;
@ -698,6 +761,7 @@ bool SGIImage::writeImage(const QImage &image)
return false;
}
_magic = 0x01da;
_bpc = 1;
_xsize = w;
_ysize = h;
@ -722,16 +786,16 @@ bool SGIImage::writeImage(const QImage &image)
}
if (verbatim_size <= rle_size) {
writeVerbatim(img);
} else {
writeRle();
return writeVerbatim(img);
}
return true;
return writeRle();
}
///////////////////////////////////////////////////////////////////////////////
RGBHandler::RGBHandler()
: QImageIOHandler()
, d(new SGIImagePrivate)
{
}
@ -746,14 +810,54 @@ bool RGBHandler::canRead() const
bool RGBHandler::read(QImage *outImage)
{
SGIImage sgi(device());
return sgi.readImage(*outImage);
d->setDevice(device());
return d->readImage(*outImage);
}
bool RGBHandler::write(const QImage &image)
{
SGIImage sgi(device());
return sgi.writeImage(image);
d->setDevice(device());
return d->writeImage(image);
}
bool RGBHandler::supportsOption(ImageOption option) const
{
if (option == QImageIOHandler::Size) {
return true;
}
if (option == QImageIOHandler::ImageFormat) {
return true;
}
return false;
}
QVariant RGBHandler::option(ImageOption option) const
{
QVariant v;
if (option == QImageIOHandler::Size) {
auto &&sgi = d;
if (sgi->isSupported()) {
v = QVariant::fromValue(sgi->size());
} else if (auto dev = device()) {
if (d->peekHeader(dev) && sgi->isSupported()) {
v = QVariant::fromValue(sgi->size());
}
}
}
if (option == QImageIOHandler::ImageFormat) {
auto &&sgi = d;
if (sgi->isSupported()) {
v = QVariant::fromValue(sgi->format());
} else if (auto dev = device()) {
if (d->peekHeader(dev) && sgi->isSupported()) {
v = QVariant::fromValue(sgi->format());
}
}
}
return v;
}
bool RGBHandler::canRead(QIODevice *device)
@ -763,20 +867,8 @@ bool RGBHandler::canRead(QIODevice *device)
return false;
}
const qint64 oldPos = device->pos();
const QByteArray head = device->readLine(64);
int readBytes = head.size();
if (device->isSequential()) {
while (readBytes > 0) {
device->ungetChar(head[readBytes-- - 1]);
}
} else {
device->seek(oldPos);
}
return head.size() >= 4 && head.startsWith("\x01\xda") && (head[2] == 0 || head[2] == 1) && (head[3] == 1 || head[3] == 2);
SGIImagePrivate sgi;
return sgi.peekHeader(device) && sgi.isSupported();
}
///////////////////////////////////////////////////////////////////////////////

View File

@ -9,7 +9,9 @@
#define KIMG_RGB_P_H
#include <QImageIOPlugin>
#include <QScopedPointer>
class SGIImagePrivate;
class RGBHandler : public QImageIOHandler
{
public:
@ -19,7 +21,13 @@ public:
bool read(QImage *image) override;
bool write(const QImage &image) override;
bool supportsOption(QImageIOHandler::ImageOption option) const override;
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
private:
QScopedPointer<SGIImagePrivate> d;
};
class RGBPlugin : public QImageIOPlugin

View File

@ -174,15 +174,21 @@ static QImage::Format imageFormat(const TgaHeader &head)
{
auto format = QImage::Format_Invalid;
if (IsSupported(head)) {
TgaHeaderInfo info(head);
// Bits 0-3 are the numbers of alpha bits (can be zero!)
const int numAlphaBits = head.flags & 0xf;
// However alpha exists only in the 32 bit format.
if ((head.pixel_size == 32) && (head.flags & 0xf)) {
// However alpha should exists only in the 32 bit format.
if ((head.pixel_size == 32) && (numAlphaBits)) {
if (numAlphaBits <= 8) {
format = QImage::Format_ARGB32;
}
}
else {
// Anyway, GIMP also saves gray images with alpha in TGA format
} else if((info.grey) && (head.pixel_size == 16) && (numAlphaBits)) {
if (numAlphaBits == 8) {
format = QImage::Format_ARGB32;
}
} else {
format = QImage::Format_RGB32;
}
}
@ -195,26 +201,13 @@ static QImage::Format imageFormat(const TgaHeader &head)
*/
static bool peekHeader(QIODevice *device, TgaHeader &header)
{
qint64 oldPos = device->pos();
QByteArray head = device->read(TgaHeader::SIZE);
int readBytes = head.size();
if (device->isSequential()) {
for (int pos = readBytes - 1; pos >= 0; --pos) {
device->ungetChar(head[pos]);
}
} else {
device->seek(oldPos);
}
if (readBytes < TgaHeader::SIZE) {
auto head = device->peek(TgaHeader::SIZE);
if (head.size() < TgaHeader::SIZE) {
return false;
}
QDataStream stream(head);
stream.setByteOrder(QDataStream::LittleEndian);
stream >> header;
return true;
}
@ -361,8 +354,7 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
uchar *src = image;
for (int y = y_start; y != y_end; y += y_step) {
QRgb *scanline = (QRgb *)img.scanLine(y);
auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
if (info.pal) {
// Paletted.
for (int x = 0; x < tga.width; x++) {
@ -372,8 +364,14 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
} else if (info.grey) {
// Greyscale.
for (int x = 0; x < tga.width; x++) {
scanline[x] = qRgb(*src, *src, *src);
src++;
if (tga.pixel_size == 16) {
scanline[x] = qRgba(*src, *src, *src, *(src + 1));
src += 2;
}
else {
scanline[x] = qRgb(*src, *src, *src);
src++;
}
}
} else {
// True Color.
@ -561,22 +559,6 @@ bool TGAHandler::canRead(QIODevice *device)
return false;
}
qint64 oldPos = device->pos();
QByteArray head = device->read(TgaHeader::SIZE);
int readBytes = head.size();
if (device->isSequential()) {
for (int pos = readBytes - 1; pos >= 0; --pos) {
device->ungetChar(head[pos]);
}
} else {
device->seek(oldPos);
}
if (readBytes < TgaHeader::SIZE) {
return false;
}
TgaHeader tga;
if (!peekHeader(device, tga)) {
qWarning("TGAHandler::canRead() error while reading the header");

View File

@ -24,15 +24,22 @@
#define META_KEY_HOSTCOMPUTER "HostComputer"
#define META_KEY_LATITUDE "Latitude"
#define META_KEY_LONGITUDE "Longitude"
#define META_KEY_HOSTCOMPUTER "HostComputer"
#define META_KEY_MANUFACTURER "Manufacturer"
#define META_KEY_MODEL "Model"
#define META_KEY_OWNER "Owner"
#define META_KEY_SOFTWARE "Software"
#define META_KEY_TITLE "Title"
#define META_KEY_XML_GIMP "XML:org.gimp.xml"
#define META_KEY_XMP_ADOBE "XML:com.adobe.xmp"
// Camera info metadata keys
#define META_KEY_MANUFACTURER "Manufacturer"
#define META_KEY_MODEL "Model"
#define META_KEY_SERIALNUMBER "SerialNumber"
// Lens info metadata keys
#define META_KEY_LENS_MANUFACTURER "LensManufacturer"
#define META_KEY_LENS_MODEL "LensModel"
#define META_KEY_LENS_SERIALNUMBER "LensSerialNumber"
// QList uses some extra space for stuff, hence the 32 here suggested by Thiago Macieira
static constexpr int kMaxQVectorSize = std::numeric_limits<int>::max() - 32;

View File

@ -1672,8 +1672,12 @@ bool XCFImageFormat::assignImageBytes(Layer &layer, uint i, uint j, const GimpPr
for (int y = 0; y < height; y++) {
uchar *dataPtr = bits + y * bytesPerLine;
uchar *alphaPtr = nullptr;
if (!layer.alpha_tiles.isEmpty())
alphaPtr = layer.alpha_tiles[j][i].scanLine(y);
if (layer.alpha_tiles.size() > j && layer.alpha_tiles.at(j).size() > i) {
QImage &alphaTile = layer.alpha_tiles[j][i];
if (alphaTile.width() >= width && alphaTile.height() > y) {
alphaPtr = alphaTile.scanLine(y);
}
}
if (bpc == 4) {
#ifdef USE_FLOAT_IMAGES
if (precision < GimpPrecision::GIMP_PRECISION_HALF_LINEAR) {
@ -1970,6 +1974,12 @@ static bool convertFloatTo16Bit(uchar *output, quint64 outputSize, uchar *input)
*/
bool XCFImageFormat::loadLevel(QDataStream &xcf_io, Layer &layer, qint32 bpp, const GimpPrecision precision)
{
auto bpc = bytesPerChannel(precision);
if ((bpc == 0) || (bpp % bpc)) {
qCDebug(XCFPLUGIN) << "XCF: the stream seems corrupted";
return false;
}
qint32 width;
qint32 height;
@ -4219,66 +4229,43 @@ bool XCFHandler::canRead(QIODevice *device)
}
const qint64 oldPos = device->pos();
if (!device->isSequential()) {
QDataStream ds(device);
XCFImageFormat::XCFImage::Header header;
bool failed = !XCFImageFormat::readXCFHeader(ds, &header);
ds.setDevice(nullptr);
device->seek(oldPos);
if (failed) {
return false;
}
switch (header.precision) {
case XCFImageFormat::GIMP_PRECISION_HALF_LINEAR:
case XCFImageFormat::GIMP_PRECISION_HALF_NON_LINEAR:
case XCFImageFormat::GIMP_PRECISION_HALF_PERCEPTUAL:
case XCFImageFormat::GIMP_PRECISION_FLOAT_LINEAR:
case XCFImageFormat::GIMP_PRECISION_FLOAT_NON_LINEAR:
case XCFImageFormat::GIMP_PRECISION_FLOAT_PERCEPTUAL:
case XCFImageFormat::GIMP_PRECISION_U8_LINEAR:
case XCFImageFormat::GIMP_PRECISION_U8_NON_LINEAR:
case XCFImageFormat::GIMP_PRECISION_U8_PERCEPTUAL:
case XCFImageFormat::GIMP_PRECISION_U16_LINEAR:
case XCFImageFormat::GIMP_PRECISION_U16_NON_LINEAR:
case XCFImageFormat::GIMP_PRECISION_U16_PERCEPTUAL:
case XCFImageFormat::GIMP_PRECISION_U32_LINEAR:
case XCFImageFormat::GIMP_PRECISION_U32_NON_LINEAR:
case XCFImageFormat::GIMP_PRECISION_U32_PERCEPTUAL:
break;
case XCFImageFormat::GIMP_PRECISION_DOUBLE_LINEAR:
case XCFImageFormat::GIMP_PRECISION_DOUBLE_NON_LINEAR:
case XCFImageFormat::GIMP_PRECISION_DOUBLE_PERCEPTUAL:
default:
qCDebug(XCFPLUGIN) << "unsupported precision" << header.precision;
return false;
}
QDataStream ds(device);
XCFImageFormat::XCFImage::Header header;
bool failed = !XCFImageFormat::readXCFHeader(ds, &header);
ds.setDevice(nullptr);
return true;
}
char head[8];
qint64 readBytes = device->read(head, sizeof(head));
if (readBytes != sizeof(head)) {
if (device->isSequential()) {
while (readBytes > 0) {
device->ungetChar(head[readBytes-- - 1]);
}
} else {
device->seek(oldPos);
}
device->seek(oldPos);
if (failed) {
return false;
}
if (device->isSequential()) {
while (readBytes > 0) {
device->ungetChar(head[readBytes-- - 1]);
}
} else {
device->seek(oldPos);
switch (header.precision) {
case XCFImageFormat::GIMP_PRECISION_HALF_LINEAR:
case XCFImageFormat::GIMP_PRECISION_HALF_NON_LINEAR:
case XCFImageFormat::GIMP_PRECISION_HALF_PERCEPTUAL:
case XCFImageFormat::GIMP_PRECISION_FLOAT_LINEAR:
case XCFImageFormat::GIMP_PRECISION_FLOAT_NON_LINEAR:
case XCFImageFormat::GIMP_PRECISION_FLOAT_PERCEPTUAL:
case XCFImageFormat::GIMP_PRECISION_U8_LINEAR:
case XCFImageFormat::GIMP_PRECISION_U8_NON_LINEAR:
case XCFImageFormat::GIMP_PRECISION_U8_PERCEPTUAL:
case XCFImageFormat::GIMP_PRECISION_U16_LINEAR:
case XCFImageFormat::GIMP_PRECISION_U16_NON_LINEAR:
case XCFImageFormat::GIMP_PRECISION_U16_PERCEPTUAL:
case XCFImageFormat::GIMP_PRECISION_U32_LINEAR:
case XCFImageFormat::GIMP_PRECISION_U32_NON_LINEAR:
case XCFImageFormat::GIMP_PRECISION_U32_PERCEPTUAL:
break;
case XCFImageFormat::GIMP_PRECISION_DOUBLE_LINEAR:
case XCFImageFormat::GIMP_PRECISION_DOUBLE_NON_LINEAR:
case XCFImageFormat::GIMP_PRECISION_DOUBLE_PERCEPTUAL:
default:
qCDebug(XCFPLUGIN) << "unsupported precision" << header.precision;
return false;
}
return qstrncmp(head, "gimp xcf", 8) == 0;
return true;
}
QImageIOPlugin::Capabilities XCFPlugin::capabilities(QIODevice *device, const QByteArray &format) const