/*
    High Efficiency Image File Format (HEIF) support for QImage.

    SPDX-FileCopyrightText: 2020 Sirius Bakke <sirius@bakke.co>
    SPDX-FileCopyrightText: 2021 Daniel Novomesky <dnovomesky@gmail.com>

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

#include "heif_p.h"
#include "microexif_p.h"
#include "util_p.h"
#include <libheif/heif.h>
#include <libheif/heif_properties.h>

#include <QColorSpace>
#include <QLoggingCategory>
#include <QPointF>
#include <QSysInfo>

#include <cstring>
#include <limits>

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

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

#ifndef HEIF_DISABLE_QT_TRANSFORMATION
/*!
 * HEIF transformations, in addition to rotations and reflections,
 * also support image cropping. Consequently, the Qt plugin, must
 * also honor the crop. This define is useful in case of problems:
 * activating it disables Qt's support for transformations,
 * delegating them to the HEIF libraries (which will therefore
 * always apply them regardless of what is requested from Qt).
 */
// #define HEIF_DISABLE_QT_TRANSFORMATION
#endif

/* *** HEIF_MAX_IMAGE_WIDTH and HEIF_MAX_IMAGE_HEIGHT ***
 * The maximum size in pixel allowed by the plugin.
 */
#ifndef HEIF_MAX_IMAGE_WIDTH
#define HEIF_MAX_IMAGE_WIDTH KIF_64K_IMAGE_PIXEL_LIMIT
#endif
#ifndef HEIF_MAX_IMAGE_HEIGHT
#define HEIF_MAX_IMAGE_HEIGHT HEIF_MAX_IMAGE_WIDTH
#endif

size_t HEIFHandler::m_initialized_count = 0;
bool HEIFHandler::m_plugins_queried = false;
bool HEIFHandler::m_heif_decoder_available = false;
bool HEIFHandler::m_heif_encoder_available = false;
bool HEIFHandler::m_hej2_decoder_available = false;
bool HEIFHandler::m_hej2_encoder_available = false;
bool HEIFHandler::m_avci_decoder_available = false;

extern "C" {
static struct heif_error heifhandler_write_callback(struct heif_context * /* ctx */, const void *data, size_t size, void *userdata)
{
    heif_error error;
    error.code = heif_error_Ok;
    error.subcode = heif_suberror_Unspecified;
    error.message = "Success";

    if (!userdata || !data || size == 0) {
        error.code = heif_error_Usage_error;
        error.subcode = heif_suberror_Null_pointer_argument;
        error.message = "Wrong parameters!";
        return error;
    }

    QIODevice *ioDevice = static_cast<QIODevice *>(userdata);
    qint64 bytesWritten = ioDevice->write(static_cast<const char *>(data), size);

    if (bytesWritten < static_cast<qint64>(size)) {
        error.code = heif_error_Encoding_error;
        error.message = "Bytes written to QIODevice are smaller than input data size";
        error.subcode = heif_suberror_Cannot_write_output_data;
    }

    return error;
}
}

HEIFHandler::HEIFHandler()
    : m_parseState(ParseHeicNotParsed)
    , m_quality(100)
    , m_orientation(0)
{
}

bool HEIFHandler::canRead() const
{
    if (m_parseState == ParseHeicNotParsed) {
        QIODevice *dev = device();
        if (dev) {
            const QByteArray header = dev->peek(28);

            if (HEIFHandler::isSupportedBMFFType(header)) {
                setFormat("heif");
                return true;
            }

            if (HEIFHandler::isSupportedHEJ2(header)) {
                setFormat("hej2");
                return true;
            }

            if (HEIFHandler::isSupportedAVCI(header)) {
                setFormat("avci");
                return true;
            }
        }
        return false;
    }

    if (m_parseState != ParseHeicError) {
        return true;
    }
    return false;
}

bool HEIFHandler::read(QImage *outImage)
{
    if (!ensureParsed()) {
        return false;
    }

    *outImage = m_current_image;
    return true;
}

bool HEIFHandler::write(const QImage &image)
{
    if (image.format() == QImage::Format_Invalid || image.isNull()) {
        qCWarning(LOG_HEIFPLUGIN) << "No image data to save";
        return false;
    }

    if (image.width() >= HEIF_MAX_IMAGE_WIDTH || image.height() >= HEIF_MAX_IMAGE_HEIGHT) {
        qCWarning(LOG_HEIFPLUGIN) << "Image size invalid:" << image.width() << "x" << image.height();
        return false;
    }

    startHeifLib();

    bool success = write_helper(image);

    finishHeifLib();

    return success;
}

