/*
    This file is part of the KDE project
    SPDX-FileCopyrightText: 2024 Mirco Miranda <mircomir@outlook.com>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/

/*
 * Info about JXR:
 * - https://learn.microsoft.com/en-us/windows/win32/wic/jpeg-xr-codec
 *
 * Sample images:
 * - http://fileformats.archiveteam.org/wiki/JPEG_XR
 * - https://github.com/bvibber/hdrfix/tree/main/samples
 */

#include "jxr_p.h"
#include "microexif_p.h"
#include "util_p.h"

#include <QColorSpace>
#include <QCoreApplication>
#include <QDataStream>
#include <QFile>
#include <QFloat16>
#include <QHash>
#include <QImage>
#include <QImageReader>
#include <QLoggingCategory>
#include <QSet>
#include <QSharedData>
#include <QTemporaryDir>

#include <JXRGlue.h>
#include <cstring>

Q_DECLARE_LOGGING_CATEGORY(LOG_JXRPLUGIN)

#ifdef QT_DEBUG
Q_LOGGING_CATEGORY(LOG_JXRPLUGIN, "kf.imageformats.plugins.jxr", QtDebugMsg)
#else
Q_LOGGING_CATEGORY(LOG_JXRPLUGIN, "kf.imageformats.plugins.jxr", QtWarningMsg)
#endif

/*!
 * Support for float images
 *
 * NOTE: Float images have values greater than 1 so they need an additional in place conversion.
 */
// #define JXR_DENY_FLOAT_IMAGE // default commented

/*!
 * Remove the needs of additional memory by disabling the conversion between
 * different color depths (e.g. RGBA64bpp to RGBA32bpp).
 *
 * NOTE: Leaving deptch conversion enabled (default) ensures maximum read compatibility.
 */
// #define JXR_DISABLE_DEPTH_CONVERSION // default commented

/*!
 * Windows displays and opens JXR files correctly out of the box. Unfortunately it doesn't
 * seem to open (P)RGBA @32bpp files as it only wants (P)BGRA32bpp files (a format not supported by Qt).
 * Only for this format an hack is activated to guarantee total compatibility of the plugin with Windows.
 */
// #define JXR_DISABLE_BGRA_HACK // default commented

/*!
 * The following functions are present in the Debian headers but not in the SUSE ones even if the source version is 1.0.1 on both.
 *
 * - ERR PKImageDecode_GetXMPMetadata_WMP(PKImageDecode *pID, U8 *pbXMPMetadata, U32 *pcbXMPMetadata);
 * - ERR PKImageDecode_GetEXIFMetadata_WMP(PKImageDecode *pID, U8 *pbEXIFMetadata, U32 *pcbEXIFMetadata);
 * - ERR PKImageDecode_GetGPSInfoMetadata_WMP(PKImageDecode *pID, U8 *pbGPSInfoMetadata, U32 *pcbGPSInfoMetadata);
 * - ERR PKImageDecode_GetIPTCNAAMetadata_WMP(PKImageDecode *pID, U8 *pbIPTCNAAMetadata, U32 *pcbIPTCNAAMetadata);
 * - ERR PKImageDecode_GetPhotoshopMetadata_WMP(PKImageDecode *pID, U8 *pbPhotoshopMetadata, U32 *pcbPhotoshopMetadata);
 *
 * As a result, their use is disabled by default. It is possible to activate their use by defining the
 * JXR_ENABLE_ADVANCED_METADATA preprocessor directive
 */

// #define JXR_ENABLE_ADVANCED_METADATA

/* *** JXR_MAX_IMAGE_WIDTH and JXR_MAX_IMAGE_HEIGHT ***
 * The maximum size in pixel allowed by the plugin.
 */
#ifndef JXR_MAX_IMAGE_WIDTH
#define JXR_MAX_IMAGE_WIDTH KIF_LARGE_IMAGE_PIXEL_LIMIT
#endif
#ifndef JXR_MAX_IMAGE_HEIGHT
#define JXR_MAX_IMAGE_HEIGHT JXR_MAX_IMAGE_WIDTH
#endif

#ifndef JXR_MAX_METADATA_SIZE
/*!
 * XMP and EXIF maximum size.
 */
#define JXR_MAX_METADATA_SIZE (4 * 1024 * 1024)
#endif

class JXRHandlerPrivate : public QSharedData
{
private:
    QSharedPointer<QTemporaryDir> m_tempDir;
    QSharedPointer<QFile> m_jxrFile;
    MicroExif m_exif;
    qint32 m_quality;
    QImageIOHandler::Transformations m_transformations;
    mutable QHash<QString, QString> m_txtMeta;

public:
    PKFactory *pFactory = nullptr;
    PKCodecFactory *pCodecFactory = nullptr;
    PKImageDecode *pDecoder = nullptr;
    PKImageEncode *pEncoder = nullptr;

    JXRHandlerPrivate()
        : m_quality(-1)
        , m_transformations(QImageIOHandler::TransformationNone)
    {
        m_tempDir = QSharedPointer<QTemporaryDir>(new QTemporaryDir);
        if (auto err = PKCreateFactory(&pFactory, PK_SDK_VERSION)) {
            qCCritical(LOG_JXRPLUGIN) << "JXRHandlerPrivate::JXRHandlerPrivate() error while initializing the JXR factory:" << err;
        } else if (auto err = PKCreateCodecFactory(&pCodecFactory, WMP_SDK_VERSION)) {
            qCCritical(LOG_JXRPLUGIN) << "JXRHandlerPrivate::JXRHandlerPrivate() error while initializing the JXR codec factory:" << err;
        }
    }
    JXRHandlerPrivate(const JXRHandlerPrivate &other) = default;

    ~JXRHandlerPrivate()
    {
        if (pDecoder) {
            if (auto err = pDecoder->Release(&pDecoder)) {
                qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::JXRHandlerPrivate() error while releasing the decoder:" << err;
            }
        }
        if (pEncoder) {
            if (auto err = pEncoder->Release(&pEncoder)) {
                qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::JXRHandlerPrivate() error while releasing the encoder:" << err;
            }
        }
        if (pCodecFactory) {
            if (auto err = pCodecFactory->Release(&pCodecFactory)) {
                qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::JXRHandlerPrivate() error while releasing the codec factory:" << err;
            }
        }
        if (pFactory) {
            if (auto err = pFactory->Release(&pFactory)) {
                qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::JXRHandlerPrivate() error while releasing the factory:" << err;
            }
        }
    }

