Compare commits

..

19 Commits

Author SHA1 Message Date
ae6b724824 GIT_SILENT Upgrade ECM and KF version requirements for 5.93.0 release. 2022-04-02 10:00:12 +00:00
3e751dd80d Fix XCF parasites metadata in QImage and support to ICC profile
- Fix parasite "gimp-comment" not set due to null QImage
- Support to parasite "icc-profile" using Qt 5.14+ API
- Added parasite "gimp-image-metadata" as QImage metadata "XML:org.gimp.xml"
- Added a XCF with XML metadata and icc prifile embedded in autotest folder (generated by GIMP 2.10.30)
- Tested with Qt 5.15.2 and Qt 6.2.3 under Windows and Qt 6.2.3 under macOS
2022-03-23 23:34:33 +00:00
e69dff73e6 avif: encoder speed 7->6 2022-03-10 09:44:50 +01:00
64cfe52bee avif: fix jumpToImage 2022-03-10 09:39:53 +01:00
8732fc8487 avif: warn about non-recommended libavif configuration 2022-03-10 09:35:08 +01:00
d9729b7190 GIT_SILENT Upgrade ECM and KF version requirements for 5.92.0 release. 2022-03-05 11:15:00 +00:00
55d3c568b2 Add Qt6 Android CI 2022-03-01 16:04:48 +00:00
4afafee6c1 Add write tests for heif/avif/jxl
Unfortunately none of them pass since it seems they can't load a png,
save it to their format with loseless quality and read it back and get
exactly the same contents than the png
2022-02-18 00:05:01 +01:00
f04084e175 jxl: encoding improvements
Plug-in can save in 8bit depth now,
previously only 16bit was supported.
Memory and dimension limits were adjusted.
2022-02-16 10:26:56 +01:00
9911c9c2ea avif: adjust dimension and memory limits
With or height can be above 32k now (up to 64k), but
image should not have more than 256megapixels
(Default memory limit of libavif)
2022-02-11 16:01:07 +01:00
4ceef5164d GIT_SILENT Upgrade ECM and KF version requirements for 5.91.0 release. 2022-02-05 15:14:16 +00:00
3d2d91a08a Fix typo, should be qCWarning
GIT_SILENT
2022-02-03 16:34:06 +02:00
0a02458560 Check executables exist in PATH before passing them to QProcess
See:
https://kde.org/info/security/advisory-20220131-1.txt
https://mail.kde.org/pipermail/kde-devel/2022-January/000943.html
2022-02-03 11:32:24 +02:00
96836e849f Fix handling of null terminated ANI metadata with Qt6
In Qt5 converting a QByteArray to a QString stops at the first null byte,
in Qt6 the QByteArray size is respected, and trailing null bytes are
therefore included in the final QString. Explicitly determine the length
of the string data to deal with that.
2022-01-22 22:03:21 +01:00
f4edb7296f Add CI qt6 support 2022-01-08 09:27:14 +01:00
56376ffd66 GIT_SILENT Upgrade ECM and KF version requirements for 5.90.0 release. 2022-01-01 12:16:00 +00:00
f534254063 GIT_SILENT: It compiles fine without deprecated methods 2021-12-26 11:35:54 +01:00
32347725cb Fix typo 2021-12-21 20:28:05 +01:00
56e762c563 Make it compile against qt6 2021-12-21 20:10:11 +01:00
15 changed files with 456 additions and 164 deletions

View File

@ -5,3 +5,5 @@ include:
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml

View File

