/**************************************************************************** ** ** Copyright (C) 2019 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the WebP plugins in the Qt ImageFormats module. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qwebphandler_p.h" #include "webp/mux.h" #include "webp/encode.h" #include #include #include #include #include static const int riffHeaderSize = 12; // RIFF_HEADER_SIZE from webp/format_constants.h QWebpHandler::QWebpHandler() : m_quality(75), m_scanState(ScanNotScanned), m_features(), m_formatFlags(0), m_loop(0), m_frameCount(0), m_demuxer(NULL), m_composited(NULL) { memset(&m_iter, 0, sizeof(m_iter)); } QWebpHandler::~QWebpHandler() { WebPDemuxReleaseIterator(&m_iter); WebPDemuxDelete(m_demuxer); delete m_composited; } bool QWebpHandler::canRead() const { if (m_scanState == ScanNotScanned && !canRead(device())) return false; if (m_scanState != ScanError) { setFormat(QByteArrayLiteral("webp")); if (m_features.has_animation && m_iter.frame_num >= m_frameCount) return false; return true; } return false; } bool QWebpHandler::canRead(QIODevice *device) { if (!device) { qWarning("QWebpHandler::canRead() called with no device"); return false; } QByteArray header = device->peek(riffHeaderSize); return header.startsWith("RIFF") && header.endsWith("WEBP"); } bool QWebpHandler::ensureScanned() const { if (m_scanState != ScanNotScanned) return m_scanState == ScanSuccess; m_scanState = ScanError; if (device()->isSequential()) { qWarning() << "Sequential devices are not supported"; return false; } qint64 oldPos = device()->pos(); device()->seek(0); QWebpHandler *that = const_cast(this); QByteArray header = device()->peek(sizeof(WebPBitstreamFeatures)); if (WebPGetFeatures((const uint8_t*)header.constData(), header.size(), &(that->m_features)) == VP8_STATUS_OK) { if (m_features.has_animation) { // For animation, we have to read and scan whole file to determine loop count and images count device()->seek(oldPos); if (that->ensureDemuxer()) { that->m_loop = WebPDemuxGetI(m_demuxer, WEBP_FF_LOOP_COUNT); that->m_frameCount = WebPDemuxGetI(m_demuxer, WEBP_FF_FRAME_COUNT); that->m_bgColor = QColor::fromRgba(QRgb(WebPDemuxGetI(m_demuxer, WEBP_FF_BACKGROUND_COLOR))); that->m_composited = new QImage(that->m_features.width, that->m_features.height, QImage::Format_ARGB32); if (that->m_features.has_alpha) that->m_composited->fill(Qt::transparent); // We do not reset device position since we have read in all data m_scanState = ScanSuccess; return true; } } else { m_scanState = ScanSuccess; } } device()->seek(oldPos); return m_scanState == ScanSuccess; } bool QWebpHandler::ensureDemuxer() { if (m_demuxer) return true; m_rawData = device()->readAll(); m_webpData.bytes = reinterpret_cast(m_rawData.constData()); m_webpData.size = m_rawData.size(); m_demuxer = WebPDemux(&m_webpData); if (m_demuxer == NULL) return false; m_formatFlags = WebPDemuxGetI(m_demuxer, WEBP_FF_FORMAT_FLAGS); return true; } bool QWebpHandler::read(QImage *image) { if (!ensureScanned() || device()->isSequential() || !ensureDemuxer()) return false; QRect prevFrameRect; if (m_iter.frame_num == 0) { // Read global meta-data chunks first WebPChunkIterator metaDataIter; if ((m_formatFlags & ICCP_FLAG) && WebPDemuxGetChunk(m_demuxer, "ICCP", 1, &metaDataIter)) { const QByteArray iccProfile = QByteArray::fromRawData(reinterpret_cast(metaDataIter.chunk.bytes), metaDataIter.chunk.size); m_colorSpace = QColorSpace::fromIccProfile(iccProfile); // ### consider parsing EXIF and/or XMP metadata too. WebPDemuxReleaseChunkIterator(&metaDataIter); } // Go to first frame if (!WebPDemuxGetFrame(m_demuxer, 1, &m_iter)) return false; } else { if (m_iter.has_alpha && m_iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) prevFrameRect = currentImageRect(); // Go to next frame if (!WebPDemuxNextFrame(&m_iter)) return false; } WebPBitstreamFeatures features; VP8StatusCode status = WebPGetFeatures(m_iter.fragment.bytes, m_iter.fragment.size, &features); if (status != VP8_STATUS_OK) return false; QImage::Format format = m_features.has_alpha ? QImage::Format_ARGB32 : QImage::Format_RGB32; QImage frame(m_iter.width, m_iter.height, format); uint8_t *output = frame.bits(); size_t output_size = frame.sizeInBytes(); #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN if (!WebPDecodeBGRAInto( reinterpret_cast(m_iter.fragment.bytes), m_iter.fragment.size, output, output_size, frame.bytesPerLine())) #else if (!WebPDecodeARGBInto( reinterpret_cast(m_iter.fragment.bytes), m_iter.fragment.size, output, output_size, frame.bytesPerLine())) #endif return false; if (!m_features.has_animation) { // Single image *image = frame; } else { // Animation QPainter painter(m_composited); if (!prevFrameRect.isEmpty()) { painter.setCompositionMode(QPainter::CompositionMode_Clear); painter.fillRect(prevFrameRect, Qt::black); } if (m_features.has_alpha) { if (m_iter.blend_method == WEBP_MUX_NO_BLEND) painter.setCompositionMode(QPainter::CompositionMode_Source); else painter.setCompositionMode(QPainter::CompositionMode_SourceOver); } painter.drawImage(currentImageRect(), frame); *image = *m_composited; } image->setColorSpace(m_colorSpace); return true; } bool QWebpHandler::write(const QImage &image) { if (image.isNull()) { qWarning() << "source image is null."; return false; } if (std::max(image.width(), image.height()) > WEBP_MAX_DIMENSION) { qWarning() << "QWebpHandler::write() source image too large for WebP: " << image.size(); return false; } QImage srcImage = image; bool alpha = srcImage.hasAlphaChannel(); QImage::Format newFormat = alpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888; if (srcImage.format() != newFormat) srcImage = srcImage.convertToFormat(newFormat); WebPPicture picture; WebPConfig config; if (!WebPPictureInit(&picture) || !WebPConfigInit(&config)) { qWarning() << "failed to init webp picture and config"; return false; } picture.width = srcImage.width(); picture.height = srcImage.height(); picture.use_argb = 1; bool failed = false; if (alpha) failed = !WebPPictureImportRGBA(&picture, srcImage.bits(), srcImage.bytesPerLine()); else failed = !WebPPictureImportRGB(&picture, srcImage.bits(), srcImage.bytesPerLine()); if (failed) { qWarning() << "failed to import image data to webp picture."; WebPPictureFree(&picture); return false; } int reqQuality = m_quality < 0 ? 75 : qMin(m_quality, 100); if (reqQuality < 100) { config.lossless = 0; config.quality = reqQuality; } else { config.lossless = 1; config.quality = 70; // For lossless, specifies compression effort; 70 is libwebp default } config.alpha_quality = config.quality; WebPMemoryWriter writer; WebPMemoryWriterInit(&writer); picture.writer = WebPMemoryWrite; picture.custom_ptr = &writer; if (!WebPEncode(&config, &picture)) { qWarning() << "failed to encode webp picture, error code: " << picture.error_code; WebPPictureFree(&picture); return false; } bool res = false; if (image.colorSpace().isValid()) { int copy_data = 0; WebPMux *mux = WebPMuxNew(); WebPData image_data = { writer.mem, writer.size }; WebPMuxSetImage(mux, &image_data, copy_data); uint8_t vp8xChunk[10]; uint8_t flags = 0x20; // Has ICCP chunk, no XMP, EXIF or animation. if (image.hasAlphaChannel()) flags |= 0x10; vp8xChunk[0] = flags; vp8xChunk[1] = 0; vp8xChunk[2] = 0; vp8xChunk[3] = 0; const unsigned width = image.width() - 1; const unsigned height = image.height() - 1; vp8xChunk[4] = width & 0xff; vp8xChunk[5] = (width >> 8) & 0xff; vp8xChunk[6] = (width >> 16) & 0xff; vp8xChunk[7] = height & 0xff; vp8xChunk[8] = (height >> 8) & 0xff; vp8xChunk[9] = (height >> 16) & 0xff; WebPData vp8x_data = { vp8xChunk, 10 }; if (WebPMuxSetChunk(mux, "VP8X", &vp8x_data, copy_data) == WEBP_MUX_OK) { QByteArray iccProfile = image.colorSpace().iccProfile(); WebPData iccp_data = { reinterpret_cast(iccProfile.constData()), static_cast(iccProfile.size()) }; if (WebPMuxSetChunk(mux, "ICCP", &iccp_data, copy_data) == WEBP_MUX_OK) { WebPData output_data; if (WebPMuxAssemble(mux, &output_data) == WEBP_MUX_OK) { res = (output_data.size == static_cast(device()->write(reinterpret_cast(output_data.bytes), output_data.size))); } WebPDataClear(&output_data); } } WebPMuxDelete(mux); } if (!res) { res = (writer.size == static_cast(device()->write(reinterpret_cast(writer.mem), writer.size))); } WebPPictureFree(&picture); return res; } QVariant QWebpHandler::option(ImageOption option) const { if (!supportsOption(option) || !ensureScanned()) return QVariant(); switch (option) { case Quality: return m_quality; case Size: return QSize(m_features.width, m_features.height); case Animation: return m_features.has_animation; case BackgroundColor: return m_bgColor; default: return QVariant(); } } void QWebpHandler::setOption(ImageOption option, const QVariant &value) { switch (option) { case Quality: m_quality = value.toInt(); return; default: break; } QImageIOHandler::setOption(option, value); } bool QWebpHandler::supportsOption(ImageOption option) const { return option == Quality || option == Size || option == Animation || option == BackgroundColor; } #if QT_DEPRECATED_SINCE(5, 13) QByteArray QWebpHandler::name() const { return QByteArrayLiteral("webp"); } #endif int QWebpHandler::imageCount() const { if (!ensureScanned()) return 0; if (!m_features.has_animation) return 1; return m_frameCount; } int QWebpHandler::currentImageNumber() const { if (!ensureScanned() || !m_features.has_animation) return 0; // Frame number in WebP starts from 1 return m_iter.frame_num - 1; } QRect QWebpHandler::currentImageRect() const { if (!ensureScanned()) return QRect(); return QRect(m_iter.x_offset, m_iter.y_offset, m_iter.width, m_iter.height); } int QWebpHandler::loopCount() const { if (!ensureScanned() || !m_features.has_animation) return 0; // Loop count in WebP starts from 0 return m_loop - 1; } int QWebpHandler::nextImageDelay() const { if (!ensureScanned() || !m_features.has_animation) return 0; return m_iter.duration; }