[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