    QString fileName() const
    {
        return m_jxrFile->fileName();
    }

    /*!
     * \brief setQuality
     * Set the image quality (write only)
     * \param q
     */
    void setQuality(qint32 q)
    {
        m_quality = q;
    }
    qint32 quality() const
    {
        return m_quality;
    }

    /*!
     * \brief setTransformation
     * Set the image transformation (read/write)
     * \param t
     */
    void setTransformation(const QImageIOHandler::Transformations& t)
    {
        m_transformations = t;
    }
    QImageIOHandler::Transformations transformation() const
    {
        return m_transformations;
    }

    static QImageIOHandler::Transformations orientationToTransformation(const ORIENTATION& o)
    {
        auto v = QImageIOHandler::TransformationNone;
        switch (o) {
        case O_FLIPV:
            v = QImageIOHandler::TransformationFlip;
            break;
        case O_FLIPH:
            v = QImageIOHandler::TransformationMirror;
            break;
        case O_FLIPVH:
            v = QImageIOHandler::TransformationRotate180;
            break;
        case O_RCW:
            v = QImageIOHandler::TransformationRotate90;
            break;
        case O_RCW_FLIPH:
            v = QImageIOHandler::TransformationFlipAndRotate90;
            break;
        case O_RCW_FLIPV:
            v = QImageIOHandler::TransformationMirrorAndRotate90;
            break;
        case O_RCW_FLIPVH:
            v = QImageIOHandler::TransformationRotate270;
            break;
        default:
            v = QImageIOHandler::TransformationNone;
            break;
        }
        return v;
    }
    static ORIENTATION transformationToOrientation(const QImageIOHandler::Transformations& t)
    {
        auto v = O_NONE;
        switch (t) {
        case QImageIOHandler::TransformationFlip:
            v = O_FLIPV;
            break;
        case QImageIOHandler::TransformationMirror:
            v = O_FLIPH;
            break;
        case QImageIOHandler::TransformationRotate180:
            v = O_FLIPVH;
            break;
        case QImageIOHandler::TransformationRotate90:
            v = O_RCW;
            break;
        case QImageIOHandler::TransformationFlipAndRotate90:
            v = O_RCW_FLIPH;
            break;
        case QImageIOHandler::TransformationMirrorAndRotate90:
            v = O_RCW_FLIPV;
            break;
        case QImageIOHandler::TransformationRotate270:
            v = O_RCW_FLIPVH;
            break;
        default:
            v = O_NONE;
            break;
        }
        return v;
    }


    /* *** READ *** */

    /*!
     * \brief initForReading
     * Initialize the device for reading.
     * \param device The source device.
     * \return True on success, otherwise false.
     */
    bool initForReading(QIODevice *device)
    {
        if (!readDevice(device)) {
            return false;
        }
        if (!initDecoder()) {
            return false;
        }
        return true;
    }

    /*!
     * \brief initForReadingAndRollBack
     * Initialize the device for reading and rollback the device to start position.
     * \param device The source device.
     * \return True on success, otherwise false.
     */
    bool initForReadingAndRollBack(QIODevice *device)
    {
        if (device) {
            device->startTransaction();
        }
        auto ok = initForReading(device);
        if (device) {
            device->rollbackTransaction();
        }
        return ok;
    }

    /*!
     * \brief jxrFormat
     * \return The JXR format.
     */
    PKPixelFormatGUID jxrFormat() const
    {
        PKPixelFormatGUID pixelFormatGUID = GUID_PKPixelFormatUndefined;
        if (pDecoder == nullptr) {
            return pixelFormatGUID;
        }
        if (auto err = pDecoder->GetPixelFormat(pDecoder, &pixelFormatGUID)) {
            qCCritical(LOG_JXRPLUGIN) << "JXRHandlerPrivate::jxrFormat() error while getting pixel format:" << err;
            return GUID_PKPixelFormatUndefined;
        }
        return pixelFormatGUID;
    }

    /*!
     * \brief imageFormat
     * Calculate the image format from the JXR format. In conversionFormat it returns the possible conversion format of the JXR to match the returned Qt format.
     * \return The QImage format. If invalid, the image cannot be read.
     */
    QImage::Format imageFormat(PKPixelFormatGUID *conversionFormat = nullptr) const
    {
        PKPixelFormatGUID tmp;
        if (conversionFormat == nullptr) {
            conversionFormat = &tmp;
        }
        *conversionFormat = GUID_PKPixelFormatUndefined;

        auto jxrfmt = jxrFormat();
        auto qtFormat = exactFormat(jxrfmt);
        if (qtFormat != QImage::Format_Invalid) {
            return qtFormat;
        }

        // *** MCH could be RGB, CMYK ***
        qtFormat = multichannelFormat(jxrfmt, colorSpace());
        if (qtFormat != QImage::Format_Invalid) {
            return qtFormat;
        }

        // *** CONVERSION WITH THE SAME DEPTH ***
        // IMPORTANT: For supported conversions see JXRGluePFC.c

        // 32-bit
        if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppBGR)) {
            *conversionFormat = GUID_PKPixelFormat24bppRGB;
            return QImage::Format_RGB888;
        };
        if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppBGRA)) {
            *conversionFormat = GUID_PKPixelFormat32bppRGBA;
            return QImage::Format_RGBA8888;
        };
        if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppPBGRA)) {
            *conversionFormat = GUID_PKPixelFormat32bppPRGBA;
            return QImage::Format_RGBA8888_Premultiplied;
        };

#ifndef JXR_DENY_FLOAT_IMAGE
        if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat128bppRGBAFixedPoint)) {
            *conversionFormat = GUID_PKPixelFormat128bppRGBAFloat;
            return QImage::Format_RGBA32FPx4;
        };
#endif // !JXR_DENY_FLOAT_IMAGE

        //  *** CONVERSION TO A LOWER DEPTH ***
        // IMPORTANT: For supported conversions see JXRGluePFC.c

#ifndef JXR_DISABLE_DEPTH_CONVERSION

#ifndef JXR_DENY_FLOAT_IMAGE
        // RGB FLOAT
        if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat96bppRGBFloat)) {
            *conversionFormat = GUID_PKPixelFormat64bppRGBHalf;
            return QImage::Format_RGBX16FPx4;
        };
