blob: f0b8cafe6a5b511ddc4f83002d5479493a95a5c6 [file] [log] [blame]
/****************************************************************************
**
** 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 <qcolor.h>
#include <qimage.h>
#include <qdebug.h>
#include <qpainter.h>
#include <qvariant.h>
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<QWebpHandler *>(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<const uint8_t *>(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;
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<const char *>(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 {
// 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<const uint8_t*>(m_iter.fragment.bytes), m_iter.fragment.size,
output, output_size, frame.bytesPerLine()))
#else
if (!WebPDecodeARGBInto(
reinterpret_cast<const uint8_t*>(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 (m_features.has_alpha && m_iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND)
m_composited->fill(Qt::transparent);
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<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)));
}
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;
}