Compare commits

..

42 Commits

Author SHA1 Message Date
Mirco Miranda
f728b87ae8 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-11 23:34:28 +02:00
Nicolas Fella
bb17f7bf84 update version for new release 2024-07-05 13:19:28 +02:00
Mirco Miranda
b849e48ef4 Fixed wrong plugin options behaviour
While working on MR !230 I noticed that the options read I entered into several plugins could not be read after reading the image.

**The patch fixes problems reading options in plugins and adds option checking in the readtest.cpp.**

In particular, the reading test does the following additional actions:
- reads options before reading the image;
- compare the options read with the options returned by the reader after reading the image;
- compares the format and size of the returned image with the format and size returned by the reader.
2024-06-19 22:18:45 +00:00
Mirco Miranda
81b7263d73 EXR: Full support for gray image/colorspace
Starting with Qt 6.8, QColorSpace supports Gray and CMYK color profiles.

- On saving, grayscale images are converted to linear gray profile;
- On loading, a Grayscale image is stored in a QImage::Format_Grayscale16 instead a RGB one;
- ScanlineConverter class was updated to gray conversions.
2024-06-17 15:17:51 +00:00
Mirco Miranda
63e21ee5f3 Disable JXR plugin due to crashes in JXRLIB
I ran a simple fuzzer on all the plugins in the repo and the JXR one crashes every few seconds. I attach some files (I have many more) that cause the crashes. For the moment I think it's best to keep it deactivated.

The strange thing is that for the same plugin I had created the PR on oss-fuzz which ran locally for over an hour without problems. I'm a bit confused.
2024-06-13 22:24:19 +00:00
Mirco Miranda
06f097046c PFM: Portable FloatMap read only support
Simple HDR format supported by GIMP (read/write) and Photoshop (read/write).
2024-06-13 22:20:40 +00:00
Mirco Miranda
950ed43623 PXR: Pixar raster read only support
Limited read only support to Pixar raster as supported by Photoshop (RGB and Gray 8-bit only).
2024-06-12 22:42:08 +00:00
Mirco Miranda
863c424390 Sanity checks (fuzzer)
I ran a stupid fuzzer on all the plugins in the repo and some plugins needs more sanity checks.

- RAS: fixed palette reading on corrupted files
- RGB: improved error detection on datastream

This patch improves the reading speed of some corrupted files and limit the maximum memory allocation of RAS palette.
2024-06-11 22:15:27 +00:00
Mirco Miranda
bd083ff354 XCF: fixed wrong composite on Grayscale images
CCBUG: 476755
2024-06-10 20:16:31 +00:00
Albert Astals Cid
99663607b2 Fix compilation 2024-06-07 15:08:53 +00:00
Mirco Miranda
7499e3b8d4 Use of metadata macro definitions
Replaced the metadata string with the common macro definition
2024-06-07 15:08:37 +00:00
Mirco Miranda
cb5ca7fc48 JXR: added CMYK image to read test
Improved read test to skip unsupported image format: the CMYK image test only works if you compile with Qt 6.8 or higher.
2024-06-07 14:09:34 +00:00
Mirco Miranda
4f61e3912c XCF: Increased maximum property size
CCBUG: 426222

The problem was caused by a check on the maximum size of properties (specifically it failed on PROP_PARASITES).
2024-06-07 11:43:31 +00:00
Mirco Miranda
b8a9c75c80 JXR support
CCBUG: 451584

An implementation of the JXR format.
2024-06-07 10:35:25 +00:00
Mirco Miranda
4995c9cd15 PSD: support native CMYK introduced by Qt 6.8
Qt 6.8 will introduce native support for the CMYK (8-bit) format.
With this patch you will finally be able to correctly see the colors of CMYK images with ICC profile.
The testing part has been updated with the addition of an (optional) json file for each image to test. Inside you enter which image to use depending on the Qt version.