#endif // !JXR_DENY_FLOAT_IMAGE

        // RGBA
        // clang-format off
        if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat64bppRGBAHalf) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat64bppRGBAFixedPoint) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat128bppRGBAFixedPoint) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat128bppRGBAFloat)) {

            *conversionFormat = GUID_PKPixelFormat32bppRGBA;
            return QImage::Format_RGBA8888;
        };
        // clang-format on

        // RGB
        // clang-format off
        if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat128bppRGBFloat) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat96bppRGBFloat) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat64bppRGBFixedPoint) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat96bppRGBFixedPoint) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat128bppRGBFixedPoint) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat48bppRGBHalf) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat64bppRGBHalf) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat48bppRGBFixedPoint) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppRGB101010) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat48bppRGB) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppRGBE) ) {

            *conversionFormat = GUID_PKPixelFormat24bppRGB;
            return QImage::Format_RGB888;
        };
        // clang-format on

        // Gray
        // clang-format off
        if (IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppGrayFloat) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat16bppGrayFixedPoint) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat32bppGrayFixedPoint) ||
            IsEqualGUID(jxrfmt, GUID_PKPixelFormat16bppGrayHalf)) {

            *conversionFormat = GUID_PKPixelFormat8bppGray;
            return QImage::Format_Grayscale8;
        };
        // clang-format on
#endif // !JXR_DISABLE_DEPTH_CONVERSION

        return QImage::Format_Invalid;
    }

    /*!
     * \brief imageSize
     * \return The image size in pixels.
     */
    QSize imageSize() const
    {
        if (pDecoder == nullptr) {
            return {};
        }
        qint32 w = 0, h = 0;
        if (auto err = pDecoder->GetSize(pDecoder, &w, &h)) {
            qCCritical(LOG_JXRPLUGIN) << "JXRHandlerPrivate::imageSize() error while getting the image size:" << err;
            return {};
        }
        if (w > JXR_MAX_IMAGE_WIDTH || h > JXR_MAX_IMAGE_HEIGHT || w < 1 || h < 1) {
            qCCritical(LOG_JXRPLUGIN) << "JXRHandlerPrivate::imageSize() Maximum image size is limited to" << JXR_MAX_IMAGE_WIDTH << "x"
                                      << JXR_MAX_IMAGE_HEIGHT << "pixels";
            return {};
        }
        return QSize(w, h);
    }

    /*!
     * \brief colorSpace
     * \return The ICC profile if exists.
     */
    QColorSpace colorSpace() const
    {
        QColorSpace cs;
        if (pDecoder == nullptr) {
            return cs;
        }
        quint32 size = 0;
        if (!pDecoder->GetColorContext(pDecoder, nullptr, &size) && size > 0 && size < kMaxQVectorSize) {
            QByteArray ba(size, 0);
            if (!pDecoder->GetColorContext(pDecoder, reinterpret_cast<quint8 *>(ba.data()), &size)) {
                cs = QColorSpace::fromIccProfile(ba);
            }
        }
        return cs;
    }

    /*!
     * \brief xmpData
     * \return The XMP data if exists.
     */
    QString xmpData() const
    {
        QString xmp;
        if (pDecoder == nullptr) {
            return xmp;
        }
#ifdef JXR_ENABLE_ADVANCED_METADATA
        quint32 size = 0;
        if (!PKImageDecode_GetXMPMetadata_WMP(pDecoder, nullptr, &size) && size > 0 && size < JXR_MAX_METADATA_SIZE) {
            QByteArray ba(size, 0);
            if (!PKImageDecode_GetXMPMetadata_WMP(pDecoder, reinterpret_cast<quint8 *>(ba.data()), &size)) {
                xmp = QString::fromUtf8(ba);
            }
        }
#endif
        return xmp;
    }

    /*!
     * \brief exifData
     * \return The EXIF data.
     */
    MicroExif exifData() const
    {
        return m_exif;
    }

    /*!
     * \brief setMetadata
     * Set the metadata into \a image
     * \param image Image on which to write metadata
     */
    void setMetadata(QImage& image)
    {
        auto xmp = xmpData();
        if (!xmp.isEmpty()) {
            image.setText(QStringLiteral(META_KEY_XMP_ADOBE), xmp);
        }
        auto descr = description();
        if (!descr.isEmpty()) {
            image.setText(QStringLiteral(META_KEY_DESCRIPTION), descr);
        }
        auto softw = software();
        if (!softw.isEmpty()) {
            image.setText(QStringLiteral(META_KEY_SOFTWARE), softw);
        }
        auto make = cameraMake();
        if (!make.isEmpty()) {
            image.setText(QStringLiteral(META_KEY_MANUFACTURER), make);
        }
        auto model = cameraModel();
        if (!model.isEmpty()) {
            image.setText(QStringLiteral(META_KEY_MODEL), model);
        }
        auto author = artist();
        if (!author.isEmpty()) {
            image.setText(QStringLiteral(META_KEY_AUTHOR), author);
        }
        auto copy = copyright();
        if (!copy.isEmpty()) {
            image.setText(QStringLiteral(META_KEY_COPYRIGHT), copy);
        }
        auto capt = caption();
        if (!capt.isEmpty()) {
            image.setText(QStringLiteral(META_KEY_TITLE), capt);
        }
        auto host = hostComputer();
        if (!host.isEmpty()) {
            image.setText(QStringLiteral(META_KEY_HOSTCOMPUTER), host);
        }
        auto docn = documentName();
        if (!docn.isEmpty()) {
            image.setText(QStringLiteral(META_KEY_DOCUMENTNAME), docn);
        }
        auto exif = exifData();
        if (!exif.isEmpty()) {
            exif.updateImageMetadata(image);
        }
    }

#define META_TEXT(name, key)                                                                                                                                   \
    QString name() const                                                                                                                                       \
    {                                                                                                                                                          \
        readTextMeta();                                                                                                                                        \
        return m_txtMeta.value(QStringLiteral(key));                                                                                                           \
    }

    META_TEXT(description, META_KEY_DESCRIPTION)
    META_TEXT(cameraMake, META_KEY_MANUFACTURER)
    META_TEXT(cameraModel, META_KEY_MODEL)
    META_TEXT(software, META_KEY_SOFTWARE)
    META_TEXT(artist, META_KEY_AUTHOR)
    META_TEXT(copyright, META_KEY_COPYRIGHT)
    META_TEXT(caption, META_KEY_TITLE)
    META_TEXT(documentName, META_KEY_DOCUMENTNAME)
    META_TEXT(hostComputer, META_KEY_HOSTCOMPUTER)