bool HEIFHandler::write_helper(const QImage &image)
{
    int save_depth; // 8 or 10bit per channel
    QImage::Format tmpformat; // format for temporary image
    const bool save_alpha = image.hasAlphaChannel();

    switch (image.format()) {
    case QImage::Format_BGR30:
    case QImage::Format_A2BGR30_Premultiplied:
    case QImage::Format_RGB30:
    case QImage::Format_A2RGB30_Premultiplied:
    case QImage::Format_Grayscale16:
    case QImage::Format_RGBX64:
    case QImage::Format_RGBA64:
    case QImage::Format_RGBA64_Premultiplied:
        save_depth = 10;
        break;
    default:
        if (image.depth() > 32) {
            save_depth = 10;
        } else {
            save_depth = 8;
        }
        break;
    }

    heif_compression_format encoder_codec = heif_compression_HEVC;
    if (format() == "hej2") {
        encoder_codec = heif_compression_JPEG2000;
        save_depth = 8; // for compatibility reasons
    }

    heif_chroma chroma;
    if (save_depth > 8) {
        if (save_alpha) {
            tmpformat = QImage::Format_RGBA64;
            chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBBAA_LE : heif_chroma_interleaved_RRGGBBAA_BE;
        } else {
            tmpformat = QImage::Format_RGBX64;
            chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBB_LE : heif_chroma_interleaved_RRGGBB_BE;
        }
    } else {
        if (save_alpha) {
            tmpformat = QImage::Format_RGBA8888;
            chroma = heif_chroma_interleaved_RGBA;
        } else {
            tmpformat = QImage::Format_RGB888;
            chroma = heif_chroma_interleaved_RGB;
        }
    }

    QImage tmpimage;
    auto cs = image.colorSpace();
    if (cs.isValid() && cs.colorModel() == QColorSpace::ColorModel::Cmyk && image.format() == QImage::Format_CMYK8888) {
        tmpimage = image.convertedToColorSpace(QColorSpace(QColorSpace::SRgb), tmpformat);
    } else if (cs.isValid() && cs.colorModel() == QColorSpace::ColorModel::Gray
               && (image.format() == QImage::Format_Grayscale8 || image.format() == QImage::Format_Grayscale16)) {
        QColorSpace::TransferFunction trc_new = cs.transferFunction();
        float gamma_new = cs.gamma();
        if (trc_new == QColorSpace::TransferFunction::Custom) {
            trc_new = QColorSpace::TransferFunction::SRgb;
        }
        tmpimage = image.convertedToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, trc_new, gamma_new), tmpformat);
    } else {
        tmpimage = image.convertToFormat(tmpformat);
    }

    struct heif_context *context = heif_context_alloc();
    struct heif_error err;
    struct heif_image *h_image = nullptr;

    err = heif_image_create(tmpimage.width(), tmpimage.height(), heif_colorspace_RGB, chroma, &h_image);
    if (err.code) {
        qCWarning(LOG_HEIFPLUGIN) << "heif_image_create error:" << err.message;
        heif_context_free(context);
        return false;
    }

    QByteArray iccprofile = tmpimage.colorSpace().iccProfile();
    if (iccprofile.size() > 0) {
        heif_image_set_raw_color_profile(h_image, "prof", iccprofile.constData(), iccprofile.size());
    }

    heif_image_add_plane(h_image, heif_channel_interleaved, image.width(), image.height(), save_depth);
    int stride = 0;
    uint8_t *const dst = heif_image_get_plane(h_image, heif_channel_interleaved, &stride);
    size_t rowbytes;

    switch (save_depth) {
    case 10:
        if (save_alpha) {
            for (int y = 0; y < tmpimage.height(); y++) {
                const uint16_t *src_word = reinterpret_cast<const uint16_t *>(tmpimage.constScanLine(y));
                uint16_t *dest_word = reinterpret_cast<uint16_t *>(dst + (y * stride));
                for (int x = 0; x < tmpimage.width(); x++) {
                    int tmp_pixelval;
                    // R
                    tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
                    *dest_word = qBound(0, tmp_pixelval, 1023);
                    src_word++;
                    dest_word++;
                    // G
                    tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
                    *dest_word = qBound(0, tmp_pixelval, 1023);
                    src_word++;
                    dest_word++;
                    // B
                    tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
                    *dest_word = qBound(0, tmp_pixelval, 1023);
                    src_word++;
                    dest_word++;
                    // A
                    tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
                    *dest_word = qBound(0, tmp_pixelval, 1023);
                    src_word++;
                    dest_word++;
                }
            }
        } else { // no alpha channel
            for (int y = 0; y < tmpimage.height(); y++) {
                const uint16_t *src_word = reinterpret_cast<const uint16_t *>(tmpimage.constScanLine(y));
                uint16_t *dest_word = reinterpret_cast<uint16_t *>(dst + (y * stride));
                for (int x = 0; x < tmpimage.width(); x++) {
                    int tmp_pixelval;
                    // R
                    tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
                    *dest_word = qBound(0, tmp_pixelval, 1023);
                    src_word++;
                    dest_word++;
                    // G
                    tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
                    *dest_word = qBound(0, tmp_pixelval, 1023);
                    src_word++;
                    dest_word++;
                    // B
                    tmp_pixelval = (int)(((float)(*src_word) / 65535.0f) * 1023.0f + 0.5f);
                    *dest_word = qBound(0, tmp_pixelval, 1023);
                    src_word++;
                    dest_word++;
                    // X
                    src_word++;
                }
            }
        }
        break;
    case 8:
        rowbytes = save_alpha ? (tmpimage.width() * 4) : (tmpimage.width() * 3);
        for (int y = 0; y < tmpimage.height(); y++) {
            memcpy(dst + (y * stride), tmpimage.constScanLine(y), rowbytes);
        }
        break;
    default:
        qCWarning(LOG_HEIFPLUGIN) << "Unsupported depth:" << save_depth;
        heif_image_release(h_image);
        heif_context_free(context);
        return false;
        break;
    }

    struct heif_encoder *encoder = nullptr;
    err = heif_context_get_encoder_for_format(context, encoder_codec, &encoder);
    if (err.code) {
        qCWarning(LOG_HEIFPLUGIN) << "Unable to get an encoder instance:" << err.message;
        heif_image_release(h_image);
        heif_context_free(context);
        return false;
    }

    heif_encoder_set_lossy_quality(encoder, m_quality);
    if (m_quality > 90) {
        if (m_quality == 100) {
            heif_encoder_set_lossless(encoder, true);
        }
        heif_encoder_set_parameter_string(encoder, "chroma", "444");
    }

    struct heif_encoding_options *encoder_options = heif_encoding_options_alloc();
    encoder_options->save_alpha_channel = save_alpha;

    if ((tmpimage.width() % 2 == 1) || (tmpimage.height() % 2 == 1)) {
        qCWarning(LOG_HEIFPLUGIN) << "Image has odd dimension!\nUse even-numbered dimension(s) for better compatibility with other HEIF implementations.";
        if (save_alpha) {
            // This helps to save alpha channel when image has odd dimension
            encoder_options->macOS_compatibility_workaround = 0;
        }
    }

    if (m_orientation >= 1 && m_orientation <= 8) {
        // Function available from HEIF v1.14
        encoder_options->image_orientation = heif_orientation(m_orientation);
    }

    struct heif_image_handle *handle;
    err = heif_context_encode_image(context, h_image, encoder, encoder_options, &handle);

    // exif metadata
    if (err.code == heif_error_Ok) {
        auto exif = MicroExif::fromImage(tmpimage);
        if (m_orientation >= 1 && m_orientation <= 8) {
            // EXIF orientation must be coherent with HEIF orientation
            exif.setOrientation(m_orientation);
        }
        if (!exif.isEmpty()) {
            auto ba = exif.toByteArray();
            err = heif_context_add_exif_metadata(context, handle, ba.constData(), ba.size());
        }
    }
    // xmp metadata
    if (err.code == heif_error_Ok) {
        auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE));
        if (!xmp.isEmpty()) {
            auto ba = xmp.toUtf8();
            err = heif_context_add_XMP_metadata(context, handle, ba.constData(), ba.size());
        }
    }

    if (encoder_options) {
        heif_encoding_options_free(encoder_options);
    }

    if (err.code) {
        qCWarning(LOG_HEIFPLUGIN) << "heif_context_encode_image failed:" << err.message;
        heif_encoder_release(encoder);
        heif_image_release(h_image);
        heif_context_free(context);
        return false;
    }

    struct heif_writer writer;
    writer.writer_api_version = 1;
    writer.write = heifhandler_write_callback;

    err = heif_context_write(context, &writer, device());

    heif_encoder_release(encoder);
    heif_image_release(h_image);

    if (err.code) {
        qCWarning(LOG_HEIFPLUGIN) << "Writing HEIF image failed:" << err.message;
        heif_context_free(context);
        return false;
    }

    heif_context_free(context);
    return true;
}

