[graphics/krita] /: [FEATURE] Add support for JPEG-XL
L. E. Segovia
null at kde.org
Fri May 6 16:41:22 BST 2022
Git commit 13e5d2e5b9f0eac5c8064b7767f0b62264a0797b by L. E. Segovia.
Committed on 05/05/2022 at 12:15.
Pushed by lsegovia into branch 'master'.
[FEATURE] Add support for JPEG-XL
This commit adds support for the following features:
- RGB, Gray and CMYK U8/U16/F16/F32
- Animated JXLs r/w
- Exif, XMP, and IPTC metadata r/w
- Parallel encoding & decoding (there's a step that isn't parallelized
on libjxl yet, though)
Note that:
- for F16 images, the import plugin will ask libjxl to return
a F32 image in order not to depend on OpenEXR.
- JXL is a single-layer format, the export will
use the document projection-- individual layers will be lost.
This is the first plugin to introduce full native animation import &
export in Krita.
CCMAIL: kimageshop at kde.org
M +10 -0 CMakeLists.txt
A +114 -0 cmake/modules/FindJPEGXL.cmake
M +5 -0 libs/koplugin/KisMimeDatabase.cpp
M +1 -0 packaging/android/apk/AndroidManifest.xml
M +4 -0 plugins/impex/CMakeLists.txt
A +26 -0 plugins/impex/jxl/CMakeLists.txt
A +505 -0 plugins/impex/jxl/JPEGXLExport.cpp [License: BSD]
A +28 -0 plugins/impex/jxl/JPEGXLExport.h [License: GPL(v2.0+)]
A +381 -0 plugins/impex/jxl/JPEGXLImport.cpp [License: BSD]
A +23 -0 plugins/impex/jxl/JPEGXLImport.h [License: GPL(v2.0+)]
A +219 -0 plugins/impex/jxl/kis_wdg_options_jpegxl.cpp [License: GPL(v2.0+)]
A +32 -0 plugins/impex/jxl/kis_wdg_options_jpegxl.h [License: GPL(v2.0+)]
A +786 -0 plugins/impex/jxl/kis_wdg_options_jpegxl.ui
A +125 -0 plugins/impex/jxl/krita_jxl.desktop
A +12 -0 plugins/impex/jxl/krita_jxl_export.json
A +12 -0 plugins/impex/jxl/krita_jxl_import.json
A +10 -0 plugins/impex/jxl/tests/CMakeLists.txt
A +- -- plugins/impex/jxl/tests/data/results/hdr_cosmos01000_cicp9-16-0_lossless.kra
A +- -- plugins/impex/jxl/tests/data/results/quad-lzw.jxl.png
A +- -- plugins/impex/jxl/tests/data/results/red.jxl.png
A +- -- plugins/impex/jxl/tests/data/results/sdr_cosmos01000_cicp1-13-0_lossless.jxl.png
A +- -- plugins/impex/jxl/tests/data/results/strike.jxl.png
A +27 -0 plugins/impex/jxl/tests/data/sources/DX-MON/LICENSE.txt
A +- -- plugins/impex/jxl/tests/data/sources/DX-MON/loading_16.jxl
A +12 -0 plugins/impex/jxl/tests/data/sources/netflix/LICENSE.txt
A +- -- plugins/impex/jxl/tests/data/sources/netflix/hdr_cosmos01000_cicp9-16-0_lossless.jxl
A +- -- plugins/impex/jxl/tests/data/sources/netflix/sdr_cosmos01000_cicp1-13-0_lossless.jxl
A +- -- plugins/impex/jxl/tests/data/sources/quad-lzw.jxl
A +- -- plugins/impex/jxl/tests/data/sources/red.jxl
A +- -- plugins/impex/jxl/tests/data/sources/strike.jxl
A +126 -0 plugins/impex/jxl/tests/kis_jpegxl_test.cpp [License: GPL(v2.0+)]
A +29 -0 plugins/impex/jxl/tests/kis_jpegxl_test.h [License: GPL(v2.0+)]
https://invent.kde.org/graphics/krita/commit/13e5d2e5b9f0eac5c8064b7767f0b62264a0797b
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c49bd9ce1a..aa937f7c52 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -817,6 +817,16 @@ set_package_properties(LibRaw PROPERTIES
TYPE OPTIONAL
PURPOSE "Required to build the raw import plugin")
+find_package(JPEGXL 0.7.0)
+set_package_properties(JPEGXL PROPERTIES
+ DESCRIPTION "JPEG XL is a royalty-free raster-graphics file format that supports both lossy and lossless compression and is experimentally supported by Chrome, Firefox, and Edge."
+ URL "https://github.com/libjxl/libjxl"
+ TYPE OPTIONAL
+ PURPOSE "Required by the Krita JPEG-XL filter")
+if (JPEGXL_FOUND)
+ list (APPEND ANDROID_EXTRA_LIBS ${JPEGXL_LIBRARIES})
+endif()
+
find_package(FFTW3)
set_package_properties(FFTW3 PROPERTIES
DESCRIPTION "A fast, free C FFT library"
diff --git a/cmake/modules/FindJPEGXL.cmake b/cmake/modules/FindJPEGXL.cmake
new file mode 100644
index 0000000000..b2577c80ca
--- /dev/null
+++ b/cmake/modules/FindJPEGXL.cmake
@@ -0,0 +1,114 @@
+# Copyright (C) 2021 Sony Interactive Entertainment Inc.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+# THE POSSIBILITY OF SUCH DAMAGE.
+
+#[=======================================================================[.rst:
+FindJPEGXL
+---------
+
+Find JPEGXL headers and libraries.
+
+Imported Targets
+^^^^^^^^^^^^^^^^
+
+``JPEGXL::jxl``
+ The JPEGXL library, if found.
+
+Result Variables
+^^^^^^^^^^^^^^^^
+
+This will define the following variables in your project:
+
+``JPEGXL_FOUND``
+ true if (the requested version of) JPEGXL is available.
+``JPEGXL_VERSION``
+ the version of JPEGXL.
+``JPEGXL_LIBRARIES``
+ the libraries to link against to use JPEGXL.
+``JPEGXL_INCLUDE_DIRS``
+ where to find the JPEGXL headers.
+``JPEGXL_COMPILE_OPTIONS``
+ this should be passed to target_compile_options(), if the
+ target is not used for linking
+
+#]=======================================================================]
+
+find_package(PkgConfig QUIET)
+if (PkgConfig_FOUND)
+ pkg_check_modules(PC_JPEGXL QUIET libjxl)
+ set(JPEGXL_COMPILE_OPTIONS "${PC_JPEGXL_CFLAGS_OTHER}")
+ set(JPEGXL_VERSION ${PC_JPEGXL_VERSION})
+
+ pkg_check_modules(PC_JPEGXL_THREADS QUIET libjxl_threads)
+ set(JPEGXL_THREADS_COMPILE_OPTIONS ${PC_JPEGXL_THREADS_CFLAGS_OTHER})
+endif ()
+
+find_path(JPEGXL_INCLUDE_DIR
+ NAMES jxl/decode.h
+ HINTS ${PC_JPEGXL_INCLUDEDIR} ${PC_JPEGXL_INCLUDE_DIRS} ${JPEGXL_INCLUDE_DIR}
+)
+
+find_library(JPEGXL_LIBRARY
+ NAMES ${JPEGXL_NAMES} jxl
+ HINTS ${PC_JPEGXL_LIBDIR} ${PC_JPEGXL_LIBRARY_DIRS}
+)
+
+find_library(JPEGXL_THREADS_LIBRARY
+ NAMES ${JPEGXL_NAMES} jxl_threads
+ HINTS ${PC_JPEGXL_LIBDIR} ${PC_JPEGXL_LIBRARY_DIRS} ${PC_JPEGXL_THREADS_LIBDIR} ${PC_JPEGXL_THREADS_LIBRARY_DIRS}
+)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(JPEGXL
+ FOUND_VAR JPEGXL_FOUND
+ REQUIRED_VARS JPEGXL_LIBRARY JPEGXL_INCLUDE_DIR
+ VERSION_VAR JPEGXL_VERSION
+)
+
+if (JPEGXL_LIBRARY AND NOT TARGET JPEGXL::jxl)
+ add_library(JPEGXL::jxl UNKNOWN IMPORTED GLOBAL)
+ set_target_properties(JPEGXL::jxl PROPERTIES
+ IMPORTED_LOCATION "${JPEGXL_LIBRARY}"
+ INTERFACE_COMPILE_OPTIONS "${JPEGXL_COMPILE_OPTIONS}"
+ INTERFACE_INCLUDE_DIRECTORIES "${JPEGXL_INCLUDE_DIR}"
+ )
+endif ()
+
+if (JPEGXL_THREADS_LIBRARY AND NOT TARGET JPEGXL::jxl_threads)
+ add_library(JPEGXL::jxl_threads UNKNOWN IMPORTED GLOBAL)
+ set_target_properties(JPEGXL::jxl_threads PROPERTIES
+ IMPORTED_LOCATION "${JPEGXL_THREADS_LIBRARY}"
+ INTERFACE_COMPILE_OPTIONS "${JPEGXL_THREADS_COMPILE_OPTIONS}"
+ INTERFACE_INCLUDE_DIRECTORIES "${JPEGXL_INCLUDE_DIR}"
+ )
+endif ()
+
+mark_as_advanced(JPEGXL_INCLUDE_DIR JPEGXL_LIBRARY JPEGXL_THREADS_LIBRARY)
+
+if (JPEGXL_FOUND)
+ set(JPEGXL_LIBRARIES ${JPEGXL_LIBRARY})
+ set(JPEGXL_INCLUDE_DIRS ${JPEGXL_INCLUDE_DIR})
+endif ()
+
+if (JPEGXL_THREADS_LIBRARY)
+ list(APPEND JPEGXL_LIBRARIES ${JPEGXL_THREADS_LIBRARY})
+endif ()
diff --git a/libs/koplugin/KisMimeDatabase.cpp b/libs/koplugin/KisMimeDatabase.cpp
index e0a0a976e7..9b688198d1 100644
--- a/libs/koplugin/KisMimeDatabase.cpp
+++ b/libs/koplugin/KisMimeDatabase.cpp
@@ -340,6 +340,11 @@ void KisMimeDatabase::fillMimeData()
mimeType.suffixes = QStringList() << "apng";
s_mimeDatabase << mimeType;
+ mimeType.mimeType = "image/jxl";
+ mimeType.description = i18nc("description of a file type", "JPEG-XL Image");
+ mimeType.suffixes = QStringList() << "jxl";
+ s_mimeDatabase << mimeType;
+
dbgPlugins << "Filled mimedatabase with" << s_mimeDatabase.count() << "special mimetypes";
}
}
diff --git a/packaging/android/apk/AndroidManifest.xml b/packaging/android/apk/AndroidManifest.xml
index 50347aa205..92cc39d88b 100644
--- a/packaging/android/apk/AndroidManifest.xml
+++ b/packaging/android/apk/AndroidManifest.xml
@@ -71,6 +71,7 @@
<data android:mimeType="application/x-krita-paintoppreset" />
<data android:mimeType="application/pdf" />
+ <data android:mimeType="image/jxl" />
</intent-filter>
<intent-filter>
diff --git a/plugins/impex/CMakeLists.txt b/plugins/impex/CMakeLists.txt
index 5f31eb3ae9..b00ed31e83 100644
--- a/plugins/impex/CMakeLists.txt
+++ b/plugins/impex/CMakeLists.txt
@@ -60,3 +60,7 @@ add_subdirectory(krz)
if (WebP_FOUND)
add_subdirectory(webp)
endif()
+
+if (JPEGXL_FOUND)
+ add_subdirectory(jxl)
+endif()
diff --git a/plugins/impex/jxl/CMakeLists.txt b/plugins/impex/jxl/CMakeLists.txt
new file mode 100644
index 0000000000..0a65d50f3a
--- /dev/null
+++ b/plugins/impex/jxl/CMakeLists.txt
@@ -0,0 +1,26 @@
+add_subdirectory(tests)
+
+set(kritajxlimport_SOURCES
+ JPEGXLImport.cpp
+)
+
+add_library(kritajxlimport MODULE ${kritajxlimport_SOURCES})
+
+target_link_libraries(kritajxlimport kritaui kritalibkra kritametadata ${JPEGXL_LIBRARIES})
+
+install(TARGETS kritajxlimport DESTINATION ${KRITA_PLUGIN_INSTALL_DIR})
+
+set(kritajxlexport_SOURCES
+ JPEGXLExport.cpp
+ kis_wdg_options_jpegxl.cpp
+)
+
+ki18n_wrap_ui(kritajxlexport_SOURCES kis_wdg_options_jpegxl.ui )
+
+add_library(kritajxlexport MODULE ${kritajxlexport_SOURCES})
+
+target_link_libraries(kritajxlexport kritaui kritalibkra kritaimpex ${JPEGXL_LIBRARIES})
+
+install(TARGETS kritajxlexport DESTINATION ${KRITA_PLUGIN_INSTALL_DIR})
+
+install( PROGRAMS krita_jxl.desktop DESTINATION ${XDG_APPS_INSTALL_DIR})
diff --git a/plugins/impex/jxl/JPEGXLExport.cpp b/plugins/impex/jxl/JPEGXLExport.cpp
new file mode 100644
index 0000000000..e90ec62d71
--- /dev/null
+++ b/plugins/impex/jxl/JPEGXLExport.cpp
@@ -0,0 +1,505 @@
+/*
+ * SPDX-FileCopyrightText: 2021 the JPEG XL Project Authors
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * SPDX-FileCopyrightText: 2022 L. E. Segovia <amy at amyspark.me>
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "JPEGXLExport.h"
+
+#include <jxl/encode_cxx.h>
+#include <jxl/resizable_parallel_runner_cxx.h>
+#include <kpluginfactory.h>
+
+#include <QBuffer>
+#include <cstdint>
+
+#include <KisDocument.h>
+#include <KisExportCheckRegistry.h>
+#include <KisImportExportErrorCode.h>
+#include <KoColorModelStandardIds.h>
+#include <KoColorProfile.h>
+#include <KoColorSpace.h>
+#include <KoConfig.h>
+#include <kis_assert.h>
+#include <kis_debug.h>
+#include <kis_exif_info_visitor.h>
+#include <kis_image_animation_interface.h>
+#include <kis_layer.h>
+#include <kis_meta_data_backend_registry.h>
+#include <kis_raster_keyframe_channel.h>
+#include <kis_time_span.h>
+
+#include "kis_wdg_options_jpegxl.h"
+
+K_PLUGIN_FACTORY_WITH_JSON(ExportFactory, "krita_jxl_export.json", registerPlugin<JPEGXLExport>();)
+
+template<class Traits>
+inline void swap(char *dstPtr, size_t numPixels)
+{
+ using Pixel = typename Traits::Pixel;
+
+ auto *pixelPtr = reinterpret_cast<Pixel *>(dstPtr);
+
+ for (size_t i = 0; i < numPixels; i++) {
+ std::swap(pixelPtr->blue, pixelPtr->red);
+ pixelPtr += 1;
+ }
+}
+
+inline void swapRgb(const KoColorSpace *cs, QByteArray &pixels)
+{
+ KIS_ASSERT(cs->colorModelId() == RGBAColorModelID);
+ KIS_ASSERT(cs->colorDepthId() == Integer8BitsColorDepthID || cs->colorDepthId() == Integer16BitsColorDepthID);
+
+ const auto numPixels = static_cast<size_t>(pixels.size()) / cs->pixelSize();
+
+ auto *currentPixel = pixels.data();
+
+ if (cs->colorDepthId() == Integer8BitsColorDepthID) {
+ swap<KoBgrU8Traits>(currentPixel, numPixels);
+ } else if (cs->colorDepthId() == Integer16BitsColorDepthID) {
+ swap<KoBgrU16Traits>(currentPixel, numPixels);
+ }
+}
+
+JPEGXLExport::JPEGXLExport(QObject *parent, const QVariantList &)
+ : KisImportExportFilter(parent)
+{
+}
+
+KisImportExportErrorCode JPEGXLExport::convert(KisDocument *document, QIODevice *io, KisPropertiesConfigurationSP cfg)
+{
+ KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(io->isWritable(), ImportExportCodes::NoAccessToWrite);
+
+ const auto image = document->savingImage();
+ const auto bounds = image->bounds();
+ const auto *const cs = image->colorSpace();
+
+ auto enc = JxlEncoderMake(nullptr);
+ auto runner = JxlResizableParallelRunnerMake(nullptr);
+ if (JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(enc.get(), JxlResizableParallelRunner, runner.get())) {
+ errFile << "JxlEncoderSetParallelRunner failed";
+ return ImportExportCodes::InternalError;
+ }
+
+ JxlResizableParallelRunnerSetThreads(runner.get(),
+ JxlResizableParallelRunnerSuggestThreads(static_cast<uint64_t>(bounds.width()), static_cast<uint64_t>(bounds.height())));
+
+ const auto pixelFormat = [&]() {
+ JxlPixelFormat pixelFormat{};
+ if (cs->colorDepthId() == Integer8BitsColorDepthID) {
+ pixelFormat.data_type = JXL_TYPE_UINT8;
+ } else if (cs->colorDepthId() == Integer16BitsColorDepthID) {
+ pixelFormat.data_type = JXL_TYPE_UINT16;
+#ifdef HAVE_OPENEXR
+ } else if (cs->colorDepthId() == Float16BitsColorDepthID) {
+ pixelFormat.data_type = JXL_TYPE_FLOAT16;
+#endif
+ } else if (cs->colorDepthId() == Float32BitsColorDepthID) {
+ pixelFormat.data_type = JXL_TYPE_FLOAT;
+ }
+ if (cs->colorModelId() == RGBAColorModelID) {
+ pixelFormat.num_channels = 4;
+ } else if (cs->colorModelId() == GrayAColorModelID) {
+ pixelFormat.num_channels = 2;
+ } else if (cs->colorModelId() == CMYKAColorModelID) {
+ pixelFormat.num_channels = 5;
+ }
+ return pixelFormat;
+ }();
+
+ if (JXL_ENC_SUCCESS != JxlEncoderUseBoxes(enc.get())) {
+ errFile << "JxlEncoderUseBoxes failed";
+ return ImportExportCodes::InternalError;
+ }
+
+ const auto basicInfo = [&]() {
+ auto info{std::make_unique<JxlBasicInfo>()};
+ JxlEncoderInitBasicInfo(info.get());
+ info->xsize = static_cast<uint32_t>(bounds.width());
+ info->ysize = static_cast<uint32_t>(bounds.height());
+ {
+ if (cs->colorDepthId() == Integer8BitsColorDepthID) {
+ info->bits_per_sample = 8;
+ info->exponent_bits_per_sample = 0;
+ info->alpha_bits = 8;
+ info->alpha_exponent_bits = 0;
+ } else if (cs->colorDepthId() == Integer16BitsColorDepthID) {
+ info->bits_per_sample = 16;
+ info->exponent_bits_per_sample = 0;
+ info->alpha_bits = 16;
+ info->alpha_exponent_bits = 0;
+#ifdef HAVE_OPENEXR
+ } else if (cs->colorDepthId() == Float16BitsColorDepthID) {
+ info->bits_per_sample = 16;
+ info->exponent_bits_per_sample = 5;
+ info->alpha_bits = 16;
+ info->alpha_exponent_bits = 5;
+#endif
+ } else if (cs->colorDepthId() == Float32BitsColorDepthID) {
+ info->bits_per_sample = 32;
+ info->exponent_bits_per_sample = 8;
+ info->alpha_bits = 32;
+ info->alpha_exponent_bits = 8;
+ }
+ }
+ if (cs->colorModelId() == RGBAColorModelID) {
+ info->num_color_channels = 3;
+ info->num_extra_channels = 1;
+ } else if (cs->colorModelId() == GrayAColorModelID) {
+ info->num_color_channels = 1;
+ info->num_extra_channels = 1;
+ } else if (cs->colorModelId() == CMYKAColorModelID) {
+ info->num_color_channels = 4;
+ info->num_extra_channels = 1;
+ }
+ info->uses_original_profile = JXL_TRUE;
+ if (image->animationInterface()->hasAnimation() && cfg->getBool("haveAnimation", true)) {
+ info->have_animation = JXL_TRUE;
+ info->animation.have_timecodes = JXL_FALSE;
+ info->animation.num_loops = 0;
+ info->animation.tps_numerator = 1;
+ info->animation.tps_denominator = static_cast<uint32_t>(image->animationInterface()->framerate());
+ }
+ return info;
+ }();
+
+ if (JXL_ENC_SUCCESS != JxlEncoderSetBasicInfo(enc.get(), basicInfo.get())) {
+ errFile << "JxlEncoderSetBasicInfo failed";
+ return ImportExportCodes::InternalError;
+ }
+
+ {
+ const auto profile = image->profile()->rawData();
+
+ if (JXL_ENC_SUCCESS
+ != JxlEncoderSetICCProfile(enc.get(),
+ reinterpret_cast<const uint8_t *>(profile.constData()),
+ static_cast<size_t>(profile.size()))) {
+ errFile << "JxlEncoderSetColorEncoding failed";
+ return ImportExportCodes::InternalError;
+ }
+ }
+
+ if (cfg->getBool("storeMetadata", false)) {
+ auto metaDataStore = [&]() -> std::unique_ptr<KisMetaData::Store> {
+ KisExifInfoVisitor exivInfoVisitor;
+ exivInfoVisitor.visit(image->rootLayer().data());
+ if (exivInfoVisitor.metaDataCount() == 1) {
+ return std::make_unique<KisMetaData::Store>(*exivInfoVisitor.exifInfo());
+ } else {
+ return {};
+ }
+ }();
+
+ if (metaDataStore && !metaDataStore->isEmpty()) {
+ KisMetaData::FilterRegistryModel model;
+ model.setEnabledFilters(cfg->getString("filters").split(","));
+ metaDataStore->applyFilters(model.enabledFilters());
+ }
+
+ if (metaDataStore && cfg->getBool("exif", true)) {
+ const KisMetaData::IOBackend *io = KisMetadataBackendRegistry::instance()->value("exif");
+
+ QBuffer ioDevice;
+
+ // Inject the data as any other IOBackend
+ io->saveTo(metaDataStore.get(), &ioDevice);
+
+ if (JXL_ENC_SUCCESS
+ != JxlEncoderAddBox(enc.get(),
+ "Exif",
+ reinterpret_cast<const uint8_t *>(ioDevice.data().constData()),
+ static_cast<size_t>(ioDevice.size()),
+ JXL_FALSE)) {
+ errFile << "JxlEncoderAddBox for EXIF failed";
+ return ImportExportCodes::InternalError;
+ }
+ }
+
+ if (metaDataStore && cfg->getBool("xmp", true)) {
+ const KisMetaData::IOBackend *io = KisMetadataBackendRegistry::instance()->value("xmp");
+
+ QBuffer ioDevice;
+
+ // Inject the data as any other IOBackend
+ io->saveTo(metaDataStore.get(), &ioDevice);
+
+ if (JXL_ENC_SUCCESS
+ != JxlEncoderAddBox(enc.get(),
+ "xml ",
+ reinterpret_cast<const uint8_t *>(ioDevice.data().constData()),
+ static_cast<size_t>(ioDevice.size()),
+ JXL_FALSE)) {
+ errFile << "JxlEncoderAddBox for XMP failed";
+ return ImportExportCodes::InternalError;
+ }
+ }
+
+ if (metaDataStore && cfg->getBool("iptc", true)) {
+ const KisMetaData::IOBackend *io = KisMetadataBackendRegistry::instance()->value("iptc");
+
+ QBuffer ioDevice;
+
+ // Inject the data as any other IOBackend
+ io->saveTo(metaDataStore.get(), &ioDevice);
+
+ if (JXL_ENC_SUCCESS
+ != JxlEncoderAddBox(enc.get(),
+ "xml ",
+ reinterpret_cast<const uint8_t *>(ioDevice.data().constData()),
+ static_cast<size_t>(ioDevice.size()),
+ JXL_FALSE)) {
+ errFile << "JxlEncoderAddBox for IPTC failed";
+ return ImportExportCodes::InternalError;
+ }
+ }
+ }
+
+ auto *frameSettings = JxlEncoderFrameSettingsCreate(enc.get(), nullptr);
+ {
+ const auto setFrameLossless = [&](bool v) {
+ if (JxlEncoderSetFrameLossless(frameSettings, v ? JXL_TRUE : JXL_FALSE) != JXL_ENC_SUCCESS) {
+ errFile << "JxlEncoderSetFrameLossless failed";
+ return false;
+ }
+ return true;
+ };
+
+ const auto setSetting = [&](JxlEncoderFrameSettingId id, int v) {
+ // https://github.com/libjxl/libjxl/issues/1210
+ if (id == JXL_ENC_FRAME_SETTING_RESAMPLING && v == -1)
+ return true;
+ if (JxlEncoderFrameSettingsSetOption(frameSettings, id, v) != JXL_ENC_SUCCESS) {
+ errFile << "JxlEncoderSetFrameLossless failed";
+ return false;
+ }
+ return true;
+ };
+
+ if (!setFrameLossless(cfg->getBool("lossless"))
+ || !setSetting(JXL_ENC_FRAME_SETTING_EFFORT, cfg->getInt("effort", 7))
+ || !setSetting(JXL_ENC_FRAME_SETTING_DECODING_SPEED, cfg->getInt("decodingSpeed", 0))
+ || !setSetting(JXL_ENC_FRAME_SETTING_RESAMPLING, cfg->getInt("resampling", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING, cfg->getInt("extraChannelResampling", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_PHOTON_NOISE, cfg->getInt("photonNoise", 0))
+ || !setSetting(JXL_ENC_FRAME_SETTING_DOTS, cfg->getInt("dots", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_PATCHES, cfg->getInt("patches", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_EPF, cfg->getInt("epf", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_GABORISH, cfg->getInt("gaborish", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_MODULAR, cfg->getInt("modular", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_KEEP_INVISIBLE, cfg->getInt("keepInvisible", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_GROUP_ORDER, cfg->getInt("groupOrder", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_RESPONSIVE, cfg->getInt("responsive", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC, cfg->getInt("progressiveAC", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC, cfg->getInt("qProgressiveAC", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC, cfg->getInt("progressiveDC", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT,
+ cfg->getInt("channelColorsGlobalPercent", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT,
+ cfg->getInt("channelColorsGroupPercent", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_PALETTE_COLORS, cfg->getInt("paletteColors", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_LOSSY_PALETTE, cfg->getInt("lossyPalette", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE, cfg->getInt("modularGroupSize", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR, cfg->getInt("modularPredictor", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT,
+ cfg->getInt("modularMATreeLearningPercent", -1))
+ || !setSetting(JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL, cfg->getInt("jpegReconCFL", -1))) {
+ return ImportExportCodes::InternalError;
+ }
+ }
+
+ {
+ if (image->animationInterface()->hasAnimation() && cfg->getBool("haveAnimation", true)) {
+ const auto *frames = image->projection()->keyframeChannel();
+ const auto times = [&]() {
+ auto t = frames->allKeyframeTimes().toList();
+ std::sort(t.begin(), t.end());
+ return t;
+ }();
+
+ auto frameHeader = []() {
+ auto header = std::make_unique<JxlFrameHeader>();
+ JxlEncoderInitFrameHeader(header.get());
+ return header;
+ }();
+
+ for (const auto i : times) {
+ frameHeader->duration = [&]() {
+ const auto nextKeyframe = frames->nextKeyframeTime(i);
+ if (nextKeyframe == -1) {
+ return static_cast<uint32_t>(image->animationInterface()->fullClipRange().end() - i);
+ } else {
+ return static_cast<uint32_t>(frames->nextKeyframeTime(i) - i);
+ }
+ }();
+
+ if (JxlEncoderSetFrameHeader(frameSettings, frameHeader.get()) != JXL_ENC_SUCCESS) {
+ errFile << "JxlEncoderSetFrameHeader failed";
+ return ImportExportCodes::InternalError;
+ }
+
+ QByteArray pixels{[&]() {
+ const auto frameData = frames->keyframeAt<KisRasterKeyframe>(i);
+ KisPaintDeviceSP dev =
+ new KisPaintDevice(*image->projection(), KritaUtils::DeviceCopyMode::CopySnapshot);
+ frameData->writeFrameToDevice(dev);
+ QByteArray p(static_cast<int>(cs->pixelSize()) * bounds.width() * bounds.height(), 0x0);
+ dev->readBytes(reinterpret_cast<quint8 *>(p.data()), image->bounds());
+ return p;
+ }()};
+
+ // BGRA -> RGBA
+ if (cs->colorModelId() == RGBAColorModelID
+ && (cs->colorDepthId() == Integer8BitsColorDepthID
+ || cs->colorDepthId() == Integer16BitsColorDepthID)) {
+ swapRgb(cs, pixels);
+ }
+
+ if (JxlEncoderAddImageFrame(frameSettings,
+ &pixelFormat,
+ pixels.data(),
+ static_cast<size_t>(pixels.size()))
+ != JXL_ENC_SUCCESS) {
+ errFile << "JxlEncoderAddImageFrame @" << i << "failed";
+ return ImportExportCodes::InternalError;
+ }
+ }
+ } else {
+ // Insert the projection itself only
+ QByteArray pixels{[&]() {
+ const auto bounds = image->bounds();
+ QByteArray p(static_cast<int>(cs->pixelSize()) * bounds.width() * bounds.height(), 0x0);
+ image->projection()->readBytes(reinterpret_cast<quint8 *>(pixels.data()), image->bounds());
+ return p;
+ }()};
+
+ // BGRA -> RGBA
+ if (cs->colorModelId() == RGBAColorModelID
+ && (cs->colorDepthId() == Integer8BitsColorDepthID
+ || cs->colorDepthId() == Integer16BitsColorDepthID)) {
+ swapRgb(cs, pixels);
+ }
+
+ if (JxlEncoderAddImageFrame(frameSettings, &pixelFormat, pixels.data(), static_cast<size_t>(pixels.size()))
+ != JXL_ENC_SUCCESS) {
+ errFile << "JxlEncoderAddImageFrame failed";
+ return ImportExportCodes::InternalError;
+ }
+ }
+ JxlEncoderCloseInput(enc.get());
+
+ QByteArray compressed(16384, 0x0);
+ auto *nextOut = reinterpret_cast<uint8_t *>(compressed.data());
+ auto availOut = static_cast<size_t>(compressed.size());
+ auto result = JXL_ENC_NEED_MORE_OUTPUT;
+ while (result == JXL_ENC_NEED_MORE_OUTPUT) {
+ result = JxlEncoderProcessOutput(enc.get(), &nextOut, &availOut);
+ if (result != JXL_ENC_ERROR) {
+ io->write(compressed.data(), compressed.size() - static_cast<int>(availOut));
+ }
+ if (result == JXL_ENC_NEED_MORE_OUTPUT) {
+ compressed.resize(compressed.size() * 2);
+ nextOut = reinterpret_cast<uint8_t *>(compressed.data());
+ availOut = static_cast<size_t>(compressed.size());
+ }
+ }
+ if (JXL_ENC_SUCCESS != result) {
+ errFile << "JxlEncoderProcessOutput failed";
+ return ImportExportCodes::ErrorWhileWriting;
+ }
+ }
+
+ return ImportExportCodes::OK;
+}
+
+void JPEGXLExport::initializeCapabilities()
+{
+ // This checks before saving for what the file format supports: anything that is supported needs to be mentioned
+ // here
+
+ QList<QPair<KoID, KoID>> supportedColorModels;
+ addCapability(KisExportCheckRegistry::instance()->get("sRGBProfileCheck")->create(KisExportCheckBase::SUPPORTED));
+ addCapability(KisExportCheckRegistry::instance()->get("ExifCheck")->create(KisExportCheckBase::SUPPORTED));
+ addCapability(KisExportCheckRegistry::instance()->get("MultiLayerCheck")->create(KisExportCheckBase::PARTIALLY));
+ addCapability(KisExportCheckRegistry::instance()->get("TiffExifCheck")->create(KisExportCheckBase::PARTIALLY));
+ supportedColorModels << QPair<KoID, KoID>() << QPair<KoID, KoID>(RGBAColorModelID, Integer8BitsColorDepthID)
+ << QPair<KoID, KoID>(GrayAColorModelID, Integer8BitsColorDepthID)
+ << QPair<KoID, KoID>(CMYKAColorModelID, Integer8BitsColorDepthID)
+ << QPair<KoID, KoID>(RGBAColorModelID, Integer16BitsColorDepthID)
+ << QPair<KoID, KoID>(GrayAColorModelID, Integer16BitsColorDepthID)
+ << QPair<KoID, KoID>(CMYKAColorModelID, Integer16BitsColorDepthID)
+#ifdef HAVE_OPENEXR
+ << QPair<KoID, KoID>(RGBAColorModelID, Float16BitsColorDepthID)
+ << QPair<KoID, KoID>(GrayAColorModelID, Float16BitsColorDepthID)
+ << QPair<KoID, KoID>(CMYKAColorModelID, Float16BitsColorDepthID)
+#endif
+ << QPair<KoID, KoID>(RGBAColorModelID, Float32BitsColorDepthID)
+ << QPair<KoID, KoID>(GrayAColorModelID, Float32BitsColorDepthID)
+ << QPair<KoID, KoID>(CMYKAColorModelID, Float32BitsColorDepthID);
+ addSupportedColorModels(supportedColorModels, "JPEG-XL");
+}
+
+KisConfigWidget *
+JPEGXLExport::createConfigurationWidget(QWidget *parent, const QByteArray & /*from*/, const QByteArray & /*to*/) const
+{
+ return new KisWdgOptionsJPEGXL(parent);
+}
+
+KisPropertiesConfigurationSP JPEGXLExport::defaultConfiguration(const QByteArray &, const QByteArray &) const
+{
+ KisPropertiesConfigurationSP cfg = new KisPropertiesConfiguration();
+
+ // WARNING: libjxl only allows setting encoding properties,
+ // so I hardcoded values from https://libjxl.readthedocs.io/en/latest/api_encoder.html
+ // https://readthedocs.org/projects/libjxl/builds/16271112/
+
+ // Options for the following were not added because they rely
+ // on the image's specific color space, or can introduce more
+ // trouble than help:
+ // JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_X
+ // JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_Y
+ // JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE
+ // JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS
+ // These are directly incompatible with the export logic:
+ // JXL_ENC_FRAME_SETTING_ALREADY_DOWNSAMPLED
+ // JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM
+
+ cfg->setProperty("haveAnimation", true);
+ cfg->setProperty("lossless", true);
+ cfg->setProperty("effort", 7);
+ cfg->setProperty("decodingSpeed", 0);
+ cfg->setProperty("resampling", -1);
+ cfg->setProperty("extraChannelResampling", -1);
+ cfg->setProperty("photonNoise", 0);
+ cfg->setProperty("dots", -1);
+ cfg->setProperty("patches", -1);
+ cfg->setProperty("epf", -1);
+ cfg->setProperty("gaborish", -1);
+ cfg->setProperty("modular", -1);
+ cfg->setProperty("keepInvisible", -1);
+ cfg->setProperty("groupOrder", -1);
+ cfg->setProperty("responsive", -1);
+ cfg->setProperty("progressiveAC", -1);
+ cfg->setProperty("qProgressiveAC", -1);
+ cfg->setProperty("progressiveDC", -1);
+ cfg->setProperty("channelColorsGlobalPercent", -1);
+ cfg->setProperty("channelColorsGroupPercent", -1);
+ cfg->setProperty("paletteColors", -1);
+ cfg->setProperty("lossyPalette", -1);
+ cfg->setProperty("modularGroupSize", -1);
+ cfg->setProperty("modularPredictor", -1);
+ cfg->setProperty("modularMATreeLearningPercent", -1);
+ cfg->setProperty("jpegReconCFL", -1);
+
+ cfg->setProperty("exif", true);
+ cfg->setProperty("xmp", true);
+ cfg->setProperty("iptc", true);
+ cfg->setProperty("storeMetadata", false);
+ cfg->setProperty("filters", "");
+ return cfg;
+}
+
+#include <JPEGXLExport.moc>
diff --git a/plugins/impex/jxl/JPEGXLExport.h b/plugins/impex/jxl/JPEGXLExport.h
new file mode 100644
index 0000000000..e71b18ac10
--- /dev/null
+++ b/plugins/impex/jxl/JPEGXLExport.h
@@ -0,0 +1,28 @@
+/*
+ * SPDX-FileCopyrightText: 2022 L. E. Segovia <amy at amyspark.me>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef JPEG_XL_EXPORT_H_
+#define JPEG_XL_EXPORT_H_
+
+#include <KisImportExportFilter.h>
+
+class JPEGXLExport : public KisImportExportFilter
+{
+ Q_OBJECT
+public:
+ JPEGXLExport(QObject *parent, const QVariantList &);
+ ~JPEGXLExport() override = default;
+
+ KisImportExportErrorCode
+ convert(KisDocument *document, QIODevice *io, KisPropertiesConfigurationSP cfg = nullptr) override;
+ KisPropertiesConfigurationSP defaultConfiguration(const QByteArray &from = "",
+ const QByteArray &to = "") const override;
+ KisConfigWidget *
+ createConfigurationWidget(QWidget *parent, const QByteArray &from = "", const QByteArray &to = "") const override;
+ void initializeCapabilities() override;
+};
+
+#endif
diff --git a/plugins/impex/jxl/JPEGXLImport.cpp b/plugins/impex/jxl/JPEGXLImport.cpp
new file mode 100644
index 0000000000..2af4229f55
--- /dev/null
+++ b/plugins/impex/jxl/JPEGXLImport.cpp
@@ -0,0 +1,381 @@
+/*
+ * SPDX-FileCopyrightText: 2021 the JPEG XL Project Authors
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * SPDX-FileCopyrightText: 2022 L. E. Segovia <amy at amyspark.me>
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "JPEGXLImport.h"
+
+#include <jxl/decode_cxx.h>
+#include <jxl/resizable_parallel_runner_cxx.h>
+#include <kpluginfactory.h>
+
+#include <QBuffer>
+#include <cstring>
+
+#include <KisDocument.h>
+#include <KisImportExportErrorCode.h>
+#include <KoColorModelStandardIds.h>
+#include <KoColorSpaceRegistry.h>
+#include <kis_assert.h>
+#include <kis_debug.h>
+#include <kis_group_layer.h>
+#include <kis_image_animation_interface.h>
+#include <kis_iterator_ng.h>
+#include <kis_meta_data_backend_registry.h>
+#include <kis_paint_layer.h>
+#include <kis_raster_keyframe_channel.h>
+
+K_PLUGIN_FACTORY_WITH_JSON(ImportFactory, "krita_jxl_import.json", registerPlugin<JPEGXLImport>();)
+
+constexpr static std::array<char, 5> exifTag = {'E', 'x', 'i', 'f', 0x0};
+constexpr static std::array<char, 5> xmpTag = {'x', 'm', 'l', ' ', 0x0};
+
+class Q_DECL_HIDDEN JPEGXLImportData
+{
+public:
+ JPEGXLImport *m{nullptr};
+ JxlBasicInfo m_info{};
+ JxlPixelFormat m_pixelFormat{};
+ JxlFrameHeader m_header{};
+ KisPaintDeviceSP m_currentFrame{nullptr};
+ int m_nextFrameTime{0};
+ KoID m_colorID;
+ KoID m_depthID;
+ bool m_forcedConversion;
+};
+
+template<class Traits>
+void imageOutRgbCallback(void *that, size_t x, size_t y, size_t numPixels, const void *pixels)
+{
+ auto *data = static_cast<JPEGXLImportData *>(that);
+ KIS_ASSERT(data);
+
+ using Pixel = typename Traits::Pixel;
+ using channels_type = typename Traits::channels_type;
+
+ auto it = data->m_currentFrame->createHLineIteratorNG(static_cast<int>(x),
+ static_cast<int>(y),
+ static_cast<int>(data->m_info.xsize));
+ const auto *src = static_cast<const channels_type *>(pixels);
+
+ for (size_t i = 0; i < numPixels; i++) {
+ auto *dst = reinterpret_cast<Pixel *>(it->rawData());
+
+ std::memcpy(dst, src, (data->m_pixelFormat.num_channels) * sizeof(channels_type));
+
+ std::swap(dst->blue, dst->red);
+
+ src += data->m_pixelFormat.num_channels;
+
+ it->nextPixel();
+ }
+}
+
+template<typename channels_type>
+void imageOutSizedCallback(void *that, size_t x, size_t y, size_t numPixels, const void *pixels)
+{
+ auto *data = static_cast<JPEGXLImportData *>(that);
+ KIS_ASSERT(data);
+
+ auto it = data->m_currentFrame->createHLineIteratorNG(static_cast<int>(x),
+ static_cast<int>(y),
+ static_cast<int>(data->m_info.xsize));
+ const auto *src = static_cast<const channels_type *>(pixels);
+
+ for (size_t i = 0; i < numPixels; i++) {
+ auto *dst = reinterpret_cast<channels_type *>(it->rawData());
+
+ std::memcpy(dst, src, (data->m_pixelFormat.num_channels) * sizeof(channels_type));
+
+ src += data->m_pixelFormat.num_channels;
+
+ it->nextPixel();
+ }
+}
+
+JPEGXLImport::JPEGXLImport(QObject *parent, const QVariantList &)
+ : KisImportExportFilter(parent)
+{
+}
+
+KisImportExportErrorCode
+JPEGXLImport::convert(KisDocument *document, QIODevice *io, KisPropertiesConfigurationSP /*configuration*/)
+{
+ if (!io->isReadable()) {
+ errFile << "Cannot read image contents";
+ return ImportExportCodes::NoAccessToRead;
+ }
+
+ JPEGXLImportData d{};
+
+ // Multi-threaded parallel runner.
+ auto runner = JxlResizableParallelRunnerMake(nullptr);
+ auto dec = JxlDecoderMake(nullptr);
+
+ KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(runner && dec, ImportExportCodes::InternalError);
+
+ if (JXL_DEC_SUCCESS
+ != JxlDecoderSubscribeEvents(dec.get(),
+ JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE | JXL_DEC_BOX
+ | JXL_DEC_FRAME)) {
+ errFile << "JxlDecoderSubscribeEvents failed";
+ return ImportExportCodes::InternalError;
+ }
+
+ if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(), JxlResizableParallelRunner, runner.get())) {
+ errFile << "JxlDecoderSetParallelRunner failed";
+ return ImportExportCodes::InternalError;
+ }
+
+ const auto data = io->readAll();
+
+ const auto validation =
+ JxlSignatureCheck(reinterpret_cast<const uint8_t *>(data.constData()), static_cast<size_t>(data.size()));
+
+ switch (validation) {
+ case JXL_SIG_NOT_ENOUGH_BYTES:
+ errFile << "Failed magic byte validation, not enough data";
+ return ImportExportCodes::FileFormatIncorrect;
+ case JXL_SIG_INVALID:
+ errFile << "Failed magic byte validation, incorrect format";
+ return ImportExportCodes::FileFormatIncorrect;
+ default:
+ break;
+ }
+
+ if (JXL_DEC_SUCCESS
+ != JxlDecoderSetInput(dec.get(),
+ reinterpret_cast<const uint8_t *>(data.constData()),
+ static_cast<size_t>(data.size()))) {
+ errFile << "JxlDecoderSetInput failed";
+ return ImportExportCodes::InternalError;
+ };
+ JxlDecoderCloseInput(dec.get());
+ if (JXL_DEC_SUCCESS != JxlDecoderSetDecompressBoxes(dec.get(), JXL_TRUE)) {
+ errFile << "JxlDecoderSetDecompressBoxes failed";
+ return ImportExportCodes::InternalError;
+ };
+
+ KisImageSP image{nullptr};
+ KisLayerSP layer{nullptr};
+ std::array<char, 5> boxType{};
+ QByteArray box(16384, 0x0);
+ auto boxSize = box.size();
+
+ for (;;) {
+ JxlDecoderStatus status = JxlDecoderProcessInput(dec.get());
+
+ if (status == JXL_DEC_ERROR) {
+ errFile << "Decoder error";
+ return ImportExportCodes::InternalError;
+ } else if (status == JXL_DEC_NEED_MORE_INPUT) {
+ errFile << "Error, already provided all input";
+ return ImportExportCodes::InternalError;
+ } else if (status == JXL_DEC_BASIC_INFO) {
+ if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec.get(), &d.m_info)) {
+ errFile << "JxlDecoderGetBasicInfo failed";
+ return ImportExportCodes::ErrorWhileReading;
+ }
+ dbgFile << "Info";
+ dbgFile << "Size:" << d.m_info.xsize << "x" << d.m_info.ysize;
+ dbgFile << "Depth:" << d.m_info.bits_per_sample << d.m_info.exponent_bits_per_sample;
+ dbgFile << "Number of channels:" << d.m_info.num_color_channels;
+ dbgFile << "Has alpha" << d.m_info.num_extra_channels << d.m_info.alpha_bits
+ << d.m_info.alpha_exponent_bits;
+ dbgFile << "Has animation:" << d.m_info.have_animation << "loops:" << d.m_info.animation.num_loops
+ << "tick:" << d.m_info.animation.tps_numerator << d.m_info.animation.tps_denominator;
+ JxlResizableParallelRunnerSetThreads(
+ runner.get(),
+ JxlResizableParallelRunnerSuggestThreads(d.m_info.xsize, d.m_info.ysize));
+
+ // XXX: libjxl does not yet provide a way to retrieve the real bit depth.
+ // See
+ // https://github.com/libjxl/libjxl/blame/35ca355660a89819e52013fa201284cb50768f80/lib/jxl/decode.cc#L568
+ if (d.m_info.exponent_bits_per_sample != 0) {
+ // Let's not rely on float16
+ d.m_pixelFormat.data_type = JXL_TYPE_FLOAT;
+ d.m_depthID = Float32BitsColorDepthID;
+ } else {
+ if (d.m_info.bits_per_sample == 8) {
+ d.m_pixelFormat.data_type = JXL_TYPE_UINT8;
+ d.m_depthID = Integer8BitsColorDepthID;
+ } else {
+ d.m_pixelFormat.data_type = JXL_TYPE_UINT16;
+ d.m_depthID = Integer16BitsColorDepthID;
+ }
+ }
+
+ if (d.m_info.num_color_channels == 1) {
+ // Grayscale
+ d.m_pixelFormat.num_channels = 2;
+ d.m_colorID = GrayAColorModelID;
+ } else if (d.m_info.num_color_channels == 3) {
+ // RGBA
+ d.m_pixelFormat.num_channels = 4;
+ d.m_colorID = RGBAColorModelID;
+ } else if (d.m_info.num_color_channels == 4) {
+ // CMYKA
+ d.m_pixelFormat.num_channels = 5;
+ d.m_colorID = CMYKAColorModelID;
+ } else {
+ warnFile << "Forcing a RGBA conversion, unknown color space";
+ d.m_pixelFormat.num_channels = 4;
+ d.m_colorID = RGBAColorModelID;
+ d.m_forcedConversion = true;
+ }
+ } else if (status == JXL_DEC_COLOR_ENCODING) {
+ // Get the ICC color profile of the pixel data
+ size_t icc_size{};
+ QByteArray icc_profile;
+ const KoColorSpace *cs{nullptr};
+ const auto tgt = d.m_forcedConversion ? JXL_COLOR_PROFILE_TARGET_DATA : JXL_COLOR_PROFILE_TARGET_ORIGINAL;
+ if (JXL_DEC_SUCCESS == JxlDecoderGetICCProfileSize(dec.get(), &d.m_pixelFormat, tgt, &icc_size)) {
+ dbgFile << "JxlDecoderGetICCProfileSize succeeded, ICC profile available";
+ icc_profile.resize(static_cast<int>(icc_size));
+ if (JXL_DEC_SUCCESS
+ != JxlDecoderGetColorAsICCProfile(dec.get(),
+ &d.m_pixelFormat,
+ tgt,
+ reinterpret_cast<uint8_t *>(icc_profile.data()),
+ static_cast<size_t>(icc_profile.size()))) {
+ document->setErrorMessage(i18nd("JPEG-XL errors", "Unable to read the image profile."));
+ return ImportExportCodes::ErrorWhileReading;
+ }
+
+ // With the profile in hand, now we can create the image.
+ const auto *profile = KoColorSpaceRegistry::instance()->createColorProfile(d.m_colorID.id(),
+ d.m_depthID.id(),
+ icc_profile);
+ cs = KoColorSpaceRegistry::instance()->colorSpace(d.m_colorID.id(), d.m_depthID.id(), profile);
+ } else {
+ // XXX: Need to either create the LCMS profile manually
+ // here or inject it into createColorProfile
+ document->setErrorMessage(i18nd("JPEG-XL errors", "JPEG-XL encoded profile not implemented"));
+ return ImportExportCodes::FormatFeaturesUnsupported;
+ }
+
+ image = new KisImage(document->createUndoStore(),
+ static_cast<int>(d.m_info.xsize),
+ static_cast<int>(d.m_info.ysize),
+ cs,
+ "JPEG-XL image");
+
+ layer = new KisPaintLayer(image, image->nextLayerName(), UCHAR_MAX);
+ if (d.m_info.have_animation) {
+ dbgFile << "Animation detected, framerate:" << d.m_info.animation.tps_numerator
+ << d.m_info.animation.tps_denominator;
+ const int framerate = static_cast<int>(static_cast<double>(d.m_info.animation.tps_denominator)
+ / static_cast<double>(d.m_info.animation.tps_numerator));
+ layer->enableAnimation();
+ image->animationInterface()->setFullClipRangeStartTime(0);
+ image->animationInterface()->setFramerate(framerate);
+ }
+ } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
+ d.m = this;
+ d.m_currentFrame = new KisPaintDevice(image->colorSpace());
+ const auto callback = [&]() {
+ if (d.m_colorID == RGBAColorModelID && d.m_depthID == Integer8BitsColorDepthID) {
+ return &::imageOutRgbCallback<KoBgrU8Traits>;
+ } else if (d.m_colorID == RGBAColorModelID && d.m_depthID == Integer16BitsColorDepthID) {
+ return &::imageOutRgbCallback<KoBgrU16Traits>;
+ } else if (d.m_pixelFormat.data_type == JXL_TYPE_UINT8) {
+ return &::imageOutSizedCallback<uint8_t>;
+ } else if (d.m_pixelFormat.data_type == JXL_TYPE_UINT16) {
+ return &::imageOutSizedCallback<uint16_t>;
+ } else {
+ return &::imageOutSizedCallback<float>;
+ }
+ }();
+
+ if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutCallback(dec.get(), &d.m_pixelFormat, callback, &d)) {
+ errFile << "JxlDecoderSetImageOutBuffer failed";
+ return ImportExportCodes::InternalError;
+ }
+ } else if (status == JXL_DEC_FRAME) {
+ if (d.m_info.have_animation) {
+ if (JXL_DEC_SUCCESS != JxlDecoderGetFrameHeader(dec.get(), &d.m_header)) {
+ document->setErrorMessage(i18nd("JPEG-XL errors", "JPEG-XL image is animated, but cannot retrieve animation frame header."));
+ return ImportExportCodes::ErrorWhileReading;
+ }
+ }
+ } else if (status == JXL_DEC_FULL_IMAGE) {
+ if (d.m_info.have_animation) {
+ dbgFile << "Importing frame @" << d.m_nextFrameTime;
+ auto *channel = layer->getKeyframeChannel(KisKeyframeChannel::Raster.id(), true);
+ auto *frame = dynamic_cast<KisRasterKeyframeChannel *>(channel);
+ frame->importFrame(d.m_nextFrameTime, d.m_currentFrame, nullptr);
+ d.m_nextFrameTime += static_cast<int>(d.m_header.duration);
+ image->animationInterface()->setFullClipRangeEndTime(d.m_nextFrameTime);
+ } else {
+ layer->paintDevice()->makeCloneFrom(d.m_currentFrame, image->bounds());
+ }
+ } else if (status == JXL_DEC_SUCCESS || status == JXL_DEC_BOX) {
+ if (!boxType.empty()) {
+ // Release buffer and get its final size.
+ const auto availOut = JxlDecoderReleaseBoxBuffer(dec.get());
+ box.resize(box.size() - static_cast<int>(availOut));
+
+ QBuffer buf(&box);
+ if (boxType == exifTag) {
+ dbgFile << "Loading EXIF data. Size: " << box.size();
+
+ const auto *backend = KisMetadataBackendRegistry::instance()->value("exif");
+
+ backend->loadFrom(layer->metaData(), &buf);
+ } else if (boxType == xmpTag) {
+ dbgFile << "Loading XMP or IPTC data. Size: " << box.size();
+
+ const auto *xmpBackend = KisMetadataBackendRegistry::instance()->value("xmp");
+ const auto *iptcBackend = KisMetadataBackendRegistry::instance()->value("iptc");
+
+ if (!xmpBackend->loadFrom(layer->metaData(), &buf)) {
+ iptcBackend->loadFrom(layer->metaData(), &buf);
+ }
+ }
+ }
+ if (status == JXL_DEC_SUCCESS) {
+ // All decoding successfully finished.
+ // It's not required to call JxlDecoderReleaseInput(dec.get()) here since
+ // the decoder will be destroyed.
+ image->addNode(layer, image->rootLayer().data());
+ document->setCurrentImage(image);
+ return ImportExportCodes::OK;
+ } else {
+ if (JxlDecoderGetBoxType(dec.get(), boxType.data(), JXL_TRUE) != JXL_DEC_SUCCESS) {
+ errFile << "JxlDecoderGetBoxType failed";
+ return ImportExportCodes::ErrorWhileReading;
+ }
+ if (boxType == exifTag || boxType == xmpTag) {
+ if (JxlDecoderSetBoxBuffer(dec.get(),
+ reinterpret_cast<uint8_t *>(boxType.data()),
+ static_cast<size_t>(box.size()))
+ != JXL_DEC_SUCCESS) {
+ errFile << "JxlDecoderSetBoxBuffer failed";
+ return ImportExportCodes::InternalError;
+ }
+ } else {
+ dbgFile << "Skipping box" << boxType.data();
+ }
+ }
+ } else if (status == JXL_DEC_BOX_NEED_MORE_OUTPUT) {
+ box.resize(boxSize * 2);
+ if (JxlDecoderSetBoxBuffer(dec.get(),
+ reinterpret_cast<uint8_t *>(boxType.data() + boxSize),
+ static_cast<size_t>(box.size() - boxSize))
+ != JXL_DEC_SUCCESS) {
+ errFile << "JxlDecoderGetBoxType failed";
+ return ImportExportCodes::ErrorWhileReading;
+ }
+ } else {
+ errFile << "Unknown decoder status" << status;
+ return ImportExportCodes::InternalError;
+ }
+ }
+
+ return ImportExportCodes::OK;
+}
+
+#include <JPEGXLImport.moc>
diff --git a/plugins/impex/jxl/JPEGXLImport.h b/plugins/impex/jxl/JPEGXLImport.h
new file mode 100644
index 0000000000..a1e071b0b9
--- /dev/null
+++ b/plugins/impex/jxl/JPEGXLImport.h
@@ -0,0 +1,23 @@
+/*
+ * SPDX-FileCopyrightText: 2022 L. E. Segovia <amy at amyspark.me>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef JPEG_XL_IMPORT_H_
+#define JPEG_XL_IMPORT_H_
+
+#include <KisImportExportFilter.h>
+
+class JPEGXLImport : public KisImportExportFilter
+{
+ Q_OBJECT
+public:
+ JPEGXLImport(QObject *parent, const QVariantList &);
+ ~JPEGXLImport() override = default;
+ bool supportsIO() const override { return true; }
+
+ KisImportExportErrorCode convert(KisDocument *document, QIODevice *io, KisPropertiesConfigurationSP configuration = nullptr) override;
+};
+
+#endif
diff --git a/plugins/impex/jxl/kis_wdg_options_jpegxl.cpp b/plugins/impex/jxl/kis_wdg_options_jpegxl.cpp
new file mode 100644
index 0000000000..1d52f7e6cf
--- /dev/null
+++ b/plugins/impex/jxl/kis_wdg_options_jpegxl.cpp
@@ -0,0 +1,219 @@
+/*
+ * SPDX-FileCopyrightText: 2022 L. E. Segovia <amy at amyspark.me>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "kis_wdg_options_jpegxl.h"
+
+KisWdgOptionsJPEGXL::KisWdgOptionsJPEGXL(QWidget *parent)
+ : KisConfigWidget(parent)
+{
+ setupUi(this);
+ // HACK ALERT!
+ // QScrollArea contents are opaque at multiple levels
+ // The contents themselves AND the viewport widget
+ {
+ scrollAreaWidgetContents->setAutoFillBackground(false);
+ scrollAreaWidgetContents->parentWidget()->setAutoFillBackground(false);
+ }
+
+ {
+ resampling->addItem(i18nd("JPEG-XL encoder options", "Default (only for low quality)"), -1);
+ resampling->addItem(i18nd("JPEG-XL encoder options", "No downsampling"), 1);
+ resampling->addItem(i18nd("JPEG-XL encoder options", "2x2 downsampling"), 2);
+ resampling->addItem(i18nd("JPEG-XL encoder options", "4x4 downsampling"), 4);
+ resampling->addItem(i18nd("JPEG-XL encoder options", "8x8 downsampling"), 8);
+
+ extraChannelResampling->addItem(i18nd("JPEG-XL encoder options", "Default (only for low quality)"), -1);
+ extraChannelResampling->addItem(i18nd("JPEG-XL encoder options", "No downsampling"), 1);
+ extraChannelResampling->addItem(i18nd("JPEG-XL encoder options", "2x2 downsampling"), 2);
+ extraChannelResampling->addItem(i18nd("JPEG-XL encoder options", "4x4 downsampling"), 4);
+ extraChannelResampling->addItem(i18nd("JPEG-XL encoder options", "8x8 downsampling"), 8);
+ }
+
+ {
+ dots->addItem(i18nd("JPEG-XL encoder options", "Default (encoder chooses)"), -1);
+ dots->addItem(i18nd("JPEG-XL encoder options", "Disabled"), 0);
+ dots->addItem(i18nd("JPEG-XL encoder options", "Enabled"), 1);
+ }
+
+ {
+ patches->addItem(i18nd("JPEG-XL encoder options", "Default (encoder chooses)"), -1);
+ patches->addItem(i18nd("JPEG-XL encoder options", "Disabled"), 0);
+ patches->addItem(i18nd("JPEG-XL encoder options", "Enabled"), 1);
+ }
+
+ {
+ gaborish->addItem(i18nd("JPEG-XL encoder options", "Default (encoder chooses)"), -1);
+ gaborish->addItem(i18nd("JPEG-XL encoder options", "Disabled"), 0);
+ gaborish->addItem(i18nd("JPEG-XL encoder options", "Enabled"), 1);
+ }
+
+ {
+ modular->addItem(i18nd("JPEG-XL encoder options", "Default (encoder chooses)"), -1);
+ modular->addItem(i18nd("JPEG-XL encoder options", "VarDCT mode (e.g. for photographic images)"), 0);
+ modular->addItem(i18nd("JPEG-XL encoder options", "Modular mode (e.g. for lossless images)"), -1);
+ }
+
+ {
+ keepInvisible->addItem(i18nd("JPEG-XL encoder options", "Default (encoder chooses)"), -1);
+ keepInvisible->addItem(i18nd("JPEG-XL encoder options", "Disabled"), 0);
+ keepInvisible->addItem(i18nd("JPEG-XL encoder options", "Enabled"), 1);
+ }
+
+ {
+ groupOrder->addItem(i18nd("JPEG-XL encoder options", "Default (encoder chooses)"), -1);
+ groupOrder->addItem(i18nd("JPEG-XL encoder options", "Scanline order"), 0);
+ groupOrder->addItem(i18nd("JPEG-XL encoder options", "Center-first order"), -1);
+ }
+
+ {
+ responsive->addItem(i18nd("JPEG-XL encoder options", "Default (encoder chooses)"), -1);
+ responsive->addItem(i18nd("JPEG-XL encoder options", "Disabled"), 0);
+ responsive->addItem(i18nd("JPEG-XL encoder options", "Enabled"), 1);
+ }
+
+ {
+ progressiveAC->addItem(i18nd("JPEG-XL encoder options", "Default (encoder chooses)"), -1);
+ progressiveAC->addItem(i18nd("JPEG-XL encoder options", "Disabled"), 0);
+ progressiveAC->addItem(i18nd("JPEG-XL encoder options", "Enabled"), 1);
+ }
+
+ {
+ qProgressiveAC->addItem(i18nd("JPEG-XL encoder options", "Default (encoder chooses)"), -1);
+ qProgressiveAC->addItem(i18nd("JPEG-XL encoder options", "Disabled"), 0);
+ qProgressiveAC->addItem(i18nd("JPEG-XL encoder options", "Enabled"), 1);
+ }
+
+ {
+ progressiveDC->addItem(i18nd("JPEG-XL encoder options", "Default (encoder chooses)"), -1);
+ progressiveDC->addItem(i18nd("JPEG-XL encoder options", "Disabled"), 0);
+ progressiveDC->addItem(i18nd("JPEG-XL encoder options", "64x64 lower resolution pass"), 1);
+ progressiveDC->addItem(i18nd("JPEG-XL encoder options", "512x512 + 64x64 lower resolution passes"), 2);
+ }
+
+ {
+ lossyPalette->addItem(i18nd("JPEG-XL encoder options", "Default (encoder chooses)"), -1);
+ lossyPalette->addItem(i18nd("JPEG-XL encoder options", "Disabled"), 0);
+ lossyPalette->addItem(i18nd("JPEG-XL encoder options", "Enabled"), 1);
+ }
+
+ {
+ modularGroupSize->addItem(i18nd("JPEG-XL encoder options", "Default (encoder chooses)"), -1);
+ modularGroupSize->addItem(i18nd("JPEG-XL encoder options", "128"), 0);
+ modularGroupSize->addItem(i18nd("JPEG-XL encoder options", "256"), 1);
+ modularGroupSize->addItem(i18nd("JPEG-XL encoder options", "512"), 2);
+ modularGroupSize->addItem(i18nd("JPEG-XL encoder options", "1024"), 3);
+ }
+
+ {
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Default (encoder chooses)"), -1);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Zero"), 0);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Left"), 1);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Top"), 2);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Avg0"), 3);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Select"), 4);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Gradient"), 5);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Weighted"), 6);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Top right"), 7);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Top left"), 8);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Left left"), 9);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Avg1"), 10);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Avg2"), 11);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Avg3"), 12);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Toptop predictive average"), 13);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Gradient + Weighted"), 14);
+ modularPredictor->addItem(i18nd("JPEG-XL encoder options", "Use all predictors"), 15);
+ }
+
+ {
+ jpegReconCFL->addItem(i18nd("JPEG-XL encoder options", "Default (encoder chooses)"), -1);
+ jpegReconCFL->addItem(i18nd("JPEG-XL encoder options", "Disabled"), 0);
+ jpegReconCFL->addItem(i18nd("JPEG-XL encoder options", "Enabled"), 1);
+ }
+
+ metaDataFilters->setModel(&m_filterRegistryModel);
+}
+
+void KisWdgOptionsJPEGXL::setConfiguration(const KisPropertiesConfigurationSP cfg)
+{
+ haveAnimation->setChecked(cfg->getBool("haveAnimation", true));
+ lossless->setChecked(cfg->getBool("lossless", true));
+ effort->setValue(cfg->getInt("effort", 7));
+ decodingSpeed->setValue(cfg->getInt("decodingSpeed", 0));
+ resampling->setCurrentIndex(resampling->findData(cfg->getInt("resampling", -1)));
+ extraChannelResampling->setCurrentIndex(
+ extraChannelResampling->findData(cfg->getInt("extraChannelResampling", -1)));
+ photonNoise->setValue(cfg->getInt("photonNoise", 0));
+ dots->setCurrentIndex(dots->findData(cfg->getInt("dots", -1)));
+ patches->setCurrentIndex(patches->findData(cfg->getInt("patches", -1)));
+ epf->setValue(cfg->getInt("epf", -1));
+ gaborish->setCurrentIndex(gaborish->findData(cfg->getInt("gaborish", -1)));
+ modular->setCurrentIndex(modular->findData(cfg->getInt("modular", -1)));
+ keepInvisible->setCurrentIndex(keepInvisible->findData(cfg->getInt("keepInvisible", -1)));
+ groupOrder->setCurrentIndex(groupOrder->findData(cfg->getInt("groupOrder", -1)));
+ responsive->setCurrentIndex(responsive->findData(cfg->getInt("progressiveAC", -1)));
+ progressiveAC->setCurrentIndex(progressiveAC->findData(cfg->getInt("progressiveAC", -1)));
+ qProgressiveAC->setCurrentIndex(qProgressiveAC->findData(cfg->getInt("qProgressiveAC", -1)));
+ progressiveDC->setCurrentIndex(progressiveDC->findData(cfg->getInt("progressiveDC", -1)));
+ channelColorsGlobalPercent->setValue(cfg->getInt("channelColorsGlobalPercent", -1));
+ channelColorsGroupPercent->setValue(cfg->getInt("channelColorsGroupPercent", -1));
+ paletteColors->setValue(cfg->getInt("paletteColors", -1));
+ lossyPalette->setCurrentIndex(lossyPalette->findData(cfg->getInt("lossyPalette", -1)));
+ modularGroupSize->setCurrentIndex(modularGroupSize->findData(cfg->getInt("modularGroupSize", -1)));
+ modularPredictor->setCurrentIndex(modularPredictor->findData(cfg->getInt("modularPredictor", -1)));
+ modularMATreeLearningPercent->setValue(cfg->getInt("modularMATreeLearningPercent", -1));
+ jpegReconCFL->setCurrentIndex(jpegReconCFL->findData(cfg->getInt("jpegReconCFL", -1)));
+
+ exif->setChecked(cfg->getBool("exif", true));
+ xmp->setChecked(cfg->getBool("xmp", true));
+ iptc->setChecked(cfg->getBool("iptc", true));
+ chkMetadata->setChecked(cfg->getBool("storeMetaData", true));
+ m_filterRegistryModel.setEnabledFilters(cfg->getString("filters").split(','));
+}
+
+KisPropertiesConfigurationSP KisWdgOptionsJPEGXL::configuration() const
+{
+ KisPropertiesConfigurationSP cfg = new KisPropertiesConfiguration();
+
+ cfg->setProperty("haveAnimation", haveAnimation->isChecked());
+ cfg->setProperty("lossless", lossless->isChecked());
+ cfg->setProperty("effort", effort->value());
+ cfg->setProperty("decodingSpeed", decodingSpeed->value());
+ cfg->setProperty("resampling", resampling->currentData());
+ cfg->setProperty("extraChannelResampling", extraChannelResampling->currentData());
+ cfg->setProperty("photonNoise", photonNoise->value());
+ cfg->setProperty("dots", dots->currentData());
+ cfg->setProperty("patches", patches->currentData());
+ cfg->setProperty("epf", epf->value());
+ cfg->setProperty("gaborish", gaborish->currentData());
+ cfg->setProperty("modular", modular->currentData());
+ cfg->setProperty("keepInvisible", keepInvisible->currentData());
+ cfg->setProperty("groupOrder", groupOrder->currentData());
+ cfg->setProperty("responsive", responsive->currentData());
+ cfg->setProperty("progressiveAC", progressiveAC->currentData());
+ cfg->setProperty("qProgressiveAC", qProgressiveAC->currentData());
+ cfg->setProperty("progressiveDC", progressiveDC->currentData());
+ cfg->setProperty("channelColorsGlobalPercent", channelColorsGlobalPercent->value());
+ cfg->setProperty("channelColorsGroupPercent", channelColorsGroupPercent->value());
+ cfg->setProperty("paletteColors", paletteColors->value());
+ cfg->setProperty("lossyPalette", lossyPalette->currentData());
+ cfg->setProperty("modularGroupSize", modularGroupSize->currentData());
+ cfg->setProperty("modularPredictor", modularPredictor->currentData());
+ cfg->setProperty("modularMATreeLearningPercent", modularMATreeLearningPercent->value());
+ cfg->setProperty("jpegReconCFL", jpegReconCFL->currentData());
+
+ cfg->setProperty("exif", exif->isChecked());
+ cfg->setProperty("xmp", xmp->isChecked());
+ cfg->setProperty("iptc", iptc->isChecked());
+ cfg->setProperty("storeMetaData", chkMetadata->isChecked());
+
+ QString enabledFilters;
+ for (const auto *filter: m_filterRegistryModel.enabledFilters()) {
+ enabledFilters += filter->id() + ',';
+ }
+ cfg->setProperty("filters", enabledFilters);
+
+ return cfg;
+}
diff --git a/plugins/impex/jxl/kis_wdg_options_jpegxl.h b/plugins/impex/jxl/kis_wdg_options_jpegxl.h
new file mode 100644
index 0000000000..f6c79fec8c
--- /dev/null
+++ b/plugins/impex/jxl/kis_wdg_options_jpegxl.h
@@ -0,0 +1,32 @@
+/*
+ * SPDX-FileCopyrightText: 2022 L. E. Segovia <amy at amyspark.me>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef DLG_JPEGXL_EXPORT_H
+#define DLG_JPEGXL_EXPORT_H
+
+#include <QVariant>
+
+#include <KisImportExportFilter.h>
+#include <kis_config_widget.h>
+#include <kis_meta_data_filter_registry_model.h>
+
+#include "ui_kis_wdg_options_jpegxl.h"
+
+class KisWdgOptionsJPEGXL : public KisConfigWidget, public Ui::KisWdgOptionsJPEGXL
+{
+ Q_OBJECT
+
+public:
+ KisWdgOptionsJPEGXL(QWidget *parent);
+
+ void setConfiguration(const KisPropertiesConfigurationSP cfg) override;
+ KisPropertiesConfigurationSP configuration() const override;
+
+private:
+ KisMetaData::FilterRegistryModel m_filterRegistryModel;
+};
+
+#endif // DLG_JPEGXL_EXPORT_H
diff --git a/plugins/impex/jxl/kis_wdg_options_jpegxl.ui b/plugins/impex/jxl/kis_wdg_options_jpegxl.ui
new file mode 100644
index 0000000000..3ff64ff9a6
--- /dev/null
+++ b/plugins/impex/jxl/kis_wdg_options_jpegxl.ui
@@ -0,0 +1,786 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <author>
+ SPDX-FileCopyrightText: 2022 L. E. Segovia <amy at amyspark.me>
+ SPDX-License-Identifier: GPL-2.0-or-later
+ </author>
+ <class>KisWdgOptionsJPEGXL</class>
+ <widget class="QWidget" name="KisWdgOptionsJPEGXL">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>480</width>
+ <height>360</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="general">
+ <attribute name="title">
+ <string>General</string>
+ </attribute>
+ <layout class="QFormLayout" name="formLayout">
+ <item row="0" column="0" colspan="2">
+ <widget class="QCheckBox" name="haveAnimation">
+ <property name="toolTip">
+ <string>If this is not enabled, only the first frame will be saved.</string>
+ </property>
+ <property name="text">
+ <string>Save as animated JPEG-XL</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <widget class="QGroupBox" name="groupBox_2">
+ <property name="title">
+ <string>Encoding options</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout_6">
+ <item row="0" column="0" colspan="2">
+ <widget class="QCheckBox" name="lossless">
+ <property name="text">
+ <string>Lossless encoding</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="lblTradeoff">
+ <property name="text">
+ <string>Tradeoff</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="0" column="0">
+ <widget class="QLabel" name="lblFaster">
+ <property name="text">
+ <string>Faster</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="lblSlowerBetter">
+ <property name="text">
+ <string>Slower/Better</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <widget class="KisSliderSpinBox" name="effort">
+ <property name="toolTip">
+ <string>Sets encoder effort/speed level without affecting decoding speed. Valid values are, from faster to slower speed: 1:lightning 2:thunder 3:falcon 4:cheetah 5:hare 6:wombat 7:squirrel 8:kitten 9:tortoise. Default: squirrel (7). </string>
+ </property>
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>9</number>
+ </property>
+ <property name="value">
+ <number>7</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Decoding speed</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <layout class="QGridLayout" name="gridLayout_4">
+ <item row="0" column="0">
+ <widget class="QLabel" name="lblSlowest">
+ <property name="text">
+ <string>Slowest/Best quality</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="lblSlowerBetter2">
+ <property name="text">
+ <string>Fastest/Slight quality loss</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <widget class="KisSliderSpinBox" name="decodingSpeed">
+ <property name="toolTip">
+ <string>Sets the decoding speed tier for the provided options. Minimum is 0 (slowest to decode, best quality/density), and maximum is 4 (fastest to decode, at the cost of some quality/density). Default is 0.</string>
+ </property>
+ <property name="maximum">
+ <number>4</number>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="advanced">
+ <attribute name="title">
+ <string>Advanced</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Plain</enum>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="sizeAdjustPolicy">
+ <enum>QAbstractScrollArea::AdjustToContents</enum>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="scrollAreaWidgetContents">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>380</width>
+ <height>740</height>
+ </rect>
+ </property>
+ <layout class="QFormLayout" name="formLayout_2">
+ <item row="0" column="0">
+ <widget class="QLabel" name="lblResampling">
+ <property name="toolTip">
+ <string>0 = Simple, 1 = Strong. Only used if Filter Strength is higher than 0 or Auto Adjust Filter is enabled.</string>
+ </property>
+ <property name="text">
+ <string comment="Filter options specifically for WebP">Color channel resampling</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="resampling">
+ <property name="toolTip">
+ <string>Sets resampling option. If enabled, the image's color channels are downsampled before compression, and upsampled to original size in the decoder.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="lblAlphaResampling">
+ <property name="text">
+ <string>Alpha channel resampling</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="extraChannelResampling">
+ <property name="toolTip">
+ <string>Sets resampling option. If enabled, the image's alpha channel is downsampled before compression, and upsampled to original size in the decoder.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="lblPhotonNoise">
+ <property name="toolTip">
+ <string>If non-zero, sets the desired target size in bytes. Takes precedence over the compression parameters.</string>
+ </property>
+ <property name="text">
+ <string>Photon noise</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QSpinBox" name="photonNoise">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="toolTip">
+ <string>Adds noise to the image emulating photographic film noise, the higher the given number, the grainier the image will be. As an example, a value of 100 gives low noise whereas a value of 3200 gives a lot of noise. The default value is 0.</string>
+ </property>
+ <property name="maximum">
+ <number>1000000000</number>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="lblTargetPSNR">
+ <property name="toolTip">
+ <string>If non-zero, specifies the minimal distortion to try to achieve. Takes precedence over Target Size.</string>
+ </property>
+ <property name="text">
+ <string>Generate dots</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QComboBox" name="dots">
+ <property name="toolTip">
+ <string>Enables or disables dots generation.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Generate patches</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <widget class="QComboBox" name="patches">
+ <property name="toolTip">
+ <string>Enables or disables patches generation.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0">
+ <widget class="QLabel" name="lblEpf">
+ <property name="text">
+ <string>Edge preserving filter</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="1">
+ <widget class="QLabel" name="lblMax">
+ <property name="text">
+ <string>Maximum strength</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="lblNone1">
+ <property name="text">
+ <string>Default (encoder chooses)</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <widget class="KisSliderSpinBox" name="epf">
+ <property name="toolTip">
+ <string>Edge preserving filter level, -1 to 3. Use -1 for the default (encoder chooses), 0 to 3 to set a strength.</string>
+ </property>
+ <property name="maximum">
+ <number>3</number>
+ </property>
+ <property name="pageStep" stdset="0">
+ <number>1</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="6" column="0">
+ <widget class="QLabel" name="lblGaborishFilter">
+ <property name="toolTip">
+ <string>Spatial Noise Shaping: 0 = off, 100 = maximum.</string>
+ </property>
+ <property name="text">
+ <string>Gaborish filter</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="1">
+ <widget class="QComboBox" name="gaborish">
+ <property name="toolTip">
+ <string>Enables or disables the gaborish filter.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="0">
+ <widget class="QLabel" name="lbModularEncoding">
+ <property name="text">
+ <string>Modular encoding</string>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="1">
+ <widget class="QComboBox" name="modular">
+ <property name="toolTip">
+ <string>Enables modular encoding.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="8" column="0">
+ <widget class="QLabel" name="lblKeepInvisible">
+ <property name="text">
+ <string>Keep color of invisible pixels</string>
+ </property>
+ </widget>
+ </item>
+ <item row="8" column="1">
+ <widget class="QComboBox" name="keepInvisible">
+ <property name="toolTip">
+ <string>Enables or disables preserving color of invisible pixels.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="9" column="0">
+ <widget class="QLabel" name="lblGroupOrder">
+ <property name="toolTip">
+ <string>Filter strength: 0 = off, 100 = strongest.</string>
+ </property>
+ <property name="text">
+ <string comment="Filter options specifically for WebP">Group order</string>
+ </property>
+ </widget>
+ </item>
+ <item row="9" column="1">
+ <widget class="QComboBox" name="groupOrder">
+ <property name="toolTip">
+ <string>Determines the order in which 256x256 regions are stored in the codestream for progressive rendering.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="10" column="0">
+ <widget class="QLabel" name="lblJPEGReconCFL">
+ <property name="text">
+ <string>Chroma-from-luma</string>
+ </property>
+ </widget>
+ </item>
+ <item row="11" column="0" colspan="2">
+ <widget class="QGroupBox" name="groupBox">
+ <property name="title">
+ <string>VarDCT parameters</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout_3">
+ <item row="0" column="0">
+ <widget class="QLabel" name="lblProgressiveAC">
+ <property name="text">
+ <string>Spectral progression</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="progressiveAC">
+ <property name="toolTip">
+ <string>Set the progressive mode for the AC coefficients of VarDCT, using spectral progression from the DCT coefficients.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="lblQProgressiveAC">
+ <property name="toolTip">
+ <string/>
+ </property>
+ <property name="text">
+ <string>Quantization</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="qProgressiveAC">
+ <property name="toolTip">
+ <string>Set the progressive mode for the AC coefficients of VarDCT, using spectral progression from the DCT coefficients.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="lblProgressiveDC">
+ <property name="text">
+ <string>Low resolution DC</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QComboBox" name="progressiveDC">
+ <property name="toolTip">
+ <string>Set the progressive mode using lower-resolution DC images for VarDCT. </string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item row="12" column="0" colspan="2">
+ <widget class="QGroupBox" name="modularEncodingParameters">
+ <property name="title">
+ <string>Modular parameters</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout_4">
+ <item row="2" column="0">
+ <widget class="QLabel" name="lblChannelColorsGlobalPercent">
+ <property name="text">
+ <string>Global channel palette range</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <layout class="QGridLayout" name="gridLayout2">
+ <item row="1" column="0" colspan="2">
+ <widget class="KisSliderSpinBox" name="channelColorsGlobalPercent">
+ <property name="toolTip">
+ <string>Use Global channel palette if the amount of colors is smaller than this percentage of range. Use 0-100 to set an explicit percentage, -1 to use the encoder default. Used for modular encoding.</string>
+ </property>
+ <property name="suffix">
+ <string>%</string>
+ </property>
+ <property name="minimum">
+ <number>-1</number>
+ </property>
+ <property name="maximum">
+ <number>100</number>
+ </property>
+ <property name="value">
+ <number>-1</number>
+ </property>
+ <property name="pageStep" stdset="0">
+ <number>1</number>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="lblNone">
+ <property name="text">
+ <string>Default (encoder chooses)</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="lblBest">
+ <property name="text">
+ <string>All</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="lblChannelColorsGroupPercent">
+ <property name="text">
+ <string>Local channel palette range</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <layout class="QGridLayout" name="gridLayout3">
+ <item row="1" column="0" colspan="2">
+ <widget class="KisSliderSpinBox" name="channelColorsGroupPercent">
+ <property name="toolTip">
+ <string>Use Local (per-group) channel palette if the amount of colors is smaller than this percentage of range. Use 0-100 to set an explicit percentage, -1 to use the encoder default. Used for modular encoding.</string>
+ </property>
+ <property name="suffix">
+ <string>%</string>
+ </property>
+ <property name="minimum">
+ <number>-1</number>
+ </property>
+ <property name="maximum">
+ <number>100</number>
+ </property>
+ <property name="value">
+ <number>-1</number>
+ </property>
+ <property name="pageStep" stdset="0">
+ <number>1</number>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="lblNone2">
+ <property name="text">
+ <string>Default (encoder chooses)</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="lblBest2">
+ <property name="text">
+ <string>All</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLabel" name="lblPaletteColors">
+ <property name="text">
+ <string>Use color palette for ... colors or less</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <widget class="QSpinBox" name="paletteColors">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="toolTip">
+ <string>Use color palette if amount of colors is smaller than or equal to this amount, or -1 to use the encoder default. Used for modular encoding.</string>
+ </property>
+ <property name="minimum">
+ <number>-1</number>
+ </property>
+ <property name="maximum">
+ <number>1000000000</number>
+ </property>
+ <property name="singleStep">
+ <number>1</number>
+ </property>
+ <property name="value">
+ <number>-1</number>
+ </property>
+ <property name="pageStep" stdset="0">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0">
+ <widget class="QLabel" name="lblLossyPalette">
+ <property name="text">
+ <string comment="Filter options specifically for WebP">Delta palette</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <widget class="QComboBox" name="lossyPalette">
+ <property name="toolTip">
+ <string>Enables or disables delta palette. Used in modular mode. </string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="0">
+ <widget class="QLabel" name="lblModularGroupSize">
+ <property name="text">
+ <string>Group size</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="1">
+ <widget class="QComboBox" name="modularGroupSize">
+ <property name="toolTip">
+ <string>Group size for modular encoding.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="0">
+ <widget class="QLabel" name="lblModularPredictor">
+ <property name="text">
+ <string>Predictor</string>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="1">
+ <widget class="QComboBox" name="modularPredictor">
+ <property name="toolTip">
+ <string>Predictor for modular encoding.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="8" column="0">
+ <widget class="QLabel" name="lblModularMATreeLearningPercent">
+ <property name="toolTip">
+ <string>Quality degradation allowed to fit the 512KB limit on prediction modes coding (0 = no degradation, 100 = maximum possible degradation).</string>
+ </property>
+ <property name="text">
+ <string>Pixels for MA tree learning</string>
+ </property>
+ </widget>
+ </item>
+ <item row="8" column="1">
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="1">
+ <widget class="QLabel" name="lblMaximumDegradation">
+ <property name="text">
+ <string>All</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="lblNoDegradation">
+ <property name="text">
+ <string>Default (encoder chooses)</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <widget class="KisSliderSpinBox" name="modularMATreeLearningPercent">
+ <property name="toolTip">
+ <string>Fraction of pixels used to learn MA trees as a percentage. -1 = default, 0 = no MA and fast decode, 50 = default value, 100 = all. Higher values use more encoder memory.
+</string>
+ </property>
+ <property name="suffix">
+ <string>%</string>
+ </property>
+ <property name="minimum">
+ <number>-1</number>
+ </property>
+ <property name="maximum">
+ <number>100</number>
+ </property>
+ <property name="value">
+ <number>-1</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="responsive">
+ <property name="toolTip">
+ <string>Enables or disables progressive encoding for modular mode.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="lblResponsive">
+ <property name="toolTip">
+ <string>If enabled, export the compressed picture back. In-loop filtering is not applied.</string>
+ </property>
+ <property name="text">
+ <string>Progressive encoding</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item row="10" column="1">
+ <widget class="QComboBox" name="jpegReconCFL"/>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="metadata">
+ <attribute name="title">
+ <string>Metadata</string>
+ </attribute>
+ <layout class="QFormLayout" name="formLayout_7">
+ <item row="0" column="0" colspan="2">
+ <widget class="QGroupBox" name="chkMetadata">
+ <property name="title">
+ <string>Store Document Metadata</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_5" columnstretch="0,0">
+ <item row="0" column="0">
+ <widget class="QGroupBox" name="groupBox_3">
+ <property name="title">
+ <string>Formats:</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout_5">
+ <item row="0" column="0" colspan="2">
+ <widget class="QCheckBox" name="exif">
+ <property name="text">
+ <string>Exif</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <widget class="QCheckBox" name="iptc">
+ <property name="text">
+ <string>IPTC</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <widget class="QCheckBox" name="xmp">
+ <property name="text">
+ <string>XMP</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QGroupBox" name="groupBox_4">
+ <property name="title">
+ <string>Filters:</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QListView" name="metaDataFilters"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>KisSliderSpinBox</class>
+ <extends>QSpinBox</extends>
+ <header location="global">kis_slider_spin_box.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/plugins/impex/jxl/krita_jxl.desktop b/plugins/impex/jxl/krita_jxl.desktop
new file mode 100644
index 0000000000..92e935ea94
--- /dev/null
+++ b/plugins/impex/jxl/krita_jxl.desktop
@@ -0,0 +1,125 @@
+[Desktop Entry]
+Categories=Qt;KDE;Office;Graphics;
+Exec=krita %F
+GenericName=Application for Drawing and Handling of Images
+GenericName[ar]=تطبيق لرسم الصور والتعامل معها
+GenericName[bg]=Приложение за рисуване и обработка на изображения
+GenericName[bs]=Aplikacija za crtanje i upravljanje slikom
+GenericName[ca]=Aplicació per a dibuix i modificació d'imatges
+GenericName[ca at valencia]=Aplicació per a dibuix i modificació d'imatges
+GenericName[da]=Tegne- og billedbehandlingsprogram
+GenericName[de]=Programm zum Zeichnen und Bearbeiten von Bildern
+GenericName[el]=Εφαρμογή για επεξεργασία και χειρισμό εικόνων
+GenericName[en_GB]=Application for Drawing and Handling of Images
+GenericName[eo]=Aplikaĵo por Desegnado kaj Mastrumado de Bildoj
+GenericName[es]=Aplicación para dibujo y manipulación de imágenes
+GenericName[et]=Joonistamise ja pilditöötluse rakendus
+GenericName[eu]=Irudiak marrazteko eta manipulatzeko aplikazioa
+GenericName[fa]=کاربرد برای ترسیم و به کار بردن تصاویر
+GenericName[fi]=Ohjelma kuvien piirtämiseen ja käsittelyyn
+GenericName[fr]=Application pour dessiner et manipuler des images
+GenericName[fy]=Aplikaasje om ôfbyldings mei te tekenjen en te bewurkjen
+GenericName[ga]=Feidhmchlár le haghaidh Líníochta agus Láimhseála Íomhánna
+GenericName[gl]=Aplicación de debuxo e edición de imaxes
+GenericName[he]=יישום לצביעה וניהול תמונות
+GenericName[hi]=चित्रों को बनाने तथा उन्हें प्रबन्धित करने का अनुप्रयोग
+GenericName[hne]=फोटू मन ल ड्रा करे अउ ओ मन ल प्रबन्धित करे के अनुपरयोग
+GenericName[hu]=Rajzoló és képkezelő alkalmazás
+GenericName[is]=Teikni og myndvinnsluforrit
+GenericName[it]=Applicazione di disegno e gestione di immagini
+GenericName[ja]=描画と画像操作のためのアプリケーション
+GenericName[kk]=Кескінді салу және өңдеу бағдарламасы
+GenericName[ko]=그림 그리기 및 처리 프로그램
+GenericName[lv]=Programma zīmēšanai un attēlu apstrādei
+GenericName[nds]=Programm för't Teken un Bildhanteren
+GenericName[ne]=रेखाचित्र बनाउन र छविको ह्यान्डल गर्नका लागि अनुप्रयोग
+GenericName[nl]=Toepassing om afbeeldingen te tekenen en te bewerken
+GenericName[nn]=Program for teikning og handsaming av bilete
+GenericName[pl]=Program do rysowania i obróbki obrazów
+GenericName[pt]=Aplicação de Desenho e Manipulação de Imagens
+GenericName[pt_BR]=Aplicativo de desenho e manipulação de imagens
+GenericName[ro]=Aplicație pentru desenare și manipularea imaginilor
+GenericName[ru]=Приложение для рисования и редактирования изображений
+GenericName[sk]=Aplikácia na kresnenie a manilupáciu s obrázkami
+GenericName[sl]=Program za risanje in rokovanje s slikami
+GenericName[sv]=Program för att rita och hantera bilder
+GenericName[ta]=பிம்பங்களை கையாளுதல் மற்றும் வரைதலுக்கான பயன்னாடு
+GenericName[tr]=Çizim ve Resim İşleme Uygulaması
+GenericName[uk]=Програма для малювання і обробки зображень
+GenericName[uz]=Rasm chizish dasturi
+GenericName[uz at cyrillic]=Расм чизиш дастури
+GenericName[wa]=Programe po dessiner et apougnî des imådjes
+GenericName[x-test]=xxApplication for Drawing and Handling of Imagesxx
+GenericName[zh_CN]=用于绘制和处理图像的应用程序
+GenericName[zh_TW]=繪圖與影像處理的應用程式
+Icon=krita
+MimeType=image/jxl;
+Name=Krita
+Name[af]=Krita
+Name[ar]=كريتا
+Name[bg]=Krita
+Name[br]=Krita
+Name[bs]=Krita
+Name[ca]=Krita
+Name[ca at valencia]=Krita
+Name[cs]=Krita
+Name[cy]=Krita
+Name[da]=Krita
+Name[de]=Krita
+Name[el]=Krita
+Name[en_GB]=Krita
+Name[eo]=Krita
+Name[es]=Krita
+Name[et]=Krita
+Name[eu]=Krita
+Name[fi]=Krita
+Name[fr]=Krita
+Name[fy]=Krita
+Name[ga]=Krita
+Name[gl]=Krita
+Name[he]=Krita
+Name[hi]=क्रिता
+Name[hne]=केरिता
+Name[hr]=Krita
+Name[hu]=Krita
+Name[ia]=Krita
+Name[is]=Krita
+Name[it]=Krita
+Name[ja]=Krita
+Name[kk]=Krita
+Name[ko]=Krita
+Name[lt]=Krita
+Name[lv]=Krita
+Name[mr]=क्रिटा
+Name[ms]=Krita
+Name[nds]=Krita
+Name[ne]=क्रिता
+Name[nl]=Krita
+Name[nn]=Krita
+Name[pl]=Krita
+Name[pt]=Krita
+Name[pt_BR]=Krita
+Name[ro]=Krita
+Name[ru]=Krita
+Name[se]=Krita
+Name[sk]=Krita
+Name[sl]=Krita
+Name[sv]=Krita
+Name[ta]=கிரிட்டா
+Name[tg]=Krita
+Name[tr]=Krita
+Name[ug]=Krita
+Name[uk]=Krita
+Name[uz]=Krita
+Name[uz at cyrillic]=Krita
+Name[wa]=Krita
+Name[xh]=Krita
+Name[x-test]=xxKritaxx
+Name[zh_CN]=Krita
+Name[zh_TW]=Krita
+StartupNotify=true
+Terminal=false
+Type=Application
+X-KDE-SubstituteUID=false
+X-KDE-Username=
+NoDisplay=true
diff --git a/plugins/impex/jxl/krita_jxl_export.json b/plugins/impex/jxl/krita_jxl_export.json
new file mode 100644
index 0000000000..9284f7bee3
--- /dev/null
+++ b/plugins/impex/jxl/krita_jxl_export.json
@@ -0,0 +1,12 @@
+{
+ "Icon": "",
+ "Id": "Krita JPEG-XL Export Filter",
+ "Type": "Service",
+ "X-KDE-Export": "image/jxl",
+ "X-KDE-Library": "kritajxlexport",
+ "X-KDE-ServiceTypes": [
+ "Krita/FileFilter"
+ ],
+ "X-KDE-Weight": 1,
+ "X-KDE-Extensions" : "jxl"
+}
diff --git a/plugins/impex/jxl/krita_jxl_import.json b/plugins/impex/jxl/krita_jxl_import.json
new file mode 100644
index 0000000000..2407e818be
--- /dev/null
+++ b/plugins/impex/jxl/krita_jxl_import.json
@@ -0,0 +1,12 @@
+{
+ "Icon": "",
+ "Id": "Krita JPEG-XL Import Filter",
+ "Type": "Service",
+ "X-KDE-Import": "image/jxl",
+ "X-KDE-Library": "kritajxlimport",
+ "X-KDE-ServiceTypes": [
+ "Krita/FileFilter"
+ ],
+ "X-KDE-Weight": 1,
+ "X-KDE-Extensions" : "jxl"
+}
diff --git a/plugins/impex/jxl/tests/CMakeLists.txt b/plugins/impex/jxl/tests/CMakeLists.txt
new file mode 100644
index 0000000000..221cc8ea14
--- /dev/null
+++ b/plugins/impex/jxl/tests/CMakeLists.txt
@@ -0,0 +1,10 @@
+include_directories(${CMAKE_SOURCE_DIR}/sdk/tests)
+
+macro_add_unittest_definitions()
+
+ecm_add_test(
+ kis_jpegxl_test.cpp
+ TEST_NAME kis_jpegxl_test
+ LINK_LIBRARIES kritametadata kritaui Qt5::Test
+ NAME_PREFIX "plugins-impex-"
+)
diff --git a/plugins/impex/jxl/tests/data/results/hdr_cosmos01000_cicp9-16-0_lossless.kra b/plugins/impex/jxl/tests/data/results/hdr_cosmos01000_cicp9-16-0_lossless.kra
new file mode 100644
index 0000000000..590ab5f3d8
Binary files /dev/null and b/plugins/impex/jxl/tests/data/results/hdr_cosmos01000_cicp9-16-0_lossless.kra differ
diff --git a/plugins/impex/jxl/tests/data/results/quad-lzw.jxl.png b/plugins/impex/jxl/tests/data/results/quad-lzw.jxl.png
new file mode 100644
index 0000000000..e186ddf2ae
Binary files /dev/null and b/plugins/impex/jxl/tests/data/results/quad-lzw.jxl.png differ
diff --git a/plugins/impex/jxl/tests/data/results/red.jxl.png b/plugins/impex/jxl/tests/data/results/red.jxl.png
new file mode 100644
index 0000000000..9cc866c68b
Binary files /dev/null and b/plugins/impex/jxl/tests/data/results/red.jxl.png differ
diff --git a/plugins/impex/jxl/tests/data/results/sdr_cosmos01000_cicp1-13-0_lossless.jxl.png b/plugins/impex/jxl/tests/data/results/sdr_cosmos01000_cicp1-13-0_lossless.jxl.png
new file mode 100644
index 0000000000..0cddd2e0dc
Binary files /dev/null and b/plugins/impex/jxl/tests/data/results/sdr_cosmos01000_cicp1-13-0_lossless.jxl.png differ
diff --git a/plugins/impex/jxl/tests/data/results/strike.jxl.png b/plugins/impex/jxl/tests/data/results/strike.jxl.png
new file mode 100644
index 0000000000..3ed5295130
Binary files /dev/null and b/plugins/impex/jxl/tests/data/results/strike.jxl.png differ
diff --git a/plugins/impex/jxl/tests/data/sources/DX-MON/LICENSE.txt b/plugins/impex/jxl/tests/data/sources/DX-MON/LICENSE.txt
new file mode 100644
index 0000000000..98a1c3fcec
--- /dev/null
+++ b/plugins/impex/jxl/tests/data/sources/DX-MON/LICENSE.txt
@@ -0,0 +1,27 @@
+Copyright (c) 2016-2020 Rachel Mant
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/plugins/impex/jxl/tests/data/sources/DX-MON/loading_16.jxl b/plugins/impex/jxl/tests/data/sources/DX-MON/loading_16.jxl
new file mode 100644
index 0000000000..1479497bff
Binary files /dev/null and b/plugins/impex/jxl/tests/data/sources/DX-MON/loading_16.jxl differ
diff --git a/plugins/impex/jxl/tests/data/sources/netflix/LICENSE.txt b/plugins/impex/jxl/tests/data/sources/netflix/LICENSE.txt
new file mode 100644
index 0000000000..954b5614b8
--- /dev/null
+++ b/plugins/impex/jxl/tests/data/sources/netflix/LICENSE.txt
@@ -0,0 +1,12 @@
+AV1 Image Files
+COPYRIGHT AND LICENSE INFORMATION
+September, 2019
+
+NETFLIX INC.
+100 Winchester Circle, Los Gatos, CA 95032, USA
+
+The AVIF images and sequences in this folder and all intellectual property rights
+therein remain the property of Netflix Inc. The images and sequences are licensed
+under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0
+International License. To view a copy of this license, visit
+http://creativecommons.org/licenses/by-nc-nd/4.0/
\ No newline at end of file
diff --git a/plugins/impex/jxl/tests/data/sources/netflix/hdr_cosmos01000_cicp9-16-0_lossless.jxl b/plugins/impex/jxl/tests/data/sources/netflix/hdr_cosmos01000_cicp9-16-0_lossless.jxl
new file mode 100644
index 0000000000..659da21e1d
Binary files /dev/null and b/plugins/impex/jxl/tests/data/sources/netflix/hdr_cosmos01000_cicp9-16-0_lossless.jxl differ
diff --git a/plugins/impex/jxl/tests/data/sources/netflix/sdr_cosmos01000_cicp1-13-0_lossless.jxl b/plugins/impex/jxl/tests/data/sources/netflix/sdr_cosmos01000_cicp1-13-0_lossless.jxl
new file mode 100644
index 0000000000..3a6f3b36be
Binary files /dev/null and b/plugins/impex/jxl/tests/data/sources/netflix/sdr_cosmos01000_cicp1-13-0_lossless.jxl differ
diff --git a/plugins/impex/jxl/tests/data/sources/quad-lzw.jxl b/plugins/impex/jxl/tests/data/sources/quad-lzw.jxl
new file mode 100644
index 0000000000..033f42eb4f
Binary files /dev/null and b/plugins/impex/jxl/tests/data/sources/quad-lzw.jxl differ
diff --git a/plugins/impex/jxl/tests/data/sources/red.jxl b/plugins/impex/jxl/tests/data/sources/red.jxl
new file mode 100644
index 0000000000..5d3cf993d0
Binary files /dev/null and b/plugins/impex/jxl/tests/data/sources/red.jxl differ
diff --git a/plugins/impex/jxl/tests/data/sources/strike.jxl b/plugins/impex/jxl/tests/data/sources/strike.jxl
new file mode 100644
index 0000000000..d0884e6e36
Binary files /dev/null and b/plugins/impex/jxl/tests/data/sources/strike.jxl differ
diff --git a/plugins/impex/jxl/tests/kis_jpegxl_test.cpp b/plugins/impex/jxl/tests/kis_jpegxl_test.cpp
new file mode 100644
index 0000000000..3ebe0e355a
--- /dev/null
+++ b/plugins/impex/jxl/tests/kis_jpegxl_test.cpp
@@ -0,0 +1,126 @@
+/*
+ * SPDX-FileCopyrightText: 2022 L. E. Segovia <amy at amyspark.me>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "kis_jpegxl_test.h"
+#include "kis_image_animation_interface.h"
+#include "kis_keyframe_channel.h"
+
+#include <simpletest.h>
+
+#include <QString>
+
+#include <kis_meta_data_backend_registry.h>
+#include <sdk/tests/filestest.h>
+#include <sdk/tests/testui.h>
+
+#ifndef FILES_DATA_DIR
+#error "FILES_DATA_DIR not set. A directory with the data used for testing the importing of files in krita"
+#endif
+
+const QString MIMETYPE = "image/jxl";
+
+void KisJPEGXLTest::testFiles()
+{
+ KisMetadataBackendRegistry::instance();
+
+ const int fuzziness = 1;
+
+ TestUtil::testFiles(QString(FILES_DATA_DIR) + "/sources", {}, {}, fuzziness, 0, true);
+
+ TestUtil::testFiles(QString(FILES_DATA_DIR) + "/sources/netflix",
+ {"hdr_cosmos01000_cicp9-16-0_lossless.jxl", "LICENSE.txt"},
+ {},
+ fuzziness,
+ 0,
+ true);
+}
+
+void KisJPEGXLTest::testAnimation()
+{
+ const auto inputFileName = TestUtil::fetchDataFileLazy("/sources/DX-MON/loading_16.jxl");
+
+ QScopedPointer<KisDocument> doc(qobject_cast<KisDocument *>(KisPart::instance()->createDocument()));
+
+ KisImportExportManager manager(doc.data());
+ doc->setFileBatchMode(true);
+
+ const auto status = manager.importDocument(inputFileName, QString());
+ QVERIFY(status.isOk());
+
+ KisImageSP image = doc->image();
+
+ // Check that it's a 32 FPS document with 24 frames.
+ auto node1 = doc->image()->root()->firstChild();
+
+ QVERIFY(node1->inherits("KisPaintLayer"));
+
+ KisPaintLayerSP layer1 = qobject_cast<KisPaintLayer *>(node1.data());
+
+ QVERIFY(layer1->isAnimated());
+
+ const auto *channel1 = layer1->getKeyframeChannel(KisKeyframeChannel::Raster.id());
+ QVERIFY(channel1);
+ QCOMPARE(channel1->keyframeCount(), 24);
+
+ QCOMPARE(image->animationInterface()->framerate(), 32);
+ QCOMPARE(image->animationInterface()->fullClipRange(), KisTimeSpan::fromTimeToTime(0, 24));
+ QCOMPARE(image->animationInterface()->currentTime(), 0);
+}
+
+void KisJPEGXLTest::testHDR()
+{
+ const auto inputFileName = TestUtil::fetchDataFileLazy("/sources/netflix/hdr_cosmos01000_cicp9-16-0_lossless.jxl");
+
+ QScopedPointer<KisDocument> doc1(qobject_cast<KisDocument *>(KisPart::instance()->createDocument()));
+
+ KisImportExportManager manager(doc1.data());
+ doc1->setFileBatchMode(true);
+
+ const auto status = manager.importDocument(inputFileName, {});
+ QVERIFY(status.isOk());
+
+ KisImageSP image = doc1->image();
+
+ {
+ const auto outputFileName = TestUtil::fetchDataFileLazy("/results/hdr_cosmos01000_cicp9-16-0_lossless.kra");
+
+ KisDocument *doc2 = KisPart::instance()->createDocument();
+ doc2->setFileBatchMode(true);
+ const auto r = doc2->importDocument(outputFileName);
+
+ QVERIFY(r);
+ QVERIFY(doc2->errorMessage().isEmpty());
+ QVERIFY(doc2->image());
+
+ doc1->image()->root()->firstChild()->paintDevice()->convertToQImage(nullptr).save("1.png");
+ doc2->image()->root()->firstChild()->paintDevice()->convertToQImage(nullptr).save("2.png");
+
+ QVERIFY(TestUtil::comparePaintDevicesClever<float>(doc1->image()->root()->firstChild()->paintDevice(),
+ doc2->image()->root()->firstChild()->paintDevice(),
+ 0.01f /* meaningless alpha */));
+
+ delete doc2;
+ }
+}
+
+#ifndef _WIN32
+void KisJPEGXLTest::testImportFromWriteonly()
+{
+ TestUtil::testImportFromWriteonly(MIMETYPE);
+}
+
+void KisJPEGXLTest::testExportToReadonly()
+{
+ TestUtil::testExportToReadonly(MIMETYPE);
+}
+#endif
+
+void KisJPEGXLTest::testImportIncorrectFormat()
+{
+ TestUtil::testImportIncorrectFormat(MIMETYPE);
+}
+
+KISTEST_MAIN(KisJPEGXLTest)
diff --git a/plugins/impex/jxl/tests/kis_jpegxl_test.h b/plugins/impex/jxl/tests/kis_jpegxl_test.h
new file mode 100644
index 0000000000..20a09c3df1
--- /dev/null
+++ b/plugins/impex/jxl/tests/kis_jpegxl_test.h
@@ -0,0 +1,29 @@
+/*
+ * SPDX-FileCopyrightText: 2022 L. E. Segovia <amy at amyspark.me>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef _KIS_JPEG_TEST_H_
+#define _KIS_JPEG_TEST_H_
+
+#include <simpletest.h>
+
+class KisJPEGXLTest : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void testAnimation();
+ void testFiles();
+ void testHDR();
+ void testImportIncorrectFormat();
+
+#ifndef Q_OS_WIN
+private Q_SLOTS:
+ void testImportFromWriteonly();
+ void testExportToReadonly();
+#endif
+};
+
+#endif
More information about the kimageshop
mailing list