#undef META_TEXT

    /* *** WRITE *** */

    /*!
     * \brief initForWriting
     * Initialize the stream for writing.
     * \return True on success, otherwise false.
     */
    bool initForWriting()
    {
        // I have to use QFile because, on Windows, the QTemporary file is locked (even if I close it)
        auto fileName = QStringLiteral("%1.jxr").arg(m_tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)));
        QSharedPointer<QFile> file(new QFile(fileName));
        m_jxrFile = file;
        return initEncoder();
    }

    /*!
     * \brief finalizeWriting
     * \param device
     * Finalize the writing operation. Must be called as last peration.
     * \return True on success, otherwise false.
     */
    bool finalizeWriting(QIODevice *device)
    {
        if (device == nullptr || pEncoder == nullptr) {
            return false;
        }
        if (auto err = pEncoder->Terminate(pEncoder)) {
            qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::finalizeWriting() error while terminating the encoder:" << err;
            return false;
        }
        if (auto err = pEncoder->Release(&pEncoder)) {
            qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::finalizeWriting() error while releasing the encoder:" << err;
            return false;
        }

        if (!deviceCopy(device, m_jxrFile.data())) {
            qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::finalizeWriting() error while writing in the target device";
            return false;
        }
        return true;
    }

    /*!
     * \brief imageToSave
     * If necessary it converts the image to be saved into the appropriate format otherwise it does nothing.
     * \param source The image to save.
     * \return The image to use for save operation.
     */
    QImage imageToSave(const QImage &source) const
    {
        // IMPORTANT: these values must be in exactMatchingFormat()
        // clang-format off
        auto valid = QSet<QImage::Format>()
            << QImage::Format_CMYK8888
#ifndef JXR_DENY_FLOAT_IMAGE
            << QImage::Format_RGBA16FPx4
            << QImage::Format_RGBX16FPx4
            << QImage::Format_RGBA32FPx4
            << QImage::Format_RGBA32FPx4_Premultiplied
            << QImage::Format_RGBX32FPx4
#endif // JXR_DENY_FLOAT_IMAGE
            << QImage::Format_RGBA64
            << QImage::Format_RGBA64_Premultiplied
            << QImage::Format_RGBA8888
            << QImage::Format_RGBA8888_Premultiplied
            << QImage::Format_RGBX8888
            << QImage::Format_BGR888
            << QImage::Format_RGB888
            << QImage::Format_RGB555
            << QImage::Format_RGB16
            << QImage::Format_Grayscale16
            << QImage::Format_Grayscale8
            << QImage::Format_Mono;
        // clang-format on

        // To avoid complex code, I will save only integer formats.
        auto qi = source;
        if (qi.format() == QImage::Format_MonoLSB) {
            qi = qi.convertToFormat(QImage::Format_Mono);
        }
        if (qi.format() == QImage::Format_Indexed8) {
            if (qi.allGray())
                qi = qi.convertToFormat(QImage::Format_Grayscale8);
            else
                qi = qi.convertToFormat(QImage::Format_RGBA8888);
        }
#ifndef JXR_DENY_FLOAT_IMAGE
        if (qi.format() == QImage::Format_RGBA16FPx4_Premultiplied) {
            qi = qi.convertToFormat(QImage::Format_RGBA16FPx4);
        }
#endif // JXR_DENY_FLOAT_IMAGE

        // generic
        if (!valid.contains(qi.format())) {
            auto alpha = qi.hasAlphaChannel();
            auto depth = qi.depth();
            if (depth >= 12 && depth <= 24 && !alpha) {
                qi = qi.convertToFormat(QImage::Format_RGB888);
            } else if (depth >= 48) {
                // JXR don't have RGBX64 format so I have two possibilities:
                // - convert to 32 bpp (convertToFormat(alpha ? QImage::Format_RGBA64 : QImage::Format_RGB888))
                // - convert to 64 bpp with fake alpha (preferred)
                qi = qi.convertToFormat(QImage::Format_RGBA64);
            } else {
                qi = qi.convertToFormat(alpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888);
            }
#ifndef JXR_DENY_FLOAT_IMAGE
            // clang-format off
        } else if (qi.format() == QImage::Format_RGBA16FPx4 ||
                   qi.format() == QImage::Format_RGBX16FPx4 ||
                   qi.format() == QImage::Format_RGBA32FPx4 ||
                   qi.format() == QImage::Format_RGBA32FPx4_Premultiplied ||
                   qi.format() == QImage::Format_RGBX32FPx4) {
            // clang-format on
            auto cs = qi.colorSpace();
            if (cs.isValid() && cs.transferFunction() != QColorSpace::TransferFunction::Linear) {
                qi = qi.convertedToColorSpace(QColorSpace(QColorSpace::SRgbLinear));
            }
        }
#endif // JXR_DENY_FLOAT_IMAGE

        return qi;
    }

    /*!
     * \brief initCodecParameters
     * Initialize the JXR codec parameters.
     * \param wmiSCP
     * \param image The image to save.
     * \return True on success, otherwise false.
     */
    bool initCodecParameters(CWMIStrCodecParam *wmiSCP, const QImage &image)
    {
        if (wmiSCP == nullptr || image.isNull()) {
            return false;
        }
        memset(wmiSCP, 0, sizeof(CWMIStrCodecParam));

        auto fmt = image.format();

        wmiSCP->bVerbose = FALSE;
        if (fmt == QImage::Format_Grayscale8 || fmt == QImage::Format_Grayscale16 || fmt == QImage::Format_Mono)
            wmiSCP->cfColorFormat = Y_ONLY;
        else if (fmt == QImage::Format_CMYK8888)
            wmiSCP->cfColorFormat = CMYK;
        else
            wmiSCP->cfColorFormat = YUV_444;
        wmiSCP->bdBitDepth = BD_LONG;
        wmiSCP->bfBitstreamFormat = FREQUENCY;
        wmiSCP->bProgressiveMode = TRUE;
        wmiSCP->olOverlap = OL_ONE;
        wmiSCP->cNumOfSliceMinus1H = wmiSCP->cNumOfSliceMinus1V = 0;
        wmiSCP->sbSubband = SB_ALL;
        wmiSCP->uAlphaMode = image.hasAlphaChannel() ? 2 : 0;
        if (quality() > -1) {
            wmiSCP->uiDefaultQPIndex = qBound(0, 100 - quality(), 100);
        }
        return true;
    }

    /*!
     * \brief updateTextMetadata
     * Read the metadata from the image and set it in the encoder.
     * \param image The image to save.
     */
    void updateTextMetadata(const QImage &image)
    {
        if (pEncoder == nullptr) {
            return;
        }

        DESCRIPTIVEMETADATA meta;
        memset(&meta, 0, sizeof(meta));

#define META_CTEXT(name, field)                                                                                                                                \
    auto field = image.text(QStringLiteral(name)).toUtf8();                                                                                                    \
    if (!field.isEmpty()) {                                                                                                                                    \
        meta.field.vt = DPKVT_LPSTR;                                                                                                                           \
        meta.field.VT.pszVal = field.data();                                                                                                                   \
    }
#define META_WTEXT(name, field)                                                                                                                                \
    auto field = image.text(QStringLiteral(name));                                                                                                             \
    if (!field.isEmpty()) {                                                                                                                                    \
        meta.field.vt = DPKVT_LPWSTR;                                                                                                                          \
        meta.field.VT.pwszVal = const_cast<quint16 *>(field.utf16());                                                                                          \
    }

        META_CTEXT(META_KEY_DESCRIPTION, pvarImageDescription)
        META_CTEXT(META_KEY_MANUFACTURER, pvarCameraMake)
        META_CTEXT(META_KEY_MODEL, pvarCameraModel)
        META_CTEXT(META_KEY_AUTHOR, pvarArtist)
        META_CTEXT(META_KEY_COPYRIGHT, pvarCopyright)
        META_CTEXT(META_KEY_DOCUMENTNAME, pvarDocumentName)
        META_CTEXT(META_KEY_HOSTCOMPUTER, pvarHostComputer)
        META_WTEXT(META_KEY_TITLE, pvarCaption)

#undef META_CTEXT
#undef META_WTEXT

        // Software must be updated
        auto software = QStringLiteral("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8();
        if (!software.isEmpty()) {
            meta.pvarSoftware.vt = DPKVT_LPSTR;
            meta.pvarSoftware.VT.pszVal = software.data();
        }

        // Date and Time (TIFF format)
        auto cDate = QDateTime::fromString(image.text(QStringLiteral(META_KEY_MODIFICATIONDATE)), Qt::ISODate);
        auto sDate = cDate.isValid() ? cDate.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")).toLatin1() : QByteArray();
        if (!sDate.isEmpty()) {
            meta.pvarDateTime.vt = DPKVT_LPSTR;
            meta.pvarDateTime.VT.pszVal = sDate.data();
        }

        auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
        if (!xmp.isNull()) {
            if (auto err = PKImageEncode_SetXMPMetadata_WMP(pEncoder, reinterpret_cast<const quint8 *>(xmp.constData()), xmp.size())) {
                qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting XMP data:" << err;
            }
        }

        auto exif = MicroExif::fromImage(image);
        if (!exif.isEmpty()) {
            auto exifIfd = exif.exifIfdByteArray(QDataStream::LittleEndian, MicroExif::V2);
            if (auto err = PKImageEncode_SetEXIFMetadata_WMP(pEncoder, reinterpret_cast<const quint8 *>(exifIfd.constData()), exifIfd.size())) {
                qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting EXIF data:" << err;
            }
            auto gpsIfd = exif.gpsIfdByteArray(QDataStream::LittleEndian, MicroExif::V2);
            if (auto err = PKImageEncode_SetGPSInfoMetadata_WMP(pEncoder, reinterpret_cast<const quint8 *>(gpsIfd.constData()), gpsIfd.size())) {
                qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting GPS data:" << err;
            }
        }

        if (auto err = pEncoder->SetDescriptiveMetadata(pEncoder, &meta)) {
            qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting descriptive data:" << err;
        }
    }

    /*!
     * \brief exactFormat
     * JXR and Qt use support image formats, some of which are identical. Use this function to convert a JXR format to Qt format.
     * \param jxrFormat Format to be converted.
     * \return A valid Qt format or QImage::Format_Invalid if there is no match
     */
    static QImage::Format exactFormat(const PKPixelFormatGUID &jxrFormat)
    {
        auto l = exactMatchingFormats();
        for (auto &&p : l) {
            if (IsEqualGUID(p.second, jxrFormat))
                return p.first;
        }
        return QImage::Format_Invalid;
    }

    /*!
     * \brief exactFormat
     * JXR and Qt use support image formats, some of which are identical. Use this function to convert a JXR format to Qt format.
     * \param qtFormat Format to be converted.
     * \return A valid JXR format or GUID_PKPixelFormatUndefined if there is no match
     */
    static PKPixelFormatGUID exactFormat(const QImage::Format &qtFormat)
    {
        auto l = exactMatchingFormats();
        for (auto &&p : l) {
            if (p.first == qtFormat)
                return p.second;
        }
        return GUID_PKPixelFormatUndefined;
    }

    /*!
     * \brief multichannelFormat
     * I can only decide how to interpret multichannels by checking the color profile.
     * If it's not present, I assume CMYK for 4 channels and RGB for 3 channels (like
     * Windows does).
     * \param jxrFormat Format to be converted.
     * \param cs The color space of the image.
     * \return A valid Qt format or QImage::Format_Invalid if there is no match
     */
    static QImage::Format multichannelFormat(const PKPixelFormatGUID &jxrFormat, const QColorSpace& cs)
    {
        auto model = QColorSpace::ColorModel::Undefined;
        if (cs.isValid()) {
            model = cs.colorModel();
        } else if (!cs.iccProfile().isEmpty()) {
            model = QColorSpace::ColorModel::Gray; // means invalid
        }

        if (IsEqualGUID(GUID_PKPixelFormat24bpp3Channels, jxrFormat)) {
            if (model == QColorSpace::ColorModel::Rgb || model == QColorSpace::ColorModel::Undefined)
                return QImage::Format_RGB888;
        }

        if (IsEqualGUID(GUID_PKPixelFormat32bpp4Channels, jxrFormat)) {
            if (model == QColorSpace::ColorModel::Cmyk || model == QColorSpace::ColorModel::Undefined)
                return QImage::Format_CMYK8888;
        }

        if (IsEqualGUID(GUID_PKPixelFormat32bpp3ChannelsAlpha, jxrFormat)) {
            if (model == QColorSpace::ColorModel::Rgb || model == QColorSpace::ColorModel::Undefined)
                return QImage::Format_RGBA8888;
        }

        if (IsEqualGUID(GUID_PKPixelFormat64bpp3ChannelsAlpha, jxrFormat)) {
            if (model == QColorSpace::ColorModel::Rgb || model == QColorSpace::ColorModel::Undefined)
                return QImage::Format_RGBA64;
        }

        return QImage::Format_Invalid;
    }

private:
    static QList<std::pair<QImage::Format, PKPixelFormatGUID>> exactMatchingFormats()
    {
        // clang-format off
        auto list = QList<std::pair<QImage::Format, PKPixelFormatGUID>>()
#ifndef JXR_DENY_FLOAT_IMAGE
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA16FPx4, GUID_PKPixelFormat64bppRGBAHalf)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBX16FPx4, GUID_PKPixelFormat64bppRGBHalf)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA32FPx4, GUID_PKPixelFormat128bppRGBAFloat)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA32FPx4_Premultiplied, GUID_PKPixelFormat128bppPRGBAFloat)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBX32FPx4, GUID_PKPixelFormat128bppRGBFloat)
#endif // JXR_DENY_FLOAT_IMAGE
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_CMYK8888, GUID_PKPixelFormat32bppCMYK)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_CMYK8888, GUID_PKPixelFormat32bppCMYKDIRECT)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_Mono, GUID_PKPixelFormatBlackWhite)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_Grayscale8, GUID_PKPixelFormat8bppGray)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_Grayscale16, GUID_PKPixelFormat16bppGray)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB555, GUID_PKPixelFormat16bppRGB555)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB16, GUID_PKPixelFormat16bppRGB565)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_BGR888, GUID_PKPixelFormat24bppBGR)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGB888, GUID_PKPixelFormat24bppRGB)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBX8888, GUID_PKPixelFormat32bppRGB)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA8888, GUID_PKPixelFormat32bppRGBA)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA8888_Premultiplied, GUID_PKPixelFormat32bppPRGBA)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA64, GUID_PKPixelFormat64bppRGBA)
            << std::pair<QImage::Format, PKPixelFormatGUID>(QImage::Format_RGBA64_Premultiplied, GUID_PKPixelFormat64bppPRGBA);
        // clang-format on
        return list;
    }

    bool deviceCopy(QIODevice *target, QIODevice *source)
    {
        if (target == nullptr || source == nullptr) {
            return false;
        }
        auto isTargetOpen = target->isOpen();
        if (!isTargetOpen && !target->open(QIODevice::WriteOnly)) {
            return false;
        }
        auto isSourceOpen = source->isOpen();
        if (!isSourceOpen && !source->open(QIODevice::ReadOnly)) {
            return false;
        }
        QByteArray buff(32768 * 4, char());
        for (;;) {
            auto read = source->read(buff.data(), buff.size());
            if (read == 0) {
                break;
            }
            if (read < 0) {
                return false;
            }
            if (target->write(buff.data(), read) != read) {
                return false;
            }
        }
        if (!isSourceOpen) {
            source->close();
        }
        if (!isTargetOpen) {
            target->close();
        }
        return true;
    }

    bool readDevice(QIODevice *device)
    {
        if (device == nullptr) {
            return false;
        }
        if (!m_jxrFile.isNull()) {
            return true;
        }
        // I have to use QFile because, on Windows, the QTemporary file is locked (even if I close it)
        auto fileName = QStringLiteral("%1.jxr").arg(m_tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)));
        QSharedPointer<QFile> file(new QFile(fileName));
        if (!file->open(QFile::WriteOnly)) {
            return false;
        }
        if (!deviceCopy(file.data(), device)) {
            qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::readDevice() error while writing in the target device";
            return false;
        }
        file->close();
        m_exif = MicroExif::fromDevice(file.data());
        m_jxrFile = file;
        return true;
    }

    bool initDecoder()
    {
        if (pDecoder) {
            return true;
        }
        if (pCodecFactory == nullptr) {
            return false;
        }
        if (auto err = pCodecFactory->CreateDecoderFromFile(qUtf8Printable(fileName()), &pDecoder)) {
            qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::initDecoder() unable to create decoder:" << err;
            return false;
        }
        setTransformation(JXRHandlerPrivate::orientationToTransformation(pDecoder->WMP.wmiI.oOrientation));
        pDecoder->WMP.wmiI.oOrientation = O_NONE; // disable the library rotation application
        return true;
    }

    bool initEncoder()
    {
        if (pDecoder) {
            return true;
        }
        if (pCodecFactory == nullptr) {
            return false;
        }
        if (auto err = pCodecFactory->CreateCodec(&IID_PKImageWmpEncode, (void **)&pEncoder)) {
            qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::initEncoder() unable to create encoder:" << err;
            return false;
        }
        pEncoder->WMP.oOrientation = JXRHandlerPrivate::transformationToOrientation(transformation());
        return true;
    }

    bool readTextMeta() const {
        if (pDecoder == nullptr) {
            return false;
        }
        if (!m_txtMeta.isEmpty()) {
            return true;
        }

        DESCRIPTIVEMETADATA meta = {};
        if (pDecoder->GetDescriptiveMetadata(pDecoder, &meta)) {
            return false;
        }

#define META_TEXT(name, field)                                                                                                                                 \
    if (meta.field.vt == DPKVT_LPSTR)                                                                                                                          \
        m_txtMeta.insert(QStringLiteral(name), QString::fromUtf8(meta.field.VT.pszVal));                                                                       \
    else if (meta.field.vt == DPKVT_LPWSTR)                                                                                                                    \
        m_txtMeta.insert(QStringLiteral(name), QString::fromUtf16(reinterpret_cast<char16_t *>(meta.field.VT.pwszVal)));

        META_TEXT(META_KEY_DESCRIPTION, pvarImageDescription)
        META_TEXT(META_KEY_MANUFACTURER, pvarCameraMake)
        META_TEXT(META_KEY_MODEL, pvarCameraModel)
        META_TEXT(META_KEY_SOFTWARE, pvarSoftware)
        META_TEXT(META_KEY_AUTHOR, pvarArtist)
        META_TEXT(META_KEY_COPYRIGHT, pvarCopyright)
        META_TEXT(META_KEY_TITLE, pvarCaption)
        META_TEXT(META_KEY_DOCUMENTNAME, pvarDocumentName)
        META_TEXT(META_KEY_HOSTCOMPUTER, pvarHostComputer)

#undef META_TEXT

        return true;
    }
};