bool HEIFHandler::read_orientation_helper(void *heif_handle, const void *heif_ctx)
{
    if (heif_handle == nullptr || heif_ctx == nullptr) {
        return false;
    }
    auto handle = reinterpret_cast<heif_image_handle *>(heif_handle);
    auto ctx = reinterpret_cast<const heif_context *>(heif_ctx);
    auto item_id = heif_image_handle_get_item_id(handle);

    // get the properties
    heif_transform_mirror_direction mirror = heif_transform_mirror_direction::heif_transform_mirror_direction_invalid;
    heif_property_id mir_id;
    if (heif_item_get_properties_of_type(ctx, item_id, heif_item_property_type_transform_mirror, &mir_id, 1) > 0) {
        mirror = heif_item_get_property_transform_mirror(ctx, item_id, mir_id);
        if (mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid)
            return false;
    }

    int rotation_ccw = -1;
    heif_property_id rot_id;
    if (heif_item_get_properties_of_type(ctx, item_id, heif_item_property_type_transform_rotation, &rot_id, 1) > 0) {
        rotation_ccw = heif_item_get_property_transform_rotation_ccw(ctx, item_id, rot_id);
        if (rotation_ccw == -1)
            return false;
    }

    if (rotation_ccw == -1 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
        m_orientation = 0;
    } else if (rotation_ccw == 0 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
        m_orientation = 1;
    } else if (rotation_ccw <= 0 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_horizontal) {
        m_orientation = 2;
    } else if (rotation_ccw == 180 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
        m_orientation = 3;
    } else if (rotation_ccw <= 0 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_vertical) {
        m_orientation = 4;
    } else if (rotation_ccw == 270 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_horizontal) {
        m_orientation = 5;
    } else if (rotation_ccw == 270 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
        m_orientation = 6;
    } else if (rotation_ccw == 270 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_vertical) {
        m_orientation = 7;
    } else if (rotation_ccw == 90 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
        m_orientation = 8;
    }

    return true;
}

bool HEIFHandler::read_crop(void *heif_handle, const void *heif_ctx, const QSize &size, QRect &crop)
{
    if (heif_handle == nullptr || heif_ctx == nullptr) {
        return false;
    }
    auto handle = reinterpret_cast<heif_image_handle *>(heif_handle);
    auto ctx = reinterpret_cast<const heif_context *>(heif_ctx);
    auto item_id = heif_image_handle_get_item_id(handle);

    heif_property_id crop_id;
    if (heif_item_get_properties_of_type(ctx, item_id, heif_item_property_type_transform_crop, &crop_id, 1) > 0) {
        int l = 0, t = 0, r = 0, b = 0;
        heif_item_get_property_transform_crop_borders(ctx, item_id, crop_id, size.width(), size.height(), &l, &t, &r, &b);
        crop = QRect(QPoint(t, l), size - QSize(b + t, r + l));
    }

    return crop.isValid();
}

