1/****************************************************************************
2**
3** Copyright (C) 2019 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the WebP plugins in the Qt ImageFormats module.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qwebphandler_p.h"
41#include "webp/mux.h"
42#include "webp/encode.h"
43#include <qcolor.h>
44#include <qimage.h>
45#include <qdebug.h>
46#include <qpainter.h>
47#include <qvariant.h>
48
49static const int riffHeaderSize = 12; // RIFF_HEADER_SIZE from webp/format_constants.h
50
51QWebpHandler::QWebpHandler() :
52 m_quality(75),
53 m_scanState(ScanNotScanned),
54 m_features(),
55 m_formatFlags(0),
56 m_loop(0),
57 m_frameCount(0),
58 m_demuxer(NULL),
59 m_composited(NULL)
60{
61 memset(s: &m_iter, c: 0, n: sizeof(m_iter));
62}
63
64QWebpHandler::~QWebpHandler()
65{
66 WebPDemuxReleaseIterator(iter: &m_iter);
67 WebPDemuxDelete(dmux: m_demuxer);
68 delete m_composited;
69}
70
71bool QWebpHandler::canRead() const
72{
73 if (m_scanState == ScanNotScanned && !canRead(device: device()))
74 return false;
75
76 if (m_scanState != ScanError) {
77 setFormat(QByteArrayLiteral("webp"));
78
79 if (m_features.has_animation && m_iter.frame_num >= m_frameCount)
80 return false;
81
82 return true;
83 }
84 return false;
85}
86
87bool QWebpHandler::canRead(QIODevice *device)
88{
89 if (!device) {
90 qWarning(msg: "QWebpHandler::canRead() called with no device");
91 return false;
92 }
93
94 QByteArray header = device->peek(maxlen: riffHeaderSize);
95 return header.startsWith(c: "RIFF") && header.endsWith(c: "WEBP");
96}
97
98bool QWebpHandler::ensureScanned() const
99{
100 if (m_scanState != ScanNotScanned)
101 return m_scanState == ScanSuccess;
102
103 m_scanState = ScanError;
104
105 if (device()->isSequential()) {
106 qWarning() << "Sequential devices are not supported";
107 return false;
108 }
109
110 qint64 oldPos = device()->pos();
111 device()->seek(pos: 0);
112
113 QWebpHandler *that = const_cast<QWebpHandler *>(this);
114 QByteArray header = device()->peek(maxlen: sizeof(WebPBitstreamFeatures));
115 if (WebPGetFeatures(data: (const uint8_t*)header.constData(), data_size: header.size(), features: &(that->m_features)) == VP8_STATUS_OK) {
116 if (m_features.has_animation) {
117 // For animation, we have to read and scan whole file to determine loop count and images count
118 device()->seek(pos: oldPos);
119
120 if (that->ensureDemuxer()) {
121 that->m_loop = WebPDemuxGetI(dmux: m_demuxer, feature: WEBP_FF_LOOP_COUNT);
122 that->m_frameCount = WebPDemuxGetI(dmux: m_demuxer, feature: WEBP_FF_FRAME_COUNT);
123 that->m_bgColor = QColor::fromRgba(rgba: QRgb(WebPDemuxGetI(dmux: m_demuxer, feature: WEBP_FF_BACKGROUND_COLOR)));
124
125 that->m_composited = new QImage(that->m_features.width, that->m_features.height, QImage::Format_ARGB32);
126 if (that->m_features.has_alpha)
127 that->m_composited->fill(color: Qt::transparent);
128
129 // We do not reset device position since we have read in all data
130 m_scanState = ScanSuccess;
131 return true;
132 }
133 } else {
134 m_scanState = ScanSuccess;
135 }
136 }
137
138 device()->seek(pos: oldPos);
139
140 return m_scanState == ScanSuccess;
141}
142
143bool QWebpHandler::ensureDemuxer()
144{
145 if (m_demuxer)
146 return true;
147
148 m_rawData = device()->readAll();
149 m_webpData.bytes = reinterpret_cast<const uint8_t *>(m_rawData.constData());
150 m_webpData.size = m_rawData.size();
151
152 m_demuxer = WebPDemux(data: &m_webpData);
153 if (m_demuxer == NULL)
154 return false;
155
156 m_formatFlags = WebPDemuxGetI(dmux: m_demuxer, feature: WEBP_FF_FORMAT_FLAGS);
157 return true;
158}
159
160bool QWebpHandler::read(QImage *image)
161{
162 if (!ensureScanned() || device()->isSequential() || !ensureDemuxer())
163 return false;
164
165 QRect prevFrameRect;
166 if (m_iter.frame_num == 0) {
167 // Read global meta-data chunks first
168 WebPChunkIterator metaDataIter;
169 if ((m_formatFlags & ICCP_FLAG) && WebPDemuxGetChunk(dmux: m_demuxer, fourcc: "ICCP", chunk_number: 1, iter: &metaDataIter)) {
170 QByteArray iccProfile = QByteArray::fromRawData(reinterpret_cast<const char *>(metaDataIter.chunk.bytes),
171 size: metaDataIter.chunk.size);
172 // Ensure the profile is 4-byte aligned.
173 if (reinterpret_cast<qintptr>(iccProfile.constData()) & 0x3)
174 iccProfile.detach();
175 m_colorSpace = QColorSpace::fromIccProfile(iccProfile);
176 // ### consider parsing EXIF and/or XMP metadata too.
177 WebPDemuxReleaseChunkIterator(iter: &metaDataIter);
178 }
179
180 // Go to first frame
181 if (!WebPDemuxGetFrame(dmux: m_demuxer, frame_number: 1, iter: &m_iter))
182 return false;
183 } else {
184 if (m_iter.has_alpha && m_iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND)
185 prevFrameRect = currentImageRect();
186
187 // Go to next frame
188 if (!WebPDemuxNextFrame(iter: &m_iter))
189 return false;
190 }
191
192 WebPBitstreamFeatures features;
193 VP8StatusCode status = WebPGetFeatures(data: m_iter.fragment.bytes, data_size: m_iter.fragment.size, features: &features);
194 if (status != VP8_STATUS_OK)
195 return false;
196
197 QImage::Format format = m_features.has_alpha ? QImage::Format_ARGB32 : QImage::Format_RGB32;
198 QImage frame(m_iter.width, m_iter.height, format);
199 uint8_t *output = frame.bits();
200 size_t output_size = frame.sizeInBytes();
201#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
202 if (!WebPDecodeBGRAInto(
203 data: reinterpret_cast<const uint8_t*>(m_iter.fragment.bytes), data_size: m_iter.fragment.size,
204 output_buffer: output, output_buffer_size: output_size, output_stride: frame.bytesPerLine()))
205#else
206 if (!WebPDecodeARGBInto(
207 reinterpret_cast<const uint8_t*>(m_iter.fragment.bytes), m_iter.fragment.size,
208 output, output_size, frame.bytesPerLine()))
209#endif
210 return false;
211
212 if (!m_features.has_animation) {
213 // Single image
214 *image = frame;
215 } else {
216 // Animation
217 QPainter painter(m_composited);
218 if (!prevFrameRect.isEmpty()) {
219 painter.setCompositionMode(QPainter::CompositionMode_Clear);
220 painter.fillRect(r: prevFrameRect, c: Qt::black);
221 }
222 if (m_features.has_alpha) {
223 if (m_iter.blend_method == WEBP_MUX_NO_BLEND)
224 painter.setCompositionMode(QPainter::CompositionMode_Source);
225 else
226 painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
227 }
228 painter.drawImage(r: currentImageRect(), image: frame);
229
230 *image = *m_composited;
231 }
232 image->setColorSpace(m_colorSpace);
233
234 return true;
235}
236
237bool QWebpHandler::write(const QImage &image)
238{
239 if (image.isNull()) {
240 qWarning() << "source image is null.";
241 return false;
242 }
243 if (std::max(a: image.width(), b: image.height()) > WEBP_MAX_DIMENSION) {
244 qWarning() << "QWebpHandler::write() source image too large for WebP: " << image.size();
245 return false;
246 }
247
248 QImage srcImage = image;
249 bool alpha = srcImage.hasAlphaChannel();
250 QImage::Format newFormat = alpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888;
251 if (srcImage.format() != newFormat)
252 srcImage = srcImage.convertToFormat(f: newFormat);
253
254 WebPPicture picture;
255 WebPConfig config;
256
257 if (!WebPPictureInit(picture: &picture) || !WebPConfigInit(config: &config)) {
258 qWarning() << "failed to init webp picture and config";
259 return false;
260 }
261
262 picture.width = srcImage.width();
263 picture.height = srcImage.height();
264 picture.use_argb = 1;
265 bool failed = false;
266 if (alpha)
267 failed = !WebPPictureImportRGBA(picture: &picture, rgba: srcImage.bits(), rgba_stride: srcImage.bytesPerLine());
268 else
269 failed = !WebPPictureImportRGB(picture: &picture, rgb: srcImage.bits(), rgb_stride: srcImage.bytesPerLine());
270
271 if (failed) {
272 qWarning() << "failed to import image data to webp picture.";
273 WebPPictureFree(picture: &picture);
274 return false;
275 }
276
277 int reqQuality = m_quality < 0 ? 75 : qMin(a: m_quality, b: 100);
278 if (reqQuality < 100) {
279 config.lossless = 0;
280 config.quality = reqQuality;
281 } else {
282 config.lossless = 1;
283 config.quality = 70; // For lossless, specifies compression effort; 70 is libwebp default
284 }
285 config.alpha_quality = config.quality;
286 WebPMemoryWriter writer;
287 WebPMemoryWriterInit(writer: &writer);
288 picture.writer = WebPMemoryWrite;
289 picture.custom_ptr = &writer;
290
291 if (!WebPEncode(config: &config, picture: &picture)) {
292 qWarning() << "failed to encode webp picture, error code: " << picture.error_code;
293 WebPPictureFree(picture: &picture);
294 WebPMemoryWriterClear(writer: &writer);
295 return false;
296 }
297
298 bool res = false;
299 if (image.colorSpace().isValid()) {
300 int copy_data = 0;
301 WebPMux *mux = WebPMuxNew();
302 WebPData image_data = { .bytes: writer.mem, .size: writer.size };
303 WebPMuxSetImage(mux, bitstream: &image_data, copy_data);
304 uint8_t vp8xChunk[10];
305 uint8_t flags = 0x20; // Has ICCP chunk, no XMP, EXIF or animation.
306 if (image.hasAlphaChannel())
307 flags |= 0x10;
308 vp8xChunk[0] = flags;
309 vp8xChunk[1] = 0;
310 vp8xChunk[2] = 0;
311 vp8xChunk[3] = 0;
312 const unsigned width = image.width() - 1;
313 const unsigned height = image.height() - 1;
314 vp8xChunk[4] = width & 0xff;
315 vp8xChunk[5] = (width >> 8) & 0xff;
316 vp8xChunk[6] = (width >> 16) & 0xff;
317 vp8xChunk[7] = height & 0xff;
318 vp8xChunk[8] = (height >> 8) & 0xff;
319 vp8xChunk[9] = (height >> 16) & 0xff;
320 WebPData vp8x_data = { .bytes: vp8xChunk, .size: 10 };
321 if (WebPMuxSetChunk(mux, fourcc: "VP8X", chunk_data: &vp8x_data, copy_data) == WEBP_MUX_OK) {
322 QByteArray iccProfile = image.colorSpace().iccProfile();
323 WebPData iccp_data = {
324 .bytes: reinterpret_cast<const uint8_t *>(iccProfile.constData()),
325 .size: static_cast<size_t>(iccProfile.size())
326 };
327 if (WebPMuxSetChunk(mux, fourcc: "ICCP", chunk_data: &iccp_data, copy_data) == WEBP_MUX_OK) {
328 WebPData output_data;
329 if (WebPMuxAssemble(mux, assembled_data: &output_data) == WEBP_MUX_OK) {
330 res = (output_data.size ==
331 static_cast<size_t>(device()->write(data: reinterpret_cast<const char *>(output_data.bytes), len: output_data.size)));
332 }
333 WebPDataClear(webp_data: &output_data);
334 }
335 }
336 WebPMuxDelete(mux);
337 }
338 if (!res) {
339 res = (writer.size ==
340 static_cast<size_t>(device()->write(data: reinterpret_cast<const char *>(writer.mem), len: writer.size)));
341 }
342 WebPPictureFree(picture: &picture);
343 WebPMemoryWriterClear(writer: &writer);
344
345 return res;
346}
347
348QVariant QWebpHandler::option(ImageOption option) const
349{
350 if (!supportsOption(option) || !ensureScanned())
351 return QVariant();
352
353 switch (option) {
354 case Quality:
355 return m_quality;
356 case Size:
357 return QSize(m_features.width, m_features.height);
358 case Animation:
359 return m_features.has_animation;
360 case BackgroundColor:
361 return m_bgColor;
362 default:
363 return QVariant();
364 }
365}
366
367void QWebpHandler::setOption(ImageOption option, const QVariant &value)
368{
369 switch (option) {
370 case Quality:
371 m_quality = value.toInt();
372 return;
373 default:
374 break;
375 }
376 QImageIOHandler::setOption(option, value);
377}
378
379bool QWebpHandler::supportsOption(ImageOption option) const
380{
381 return option == Quality
382 || option == Size
383 || option == Animation
384 || option == BackgroundColor;
385}
386
387int QWebpHandler::imageCount() const
388{
389 if (!ensureScanned())
390 return 0;
391
392 if (!m_features.has_animation)
393 return 1;
394
395 return m_frameCount;
396}
397
398int QWebpHandler::currentImageNumber() const
399{
400 if (!ensureScanned() || !m_features.has_animation)
401 return 0;
402
403 // Frame number in WebP starts from 1
404 return m_iter.frame_num - 1;
405}
406
407QRect QWebpHandler::currentImageRect() const
408{
409 if (!ensureScanned())
410 return QRect();
411
412 return QRect(m_iter.x_offset, m_iter.y_offset, m_iter.width, m_iter.height);
413}
414
415int QWebpHandler::loopCount() const
416{
417 if (!ensureScanned() || !m_features.has_animation)
418 return 0;
419
420 // Loop count in WebP starts from 0
421 return m_loop - 1;
422}
423
424int QWebpHandler::nextImageDelay() const
425{
426 if (!ensureScanned() || !m_features.has_animation)
427 return 0;
428
429 return m_iter.duration;
430}
431

source code of qtimageformats/src/plugins/imageformats/webp/qwebphandler.cpp