bool JXRHandler::read(QImage *outImage)
{
    if (!d->initForReading(device())) {
        return false;
    }

    PKPixelFormatGUID convFmt;
    auto imageFmt = d->imageFormat(&convFmt);
    auto img = imageAlloc(d->imageSize(), imageFmt, ImageInitToZero::FPOnly);
    if (img.isNull()) {
        return false;
    }

    // resolution
    float hres = 0, vres = 0;
    if (auto err = d->pDecoder->GetResolution(d->pDecoder, &hres, &vres)) {
        qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() error while reading resolution:" << err;
    } else {
        const qint32 hdpm = dpi2ppm(hres);
        if (hdpm > 0) {
            img.setDotsPerMeterX(hdpm);
        }
        const qint32 vdpm = dpi2ppm(vres);
        if (vdpm > 0) {
            img.setDotsPerMeterY(vdpm);
        }
    }

    // alpha copy mode
    if (img.hasAlphaChannel()) {
        d->pDecoder->WMP.wmiSCP.uAlphaMode = 2; // or 1 (?)
    }

    PKRect rect = {0, 0, img.width(), img.height()};
    if (IsEqualGUID(convFmt, GUID_PKPixelFormatUndefined)) { // direct storing
        if (auto err = d->pDecoder->Copy(d->pDecoder, &rect, img.bits(), img.bytesPerLine())) {
            qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to copy data:" << err;
            return false;
        }
    } else { // conversion to a known format
        PKFormatConverter *pConverter = nullptr;
        if (auto err = d->pCodecFactory->CreateFormatConverter(&pConverter)) {
            qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to create the converter:" << err;
            return false;
        }
        if (auto err = pConverter->Initialize(pConverter, d->pDecoder, nullptr, convFmt)) {
            qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to initialize the converter:" << err;
            if (auto err = pConverter->Release(&pConverter)) {
                qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() error while releasing the converter:" << err;
            }
            return false;
        }
        if (d->pDecoder->WMP.wmiI.cBitsPerUnit == size_t(img.depth())) { // in place conversion
            if (auto err = pConverter->Copy(pConverter, &rect, img.bits(), img.bytesPerLine())) {
                qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to copy converted data:" << err;
                if (auto err = pConverter->Release(&pConverter)) {
                    qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() error while releasing the converter:" << err;
                }
                return false;
            }
        } else { // additional buffer needed
            qint64 convStrideSize = (img.width() * d->pDecoder->WMP.wmiI.cBitsPerUnit + 7) / 8;
            qint64 buffSize = convStrideSize * img.height();
            qint64 limit = QImageReader::allocationLimit();
            if (limit && (buffSize + img.sizeInBytes()) > limit * 1024 * 1024) {
                qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to covert due to allocation limit set:" << limit << "MiB";
                if (auto err = pConverter->Release(&pConverter)) {
                    qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() error while releasing the converter:" << err;
                }
                return false;
            }
            QVector<quint8> ba(buffSize);
            if (auto err = pConverter->Copy(pConverter, &rect, ba.data(), convStrideSize)) {
                qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to copy converted data:" << err;
                if (auto err = pConverter->Release(&pConverter)) {
                    qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() error while releasing the converter:" << err;
                }
                return false;
            }
            for (qint32 y = 0, h = img.height(); y < h; ++y) {
                std::memcpy(img.scanLine(y), ba.data() + convStrideSize * y, (std::min)(convStrideSize, qint64(img.bytesPerLine())));
            }
        }
        if (auto err = pConverter->Release(&pConverter)) {
            qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() error while releasing the converter:" << err;
        }
    }

    // Metadata (e.g.: icc profile, description, etc...)
    img.setColorSpace(d->colorSpace());
    d->setMetadata(img);

#ifndef JXR_DENY_FLOAT_IMAGE
    // JXR float are stored in scRGB.
    if (img.format() == QImage::Format_RGBX16FPx4 || img.format() == QImage::Format_RGBA16FPx4 || img.format() == QImage::Format_RGBA16FPx4_Premultiplied ||
        img.format() == QImage::Format_RGBX32FPx4 || img.format() == QImage::Format_RGBA32FPx4 || img.format() == QImage::Format_RGBA32FPx4_Premultiplied) {
        auto hasAlpha = img.hasAlphaChannel();
        for (qint32 y = 0, h = img.height(); y < h; ++y) {
            if (img.depth() == 64) {
                auto line = reinterpret_cast<qfloat16 *>(img.scanLine(y));
                for (int x = 0, w = img.width() * 4; x < w; x += 4)
                    line[x + 3] = hasAlpha ? std::clamp(line[x + 3], qfloat16(0), qfloat16(1)) : qfloat16(1);
            } else {
                auto line = reinterpret_cast<float *>(img.scanLine(y));
                for (int x = 0, w = img.width() * 4; x < w; x += 4)
                    line[x + 3] = hasAlpha ? std::clamp(line[x + 3], float(0), float(1)) : float(1);
            }
        }
        if (!img.colorSpace().isValid()) {
            img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
        }
    }
#endif

    *outImage = img;
    return true;
}

