diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f9f61c..b7bbd69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.6) project(KImageFormats) @@ -54,6 +54,13 @@ set_package_properties(libavif PROPERTIES PURPOSE "Required for the QImage plugin for AVIF images" ) +option(KIMAGEFORMATS_HEIF "Enable plugin for HEIF format" OFF) +if(KIMAGEFORMATS_HEIF) + include(FindPkgConfig) + pkg_check_modules(LibHeif IMPORTED_TARGET libheif>=1.10.0) +endif() +add_feature_info(LibHeif LibHeif_FOUND "required for the QImage plugin for HEIF/HEIC images") + add_definitions(-DQT_NO_FOREACH) # 050d00 (5.13) triggers a BIC in qimageiohandler.h, in Qt 5.13, so do not enable that until we can require 5.14 # https://codereview.qt-project.org/c/qt/qtbase/+/279215 diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 8af6256..2de3b9c 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -76,6 +76,12 @@ if (TARGET avif) ) endif() +if (LibHeif_FOUND) + kimageformats_read_tests( + heif + ) +endif() + # Allow some fuzziness when reading this formats, to allow for # rounding errors (eg: in alpha blending). kimageformats_read_tests(FUZZ 1 diff --git a/autotests/read/heif/rgb.heif b/autotests/read/heif/rgb.heif new file mode 100644 index 0000000..811b2d3 Binary files /dev/null and b/autotests/read/heif/rgb.heif differ diff --git a/autotests/read/heif/rgb.png b/autotests/read/heif/rgb.png new file mode 100644 index 0000000..5cd921f Binary files /dev/null and b/autotests/read/heif/rgb.png differ diff --git a/autotests/read/heif/rgba.heif b/autotests/read/heif/rgba.heif new file mode 100644 index 0000000..30aff5d Binary files /dev/null and b/autotests/read/heif/rgba.heif differ diff --git a/autotests/read/heif/rgba.png b/autotests/read/heif/rgba.png new file mode 100644 index 0000000..93ebc57 Binary files /dev/null and b/autotests/read/heif/rgba.png differ diff --git a/src/imageformats/CMakeLists.txt b/src/imageformats/CMakeLists.txt index 1888386..6d41198 100644 --- a/src/imageformats/CMakeLists.txt +++ b/src/imageformats/CMakeLists.txt @@ -71,6 +71,15 @@ install(FILES hdr.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugi ################################## +if (LibHeif_FOUND) + kimageformats_add_plugin(kimg_heif JSON "heif.json" SOURCES heif.cpp) + target_link_libraries(kimg_heif PkgConfig::LibHeif) + kde_target_enable_exceptions(kimg_heif PRIVATE) + install(FILES heif.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/) +endif() + +################################## + kimageformats_add_plugin(kimg_pcx JSON "pcx.json" SOURCES pcx.cpp) install(FILES pcx.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/) diff --git a/src/imageformats/heif.cpp b/src/imageformats/heif.cpp new file mode 100644 index 0000000..d399273 --- /dev/null +++ b/src/imageformats/heif.cpp @@ -0,0 +1,734 @@ +/* + 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 +#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 + +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; + } + + 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); + + if (tmpimage.colorSpace().isValid()) { + 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); + 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); + + return that->ensureDecoder(); +} +bool HEIFHandler::ensureDecoder() +{ + if (m_parseState != ParseHeicNotParsed) { + if (m_parseState == ParseHeicSuccess) { + return true; + } + return false; + } + + const QByteArray buffer = device()->readAll(); + if (buffer.isEmpty()) { + m_parseState = ParseHeicError; + return false; + } + + try { + heif::Context ctx; + ctx.read_from_memory_without_copy((const void *)(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 = QImage(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) { + int rawProfileSize = (int) heif_image_handle_get_raw_color_profile_size(handle.get_raw_image_handle()); + if (rawProfileSize > 0) { + 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() << "icc profile is invalid"; + } + } + } else { + qWarning() << "icc profile is empty"; + } + + } 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() << "invalid color profile created 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; +} + +QImageIOPlugin::Capabilities HEIFPlugin::capabilities(QIODevice *device, const QByteArray &format) const +{ + if (format == "heif" || format == "heic") { + Capabilities format_cap; + if (heif_have_decoder_for_format(heif_compression_HEVC)) { + format_cap |= CanRead; + } + if (heif_have_encoder_for_format(heif_compression_HEVC)) { + format_cap |= CanWrite; + } + return format_cap; + } + if (!format.isEmpty()) { + return {}; + } + if (!device->isOpen()) { + return {}; + } + + Capabilities cap; + if (device->isReadable() && HEIFHandler::canRead(device) && heif_have_decoder_for_format(heif_compression_HEVC)) { + cap |= CanRead; + } + + if (device->isWritable() && heif_have_encoder_for_format(heif_compression_HEVC)) { + 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; +} diff --git a/src/imageformats/heif.desktop b/src/imageformats/heif.desktop new file mode 100644 index 0000000..049b5fd --- /dev/null +++ b/src/imageformats/heif.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=QImageIOPlugins +X-KDE-ImageFormat=heif +X-KDE-MimeType=image/heif +X-KDE-Read=true +X-KDE-Write=true diff --git a/src/imageformats/heif.json b/src/imageformats/heif.json new file mode 100644 index 0000000..02307c2 --- /dev/null +++ b/src/imageformats/heif.json @@ -0,0 +1,4 @@ +{ + "Keys": [ "heif", "heic" ], + "MimeTypes": [ "image/heif", "image/heif" ] +} diff --git a/src/imageformats/heif_p.h b/src/imageformats/heif_p.h new file mode 100644 index 0000000..dd3e342 --- /dev/null +++ b/src/imageformats/heif_p.h @@ -0,0 +1,55 @@ +/* + 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 +*/ + +#ifndef KIMG_HEIF_P_H +#define KIMG_HEIF_P_H + +#include +#include + +class HEIFHandler : public QImageIOHandler +{ +public: + HEIFHandler(); + + bool canRead() const override; + bool read(QImage *image) override; + bool write(const QImage &image) override; + + static bool canRead(QIODevice *device); + + QVariant option(ImageOption option) const override; + void setOption(ImageOption option, const QVariant &value) override; + bool supportsOption(ImageOption option) const override; +private: + bool ensureParsed() const; + bool ensureDecoder(); + + enum ParseHeicState { + ParseHeicError = -1, + ParseHeicNotParsed = 0, + ParseHeicSuccess = 1 + }; + + ParseHeicState m_parseState; + int m_quality; + QImage m_current_image; +}; + +class HEIFPlugin : public QImageIOPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "heif.json") + +public: + Capabilities capabilities(QIODevice *device, const QByteArray &format) const override; + QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override; +}; + +#endif // KIMG_HEIF_P_H