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"