bool HEIFHandler::isSupportedBMFFType(const QByteArray &header)
{
    if (header.size() < 28) {
        return false;
    }

    const char *buffer = header.constData();
    if (memcmp(buffer + 4, "ftyp", 4) == 0) {
        if (memcmp(buffer + 8, "heic", 4) == 0) {
            return true;
        }
        if (memcmp(buffer + 8, "heis", 4) == 0) {
            return true;
        }
        if (memcmp(buffer + 8, "heix", 4) == 0) {
            return true;
        }

        /* we want to avoid loading AVIF files via this plugin */
        if (memcmp(buffer + 8, "mif1", 4) == 0) {
            for (int offset = 16; offset <= 24; offset += 4) {
                if (memcmp(buffer + offset, "avif", 4) == 0) {
                    return false;
                }
            }
            return true;
        }

        if (memcmp(buffer + 8, "mif2", 4) == 0) {
            return true;
        }
        if (memcmp(buffer + 8, "msf1", 4) == 0) {
            return true;
        }
    }

    return false;
}

bool HEIFHandler::isSupportedHEJ2(const QByteArray &header)
{
    if (header.size() < 28) {
        return false;
    }

    const char *buffer = header.constData();
    if (memcmp(buffer + 4, "ftypj2ki", 8) == 0) {
        return true;
    }

    return false;
}

bool HEIFHandler::isSupportedAVCI(const QByteArray &header)
{
    if (header.size() < 28) {
        return false;
    }

    const char *buffer = header.constData();
    if (memcmp(buffer + 4, "ftypavci", 8) == 0) {
        return true;
    }

    return false;
}

QVariant HEIFHandler::option(ImageOption option) const
{
    if (option == Quality) {
        return m_quality;
    }

    if (!supportsOption(option) || !ensureParsed()) {
        return QVariant();
    }

    switch (option) {
    case Size:
        return m_current_image.size();
    case ImageTransformation:
        return int(MicroExif::orientationToTransformation(m_orientation));
    default:
        return QVariant();
    }
}

void HEIFHandler::setOption(ImageOption option, const QVariant &value)
{
    switch (option) {
    case Quality:
        m_quality = value.toInt();
        if (m_quality > 100) {
            m_quality = 100;
        } else if (m_quality < 0) {
            m_quality = 100;
        }
        break;
    case ImageTransformation:
        m_orientation = MicroExif::transformationToOrientation(QImageIOHandler::Transformation(value.toUInt()));
        break;
    default:
        QImageIOHandler::setOption(option, value);
        break;
    }
}

bool HEIFHandler::supportsOption(ImageOption option) const
{
    auto ok = option == Quality || option == Size;
#ifndef HEIF_DISABLE_QT_TRANSFORMATION
    ok = ok || option == ImageTransformation;
#endif
    return ok;
}

bool HEIFHandler::ensureParsed() const
{
    if (m_parseState == ParseHeicSuccess) {
        return true;
    }
    if (m_parseState == ParseHeicError) {
        return false;
    }

    HEIFHandler *that = const_cast<HEIFHandler *>(this);

    startHeifLib();

    bool success = that->ensureDecoder();

    finishHeifLib();

    return success;
}

