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 | |
49 | static const int = 12; // RIFF_HEADER_SIZE from webp/format_constants.h |
50 | |
51 | QWebpHandler::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 | |
64 | QWebpHandler::~QWebpHandler() |
65 | { |
66 | WebPDemuxReleaseIterator(iter: &m_iter); |
67 | WebPDemuxDelete(dmux: m_demuxer); |
68 | delete m_composited; |
69 | } |
70 | |
71 | bool 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 | |
87 | bool QWebpHandler::canRead(QIODevice *device) |
88 | { |
89 | if (!device) { |
90 | qWarning(msg: "QWebpHandler::canRead() called with no device" ); |
91 | return false; |
92 | } |
93 | |
94 | QByteArray = device->peek(maxlen: riffHeaderSize); |
95 | return header.startsWith(c: "RIFF" ) && header.endsWith(c: "WEBP" ); |
96 | } |
97 | |
98 | bool 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 = 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 | |
143 | bool 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 | |
160 | bool 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 | |
237 | bool 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 | |
348 | QVariant 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 | |
367 | void 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 | |
379 | bool QWebpHandler::supportsOption(ImageOption option) const |
380 | { |
381 | return option == Quality |
382 | || option == Size |
383 | || option == Animation |
384 | || option == BackgroundColor; |
385 | } |
386 | |
387 | int 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 | |
398 | int 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 | |
407 | QRect 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 | |
415 | int 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 | |
424 | int QWebpHandler::nextImageDelay() const |
425 | { |
426 | if (!ensureScanned() || !m_features.has_animation) |
427 | return 0; |
428 | |
429 | return m_iter.duration; |
430 | } |
431 | |