bool JXRHandler::write(const QImage &image)
{
    // JXR is stored in a TIFF V6 container that is limited to 4GiB. The size
    // is limited to 4GB to leave room for IFDs, Metadata, etc...
    if (qint64(image.sizeInBytes()) > 4000000000ll) {
        qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() image too large: the image cannot exceed 4GB.";
        return false;
    }
    if (image.width() > JXR_MAX_IMAGE_WIDTH || image.height() > JXR_MAX_IMAGE_HEIGHT) {
        qCCritical(LOG_JXRPLUGIN) << "JXRHandler::write() Maximum image size is limited to" << JXR_MAX_IMAGE_WIDTH << "x" << JXR_MAX_IMAGE_HEIGHT << "pixels";
        return {};
    }

    if (!d->initForWriting()) {
        return false;
    }
    struct WMPStream *pEncodeStream = nullptr;
    if (auto err = d->pFactory->CreateStreamFromFilename(&pEncodeStream, qUtf8Printable(d->fileName()), "wb")) {
        qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() unable to create stream:" << err;
        return false;
    }

    // convert the image to a supported format
    auto qi = d->imageToSave(image);
    auto jxlfmt = d->exactFormat(qi.format());
    if (IsEqualGUID(jxlfmt, GUID_PKPixelFormatUndefined)) {
        qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() something wrong when calculating the target format for" << qi.format();
        return false;
    }
#ifndef JXR_DISABLE_BGRA_HACK
    if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppRGB)) {
        jxlfmt = GUID_PKPixelFormat32bppBGR;
        qi.rgbSwap();
    }
    if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppRGBA)) {
        jxlfmt = GUID_PKPixelFormat32bppBGRA;
        qi.rgbSwap();
    }
    if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppPRGBA)) {
        jxlfmt = GUID_PKPixelFormat32bppPBGRA;
        qi.rgbSwap();
    }
