Compare commits

...

29 Commits

Author SHA1 Message Date
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
bb17f7bf84 update version for new release 2024-07-05 13:19:28 +02:00
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
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
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
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
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
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
bd083ff354 XCF: fixed wrong composite on Grayscale images
CCBUG: 476755
2024-06-10 20:16:31 +00:00
99663607b2 Fix compilation 2024-06-07 15:08:53 +00:00
7499e3b8d4 Use of metadata macro definitions
Replaced the metadata string with the common macro definition
2024-06-07 15:08:37 +00:00
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
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
b8a9c75c80 JXR support
CCBUG: 451584

An implementation of the JXR format.
2024-06-07 10:35:25 +00:00
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
a54c5e876c update version for new release 2024-05-31 17:41:40 +02:00
118 changed files with 3667 additions and 323 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

@ -1,11 +1,11 @@
cmake_minimum_required(VERSION 3.16)
set(KF_VERSION "6.3.0") # handled by release scripts
set(KF_DEP_VERSION "6.3.0") # handled by release scripts
set(KF_VERSION "6.5.0") # handled by release scripts
set(KF_DEP_VERSION "6.5.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION})
include(FeatureSummary)
find_package(ECM 6.3.0 NO_MODULE)
find_package(ECM 6.5.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)
@ -75,13 +75,18 @@ 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.5
KF 5.102

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
@ -114,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

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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

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.

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

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,28 +262,39 @@ 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));
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;
}
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;
// 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";
@ -199,11 +313,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";

111
autotests/templateimage.cpp Normal file
View File

@ -0,0 +1,111 @@
/*
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(TestFlags &flags, QString& comment) const
{
auto fi = jsonImage(flags, comment);
if ((flags & TestFlag::SkipTest) == TestFlag::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(TestFlags &flags, QString& comment) const
{
flags = TestFlag::None;
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();
comment = obj.value("comment").toString();
if(obj.value("disableAutoTransform").toBool())
flags |= TestFlag::DisableAutotransform;
// filter
if (name.isEmpty() && !unsupportedFormat)
continue;
if (!minQt.isNull() && currentQt < minQt)
continue;
if (!maxQt.isNull() && currentQt > maxQt)
continue;
if (unsupportedFormat) {
flags |= TestFlag::SkipTest;
break;
}
return QFileInfo(QStringLiteral("%1/%2").arg(fi.path(), name));
}
return {};
}

83
autotests/templateimage.h Normal file
View File

@ -0,0 +1,83 @@
/*
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:
enum TestFlag {
None = 0x0,
SkipTest = 0x1,
DisableAutotransform = 0x2
};
Q_DECLARE_FLAGS(TestFlags, TestFlag)
/*!
* \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 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(TestFlags &flags, QString& comment) 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 flags Flags for modifying test behavior.
* \return The template image read from the corresponding JSON.
*/
QFileInfo jsonImage(TestFlags &flags, QString& comment) const;
private:
QFileInfo m_fi;
};
Q_DECLARE_OPERATORS_FOR_FLAGS(TemplateImage::TestFlags)
#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

@ -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