In short:
- Added native CMYK suport to PSD reader
- CMYK with alpha is converted using QColorSpace in a RGBA image
- Read tests changed to use the correct comparison image based on the Qt version
- Fixed also XCF tests: now works with all Qt version (see also [QTBUG-120614](https://bugreports.qt.io/browse/QTBUG-120614))
- Work around for CCBUG: 468288
2024-06-07 10:16:58 +00:00
Nicolas Fella
a54c5e876c update version for new release 2024-05-31 17:41:40 +02:00
Nicolas Fella
6c1a7ad339 update version for new release 2024-05-31 15:15:35 +02:00
Nicolas Fella
c721fa481b Remove explicit maintainer from metainfo
All frameworks are maintained by the KDE community
2024-05-12 21:27:40 +02:00
Nicolas Fella
ea15fed399 update version for new release 2024-05-12 14:06:57 +02:00
Ben Cooksley
c2fabef501 Ensure dependencies are provided on Android 2024-05-06 00:02:54 +12:00
Nicolas Fella
5b2c190823 update version for new release 2024-05-03 11:49:56 +02:00
Jonathan Riddell
1b94554323 update version for new release 2024-04-12 16:12:29 +01:00
Antonio Rojas
c2c12b1d7e Fix build with Qt 6.7 on 32 bits
Qt changed the argument type again in 0ed34d1992
2024-04-11 08:30:45 +02:00
Jonathan Riddell
c169296fbf update version for new release 2024-04-05 11:45:58 +01:00
Friedrich W. H. Kossebau
29aec82e67 Add KF_VERSION & KF_DEP_VERSION variables
For consistency with the other KF modules, but also to have some version
info with working copies/checkouts, as well as min required KF deps.
2024-03-18 16:24:41 +01:00
Mirco Miranda
95ee381195 XCF: testcase update for fixed Qt
Updated testcases to work with Qt without alpha [bug](https://bugreports.qt.io/browse/QTBUG-120614.).

Needs Qt >= 6.6.2 for tests to pass
2024-03-14 21:49:09 +00:00
Mirco Miranda
8e5951471d TGA: added options support
- Added Size and Format options support
- Fixed a double image allocation when reading RGBA images (RGB was always allocated and then replaced by RGBA one)
- Fixed the code for sequential devices

The Size option is used by the KIO 5 thumbnailer to avoid to use too memory. A backport to KF5 would serve CCBUG: 413801 and CCBUG: 479612
2024-03-04 23:47:59 +00:00
Mirco Miranda
0710bc65f6 More header checks (CCBUG: 479612) 2024-02-29 15:55:57 +01:00
Jonathan Esk-Riddell
4be09ba419 update version for new release 2024-02-21 11:15:16 +00:00
Mirco Miranda
6cbdf9cf54 Workaround QTBUG-120614 2024-02-09 07:55:28 +00:00
Jonathan Esk-Riddell
7d6de20e8c update version for new release 2024-02-01 09:26:20 +00:00
David Faure
b37c991e39 Port to ECMFeatureSummary
This avoids a feature summary in the middle of the cmake configuration
when this module is used as a git submodule.

GIT_SILENT
2024-01-26 14:17:50 +01:00
Jonathan Esk-Riddell
249046f25d update version for new release 2024-01-10 11:27:02 +00:00
Mirco Miranda
9f7b1b8dee ScanlineConverter: fix indexed conversion and support for source depth less than 24 bits 2024-01-06 09:04:06 +01:00
Mirco Miranda
f065104b72 Less space used when saving a grayscale image 2024-01-06 08:20:14 +01:00
Antonio Rojas
f34185197a Fix build with Qt 6.7 2023-12-20 18:32:28 +01:00
Mirco Miranda
9f24023ca7 The maximum number of channels explained better
Just a clarification on why the maximum value of channels present in the specifications is not used.
2023-12-20 16:43:12 +00:00
Laurent Montel
8d1ef536be Code is qt6 only now. Remove unused check 2023-12-16 11:16:53 +00:00
Daniel Novomeský
da8ed31aec avif: new quality settings
Prior libavif 1.0, quality was set using two numbers
(minQuantizer and maxQuantizer).
New versions of libavif recommend using
new/single quality parameter instead.
Compression using the new settings is different
compared to the old.
For example, new quality 68 gives similar compression
like old default 52.
2023-12-13 17:40:21 +01:00
Daniel Novomeský
ce8383e5fd HEIF plug-in extended to support HEJ2 format
HEJ2 is JPEG 2000 codec encapsulated in HEIF container.
Only HEJ2 reading is implemented.
2023-12-07 13:26:43 +01:00
Laurent Montel
db0adee62f GIT_SILENT: Port to new CI template 2023-11-20 07:14:46 +01:00
Volker Krause
91a342e90d Cleanup pre-Qt 6.5 code 2023-11-19 14:14:36 +01:00
122 changed files with 3713 additions and 365 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

@@ -2,7 +2,9 @@
# SPDX-License-Identifier: CC0-1.0
include:
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
- project: sysadmin/ci-utilities
file:
- /gitlab-templates/linux-qt6.yml
- /gitlab-templates/android-qt6.yml
- /gitlab-templates/freebsd-qt6.yml
- /gitlab-templates/windows-qt6.yml

View File

@@ -1,5 +1,5 @@
Dependencies:
- 'on': ['Linux', 'FreeBSD', 'macOS', 'Windows']
- 'on': ['Linux', 'FreeBSD', 'macOS', 'Windows', 'Android']
'require':
'frameworks/extra-cmake-modules': '@same'
'frameworks/karchive' : '@same'

View File

@@ -1,9 +1,11 @@
cmake_minimum_required(VERSION 3.16)
project(KImageFormats)
set(KF_VERSION "6.4.0") # handled by release scripts
set(KF_DEP_VERSION "6.4.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION})
include(FeatureSummary)
find_package(ECM 5.240.0 NO_MODULE)
find_package(ECM 6.4.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)
@@ -22,7 +24,7 @@ include(FindPkgConfig)
set(REQUIRED_QT_VERSION 6.5.0)
find_package(Qt6Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
find_package(KF6Archive)
find_package(KF6Archive ${KF_DEP_VERSION})
set_package_properties(KF6Archive PROPERTIES
TYPE OPTIONAL
PURPOSE "Required for the QImage plugin for Krita and OpenRaster images"
@@ -73,15 +75,20 @@ if(KIMAGEFORMATS_JXL)
endif()
add_feature_info(LibJXL LibJXL_FOUND "required for the QImage plugin for JPEG XL images")
# note: module FindLibRaw missing from https://invent.kde.org/frameworks/extra-cmake-modules
find_package(LibRaw 0.20.2)
set_package_properties(LibRaw PROPERTIES
TYPE OPTIONAL
PURPOSE "Required for the QImage plugin for RAW images"
)
option(KIMAGEFORMATS_JXR "Enable plugin for JPEG XR format" OFF)
if(KIMAGEFORMATS_JXR)
find_package(LibJXR)
endif()
add_feature_info(LibJXR LibJXR_FOUND "required for the QImage plugin for JPEG XR images")
ecm_set_disabled_deprecation_versions(
QT 6.4
QT 6.5
KF 5.102
)
@@ -91,6 +98,7 @@ if (BUILD_TESTING)
add_subdirectory(tests)
endif()
feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
include(ECMFeatureSummary)
ecm_feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT)

View File

@@ -18,16 +18,19 @@ The following image formats have read-only support:
- Gimp (xcf)
- Krita (kra)
- OpenRaster (ora)
- Pixar raster (pxr)
- Portable FloatMap (pfm)
- Photoshop documents (psd, psb, pdd, psdt)
- Radiance HDR (hdr)
- Sun Raster (im1, im8, im24, im32, ras, sun)
The following image formats have read and write support:
- AV1 Image File Format (AVIF)
- AV1 Image File Format (avif)
- Encapsulated PostScript (eps)
- High Efficiency Image File Format (heif). Can be enabled with the KIMAGEFORMATS_HEIF build option.
- JPEG XL (jxl)
- JPEG XR (jxr). Can be enabled with the KIMAGEFORMATS_JXR build option.
- OpenEXR (exr)
- Personal Computer Exchange (pcx)
- Quite OK Image format (qoi)

View File

@@ -11,7 +11,7 @@ macro(kimageformats_read_tests)
endif()
if (NOT TARGET readtest)
add_executable(readtest readtest.cpp)
add_executable(readtest readtest.cpp templateimage.cpp)
target_link_libraries(readtest Qt6::Gui)
target_compile_definitions(readtest
PRIVATE IMAGEDIR="${CMAKE_CURRENT_SOURCE_DIR}/read")
@@ -66,7 +66,9 @@ endmacro()
kimageformats_read_tests(
hdr
pcx
pfm
psd
pxr
qoi
ras
rgb
@@ -97,6 +99,12 @@ if (LibHeif_FOUND)
kimageformats_write_tests(FUZZ 1
heif-nodatacheck-lossless
)
if (LibHeif_VERSION VERSION_GREATER_EQUAL "1.17.0")
kimageformats_read_tests(FUZZ 1
hej2
)
endif()
endif()
if (LibJXL_FOUND AND LibJXLThreads_FOUND)
@@ -108,6 +116,15 @@ if (LibJXL_FOUND AND LibJXLThreads_FOUND)
)
endif()
if (LibJXR_FOUND)
kimageformats_read_tests(
jxr
)
kimageformats_write_tests(
jxr-nodatacheck
)
endif()
# Allow some fuzziness when reading this formats, to allow for
# rounding errors (eg: in alpha blending).
kimageformats_read_tests(FUZZ 1

BIN
autotests/read/exr/gray.exr Normal file

Binary file not shown.

BIN
autotests/read/exr/gray.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"minQtVersion" : "6.8.0",
"fileName" : "testcard_cmyk8.tif"
},
{
"maxQtVersion" : "6.7.99",
"unsupportedFormat" : true,
"comment" : "Qt versions lower than 6.8 do not support CMYK format so this test should be skipped."
}
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"minQtVersion" : "6.8.0",
"fileName" : "cmyk16_testcard_qt6_8.tif"
},
{
"minQtVersion" : "6.0.0",
"maxQtVersion" : "6.7.99",
"fileName" : "cmyk16_testcard.png"
}
]

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"minQtVersion" : "6.8.0",
"fileName" : "cmyk8_testcard_qt6_8.tif"
},
{
"minQtVersion" : "6.0.0",
"maxQtVersion" : "6.7.99",
"fileName" : "cmyk8_testcard.png"
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"minQtVersion" : "6.8.0",
"fileName" : "cmyka-16bits_qt6_8.png"
},
{
"minQtVersion" : "6.0.0",
"maxQtVersion" : "6.7.99",
"fileName" : "cmyka-16bits.png"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@@ -0,0 +1,11 @@
[
{
"minQtVersion" : "6.8.0",
"fileName" : "cmyka-8bits_qt6_8.png"
},
{
"minQtVersion" : "6.0.0",
"maxQtVersion" : "6.7.99",
"fileName" : "cmyka-8bits.png"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@@ -0,0 +1,11 @@
[
{
"minQtVersion" : "6.8.0",
"fileName" : "mch-16bits_qt_6_8.tif"
},
{
"minQtVersion" : "6.0.0",
"maxQtVersion" : "6.7.99",
"fileName" : "mch-16bits.png"
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"minQtVersion" : "6.8.0",
"fileName" : "mch-8bits_qt_6.8.tif"
},
{
"minQtVersion" : "6.0.0",
"maxQtVersion" : "6.7.99",
"fileName" : "mch-8bits.png"
}
]

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: 12 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -0,0 +1,32 @@
[
{
"minQtVersion" : "6.7.0",
"fileName" : "birthday16.png",
"seeAlso" : "https://bugreports.qt.io/browse/QTBUG-120614"
},
{
"minQtVersion" : "6.0.0",
"maxQtVersion" : "6.2.10",
"fileName" : "birthday16_alphabug.png"
},
{
"minQtVersion" : "6.3.0",
"maxQtVersion" : "6.3.2",
"fileName" : "birthday32_alphabug.png"
},
{
"minQtVersion" : "6.4.0",
"maxQtVersion" : "6.4.3",
"fileName" : "birthday32_alphabug.png"
},
{
"minQtVersion" : "6.5.0",
"maxQtVersion" : "6.5.4",
"fileName" : "birthday16_alphabug.png"
},
{
"minQtVersion" : "6.6.0",
"maxQtVersion" : "6.6.1",
"fileName" : "birthday16_alphabug.png"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -0,0 +1,32 @@
[
{
"minQtVersion" : "6.7.0",
"fileName" : "birthday32.png",
"seeAlso" : "https://bugreports.qt.io/browse/QTBUG-120614"
},
{
"minQtVersion" : "6.0.0",
"maxQtVersion" : "6.2.10",
"fileName" : "birthday32_alphabug.png"
},
{
"minQtVersion" : "6.3.0",
"maxQtVersion" : "6.3.2",
"fileName" : "birthday32_alphabug.png"
},
{
"minQtVersion" : "6.4.0",
"maxQtVersion" : "6.4.3",
"fileName" : "birthday32_alphabug.png"
},
{
"minQtVersion" : "6.5.0",
"maxQtVersion" : "6.5.4",
"fileName" : "birthday32_alphabug.png"
},
{
"minQtVersion" : "6.6.0",
"maxQtVersion" : "6.6.1",
"fileName" : "birthday32_alphabug.png"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

View File

@@ -16,6 +16,7 @@
#include <QTextStream>
#include "../tests/format-enum.h"
#include "templateimage.h"
#include "fuzzyeq.cpp"
@@ -89,13 +90,115 @@ static QImage::Format preferredFormat(QImage::Format fmt)
}
}
/*!
* \brief The OptionTest class
* Class for testing image options.
* Supports the most common options:
* - Size
* - ImageFormat
* - ImageTransformation (rotations)
* \todo Add missing options if needed.
*/
class OptionTest
{
public:
OptionTest()
: m_size(QSize())
, m_format(QImage::Format_Invalid)
, m_transformations(QImageIOHandler::TransformationNone)
{
}
OptionTest(const OptionTest&) = default;
OptionTest& operator =(const OptionTest&) = default;
/*!
* \brief store
* Stores the supported options of the reader.
* \param reader
* \return True on success, otherwise false.
*/
bool store(const QImageReader *reader = nullptr)
{
if (reader == nullptr) {
return false;
}
bool ok = true;
if (reader->supportsOption(QImageIOHandler::Size)) {
m_size = reader->size();
if (m_size.isEmpty())
ok = false;
}
if (reader->supportsOption(QImageIOHandler::ImageFormat)) {
m_format = reader->imageFormat();
if (m_format == QImage::Format_Invalid)
ok = false;
}
if (reader->supportsOption(QImageIOHandler::ImageTransformation)) {
m_transformations = reader->transformation();
if (m_transformations < 0 || m_transformations > 7)
ok = false;
}
return ok;
}
/*!
* \brief compare
* Compare the stored values with the ones read from the image reader.
* \param reader
* \return True on success, otherwise false.
*/
bool compare(const QImageReader *reader)
{
if (reader == nullptr) {
return false;
}
bool ok = true;
if (reader->supportsOption(QImageIOHandler::Size)) {
ok = ok && (m_size == reader->size());
}
if (reader->supportsOption(QImageIOHandler::ImageFormat)) {
ok = ok && (m_format == reader->imageFormat());
}
if (reader->supportsOption(QImageIOHandler::ImageTransformation)) {
ok = ok && (m_transformations == reader->transformation());
}
return ok;
}
/*!
* \brief compare
* Compare the image properties with the ones stored.
* \param image
* \return True on success, otherwise false.
*/
bool compare(const QImage& image)
{
bool ok = true;
if (!m_size.isEmpty()) {
// 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());
}
return ok;
}
private:
QSize m_size;
QImage::Format m_format;
QImageIOHandler::Transformations m_transformations;
};
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
QCoreApplication::removeLibraryPath(QStringLiteral(PLUGIN_DIR));
QCoreApplication::addLibraryPath(QStringLiteral(PLUGIN_DIR));
QCoreApplication::setApplicationName(QStringLiteral("readtest"));
QCoreApplication::setApplicationVersion(QStringLiteral("1.1.0"));
QCoreApplication::setApplicationVersion(QStringLiteral("1.2.0"));
QCommandLineParser parser;
parser.setApplicationDescription(QStringLiteral("Performs basic image conversion checking."));
@@ -159,22 +262,30 @@ int main(int argc, char **argv)
QTextStream(stdout) << "* Run on RANDOM ACCESS device\n";
}
for (const QFileInfo &fi : lstImgDir) {
if (!fi.suffix().compare("png", Qt::CaseInsensitive) || !fi.suffix().compare("tif", Qt::CaseInsensitive)) {
TemplateImage timg(fi);
if (timg.isTemplate()) {
continue;
}
int suffixPos = fi.filePath().size() - suffix.size();
QString inputfile = fi.filePath();
QString fmt = QStringLiteral("png");
QString expfile = fi.filePath().replace(suffixPos, suffix.size(), fmt);
if (!QFile::exists(expfile)) { // try with tiff
fmt = QStringLiteral("tif");
expfile = fi.filePath().replace(suffixPos, suffix.size(), fmt);
}
QString expfilename = QFileInfo(expfile).fileName();
std::unique_ptr<QIODevice> inputDevice(seq ? new SequentialFile(inputfile) : new QFile(inputfile));
bool skipTest = false;
QFileInfo expFileInfo = timg.compareImage(skipTest);
if (skipTest) {
QTextStream(stdout) << "SKIP : " << fi.fileName() << ": image format not supported by current Qt version!\n";
++skipped;
continue;
}
if (!formatStrings.contains(expFileInfo.suffix(), Qt::CaseInsensitive)) {
// Work Around for CCBUG: 468288
QTextStream(stdout) << "SKIP : " << fi.fileName() << ": comparison image " << expFileInfo.fileName() << " cannot be loaded due to the lack of "
<< expFileInfo.suffix().toUpper() << " plugin!\n";
++skipped;
continue;
}
QString expfilename = expFileInfo.fileName();
std::unique_ptr<QIODevice> inputDevice(seq ? new SequentialFile(fi.filePath()) : new QFile(fi.filePath()));
QImageReader inputReader(inputDevice.get(), format);
QImageReader expReader(expfile, fmt.toLatin1());
QImageReader expReader(expFileInfo.filePath());
QImage inputImage;
QImage expImage;
@@ -199,11 +310,32 @@ int main(int argc, char **argv)
}
continue;
}
OptionTest optionTest;
if (!optionTest.store(&inputReader)) {
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": error while reading options\n";
++failed;
continue;
}
if (!inputReader.read(&inputImage)) {
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": failed to load: " << inputReader.errorString() << "\n";
++failed;
continue;
}
if (!optionTest.compare(&inputReader)) {
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": error while comparing options\n";
++failed;
continue;
}
if (!optionTest.compare(inputImage)) {
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": error while comparing the image properties with options\n";
++failed;
continue;
}
if (expImage.width() != inputImage.width()) {
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": width was " << inputImage.width() << " but " << expfilename << " width was "
<< expImage.width() << "\n";

106
autotests/templateimage.cpp Normal file
View File

@@ -0,0 +1,106 @@
/*
SPDX-FileCopyrightText: 2024 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "templateimage.h"
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QVersionNumber>
TemplateImage::TemplateImage(const QFileInfo &fi) :
m_fi(fi)
{
}
bool TemplateImage::isTemplate() const
{
auto list = suffixes();
for (auto&& suffix : list) {
if (!m_fi.suffix().compare(suffix, Qt::CaseInsensitive))
return true;
}
return false;
}
QFileInfo TemplateImage::compareImage(bool &skipTest) const
{
auto fi = jsonImage(skipTest);
if (skipTest) {
return {};
}
if (fi.exists()) {
return fi;
}
return legacyImage();
}
QStringList TemplateImage::suffixes()
{
return QStringList({"png", "tif", "tiff", "json"});
}
QFileInfo TemplateImage::legacyImage() const
{
auto list = suffixes();
for (auto&& suffix : list) {
auto fi = QFileInfo(QStringLiteral("%1/%2.%3").arg(m_fi.path(), m_fi.completeBaseName(), suffix));
if (fi.exists()) {
return fi;
}
}
return {};
}
QFileInfo TemplateImage::jsonImage(bool &skipTest) const
{
auto fi = QFileInfo(QStringLiteral("%1.json").arg(m_fi.filePath()));
if (!fi.exists()) {
return {};
}
QFile f(fi.filePath());
if (!f.open(QFile::ReadOnly)) {
return {};
}
QJsonParseError err;
auto doc = QJsonDocument::fromJson(f.readAll(), &err);
if (err.error != QJsonParseError::NoError || !doc.isArray()) {
return {};
}
auto currentQt = QVersionNumber::fromString(qVersion());
auto arr = doc.array();
for (auto val : arr) {
if (!val.isObject())
continue;
auto obj = val.toObject();
auto minQt = QVersionNumber::fromString(obj.value("minQtVersion").toString());
auto maxQt = QVersionNumber::fromString(obj.value("maxQtVersion").toString());
auto name = obj.value("fileName").toString();
auto unsupportedFormat = obj.value("unsupportedFormat").toBool();
// filter
if (name.isEmpty() && !unsupportedFormat)
continue;
if (!minQt.isNull() && currentQt < minQt)
continue;
if (!maxQt.isNull() && currentQt > maxQt)
continue;
if (unsupportedFormat) {
skipTest = true;
break;
}
return QFileInfo(QStringLiteral("%1/%2").arg(fi.path(), name));
}
return {};
}

74
autotests/templateimage.h Normal file
View File

@@ -0,0 +1,74 @@
/*
SPDX-FileCopyrightText: 2024 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef TEMPLATEIMAGE_H
#define TEMPLATEIMAGE_H
#include <QFileInfo>
/*!
* \brief The TemplateImage class
* Given an image name, it decides the template image to compare it with.
*/
class TemplateImage
{
public:
/*!
* \brief TemplateImage
* \param fi The image to test.
*/
TemplateImage(const QFileInfo& fi);
/*!
* \brief TemplateImage
* Default copy constructor.
*/
TemplateImage(const TemplateImage& other) = default;
/*!
* \brief operator =
* Default copy operator
*/
TemplateImage& operator=(const TemplateImage& other) = default;
/*!
* \brief isTemplate
* \return True if the image is a template, false otherwise.
* \sa suffixes
*/
bool isTemplate() const;
/*!
* \brief compareImage
* \param skipTest True if the test should be skipped (e.g. image format not supported by current Qt version).
* \return The template image to use for the comparison.
*/
QFileInfo compareImage(bool &skipTest) const;
/*!
* \brief suffixes
* \return The list of suffixes considered templates.
*/
static QStringList suffixes();
private:
/*!
* \brief legacyImage
* \return The template image calculated from the source image name.
*/
QFileInfo legacyImage() const;
/*!
* \brief jsonImage
* \param skipTest True if the test should be skipped (not supported).
* \return The template image read from the corresponding JSON.
*/
QFileInfo jsonImage(bool &skipTest) const;
private:
QFileInfo m_fi;
};
#endif // TEMPLATEIMAGE_H

View File

@@ -0,0 +1,24 @@
# - Find LibJXR
# Find the JXR library
# This module defines
# LIBJXR_INCLUDE_DIRS, where to find jxrlib/JXRGlue.h
# LIBJXR_LIBRARIES, the libraries needed to use JXR
#
# Based on cmake code found at https://github.com/microsoft/vcpkg/blob/master/ports/jxrlib/FindJXR.cmake
find_path(LIBJXR_INCLUDE_DIRS
NAMES JXRGlue.h
PATH_SUFFIXES jxrlib
)
mark_as_advanced(LIBJXR_INCLUDE_DIRS)
include(SelectLibraryConfigurations)
find_library(LIBJPEGXR_LIBRARY NAMES jpegxr)
find_library(LIBJXRGLUE_LIBRARY NAMES jxrglue)
set(LIBJXR_LIBRARIES ${LIBJPEGXR_LIBRARY} ${LIBJXRGLUE_LIBRARY})
mark_as_advanced(LIBJXR_LIBRARIES)
include(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(LibJXR DEFAULT_MSG LIBJXR_INCLUDE_DIRS LIBJXR_LIBRARIES)

View File

@@ -1,4 +1,3 @@
maintainer: alexmerry
description: Image format plugins for Qt
tier: 2
type: functional

View File

@@ -83,7 +83,15 @@ kimageformats_add_plugin(kimg_pic SOURCES pic.cpp)
##################################
kimageformats_add_plugin(kimg_psd SOURCES psd.cpp)
kimageformats_add_plugin(kimg_pfm SOURCES pfm.cpp)
##################################
kimageformats_add_plugin(kimg_psd SOURCES psd.cpp scanlineconverter.cpp)
##################################
kimageformats_add_plugin(kimg_pxr SOURCES pxr.cpp)
##################################
@@ -115,6 +123,20 @@ endif()
##################################
if (LibJXR_FOUND)
kimageformats_add_plugin(kimg_jxr SOURCES jxr.cpp)
kde_enable_exceptions()
target_include_directories(kimg_jxr PRIVATE ${LIBJXR_INCLUDE_DIRS})
target_link_libraries(kimg_jxr PRIVATE jpegxr jxrglue)
target_compile_definitions(kimg_jxr PRIVATE INITGUID)
if (NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=undef")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-error=undef")
endif()
endif()
##################################
if (KF6Archive_FOUND)
kimageformats_add_plugin(kimg_kra SOURCES kra.cpp)

View File

@@ -16,9 +16,34 @@
#include <cfloat>
/*
Quality range - compression/subsampling
100 - lossless RGB compression
< KIMG_AVIF_QUALITY_BEST, 100 ) - YUV444 color subsampling
< KIMG_AVIF_QUALITY_HIGH, KIMG_AVIF_QUALITY_BEST ) - YUV422 color subsampling
< 0, KIMG_AVIF_QUALITY_HIGH ) - YUV420 color subsampling
< 0, KIMG_AVIF_QUALITY_LOW ) - lossy compression of alpha channel
*/
#ifndef KIMG_AVIF_DEFAULT_QUALITY
#define KIMG_AVIF_DEFAULT_QUALITY 68
#endif
#ifndef KIMG_AVIF_QUALITY_BEST
#define KIMG_AVIF_QUALITY_BEST 90
#endif
#ifndef KIMG_AVIF_QUALITY_HIGH
#define KIMG_AVIF_QUALITY_HIGH 80
#endif
#ifndef KIMG_AVIF_QUALITY_LOW
#define KIMG_AVIF_QUALITY_LOW 51
#endif
QAVIFHandler::QAVIFHandler()
: m_parseState(ParseAvifNotParsed)
, m_quality(52)
, m_quality(KIMG_AVIF_DEFAULT_QUALITY)
, m_container_width(0)
, m_container_height(0)
, m_rawAvifData(AVIF_DATA_EMPTY)
@@ -519,9 +544,17 @@ bool QAVIFHandler::write(const QImage &image)
}
}
if (m_quality > 100) {
m_quality = 100;
} else if (m_quality < 0) {
m_quality = KIMG_AVIF_DEFAULT_QUALITY;
}
#if AVIF_VERSION < 1000000
int maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY * (100 - qBound(0, m_quality, 100)) / 100;
int minQuantizer = 0;
int maxQuantizerAlpha = 0;
#endif
avifResult res;
bool save_grayscale; // true - monochrome, false - colors
@@ -567,13 +600,15 @@ bool QAVIFHandler::write(const QImage &image)
break;
}
// quality settings
#if AVIF_VERSION < 1000000
// deprecated quality settings
if (maxQuantizer > 20) {
minQuantizer = maxQuantizer - 20;
if (maxQuantizer > 40) { // we decrease quality of alpha channel here
maxQuantizerAlpha = maxQuantizer - 40;
}
}
#endif
if (save_grayscale && !image.hasAlphaChannel()) { // we are going to save grayscale image without alpha channel
if (save_depth > 8) {
@@ -646,8 +681,8 @@ bool QAVIFHandler::write(const QImage &image)
QImage tmpcolorimage = image.convertToFormat(tmpformat);
avifPixelFormat pixel_format = AVIF_PIXEL_FORMAT_YUV420;
if (maxQuantizer < 20) {
if (maxQuantizer < 10) {
if (m_quality >= KIMG_AVIF_QUALITY_HIGH) {
if (m_quality >= KIMG_AVIF_QUALITY_BEST) {
pixel_format = AVIF_PIXEL_FORMAT_YUV444; // best quality
} else {
pixel_format = AVIF_PIXEL_FORMAT_YUV422; // high quality
@@ -807,6 +842,8 @@ bool QAVIFHandler::write(const QImage &image)
avifRWData raw = AVIF_DATA_EMPTY;
avifEncoder *encoder = avifEncoderCreate();
encoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64);
#if AVIF_VERSION < 1000000
encoder->minQuantizer = minQuantizer;
encoder->maxQuantizer = maxQuantizer;
@@ -814,6 +851,17 @@ bool QAVIFHandler::write(const QImage &image)
encoder->minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
encoder->maxQuantizerAlpha = maxQuantizerAlpha;
}
#else
encoder->quality = m_quality;
if (image.hasAlphaChannel()) {
if (m_quality >= KIMG_AVIF_QUALITY_LOW) {
encoder->qualityAlpha = 100;
} else {
encoder->qualityAlpha = 100 - (KIMG_AVIF_QUALITY_LOW - m_quality) / 2;
}
}
#endif
encoder->speed = 6;
@@ -870,7 +918,7 @@ void QAVIFHandler::setOption(ImageOption option, const QVariant &value)
if (m_quality > 100) {
m_quality = 100;
} else if (m_quality < 0) {
m_quality = 52;
m_quality = KIMG_AVIF_DEFAULT_QUALITY;
}
return;
default:

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
@@ -90,17 +81,7 @@
#include <QImageIOPlugin>
#include <QLocale>
#include <QThread>
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
#include <QTimeZone>
#endif
// 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 (QT_VERSION_MAJOR >= 6) && !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
class K_IStream : public Imf::IStream
{
@@ -212,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)
@@ -251,13 +216,7 @@ bool EXRHandler::canRead() const
static QImage::Format imageFormat(const Imf::RgbaInputFile &file)
{
auto isRgba = file.channels() & Imf::RgbaChannels::WRITE_A;
#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 : QImage::Format_RGBX16FPx4);
#else
return (isRgba ? QImage::Format_RGBA64 : QImage::Format_RGBX64);
#endif
}
/*!
@@ -293,23 +252,23 @@ static void readMetadata(const Imf::Header &header, QImage &image)
{
// set some useful metadata
if (auto comments = header.findTypedAttribute<Imf::StringAttribute>("comments")) {
image.setText(QStringLiteral("Comment"), QString::fromStdString(comments->value()));
image.setText(QStringLiteral(META_KEY_COMMENT), QString::fromStdString(comments->value()));
}
if (auto owner = header.findTypedAttribute<Imf::StringAttribute>("owner")) {
image.setText(QStringLiteral("Owner"), QString::fromStdString(owner->value()));
image.setText(QStringLiteral(META_KEY_OWNER), QString::fromStdString(owner->value()));
}
if (auto lat = header.findTypedAttribute<Imf::FloatAttribute>("latitude")) {
image.setText(QStringLiteral("Latitude"), QLocale::c().toString(lat->value()));
image.setText(QStringLiteral(META_KEY_LATITUDE), QLocale::c().toString(lat->value()));
}
if (auto lon = header.findTypedAttribute<Imf::FloatAttribute>("longitude")) {
image.setText(QStringLiteral("Longitude"), QLocale::c().toString(lon->value()));
image.setText(QStringLiteral(META_KEY_LONGITUDE), QLocale::c().toString(lon->value()));
}
if (auto alt = header.findTypedAttribute<Imf::FloatAttribute>("altitude")) {
image.setText(QStringLiteral("Altitude"), QLocale::c().toString(alt->value()));
image.setText(QStringLiteral(META_KEY_ALTITUDE), QLocale::c().toString(alt->value()));
}
if (auto capDate = header.findTypedAttribute<Imf::StringAttribute>("capDate")) {
@@ -319,12 +278,8 @@ static void readMetadata(const Imf::Header &header, QImage &image)
}
auto dateTime = QDateTime::fromString(QString::fromStdString(capDate->value()), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
if (dateTime.isValid()) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
dateTime.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(off));
#else
dateTime.setOffsetFromUtc(off);
#endif
image.setText(QStringLiteral("CreationDate"), dateTime.toString(Qt::ISODate));
image.setText(QStringLiteral(META_KEY_CREATIONDATE), dateTime.toString(Qt::ISODate));
}
}
@@ -339,7 +294,7 @@ static void readMetadata(const Imf::Header &header, QImage &image)
// Non-standard attribute
if (auto xmp = header.findTypedAttribute<Imf::StringAttribute>("xmp")) {
image.setText(QStringLiteral("XML:com.adobe.xmp"), QString::fromStdString(xmp->value()));
image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromStdString(xmp->value()));
}
/* TODO: OpenEXR 3.2 metadata
@@ -368,8 +323,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();
@@ -387,8 +340,6 @@ static void readColorSpace(const Imf::Header &header, QImage &image)
#ifdef EXR_CONVERT_TO_SRGB
image.convertToColorSpace(QColorSpace(QColorSpace::SRgb));
#endif
#endif // !EXR_USE_LEGACY_CONVERSIONS
}
bool EXRHandler::read(QImage *outImage)
@@ -439,7 +390,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
@@ -450,30 +400,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 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
}
}
@@ -537,18 +471,18 @@ static void setMetadata(const QImage &image, Imf::Header &header)
auto dateTime = QDateTime::currentDateTime();
for (auto &&key : image.textKeys()) {
auto text = image.text(key);
if (!key.compare(QStringLiteral("Comment"), Qt::CaseInsensitive)) {
if (!key.compare(QStringLiteral(META_KEY_COMMENT), Qt::CaseInsensitive)) {
header.insert("comments", Imf::StringAttribute(text.toStdString()));
}
if (!key.compare(QStringLiteral("Owner"), Qt::CaseInsensitive)) {
if (!key.compare(QStringLiteral(META_KEY_OWNER), Qt::CaseInsensitive)) {
header.insert("owner", Imf::StringAttribute(text.toStdString()));
}
// clang-format off
if (!key.compare(QStringLiteral("Latitude"), Qt::CaseInsensitive) ||
!key.compare(QStringLiteral("Longitude"), Qt::CaseInsensitive) ||
!key.compare(QStringLiteral("Altitude"), Qt::CaseInsensitive)) {
if (!key.compare(QStringLiteral(META_KEY_LATITUDE), Qt::CaseInsensitive) ||
!key.compare(QStringLiteral(META_KEY_LONGITUDE), Qt::CaseInsensitive) ||
!key.compare(QStringLiteral(META_KEY_ALTITUDE), Qt::CaseInsensitive)) {
// clang-format on
auto ok = false;
auto value = QLocale::c().toFloat(text, &ok);
@@ -557,7 +491,7 @@ static void setMetadata(const QImage &image, Imf::Header &header)
}
}
if (!key.compare(QStringLiteral("CreationDate"), Qt::CaseInsensitive)) {
if (!key.compare(QStringLiteral(META_KEY_CREATIONDATE), Qt::CaseInsensitive)) {
auto dt = QDateTime::fromString(text, Qt::ISODate);
if (dt.isValid()) {
dateTime = dt;
@@ -565,7 +499,7 @@ static void setMetadata(const QImage &image, Imf::Header &header)
}
#ifndef EXR_DISABLE_XMP_ATTRIBUTE // warning: Non-standard attribute!
if (!key.compare(QStringLiteral("XML:com.adobe.xmp"), Qt::CaseInsensitive)) {
if (!key.compare(QStringLiteral(META_KEY_XMP_ADOBE), Qt::CaseInsensitive)) {
header.insert("xmp", Imf::StringAttribute(text.toStdString()));
}
#endif
@@ -628,22 +562,24 @@ bool EXRHandler::write(const QImage &image)
// write the EXR
K_OStream ostr(device(), QByteArray());
Imf::RgbaOutputFile file(ostr, header, image.hasAlphaChannel() ? Imf::RgbaChannels::WRITE_RGBA : Imf::RgbaChannels::WRITE_RGB);
auto channelsType = image.hasAlphaChannel() ? Imf::RgbaChannels::WRITE_RGBA : Imf::RgbaChannels::WRITE_RGB;
if (image.format() == QImage::Format_Mono ||
image.format() == QImage::Format_MonoLSB ||
image.format() == QImage::Format_Grayscale16 ||
image.format() == QImage::Format_Grayscale8) {
channelsType = Imf::RgbaChannels::WRITE_Y;
}
Imf::RgbaOutputFile file(ostr, header, channelsType);
Imf::Array2D<Imf::Rgba> pixels;
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);
slc.setDefaultSourceColorSpace(QColorSpace(QColorSpace::SRgb));
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;
@@ -655,18 +591,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);
@@ -699,10 +623,12 @@ void EXRHandler::setOption(ImageOption option, const QVariant &value)
bool EXRHandler::supportsOption(ImageOption option) const
{
if (option == QImageIOHandler::Size) {
return true;
if (auto d = device())
return !d->isSequential();
}
if (option == QImageIOHandler::ImageFormat) {
return true;
if (auto d = device())
return !d->isSequential();
}
if (option == QImageIOHandler::CompressionRatio) {
return true;
@@ -721,6 +647,9 @@ QVariant EXRHandler::option(ImageOption option) const
if (auto d = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
if (m_startPos > -1) {
d->seek(m_startPos);
}
try {
K_IStream istr(d, QByteArray());
Imf::RgbaInputFile file(istr);
@@ -743,6 +672,9 @@ QVariant EXRHandler::option(ImageOption option) const
if (auto d = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
if (m_startPos > -1) {
d->seek(m_startPos);
}
try {
K_IStream istr(d, QByteArray());
Imf::RgbaInputFile file(istr);

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());
QSize size = readHeaderSize(s.device());
if (!size.isValid()) {
const Header& h = d->header(s.device());
if (!h.isValid()) {
return false;
}
QImage img;
if (!LoadHDR(s, size.width(), size.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,13 +446,10 @@ QVariant HDRHandler::option(ImageOption option) const
QVariant v;
if (option == QImageIOHandler::Size) {
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);
}
}
}
@@ -318,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)
{
}
@@ -348,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:
@@ -22,6 +24,9 @@ public:
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
private:
const QScopedPointer<HDRHandlerPrivate> d;
};
class HDRPlugin : public QImageIOPlugin

View File

@@ -22,6 +22,7 @@ size_t HEIFHandler::m_initialized_count = 0;
bool HEIFHandler::m_plugins_queried = false;
bool HEIFHandler::m_heif_decoder_available = false;
bool HEIFHandler::m_heif_encoder_available = false;
bool HEIFHandler::m_hej2_decoder_available = false;
extern "C" {
static struct heif_error heifhandler_write_callback(struct heif_context * /* ctx */, const void *data, size_t size, void *userdata)
@@ -59,12 +60,25 @@ HEIFHandler::HEIFHandler()
bool HEIFHandler::canRead() const
{
if (m_parseState == ParseHeicNotParsed && !canRead(device())) {
if (m_parseState == ParseHeicNotParsed) {
QIODevice *dev = device();
if (dev) {
const QByteArray header = dev->peek(28);
if (HEIFHandler::isSupportedBMFFType(header)) {
setFormat("heif");
return true;
}
if (HEIFHandler::isSupportedHEJ2(header)) {
setFormat("hej2");
return true;
}
}
return false;
}
if (m_parseState != ParseHeicError) {
setFormat("heif");
return true;
}
return false;
@@ -300,17 +314,6 @@ bool HEIFHandler::write_helper(const QImage &image)
return true;
}
bool HEIFHandler::canRead(QIODevice *device)
{
if (!device) {
qWarning("HEIFHandler::canRead() called with no device");
return false;
}
const QByteArray header = device->peek(28);
return HEIFHandler::isSupportedBMFFType(header);
}
bool HEIFHandler::isSupportedBMFFType(const QByteArray &header)
{
if (header.size() < 28) {
@@ -350,6 +353,22 @@ bool HEIFHandler::isSupportedBMFFType(const QByteArray &header)
return false;
}
bool HEIFHandler::isSupportedHEJ2(const QByteArray &header)
{
if (header.size() < 28) {
return false;
}
const char *buffer = header.constData();
if (qstrncmp(buffer + 4, "ftyp", 4) == 0) {
if (qstrncmp(buffer + 8, "j2ki", 4) == 0) {
return true;
}
}
return false;
}
QVariant HEIFHandler::option(ImageOption option) const
{
if (option == Quality) {
@@ -425,7 +444,7 @@ bool HEIFHandler::ensureDecoder()
}
const QByteArray buffer = device()->readAll();
if (!HEIFHandler::isSupportedBMFFType(buffer)) {
if (!HEIFHandler::isSupportedBMFFType(buffer) && !HEIFHandler::isSupportedHEJ2(buffer)) {
m_parseState = ParseHeicError;
return false;
}
@@ -814,6 +833,9 @@ bool HEIFHandler::isHeifDecoderAvailable()
}
#endif
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
m_hej2_decoder_available = heif_have_decoder_for_format(heif_compression_JPEG2000);
#endif
m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC);
m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC);
m_plugins_queried = true;
@@ -839,6 +861,9 @@ bool HEIFHandler::isHeifEncoderAvailable()
}
#endif
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
m_hej2_decoder_available = heif_have_decoder_for_format(heif_compression_JPEG2000);
#endif
m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC);
m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC);
m_plugins_queried = true;
@@ -853,6 +878,34 @@ bool HEIFHandler::isHeifEncoderAvailable()
return m_heif_encoder_available;
}
bool HEIFHandler::isHej2DecoderAvailable()
{
QMutexLocker locker(&getHEIFHandlerMutex());
if (!m_plugins_queried) {
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (m_initialized_count == 0) {
heif_init(nullptr);
}
#endif
m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC);
m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC);
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
m_hej2_decoder_available = heif_have_decoder_for_format(heif_compression_JPEG2000);
#endif
m_plugins_queried = true;
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (m_initialized_count == 0) {
heif_deinit();
}
#endif
}
return m_hej2_decoder_available;
}
void HEIFHandler::startHeifLib()
{
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
@@ -901,6 +954,15 @@ QImageIOPlugin::Capabilities HEIFPlugin::capabilities(QIODevice *device, const Q
}
return format_cap;
}
if (format == "hej2") {
Capabilities format_cap;
if (HEIFHandler::isHej2DecoderAvailable()) {
format_cap |= CanRead;
}
return format_cap;
}
if (!format.isEmpty()) {
return {};
}
@@ -909,8 +971,16 @@ QImageIOPlugin::Capabilities HEIFPlugin::capabilities(QIODevice *device, const Q
}
Capabilities cap;
if (device->isReadable() && HEIFHandler::canRead(device) && HEIFHandler::isHeifDecoderAvailable()) {
cap |= CanRead;
if (device->isReadable()) {
const QByteArray header = device->peek(28);
if (HEIFHandler::isSupportedBMFFType(header) && HEIFHandler::isHeifDecoderAvailable()) {
cap |= CanRead;
}
if (HEIFHandler::isSupportedHEJ2(header) && HEIFHandler::isHej2DecoderAvailable()) {
cap |= CanRead;
}
}
if (device->isWritable() && HEIFHandler::isHeifEncoderAvailable()) {

View File

@@ -1,4 +1,4 @@
{
"Keys": [ "heif", "heic" ],
"MimeTypes": [ "image/heif", "image/heif" ]
"Keys": [ "heif", "heic", "hej2" ],
"MimeTypes": [ "image/heif", "image/heif", "image/hej2k" ]
}

View File

@@ -24,17 +24,18 @@ public:
bool read(QImage *image) override;
bool write(const QImage &image) override;
static bool canRead(QIODevice *device);
QVariant option(ImageOption option) const override;
void setOption(ImageOption option, const QVariant &value) override;
bool supportsOption(ImageOption option) const override;
static bool isHeifDecoderAvailable();
static bool isHeifEncoderAvailable();
static bool isHej2DecoderAvailable();
static bool isSupportedBMFFType(const QByteArray &header);
static bool isSupportedHEJ2(const QByteArray &header);
private:
static bool isSupportedBMFFType(const QByteArray &header);
bool ensureParsed() const;
bool ensureDecoder();
@@ -57,6 +58,7 @@ private:
static bool m_plugins_queried;
static bool m_heif_decoder_available;
static bool m_heif_encoder_available;
static bool m_hej2_decoder_available;
static QMutex &getHEIFHandlerMutex();
};

1110
src/imageformats/jxr.cpp Normal file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More