diff --git a/CMakeLists.txt b/CMakeLists.txt index e916398..a974f48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,13 @@ set_package_properties(OpenEXR PROPERTIES TYPE OPTIONAL PURPOSE "Required for the QImage plugin for OpenEXR images" ) + +find_package(libavif 0.8.2 CONFIG) +set_package_properties(libavif PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for the QImage plugin for AVIF 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 4d90f99..1eebcfc 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -70,6 +70,12 @@ if (KF5Archive_FOUND) ) endif() +if (TARGET avif) + kimageformats_read_tests( + avif + ) +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/avif/bw.avif b/autotests/read/avif/bw.avif new file mode 100644 index 0000000..940043b Binary files /dev/null and b/autotests/read/avif/bw.avif differ diff --git a/autotests/read/avif/bw.png b/autotests/read/avif/bw.png new file mode 100644 index 0000000..e2d506b Binary files /dev/null and b/autotests/read/avif/bw.png differ diff --git a/autotests/read/avif/bwa.avif b/autotests/read/avif/bwa.avif new file mode 100644 index 0000000..f9cf4c9 Binary files /dev/null and b/autotests/read/avif/bwa.avif differ diff --git a/autotests/read/avif/bwa.png b/autotests/read/avif/bwa.png new file mode 100644 index 0000000..a754150 Binary files /dev/null and b/autotests/read/avif/bwa.png differ diff --git a/autotests/read/avif/rgb.avif b/autotests/read/avif/rgb.avif new file mode 100644 index 0000000..6560e06 Binary files /dev/null and b/autotests/read/avif/rgb.avif differ diff --git a/autotests/read/avif/rgb.png b/autotests/read/avif/rgb.png new file mode 100644 index 0000000..267d678 Binary files /dev/null and b/autotests/read/avif/rgb.png differ diff --git a/autotests/read/avif/rgba.avif b/autotests/read/avif/rgba.avif new file mode 100644 index 0000000..5441b45 Binary files /dev/null and b/autotests/read/avif/rgba.avif differ diff --git a/autotests/read/avif/rgba.png b/autotests/read/avif/rgba.png new file mode 100644 index 0000000..cb0cae0 Binary files /dev/null and b/autotests/read/avif/rgba.png differ diff --git a/src/imageformats/CMakeLists.txt b/src/imageformats/CMakeLists.txt index 8a53bba..c2b442e 100644 --- a/src/imageformats/CMakeLists.txt +++ b/src/imageformats/CMakeLists.txt @@ -24,6 +24,14 @@ endfunction() ################################## +if (TARGET avif) + kimageformats_add_plugin(kimg_avif JSON "avif.json" SOURCES "avif.cpp") + target_link_libraries(kimg_avif "avif") + install(FILES avif.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/) +endif() + +################################## + install(FILES dds-qt.desktop RENAME dds.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/) ################################## diff --git a/src/imageformats/avif.cpp b/src/imageformats/avif.cpp new file mode 100644 index 0000000..5909607 --- /dev/null +++ b/src/imageformats/avif.cpp @@ -0,0 +1,981 @@ +/* + AV1 Image File Format (AVIF) support for QImage. + + SPDX-FileCopyrightText: 2020 Daniel Novomesky + + SPDX-License-Identifier: BSD-2-Clause +*/ + +#include +#include + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) +#include +#endif + +#include "avif_p.h" + + +QAVIFHandler::QAVIFHandler() : + m_parseState(ParseAvifNotParsed), + m_quality(52), + m_container_width(0), + m_container_height(0), + m_rawAvifData(AVIF_DATA_EMPTY), + m_decoder(nullptr), + m_must_jump_to_next_image(false) +{ +} + +QAVIFHandler::~QAVIFHandler() +{ + if (m_decoder) { + avifDecoderDestroy(m_decoder); + } +} + +bool QAVIFHandler::canRead() const +{ + if (m_parseState == ParseAvifNotParsed && !canRead(device())) { + return false; + } + + if (m_parseState != ParseAvifError) { + setFormat("avif"); + return true; + } + return false; +} + +bool QAVIFHandler::canRead(QIODevice *device) +{ + if (!device) { + return false; + } + QByteArray header = device->peek(144); + if (header.size() < 12) { + return false; + } + + avifROData input; + input.data = (const uint8_t *) header.constData(); + input.size = header.size(); + + if (avifPeekCompatibleFileType(&input)) { + return true; + } + return false; +} + +bool QAVIFHandler::ensureParsed() const +{ + if (m_parseState == ParseAvifSuccess) { + return true; + } + if (m_parseState == ParseAvifError) { + return false; + } + + QAVIFHandler *that = const_cast(this); + + return that->ensureDecoder(); +} + +bool QAVIFHandler::ensureDecoder() +{ + if (m_decoder) { + return true; + } + + m_rawData = device()->readAll(); + + m_rawAvifData.data = (const uint8_t *) m_rawData.constData(); + m_rawAvifData.size = m_rawData.size(); + + if (avifPeekCompatibleFileType(&m_rawAvifData) == AVIF_FALSE) { + m_parseState = ParseAvifError; + return false; + } + + + m_decoder = avifDecoderCreate(); + + avifResult decodeResult; + + decodeResult = avifDecoderSetIOMemory(m_decoder, m_rawAvifData.data, m_rawAvifData.size); + if (decodeResult != AVIF_RESULT_OK) { + qWarning("ERROR: avifDecoderSetIOMemory failed: %s\n", avifResultToString(decodeResult)); + + avifDecoderDestroy(m_decoder); + m_decoder = nullptr; + m_parseState = ParseAvifError; + return false; + } + + decodeResult = avifDecoderParse(m_decoder); + if (decodeResult != AVIF_RESULT_OK) { + qWarning("ERROR: Failed to parse input: %s\n", avifResultToString(decodeResult)); + + avifDecoderDestroy(m_decoder); + m_decoder = nullptr; + m_parseState = ParseAvifError; + return false; + } + + decodeResult = avifDecoderNextImage(m_decoder); + + if (decodeResult == AVIF_RESULT_OK) { + + m_container_width = m_decoder->image->width; + m_container_height = m_decoder->image->height; + + if ((m_container_width > 32768) || (m_container_height > 32768)) { + qWarning("AVIF image (%dx%d) is too large!", m_container_width, m_container_height); + m_parseState = ParseAvifError; + return false; + } + + if ((m_container_width == 0) || (m_container_height == 0)) { + qWarning("Empty image, nothing to decode"); + m_parseState = ParseAvifError; + return false; + } + + m_parseState = ParseAvifSuccess; + if (decode_one_frame()) { + return true; + } else { + m_parseState = ParseAvifError; + return false; + } + } else { + qWarning("ERROR: Failed to decode image: %s\n", avifResultToString(decodeResult)); + } + + avifDecoderDestroy(m_decoder); + m_decoder = nullptr; + m_parseState = ParseAvifError; + return false; +} + +bool QAVIFHandler::decode_one_frame() +{ + if (!ensureParsed()) { + return false; + } + + bool loadalpha; + + if (m_decoder->image->alphaPlane) { + loadalpha = true; + } else { + loadalpha = false; + } + + QImage::Format resultformat; + + if (m_decoder->image->depth > 8) { + if (loadalpha) { + resultformat = QImage::Format_RGBA64; + } else { + resultformat = QImage::Format_RGBX64; + } + } else { + if (loadalpha) { + resultformat = QImage::Format_RGBA8888; + } else { + resultformat = QImage::Format_RGB888; + } + } + QImage result(m_decoder->image->width, m_decoder->image->height, resultformat); + + if (result.isNull()) { + qWarning("Memory cannot be allocated"); + return false; + } + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + if (m_decoder->image->icc.data && (m_decoder->image->icc.size > 0)) { + result.setColorSpace(QColorSpace::fromIccProfile(QByteArray::fromRawData((const char *) m_decoder->image->icc.data, (int) m_decoder->image->icc.size))); + if (! result.colorSpace().isValid()) { + qWarning("Invalid QColorSpace created from ICC!\n"); + } + } else { + + avifColorPrimaries primaries_to_load; + avifTransferCharacteristics trc_to_load; + + if ((m_decoder->image->colorPrimaries == 2 /* AVIF_COLOR_PRIMARIES_UNSPECIFIED */) || + (m_decoder->image->colorPrimaries == 0 /* AVIF_COLOR_PRIMARIES_UNKNOWN */)) { + primaries_to_load = (avifColorPrimaries) 1; // AVIF_COLOR_PRIMARIES_BT709 + } else { + primaries_to_load = m_decoder->image->colorPrimaries; + } + if ((m_decoder->image->transferCharacteristics == 2 /* AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED */) || + (m_decoder->image->transferCharacteristics == 0 /* AVIF_TRANSFER_CHARACTERISTICS_UNKNOWN */)) { + trc_to_load = (avifTransferCharacteristics) 13; // AVIF_TRANSFER_CHARACTERISTICS_SRGB + } else { + trc_to_load = m_decoder->image->transferCharacteristics; + } + + float prim[8]; // outPrimaries: rX, rY, gX, gY, bX, bY, wX, wY + avifColorPrimariesGetValues(primaries_to_load, prim); + + QPointF redPoint(prim[0], prim[1]); + QPointF greenPoint(prim[2], prim[3]); + QPointF bluePoint(prim[4], prim[5]); + QPointF whitePoint(prim[6], prim[7]); + + + QColorSpace::TransferFunction q_trc = QColorSpace::TransferFunction::Custom; + float q_trc_gamma = 0.0f; + + switch (trc_to_load) { + /* AVIF_TRANSFER_CHARACTERISTICS_BT470M */ + case 4: + q_trc = QColorSpace::TransferFunction::Gamma; + q_trc_gamma = 2.2f; + break; + /* AVIF_TRANSFER_CHARACTERISTICS_BT470BG */ + case 5: + q_trc = QColorSpace::TransferFunction::Gamma; + q_trc_gamma = 2.8f; + break; + /* AVIF_TRANSFER_CHARACTERISTICS_LINEAR */ + case 8: + q_trc = QColorSpace::TransferFunction::Linear; + break; + /* AVIF_TRANSFER_CHARACTERISTICS_SRGB */ + case 13: + q_trc = QColorSpace::TransferFunction::SRgb; + break; + default: + qWarning("CICP colorPrimaries: %d, transferCharacteristics: %d\nThe colorspace is unsupported by this plug-in yet.", + m_decoder->image->colorPrimaries, m_decoder->image->transferCharacteristics); + q_trc = QColorSpace::TransferFunction::SRgb; + break; + } + + if (q_trc != QColorSpace::TransferFunction::Custom) { //we create new colorspace using Qt + switch (primaries_to_load) { + /* AVIF_COLOR_PRIMARIES_BT709 */ + case 1: + result.setColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, q_trc, q_trc_gamma)); + break; + /* AVIF_COLOR_PRIMARIES_SMPTE432 */ + case 12: + result.setColorSpace(QColorSpace(QColorSpace::Primaries::DciP3D65, q_trc, q_trc_gamma)); + break; + default: + result.setColorSpace(QColorSpace(whitePoint, redPoint, greenPoint, bluePoint, q_trc, q_trc_gamma)); + break; + } + } + + if (! result.colorSpace().isValid()) { + qWarning("Invalid QColorSpace created from NCLX/CICP!\n"); + } + } +#endif + + avifRGBImage rgb; + avifRGBImageSetDefaults(&rgb, m_decoder->image); + + if (m_decoder->image->depth > 8) { + rgb.depth = 16; + rgb.format = AVIF_RGB_FORMAT_RGBA; + + if (!loadalpha) { + rgb.ignoreAlpha = AVIF_TRUE; + result.fill(Qt::black); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) + if (m_decoder->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) { + resultformat = QImage::Format_Grayscale16; + } +#endif + } + } else { + rgb.depth = 8; + if (loadalpha) { + rgb.format = AVIF_RGB_FORMAT_RGBA; + resultformat = QImage::Format_ARGB32; + } else { + rgb.format = AVIF_RGB_FORMAT_RGB; + + if (m_decoder->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) { + resultformat = QImage::Format_Grayscale8; + } else { + resultformat = QImage::Format_RGB32; + } + } + } + + rgb.rowBytes = result.bytesPerLine(); + rgb.pixels = result.bits(); + + avifResult res = avifImageYUVToRGB(m_decoder->image, &rgb); + if (res != AVIF_RESULT_OK) { + qWarning("ERROR in avifImageYUVToRGB: %s\n", avifResultToString(res)); + return false; + } + + if (m_decoder->image->transformFlags & AVIF_TRANSFORM_CLAP) { + if ((m_decoder->image->clap.widthD > 0) && (m_decoder->image->clap.heightD > 0) && + (m_decoder->image->clap.horizOffD > 0) && (m_decoder->image->clap.vertOffD > 0)) { + int new_width, new_height, offx, offy; + + new_width = (int)((double)(m_decoder->image->clap.widthN) / (m_decoder->image->clap.widthD) + 0.5); + if (new_width > result.width()) { + new_width = result.width(); + } + + new_height = (int)((double)(m_decoder->image->clap.heightN) / (m_decoder->image->clap.heightD) + 0.5); + if (new_height > result.height()) { + new_height = result.height(); + } + + if (new_width > 0 && new_height > 0) { + + offx = ((double)((int32_t) m_decoder->image->clap.horizOffN)) / (m_decoder->image->clap.horizOffD) + + (result.width() - new_width) / 2.0 + 0.5; + if (offx < 0) { + offx = 0; + } else if (offx > (result.width() - new_width)) { + offx = result.width() - new_width; + } + + offy = ((double)((int32_t) m_decoder->image->clap.vertOffN)) / (m_decoder->image->clap.vertOffD) + + (result.height() - new_height) / 2.0 + 0.5; + if (offy < 0) { + offy = 0; + } else if (offy > (result.height() - new_height)) { + offy = result.height() - new_height; + } + + result = result.copy(offx, offy, new_width, new_height); + } + } + + else { //Zero values, we need to avoid 0 divide. + qWarning("ERROR: Wrong values in avifCleanApertureBox\n"); + } + } + + if (m_decoder->image->transformFlags & AVIF_TRANSFORM_IROT) { + QTransform transform; + switch (m_decoder->image->irot.angle) { + case 1: + transform.rotate(-90); + result = result.transformed(transform); + break; + case 2: + transform.rotate(180); + result = result.transformed(transform); + break; + case 3: + transform.rotate(90); + result = result.transformed(transform); + break; + } + } + + if (m_decoder->image->transformFlags & AVIF_TRANSFORM_IMIR) { + switch (m_decoder->image->imir.axis) { + case 0: //vertical + result = result.mirrored(false, true); + break; + case 1: //horizontal + result = result.mirrored(true, false); + break; + } + } + + if (resultformat == result.format()) { + m_current_image = result; + } else { + m_current_image = result.convertToFormat(resultformat); + } + + m_must_jump_to_next_image = false; + return true; +} + +bool QAVIFHandler::read(QImage *image) +{ + if (!ensureParsed()) { + return false; + } + + if (m_must_jump_to_next_image) { + jumpToNextImage(); + } + + *image = m_current_image; + if (imageCount() >= 2) { + m_must_jump_to_next_image = true; + } + return true; +} + +bool QAVIFHandler::write(const QImage &image) +{ + if (image.format() == QImage::Format_Invalid) { + qWarning("No image data to save"); + return false; + } + + if ((image.width() > 32768) || (image.height() > 32768)) { + qWarning("Image is too large"); + return false; + } + + int maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY * (100 - qBound(0, m_quality, 100)) / 100; + int minQuantizer = 0; + int maxQuantizerAlpha = 0; + avifResult res; + + bool save_grayscale; //true - monochrome, false - colors + int save_depth; //8 or 10bit per channel + QImage::Format tmpformat; //format for temporary image + + avifImage *avif = nullptr; + + //grayscale detection + switch (image.format()) { + case QImage::Format_Mono: + case QImage::Format_MonoLSB: + case QImage::Format_Grayscale8: +#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) + case QImage::Format_Grayscale16: +#endif + save_grayscale = true; + break; + case QImage::Format_Indexed8: + save_grayscale = image.isGrayscale(); + break; + default: + save_grayscale = false; + break; + } + + //depth detection + switch (image.format()) { + case QImage::Format_BGR30: + case QImage::Format_A2BGR30_Premultiplied: + case QImage::Format_RGB30: + case QImage::Format_A2RGB30_Premultiplied: +#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) + case QImage::Format_Grayscale16: +#endif + 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; + } + + //quality settings + if (maxQuantizer > 20) { + minQuantizer = maxQuantizer - 20; + if (maxQuantizer > 40) { //we decrease quality of alpha channel here + maxQuantizerAlpha = maxQuantizer - 40; + } + } + + if (save_grayscale && !image.hasAlphaChannel()) { //we are going to save grayscale image without alpha channel +#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) + if (save_depth > 8) { + tmpformat = QImage::Format_Grayscale16; + } else { + tmpformat = QImage::Format_Grayscale8; + } +#else + tmpformat = QImage::Format_Grayscale8; + save_depth = 8; +#endif + QImage tmpgrayimage = image.convertToFormat(tmpformat); + + avif = avifImageCreate(tmpgrayimage.width(), tmpgrayimage.height(), save_depth, AVIF_PIXEL_FORMAT_YUV400); + avifImageAllocatePlanes(avif, AVIF_PLANES_YUV); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + if (tmpgrayimage.colorSpace().isValid()) { + avif->colorPrimaries = (avifColorPrimaries)1; + avif->matrixCoefficients = (avifMatrixCoefficients)1; + + switch (tmpgrayimage.colorSpace().transferFunction()) { + case QColorSpace::TransferFunction::Linear: + /* AVIF_TRANSFER_CHARACTERISTICS_LINEAR */ + avif->transferCharacteristics = (avifTransferCharacteristics)8; + break; + case QColorSpace::TransferFunction::SRgb: + /* AVIF_TRANSFER_CHARACTERISTICS_SRGB */ + avif->transferCharacteristics = (avifTransferCharacteristics)13; + break; + default: + /* AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED */ + break; + } + + } +#endif + + if (save_depth > 8) { // QImage::Format_Grayscale16 + for (int y = 0; y < tmpgrayimage.height(); y++) { + const uint16_t *src16bit = reinterpret_cast(tmpgrayimage.constScanLine(y)); + uint16_t *dest16bit = reinterpret_cast(avif->yuvPlanes[0] + y * avif->yuvRowBytes[0]); + for (int x = 0; x < tmpgrayimage.width(); x++) { + int tmp_pixelval = (int)(((float)(*src16bit) / 65535.0f) * 1023.0f + 0.5f); //downgrade to 10 bits + *dest16bit = qBound(0, tmp_pixelval, 1023); + dest16bit++; + src16bit++; + } + } + } else { // QImage::Format_Grayscale8 + for (int y = 0; y < tmpgrayimage.height(); y++) { + const uchar *src8bit = tmpgrayimage.constScanLine(y); + uint8_t *dest8bit = avif->yuvPlanes[0] + y * avif->yuvRowBytes[0]; + for (int x = 0; x < tmpgrayimage.width(); x++) { + *dest8bit = *src8bit; + dest8bit++; + src8bit++; + } + } + } + + } else { //we are going to save color image + if (save_depth > 8) { + if (image.hasAlphaChannel()) { + tmpformat = QImage::Format_RGBA64; + } else { + tmpformat = QImage::Format_RGBX64; + } + } else { //8bit depth + if (image.hasAlphaChannel()) { + tmpformat = QImage::Format_RGBA8888; + } else { + tmpformat = QImage::Format_RGB888; + } + } + + QImage tmpcolorimage = image.convertToFormat(tmpformat); + + avifPixelFormat pixel_format = AVIF_PIXEL_FORMAT_YUV420; + if (maxQuantizer < 20) { + if (maxQuantizer < 10) { + pixel_format = AVIF_PIXEL_FORMAT_YUV444; //best quality + } else { + pixel_format = AVIF_PIXEL_FORMAT_YUV422; //high quality + } + } + + avifMatrixCoefficients matrix_to_save = (avifMatrixCoefficients)1; //default for Qt 5.12 and 5.13; + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + + avifColorPrimaries primaries_to_save = (avifColorPrimaries)2; + avifTransferCharacteristics transfer_to_save = (avifTransferCharacteristics)2; + + if (tmpcolorimage.colorSpace().isValid()) { + switch (tmpcolorimage.colorSpace().primaries()) { + case QColorSpace::Primaries::SRgb: + /* AVIF_COLOR_PRIMARIES_BT709 */ + primaries_to_save = (avifColorPrimaries)1; + /* AVIF_MATRIX_COEFFICIENTS_BT709 */ + matrix_to_save = (avifMatrixCoefficients)1; + break; + case QColorSpace::Primaries::DciP3D65: + /* AVIF_NCLX_COLOUR_PRIMARIES_P3, AVIF_NCLX_COLOUR_PRIMARIES_SMPTE432 */ + primaries_to_save = (avifColorPrimaries)12; + /* AVIF_MATRIX_COEFFICIENTS_CHROMA_DERIVED_NCL */ + matrix_to_save = (avifMatrixCoefficients)12; + break; + default: + /* AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED */ + primaries_to_save = (avifColorPrimaries)2; + /* AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED */ + matrix_to_save = (avifMatrixCoefficients)2; + break; + } + + switch (tmpcolorimage.colorSpace().transferFunction()) { + case QColorSpace::TransferFunction::Linear: + /* AVIF_TRANSFER_CHARACTERISTICS_LINEAR */ + transfer_to_save = (avifTransferCharacteristics)8; + break; + case QColorSpace::TransferFunction::Gamma: + if (qAbs(tmpcolorimage.colorSpace().gamma() - 2.2f) < 0.1f) { + /* AVIF_TRANSFER_CHARACTERISTICS_BT470M */ + transfer_to_save = (avifTransferCharacteristics)4; + } else if (qAbs(tmpcolorimage.colorSpace().gamma() - 2.8f) < 0.1f) { + /* AVIF_TRANSFER_CHARACTERISTICS_BT470BG */ + transfer_to_save = (avifTransferCharacteristics)5; + } else { + /* AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED */ + transfer_to_save = (avifTransferCharacteristics)2; + } + break; + case QColorSpace::TransferFunction::SRgb: + /* AVIF_TRANSFER_CHARACTERISTICS_SRGB */ + transfer_to_save = (avifTransferCharacteristics)13; + break; + default: + /* AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED */ + transfer_to_save = (avifTransferCharacteristics)2; + break; + } + + //in case primaries or trc were not identified + if ((primaries_to_save == 2) || + (transfer_to_save == 2)) { + + //upgrade image to higher bit depth + if (save_depth == 8) { + save_depth = 10; + if (tmpcolorimage.hasAlphaChannel()) { + tmpcolorimage = tmpcolorimage.convertToFormat(QImage::Format_RGBA64); + } else { + tmpcolorimage = tmpcolorimage.convertToFormat(QImage::Format_RGBX64); + } + } + + if ((primaries_to_save == 2) && + (transfer_to_save != 2)) { //other primaries but known trc + primaries_to_save = (avifColorPrimaries)1; // AVIF_COLOR_PRIMARIES_BT709 + matrix_to_save = (avifMatrixCoefficients)1; // AVIF_MATRIX_COEFFICIENTS_BT709 + + switch (transfer_to_save) { + case 8: // AVIF_TRANSFER_CHARACTERISTICS_LINEAR + tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::Linear)); + break; + case 4: // AVIF_TRANSFER_CHARACTERISTICS_BT470M + tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, 2.2f)); + break; + case 5: // AVIF_TRANSFER_CHARACTERISTICS_BT470BG + tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, 2.8f)); + break; + default: // AVIF_TRANSFER_CHARACTERISTICS_SRGB + any other + tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::SRgb)); + transfer_to_save = (avifTransferCharacteristics)13; + break; + } + } else if ((primaries_to_save != 2) && + (transfer_to_save == 2)) { //recognized primaries but other trc + transfer_to_save = (avifTransferCharacteristics)13; + tmpcolorimage.convertToColorSpace(tmpcolorimage.colorSpace().withTransferFunction(QColorSpace::TransferFunction::SRgb)); + } else { //unrecognized profile + primaries_to_save = (avifColorPrimaries)1; // AVIF_COLOR_PRIMARIES_BT709 + transfer_to_save = (avifTransferCharacteristics)13; + matrix_to_save = (avifMatrixCoefficients)1; // AVIF_MATRIX_COEFFICIENTS_BT709 + tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::SRgb)); + } + } + } +#endif + avif = avifImageCreate(tmpcolorimage.width(), tmpcolorimage.height(), save_depth, pixel_format); + avif->matrixCoefficients = matrix_to_save; + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + avif->colorPrimaries = primaries_to_save; + avif->transferCharacteristics = transfer_to_save; +#endif + + avifRGBImage rgb; + avifRGBImageSetDefaults(&rgb, avif); + rgb.rowBytes = tmpcolorimage.bytesPerLine(); + rgb.pixels = const_cast(tmpcolorimage.constBits()); + + if (save_depth > 8) { //10bit depth + rgb.depth = 16; + + if (tmpcolorimage.hasAlphaChannel()) { + avif->alphaRange = AVIF_RANGE_FULL; + } else { + rgb.ignoreAlpha = AVIF_TRUE; + } + + rgb.format = AVIF_RGB_FORMAT_RGBA; + } else { //8bit depth + rgb.depth = 8; + + if (tmpcolorimage.hasAlphaChannel()) { + rgb.format = AVIF_RGB_FORMAT_RGBA; + avif->alphaRange = AVIF_RANGE_FULL; + } else { + rgb.format = AVIF_RGB_FORMAT_RGB; + } + } + + res = avifImageRGBToYUV(avif, &rgb); + if (res != AVIF_RESULT_OK) { + qWarning("ERROR in avifImageRGBToYUV: %s\n", avifResultToString(res)); + return false; + } + } + + avifRWData raw = AVIF_DATA_EMPTY; + avifEncoder *encoder = avifEncoderCreate(); + encoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64); + encoder->minQuantizer = minQuantizer; + encoder->maxQuantizer = maxQuantizer; + + if (image.hasAlphaChannel()) { + encoder->minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS; + encoder->maxQuantizerAlpha = maxQuantizerAlpha; + } + + encoder->speed = 8; + + res = avifEncoderWrite(encoder, avif, &raw); + avifEncoderDestroy(encoder); + avifImageDestroy(avif); + + if (res == AVIF_RESULT_OK) { + qint64 status = device()->write((const char *)raw.data, raw.size); + avifRWDataFree(&raw); + + if (status > 0) { + return true; + } else if (status == -1) { + qWarning("Write error: %s\n", qUtf8Printable(device()->errorString())); + return false; + } + } else { + qWarning("ERROR: Failed to encode: %s\n", avifResultToString(res)); + } + + return false; +} + + +QVariant QAVIFHandler::option(ImageOption option) const +{ + if (!supportsOption(option) || !ensureParsed()) { + return QVariant(); + } + + switch (option) { + case Quality: + return m_quality; + case Size: + return m_current_image.size(); + case Animation: + if (imageCount() >= 2) { + return true; + } else { + return false; + } + default: + return QVariant(); + } +} + +void QAVIFHandler::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 = 52; + } + return; + default: + break; + } + QImageIOHandler::setOption(option, value); +} + +bool QAVIFHandler::supportsOption(ImageOption option) const +{ + return option == Quality + || option == Size + || option == Animation; +} + +int QAVIFHandler::imageCount() const +{ + if (!ensureParsed()) { + return 0; + } + + if (m_decoder->imageCount >= 1) { + return m_decoder->imageCount; + } + return 0; +} + +int QAVIFHandler::currentImageNumber() const +{ + if (m_parseState == ParseAvifNotParsed) { + return -1; + } + + if (m_parseState == ParseAvifError || !m_decoder) { + return 0; + } + + return m_decoder->imageIndex; +} + +bool QAVIFHandler::jumpToNextImage() +{ + if (!ensureParsed()) { + return false; + } + + if (m_decoder->imageCount < 2) { + return true; + } + + if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { //start from begining + avifDecoderReset(m_decoder); + } + + avifResult decodeResult = avifDecoderNextImage(m_decoder); + + if (decodeResult != AVIF_RESULT_OK) { + qWarning("ERROR: Failed to decode Next image in sequence: %s\n", avifResultToString(decodeResult)); + m_parseState = ParseAvifError; + return false; + } + + if ((m_container_width != m_decoder->image->width) || + (m_container_height != m_decoder->image->height)) { + qWarning("Decoded image sequence size (%dx%d) do not match first image size (%dx%d)!\n", + m_decoder->image->width, m_decoder->image->height, + m_container_width, m_container_height); + + m_parseState = ParseAvifError; + return false; + } + + if (decode_one_frame()) { + return true; + } else { + m_parseState = ParseAvifError; + return false; + } + +} + +bool QAVIFHandler::jumpToImage(int imageNumber) +{ + if (!ensureParsed()) { + return false; + } + + if (m_decoder->imageCount < 2) { //not an animation + if (imageNumber == 0) { + return true; + } else { + return false; + } + } + + if (imageNumber < 0 || imageNumber >= m_decoder->imageCount) { //wrong index + return false; + } + + if (imageNumber == m_decoder->imageCount) { // we are here already + return true; + } + + avifResult decodeResult = avifDecoderNthImage(m_decoder, imageNumber); + + if (decodeResult != AVIF_RESULT_OK) { + qWarning("ERROR: Failed to decode %d th Image in sequence: %s\n", imageNumber, avifResultToString(decodeResult)); + m_parseState = ParseAvifError; + return false; + } + + if ((m_container_width != m_decoder->image->width) || + (m_container_height != m_decoder->image->height)) { + qWarning("Decoded image sequence size (%dx%d) do not match declared container size (%dx%d)!\n", + m_decoder->image->width, m_decoder->image->height, + m_container_width, m_container_height); + + m_parseState = ParseAvifError; + return false; + } + + if (decode_one_frame()) { + return true; + } else { + m_parseState = ParseAvifError; + return false; + } +} + +int QAVIFHandler::nextImageDelay() const +{ + if (!ensureParsed()) { + return 0; + } + + if (m_decoder->imageCount < 2) { + return 0; + } + + int delay_ms = 1000.0 * m_decoder->imageTiming.duration; + if (delay_ms < 1) { + delay_ms = 1; + } + return delay_ms; +} + +int QAVIFHandler::loopCount() const +{ + if (!ensureParsed()) { + return 0; + } + + if (m_decoder->imageCount < 2) { + return 0; + } + + return 1; +} + +QImageIOPlugin::Capabilities QAVIFPlugin::capabilities(QIODevice *device, const QByteArray &format) const +{ + if (format == "avif") { + return Capabilities(CanRead | CanWrite); + } + + if (format == "avifs") { + return Capabilities(CanRead); + } + + if (!format.isEmpty()) { + return {}; + } + if (!device->isOpen()) { + return {}; + } + + Capabilities cap; + if (device->isReadable() && QAVIFHandler::canRead(device)) { + cap |= CanRead; + } + if (device->isWritable()) { + cap |= CanWrite; + } + return cap; +} + +QImageIOHandler *QAVIFPlugin::create(QIODevice *device, const QByteArray &format) const +{ + QImageIOHandler *handler = new QAVIFHandler; + handler->setDevice(device); + handler->setFormat(format); + return handler; +} diff --git a/src/imageformats/avif.desktop b/src/imageformats/avif.desktop new file mode 100644 index 0000000..fcda177 --- /dev/null +++ b/src/imageformats/avif.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=QImageIOPlugins +X-KDE-ImageFormat=avif +X-KDE-MimeType=image/avif +X-KDE-Read=true +X-KDE-Write=true diff --git a/src/imageformats/avif.json b/src/imageformats/avif.json new file mode 100644 index 0000000..47d06b4 --- /dev/null +++ b/src/imageformats/avif.json @@ -0,0 +1,4 @@ +{ + "Keys": [ "avif", "avifs" ], + "MimeTypes": [ "image/avif", "image/avif-sequence" ] +} diff --git a/src/imageformats/avif_p.h b/src/imageformats/avif_p.h new file mode 100644 index 0000000..6876645 --- /dev/null +++ b/src/imageformats/avif_p.h @@ -0,0 +1,80 @@ +/* + AV1 Image File Format (AVIF) support for QImage. + + SPDX-FileCopyrightText: 2020 Daniel Novomesky + + SPDX-License-Identifier: BSD-2-Clause +*/ + +#ifndef KIMG_AVIF_P_H +#define KIMG_AVIF_P_H + +#include +#include +#include +#include +#include +#include + +class QAVIFHandler : public QImageIOHandler +{ +public: + QAVIFHandler(); + ~QAVIFHandler(); + + 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; + + int imageCount() const override; + int currentImageNumber() const override; + bool jumpToNextImage() override; + bool jumpToImage (int imageNumber) override; + + int nextImageDelay() const override; + + int loopCount() const override; +private: + bool ensureParsed() const; + bool ensureDecoder(); + bool decode_one_frame(); + + enum ParseAvifState + { + ParseAvifError = -1, + ParseAvifNotParsed = 0, + ParseAvifSuccess = 1 + }; + + ParseAvifState m_parseState; + int m_quality; + + uint32_t m_container_width; + uint32_t m_container_height; + + QByteArray m_rawData; + avifROData m_rawAvifData; + + avifDecoder *m_decoder; + QImage m_current_image; + + bool m_must_jump_to_next_image; +}; + +class QAVIFPlugin : public QImageIOPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA (IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "avif.json") + +public: + Capabilities capabilities (QIODevice *device, const QByteArray &format) const override; + QImageIOHandler *create (QIODevice *device, const QByteArray &format = QByteArray()) const override; +}; + +#endif // KIMG_AVIF_P_H