#endif

    // initialize the codec parameters
    CWMIStrCodecParam wmiSCP;
    if (!d->initCodecParameters(&wmiSCP, qi)) {
        qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() something wrong when calculating encoder parameters for" << qi.format();
        return false;
    }
    if (auto err = d->pEncoder->Initialize(d->pEncoder, pEncodeStream, &wmiSCP, sizeof(wmiSCP))) {
        qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while initializing the encoder:" << err;
        return false;
    }

    // setting mandatory image info
    if (auto err = d->pEncoder->SetPixelFormat(d->pEncoder, jxlfmt)) {
        qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting the image format:" << err;
        return false;
    }
    if (auto err = d->pEncoder->SetSize(d->pEncoder, qi.width(), qi.height())) {
        qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting the image size:" << err;
        return false;
    }
    if (auto err = d->pEncoder->SetResolution(d->pEncoder, dppm2dpi(qi.dotsPerMeterX()), dppm2dpi(qi.dotsPerMeterY()))) {
        qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting the image resolution:" << err;
        return false;
    }

    // setting metadata (a failure of setting metadata doesn't stop the encoding)
    auto cs = qi.colorSpace().iccProfile();
    if (!cs.isEmpty()) {
        if (auto err = d->pEncoder->SetColorContext(d->pEncoder, reinterpret_cast<quint8 *>(cs.data()), cs.size())) {
            qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting ICC profile:" << err;
        }
    }
    d->updateTextMetadata(image);

    // writing the image
    if (auto err = d->pEncoder->WritePixels(d->pEncoder, qi.height(), qi.bits(), qi.bytesPerLine())) {
        qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while encoding the image:" << err;
        return false;
    }
    if (!d->finalizeWriting(device())) {
        return false;
    }
    return true;
}

