File indexing completed on 2024-12-22 04:15:59

0001 /*
0002  * This file is part of Krita
0003  *
0004  * SPDX-FileCopyrightText: 2021 L. E. Segovia <amy@amyspark.me>
0005  *
0006  * SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include <kpluginfactory.h>
0010 #include <webp/demux.h>
0011 
0012 #include <QBuffer>
0013 #include <QByteArray>
0014 
0015 #include <cmath>
0016 #include <cstdint>
0017 #include <memory>
0018 
0019 #include <KisDocument.h>
0020 #include <KisImportExportErrorCode.h>
0021 #include <KoColorModelStandardIds.h>
0022 #include <KoColorProfile.h>
0023 #include <KoCompositeOpRegistry.h>
0024 #include <KoDialog.h>
0025 #include <kis_group_layer.h>
0026 #include <kis_image_animation_interface.h>
0027 #include <kis_keyframe_channel.h>
0028 #include <kis_meta_data_backend_registry.h>
0029 #include <kis_paint_layer.h>
0030 #include <kis_painter.h>
0031 #include <kis_properties_configuration.h>
0032 #include <kis_raster_keyframe_channel.h>
0033 
0034 #include "kis_webp_import.h"
0035 
0036 K_PLUGIN_FACTORY_WITH_JSON(KisWebPImportFactory, "krita_webp_import.json", registerPlugin<KisWebPImport>();)
0037 
0038 KisWebPImport::KisWebPImport(QObject *parent, const QVariantList &)
0039     : KisImportExportFilter(parent)
0040 {
0041 }
0042 
0043 KisWebPImport::~KisWebPImport() = default;
0044 
0045 KisImportExportErrorCode KisWebPImport::convert(KisDocument *document,
0046                                                 QIODevice *io,
0047                                                 KisPropertiesConfigurationSP)
0048 {
0049     const QByteArray buf = io->readAll();
0050 
0051     if (buf.isEmpty()) {
0052         return ImportExportCodes::ErrorWhileReading;
0053     }
0054 
0055     const uint8_t *data = reinterpret_cast<const uint8_t *>(buf.constData());
0056     const size_t data_size = static_cast<size_t>(buf.size());
0057 
0058     const WebPData webpData = {data, data_size};
0059 
0060     WebPDemuxer *demux = WebPDemux(&webpData);
0061     if (!demux) {
0062         dbgFile << "WebP demuxer initialization failure";
0063         return ImportExportCodes::InternalError;
0064     }
0065 
0066     const uint32_t width = WebPDemuxGetI(demux, WEBP_FF_CANVAS_WIDTH);
0067     const uint32_t height = WebPDemuxGetI(demux, WEBP_FF_CANVAS_HEIGHT);
0068     const uint32_t flags = WebPDemuxGetI(demux, WEBP_FF_FORMAT_FLAGS);
0069     const uint32_t bg = WebPDemuxGetI(demux, WEBP_FF_BACKGROUND_COLOR);
0070 
0071     const KoColorSpace *colorSpace = KoColorSpaceRegistry::instance()->rgb8();
0072     const KoColorSpace *imageColorSpace = nullptr;
0073 
0074     bool isRgba = true;
0075 
0076     {
0077         WebPChunkIterator chunk_iter;
0078         if (flags & ICCP_FLAG) {
0079             if (WebPDemuxGetChunk(demux, "ICCP", 1, &chunk_iter)) {
0080                 dbgFile << "WebPDemuxGetChunk on ICCP succeeded, ICC profile "
0081                            "available";
0082 
0083                 const QByteArray iccProfile(
0084                     reinterpret_cast<const char *>(chunk_iter.chunk.bytes),
0085                     static_cast<int>(chunk_iter.chunk.size));
0086                 const KoColorProfile *profile =
0087                     KoColorSpaceRegistry::instance()->createColorProfile(
0088                         RGBAColorModelID.id(),
0089                         Integer8BitsColorDepthID.id(),
0090                         iccProfile);
0091                 imageColorSpace = KoColorSpaceRegistry::instance()->colorSpace(
0092                     RGBAColorModelID.id(),
0093                     Integer8BitsColorDepthID.id(),
0094                     profile);
0095 
0096                 // Assign as non-RGBA color space to convert it back later
0097                 if (!imageColorSpace) {
0098                     const QString colId = profile->colorModelID();
0099                     const KoColorProfile *cProfile =
0100                         KoColorSpaceRegistry::instance()->createColorProfile(
0101                             colId,
0102                             Integer8BitsColorDepthID.id(),
0103                             iccProfile);
0104                     imageColorSpace = KoColorSpaceRegistry::instance()->colorSpace(
0105                         colId,
0106                         Integer8BitsColorDepthID.id(),
0107                         cProfile);
0108                     if (imageColorSpace) {
0109                         isRgba = false;
0110                     }
0111                 }
0112             }
0113         }
0114         WebPDemuxReleaseChunkIterator(&chunk_iter);
0115     }
0116 
0117     if (isRgba && imageColorSpace) {
0118         colorSpace = imageColorSpace;
0119     }
0120 
0121     const KoColor bgColor(
0122         QColor(bg >> 8 & 0xFFu, bg >> 16 & 0xFFu, bg >> 24 & 0xFFu, bg & 0xFFu),
0123         colorSpace);
0124 
0125     KisImageSP image = new KisImage(document->createUndoStore(),
0126                                     static_cast<qint32>(width),
0127                                     static_cast<qint32>(height),
0128                                     colorSpace,
0129                                     i18n("WebP Image"));
0130 
0131     KisPaintLayerSP layer(
0132         new KisPaintLayer(image, image->nextLayerName(), 255));
0133 
0134     {
0135         WebPChunkIterator chunk_iter;
0136         if (flags & EXIF_FLAG) {
0137             if (WebPDemuxGetChunk(demux, "EXIF", 1, &chunk_iter)) {
0138                 dbgFile << "Loading EXIF data. Size: " << chunk_iter.chunk.size;
0139 
0140                 QBuffer buf;
0141                 buf.setData(
0142                     reinterpret_cast<const char *>(chunk_iter.chunk.bytes),
0143                     static_cast<int>(chunk_iter.chunk.size));
0144 
0145                 const KisMetaData::IOBackend *backend =
0146                     KisMetadataBackendRegistry::instance()->value("exif");
0147 
0148                 backend->loadFrom(layer->metaData(), &buf);
0149             }
0150         }
0151         WebPDemuxReleaseChunkIterator(&chunk_iter);
0152     }
0153 
0154     {
0155         WebPChunkIterator chunk_iter;
0156         if (flags & XMP_FLAG) {
0157             if (WebPDemuxGetChunk(demux, "XMP ", 1, &chunk_iter)) {
0158                 dbgFile << "Loading XMP data. Size: " << chunk_iter.chunk.size;
0159 
0160                 QBuffer buf;
0161                 buf.setData(
0162                     reinterpret_cast<const char *>(chunk_iter.chunk.bytes),
0163                     static_cast<int>(chunk_iter.chunk.size));
0164 
0165                 const KisMetaData::IOBackend *xmpBackend =
0166                     KisMetadataBackendRegistry::instance()->value("xmp");
0167 
0168                 xmpBackend->loadFrom(layer->metaData(), &buf);
0169             }
0170         }
0171         WebPDemuxReleaseChunkIterator(&chunk_iter);
0172     }
0173 
0174     {
0175         WebPIterator iter;
0176         if (WebPDemuxGetFrame(demux, 1, &iter)) {
0177             int nextTimestamp = 0;
0178             WebPDecoderConfig config;
0179 
0180             KisPaintDeviceSP compositedFrame(
0181                 new KisPaintDevice(image->colorSpace()));
0182 
0183             do {
0184                 if (!WebPInitDecoderConfig(&config)) {
0185                     dbgFile << "WebP decode config initialization failure";
0186                     return ImportExportCodes::InternalError;
0187                 }
0188 
0189                 {
0190                     const VP8StatusCode result =
0191                         WebPGetFeatures(iter.fragment.bytes,
0192                                         iter.fragment.size,
0193                                         &config.input);
0194                     dbgFile << "WebP import validation status: " << result;
0195                     switch (result) {
0196                     case VP8_STATUS_OK:
0197                         break;
0198                     case VP8_STATUS_OUT_OF_MEMORY:
0199                         return ImportExportCodes::InsufficientMemory;
0200                     case VP8_STATUS_INVALID_PARAM:
0201                         return ImportExportCodes::InternalError;
0202                     case VP8_STATUS_BITSTREAM_ERROR:
0203                         return ImportExportCodes::FileFormatIncorrect;
0204                     case VP8_STATUS_UNSUPPORTED_FEATURE:
0205                         return ImportExportCodes::FormatFeaturesUnsupported;
0206                     case VP8_STATUS_SUSPENDED:
0207                     case VP8_STATUS_USER_ABORT:
0208                         return ImportExportCodes::InternalError;
0209                         return ImportExportCodes::InternalError;
0210                     case VP8_STATUS_NOT_ENOUGH_DATA:
0211                         return ImportExportCodes::FileFormatIncorrect;
0212                     }
0213                 }
0214 
0215                 // Doesn't make sense to ask for options for each individual
0216                 // frame. See jxl plugin for a similar approach.
0217                 config.output.colorspace = MODE_BGRA;
0218                 config.options.use_threads = 1;
0219 
0220                 {
0221                     const VP8StatusCode result = WebPDecode(iter.fragment.bytes,
0222                                                             iter.fragment.size,
0223                                                             &config);
0224 
0225                     dbgFile << "WebP frame:" << iter.frame_num
0226                             << ", import status: " << result;
0227                     switch (result) {
0228                     case VP8_STATUS_OK:
0229                         break;
0230                     case VP8_STATUS_OUT_OF_MEMORY:
0231                         return ImportExportCodes::InsufficientMemory;
0232                     case VP8_STATUS_INVALID_PARAM:
0233                         return ImportExportCodes::InternalError;
0234                     case VP8_STATUS_BITSTREAM_ERROR:
0235                         return ImportExportCodes::FileFormatIncorrect;
0236                     case VP8_STATUS_UNSUPPORTED_FEATURE:
0237                         return ImportExportCodes::FormatFeaturesUnsupported;
0238                     case VP8_STATUS_SUSPENDED:
0239                     case VP8_STATUS_USER_ABORT:
0240                         return ImportExportCodes::InternalError;
0241                         return ImportExportCodes::InternalError;
0242                     case VP8_STATUS_NOT_ENOUGH_DATA:
0243                         return ImportExportCodes::FileFormatIncorrect;
0244                     }
0245                 }
0246 
0247                 // Check for "we're initializing the first frame".
0248                 // This code had previously "config.input.has_animation",
0249                 // this is incorrect when using the demuxer because
0250                 // each frame is yielded through GetFrame().
0251                 if (iter.num_frames > 0 && iter.frame_num == 1) {
0252                     dbgFile << "Animation detected, estimated framerate:"
0253                             << static_cast<double>(1000) / iter.duration;
0254                     const int framerate = std::lround(
0255                         1000.0 / static_cast<double>(iter.duration));
0256                     layer->enableAnimation();
0257                     image->animationInterface()->setDocumentRangeEndFrame(0);
0258                     image->animationInterface()->setFramerate(framerate);
0259                 }
0260 
0261                 const QRect bounds(
0262                     QPoint{iter.x_offset, iter.y_offset},
0263                     QSize{config.output.width, config.output.height});
0264 
0265                 {
0266                     KisPaintDeviceSP currentFrame(
0267                         new KisPaintDevice(image->colorSpace()));
0268                     currentFrame->fill(bounds, bgColor);
0269 
0270                     currentFrame->writeBytes(config.output.u.RGBA.rgba,
0271                                              iter.x_offset,
0272                                              iter.y_offset,
0273                                              config.output.width,
0274                                              config.output.height);
0275 
0276                     KisPainter painter(compositedFrame);
0277                     painter.setCompositeOpId(iter.blend_method == WEBP_MUX_BLEND
0278                                                  ? COMPOSITE_OVER
0279                                                  : COMPOSITE_COPY);
0280 
0281                     painter.bitBlt(
0282                         {iter.x_offset, iter.y_offset},
0283                         currentFrame,
0284                         {QPoint(iter.x_offset, iter.y_offset),
0285                          QSize(config.output.width, config.output.height)});
0286                 }
0287 
0288                 if (iter.num_frames > 1) {
0289                     const int currentFrameTime =
0290                         std::lround(static_cast<double>(nextTimestamp)
0291                                     / static_cast<double>(iter.duration));
0292                     dbgFile << QString(
0293                                    "Importing frame %1 @ %2, duration %3 ms, "
0294                                    "blending %4, disposal %5")
0295                                    .arg(iter.frame_num)
0296                                    .arg(currentFrameTime)
0297                                    .arg(iter.duration)
0298                                    .arg(iter.blend_method)
0299                                    .arg(iter.dispose_method)
0300                                    .toStdString()
0301                                    .c_str();
0302                     KisKeyframeChannel *channel = layer->getKeyframeChannel(
0303                         KisKeyframeChannel::Raster.id(),
0304                         true);
0305                     auto *frame =
0306                         dynamic_cast<KisRasterKeyframeChannel *>(channel);
0307                     image->animationInterface()->setDocumentRangeEndFrame(
0308                         std::lround(static_cast<double>(nextTimestamp)
0309                                     / static_cast<double>(iter.duration)));
0310                     frame->importFrame(currentFrameTime,
0311                                        compositedFrame,
0312                                        nullptr);
0313                     nextTimestamp += iter.duration;
0314                 } else {
0315                     layer->paintDevice()->makeCloneFrom(compositedFrame,
0316                                                         image->bounds());
0317                 }
0318 
0319                 if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
0320                     compositedFrame->fill(bounds, bgColor);
0321                 }
0322 
0323                 WebPFreeDecBuffer(&config.output);
0324             } while (WebPDemuxNextFrame(&iter));
0325         }
0326         WebPDemuxReleaseIterator(&iter);
0327     }
0328 
0329     WebPDemuxDelete(demux);
0330 
0331     image->addNode(layer.data(), image->rootLayer().data());
0332 
0333     if (!isRgba) {
0334         image->convertImageColorSpace(imageColorSpace, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags());
0335     }
0336 
0337     document->setCurrentImage(image);
0338 
0339     return ImportExportCodes::OK;
0340 }
0341 
0342 #include "kis_webp_import.moc"