@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.16)
project(KImageFormats)
include(FeatureSummary)
find_package(ECM 5.89.0 NO_MODULE)
find_package(ECM 5.93.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)
@ -20,7 +20,7 @@ include(CheckIncludeFiles)
include(FindPkgConfig)
set(REQUIRED_QT_VERSION 5.15.2)
find_package(Qt5Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
find_package(Qt${QT_MAJOR_VERSION}Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
find_package(KF5Archive)
set_package_properties(KF5Archive PROPERTIES
@ -32,12 +32,12 @@ set_package_properties(KF5Archive PROPERTIES
# this available in PATH
set(BUILD_EPS_PLUGIN FALSE)
if (UNIX)
find_package(Qt5PrintSupport ${REQUIRED_QT_VERSION} NO_MODULE)
set_package_properties(Qt5PrintSupport PROPERTIES
find_package(Qt${QT_MAJOR_VERSION}PrintSupport ${REQUIRED_QT_VERSION} NO_MODULE)
set_package_properties(Qt${QT_MAJOR_VERSION}PrintSupport PROPERTIES
PURPOSE "Required for the QImage plugin for EPS images"
TYPE OPTIONAL
)
if (Qt5PrintSupport_FOUND)
if (TARGET Qt${QT_MAJOR_VERSION}::PrintSupport)
set(BUILD_EPS_PLUGIN TRUE)
endif()
endif()
@ -70,10 +70,8 @@ if(KIMAGEFORMATS_JXL)
endif()
add_feature_info(LibJXL LibJXL_FOUND "required for the QImage plugin for JPEG XL images")
# 050d00 (5.13) triggers a BIC in qimageiohandler.h, in Qt 5.13, so do not enable that until we can require 5.14
# https://codereview.qt-project.org/c/qt/qtbase/+/279215
add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050f02)
add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x055100)
add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x055900)
add_subdirectory(src)
if (BUILD_TESTING)
add_subdirectory(autotests)

View File

@ -15,7 +15,7 @@ macro(kimageformats_read_tests)
if (NOT TARGET readtest)
add_executable(readtest readtest.cpp)
target_link_libraries(readtest Qt5::Gui)
target_link_libraries(readtest Qt${QT_MAJOR_VERSION}::Gui)
target_compile_definitions(readtest
PRIVATE IMAGEDIR="${CMAKE_CURRENT_SOURCE_DIR}/read")
ecm_mark_as_test(readtest)
@ -30,23 +30,35 @@ macro(kimageformats_read_tests)
endmacro()
macro(kimageformats_write_tests)
cmake_parse_arguments(KIF_RT "" "FUZZ" "" ${ARGN})
set(_fuzzarg)
if (KIF_RT_FUZZ)
set(_fuzzarg -f ${KIF_RT_FUZZ})
endif()
if (NOT TARGET writetest)
add_executable(writetest writetest.cpp)
target_link_libraries(writetest Qt5::Gui)
target_link_libraries(writetest Qt${QT_MAJOR_VERSION}::Gui)
target_compile_definitions(writetest
PRIVATE IMAGEDIR="${CMAKE_CURRENT_SOURCE_DIR}/write")
ecm_mark_as_test(writetest)
endif()
foreach(_testname ${ARGN})
foreach(_testname ${KIF_RT_UNPARSED_ARGUMENTS})
string(REGEX MATCH "-lossless$" _is_lossless "${_testname}")
string(REGEX MATCH "-nodatacheck" _is_no_data_check "${_testname}")
unset(lossless_arg)
unset(no_data_check_arg)
if (_is_lossless)
set(lossless_arg "--lossless")
string(REGEX REPLACE "-lossless$" "" _testname "${_testname}")
endif()
if (_is_no_data_check)
set(no_data_check_arg "--no-data-check")
string(REGEX REPLACE "-nodatacheck$" "" _testname "${_testname}")
endif()
add_test(
NAME kimageformats-write-${_testname}
COMMAND writetest ${lossless_arg} ${_testname}
COMMAND writetest ${lossless_arg} ${no_data_check_arg} ${_fuzzarg} ${_testname}
)
endforeach(_testname)
endmacro()
@ -74,18 +86,29 @@ if (TARGET avif)
kimageformats_read_tests(
avif
)
# because the plug-ins use RGB->YUV conversion which sometimes results in 1 value difference.
kimageformats_write_tests(FUZZ 1
avif-nodatacheck-lossless
)
endif()
if (LibHeif_FOUND)
kimageformats_read_tests(
heif
)
# because the plug-ins use RGB->YUV conversion which sometimes results in 1 value difference.
kimageformats_write_tests(FUZZ 1
heif-nodatacheck-lossless
)
endif()
if (LibJXL_FOUND AND LibJXLThreads_FOUND)
kimageformats_read_tests(
jxl
)
kimageformats_write_tests(
jxl-nodatacheck-lossless
)
endif()
# Allow some fuzziness when reading this formats, to allow for
@ -117,19 +140,19 @@ if (OpenEXR_FOUND)
# FIXME: OpenEXR tests
endif()
find_package(Qt5Test ${REQUIRED_QT_VERSION} CONFIG QUIET)
find_package(Qt${QT_MAJOR_VERSION}Test ${REQUIRED_QT_VERSION} CONFIG QUIET)
if(NOT Qt5Test_FOUND)
message(STATUS "Qt5Test not found, some autotests will not be built.")
if(NOT TARGET Qt${QT_MAJOR_VERSION}::Test)
message(STATUS "Qt${QT_MAJOR_VERSION}Test not found, some autotests will not be built.")
return()
endif()
add_executable(pictest pictest.cpp)
target_link_libraries(pictest Qt5::Gui Qt5::Test)
target_link_libraries(pictest Qt${QT_MAJOR_VERSION}::Gui Qt${QT_MAJOR_VERSION}::Test)
ecm_mark_as_test(pictest)
add_test(NAME kimageformats-pic COMMAND pictest)
add_executable(anitest anitest.cpp)
target_link_libraries(anitest Qt5::Gui Qt5::Test)
target_link_libraries(anitest Qt${QT_MAJOR_VERSION}::Gui Qt${QT_MAJOR_VERSION}::Test)
ecm_mark_as_test(anitest)
add_test(NAME kimageformats-ani COMMAND anitest)

37
autotests/fuzzyeq.cpp Normal file
View File

@ -0,0 +1,37 @@
/*
SPDX-FileCopyrightText: 2014 Alex Merry <alex.merry@kdemail.net>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
template<class Trait>
static bool fuzzyeq(const QImage &im1, const QImage &im2, uchar fuzziness)
{
Q_ASSERT(im1.format() == im2.format());
Q_ASSERT(im1.depth() == 24 || im1.depth() == 32 || im1.depth() == 64);
const int height = im1.height();
const int width = im1.width();
for (int i = 0; i < height; ++i) {
const Trait *line1 = reinterpret_cast<const Trait *>(im1.scanLine(i));
const Trait *line2 = reinterpret_cast<const Trait *>(im2.scanLine(i));
for (int j = 0; j < width; ++j) {
if (line1[j] > line2[j]) {
if (line1[j] - line2[j] > fuzziness) {
return false;
}
} else {
if (line2[j] - line1[j] > fuzziness) {
return false;
}
}
}
}
return true;
}
// allow each byte to be different by up to 1, to allow for rounding errors
static bool fuzzyeq(const QImage &im1, const QImage &im2, uchar fuzziness)
{
return (im1.depth() == 64) ? fuzzyeq<quint16>(im1, im2, fuzziness) : fuzzyeq<quint8>(im1, im2, fuzziness);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

View File

@ -16,6 +16,8 @@
#include "../tests/format-enum.h"
#include "fuzzyeq.cpp"
static void writeImageData(const char *name, const QString &filename, const QImage &image)
{
QFile file(filename);
@ -31,38 +33,6 @@ static void writeImageData(const char *name, const QString &filename, const QIma
}
}
template<class Trait>
static bool fuzzyeq(const QImage &im1, const QImage &im2, uchar fuzziness)
{
Q_ASSERT(im1.format() == im2.format());
Q_ASSERT(im1.depth() == 24 || im1.depth() == 32 || im1.depth() == 64);
const int height = im1.height();
const int width = im1.width();
for (int i = 0; i < height; ++i) {
const Trait *line1 = reinterpret_cast<const Trait *>(im1.scanLine(i));
const Trait *line2 = reinterpret_cast<const Trait *>(im2.scanLine(i));
for (int j = 0; j < width; ++j) {
if (line1[j] > line2[j]) {
if (line1[j] - line2[j] > fuzziness) {
return false;
}
} else {
if (line2[j] - line1[j] > fuzziness) {
return false;
}
}
}
}
return true;
}
// allow each byte to be different by up to 1, to allow for rounding errors
static bool fuzzyeq(const QImage &im1, const QImage &im2, uchar fuzziness)
{
return (im1.depth() == 64) ? fuzzyeq<quint16>(im1, im2, fuzziness) : fuzzyeq<quint8>(im1, im2, fuzziness);
}
// Returns the original format if we support, or returns
// format which we preferred to use for `fuzzyeq()`.
// We do only support formats with 8-bits/16-bits pre pixel.

View File

@ -16,6 +16,8 @@
#include <QImageWriter>
#include <QTextStream>
#include "fuzzyeq.cpp"
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
@ -31,7 +33,13 @@ int main(int argc, char **argv)
parser.addPositionalArgument(QStringLiteral("format"), QStringLiteral("format to test."));
QCommandLineOption lossless(QStringList() << QStringLiteral("l") << QStringLiteral("lossless"),
QStringLiteral("Check that reading back the data gives the same image."));
QCommandLineOption ignoreDataCheck({QStringLiteral("no-data-check")}, QStringLiteral("Don't check that write data is exactly the same."));
QCommandLineOption fuzz(QStringList() << QStringLiteral("f") << QStringLiteral("fuzz"),
QStringLiteral("Allow for some deviation in ARGB data."),
QStringLiteral("max"));
parser.addOption(lossless);
parser.addOption(ignoreDataCheck);
parser.addOption(fuzz);
parser.process(app);
@ -44,11 +52,26 @@ int main(int argc, char **argv)
parser.showHelp(1);
}
uchar fuzziness = 0;
if (parser.isSet(fuzz)) {
bool ok;
uint fuzzarg = parser.value(fuzz).toUInt(&ok);
if (!ok || fuzzarg > 255) {
QTextStream(stderr) << "Error: max fuzz argument must be a number between 0 and 255\n";
parser.showHelp(1);
}
fuzziness = uchar(fuzzarg);
}
QString suffix = args.at(0);
QByteArray format = suffix.toLatin1();
QDir imgdir(QStringLiteral(IMAGEDIR));
imgdir.setNameFilters(QStringList(QLatin1String("*.") + suffix));
if (parser.isSet(ignoreDataCheck)) {
imgdir.setNameFilters({QLatin1String("*.png")});
} else {
imgdir.setNameFilters(QStringList(QLatin1String("*.") + suffix));
}
imgdir.setFilter(QDir::Files);
int passed = 0;
@ -58,8 +81,13 @@ int main(int argc, char **argv)
<< "Starting basic write tests for " << suffix << " images *********\n";
const QFileInfoList lstImgDir = imgdir.entryInfoList();
for (const QFileInfo &fi : lstImgDir) {
int suffixPos = fi.filePath().count() - suffix.count();
QString pngfile = fi.filePath().replace(suffixPos, suffix.count(), QStringLiteral("png"));
QString pngfile;
if (parser.isSet(ignoreDataCheck)) {
pngfile = fi.filePath();
} else {
int suffixPos = fi.filePath().count() - suffix.count();
pngfile = fi.filePath().replace(suffixPos, suffix.count(), QStringLiteral("png"));
}
QString pngfilename = QFileInfo(pngfile).fileName();
QImageReader pngReader(pngfile, "png");
@ -70,29 +98,13 @@ int main(int argc, char **argv)
continue;
}
QFile expFile(fi.filePath());
if (!expFile.open(QIODevice::ReadOnly)) {
QTextStream(stdout) << "ERROR: " << fi.fileName() << ": could not open " << fi.fileName() << ": " << expFile.errorString() << "\n";
++failed;
continue;
}
QByteArray expData = expFile.readAll();
if (expData.isEmpty()) {
// check if there was actually anything to read
expFile.reset();
char buf[1];
qint64 result = expFile.read(buf, 1);
if (result < 0) {
QTextStream(stdout) << "ERROR: " << fi.fileName() << ": could not load " << fi.fileName() << ": " << expFile.errorString() << "\n";
++failed;
continue;
}
}
QByteArray writtenData;
{
QBuffer buffer(&writtenData);
QImageWriter imgWriter(&buffer, format.constData());
if (parser.isSet(lossless)) {
imgWriter.setQuality(100);
}
if (!imgWriter.write(pngImage)) {
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": failed to write image data\n";
++failed;
@ -100,10 +112,31 @@ int main(int argc, char **argv)
}
}
if (expData != writtenData) {
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": written data differs from " << fi.fileName() << "\n";
++failed;
continue;
if (!parser.isSet(ignoreDataCheck)) {
QFile expFile(fi.filePath());
if (!expFile.open(QIODevice::ReadOnly)) {
QTextStream(stdout) << "ERROR: " << fi.fileName() << ": could not open " << fi.fileName() << ": " << expFile.errorString() << "\n";
++failed;
continue;
}
QByteArray expData = expFile.readAll();
if (expData.isEmpty()) {
// check if there was actually anything to read
expFile.reset();
char buf[1];
qint64 result = expFile.read(buf, 1);
if (result < 0) {
QTextStream(stdout) << "ERROR: " << fi.fileName() << ": could not load " << fi.fileName() << ": " << expFile.errorString() << "\n";
++failed;
continue;
}
}
if (expData != writtenData) {
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": written data differs from " << fi.fileName() << "\n";
++failed;
continue;
}
}
QImage reReadImage;
@ -119,8 +152,18 @@ int main(int argc, char **argv)
}
if (parser.isSet(lossless)) {
if (pngImage != reReadImage) {
if (!fuzzyeq(pngImage, reReadImage, fuzziness)) {
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": re-reading the data resulted in a different image\n";
if (pngImage.size() == reReadImage.size()) {
for (int i = 0; i < pngImage.width(); ++i) {
for (int j = 0; j < pngImage.height(); ++j) {
if (pngImage.pixel(i, j) != reReadImage.pixel(i, j)) {
QTextStream(stdout) << "Pixel is different " << i << ',' << j << ' ' << pngImage.pixel(i, j) << ' ' << reReadImage.pixel(i, j)
<< '\n';
}
}
}
}
++failed;
continue;
}

View File

@ -12,41 +12,41 @@ function(kimageformats_add_plugin plugin)
add_library(${plugin} MODULE ${KIF_ADD_PLUGIN_SOURCES})
set_target_properties(${plugin} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/imageformats")
target_link_libraries(${plugin} Qt5::Gui)
target_link_libraries(${plugin} Qt${QT_MAJOR_VERSION}::Gui)
install(TARGETS ${plugin} DESTINATION ${KDE_INSTALL_QTPLUGINDIR}/imageformats)
endfunction()
##################################
kimageformats_add_plugin(kimg_ani SOURCES ani.cpp)
install(FILES ani.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES ani.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
if (TARGET avif)
kimageformats_add_plugin(kimg_avif SOURCES "avif.cpp")
target_link_libraries(kimg_avif "avif")
install(FILES avif.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES avif.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
endif()
##################################
install(FILES dds-qt.desktop RENAME dds.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES dds-qt.desktop RENAME dds.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
if (BUILD_EPS_PLUGIN)
if (Qt5PrintSupport_FOUND)
if (TARGET Qt${QT_MAJOR_VERSION}::PrintSupport)
kimageformats_add_plugin(kimg_eps SOURCES eps.cpp)
target_link_libraries(kimg_eps Qt5::PrintSupport)
install(FILES eps.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
target_link_libraries(kimg_eps Qt${QT_MAJOR_VERSION}::PrintSupport)
install(FILES eps.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
endif()
endif()
##################################
# need this for Qt's version of the plugin
install(FILES jp2.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES jp2.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
@ -64,13 +64,13 @@ if(OpenEXR_FOUND)
endif()
kde_target_enable_exceptions(kimg_exr PRIVATE)
install(FILES exr.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES exr.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
endif()
##################################
kimageformats_add_plugin(kimg_hdr SOURCES hdr.cpp)
install(FILES hdr.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES hdr.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
@ -78,7 +78,7 @@ if (LibHeif_FOUND)
kimageformats_add_plugin(kimg_heif SOURCES heif.cpp)
target_link_libraries(kimg_heif PkgConfig::LibHeif)
kde_target_enable_exceptions(kimg_heif PRIVATE)
install(FILES heif.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES heif.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
endif()
##################################
@ -86,43 +86,43 @@ endif()
if (LibJXL_FOUND AND LibJXLThreads_FOUND)
kimageformats_add_plugin(kimg_jxl SOURCES jxl.cpp)
target_link_libraries(kimg_jxl PkgConfig::LibJXL PkgConfig::LibJXLThreads)
install(FILES jxl.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES jxl.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
endif()
##################################
kimageformats_add_plugin(kimg_pcx SOURCES pcx.cpp)
install(FILES pcx.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES pcx.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
kimageformats_add_plugin(kimg_pic SOURCES pic.cpp)
install(FILES pic.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES pic.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
kimageformats_add_plugin(kimg_psd SOURCES psd.cpp)
install(FILES psd.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES psd.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
kimageformats_add_plugin(kimg_ras SOURCES ras.cpp)
install(FILES ras.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES ras.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
kimageformats_add_plugin(kimg_rgb SOURCES rgb.cpp)
install(FILES rgb.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES rgb.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
kimageformats_add_plugin(kimg_tga SOURCES tga.cpp)
install(FILES tga.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES tga.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
kimageformats_add_plugin(kimg_xcf SOURCES xcf.cpp)
install(FILES xcf.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES xcf.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
##################################
@ -130,10 +130,10 @@ if (KF5Archive_FOUND)
kimageformats_add_plugin(kimg_kra SOURCES kra.cpp)
target_link_libraries(kimg_kra KF5::Archive)
install(FILES kra.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES kra.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
kimageformats_add_plugin(kimg_ora SOURCES ora.cpp)
target_link_libraries(kimg_ora KF5::Archive)
install(FILES ora.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/)
install(FILES ora.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}/qimageioplugins/)
endif()

View File

@ -12,6 +12,8 @@
#include <QVariant>
#include <QtEndian>
#include <cstring>
namespace
{
struct ChunkHeader {
@ -419,7 +421,7 @@ bool ANIHandler::ensureScanned() const
}
// FIXME encoding
const QString stringValue = QString::fromLocal8Bit(value);
const QString stringValue = QString::fromLocal8Bit(value.constData(), std::strlen(value.constData()));
if (chunkId == "INAM") {
mutableThis->m_name = stringValue;
} else if (chunkId == "IART") {

View File

@ -133,7 +133,7 @@ bool QAVIFHandler::ensureDecoder()
m_container_width = m_decoder->image->width;
m_container_height = m_decoder->image->height;
if ((m_container_width > 32768) || (m_container_height > 32768)) {
if ((m_container_width > 65535) || (m_container_height > 65535)) {
qWarning("AVIF image (%dx%d) is too large!", m_container_width, m_container_height);
m_parseState = ParseAvifError;
return false;
@ -145,6 +145,12 @@ bool QAVIFHandler::ensureDecoder()
return false;
}
if (m_container_width > ((16384 * 16384) / m_container_height)) {
qWarning("AVIF image (%dx%d) has more than 256 megapixels!", m_container_width, m_container_height);
m_parseState = ParseAvifError;
return false;
}
m_parseState = ParseAvifSuccess;
if (decode_one_frame()) {
return true;
@ -417,15 +423,39 @@ bool QAVIFHandler::read(QImage *image)
bool QAVIFHandler::write(const QImage &image)
{
if (image.format() == QImage::Format_Invalid) {
qWarning("No image data to save");
qWarning("No image data to save!");
return false;
}
if ((image.width() > 32768) || (image.height() > 32768)) {
qWarning("Image is too large");
if ((image.width() > 0) && (image.height() > 0)) {
if ((image.width() > 65535) || (image.height() > 65535)) {
qWarning("Image (%dx%d) is too large to save!", image.width(), image.height());
return false;
}
if (image.width() > ((16384 * 16384) / image.height())) {
qWarning("Image (%dx%d) will not be saved because it has more than 256 megapixels!", image.width(), image.height());
return false;
}
if ((image.width() > 32768) || (image.height() > 32768)) {
qWarning("Image (%dx%d) has a dimension above 32768 pixels, saved AVIF may not work in other software!", image.width(), image.height());
}
} else {
qWarning("Image has zero dimension!");
return false;
}
const char *encoder_name = avifCodecName(AVIF_CODEC_CHOICE_AUTO, AVIF_CODEC_FLAG_CAN_ENCODE);
if (!encoder_name) {
qWarning("Cannot save AVIF images because libavif was built without AV1 encoders!");
return false;
}
if (m_quality >= 100 && !avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE)) {
qWarning("You are using %s encoder. It is recommended to enable libAOM encoder in libavif for better near-lossless compression.", encoder_name);
}
int maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY * (100 - qBound(0, m_quality, 100)) / 100;
int minQuantizer = 0;
int maxQuantizerAlpha = 0;
@ -718,7 +748,7 @@ bool QAVIFHandler::write(const QImage &image)
encoder->maxQuantizerAlpha = maxQuantizerAlpha;
}
encoder->speed = 7;
encoder->speed = 6;
res = avifEncoderWrite(encoder, avif, &raw);
avifEncoderDestroy(encoder);
@ -871,7 +901,8 @@ bool QAVIFHandler::jumpToImage(int imageNumber)
return false;
}
if (imageNumber == m_decoder->imageCount) { // we are here already
if (imageNumber == m_decoder->imageIndex) { // we are here already
m_must_jump_to_next_image = false;
return true;
}

View File

@ -15,6 +15,7 @@
#include <QPainter>
#include <QPrinter>
#include <QProcess>
#include <QStandardPaths>
#include <QTemporaryFile>
// logging category for this framework, default: log stuff >= warning
@ -176,6 +177,12 @@ bool EPSHandler::read(QImage *image)
// create GS command line
const QString gsExec = QStandardPaths::findExecutable(QStringLiteral("gs"));
if (gsExec.isEmpty()) {
qCWarning(EPSPLUGIN) << "Couldn't find gs exectuable (from GhostScript) in PATH.";
return false;
}
QStringList gsArgs;
gsArgs << QLatin1String("-sOutputFile=") + tmpFile.fileName() << QStringLiteral("-q") << QStringLiteral("-g%1x%2").arg(wantedWidth).arg(wantedHeight)
<< QStringLiteral("-dSAFER") << QStringLiteral("-dPARANOIDSAFER") << QStringLiteral("-dNOPAUSE") << QStringLiteral("-sDEVICE=ppm")
@ -192,7 +199,7 @@ bool EPSHandler::read(QImage *image)
QProcess converter;
converter.setProcessChannelMode(QProcess::ForwardedErrorChannel);
converter.start(QStringLiteral("gs"), gsArgs);
converter.start(gsExec, gsArgs);
if (!converter.waitForStarted(3000)) {
qCWarning(EPSPLUGIN) << "Reading EPS files requires gs (from GhostScript)";
return false;

View File

@ -12,6 +12,7 @@
#include "jxl_p.h"
#include <jxl/encode.h>
#include <jxl/thread_parallel_runner.h>
#include <string.h>
QJpegXLHandler::QJpegXLHandler()
: m_parseState(ParseJpegXLNotParsed)
@ -174,19 +175,30 @@ bool QJpegXLHandler::ensureDecoder()
return false;
}
if (m_basicinfo.xsize > 32768 || m_basicinfo.ysize > 32768) {
if (m_basicinfo.xsize > 65535 || m_basicinfo.ysize > 65535) {
qWarning("JXL image (%dx%d) is too large", m_basicinfo.xsize, m_basicinfo.ysize);
m_parseState = ParseJpegXLError;
return false;
} else if (sizeof(void *) <= 4) {
}
if (sizeof(void *) <= 4) {
/* On 32bit systems, there is limited address space.
* We skip imagess bigger than 8192 x 8192 pixels.
* If we don't do it, abort() in libjxl may close whole application */
if ((m_basicinfo.xsize * m_basicinfo.ysize) > 67108864) {
if (m_basicinfo.xsize > ((8192 * 8192) / m_basicinfo.ysize)) {
qWarning("JXL image (%dx%d) is too large for 32bit build of the plug-in", m_basicinfo.xsize, m_basicinfo.ysize);
m_parseState = ParseJpegXLError;
return false;
}
} else {
/* On 64bit systems
* We skip images bigger than 16384 x 16384 pixels.
* It is an artificial limit not to use extreme amount of memory */
if (m_basicinfo.xsize > ((16384 * 16384) / m_basicinfo.ysize)) {
qWarning("JXL image (%dx%d) is bigger than security limit 256 megapixels", m_basicinfo.xsize, m_basicinfo.ysize);
m_parseState = ParseJpegXLError;
return false;
}
}
m_parseState = ParseJpegXLBasicInfoParsed;
@ -411,11 +423,59 @@ bool QJpegXLHandler::write(const QImage &image)
return false;
}
if ((image.width() > 32768) || (image.height() > 32768)) {
qWarning("Image is too large");
if ((image.width() > 0) && (image.height() > 0)) {
if ((image.width() > 65535) || (image.height() > 65535)) {
qWarning("Image (%dx%d) is too large to save!", image.width(), image.height());
return false;
}
if (sizeof(void *) <= 4) {
if (image.width() > ((8192 * 8192) / image.height())) {
qWarning("Image (%dx%d) is too large save via 32bit build of JXL plug-in", image.width(), image.height());
return false;
}
} else {
if (image.width() > ((16384 * 16384) / image.height())) {
qWarning("Image (%dx%d) will not be saved because it has more than 256 megapixels", image.width(), image.height());
return false;
}
}
} else {
qWarning("Image has zero dimension!");
return false;
}
int save_depth = 8; // 8 or 16
// depth detection
switch (image.format()) {
case QImage::Format_BGR30:
case QImage::Format_A2BGR30_Premultiplied:
case QImage::Format_RGB30:
case QImage::Format_A2RGB30_Premultiplied:
case QImage::Format_Grayscale16:
case QImage::Format_RGBX64:
case QImage::Format_RGBA64:
case QImage::Format_RGBA64_Premultiplied:
save_depth = 16;
break;
case QImage::Format_RGB32:
case QImage::Format_ARGB32:
case QImage::Format_ARGB32_Premultiplied:
case QImage::Format_RGB888:
case QImage::Format_RGBX8888:
case QImage::Format_RGBA8888:
case QImage::Format_RGBA8888_Premultiplied:
save_depth = 8;
break;
default:
if (image.depth() > 32) {
save_depth = 16;
} else {
save_depth = 8;
}
break;
}
JxlEncoder *encoder = JxlEncoderCreate(nullptr);
if (!encoder) {
qWarning("Failed to create Jxl encoder");
@ -456,16 +516,16 @@ bool QJpegXLHandler::write(const QImage &image)
bool convert_color_profile;
QByteArray iccprofile;
if (image.colorSpace().isValid()) {
if (image.colorSpace().isValid() && (m_quality < 100)) {
if (image.colorSpace().primaries() != QColorSpace::Primaries::SRgb || image.colorSpace().transferFunction() != QColorSpace::TransferFunction::SRgb) {
convert_color_profile = true;
} else {
convert_color_profile = false;
}
} else { // no profile or Qt-unsupported ICC profile
} else { // lossless or no profile or Qt-unsupported ICC profile
convert_color_profile = false;
iccprofile = image.colorSpace().iccProfile();
if (iccprofile.size() > 0) {
if (iccprofile.size() > 0 || m_quality == 100) {
output_info.uses_original_profile = 1;
}
}
@ -474,19 +534,45 @@ bool QJpegXLHandler::write(const QImage &image)
QImage::Format tmpformat;
JxlEncoderStatus status;
pixel_format.data_type = JXL_TYPE_UINT16;
pixel_format.endianness = JXL_NATIVE_ENDIAN;
pixel_format.align = 0;
if (image.hasAlphaChannel()) {
tmpformat = QImage::Format_RGBA64;
pixel_format.num_channels = 4;
output_info.alpha_bits = 16;
output_info.num_extra_channels = 1;
} else {
tmpformat = QImage::Format_RGBX64;
pixel_format.num_channels = 3;
output_info.alpha_bits = 0;
output_info.intensity_target = 255.0f;
output_info.orientation = JXL_ORIENT_IDENTITY;
output_info.num_color_channels = 3;
output_info.animation.tps_numerator = 10;
output_info.animation.tps_denominator = 1;
if (save_depth > 8) { // 16bit depth
pixel_format.data_type = JXL_TYPE_UINT16;
output_info.bits_per_sample = 16;
if (image.hasAlphaChannel()) {
tmpformat = QImage::Format_RGBA64;
pixel_format.num_channels = 4;
output_info.alpha_bits = 16;
output_info.num_extra_channels = 1;
} else {
tmpformat = QImage::Format_RGBX64;
pixel_format.num_channels = 3;
output_info.alpha_bits = 0;
}
} else { // 8bit depth
pixel_format.data_type = JXL_TYPE_UINT8;
output_info.bits_per_sample = 8;
if (image.hasAlphaChannel()) {
tmpformat = QImage::Format_RGBA8888;
pixel_format.num_channels = 4;
output_info.alpha_bits = 8;
output_info.num_extra_channels = 1;
} else {
tmpformat = QImage::Format_RGB888;
pixel_format.num_channels = 3;
output_info.alpha_bits = 0;
}
}
const QImage tmpimage =
@ -494,7 +580,7 @@ bool QJpegXLHandler::write(const QImage &image)
const size_t xsize = tmpimage.width();
const size_t ysize = tmpimage.height();
const size_t buffer_size = 2 * pixel_format.num_channels * xsize * ysize;
const size_t buffer_size = (save_depth > 8) ? (2 * pixel_format.num_channels * xsize * ysize) : (pixel_format.num_channels * xsize * ysize);
if (xsize == 0 || ysize == 0 || tmpimage.isNull()) {
qWarning("Unable to allocate memory for output image");
@ -507,12 +593,6 @@ bool QJpegXLHandler::write(const QImage &image)
output_info.xsize = tmpimage.width();
output_info.ysize = tmpimage.height();
output_info.bits_per_sample = 16;
output_info.intensity_target = 255.0f;
output_info.orientation = JXL_ORIENT_IDENTITY;
output_info.num_color_channels = 3;
output_info.animation.tps_numerator = 10;
output_info.animation.tps_denominator = 1;
status = JxlEncoderSetBasicInfo(encoder, &output_info);
if (status != JXL_ENC_SUCCESS) {
@ -546,39 +626,60 @@ bool QJpegXLHandler::write(const QImage &image)
}
}
if (image.hasAlphaChannel()) {
if (image.hasAlphaChannel() || ((save_depth == 8) && (xsize % 4 == 0))) {
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, (void *)tmpimage.constBits(), buffer_size);
} else {
uint16_t *tmp_buffer = new (std::nothrow) uint16_t[3 * xsize * ysize];
if (!tmp_buffer) {
qWarning("Memory allocation error");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
if (save_depth > 8) { // 16bit depth without alpha channel
uint16_t *tmp_buffer = new (std::nothrow) uint16_t[3 * xsize * ysize];
if (!tmp_buffer) {
qWarning("Memory allocation error");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
JxlEncoderDestroy(encoder);
return false;
}
uint16_t *dest_pixels = tmp_buffer;
for (int y = 0; y < tmpimage.height(); y++) {
const uint16_t *src_pixels = reinterpret_cast<const uint16_t *>(tmpimage.constScanLine(y));
for (int x = 0; x < tmpimage.width(); x++) {
// R
*dest_pixels = *src_pixels;
dest_pixels++;
src_pixels++;
// G
*dest_pixels = *src_pixels;
dest_pixels++;
src_pixels++;
// B
*dest_pixels = *src_pixels;
dest_pixels++;
src_pixels += 2; // skipalpha
uint16_t *dest_pixels = tmp_buffer;
for (int y = 0; y < tmpimage.height(); y++) {
const uint16_t *src_pixels = reinterpret_cast<const uint16_t *>(tmpimage.constScanLine(y));
for (int x = 0; x < tmpimage.width(); x++) {
// R
*dest_pixels = *src_pixels;
dest_pixels++;
src_pixels++;
// G
*dest_pixels = *src_pixels;
dest_pixels++;
src_pixels++;
// B
*dest_pixels = *src_pixels;
dest_pixels++;
src_pixels += 2; // skipalpha
}
}
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, (void *)tmp_buffer, buffer_size);
delete[] tmp_buffer;
} else { // 8bit depth without alpha channel
uchar *tmp_buffer8 = new (std::nothrow) uchar[3 * xsize * ysize];
if (!tmp_buffer8) {
qWarning("Memory allocation error");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
uchar *dest_pixels8 = tmp_buffer8;
const size_t rowbytes = 3 * xsize;
for (int y = 0; y < tmpimage.height(); y++) {
memcpy(dest_pixels8, tmpimage.constScanLine(y), rowbytes);
dest_pixels8 += rowbytes;
}
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, (void *)tmp_buffer8, buffer_size);
delete[] tmp_buffer8;
}
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, (void *)tmp_buffer, buffer_size);
delete[] tmp_buffer;
}
if (status == JXL_ENC_ERROR) {

View File

@ -16,8 +16,9 @@
#include <QStack>
#include <QVector>
#include <QtEndian>
#include <stdlib.h>
#include <QColorSpace>
#include <stdlib.h>
#include <string.h>
#include "gimp_p.h"
@ -351,6 +352,8 @@ private:
bool initialized; //!< Is the QImage initialized?
QImage image; //!< final QImage
QHash<QString,QByteArray> parasites; //!< parasites data
XCFImage(void)
: initialized(false)
{
@ -405,6 +408,7 @@ private:
bool composeTiles(XCFImage &xcf_image);
void setGrayPalette(QImage &image);
void setPalette(XCFImage &xcf_image, QImage &image);
void setImageParasites(const XCFImage &xcf_image, QImage &image);
static void assignImageBytes(Layer &layer, uint i, uint j);
bool loadHierarchy(QDataStream &xcf_io, Layer &layer);
bool loadLevel(QDataStream &xcf_io, Layer &layer, qint32 bpp);
@ -665,6 +669,9 @@ bool XCFImageFormat::readXCF(QIODevice *device, QImage *outImage)
return false;
}
// The image was created: now I can set metadata and ICC color profile inside it.
setImageParasites(xcf_image, xcf_image.image);
*outImage = xcf_image.image;
return true;
}
@ -715,15 +722,15 @@ bool XCFImageFormat::loadImageProperties(QDataStream &xcf_io, XCFImage &xcf_imag
property.readBytes(tag, size);
quint32 flags;
char *data = nullptr;
QByteArray data;
property >> flags >> data;
if (tag && strncmp(tag, "gimp-comment", strlen("gimp-comment")) == 0) {
xcf_image.image.setText(QStringLiteral("Comment"), QString::fromUtf8(data));
}
// WARNING: you cannot add metadata to QImage here because it can be null.
// Adding a metadata to a QImage when it is null, does nothing (metas are lost).
if(tag) // store metadata for future use
xcf_image.parasites.insert(QString::fromUtf8(tag), data);
delete[] tag;
delete[] data;
}
break;
@ -1234,6 +1241,77 @@ void XCFImageFormat::setPalette(XCFImage &xcf_image, QImage &image)
image.setColorTable(xcf_image.palette);
}
/*!
* Copy the parasites info to QImage.
* \param xcf_image XCF image containing the parasites read from the data stream.
* \param image image to apply the parasites data.
* \note Some comment taken from https://gitlab.gnome.org/GNOME/gimp/-/blob/master/devel-docs/parasites.txt
*/
void XCFImageFormat::setImageParasites(const XCFImage &xcf_image, QImage &image)
{
auto&& p = xcf_image.parasites;
auto keys = p.keys();
for (auto&& key : qAsConst(keys)) {
auto value = p.value(key);
if(value.isEmpty())
continue;
// "icc-profile" (IMAGE, PERSISTENT | UNDOABLE)
// This contains an ICC profile describing the color space the
// image was produced in. TIFF images stored in PhotoShop do
// oftentimes contain embedded profiles. An experimental color
// manager exists to use this parasite, and it will be used
// for interchange between TIFF and PNG (identical profiles)
if (key == QStringLiteral("icc-profile")) {
auto cs = QColorSpace::fromIccProfile(value);
if(cs.isValid())
image.setColorSpace(cs);
continue;
}
// "gimp-comment" (IMAGE, PERSISTENT)
// Standard GIF-style image comments. This parasite should be
// human-readable text in UTF-8 encoding. A trailing \0 might
// be included and is not part of the comment. Note that 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));
continue;
}
// "gimp-image-metadata"
// Saved by GIMP 2.10.30 but it is not mentioned in the specification.
// It is an XML block with the properties set using GIMP.
if (key == QStringLiteral("gimp-image-metadata")) {
// 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));
continue;
}
#if 0 // Unable to generate it using latest GIMP version
// "gimp-metadata" (IMAGE, PERSISTENT)
// The metadata associated with the image, serialized as one XMP
// packet. This metadata includes the contents of any XMP, EXIF
// and IPTC blocks from the original image, as well as
// user-specified values such as image comment, copyright,
// license, etc.
if (key == QStringLiteral("gimp-metadata")) {
// 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 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));
continue;
}
#endif
}
}
/*!
* Copy the bytes from the tile buffer into the image tile QImage, taking into
* account all the myriad different modes.

View File

@ -5,7 +5,7 @@ include(ECMMarkAsTest)
macro(kimageformats_executable_tests)
foreach(_testname ${ARGN})
add_executable(${_testname} ${_testname}.cpp)
target_link_libraries(${_testname} Qt5::Gui)
target_link_libraries(${_testname} Qt${QT_MAJOR_VERSION}::Gui)
ecm_mark_as_test(${_testname})
endforeach(_testname)
endmacro()