@ -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,13 +83,6 @@
#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
class K_IStream : public Imf::IStream
{
public:
@ -209,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)
@ -248,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
}
/*!
@ -290,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")) {
@ -317,7 +279,7 @@ 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()) {
dateTime.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(off));
image.setText(QStringLiteral("CreationDate"), dateTime.toString(Qt::ISODate));
image.setText(QStringLiteral(META_KEY_CREATIONDATE), dateTime.toString(Qt::ISODate));
}
}
@ -332,26 +294,30 @@ 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
*
* 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()));
}
}
/*!
@ -361,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();
@ -380,8 +344,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)
@ -432,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
@ -443,30 +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 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
}
}
@ -530,18 +475,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);
@ -550,7 +495,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;
@ -558,10 +503,30 @@ 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
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()));
@ -578,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)
@ -633,17 +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);
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 +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);
@ -699,10 +645,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 +669,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 +694,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

@ -16,11 +16,19 @@
#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
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 +137,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
@ -568,10 +581,25 @@ 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
pixel_format.data_type = JXL_TYPE_UINT16;
@ -777,14 +805,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 +832,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 +870,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 +886,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

View File

@ -64,6 +64,7 @@ private:
int m_quality;
int m_currentimage_index;
int m_previousimage_index;
QImageIOHandler::Transformations m_transformations;
QByteArray m_rawData;

1110
src/imageformats/jxr.cpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
{
"Keys": [ "jxr", "wdp", "hdp" ],
"MimeTypes": [ "image/jxr", "image/vnd.ms-photo", "image/vnd.ms-photo" ]
}

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

@ -0,0 +1,47 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2024 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KIMG_JXR_P_H
#define KIMG_JXR_P_H
#include <QImageIOPlugin>
#include <QSharedDataPointer>
class JXRHandlerPrivate;
class JXRHandler : public QImageIOHandler
{
public:
JXRHandler();
bool canRead() const override;
bool read(QImage *outImage) override;
bool write(const QImage &image) override;
void setOption(ImageOption option, const QVariant &value) override;
bool supportsOption(QImageIOHandler::ImageOption option) const override;
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
private:
mutable QSharedDataPointer<JXRHandlerPrivate> d;
qint32 m_quality;
};
class JXRPlugin : public QImageIOPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "jxr.json")
public:
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
};
#endif // KIMG_JXR_P_H

View File

@ -308,6 +308,11 @@ static bool readImage4(QImage &img, QDataStream &s, const PCXHEADER &header)
return false;
}
if (header.BytesPerLine < (header.width() / 8)) {
qWarning() << "PCX image has invalid BytesPerLine value";
return false;
}
for (int y = 0; y < header.height(); ++y) {
if (s.atEnd()) {
return false;
@ -344,6 +349,47 @@ 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(), QImage::Format_Indexed8);
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);
@ -418,6 +464,8 @@ static bool readImage24(QImage &img, QDataStream &s, const PCXHEADER &header)
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 +482,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]);
}
}
@ -672,6 +721,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) {

324
src/imageformats/pfm.cpp Normal file
View File

@ -0,0 +1,324 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2024 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
/*
* See also: https://www.pauldebevec.com/Research/HDR/PFM/
*/
#include "pfm_p.h"
#include "util_p.h"
#include <QColorSpace>
#include <QDataStream>
#include <QIODevice>
#include <QImage>
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(LOG_PFMPLUGIN)
Q_LOGGING_CATEGORY(LOG_PFMPLUGIN, "kf.imageformats.plugins.pfm", QtWarningMsg)
class PFMHeader
{
private:
/*!
* \brief m_bw True if grayscale.
*/
bool m_bw;
/*!
* \brief m_ps True if saved by Photoshop (Photoshop variant).
*
* When \a false the format of the header is the following (GIMP):
* [type]
* [xres] [yres]
* [byte_order]
*
* When \a true the format of the header is the following (Photoshop):
* [type]
* [xres]
* [yres]
* [byte_order]
*/
bool m_ps;
/*!
* \brief m_width The image width.
*/
qint32 m_width;
/*!
* \brief m_height The image height.
*/
qint32 m_height;
/*!
* \brief m_byteOrder The image byte orger.
*/
QDataStream::ByteOrder m_byteOrder;
public:
PFMHeader() :
m_bw(false),
m_ps(false),
m_width(0),
m_height(0),
m_byteOrder(QDataStream::BigEndian)
{
}
bool isValid() const
{
return (m_width > 0 && m_height > 0);
}
bool isBlackAndWhite() const
{
return m_bw;
}
bool isPhotoshop() const
{
return m_ps;
}
qint32 width() const
{
return m_width;
}
qint32 height() const
{
return m_height;
}
QSize size() const
{
return QSize(m_width, m_height);
}
QDataStream::ByteOrder byteOrder() const
{
return m_byteOrder;
}
QImage::Format format() const
{
if (isValid()) {
return QImage::Format_RGBX32FPx4;
}
return QImage::Format_Invalid;
}
bool read(QIODevice *d)
{
auto pf = d->read(3);
if (pf == QByteArray("PF\n")) {
m_bw = false;
} else if (pf == QByteArray("Pf\n")) {
m_bw = true;
} else {
return false;
}
auto wh = QString::fromLatin1(d->readLine(128));
auto list = wh.split(QStringLiteral(" "));
if (list.size() == 1) {
m_ps = true; // try for Photoshop version
list << QString::fromLatin1(d->readLine(128));
}
if (list.size() != 2) {
return false;
}
auto ok_o = false;
auto ok_w = false;
auto ok_h = false;
auto o = QString::fromLatin1(d->readLine(128)).toDouble(&ok_o);
auto w = list.first().toInt(&ok_w);
auto h = list.last().toInt(&ok_h);
if (!ok_o || !ok_w || !ok_h || o == 0) {
return false;
}
m_width = w;
m_height = h;
m_byteOrder = o > 0 ? QDataStream::BigEndian : QDataStream::LittleEndian;
return isValid();
}
bool peek(QIODevice *d)
{
d->startTransaction();
auto ok = read(d);
d->rollbackTransaction();
return ok;
}
};
class PFMHandlerPrivate
{
public:
PFMHandlerPrivate() {}
~PFMHandlerPrivate() {}
PFMHeader m_header;
};
PFMHandler::PFMHandler()
: QImageIOHandler()
, d(new PFMHandlerPrivate)
{
}
bool PFMHandler::canRead() const
{
if (canRead(device())) {
setFormat("pfm");
return true;
}
return false;
}
bool PFMHandler::canRead(QIODevice *device)
{
if (!device) {
qCWarning(LOG_PFMPLUGIN) << "PFMHandler::canRead() called with no device";
return false;
}
PFMHeader h;
if (!h.peek(device)) {
return false;
}
return h.isValid();
}
bool PFMHandler::read(QImage *image)
{
auto&& header = d->m_header;
if (!header.read(device())) {
qCWarning(LOG_PFMPLUGIN) << "PFMHandler::read() invalid header";
return false;
}
QDataStream s(device());
s.setFloatingPointPrecision(QDataStream::SinglePrecision);
s.setByteOrder(header.byteOrder());
auto img = imageAlloc(header.size(), header.format());
if (img.isNull()) {
qCWarning(LOG_PFMPLUGIN) << "PFMHandler::read() error while allocating the image";
return false;
}
for (auto y = 0, h = img.height(); y < h; ++y) {
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];
}
if (s.status() != QDataStream::Ok) {
qCWarning(LOG_PFMPLUGIN) << "PFMHandler::read() detected corrupted data";
return false;
}
}
}
img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
*image = img;
return true;
}
bool PFMHandler::supportsOption(ImageOption option) const
{
if (option == QImageIOHandler::Size) {
return true;
}
if (option == QImageIOHandler::ImageFormat) {
return true;
}
if (option == QImageIOHandler::Endianness) {
return true;
}
return false;
}
QVariant PFMHandler::option(ImageOption option) const
{
QVariant v;
if (option == QImageIOHandler::Size) {
auto&& h = d->m_header;
if (h.isValid()) {
v = QVariant::fromValue(h.size());
} else if (auto dev = device()) {
if (h.peek(dev)) {
v = QVariant::fromValue(h.size());
}
}
}
if (option == QImageIOHandler::ImageFormat) {
auto&& h = d->m_header;
if (h.isValid()) {
v = QVariant::fromValue(h.format());
} else if (auto dev = device()) {
if (h.peek(dev)) {
v = QVariant::fromValue(h.format());
}
}
}
if (option == QImageIOHandler::Endianness) {
auto&& h = d->m_header;
if (h.isValid()) {
v = QVariant::fromValue(h.byteOrder());
} else if (auto dev = device()) {
if (h.peek(dev)) {
v = QVariant::fromValue(h.byteOrder());
}
}
}
return v;
}
QImageIOPlugin::Capabilities PFMPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
if (format == "pfm") {
return Capabilities(CanRead);
}
if (!format.isEmpty()) {
return {};
}
if (!device->isOpen()) {
return {};
}
Capabilities cap;
if (device->isReadable() && PFMHandler::canRead(device)) {
cap |= CanRead;
}
return cap;
}
QImageIOHandler *PFMPlugin::create(QIODevice *device, const QByteArray &format) const
{
QImageIOHandler *handler = new PFMHandler;
handler->setDevice(device);
handler->setFormat(format);
return handler;
}
#include "moc_pfm_p.cpp"

View File

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

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