Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qwebphandler.cpp
Go to the documentation of this file.
1// Copyright (C) 2019 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
6#include "webp/mux.h"
7#include "webp/encode.h"
8#include <qcolor.h>
9#include <qimage.h>
10#include <qdebug.h>
11#include <qpainter.h>
12#include <qvariant.h>
13#include <QtEndian>
14
15static const int riffHeaderSize = 12; // RIFF_HEADER_SIZE from webp/format_constants.h
16
17QT_BEGIN_NAMESPACE
18
19QWebpHandler::QWebpHandler() :
20 m_quality(75),
21 m_scanState(ScanNotScanned),
22 m_features(),
23 m_formatFlags(0),
24 m_loop(0),
25 m_frameCount(0),
26 m_demuxer(NULL),
27 m_composited(NULL)
28{
29 memset(&m_iter, 0, sizeof(m_iter));
30}
31
32QWebpHandler::~QWebpHandler()
33{
34 WebPDemuxReleaseIterator(&m_iter);
35 WebPDemuxDelete(m_demuxer);
36 delete m_composited;
37}
38
39bool QWebpHandler::canRead() const
40{
41 if (m_scanState == ScanNotScanned && !canRead(device()))
42 return false;
43
44 if (m_scanState != ScanError) {
45 setFormat(QByteArrayLiteral("webp"));
46
47 if (m_features.has_animation && m_iter.frame_num >= m_frameCount)
48 return false;
49
50 return true;
51 }
52 return false;
53}
54
55bool QWebpHandler::canRead(QIODevice *device)
56{
57 if (!device) {
58 qWarning("QWebpHandler::canRead() called with no device");
59 return false;
60 }
61
62 QByteArray header = device->peek(riffHeaderSize);
63 return header.startsWith("RIFF") && header.endsWith("WEBP");
64}
65
66bool QWebpHandler::ensureScanned() const
67{
68 if (m_scanState != ScanNotScanned)
69 return m_scanState == ScanSuccess;
70
71 m_scanState = ScanError;
72
73 QWebpHandler *that = const_cast<QWebpHandler *>(this);
74 const int headerBytesNeeded = sizeof(WebPBitstreamFeatures);
75 QByteArray header = device()->peek(headerBytesNeeded);
76 if (header.size() < headerBytesNeeded)
77 return false;
78
79 // We do no random access during decoding, just a readAll() of the whole image file. So if
80 // if it is all available already, we can accept a sequential device. The riff header contains
81 // the file size minus 8 bytes header
82 qint64 byteSize = qFromLittleEndian<quint32>(header.constData() + 4);
83 if (device()->isSequential() && device()->bytesAvailable() < byteSize + 8) {
84 qWarning() << "QWebpHandler: Insufficient data available in sequential device";
85 return false;
86 }
87 if (WebPGetFeatures((const uint8_t*)header.constData(), header.size(), &(that->m_features)) == VP8_STATUS_OK) {
88 if (m_features.has_animation) {
89 // For animation, we have to read and scan whole file to determine loop count and images count
90 if (that->ensureDemuxer()) {
91 that->m_loop = WebPDemuxGetI(m_demuxer, WEBP_FF_LOOP_COUNT);
92 that->m_frameCount = WebPDemuxGetI(m_demuxer, WEBP_FF_FRAME_COUNT);
93 that->m_bgColor = QColor::fromRgba(QRgb(WebPDemuxGetI(m_demuxer, WEBP_FF_BACKGROUND_COLOR)));
94
95 QSize sz(that->m_features.width, that->m_features.height);
96 that->m_composited = new QImage;
97 if (!QImageIOHandler::allocateImage(sz, QImage::Format_ARGB32, that->m_composited))
98 return false;
99 if (that->m_features.has_alpha)
100 that->m_composited->fill(Qt::transparent);
101
102 m_scanState = ScanSuccess;
103 }
104 } else {
105 m_scanState = ScanSuccess;
106 }
107 }
108
109 return m_scanState == ScanSuccess;
110}
111
112bool QWebpHandler::ensureDemuxer()
113{
114 if (m_demuxer)
115 return true;
116
117 m_rawData = device()->readAll();
118
119 m_demuxer = [&] {
120 WebPData data = {};
121 data.bytes = reinterpret_cast<const uint8_t *>(m_rawData.constData());
122 data.size = m_rawData.size();
123 return WebPDemux(&data); // reads `*data`, doesn't store `data`
124 }();
125
126 if (m_demuxer == NULL)
127 return false;
128
129 m_formatFlags = WebPDemuxGetI(m_demuxer, WEBP_FF_FORMAT_FLAGS);
130 return true;
131}
132
133bool QWebpHandler::read(QImage *image)
134{
135 if (!ensureScanned() || !ensureDemuxer())
136 return false;
137
138 QRect prevFrameRect;
139 if (m_iter.frame_num == 0) {
140 // Read global meta-data chunks first
141 WebPChunkIterator metaDataIter;
142 if ((m_formatFlags & ICCP_FLAG) && WebPDemuxGetChunk(m_demuxer, "ICCP", 1, &metaDataIter)) {
143 QByteArray iccProfile = QByteArray::fromRawData(reinterpret_cast<const char *>(metaDataIter.chunk.bytes),
144 metaDataIter.chunk.size);
145 // Ensure the profile is 4-byte aligned.
146 if (reinterpret_cast<qintptr>(iccProfile.constData()) & 0x3)
147 iccProfile.detach();
148 m_colorSpace = QColorSpace::fromIccProfile(iccProfile);
149 // ### consider parsing EXIF and/or XMP metadata too.
150 WebPDemuxReleaseChunkIterator(&metaDataIter);
151 }
152
153 // Go to first frame
154 if (!WebPDemuxGetFrame(m_demuxer, 1, &m_iter))
155 return false;
156 } else {
157 if (m_iter.has_alpha && m_iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND)
158 prevFrameRect = currentImageRect();
159
160 // Go to next frame
161 if (!WebPDemuxNextFrame(&m_iter))
162 return false;
163 }
164
165 WebPBitstreamFeatures features;
166 VP8StatusCode status = WebPGetFeatures(m_iter.fragment.bytes, m_iter.fragment.size, &features);
167 if (status != VP8_STATUS_OK)
168 return false;
169
170 QImage::Format format = m_features.has_alpha ? QImage::Format_ARGB32 : QImage::Format_RGB32;
171 QImage frame;
172 if (!QImageIOHandler::allocateImage(QSize(m_iter.width, m_iter.height), format, &frame))
173 return false;
174 uint8_t *output = frame.bits();
175 size_t output_size = frame.sizeInBytes();
176#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
177 if (!WebPDecodeBGRAInto(
178 reinterpret_cast<const uint8_t*>(m_iter.fragment.bytes), m_iter.fragment.size,
179 output, output_size, frame.bytesPerLine()))
180#else
181 if (!WebPDecodeARGBInto(
182 reinterpret_cast<const uint8_t*>(m_iter.fragment.bytes), m_iter.fragment.size,
183 output, output_size, frame.bytesPerLine()))
184#endif
185 return false;
186
187 if (m_features.has_animation) {
188 // Animation
189 QPainter painter(m_composited);
190 if (!prevFrameRect.isEmpty()) {
191 painter.setCompositionMode(QPainter::CompositionMode_Clear);
192 painter.fillRect(prevFrameRect, Qt::black);
193 }
194 if (m_features.has_alpha) {
195 if (m_iter.blend_method == WEBP_MUX_NO_BLEND)
196 painter.setCompositionMode(QPainter::CompositionMode_Source);
197 else
198 painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
199 }
200 painter.drawImage(currentImageRect(), frame);
201
202 *image = *m_composited;
203 } else {
204 // Single image
205 *image = std::move(frame);
206 }
207 image->setColorSpace(m_colorSpace);
208
209 return true;
210}
211
212bool QWebpHandler::write(const QImage &image)
213{
214 if (image.isNull()) {
215 qWarning() << "source image is null.";
216 return false;
217 }
218 if (std::max(image.width(), image.height()) > WEBP_MAX_DIMENSION) {
219 qWarning() << "QWebpHandler::write() source image too large for WebP: " << image.size();
220 return false;
221 }
222
223 const bool alpha = image.hasAlphaChannel();
224 QImage::Format newFormat = alpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888;
225 const QImage srcImage = (image.format() == newFormat) ? image : image.convertedTo(newFormat);
226
227 WebPPicture picture;
228 WebPConfig config;
229
230 if (!WebPPictureInit(&picture) || !WebPConfigInit(&config)) {
231 qWarning() << "failed to init webp picture and config";
232 return false;
233 }
234
235 picture.width = srcImage.width();
236 picture.height = srcImage.height();
237 picture.use_argb = 1;
238 bool failed = false;
239 if (alpha)
240 failed = !WebPPictureImportRGBA(&picture, srcImage.constBits(), srcImage.bytesPerLine());
241 else
242 failed = !WebPPictureImportRGB(&picture, srcImage.constBits(), srcImage.bytesPerLine());
243
244 if (failed) {
245 qWarning() << "failed to import image data to webp picture.";
246 WebPPictureFree(&picture);
247 return false;
248 }
249
250 int reqQuality = m_quality < 0 ? 75 : qMin(m_quality, 100);
251 if (reqQuality < 100) {
252 config.lossless = 0;
253 config.quality = reqQuality;
254 } else {
255 config.lossless = 1;
256 config.quality = 70; // For lossless, specifies compression effort; 70 is libwebp default
257 }
258 config.alpha_quality = config.quality;
259 WebPMemoryWriter writer;
260 WebPMemoryWriterInit(&writer);
261 picture.writer = WebPMemoryWrite;
262 picture.custom_ptr = &writer;
263
264 if (!WebPEncode(&config, &picture)) {
265 qWarning() << "failed to encode webp picture, error code: " << picture.error_code;
266 WebPPictureFree(&picture);
267 WebPMemoryWriterClear(&writer);
268 return false;
269 }
270
271 bool res = false;
272 if (image.colorSpace().isValid()) {
273 int copy_data = 0;
274 WebPMux *mux = WebPMuxNew();
275 WebPData image_data = { writer.mem, writer.size };
276 WebPMuxSetImage(mux, &image_data, copy_data);
277 uint8_t vp8xChunk[10];
278 uint8_t flags = 0x20; // Has ICCP chunk, no XMP, EXIF or animation.
279 if (image.hasAlphaChannel())
280 flags |= 0x10;
281 vp8xChunk[0] = flags;
282 vp8xChunk[1] = 0;
283 vp8xChunk[2] = 0;
284 vp8xChunk[3] = 0;
285 const unsigned width = image.width() - 1;
286 const unsigned height = image.height() - 1;
287 vp8xChunk[4] = width & 0xff;
288 vp8xChunk[5] = (width >> 8) & 0xff;
289 vp8xChunk[6] = (width >> 16) & 0xff;
290 vp8xChunk[7] = height & 0xff;
291 vp8xChunk[8] = (height >> 8) & 0xff;
292 vp8xChunk[9] = (height >> 16) & 0xff;
293 WebPData vp8x_data = { vp8xChunk, 10 };
294 if (WebPMuxSetChunk(mux, "VP8X", &vp8x_data, copy_data) == WEBP_MUX_OK) {
295 QByteArray iccProfile = image.colorSpace().iccProfile();
296 WebPData iccp_data = {
297 reinterpret_cast<const uint8_t *>(iccProfile.constData()),
298 static_cast<size_t>(iccProfile.size())
299 };
300 if (WebPMuxSetChunk(mux, "ICCP", &iccp_data, copy_data) == WEBP_MUX_OK) {
301 WebPData output_data;
302 if (WebPMuxAssemble(mux, &output_data) == WEBP_MUX_OK) {
303 res = (output_data.size ==
304 static_cast<size_t>(device()->write(reinterpret_cast<const char *>(output_data.bytes), output_data.size)));
305 }
306 WebPDataClear(&output_data);
307 }
308 }
309 WebPMuxDelete(mux);
310 }
311 if (!res) {
312 res = (writer.size ==
313 static_cast<size_t>(device()->write(reinterpret_cast<const char *>(writer.mem), writer.size)));
314 }
315 WebPPictureFree(&picture);
316 WebPMemoryWriterClear(&writer);
317
318 return res;
319}
320
321QVariant QWebpHandler::option(ImageOption option) const
322{
323 if (!supportsOption(option) || !ensureScanned())
324 return QVariant();
325
326 switch (option) {
327 case Quality:
328 return m_quality;
329 case Size:
330 return QSize(m_features.width, m_features.height);
331 case Animation:
332 return m_features.has_animation;
333 case BackgroundColor:
334 return m_bgColor;
335 default:
336 return QVariant();
337 }
338}
339
340void QWebpHandler::setOption(ImageOption option, const QVariant &value)
341{
342 switch (option) {
343 case Quality:
344 m_quality = value.toInt();
345 return;
346 default:
347 break;
348 }
349 QImageIOHandler::setOption(option, value);
350}
351
352bool QWebpHandler::supportsOption(ImageOption option) const
353{
354 return option == Quality
355 || option == Size
356 || option == Animation
357 || option == BackgroundColor;
358}
359
360int QWebpHandler::imageCount() const
361{
362 if (!ensureScanned())
363 return 0;
364
365 if (!m_features.has_animation)
366 return 1;
367
368 return m_frameCount;
369}
370
371int QWebpHandler::currentImageNumber() const
372{
373 if (!ensureScanned() || !m_features.has_animation)
374 return 0;
375
376 // Frame number in WebP starts from 1
377 return m_iter.frame_num - 1;
378}
379
380QRect QWebpHandler::currentImageRect() const
381{
382 if (!ensureScanned())
383 return QRect();
384
385 return QRect(m_iter.x_offset, m_iter.y_offset, m_iter.width, m_iter.height);
386}
387
388int QWebpHandler::loopCount() const
389{
390 if (!ensureScanned() || !m_features.has_animation)
391 return 0;
392
393 // Loop count in WebP starts from 0
394 return m_loop - 1;
395}
396
397int QWebpHandler::nextImageDelay() const
398{
399 if (!ensureScanned() || !m_features.has_animation)
400 return 0;
401
402 return m_iter.duration;
403}
404
405QT_END_NAMESPACE
QIODevice * device() const
Returns the device currently assigned to QImageReader, or \nullptr if no device has been assigned.
static const int riffHeaderSize