bool HEIFHandler::ensureDecoder()
{
    if (m_parseState != ParseHeicNotParsed) {
        if (m_parseState == ParseHeicSuccess) {
            return true;
        }
        return false;
    }

    const QByteArray buffer = device()->readAll();
    if (!HEIFHandler::isSupportedBMFFType(buffer) && !HEIFHandler::isSupportedHEJ2(buffer) && !HEIFHandler::isSupportedAVCI(buffer)) {
        m_parseState = ParseHeicError;
        return false;
    }

    struct heif_context *ctx = heif_context_alloc();
    struct heif_error err = heif_context_read_from_memory(ctx, static_cast<const void *>(buffer.constData()), buffer.size(), nullptr);

    if (err.code) {
        qCWarning(LOG_HEIFPLUGIN) << "heif_context_read_from_memory error:" << err.message;
        heif_context_free(ctx);
        m_parseState = ParseHeicError;
        return false;
    }

    struct heif_image_handle *handle = nullptr;
    err = heif_context_get_primary_image_handle(ctx, &handle);
    if (err.code) {
        qCWarning(LOG_HEIFPLUGIN) << "heif_context_get_primary_image_handle error:" << err.message;
        heif_context_free(ctx);
        m_parseState = ParseHeicError;
        return false;
    }

    if ((heif_image_handle_get_width(handle) == 0) || (heif_image_handle_get_height(handle) == 0)) {
        m_parseState = ParseHeicError;
        heif_image_handle_release(handle);
        heif_context_free(ctx);
        qCWarning(LOG_HEIFPLUGIN) << "HEIC image has zero dimension";
        return false;
    }

    const int bit_depth = heif_image_handle_get_luma_bits_per_pixel(handle);

    if (bit_depth < 8) {
        m_parseState = ParseHeicError;
        heif_image_handle_release(handle);
        heif_context_free(ctx);
        qCWarning(LOG_HEIFPLUGIN) << "HEIF image with undefined or unsupported bit depth.";
        return false;
    }

    const bool hasAlphaChannel = heif_image_handle_has_alpha_channel(handle);
    heif_chroma chroma;

    QImage::Format target_image_format;

    if (bit_depth == 10 || bit_depth == 12 || bit_depth == 16) {
        if (hasAlphaChannel) {
            chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBBAA_LE : heif_chroma_interleaved_RRGGBBAA_BE;
            target_image_format = QImage::Format_RGBA64;
        } else {
            chroma = (QSysInfo::ByteOrder == QSysInfo::LittleEndian) ? heif_chroma_interleaved_RRGGBB_LE : heif_chroma_interleaved_RRGGBB_BE;
            target_image_format = QImage::Format_RGBX64;
        }
    } else if (bit_depth == 8) {
        if (hasAlphaChannel) {
            chroma = heif_chroma_interleaved_RGBA;
            target_image_format = QImage::Format_ARGB32;
        } else {
            chroma = heif_chroma_interleaved_RGB;
            target_image_format = QImage::Format_RGB32;
        }
    } else {
        m_parseState = ParseHeicError;
        heif_image_handle_release(handle);
        heif_context_free(ctx);
        qCWarning(LOG_HEIFPLUGIN) << "Unsupported bit depth:" << bit_depth;
        return false;
    }

    bool ignore_transformations = false;
    struct heif_decoding_options *decoder_option = heif_decoding_options_alloc();

    decoder_option->strict_decoding = 1;
#ifdef HEIF_DISABLE_QT_TRANSFORMATION
    decoder_option->ignore_transformations = ignore_transformations;
#else
    decoder_option->ignore_transformations = ignore_transformations = read_orientation_helper(handle, ctx);
#endif

    struct heif_image *img = nullptr;
    err = heif_decode_image(handle, &img, heif_colorspace_RGB, chroma, decoder_option);

    if (err.code == heif_error_Invalid_input && err.subcode == heif_suberror_Unknown_NCLX_matrix_coefficients && img == nullptr && buffer.contains("Xiaomi")) {
        qCWarning(LOG_HEIFPLUGIN) << "Non-standard HEIF image with invalid matrix_coefficients, probably made by a Xiaomi device!";

        // second try to decode with strict decoding disabled
        decoder_option->strict_decoding = 0;
        err = heif_decode_image(handle, &img, heif_colorspace_RGB, chroma, decoder_option);
    }

    if (decoder_option) {
        heif_decoding_options_free(decoder_option);
    }

    if (err.code) {
        qCWarning(LOG_HEIFPLUGIN) << "heif_decode_image error:" << err.message;
        heif_image_handle_release(handle);
        heif_context_free(ctx);
        m_parseState = ParseHeicError;
        return false;
    }

    const int imageWidth = heif_image_get_width(img, heif_channel_interleaved);
    const int imageHeight = heif_image_get_height(img, heif_channel_interleaved);

    QSize imageSize;
    if (imageWidth < HEIF_MAX_IMAGE_WIDTH && imageHeight < HEIF_MAX_IMAGE_HEIGHT) {
        imageSize = QSize(imageWidth, imageHeight);
    }

    if (!imageSize.isValid()) {
        heif_image_release(img);
        heif_image_handle_release(handle);
        heif_context_free(ctx);
        m_parseState = ParseHeicError;
        qCWarning(LOG_HEIFPLUGIN) << "HEIC image size invalid:" << imageWidth << "x" << imageHeight;
        return false;
    }

    int stride = 0;
    const uint8_t *const src = heif_image_get_plane_readonly(img, heif_channel_interleaved, &stride);

    if (!src || stride <= 0) {
        heif_image_release(img);
        heif_image_handle_release(handle);
        heif_context_free(ctx);
        m_parseState = ParseHeicError;
        qCWarning(LOG_HEIFPLUGIN) << "HEIC data pixels information not valid!";
        return false;
    }

    m_current_image = imageAlloc(imageSize, target_image_format);
    if (m_current_image.isNull()) {
        heif_image_release(img);
        heif_image_handle_release(handle);
        heif_context_free(ctx);
        m_parseState = ParseHeicError;
        qCWarning(LOG_HEIFPLUGIN) << "Unable to allocate memory!";
        return false;
    }

    switch (bit_depth) {
    case 16:
        if (hasAlphaChannel) {
            for (int y = 0; y < imageHeight; y++) {
                memcpy(m_current_image.scanLine(y), src + (y * stride), 8 * size_t(imageWidth));
            }
        } else { // no alpha channel
            for (int y = 0; y < imageHeight; y++) {
                const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
                uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
                for (int x = 0; x < imageWidth; x++) {
                    // R
                    *dest_data = *src_word;
                    src_word++;
                    dest_data++;
                    // G
                    *dest_data = *src_word;
                    src_word++;
                    dest_data++;
                    // B
                    *dest_data = *src_word;
                    src_word++;
                    dest_data++;
                    // X = 0xffff
                    *dest_data = 0xffff;
                    dest_data++;
                }
            }
        }
        break;
    case 12:
        if (hasAlphaChannel) {
            for (int y = 0; y < imageHeight; y++) {
                const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
                uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
                for (int x = 0; x < imageWidth; x++) {
                    int tmpvalue;
                    // R
                    tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
                    tmpvalue = qBound(0, tmpvalue, 65535);
                    *dest_data = (uint16_t)tmpvalue;
                    src_word++;
                    dest_data++;
                    // G
                    tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
                    tmpvalue = qBound(0, tmpvalue, 65535);
                    *dest_data = (uint16_t)tmpvalue;
                    src_word++;
                    dest_data++;
                    // B
                    tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
                    tmpvalue = qBound(0, tmpvalue, 65535);
                    *dest_data = (uint16_t)tmpvalue;
                    src_word++;
                    dest_data++;
                    // A
                    tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
                    tmpvalue = qBound(0, tmpvalue, 65535);
                    *dest_data = (uint16_t)tmpvalue;
                    src_word++;
                    dest_data++;
                }
            }
        } else { // no alpha channel
            for (int y = 0; y < imageHeight; y++) {
                const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
                uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
                for (int x = 0; x < imageWidth; x++) {
                    int tmpvalue;
                    // R
                    tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
                    tmpvalue = qBound(0, tmpvalue, 65535);
                    *dest_data = (uint16_t)tmpvalue;
                    src_word++;
                    dest_data++;
                    // G
                    tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
                    tmpvalue = qBound(0, tmpvalue, 65535);
                    *dest_data = (uint16_t)tmpvalue;
                    src_word++;
                    dest_data++;
                    // B
                    tmpvalue = (int)(((float)(0x0fff & (*src_word)) / 4095.0f) * 65535.0f + 0.5f);
                    tmpvalue = qBound(0, tmpvalue, 65535);
                    *dest_data = (uint16_t)tmpvalue;
                    src_word++;
                    dest_data++;
                    // X = 0xffff
                    *dest_data = 0xffff;
                    dest_data++;
                }
            }
        }
        break;
    case 10:
        if (hasAlphaChannel) {
            for (int y = 0; y < imageHeight; y++) {
                const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
                uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
                for (int x = 0; x < imageWidth; x++) {
                    int tmpvalue;
                    // R
                    tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
                    tmpvalue = qBound(0, tmpvalue, 65535);
                    *dest_data = (uint16_t)tmpvalue;
                    src_word++;
                    dest_data++;
                    // G
                    tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
                    tmpvalue = qBound(0, tmpvalue, 65535);
                    *dest_data = (uint16_t)tmpvalue;
                    src_word++;
                    dest_data++;
                    // B
                    tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
                    tmpvalue = qBound(0, tmpvalue, 65535);
                    *dest_data = (uint16_t)tmpvalue;
                    src_word++;
                    dest_data++;
                    // A
                    tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
                    tmpvalue = qBound(0, tmpvalue, 65535);
                    *dest_data = (uint16_t)tmpvalue;
                    src_word++;
                    dest_data++;
                }
            }
        } else { // no alpha channel
            for (int y = 0; y < imageHeight; y++) {
                const uint16_t *src_word = reinterpret_cast<const uint16_t *>(src + (y * stride));
                uint16_t *dest_data = reinterpret_cast<uint16_t *>(m_current_image.scanLine(y));
                for (int x = 0; x < imageWidth; x++) {
                    int tmpvalue;
                    // R
                    tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
                    tmpvalue = qBound(0, tmpvalue, 65535);
                    *dest_data = (uint16_t)tmpvalue;
                    src_word++;
                    dest_data++;
                    // G
                    tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
                    tmpvalue = qBound(0, tmpvalue, 65535);
                    *dest_data = (uint16_t)tmpvalue;
                    src_word++;
                    dest_data++;
                    // B
                    tmpvalue = (int)(((float)(0x03ff & (*src_word)) / 1023.0f) * 65535.0f + 0.5f);
                    tmpvalue = qBound(0, tmpvalue, 65535);
                    *dest_data = (uint16_t)tmpvalue;
                    src_word++;
                    dest_data++;
                    // X = 0xffff
                    *dest_data = 0xffff;
                    dest_data++;
                }
            }
        }
        break;
    case 8:
        if (hasAlphaChannel) {
            for (int y = 0; y < imageHeight; y++) {
                const uint8_t *src_byte = src + (y * stride);
                uint32_t *dest_pixel = reinterpret_cast<uint32_t *>(m_current_image.scanLine(y));
                for (int x = 0; x < imageWidth; x++) {
                    int red = *src_byte++;
                    int green = *src_byte++;
                    int blue = *src_byte++;
                    int alpha = *src_byte++;
                    *dest_pixel = qRgba(red, green, blue, alpha);
                    dest_pixel++;
                }
            }
        } else { // no alpha channel
            for (int y = 0; y < imageHeight; y++) {
                const uint8_t *src_byte = src + (y * stride);
                uint32_t *dest_pixel = reinterpret_cast<uint32_t *>(m_current_image.scanLine(y));
                for (int x = 0; x < imageWidth; x++) {
                    int red = *src_byte++;
                    int green = *src_byte++;
                    int blue = *src_byte++;
                    *dest_pixel = qRgb(red, green, blue);
                    dest_pixel++;
                }
            }
        }
        break;
    default:
        heif_image_release(img);
        heif_image_handle_release(handle);
        heif_context_free(ctx);
        m_parseState = ParseHeicError;
        qCWarning(LOG_HEIFPLUGIN) << "Unsupported bit depth:" << bit_depth;
        return false;
        break;
    }

    if (ignore_transformations) {
        QRect crop_rect;
        if (read_crop(handle, ctx, m_current_image.size(), crop_rect))
            m_current_image = m_current_image.copy(crop_rect);
    }

    heif_color_profile_type profileType = heif_image_handle_get_color_profile_type(handle);
    if (profileType == heif_color_profile_type_prof || profileType == heif_color_profile_type_rICC) {
        size_t rawProfileSize = heif_image_handle_get_raw_color_profile_size(handle);
        if (rawProfileSize > 0 && rawProfileSize < std::numeric_limits<int>::max()) {
            QByteArray ba(rawProfileSize, 0);
            err = heif_image_handle_get_raw_color_profile(handle, ba.data());
            if (err.code) {
                qCWarning(LOG_HEIFPLUGIN) << "icc profile loading failed";
            } else {
                QColorSpace colorspace = QColorSpace::fromIccProfile(ba);
                if (!colorspace.isValid()) {
                    qCWarning(LOG_HEIFPLUGIN) << "HEIC image has Qt-unsupported or invalid ICC profile!";
                } else if (colorspace.colorModel() == QColorSpace::ColorModel::Cmyk) {
                    qCWarning(LOG_HEIFPLUGIN) << "CMYK ICC profile is not expected for HEIF, discarding the ICCprofile!";
                    colorspace = QColorSpace();
                } else if (colorspace.colorModel() == QColorSpace::ColorModel::Gray) {
                    if (hasAlphaChannel) {
                        QPointF gray_whitePoint = colorspace.whitePoint();
                        if (gray_whitePoint.isNull()) {
                            gray_whitePoint = QPointF(0.3127f, 0.329f);
                        }

                        const QPointF redP(0.64f, 0.33f);
                        const QPointF greenP(0.3f, 0.6f);
                        const QPointF blueP(0.15f, 0.06f);

                        QColorSpace::TransferFunction trc_new = colorspace.transferFunction();
                        float gamma_new = colorspace.gamma();
                        if (trc_new == QColorSpace::TransferFunction::Custom) {
                            trc_new = QColorSpace::TransferFunction::SRgb;
                        }
                        colorspace = QColorSpace(gray_whitePoint, redP, greenP, blueP, trc_new, gamma_new);
                        if (!colorspace.isValid()) {
                            qCWarning(LOG_HEIFPLUGIN) << "HEIF plugin created invalid QColorSpace!";
                        }
                    } else { // no alpha channel
                        m_current_image.convertTo(bit_depth > 8 ? QImage::Format_Grayscale16 : QImage::Format_Grayscale8);
                    }
                }
                m_current_image.setColorSpace(colorspace);
            }
        } else {
            qCWarning(LOG_HEIFPLUGIN) << "icc profile is empty or above limits";
        }

    } else if (profileType == heif_color_profile_type_nclx) {
        struct heif_color_profile_nclx *nclx = nullptr;
        err = heif_image_handle_get_nclx_color_profile(handle, &nclx);
        if (err.code || !nclx) {
            qCWarning(LOG_HEIFPLUGIN) << "nclx profile loading failed";
        } else {
            const QPointF redPoint(nclx->color_primary_red_x, nclx->color_primary_red_y);
            const QPointF greenPoint(nclx->color_primary_green_x, nclx->color_primary_green_y);
            const QPointF bluePoint(nclx->color_primary_blue_x, nclx->color_primary_blue_y);
            const QPointF whitePoint(nclx->color_primary_white_x, nclx->color_primary_white_y);

            QColorSpace::TransferFunction q_trc = QColorSpace::TransferFunction::Custom;
            float q_trc_gamma = 0.0f;

            switch (nclx->transfer_characteristics) {
            case 4:
                q_trc = QColorSpace::TransferFunction::Gamma;
                q_trc_gamma = 2.2f;
                break;
            case 5:
                q_trc = QColorSpace::TransferFunction::Gamma;
                q_trc_gamma = 2.8f;
                break;
            case 8:
                q_trc = QColorSpace::TransferFunction::Linear;
                break;
            case 2:
            case 13:
                q_trc = QColorSpace::TransferFunction::SRgb;
                break;
            case 16:
                q_trc = QColorSpace::TransferFunction::St2084;
                break;
            case 18:
                q_trc = QColorSpace::TransferFunction::Hlg;
                break;
            default:
                qCWarning(LOG_HEIFPLUGIN) << "CICP color_primaries: %d, transfer_characteristics: %d\nThe colorspace is unsupported by this plug-in yet."
                                          << nclx->color_primaries
                                          << nclx->transfer_characteristics;
                q_trc = QColorSpace::TransferFunction::SRgb;
                break;
            }

            if (q_trc != QColorSpace::TransferFunction::Custom) { // we create new colorspace using Qt
                switch (nclx->color_primaries) {
                case 1:
                case 2:
                    m_current_image.setColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, q_trc, q_trc_gamma));
                    break;
                case 12:
                    m_current_image.setColorSpace(QColorSpace(QColorSpace::Primaries::DciP3D65, q_trc, q_trc_gamma));
                    break;
                default:
                    m_current_image.setColorSpace(QColorSpace(whitePoint, redPoint, greenPoint, bluePoint, q_trc, q_trc_gamma));
                    break;
                }
            }
            heif_nclx_color_profile_free(nclx);

            if (!m_current_image.colorSpace().isValid()) {
                qCWarning(LOG_HEIFPLUGIN) << "HEIC plugin created invalid QColorSpace from NCLX!";
            }
        }

    } else {
        m_current_image.setColorSpace(QColorSpace(QColorSpace::SRgb));
    }

    // read metadata
    if (auto numMetadata = heif_image_handle_get_number_of_metadata_blocks(handle, nullptr)) {
        QVector<heif_item_id> ids(numMetadata);
        heif_image_handle_get_list_of_metadata_block_IDs(handle, nullptr, ids.data(), numMetadata);
        for (int n = 0; n < numMetadata; ++n) {
            auto itemtype = heif_image_handle_get_metadata_type(handle, ids[n]);
            auto contenttype = heif_image_handle_get_metadata_content_type(handle, ids[n]);
            auto isExif = !std::strcmp(itemtype, "Exif");
            auto isXmp = !std::strcmp(contenttype, "application/rdf+xml");
            if (isExif || isXmp) {
                auto sz = heif_image_handle_get_metadata_size(handle, ids[n]);
                if (sz == 0 || sz >= HEIF_MAX_METADATA_SIZE)
                    continue;
                QByteArray ba(sz, char());
                auto err = heif_image_handle_get_metadata(handle, ids[n], ba.data());
                if (err.code != heif_error_Ok) {
                    qCWarning(LOG_HEIFPLUGIN) << "Error while reading metadata" << err.message;
                    continue;
                }
                if (isXmp) {
                    m_current_image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromUtf8(ba));
                } else if (isExif) {
                    auto exif = MicroExif::fromByteArray(ba, true);
                    if (!exif.isEmpty()) {
                        exif.updateImageResolution(m_current_image);
                        exif.updateImageMetadata(m_current_image);
                    }
                }
            }
        }
    }

    heif_image_release(img);
    heif_image_handle_release(handle);
    heif_context_free(ctx);
    m_parseState = ParseHeicSuccess;
    return true;
}

