2022-06-08 12:55:18 +00:00
|
|
|
// Copyright (C) 2019 The Qt Company Ltd.
|
2025-01-03 06:12:48 +00:00
|
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
2013-04-25 07:26:53 +00:00
|
|
|
|
|
|
|
#include "qwebphandler_p.h"
|
2019-07-29 11:33:47 +00:00
|
|
|
#include "webp/mux.h"
|
2013-04-25 07:26:53 +00:00
|
|
|
#include "webp/encode.h"
|
2016-03-05 07:40:30 +00:00
|
|
|
#include <qcolor.h>
|
2013-04-25 07:26:53 +00:00
|
|
|
#include <qimage.h>
|
|
|
|
#include <qdebug.h>
|
2016-03-05 07:40:30 +00:00
|
|
|
#include <qpainter.h>
|
2013-04-25 07:26:53 +00:00
|
|
|
#include <qvariant.h>
|
2022-09-08 12:52:19 +00:00
|
|
|
#include <QtEndian>
|
2013-04-25 07:26:53 +00:00
|
|
|
|
|
|
|
static const int riffHeaderSize = 12; // RIFF_HEADER_SIZE from webp/format_constants.h
|
|
|
|
|
|
|
|
QWebpHandler::QWebpHandler() :
|
|
|
|
m_quality(75),
|
2016-03-05 07:40:30 +00:00
|
|
|
m_scanState(ScanNotScanned),
|
2016-11-24 18:36:15 +00:00
|
|
|
m_features(),
|
2019-07-29 11:32:17 +00:00
|
|
|
m_formatFlags(0),
|
2016-03-05 07:40:30 +00:00
|
|
|
m_loop(0),
|
|
|
|
m_frameCount(0),
|
|
|
|
m_demuxer(NULL),
|
|
|
|
m_composited(NULL)
|
2013-04-25 07:26:53 +00:00
|
|
|
{
|
2016-03-05 07:40:30 +00:00
|
|
|
memset(&m_iter, 0, sizeof(m_iter));
|
|
|
|
}
|
|
|
|
|
|
|
|
QWebpHandler::~QWebpHandler()
|
|
|
|
{
|
|
|
|
WebPDemuxReleaseIterator(&m_iter);
|
|
|
|
WebPDemuxDelete(m_demuxer);
|
|
|
|
delete m_composited;
|
2013-04-25 07:26:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool QWebpHandler::canRead() const
|
|
|
|
{
|
|
|
|
if (m_scanState == ScanNotScanned && !canRead(device()))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (m_scanState != ScanError) {
|
|
|
|
setFormat(QByteArrayLiteral("webp"));
|
2017-06-26 14:00:39 +00:00
|
|
|
|
|
|
|
if (m_features.has_animation && m_iter.frame_num >= m_frameCount)
|
|
|
|
return false;
|
|
|
|
|
2013-04-25 07:26:53 +00:00
|
|
|
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;
|
|
|
|
|
2022-09-08 12:52:19 +00:00
|
|
|
QWebpHandler *that = const_cast<QWebpHandler *>(this);
|
|
|
|
const int headerBytesNeeded = sizeof(WebPBitstreamFeatures);
|
|
|
|
QByteArray header = device()->peek(headerBytesNeeded);
|
|
|
|
if (header.size() < headerBytesNeeded)
|
2013-04-25 07:26:53 +00:00
|
|
|
return false;
|
|
|
|
|
2022-09-08 12:52:19 +00:00
|
|
|
// We do no random access during decoding, just a readAll() of the whole image file. So if
|
|
|
|
// if it is all available already, we can accept a sequential device. The riff header contains
|
|
|
|
// the file size minus 8 bytes header
|
|
|
|
qint64 byteSize = qFromLittleEndian<quint32>(header.constData() + 4);
|
|
|
|
if (device()->isSequential() && device()->bytesAvailable() < byteSize + 8) {
|
|
|
|
qWarning() << "QWebpHandler: Insufficient data available in sequential device";
|
|
|
|
return false;
|
|
|
|
}
|
2016-03-05 07:40:30 +00:00
|
|
|
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
|
|
|
|
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)));
|
|
|
|
|
2021-03-01 16:27:21 +00:00
|
|
|
QSize sz(that->m_features.width, that->m_features.height);
|
|
|
|
that->m_composited = new QImage;
|
|
|
|
if (!QImageIOHandler::allocateImage(sz, QImage::Format_ARGB32, that->m_composited))
|
|
|
|
return false;
|
2017-06-26 13:27:12 +00:00
|
|
|
if (that->m_features.has_alpha)
|
|
|
|
that->m_composited->fill(Qt::transparent);
|
2016-03-05 07:40:30 +00:00
|
|
|
|
|
|
|
m_scanState = ScanSuccess;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
m_scanState = ScanSuccess;
|
|
|
|
}
|
|
|
|
}
|
2013-04-25 07:26:53 +00:00
|
|
|
|
|
|
|
return m_scanState == ScanSuccess;
|
|
|
|
}
|
|
|
|
|
2016-03-05 07:40:30 +00:00
|
|
|
bool QWebpHandler::ensureDemuxer()
|
|
|
|
{
|
|
|
|
if (m_demuxer)
|
|
|
|
return true;
|
|
|
|
|
|
|
|
m_rawData = device()->readAll();
|
|
|
|
m_webpData.bytes = reinterpret_cast<const uint8_t *>(m_rawData.constData());
|
|
|
|
m_webpData.size = m_rawData.size();
|
|
|
|
|
|
|
|
m_demuxer = WebPDemux(&m_webpData);
|
|
|
|
if (m_demuxer == NULL)
|
|
|
|
return false;
|
|
|
|
|
2019-07-29 11:32:17 +00:00
|
|
|
m_formatFlags = WebPDemuxGetI(m_demuxer, WEBP_FF_FORMAT_FLAGS);
|
2016-03-05 07:40:30 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2013-04-25 07:26:53 +00:00
|
|
|
bool QWebpHandler::read(QImage *image)
|
|
|
|
{
|
2022-09-08 12:52:19 +00:00
|
|
|
if (!ensureScanned() || !ensureDemuxer())
|
2016-03-05 07:40:30 +00:00
|
|
|
return false;
|
|
|
|
|
2020-01-10 20:52:31 +00:00
|
|
|
QRect prevFrameRect;
|
2016-03-05 07:40:30 +00:00
|
|
|
if (m_iter.frame_num == 0) {
|
2019-07-29 11:32:17 +00:00
|
|
|
// Read global meta-data chunks first
|
|
|
|
WebPChunkIterator metaDataIter;
|
|
|
|
if ((m_formatFlags & ICCP_FLAG) && WebPDemuxGetChunk(m_demuxer, "ICCP", 1, &metaDataIter)) {
|
2020-05-18 11:51:09 +00:00
|
|
|
QByteArray iccProfile = QByteArray::fromRawData(reinterpret_cast<const char *>(metaDataIter.chunk.bytes),
|
|
|
|
metaDataIter.chunk.size);
|
|
|
|
// Ensure the profile is 4-byte aligned.
|
|
|
|
if (reinterpret_cast<qintptr>(iccProfile.constData()) & 0x3)
|
|
|
|
iccProfile.detach();
|
2019-07-29 11:32:17 +00:00
|
|
|
m_colorSpace = QColorSpace::fromIccProfile(iccProfile);
|
|
|
|
// ### consider parsing EXIF and/or XMP metadata too.
|
|
|
|
WebPDemuxReleaseChunkIterator(&metaDataIter);
|
|
|
|
}
|
|
|
|
|
2016-03-05 07:40:30 +00:00
|
|
|
// Go to first frame
|
|
|
|
if (!WebPDemuxGetFrame(m_demuxer, 1, &m_iter))
|
|
|
|
return false;
|
|
|
|
} else {
|
2020-01-10 20:52:31 +00:00
|
|
|
if (m_iter.has_alpha && m_iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND)
|
|
|
|
prevFrameRect = currentImageRect();
|
|
|
|
|
2016-03-05 07:40:30 +00:00
|
|
|
// 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)
|
2013-04-25 07:26:53 +00:00
|
|
|
return false;
|
|
|
|
|
2018-05-25 14:15:14 +00:00
|
|
|
QImage::Format format = m_features.has_alpha ? QImage::Format_ARGB32 : QImage::Format_RGB32;
|
2021-03-01 16:27:21 +00:00
|
|
|
QImage frame;
|
|
|
|
if (!QImageIOHandler::allocateImage(QSize(m_iter.width, m_iter.height), format, &frame))
|
|
|
|
return false;
|
2016-03-05 07:40:30 +00:00
|
|
|
uint8_t *output = frame.bits();
|
2017-07-11 08:44:30 +00:00
|
|
|
size_t output_size = frame.sizeInBytes();
|
2013-04-25 07:26:53 +00:00
|
|
|
#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
|
2016-03-05 07:40:30 +00:00
|
|
|
if (!WebPDecodeBGRAInto(
|
|
|
|
reinterpret_cast<const uint8_t*>(m_iter.fragment.bytes), m_iter.fragment.size,
|
|
|
|
output, output_size, frame.bytesPerLine()))
|
2013-04-25 07:26:53 +00:00
|
|
|
#else
|
2016-03-05 07:40:30 +00:00
|
|
|
if (!WebPDecodeARGBInto(
|
|
|
|
reinterpret_cast<const uint8_t*>(m_iter.fragment.bytes), m_iter.fragment.size,
|
|
|
|
output, output_size, frame.bytesPerLine()))
|
2013-04-25 07:26:53 +00:00
|
|
|
#endif
|
|
|
|
return false;
|
|
|
|
|
2016-03-05 07:40:30 +00:00
|
|
|
if (!m_features.has_animation) {
|
|
|
|
// Single image
|
|
|
|
*image = frame;
|
|
|
|
} else {
|
|
|
|
// Animation
|
|
|
|
QPainter painter(m_composited);
|
2020-01-10 20:52:31 +00:00
|
|
|
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);
|
|
|
|
}
|
2016-03-05 07:40:30 +00:00
|
|
|
painter.drawImage(currentImageRect(), frame);
|
|
|
|
|
|
|
|
*image = *m_composited;
|
|
|
|
}
|
2019-07-29 11:32:17 +00:00
|
|
|
image->setColorSpace(m_colorSpace);
|
2016-03-05 07:40:30 +00:00
|
|
|
|
2013-04-25 07:26:53 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool QWebpHandler::write(const QImage &image)
|
|
|
|
{
|
|
|
|
if (image.isNull()) {
|
|
|
|
qWarning() << "source image is null.";
|
|
|
|
return false;
|
|
|
|
}
|
2019-07-29 08:56:03 +00:00
|
|
|
if (std::max(image.width(), image.height()) > WEBP_MAX_DIMENSION) {
|
|
|
|
qWarning() << "QWebpHandler::write() source image too large for WebP: " << image.size();
|
|
|
|
return false;
|
|
|
|
}
|
2013-04-25 07:26:53 +00:00
|
|
|
|
|
|
|
QImage srcImage = image;
|
2018-05-25 14:15:14 +00:00
|
|
|
bool alpha = srcImage.hasAlphaChannel();
|
|
|
|
QImage::Format newFormat = alpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888;
|
|
|
|
if (srcImage.format() != newFormat)
|
|
|
|
srcImage = srcImage.convertToFormat(newFormat);
|
2013-04-25 07:26:53 +00:00
|
|
|
|
|
|
|
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;
|
2018-05-25 14:15:14 +00:00
|
|
|
bool failed = false;
|
|
|
|
if (alpha)
|
|
|
|
failed = !WebPPictureImportRGBA(&picture, srcImage.bits(), srcImage.bytesPerLine());
|
|
|
|
else
|
|
|
|
failed = !WebPPictureImportRGB(&picture, srcImage.bits(), srcImage.bytesPerLine());
|
2013-04-25 07:26:53 +00:00
|
|
|
|
2018-05-25 14:15:14 +00:00
|
|
|
if (failed) {
|
|
|
|
qWarning() << "failed to import image data to webp picture.";
|
2013-04-25 07:26:53 +00:00
|
|
|
WebPPictureFree(&picture);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-05-28 13:05:13 +00:00
|
|
|
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;
|
2019-07-29 11:33:47 +00:00
|
|
|
WebPMemoryWriter writer;
|
|
|
|
WebPMemoryWriterInit(&writer);
|
|
|
|
picture.writer = WebPMemoryWrite;
|
|
|
|
picture.custom_ptr = &writer;
|
2013-04-25 07:26:53 +00:00
|
|
|
|
|
|
|
if (!WebPEncode(&config, &picture)) {
|
|
|
|
qWarning() << "failed to encode webp picture, error code: " << picture.error_code;
|
|
|
|
WebPPictureFree(&picture);
|
2020-05-18 11:51:09 +00:00
|
|
|
WebPMemoryWriterClear(&writer);
|
2013-04-25 07:26:53 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-07-29 11:33:47 +00:00
|
|
|
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<const uint8_t *>(iccProfile.constData()),
|
|
|
|
static_cast<size_t>(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<size_t>(device()->write(reinterpret_cast<const char *>(output_data.bytes), output_data.size)));
|
|
|
|
}
|
|
|
|
WebPDataClear(&output_data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
WebPMuxDelete(mux);
|
|
|
|
}
|
|
|
|
if (!res) {
|
|
|
|
res = (writer.size ==
|
|
|
|
static_cast<size_t>(device()->write(reinterpret_cast<const char *>(writer.mem), writer.size)));
|
|
|
|
}
|
2013-04-25 07:26:53 +00:00
|
|
|
WebPPictureFree(&picture);
|
2020-05-18 11:51:09 +00:00
|
|
|
WebPMemoryWriterClear(&writer);
|
2013-04-25 07:26:53 +00:00
|
|
|
|
2019-07-29 11:33:47 +00:00
|
|
|
return res;
|
2013-04-25 07:26:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2016-03-05 07:40:30 +00:00
|
|
|
case Animation:
|
|
|
|
return m_features.has_animation;
|
|
|
|
case BackgroundColor:
|
|
|
|
return m_bgColor;
|
2013-04-25 07:26:53 +00:00
|
|
|
default:
|
|
|
|
return QVariant();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void QWebpHandler::setOption(ImageOption option, const QVariant &value)
|
|
|
|
{
|
|
|
|
switch (option) {
|
|
|
|
case Quality:
|
2017-11-13 10:40:25 +00:00
|
|
|
m_quality = value.toInt();
|
2013-04-25 07:26:53 +00:00
|
|
|
return;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
2017-10-06 11:11:45 +00:00
|
|
|
QImageIOHandler::setOption(option, value);
|
2013-04-25 07:26:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool QWebpHandler::supportsOption(ImageOption option) const
|
|
|
|
{
|
2016-03-05 07:40:30 +00:00
|
|
|
return option == Quality
|
|
|
|
|| option == Size
|
|
|
|
|| option == Animation
|
|
|
|
|| option == BackgroundColor;
|
2013-04-25 07:26:53 +00:00
|
|
|
}
|
|
|
|
|
2016-03-05 07:40:30 +00:00
|
|
|
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;
|
|
|
|
}
|