| 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 | |