/* 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 "libheif/heif_cxx.h" #include "util_p.h" #include #include #include #include #include #include namespace // Private. { struct HeifQIODeviceWriter : public heif::Context::Writer { HeifQIODeviceWriter(QIODevice *device) : m_ioDevice(device) { } heif_error write(const void *data, size_t size) override { heif_error error; error.code = heif_error_Ok; error.subcode = heif_suberror_Unspecified; error.message = errorOkMessage; qint64 bytesWritten = m_ioDevice->write(static_cast(data), size); if (bytesWritten < static_cast(size)) { error.code = heif_error_Encoding_error; error.message = QIODeviceWriteErrorMessage; error.subcode = heif_suberror_Cannot_write_output_data; } return error; } static constexpr const char *errorOkMessage = "Success"; static constexpr const char *QIODeviceWriteErrorMessage = "Bytes written to QIODevice are smaller than input data size"; private: QIODevice *m_ioDevice; }; } // namespace 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; HEIFHandler::HEIFHandler() : m_parseState(ParseHeicNotParsed) , m_quality(100) { } bool HEIFHandler::canRead() const { if (m_parseState == ParseHeicNotParsed && !canRead(device())) { return false; } if (m_parseState != ParseHeicError) { setFormat("heif"); 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()) { qWarning("No image data to save"); return false; } #if LIBHEIF_HAVE_VERSION(1, 13, 0) startHeifLib(); #endif bool success = write_helper(image); #if LIBHEIF_HAVE_VERSION(1, 13, 0) finishHeifLib(); #endif 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_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; } } const QImage tmpimage = image.convertToFormat(tmpformat); try { heif::Context ctx; heif::Image heifImage; heifImage.create(tmpimage.width(), tmpimage.height(), heif_colorspace_RGB, chroma); QByteArray iccprofile = tmpimage.colorSpace().iccProfile(); if (iccprofile.size() > 0) { std::vector rawProfile(iccprofile.begin(), iccprofile.end()); heifImage.set_raw_color_profile(heif_color_profile_type_prof, rawProfile); } heifImage.add_plane(heif_channel_interleaved, image.width(), image.height(), save_depth); int stride = 0; uint8_t *const dst = heifImage.get_plane(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: qWarning() << "Unsupported depth:" << save_depth; return false; break; } heif::Encoder encoder(heif_compression_HEVC); encoder.set_lossy_quality(m_quality); if (m_quality > 90) { if (m_quality == 100) { encoder.set_lossless(true); } encoder.set_string_parameter("chroma", "444"); } heif::Context::EncodingOptions encodingOptions; encodingOptions.save_alpha_channel = save_alpha; if ((tmpimage.width() % 2 == 1) || (tmpimage.height() % 2 == 1)) { qWarning() << "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 encodingOptions.macOS_compatibility_workaround = 0; } } ctx.encode_image(heifImage, encoder, encodingOptions); HeifQIODeviceWriter writer(device()); ctx.write(writer); } catch (const heif::Error &err) { qWarning() << "libheif error:" << err.get_message().c_str(); return false; } return true; } bool HEIFHandler::canRead(QIODevice *device) { if (!device) { qWarning("HEIFHandler::canRead() called with no device"); return false; } const QByteArray header = device->peek(28); return HEIFHandler::isSupportedBMFFType(header); } bool HEIFHandler::isSupportedBMFFType(const QByteArray &header) { if (header.size() < 28) { return false; } const char *buffer = header.constData(); if (qstrncmp(buffer + 4, "ftyp", 4) == 0) { if (qstrncmp(buffer + 8, "heic", 4) == 0) { return true; } if (qstrncmp(buffer + 8, "heis", 4) == 0) { return true; } if (qstrncmp(buffer + 8, "heix", 4) == 0) { return true; } /* we want to avoid loading AVIF files via this plugin */ if (qstrncmp(buffer + 8, "mif1", 4) == 0) { for (int offset = 16; offset <= 24; offset += 4) { if (qstrncmp(buffer + offset, "avif", 4) == 0) { return false; } } return true; } if (qstrncmp(buffer + 8, "mif2", 4) == 0) { return true; } if (qstrncmp(buffer + 8, "msf1", 4) == 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(); break; default: return QVariant(); break; } } 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; default: QImageIOHandler::setOption(option, value); break; } } bool HEIFHandler::supportsOption(ImageOption option) const { return option == Quality || option == Size; } bool HEIFHandler::ensureParsed() const { if (m_parseState == ParseHeicSuccess) { return true; } if (m_parseState == ParseHeicError) { return false; } HEIFHandler *that = const_cast(this); #if LIBHEIF_HAVE_VERSION(1, 13, 0) startHeifLib(); #endif bool success = that->ensureDecoder(); #if LIBHEIF_HAVE_VERSION(1, 13, 0) finishHeifLib(); #endif 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)) { m_parseState = ParseHeicError; return false; } try { heif::Context ctx; ctx.read_from_memory_without_copy(static_cast(buffer.constData()), buffer.size()); heif::ImageHandle handle = ctx.get_primary_image_handle(); const bool hasAlphaChannel = handle.has_alpha_channel(); const int bit_depth = handle.get_luma_bits_per_pixel(); heif_chroma chroma; QImage::Format target_image_format; if (bit_depth == 10 || bit_depth == 12) { 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; if (bit_depth > 0) { qWarning() << "Unsupported bit depth:" << bit_depth; } else { qWarning() << "Undefined bit depth."; } return false; } heif::Image img = handle.decode_image(heif_colorspace_RGB, chroma); const int imageWidth = img.get_width(heif_channel_interleaved); const int imageHeight = img.get_height(heif_channel_interleaved); QSize imageSize(imageWidth, imageHeight); if (!imageSize.isValid()) { m_parseState = ParseHeicError; qWarning() << "HEIC image size invalid:" << imageSize; return false; } int stride = 0; const uint8_t *const src = img.get_plane(heif_channel_interleaved, &stride); if (!src || stride <= 0) { m_parseState = ParseHeicError; qWarning() << "HEIC data pixels information not valid!"; return false; } m_current_image = imageAlloc(imageSize, target_image_format); if (m_current_image.isNull()) { m_parseState = ParseHeicError; qWarning() << "Unable to allocate memory!"; return false; } switch (bit_depth) { 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: m_parseState = ParseHeicError; qWarning() << "Unsupported bit depth:" << bit_depth; return false; break; } heif_color_profile_type profileType = heif_image_handle_get_color_profile_type(handle.get_raw_image_handle()); struct heif_error err; 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.get_raw_image_handle()); if (rawProfileSize > 0 && rawProfileSize < std::numeric_limits::max()) { QByteArray ba(rawProfileSize, 0); err = heif_image_handle_get_raw_color_profile(handle.get_raw_image_handle(), ba.data()); if (err.code) { qWarning() << "icc profile loading failed"; } else { m_current_image.setColorSpace(QColorSpace::fromIccProfile(ba)); if (!m_current_image.colorSpace().isValid()) { qWarning() << "HEIC image has Qt-unsupported or invalid ICC profile!"; } } } else { qWarning() << "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.get_raw_image_handle(), &nclx); if (err.code || !nclx) { qWarning() << "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; default: qWarning("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()) { qWarning() << "HEIC plugin created invalid QColorSpace from NCLX!"; } } } else { m_current_image.setColorSpace(QColorSpace(QColorSpace::SRgb)); } } catch (const heif::Error &err) { m_parseState = ParseHeicError; qWarning() << "libheif error:" << err.get_message().c_str(); return false; } m_parseState = ParseHeicSuccess; return true; } bool HEIFHandler::isHeifDecoderAvailable() { QMutexLocker locker(&getHEIFHandlerMutex()); if (!m_plugins_queried) { #if LIBHEIF_HAVE_VERSION(1, 13, 0) if (m_initialized_count == 0) { heif_init(nullptr); } #endif 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_plugins_queried = true; #if LIBHEIF_HAVE_VERSION(1, 13, 0) if (m_initialized_count == 0) { heif_deinit(); } #endif } return m_heif_decoder_available; } bool HEIFHandler::isHeifEncoderAvailable() { QMutexLocker locker(&getHEIFHandlerMutex()); if (!m_plugins_queried) { #if LIBHEIF_HAVE_VERSION(1, 13, 0) if (m_initialized_count == 0) { heif_init(nullptr); } #endif m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC); m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC); m_plugins_queried = true; #if LIBHEIF_HAVE_VERSION(1, 13, 0) if (m_initialized_count == 0) { heif_deinit(); } #endif } return m_heif_encoder_available; } void HEIFHandler::startHeifLib() { #if LIBHEIF_HAVE_VERSION(1, 13, 0) QMutexLocker locker(&getHEIFHandlerMutex()); if (m_initialized_count == 0) { heif_init(nullptr); } m_initialized_count++; #endif } void HEIFHandler::finishHeifLib() { #if LIBHEIF_HAVE_VERSION(1, 13, 0) QMutexLocker locker(&getHEIFHandlerMutex()); if (m_initialized_count == 0) { return; } m_initialized_count--; if (m_initialized_count == 0) { heif_deinit(); } #endif } 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.isEmpty()) { return {}; } if (!device->isOpen()) { return {}; } Capabilities cap; if (device->isReadable() && HEIFHandler::canRead(device) && HEIFHandler::isHeifDecoderAvailable()) { cap |= CanRead; } if (device->isWritable() && HEIFHandler::isHeifEncoderAvailable()) { 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; }