Compare commits

...

19 Commits

Author SHA1 Message Date
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
6c1a7ad339 update version for new release 2024-05-31 15:15:35 +02:00
c721fa481b Remove explicit maintainer from metainfo
All frameworks are maintained by the KDE community
2024-05-12 21:27:40 +02:00
ea15fed399 update version for new release 2024-05-12 14:06:57 +02:00
c2fabef501 Ensure dependencies are provided on Android 2024-05-06 00:02:54 +12:00
91 changed files with 2848 additions and 139 deletions

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,11 +1,11 @@
cmake_minimum_required(VERSION 3.16)
set(KF_VERSION "6.2.0") # handled by release scripts
set(KF_DEP_VERSION "6.2.0") # handled by release scripts
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 6.2.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)
@ -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

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: 7.4 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,114 @@ 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()) {
ok = ok && (m_size == image.size());
}
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 +261,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 +309,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

@ -99,6 +99,11 @@
#define EXR_USE_QT6_FLOAT_IMAGE // default uncommented
#endif
// Qt 6.8 allow to create and use Gray profile, so we can load a Gray image as Grayscale format instead RGB one.
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
#define EXR_GRAY_SUPPORT_ENABLED
#endif
class K_IStream : public Imf::IStream
{
public:
@ -248,10 +253,15 @@ bool EXRHandler::canRead() const
static QImage::Format imageFormat(const Imf::RgbaInputFile &file)
{
auto isRgba = file.channels() & Imf::RgbaChannels::WRITE_A;
#ifdef EXR_GRAY_SUPPORT_ENABLED
auto isGray = file.channels() & Imf::RgbaChannels::WRITE_Y;
#else
auto isGray = false;
#endif
#if defined(EXR_USE_LEGACY_CONVERSIONS)
return (isRgba ? QImage::Format_ARGB32 : QImage::Format_RGB32);
#elif defined(EXR_USE_QT6_FLOAT_IMAGE)
return (isRgba ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4);
return (isRgba ? QImage::Format_RGBA16FPx4 : isGray ? QImage::Format_Grayscale16 : QImage::Format_RGBX16FPx4);
#else
return (isRgba ? QImage::Format_RGBA64 : QImage::Format_RGBX64);
#endif
@ -290,23 +300,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 +327,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,7 +342,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
@ -373,8 +383,17 @@ static void readColorSpace(const Imf::Header &header, QImage &image)
QColorSpace::TransferFunction::Linear);
}
if (!cs.isValid()) {
#ifdef EXR_GRAY_SUPPORT_ENABLED
if (image.format() == QImage::Format_Grayscale16 || image.format() == QImage::Format_Grayscale8) {
cs = QColorSpace(QPointF(0.31271, 0.32902), QColorSpace::TransferFunction::Linear);
cs.setDescription(QStringLiteral("Gray Linear build-in"));
} else {
cs = QColorSpace(QColorSpace::SRgbLinear);
}
#else
cs = QColorSpace(QColorSpace::SRgbLinear);
#endif
}
image.setColorSpace(cs);
#ifdef EXR_CONVERT_TO_SRGB
@ -443,6 +462,13 @@ bool EXRHandler::read(QImage *outImage)
file.readPixels(my, std::min(my + EXR_LINES_PER_BLOCK - 1, dw.max.y));
for (n = 0; n < std::min(EXR_LINES_PER_BLOCK, height - y); ++n) {
if (image.format() == QImage::Format_Grayscale16) { // grayscale image
auto scanLine = reinterpret_cast<quint16 *>(image.scanLine(y + n));
for (int x = 0; x < width; ++x) {
*(scanLine + x) = quint16(qBound(0.f, float(pixels[n][x].r) * 65535.f + 0.5f, 65535.f));
}
continue;
}
#if defined(EXR_USE_LEGACY_CONVERSIONS)
Q_UNUSED(isRgba)
auto scanLine = reinterpret_cast<QRgb *>(image.scanLine(y + n));
@ -530,18 +556,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 +576,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,7 +584,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
@ -639,7 +665,16 @@ bool EXRHandler::write(const QImage &image)
auto convFormat = image.hasAlphaChannel() ? QImage::Format_RGBA64 : QImage::Format_RGBX64;
#endif
ScanLineConverter slc(convFormat);
#ifdef EXR_GRAY_SUPPORT_ENABLED
if (channelsType == Imf::RgbaChannels::WRITE_Y) {
slc.setDefaultSourceColorSpace(QColorSpace(QColorSpace(QColorSpace::SRgb).whitePoint(), QColorSpace::TransferFunction::SRgb)); // Creates a custom grayscale color space
} else {
slc.setDefaultSourceColorSpace(QColorSpace(QColorSpace::SRgb));
}
#else
slc.setDefaultSourceColorSpace(QColorSpace(QColorSpace::SRgb));
#endif
slc.setTargetColorSpace(QColorSpace(QColorSpace::SRgbLinear));
for (int y = 0, n = 0; y < height; y += n) {
for (n = 0; n < std::min(EXR_LINES_PER_BLOCK, height - y); ++n) {
@ -699,10 +734,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 +758,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 +783,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

@ -269,13 +269,13 @@ bool HDRHandler::read(QImage *outImage)
{
QDataStream s(device());
QSize size = readHeaderSize(s.device());
if (!size.isValid()) {
m_imageSize = readHeaderSize(s.device());
if (!m_imageSize.isValid()) {
return false;
}
QImage img;
if (!LoadHDR(s, size.width(), size.height(), img)) {
if (!LoadHDR(s, m_imageSize.width(), m_imageSize.height(), img)) {
// qDebug() << "Error loading HDR file.";
return false;
}
@ -303,7 +303,9 @@ QVariant HDRHandler::option(ImageOption option) const
QVariant v;
if (option == QImageIOHandler::Size) {
if (auto d = device()) {
if (!m_imageSize.isEmpty()) {
v = QVariant::fromValue(m_imageSize);
} else if (auto d = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
auto size = readHeaderSize(d);

View File

@ -22,6 +22,13 @@ public:
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
private:
/*!
* \brief m_imageSize
* Image size cache used by option()
*/
QSize m_imageSize;
};
class HDRPlugin : public QImageIOPlugin

1141
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

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

@ -0,0 +1,340 @@
/*
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 isBlackAndWhite() ? QImage::Format_Grayscale16 : 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) {
float f;
if (header.isBlackAndWhite()) {
auto line = reinterpret_cast<quint16*>(img.scanLine(header.isPhotoshop() ? y : h - y - 1));
for (auto x = 0, w = img.width(); x < w; ++x) {
s >> f;
// QColorSpace does not handle gray linear profile, so I have to convert to non-linear
f = f < 0.0031308f ? (f * 12.92f) : (1.055 * std::pow(f, 1.0 / 2.4) - 0.055);
line[x] = quint16(std::clamp(f, float(0), float(1)) * std::numeric_limits<quint16>::max() + float(0.5));
if (s.status() != QDataStream::Ok) {
qCWarning(LOG_PFMPLUGIN) << "PFMHandler::read() detected corrupted data";
return false;
}
}
} else {
auto line = reinterpret_cast<float*>(img.scanLine(header.isPhotoshop() ? y : h - y - 1));
for (auto x = 0, n = img.width() * 4; x < n; x += 4) {
s >> f;
line[x] = std::clamp(f, float(0), float(1));
s >> f;
line[x + 1] = std::clamp(f, float(0), float(1));
s >> f;
line[x + 2] = std::clamp(f, float(0), float(1));
line[x + 3] = float(1);
if (s.status() != QDataStream::Ok) {
qCWarning(LOG_PFMPLUGIN) << "PFMHandler::read() detected corrupted data";
return false;
}
}
}
}
if (!header.isBlackAndWhite()) {
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" ]
}

42
src/imageformats/pfm_p.h Normal file
View File

@ -0,0 +1,42 @@
/*
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_PFM_P_H
#define KIMG_PFM_P_H
#include <QImageIOPlugin>
#include <QScopedPointer>
class PFMHandlerPrivate;
class PFMHandler : public QImageIOHandler
{
public:
PFMHandler();
bool canRead() const override;
bool read(QImage *image) override;
bool supportsOption(QImageIOHandler::ImageOption option) const override;
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
private:
const QScopedPointer<PFMHandlerPrivate> d;
};
class PFMPlugin : public QImageIOPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "pfm.json")
public:
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
};
#endif // KIMG_PFM_P_H

View File

@ -23,7 +23,7 @@
* - Color spaces other than RGB/Grayscale cannot be read due to lack of QImage
* support. Where possible, a conversion to RGB is done:
* - CMYK images are converted using an approximated way that ignores the color
* information (ICC profile).
* information (ICC profile) with Qt less than 6.8.
* - LAB images are converted to sRGB using literature formulas.
* - MULICHANNEL images more than 3 channels are converted as CMYK images.
* - DUOTONE images are considered as Grayscale images.
@ -34,6 +34,7 @@
#include "fastmath_p.h"
#include "psd_p.h"
#include "scanlineconverter_p.h"
#include "util_p.h"
#include <QDataStream>
@ -60,9 +61,24 @@ typedef quint8 uchar;
*/
//#define PSD_FAST_LAB_CONVERSION
/*
* Since Qt version 6.8, the 8-bit CMYK format is natively supported.
* If you encounter problems with native CMYK support you can continue to force the plugin to convert
* to RGB as in previous versions by defining PSD_NATIVE_CMYK_SUPPORT_DISABLED.
*/
//#define PSD_NATIVE_CMYK_SUPPORT_DISABLED
namespace // Private.
{
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0) || defined(PSD_NATIVE_CMYK_SUPPORT_DISABLED)
# define CMYK_FORMAT QImage::Format_Invalid
#else
# define CMYK_FORMAT QImage::Format_CMYK8888
#endif
#define NATIVE_CMYK (CMYK_FORMAT != QImage::Format_Invalid)
enum Signature : quint32 {
S_8BIM = 0x3842494D, // '8BIM'
S_8B64 = 0x38423634, // '8B64'
@ -96,6 +112,10 @@ enum LayerId : quint32 {
};
struct PSDHeader {
PSDHeader() {
memset(this, 0, sizeof(PSDHeader));
}
uint signature;
ushort version;
uchar reserved[6];
@ -477,7 +497,7 @@ PSDColorModeDataSection readColorModeDataSection(QDataStream &s, bool *ok = null
*/
static bool setColorSpace(QImage& img, const PSDImageResourceSection& irs)
{
if (!irs.contains(IRI_ICCPROFILE))
if (!irs.contains(IRI_ICCPROFILE) || img.isNull())
return false;
auto irb = irs.value(IRI_ICCPROFILE);
auto cs = QColorSpace::fromIccProfile(irb.data);
@ -505,7 +525,7 @@ static bool setXmpData(QImage& img, const PSDImageResourceSection& irs)
// NOTE: "XML:com.adobe.xmp" is the meta set by Qt reader when an
// XMP packet is found (e.g. when reading a PNG saved by Photoshop).
// I'm reusing the same key because a programs could search for it.
img.setText(QStringLiteral("XML:com.adobe.xmp"), xmp);
img.setText(QStringLiteral(META_KEY_XMP_ADOBE), xmp);
return true;
}
@ -742,7 +762,9 @@ static QImage::Format imageFormat(const PSDHeader &header, bool alpha)
break;
case CM_MULTICHANNEL: // Treat MCH as CMYK (number of channel check is done in IsSupported())
case CM_CMYK: // Photoshop supports CMYK/MCH 8-bits and 16-bits only
if (header.depth == 16)
if (NATIVE_CMYK && header.channel_count == 4 && (header.depth == 16 || header.depth == 8))
format = CMYK_FORMAT;
else if (header.depth == 16)
format = header.channel_count < 5 || !alpha ? QImage::Format_RGBX64 : QImage::Format_RGBA64;
else if (header.depth == 8)
format = header.channel_count < 5 || !alpha ? QImage::Format_RGB888 : QImage::Format_RGBA8888;
@ -844,6 +866,18 @@ inline void planarToChunchy(uchar *target, const char *source, qint32 width, qin
}
}
template<class T>
inline void planarToChunchyCMYK(uchar *target, const char *source, qint32 width, qint32 c, qint32 cn)
{
auto s = reinterpret_cast<const T*>(source);
auto t = reinterpret_cast<quint8*>(target);
const T d = std::numeric_limits<T>::max() / std::numeric_limits<quint8>::max();
for (qint32 x = 0; x < width; ++x) {
t[x * cn + c] = quint8((std::numeric_limits<T>::max() - xchg(s[x])) / d);
}
}
template<class T>
inline void planarToChunchyFloatToUInt16(uchar *target, const char *source, qint32 width, qint32 c, qint32 cn)
{
@ -903,6 +937,19 @@ inline void monoInvert(uchar *target, const char* source, qint32 bytes)
}
}
template<class T>
inline void rawChannelsCopyToCMYK(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width)
{
auto s = reinterpret_cast<const T*>(source);
auto t = reinterpret_cast<quint8*>(target);
const T d = std::numeric_limits<T>::max() / std::numeric_limits<quint8>::max();
for (qint32 c = 0, cs = std::min(targetChannels, sourceChannels); c < cs; ++c) {
for (qint32 x = 0; x < width; ++x) {
t[x * targetChannels + c] = (std::numeric_limits<T>::max() - s[x * sourceChannels + c]) / d;
}
}
}
template<class T>
inline void rawChannelsCopy(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width)
{
@ -915,6 +962,17 @@ inline void rawChannelsCopy(uchar *target, qint32 targetChannels, const char *so
}
}
template<class T>
inline void rawChannelCopy(uchar *target, qint32 targetChannels, qint32 targetChannel, const char *source, qint32 sourceChannels, qint32 sourceChannel, qint32 width)
{
auto s = reinterpret_cast<const T*>(source);
auto t = reinterpret_cast<T*>(target);
for (qint32 x = 0; x < width; ++x) {
t[x * targetChannels + targetChannel] = s[x * sourceChannels + sourceChannel];
}
}
template<class T>
inline void cmykToRgb(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width, bool alpha = false)
{
@ -1103,6 +1161,7 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
auto imgChannels = imageChannels(img.format());
auto channel_num = std::min(qint32(header.channel_count), imgChannels);
auto raw_count = qsizetype(header.width * header.depth + 7) / 8;
auto native_cmyk = img.format() == CMYK_FORMAT;
if (header.height > kMaxQVectorSize / header.channel_count / sizeof(quint32)) {
qWarning() << "LoadPSD() header height/channel_count too big" << header.height << header.channel_count;
@ -1138,13 +1197,25 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
// clang-format off
// checks the need of color conversion (that requires random access to the image)
auto randomAccess = (header.color_mode == CM_CMYK) ||
auto randomAccess = (header.color_mode == CM_CMYK && !native_cmyk) ||
(header.color_mode == CM_MULTICHANNEL && !native_cmyk) ||
(header.color_mode == CM_LABCOLOR) ||
(header.color_mode == CM_MULTICHANNEL) ||
(header.color_mode != CM_INDEXED && img.hasAlphaChannel());
// clang-format on
if (randomAccess) {
// CMYK with spots (e.g. CMYKA) ICC conversion to RGBA/RGBX
QImage tmpCmyk;
ScanLineConverter iccConv(img.format());
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) && !defined(PSD_NATIVE_CMYK_SUPPORT_DISABLED)
if (header.color_mode == CM_CMYK && img.format() != QImage::Format_CMYK8888) {
auto tmpi = QImage(header.width, 1, QImage::Format_CMYK8888);
if (setColorSpace(tmpi, irs))
tmpCmyk = tmpi;
iccConv.setTargetColorSpace(QColorSpace(QColorSpace::SRgb));
}
#endif
// In order to make a colorspace transformation, we need all channels of a scanline
QByteArray psdScanline;
psdScanline.resize(qsizetype(header.width * header.depth * header.channel_count + 7) / 8);
@ -1200,11 +1271,27 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
// Conversion to RGB
if (header.color_mode == CM_CMYK || header.color_mode == CM_MULTICHANNEL) {
if (tmpCmyk.isNull()) {
if (header.depth == 8)
cmykToRgb<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
else if (header.depth == 16)
cmykToRgb<quint16>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
}
else if (header.depth == 8) {
rawChannelsCopyToCMYK<quint8>(tmpCmyk.bits(), 4, psdScanline.data(), header.channel_count, header.width);
if (auto rgbPtr = iccConv.convertedScanLine(tmpCmyk, 0))
std::memcpy(img.scanLine(y), rgbPtr, img.bytesPerLine());
if (imgChannels == 4 && header.channel_count >= 5)
rawChannelCopy<quint8>(img.scanLine(y), imgChannels, 3, psdScanline.data(), header.channel_count, 4, header.width);
}
else if (header.depth == 16) {
rawChannelsCopyToCMYK<quint16>(tmpCmyk.bits(), 4, psdScanline.data(), header.channel_count, header.width);
if (auto rgbPtr = iccConv.convertedScanLine(tmpCmyk, 0))
std::memcpy(img.scanLine(y), rgbPtr, img.bytesPerLine());
if (imgChannels == 4 && header.channel_count >= 5)
rawChannelCopy<quint16>(img.scanLine(y), imgChannels, 3, psdScanline.data(), header.channel_count, 4, header.width);
}
}
if (header.color_mode == CM_LABCOLOR) {
if (header.depth == 8)
labToRgb<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
@ -1235,10 +1322,16 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
if (header.depth == 1) { // Bitmap
monoInvert(scanLine, rawStride.data(), std::min(rawStride.size(), img.bytesPerLine()));
}
else if (header.depth == 8) { // 8-bits images: Indexed, Grayscale, RGB/RGBA
else if (header.depth == 8) { // 8-bits images: Indexed, Grayscale, RGB/RGBA, CMYK, MCH4
if (native_cmyk)
planarToChunchyCMYK<quint8>(scanLine, rawStride.data(), header.width, c, imgChannels);
else
planarToChunchy<quint8>(scanLine, rawStride.data(), header.width, c, imgChannels);
}
else if (header.depth == 16) { // 16-bits integer images: Grayscale, RGB/RGBA
else if (header.depth == 16) { // 16-bits integer images: Grayscale, RGB/RGBA, CMYK, MCH4
if (native_cmyk)
planarToChunchyCMYK<quint16>(scanLine, rawStride.data(), header.width, c, imgChannels);
else
planarToChunchy<quint16>(scanLine, rawStride.data(), header.width, c, imgChannels);
}
else if (header.depth == 32 && header.color_mode == CM_RGB) { // 32-bits float images: RGB/RGBA
@ -1251,7 +1344,6 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
}
}
// Resolution info
if (!setResolution(img, irs)) {
// qDebug() << "No resolution info found!";
@ -1287,7 +1379,17 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
} // Private
class PSDHandlerPrivate
{
public:
PSDHandlerPrivate() {}
~PSDHandlerPrivate() {}
PSDHeader m_header;
};
PSDHandler::PSDHandler()
: QImageIOHandler()
, d(new PSDHandlerPrivate)
{
}
@ -1305,7 +1407,7 @@ bool PSDHandler::read(QImage *image)
QDataStream s(device());
s.setByteOrder(QDataStream::BigEndian);
PSDHeader header;
auto&& header = d->m_header;
s >> header;
// Check image file format.
@ -1342,18 +1444,20 @@ QVariant PSDHandler::option(ImageOption option) const
QVariant v;
if (option == QImageIOHandler::Size) {
if (auto d = device()) {
auto&& header = d->m_header;
if (IsValid(header)) {
v = QVariant::fromValue(QSize(header.width, header.height));
}
else if (auto dev = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
auto ba = d->read(sizeof(PSDHeader));
d->rollbackTransaction();
dev->startTransaction();
auto ba = dev->read(sizeof(PSDHeader));
dev->rollbackTransaction();
QDataStream s(ba);
s.setByteOrder(QDataStream::BigEndian);
PSDHeader header;
s >> header;
if (s.status() == QDataStream::Ok && IsValid(header))
v = QVariant::fromValue(QSize(header.width, header.height));
}
@ -1384,7 +1488,11 @@ bool PSDHandler::canRead(QIODevice *device)
}
if (device->isSequential()) {
if (header.color_mode == CM_CMYK || header.color_mode == CM_LABCOLOR || header.color_mode == CM_MULTICHANNEL) {
if (header.color_mode == CM_CMYK || header.color_mode == CM_MULTICHANNEL) {
if (header.channel_count != 4 || !NATIVE_CMYK)
return false;
}
if (header.color_mode == CM_LABCOLOR) {
return false;
}
if (header.color_mode == CM_RGB && header.channel_count > 3) {

View File

@ -9,7 +9,9 @@
#define KIMG_PSD_P_H
#include <QImageIOPlugin>
#include <QScopedPointer>
class PSDHandlerPrivate;
class PSDHandler : public QImageIOHandler
{
public:
@ -22,6 +24,9 @@ public:
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
private:
const QScopedPointer<PSDHandlerPrivate> d;
};
class PSDPlugin : public QImageIOPlugin

282
src/imageformats/pxr.cpp Normal file
View File

@ -0,0 +1,282 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2024 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "pxr_p.h"
#include "util_p.h"
#include <QIODevice>
#include <QImage>
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(LOG_PXRPLUGIN)
Q_LOGGING_CATEGORY(LOG_PXRPLUGIN, "kf.imageformats.plugins.pxr", QtWarningMsg)
class PXRHeader
{
private:
QByteArray m_rawHeader;
quint16 ui16(quint8 c1, quint8 c2) const {
return (quint16(c2) << 8) | quint16(c1);
}
quint32 ui32(quint8 c1, quint8 c2, quint8 c3, quint8 c4) const {
return (quint32(c4) << 24) | (quint32(c3) << 16) | (quint32(c2) << 8) | quint32(c1);
}
public:
PXRHeader()
{
}
bool isValid() const
{
return (m_rawHeader.size() == 512 &&
m_rawHeader.startsWith(QByteArray::fromRawData("\x80\xE8\x00\x00", 4)));
}
bool isSupported() const
{
return format() != QImage::Format_Invalid;
}
qint32 width() const
{
if (!isValid()) {
return 0;
}
return qint32(ui16(m_rawHeader.at(418), m_rawHeader.at(419)));
}
qint32 height() const
{
if (!isValid()) {
return 0;
}
return qint32(ui16(m_rawHeader.at(416), m_rawHeader.at(417)));
}
QSize size() const
{
return QSize(width(), height());
}
qint32 channel() const
{
if (!isValid()) {
return 0;
}
return qint32(ui16(m_rawHeader.at(424), m_rawHeader.at(425)));
}
qint32 depth() const
{
if (!isValid()) {
return 0;
}
return qint32(ui16(m_rawHeader.at(426), m_rawHeader.at(427)));
}
// supposing the image offset (always 1024 on sample files)
qint32 offset() const
{
if (!isValid()) {
return 0;
}
return qint32(ui16(m_rawHeader.at(428), m_rawHeader.at(429)));
}
QImage::Format format() const
{
if (channel() == 14 && depth() == 2) {
return QImage::Format_RGB888;
}
if (channel() == 8 && depth() == 2) {
return QImage::Format_Grayscale8;
}
return QImage::Format_Invalid;
}
qsizetype strideSize() const
{
if (format() == QImage::Format_RGB888) {
return width() * 3;
}
if (format() == QImage::Format_Grayscale8) {
return width();
}
return 0;
}
bool read(QIODevice *d)
{
m_rawHeader = d->read(512);
return isValid();
}
bool peek(QIODevice *d)
{
d->startTransaction();
auto ok = read(d);
d->rollbackTransaction();
return ok;
}
bool jumpToImageData(QIODevice *d) const
{
if (d->isSequential()) {
if (auto sz = std::max(offset() - qint32(m_rawHeader.size()), 0)) {
return d->read(sz).size() == sz;
}
return true;
}
return d->seek(offset());
}
};
class PXRHandlerPrivate
{
public:
PXRHandlerPrivate() {}
~PXRHandlerPrivate() {}
PXRHeader m_header;
};
PXRHandler::PXRHandler()
: QImageIOHandler()
, d(new PXRHandlerPrivate)
{
}
bool PXRHandler::canRead() const
{
if (canRead(device())) {
setFormat("pxr");
return true;
}
return false;
}
bool PXRHandler::canRead(QIODevice *device)
{
if (!device) {
qCWarning(LOG_PXRPLUGIN) << "PXRHandler::canRead() called with no device";
return false;
}
PXRHeader h;
if (!h.peek(device)) {
return false;
}
return h.isSupported();
}
bool PXRHandler::read(QImage *image)
{
auto&& header = d->m_header;
if (!header.read(device())) {
qCWarning(LOG_PXRPLUGIN) << "PXRHandler::read() invalid header";
return false;
}
auto img = imageAlloc(header.size(), header.format());
if (img.isNull()) {
qCWarning(LOG_PXRPLUGIN) << "PXRHandler::read() error while allocating the image";
return false;
}
auto d = device();
if (!header.jumpToImageData(d)) {
qCWarning(LOG_PXRPLUGIN) << "PXRHandler::read() error while seeking image data";
return false;
}
auto size = std::min(img.bytesPerLine(), header.strideSize());
for (auto y = 0, h = img.height(); y < h; ++y) {
auto line = reinterpret_cast<char*>(img.scanLine(y));
if (d->read(line, size) != size) {
qCWarning(LOG_PXRPLUGIN) << "PXRHandler::read() error while reading image scanline";
return false;
}
}
*image = img;
return true;
}
bool PXRHandler::supportsOption(ImageOption option) const
{
if (option == QImageIOHandler::Size) {
return true;
}
if (option == QImageIOHandler::ImageFormat) {
return true;
}
return false;
}
QVariant PXRHandler::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 d = device()) {
if (h.peek(d)) {
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 d = device()) {
if (h.peek(d)) {
v = QVariant::fromValue(h.format());
}
}
}
return v;
}
QImageIOPlugin::Capabilities PXRPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
if (format == "pxr") {
return Capabilities(CanRead);
}
if (!format.isEmpty()) {
return {};
}
if (!device->isOpen()) {
return {};
}
Capabilities cap;
if (device->isReadable() && PXRHandler::canRead(device)) {
cap |= CanRead;
}
return cap;
}
QImageIOHandler *PXRPlugin::create(QIODevice *device, const QByteArray &format) const
{
QImageIOHandler *handler = new PXRHandler;
handler->setDevice(device);
handler->setFormat(format);
return handler;
}
#include "moc_pxr_p.cpp"

View File

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

42
src/imageformats/pxr_p.h Normal file
View File

@ -0,0 +1,42 @@
/*
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_PXR_P_H
#define KIMG_PXR_P_H
#include <QImageIOPlugin>
#include <QScopedPointer>
class PXRHandlerPrivate;
class PXRHandler : public QImageIOHandler
{
public:
PXRHandler();
bool canRead() const override;
bool read(QImage *image) override;
bool supportsOption(QImageIOHandler::ImageOption option) const override;
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
private:
const QScopedPointer<PXRHandlerPrivate> d;
};
class PXRPlugin : public QImageIOPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "pxr.json")
public:
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
};
#endif // KIMG_PXR_P_H

View File

@ -31,6 +31,18 @@ namespace // Private
#define QOI_END_STREAM_PAD 8
struct QoiHeader {
QoiHeader()
: MagicNumber(0)
, Width(0)
, Height(0)
, Channels(0)
, Colorspace(2)
{
}
QoiHeader(const QoiHeader&) = default;
QoiHeader& operator=(const QoiHeader&) = default;
quint32 MagicNumber;
quint32 Width;
quint32 Height;
@ -297,7 +309,19 @@ static bool SaveQOI(QIODevice *device, const QoiHeader &qoi, const QImage &img)
} // namespace
class QOIHandlerPrivate
{
public:
QOIHandlerPrivate() {}
~QOIHandlerPrivate() {}
QoiHeader m_header;
};
QOIHandler::QOIHandler()
: QImageIOHandler()
, d(new QOIHandlerPrivate)
{
}
@ -328,7 +352,7 @@ bool QOIHandler::canRead(QIODevice *device)
QDataStream stream(head);
stream.setByteOrder(QDataStream::BigEndian);
QoiHeader qoi = {0, 0, 0, 0, 2};
QoiHeader qoi;
stream >> qoi;
return IsSupported(qoi);
@ -340,7 +364,7 @@ bool QOIHandler::read(QImage *image)
s.setByteOrder(QDataStream::BigEndian);
// Read image header
QoiHeader qoi = {0, 0, 0, 0, 2};
auto&& qoi = d->m_header;
s >> qoi;
// Check if file is supported
@ -402,7 +426,10 @@ QVariant QOIHandler::option(ImageOption option) const
QVariant v;
if (option == QImageIOHandler::Size) {
if (auto d = device()) {
auto&& header = d->m_header;
if (IsSupported(header)) {
v = QVariant::fromValue(QSize(header.Width, header.Height));
} else if (auto d = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
auto ba = d->read(sizeof(QoiHeader));
@ -410,10 +437,7 @@ QVariant QOIHandler::option(ImageOption option) const
QDataStream s(ba);
s.setByteOrder(QDataStream::BigEndian);
QoiHeader header = {0, 0, 0, 0, 2};
s >> header;
if (s.status() == QDataStream::Ok && IsSupported(header)) {
v = QVariant::fromValue(QSize(header.Width, header.Height));
}
@ -421,7 +445,10 @@ QVariant QOIHandler::option(ImageOption option) const
}
if (option == QImageIOHandler::ImageFormat) {
if (auto d = device()) {
auto&& header = d->m_header;
if (IsSupported(header)) {
v = QVariant::fromValue(imageFormat(header));
} else if (auto d = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
auto ba = d->read(sizeof(QoiHeader));
@ -429,10 +456,7 @@ QVariant QOIHandler::option(ImageOption option) const
QDataStream s(ba);
s.setByteOrder(QDataStream::BigEndian);
QoiHeader header = {0, 0, 0, 0, 2};
s >> header;
if (s.status() == QDataStream::Ok && IsSupported(header)) {
v = QVariant::fromValue(imageFormat(header));
}

View File

@ -9,7 +9,9 @@
#define KIMG_QOI_P_H
#include <QImageIOPlugin>
#include <QScopedPointer>
class QOIHandlerPrivate;
class QOIHandler : public QImageIOHandler
{
public:
@ -23,6 +25,9 @@ public:
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
private:
const QScopedPointer<QOIHandlerPrivate> d;
};
class QOIPlugin : public QImageIOPlugin

View File

@ -221,9 +221,16 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
// Read palette if needed.
if (ras.ColorMapType == RAS_COLOR_MAP_TYPE_RGB) {
// max 256 rgb elements palette is supported
if (ras.ColorMapLength > 768) {
return false;
}
QList<quint8> palette(ras.ColorMapLength);
for (quint32 i = 0; i < ras.ColorMapLength; ++i) {
s >> palette[i];
if (s.status() != QDataStream::Ok) {
return false;
}
}
QList<QRgb> colorTable;
for (quint32 i = 0, n = ras.ColorMapLength / 3; i < n; ++i) {
@ -233,9 +240,6 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
colorTable << qRgb(255, 255, 255);
}
img.setColorTable(colorTable);
if (s.status() != QDataStream::Ok) {
return false;
}
}
LineDecoder dec(s.device(), ras);
@ -332,7 +336,19 @@ static bool LoadRAS(QDataStream &s, const RasHeader &ras, QImage &img)
}
} // namespace
class RASHandlerPrivate
{
public:
RASHandlerPrivate() {}
~RASHandlerPrivate() {}
RasHeader m_header;
};
RASHandler::RASHandler()
: QImageIOHandler()
, d(new RASHandlerPrivate)
{
}
@ -380,7 +396,7 @@ bool RASHandler::read(QImage *outImage)
s.setByteOrder(QDataStream::BigEndian);
// Read image header.
RasHeader ras;
auto&& ras = d->m_header;
s >> ras;
if (ras.ColorMapLength > kMaxQVectorSize) {
@ -422,18 +438,19 @@ QVariant RASHandler::option(ImageOption option) const
QVariant v;
if (option == QImageIOHandler::Size) {
if (auto d = device()) {
auto&& header = d->m_header;
if (IsSupported(header)) {
v = QVariant::fromValue(QSize(header.Width, header.Height));
}
else if (auto dev = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
auto ba = d->read(RasHeader::SIZE);
d->rollbackTransaction();
dev->startTransaction();
auto ba = dev->read(RasHeader::SIZE);
dev->rollbackTransaction();
QDataStream s(ba);
s.setByteOrder(QDataStream::BigEndian);
RasHeader header;
s >> header;
if (s.status() == QDataStream::Ok && IsSupported(header)) {
v = QVariant::fromValue(QSize(header.Width, header.Height));
}
@ -441,18 +458,19 @@ QVariant RASHandler::option(ImageOption option) const
}
if (option == QImageIOHandler::ImageFormat) {
if (auto d = device()) {
auto&& header = d->m_header;
if (IsSupported(header)) {
v = QVariant::fromValue(imageFormat(header));
}
else if (auto dev = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
auto ba = d->read(RasHeader::SIZE);
d->rollbackTransaction();
dev->startTransaction();
auto ba = dev->read(RasHeader::SIZE);
dev->rollbackTransaction();
QDataStream s(ba);
s.setByteOrder(QDataStream::BigEndian);
RasHeader header;
s >> header;
if (s.status() == QDataStream::Ok && IsSupported(header)) {
v = QVariant::fromValue(imageFormat(header));
}

View File

@ -10,7 +10,9 @@
#define KIMG_RAS_P_H
#include <QImageIOPlugin>
#include <QScopedPointer>
class RASHandlerPrivate;
class RASHandler : public QImageIOHandler
{
public:
@ -23,6 +25,9 @@ public:
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
private:
const QScopedPointer<RASHandlerPrivate> d;
};
class RASPlugin : public QImageIOPlugin

View File

@ -690,29 +690,29 @@ bool LoadRAW(QImageIOHandler *handler, QImage &img)
xmpPacket = QString::fromUtf8(xmpdata, xmplen);
}
// Add info from LibRAW structs (e.g. GPS position, info about lens, info about shot and flash, etc...)
img.setText(QStringLiteral("XML:com.adobe.xmp"), updateXmpPacket(xmpPacket, rawProcessor.get()));
img.setText(QStringLiteral(META_KEY_XMP_ADOBE), updateXmpPacket(xmpPacket, rawProcessor.get()));
auto model = QString::fromUtf8(iparams.normalized_model);
if (!model.isEmpty()) {
img.setText(QStringLiteral("Model"), model);
img.setText(QStringLiteral(META_KEY_MODEL), model);
}
auto manufacturer = QString::fromUtf8(iparams.normalized_make);
if (!manufacturer.isEmpty()) {
img.setText(QStringLiteral("Manufacturer"), manufacturer);
img.setText(QStringLiteral(META_KEY_MANUFACTURER), manufacturer);
}
auto software = QString::fromUtf8(iparams.software);
if (!software.isEmpty()) {
img.setText(QStringLiteral("Software"), software);
img.setText(QStringLiteral(META_KEY_SOFTWARE), software);
}
auto &&iother = rawProcessor->imgdata.other;
auto description = QString::fromUtf8(iother.desc);
if (!description.isEmpty()) {
img.setText(QStringLiteral("Description"), description);
img.setText(QStringLiteral(META_KEY_DESCRIPTION), description);
}
auto artist = QString::fromUtf8(iother.artist);
if (!artist.isEmpty()) {
img.setText(QStringLiteral("Author"), artist);
img.setText(QStringLiteral(META_KEY_AUTHOR), artist);
}
return true;

View File

@ -360,11 +360,15 @@ bool SGIImage::readImage(QImage &img)
}
_lengthtab = new quint32[_numrows];
for (l = 0; l < _numrows; l++) {
for (l = 0; !_stream.atEnd() && l < _numrows; l++) {
_stream >> _lengthtab[l];
}
}
if (_stream.status() != QDataStream::Ok) {
return false;
}
_data = _dev->readAll();
// sanity check

View File

@ -72,9 +72,18 @@ const uchar *ScanLineConverter::convertedScanLine(const QImage &image, qint32 y)
if (!cs.isValid()) {
cs = _defaultColorSpace;
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
if (tmp.depth() < 8 && cs.colorModel() == QColorSpace::ColorModel::Gray) {
tmp.convertTo(QImage::Format_Grayscale8);
}
else if (tmp.depth() < 24 && cs.colorModel() == QColorSpace::ColorModel::Rgb) {
tmp.convertTo(tmp.hasAlphaChannel() ? QImage::Format_ARGB32 : QImage::Format_RGB32);
}
#else
if (tmp.depth() < 24) {
tmp.convertTo(tmp.hasAlphaChannel() ? QImage::Format_ARGB32 : QImage::Format_RGB32);
}
#endif
tmp.setColorSpace(cs);
tmp.convertToColorSpace(_colorSpace);
}

View File

@ -56,18 +56,18 @@ enum TGAType {
/** Tga Header. */
struct TgaHeader {
uchar id_length;
uchar colormap_type;
uchar image_type;
ushort colormap_index;
ushort colormap_length;
uchar colormap_size;
ushort x_origin;
ushort y_origin;
ushort width;
ushort height;
uchar pixel_size;
uchar flags;
uchar id_length = 0;
uchar colormap_type = 0;
uchar image_type = 0;
ushort colormap_index = 0;
ushort colormap_length = 0;
uchar colormap_size = 0;
ushort x_origin = 0;
ushort y_origin = 0;
ushort width = 0;
ushort height = 0;
uchar pixel_size = 0;
uchar flags = 0;
enum {
SIZE = 18,
@ -407,7 +407,18 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
} // namespace
class TGAHandlerPrivate
{
public:
TGAHandlerPrivate() {}
~TGAHandlerPrivate() {}
TgaHeader m_header;
};
TGAHandler::TGAHandler()
: QImageIOHandler()
, d(new TGAHandlerPrivate)
{
}
@ -424,20 +435,20 @@ bool TGAHandler::read(QImage *outImage)
{
// qDebug() << "Loading TGA file!";
auto d = device();
TgaHeader tga;
if (!peekHeader(d, tga) || !IsSupported(tga)) {
auto dev = device();
auto&& tga = d->m_header;
if (!peekHeader(dev, tga) || !IsSupported(tga)) {
// qDebug() << "This TGA file is not valid.";
return false;
}
if (d->isSequential()) {
d->read(TgaHeader::SIZE + tga.id_length);
if (dev->isSequential()) {
dev->read(TgaHeader::SIZE + tga.id_length);
} else {
d->seek(TgaHeader::SIZE + tga.id_length);
dev->seek(TgaHeader::SIZE + tga.id_length);
}
QDataStream s(d);
QDataStream s(dev);
s.setByteOrder(QDataStream::LittleEndian);
// Check image file format.
@ -519,18 +530,22 @@ QVariant TGAHandler::option(ImageOption option) const
QVariant v;
if (option == QImageIOHandler::Size) {
if (auto d = device()) {
TgaHeader header;
if (peekHeader(d, header) && IsSupported(header)) {
auto&& header = d->m_header;
if (IsSupported(header)) {
v = QVariant::fromValue(QSize(header.width, header.height));
} else if (auto dev = device()) {
if (peekHeader(dev, header) && IsSupported(header)) {
v = QVariant::fromValue(QSize(header.width, header.height));
}
}
}
if (option == QImageIOHandler::ImageFormat) {
if (auto d = device()) {
TgaHeader header;
if (peekHeader(d, header) && IsSupported(header)) {
auto&& header = d->m_header;
if (IsSupported(header)) {
v = QVariant::fromValue(imageFormat(header));
} else if (auto dev = device()) {
if (peekHeader(dev, header) && IsSupported(header)) {
v = QVariant::fromValue(imageFormat(header));
}
}

View File

@ -9,7 +9,9 @@
#define KIMG_TGA_P_H
#include <QImageIOPlugin>
#include <QScopedPointer>
class TGAHandlerPrivate;
class TGAHandler : public QImageIOHandler
{
public:
@ -23,6 +25,9 @@ public:
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
private:
const QScopedPointer<TGAHandlerPrivate> d;
};
class TGAPlugin : public QImageIOPlugin

View File

@ -13,6 +13,26 @@
#include <QImage>
#include <QImageIOHandler>
// Image metadata keys to use in plugins (so they are consistent)
#define META_KEY_ALTITUDE "Altitude"
#define META_KEY_AUTHOR "Author"
#define META_KEY_COMMENT "Comment"
#define META_KEY_COPYRIGHT "Copyright"
#define META_KEY_CREATIONDATE "CreationDate"
#define META_KEY_DESCRIPTION "Description"
#define META_KEY_DOCUMENTNAME "DocumentName"
#define META_KEY_HOSTCOMPUTER "HostComputer"
#define META_KEY_LATITUDE "Latitude"
#define META_KEY_LONGITUDE "Longitude"
#define META_KEY_HOSTCOMPUTER "HostComputer"
#define META_KEY_MANUFACTURER "Manufacturer"
#define META_KEY_MODEL "Model"
#define META_KEY_OWNER "Owner"
#define META_KEY_SOFTWARE "Software"
#define META_KEY_TITLE "Title"
#define META_KEY_XML_GIMP "XML:org.gimp.xml"
#define META_KEY_XMP_ADOBE "XML:com.adobe.xmp"
// QList uses some extra space for stuff, hence the 32 here suggested by Thiago Macieira
static constexpr int kMaxQVectorSize = std::numeric_limits<int>::max() - 32;

View File

@ -1091,7 +1091,9 @@ bool XCFImageFormat::loadProperty(QDataStream &xcf_io, PropType &type, QByteArra
size = 0;
} else {
xcf_io >> size;
if (size > 256000) {
if (size > 256000 * 4) {
// NOTE: I didn't find any reference to maximum property dimensions in the specs, so I assume it's just a sanity check.
qCDebug(XCFPLUGIN) << "XCF: loadProperty skips" << type << "due to size being too large";
return false;
}
data = new char[size];
@ -1594,7 +1596,7 @@ void XCFImageFormat::setImageParasites(const XCFImage &xcf_image, QImage &image)
// comments may also be present in the "gimp-metadata" parasite.
if (key == QStringLiteral("gimp-comment")) {
value.replace('\0', QByteArray());
image.setText(QStringLiteral("Comment"), QString::fromUtf8(value));
image.setText(QStringLiteral(META_KEY_COMMENT), QString::fromUtf8(value));
continue;
}
@ -1605,7 +1607,7 @@ void XCFImageFormat::setImageParasites(const XCFImage &xcf_image, QImage &image)
// NOTE: I arbitrary defined the metadata "XML:org.gimp.xml" because it seems
// a GIMP proprietary XML format (no xmlns defined)
value.replace('\0', QByteArray());
image.setText(QStringLiteral("XML:org.gimp.xml"), QString::fromUtf8(value));
image.setText(QStringLiteral(META_KEY_XML_GIMP), QString::fromUtf8(value));
continue;
}
@ -1621,7 +1623,7 @@ void XCFImageFormat::setImageParasites(const XCFImage &xcf_image, QImage &image)
// XMP packet is found (e.g. when reading a PNG saved by Photoshop).
// I reused the same key because some programs could search for it.
value.replace('\0', QByteArray());
image.setText(QStringLiteral("XML:com.adobe.xmp"), QString::fromUtf8(value));
image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromUtf8(value));
continue;
}
#endif
@ -2753,10 +2755,10 @@ void XCFImageFormat::copyLayerToImage(XCFImage &xcf_image)
// For each tile...
for (uint j = 0; j < layer.nrows; j++) {
uint y = j * TILE_HEIGHT;
qint32 y = qint32(j * TILE_HEIGHT);
for (uint i = 0; i < layer.ncols; i++) {
uint x = i * TILE_WIDTH;
qint32 x = qint32(i * TILE_WIDTH);
// This seems the best place to apply the dissolve because it
// depends on the global position of each tile's
@ -3043,7 +3045,7 @@ void XCFImageFormat::mergeLayerIntoImage(XCFImage &xcf_image)
merge = mergeRGBToRGB;
break;
case GRAY_GIMAGE:
if (layer.opacity == OPAQUE_OPACITY) {
if (layer.opacity == OPAQUE_OPACITY && xcf_image.image.depth() <= 8) {
merge = mergeGrayToGray;
} else {
merge = mergeGrayToRGB;
@ -3179,10 +3181,10 @@ void XCFImageFormat::mergeLayerIntoImage(XCFImage &xcf_image)
qCDebug(XCFPLUGIN) << "Using QPainter for mode" << layer.mode;
for (uint j = 0; j < layer.nrows; j++) {
uint y = j * TILE_HEIGHT;
qint32 y = qint32(j * TILE_HEIGHT);
for (uint i = 0; i < layer.ncols; i++) {
uint x = i * TILE_WIDTH;
qint32 x = qint32(i * TILE_WIDTH);
QImage &tile = layer.image_tiles[j][i];
if (x + layer.x_offset < MAX_IMAGE_WIDTH &&
@ -3208,10 +3210,10 @@ void XCFImageFormat::mergeLayerIntoImage(XCFImage &xcf_image)
#endif
for (uint j = 0; j < layer.nrows; j++) {
uint y = j * TILE_HEIGHT;
qint32 y = qint32(j * TILE_HEIGHT);
for (uint i = 0; i < layer.ncols; i++) {
uint x = i * TILE_WIDTH;
qint32 x = qint32(i * TILE_WIDTH);
// This seems the best place to apply the dissolve because it
// depends on the global position of each tile's
@ -3851,6 +3853,9 @@ bool XCFImageFormat::mergeGrayAToRGB(const Layer &layer, uint i, uint j, int k,
}
switch (layer.mode) {
case GIMP_LAYER_MODE_NORMAL:
case GIMP_LAYER_MODE_NORMAL_LEGACY:
break;
case GIMP_LAYER_MODE_MULTIPLY:
case GIMP_LAYER_MODE_MULTIPLY_LEGACY: {
src = INT_MULT(src, dst);
@ -4144,7 +4149,9 @@ bool XCFHandler::canRead() const
bool XCFHandler::read(QImage *image)
{
XCFImageFormat xcfif;
return xcfif.readXCF(device(), image);
auto ok = xcfif.readXCF(device(), image);
m_imageSize = image->size();
return ok;
}
bool XCFHandler::write(const QImage &)
@ -4164,6 +4171,9 @@ QVariant XCFHandler::option(ImageOption option) const
QVariant v;
if (option == QImageIOHandler::Size) {
if (!m_imageSize.isEmpty()) {
return m_imageSize;
}
/*
* The image structure always starts at offset 0 in the XCF file.
* byte[9] "gimp xcf " File type identification
@ -4176,7 +4186,7 @@ QVariant XCFHandler::option(ImageOption option) const
* uint32 width Width of canvas
* uint32 height Height of canvas
*/
if (auto d = device()) {
else if (auto d = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
auto ba9 = d->read(9); // "gimp xcf "

View File

@ -24,6 +24,13 @@ public:
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
private:
/*!
* \brief m_imageSize
* Image size cache used by option()
*/
QSize m_imageSize;
};
class XCFPlugin : public QImageIOPlugin