bool HEIFHandler::isHeifDecoderAvailable()
{
    HEIFHandler::queryHeifLib();

    return m_heif_decoder_available;
}

bool HEIFHandler::isHeifEncoderAvailable()
{
    HEIFHandler::queryHeifLib();

    return m_heif_encoder_available;
}

bool HEIFHandler::isHej2DecoderAvailable()
{
    HEIFHandler::queryHeifLib();

    return m_hej2_decoder_available;
}

bool HEIFHandler::isHej2EncoderAvailable()
{
    HEIFHandler::queryHeifLib();

    return m_hej2_encoder_available;
}

bool HEIFHandler::isAVCIDecoderAvailable()
{
    HEIFHandler::queryHeifLib();

    return m_avci_decoder_available;
}

void HEIFHandler::queryHeifLib()
{
    QMutexLocker locker(&getHEIFHandlerMutex());

    if (!m_plugins_queried) {
        if (m_initialized_count == 0) {
            heif_init(nullptr);
        }

        m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC);
        m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC);
        m_hej2_decoder_available = heif_have_decoder_for_format(heif_compression_JPEG2000);
        m_hej2_encoder_available = heif_have_encoder_for_format(heif_compression_JPEG2000);
#if LIBHEIF_HAVE_VERSION(1, 19, 6)
        m_avci_decoder_available = heif_have_decoder_for_format(heif_compression_AVC);
