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 m_webpData.bytes = reinterpret_cast<const uint8_t *>(m_rawData.constData());
119 m_webpData.size = m_rawData.size();
120
121 m_demuxer = WebPDemux(&m_webpData);
122 if (m_demuxer == NULL)
123 return false;
124
125 m_formatFlags = WebPDemuxGetI(m_demuxer, WEBP_FF_FORMAT_FLAGS);
126 return true;
127}
128
129bool QWebpHandler::read(QImage *image)
130{
131 if (!ensureScanned() || !ensureDemuxer())
132 return false;
133
134 QRect prevFrameRect;
135 if (m_iter.frame_num == 0) {
136 // Read global meta-data chunks first
137 WebPChunkIterator metaDataIter;
138 if ((m_formatFlags & ICCP_FLAG) && WebPDemuxGetChunk(m_demuxer, "ICCP", 1, &metaDataIter)) {
139 QByteArray iccProfile = QByteArray::fromRawData(reinterpret_cast<const char *>(metaDataIter.chunk.bytes),
140 metaDataIter.chunk.size);
141 // Ensure the profile is 4-byte aligned.
142 if (reinterpret_cast<qintptr>(iccProfile.constData()) & 0x3)
143 iccProfile.detach();
144 m_colorSpace = QColorSpace::fromIccProfile(iccProfile);
145 // ### consider parsing EXIF and/or XMP metadata too.
146 WebPDemuxReleaseChunkIterator(&metaDataIter);
147 }
148
149 // Go to first frame
150 if (!WebPDemuxGetFrame(m_demuxer, 1, &m_iter))
151 return false;
152 } else {
153 if (m_iter.has_alpha && m_iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND)
154 prevFrameRect = currentImageRect();
155
156 // Go to next frame
157 if (!WebPDemuxNextFrame(&m_iter))
158 return false;
159 }
160
161 WebPBitstreamFeatures features;
162 VP8StatusCode status = WebPGetFeatures(m_iter.fragment.bytes, m_iter.fragment.size, &features);
163 if (status != VP8_STATUS_OK)
164 return false;
165
166 QImage::Format format = m_features.has_alpha ? QImage::Format_ARGB32 : QImage::Format_RGB32;
167 QImage frame;
168 if (!QImageIOHandler::allocateImage(QSize(m_iter.width, m_iter.height), format, &frame))
169 return false;
170 uint8_t *output = frame.bits();
171 size_t output_size = frame.sizeInBytes();
172#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
173 if (!WebPDecodeBGRAInto(
174 reinterpret_cast<const uint8_t*>(m_iter.fragment.bytes), m_iter.fragment.size,
175 output, output_size, frame.bytesPerLine()))
176#else
177 if (!WebPDecodeARGBInto(
178 reinterpret_cast<const uint8_t*>(m_iter.fragment.bytes), m_iter.fragment.size,
179 output, output_size, frame.bytesPerLine()))
180#endif
181 return false;
182
183 if (!m_features.has_animation) {
184 // Single image
185 *image = frame;
186 } else {
187 // Animation
188 QPainter painter(m_composited);
189 if (!prevFrameRect.isEmpty()) {
190 painter.setCompositionMode(QPainter::CompositionMode_Clear);
191 painter.fillRect(prevFrameRect, Qt::black);
192 }
193 if (m_features.has_alpha) {
194 if (m_iter.blend_method == WEBP_MUX_NO_BLEND)
195 painter.setCompositionMode(QPainter::CompositionMode_Source);
196 else
197 painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
198 }
199 painter.drawImage(currentImageRect(), frame);
200
201 *image = *m_composited;
202 }
203 image->setColorSpace(m_colorSpace);
204
205 return true;
206}
207
208bool QWebpHandler::write(const QImage &image)
209{
210 if (image.isNull()) {
211 qWarning() << "source image is null.";
212 return false;
213 }
214 if (std::max(image.width(), image.height()) > WEBP_MAX_DIMENSION) {
215 qWarning() << "QWebpHandler::write() source image too large for WebP: " << image.size();
216 return false;
217 }
218
219 const bool alpha = image.hasAlphaChannel();
220 QImage::Format newFormat = alpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888;
221 const QImage srcImage = (image.format() == newFormat) ? image : image.convertedTo(newFormat);
222
223 WebPPicture picture;
224 WebPConfig config;
225
226 if (!WebPPictureInit(&picture) || !WebPConfigInit(&config)) {
227 qWarning() << "failed to init webp picture and config";
228 return false;
229 }
230
231 picture.width = srcImage.width();
232 picture.height = srcImage.height();
233 picture.use_argb = 1;
234 bool failed = false;
235 if (alpha)
236 failed = !WebPPictureImportRGBA(&picture, srcImage.constBits(), srcImage.bytesPerLine());
237 else
238 failed = !WebPPictureImportRGB(&picture, srcImage.constBits(), srcImage.bytesPerLine());
239
240 if (failed) {
241 qWarning() << "failed to import image data to webp picture.";
242 WebPPictureFree(&picture);
243 return false;
244 }
245
246 int reqQuality = m_quality < 0 ? 75 : qMin(m_quality, 100);
247 if (reqQuality < 100) {
248 config.lossless = 0;
249 config.quality = reqQuality;
250 } else {
251 config.lossless = 1;
252 config.quality = 70; // For lossless, specifies compression effort; 70 is libwebp default
253 }
254 config.alpha_quality = config.quality;
255 WebPMemoryWriter writer;
256 WebPMemoryWriterInit(&writer);
257 picture.writer = WebPMemoryWrite;
258 picture.custom_ptr = &writer;
259
260 if (!WebPEncode(&config, &picture)) {
261 qWarning() << "failed to encode webp picture, error code: " << picture.error_code;
262 WebPPictureFree(&picture);
263 WebPMemoryWriterClear(&writer);
264 return false;
265 }
266
267 bool res = false;
268 if (image.colorSpace().isValid()) {
269 int copy_data = 0;
270 WebPMux *mux = WebPMuxNew();
271 WebPData image_data = { writer.mem, writer.size };
272 WebPMuxSetImage(mux, &image_data, copy_data);
273 uint8_t vp8xChunk[10];
274 uint8_t flags = 0x20; // Has ICCP chunk, no XMP, EXIF or animation.
275 if (image.hasAlphaChannel())
276 flags |= 0x10;
277 vp8xChunk[0] = flags;
278 vp8xChunk[1] = 0;
279 vp8xChunk[2] = 0;
280 vp8xChunk[3] = 0;
281 const unsigned width = image.width() - 1;
282 const unsigned height = image.height() - 1;
283 vp8xChunk[4] = width & 0xff;
284 vp8xChunk[5] = (width >> 8) & 0xff;
285 vp8xChunk[6] = (width >> 16) & 0xff;
286 vp8xChunk[7] = height & 0xff;
287 vp8xChunk[8] = (height >> 8) & 0xff;
288 vp8xChunk[9] = (height >> 16) & 0xff;
289 WebPData vp8x_data = { vp8xChunk, 10 };
290 if (WebPMuxSetChunk(mux, "VP8X", &vp8x_data, copy_data) == WEBP_MUX_OK) {
291 QByteArray iccProfile = image.colorSpace().iccProfile();
292 WebPData iccp_data = {
293 reinterpret_cast<const uint8_t *>(iccProfile.constData()),
294 static_cast<size_t>(iccProfile.size())
295 };
296 if (WebPMuxSetChunk(mux, "ICCP", &iccp_data, copy_data) == WEBP_MUX_OK) {
297 WebPData output_data;
298 if (WebPMuxAssemble(mux, &output_data) == WEBP_MUX_OK) {
299 res = (output_data.size ==
300 static_cast<size_t>(device()->write(reinterpret_cast<const char *>(output_data.bytes), output_data.size)));
301 }
302 WebPDataClear(&output_data);
303 }
304 }
305 WebPMuxDelete(mux);
306 }
307 if (!res) {
308 res = (writer.size ==
309 static_cast<size_t>(device()->write(reinterpret_cast<const char *>(writer.mem), writer.size)));
310 }
311 WebPPictureFree(&picture);
312 WebPMemoryWriterClear(&writer);
313
314 return res;
315}
316
317QVariant QWebpHandler::option(ImageOption option) const
318{
319 if (!supportsOption(option) || !ensureScanned())
320 return QVariant();
321
322 switch (option) {
323 case Quality:
324 return m_quality;
325 case Size:
326 return QSize(m_features.width, m_features.height);
327 case Animation:
328 return m_features.has_animation;
329 case BackgroundColor:
330 return m_bgColor;
331 default:
332 return QVariant();
333 }
334}
335
336void QWebpHandler::setOption(ImageOption option, const QVariant &value)
337{
338 switch (option) {
339 case Quality:
340 m_quality = value.toInt();
341 return;
342 default:
343 break;
344 }
345 QImageIOHandler::setOption(option, value);
346}
347
348bool QWebpHandler::supportsOption(ImageOption option) const
349{
350 return option == Quality
351 || option == Size
352 || option == Animation
353 || option == BackgroundColor;
354}
355
356int QWebpHandler::imageCount() const
357{
358 if (!ensureScanned())
359 return 0;
360
361 if (!m_features.has_animation)
362 return 1;
363
364 return m_frameCount;
365}
366
367int QWebpHandler::currentImageNumber() const
368{
369 if (!ensureScanned() || !m_features.has_animation)
370 return 0;
371
372 // Frame number in WebP starts from 1
373 return m_iter.frame_num - 1;
374}
375
376QRect QWebpHandler::currentImageRect() const
377{
378 if (!ensureScanned())
379 return QRect();
380
381 return QRect(m_iter.x_offset, m_iter.y_offset, m_iter.width, m_iter.height);
382}
383
384int QWebpHandler::loopCount() const
385{
386 if (!ensureScanned() || !m_features.has_animation)
387 return 0;
388
389 // Loop count in WebP starts from 0
390 return m_loop - 1;
391}
392
393int QWebpHandler::nextImageDelay() const
394{
395 if (!ensureScanned() || !m_features.has_animation)
396 return 0;
397
398 return m_iter.duration;
399}
400
401QT_END_NAMESPACE
QIODevice * device() const
Returns the device currently assigned to QImageReader, or \nullptr if no device has been assigned.
static const int riffHeaderSize