/* High Efficiency Image File Format (HEIF) support for QImage. SPDX-FileCopyrightText: 2020 Sirius Bakke SPDX-FileCopyrightText: 2021 Daniel Novomesky SPDX-License-Identifier: LGPL-2.0-or-later */ #include "heif_p.h" #include "microexif_p.h" #include "util_p.h" #include #include #include #include #include #include #include #include #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(userdata); qint64 bytesWritten = ioDevice->write(static_cast(data), size); if (bytesWritten < static_cast(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(tmpimage.constScanLine(y)); uint16_t *dest_word = reinterpret_cast(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(tmpimage.constScanLine(y)); uint16_t *dest_word = reinterpret_cast(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_handle); auto ctx = reinterpret_cast(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_handle); auto ctx = reinterpret_cast(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(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(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(src + (y * stride)); uint16_t *dest_data = reinterpret_cast(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(src + (y * stride)); uint16_t *dest_data = reinterpret_cast(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(src + (y * stride)); uint16_t *dest_data = reinterpret_cast(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(src + (y * stride)); uint16_t *dest_data = reinterpret_cast(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(src + (y * stride)); uint16_t *dest_data = reinterpret_cast(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(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(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::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 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"