#endif
        m_plugins_queried = true;

        if (m_initialized_count == 0) {
            heif_deinit();
        }
    }
}

void HEIFHandler::startHeifLib()
{
    QMutexLocker locker(&getHEIFHandlerMutex());

    if (m_initialized_count == 0) {
        heif_init(nullptr);
    }

    m_initialized_count++;
}

void HEIFHandler::finishHeifLib()
{
    QMutexLocker locker(&getHEIFHandlerMutex());

    if (m_initialized_count == 0) {
        return;
    }

    m_initialized_count--;
    if (m_initialized_count == 0) {
        heif_deinit();
    }
}

QMutex &HEIFHandler::getHEIFHandlerMutex()
{
    static QMutex heif_handler_mutex;
    return heif_handler_mutex;
}

QImageIOPlugin::Capabilities HEIFPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
    if (format == "heif" || format == "heic") {
        Capabilities format_cap;
        if (HEIFHandler::isHeifDecoderAvailable()) {
            format_cap |= CanRead;
        }
        if (HEIFHandler::isHeifEncoderAvailable()) {
            format_cap |= CanWrite;
        }
        return format_cap;
    }

    if (format == "hej2") {
        Capabilities format_cap;
        if (HEIFHandler::isHej2DecoderAvailable()) {
            format_cap |= CanRead;
        }
        if (HEIFHandler::isHej2EncoderAvailable()) {
            format_cap |= CanWrite;
        }
        return format_cap;
    }

    if (format == "avci") {
        Capabilities format_cap;
        if (HEIFHandler::isAVCIDecoderAvailable()) {
            format_cap |= CanRead;
        }
        return format_cap;
    }

    if (!format.isEmpty()) {
        return {};
    }
    if (!device->isOpen()) {
        return {};
    }

    Capabilities cap;
    if (device->isReadable()) {
        const QByteArray header = device->peek(28);

        if ((HEIFHandler::isSupportedBMFFType(header) && HEIFHandler::isHeifDecoderAvailable())
            || (HEIFHandler::isSupportedHEJ2(header) && HEIFHandler::isHej2DecoderAvailable())
            || (HEIFHandler::isSupportedAVCI(header) && HEIFHandler::isAVCIDecoderAvailable())) {
            cap |= CanRead;
        }
    }

    if (device->isWritable() && (HEIFHandler::isHeifEncoderAvailable() || HEIFHandler::isHej2EncoderAvailable())) {
        cap |= CanWrite;
    }
    return cap;
}

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

#include "moc_heif_p.cpp"