void JXRHandler::setOption(ImageOption option, const QVariant &value)
{
    if (option == QImageIOHandler::Quality) {
        auto ok = false;
        auto q = value.toInt(&ok);
        if (ok) {
            d->setQuality(q);
        }
    }

    if (option == QImageIOHandler::ImageTransformation) {
        auto ok = false;
        auto t = value.toInt(&ok);
        if (ok) {
            d->setTransformation(QImageIOHandler::Transformation(t));
        }
    }
}

bool JXRHandler::supportsOption(ImageOption option) const
{
    if (option == QImageIOHandler::Size) {
        return true;
    }
    if (option == QImageIOHandler::ImageFormat) {
        return true;
    }
    if (option == QImageIOHandler::Quality) {
        return true;
    }
    if (option == QImageIOHandler::ImageTransformation) {
        return true;
    }
    return false;
}

QVariant JXRHandler::option(ImageOption option) const
{
    QVariant v;

    if (option == QImageIOHandler::Size) {
        if (d->initForReadingAndRollBack(device())) {
            auto size = d->imageSize();
            if (size.isValid()) {
                v = QVariant::fromValue(size);
            }
        }
    }

    if (option == QImageIOHandler::ImageFormat) {
        if (d->initForReadingAndRollBack(device())) {
            v = QVariant::fromValue(d->imageFormat());
        }
    }

    if (option == QImageIOHandler::Quality) {
        v = d->quality();
    }

    if (option == QImageIOHandler::ImageTransformation) {
        // ignore result: I might want to read the value set in writing
        d->initForReadingAndRollBack(device());
        v = int(d->transformation());
    }

    return v;
}

JXRHandler::JXRHandler()
    : d(new JXRHandlerPrivate)
{
}

bool JXRHandler::canRead() const
{
    if (canRead(device())) {
        setFormat("jxr");
        return true;
    }
    return false;
}

bool JXRHandler::canRead(QIODevice *device)
{
    if (!device) {
        qCWarning(LOG_JXRPLUGIN) << "JXRHandler::canRead() called with no device";
        return false;
    }

    // JPEG XR image data is stored in TIFF-like container format (II and 0xBC01 version)
    if (device->peek(4) == QByteArray::fromRawData("\x49\x49\xbc\x01", 4)) {
        return true;
    }

    return false;
}

QImageIOPlugin::Capabilities JXRPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
    if (format == "jxr") {
        return Capabilities(CanRead | CanWrite);
    }
    if (format == "wdp" || format == "hdp") {
        return Capabilities(CanRead);
    }
    if (!format.isEmpty()) {
        return {};
    }
    if (!device->isOpen()) {
        return {};
    }

    Capabilities cap;
    if (device->isReadable() && JXRHandler::canRead(device)) {
        cap |= CanRead;
    }
    if (device->isWritable()) {
        cap |= CanWrite;
    }
    return cap;
}

QImageIOHandler *JXRPlugin::create(QIODevice *device, const QByteArray &format) const
{
    QImageIOHandler *handler = new JXRHandler;
    handler->setDevice(device);
    handler->setFormat(format);
    return handler;
}

#include "moc_jxr_p.cpp"
