File indexing completed on 2024-05-26 04:27:28

0001 /* This file is part of the KDE project
0002  * SPDX-FileCopyrightText: 2008 Boudewijn Rempt <boud@valdyas.org>
0003  * SPDX-FileCopyrightText: 2009 Sven Langkamp <sven.langkamp@gmail.com>
0004  *
0005  * SPDX-License-Identifier: LGPL-2.0-or-later
0006  */
0007 #include <brushengine/kis_paintop_preset.h>
0008 
0009 #include <QFile>
0010 #include <QSize>
0011 #include <QImage>
0012 #include <QImageWriter>
0013 #include <QImageReader>
0014 #include <QDomDocument>
0015 #include <QBuffer>
0016 
0017 #include <KisDirtyStateSaver.h>
0018 
0019 #include <brushengine/kis_paintop_settings.h>
0020 #include "kis_paintop_registry.h"
0021 #include "kis_painter.h"
0022 #include <brushengine/kis_paint_information.h>
0023 #include "kis_paint_device.h"
0024 #include "kis_image.h"
0025 #include "KisPaintOpPresetUpdateProxy.h"
0026 #include <brushengine/kis_paintop_config_widget.h>
0027 #include <KisRequiredResourcesOperators.h>
0028 #include <KoLocalStrokeCanvasResources.h>
0029 #include <KisResourceModel.h>
0030 #include "KisPaintopSettingsIds.h"
0031 #include <KisResourceTypes.h>
0032 #include <KisResourceModelProvider.h>
0033 #include <krita_container_utils.h>
0034 #include <KoResourceCacheInterface.h>
0035 
0036 #include <KoStore.h>
0037 
0038 struct Q_DECL_HIDDEN KisPaintOpPreset::Private {
0039 
0040     struct UpdateListener : public KisPaintOpSettings::UpdateListener {
0041         UpdateListener(KisPaintOpPreset *parentPreset)
0042             : m_parentPreset(parentPreset)
0043         {
0044         }
0045 
0046         void setDirty(bool value) override {
0047             m_parentPreset->setDirty(value);
0048         }
0049 
0050         bool isDirty() const override {
0051             return m_parentPreset->isDirty();
0052         }
0053 
0054         void notifySettingsChanged() override {
0055             KisPaintOpPresetUpdateProxy* proxy = m_parentPreset->updateProxyNoCreate();
0056             if (proxy) {
0057                 proxy->notifySettingsChanged();
0058             }
0059         }
0060 
0061     private:
0062         KisPaintOpPreset *m_parentPreset;
0063     };
0064 
0065 public:
0066     Private(KisPaintOpPreset *q)
0067         : settingsUpdateListener(new UpdateListener(q)),
0068           version("5.0")
0069     {
0070     }
0071 
0072     KisPaintOpSettingsSP settings {0};
0073     QScopedPointer<KisPaintOpPresetUpdateProxy> updateProxy;
0074     KisPaintOpSettings::UpdateListenerSP settingsUpdateListener;
0075     QString version;
0076 };
0077 
0078 
0079 KisPaintOpPreset::KisPaintOpPreset()
0080     : KoResource(QString())
0081     , d(new Private(this))
0082 {
0083 }
0084 
0085 KisPaintOpPreset::KisPaintOpPreset(const QString & fileName)
0086     : KoResource(fileName)
0087     , d(new Private(this))
0088 {
0089     setName(name().replace("_", " "));
0090 }
0091 
0092 KisPaintOpPreset::~KisPaintOpPreset()
0093 {
0094     delete d;
0095 }
0096 
0097 KisPaintOpPreset::KisPaintOpPreset(const KisPaintOpPreset &rhs)
0098     : KoResource(rhs)
0099     , d(new Private(this))
0100 {
0101     if (rhs.settings()) {
0102         setSettings(rhs.settings()); // the settings are cloned inside!
0103     }
0104     KIS_SAFE_ASSERT_RECOVER_NOOP(isDirty() == rhs.isDirty());
0105     // only valid if we could clone the settings
0106     setValid(rhs.settings());
0107 
0108     setName(rhs.name());
0109     setImage(rhs.image());
0110 }
0111 
0112 KoResourceSP KisPaintOpPreset::clone() const
0113 {
0114     return KoResourceSP(new KisPaintOpPreset(*this));
0115 }
0116 
0117 void KisPaintOpPreset::setPaintOp(const KoID & paintOp)
0118 {
0119     Q_ASSERT(d->settings);
0120     d->settings->setProperty("paintop", paintOp.id());
0121 }
0122 
0123 KoID KisPaintOpPreset::paintOp() const
0124 {
0125     Q_ASSERT(d->settings);
0126     return KoID(d->settings->getString("paintop"));
0127 }
0128 
0129 QString KisPaintOpPreset::name() const
0130 {
0131     return KoResource::name().replace("_", " ");
0132 }
0133 
0134 void KisPaintOpPreset::setSettings(KisPaintOpSettingsSP settings)
0135 {
0136     Q_ASSERT(settings);
0137     Q_ASSERT(!settings->getString("paintop", QString()).isEmpty());
0138 
0139     KisDirtyStateSaver<KisPaintOpPreset*> dirtyStateSaver(this);
0140 
0141     if (d->settings) {
0142         d->settings->setUpdateListener(KisPaintOpSettings::UpdateListenerWSP());
0143         d->settings = 0;
0144     }
0145 
0146     if (settings) {
0147         d->settings = settings->clone();
0148         d->settings->setUpdateListener(d->settingsUpdateListener);
0149     }
0150 
0151     if (d->updateProxy) {
0152         d->updateProxy->notifyUniformPropertiesChanged();
0153         d->updateProxy->notifySettingsChanged();
0154     }
0155     setValid(true);
0156 }
0157 
0158 KisPaintOpSettingsSP KisPaintOpPreset::settings() const
0159 {
0160     Q_ASSERT(d->settings);
0161     Q_ASSERT(!d->settings->getString("paintop", QString()).isEmpty());
0162 
0163     return d->settings;
0164 }
0165 
0166 bool KisPaintOpPreset::loadFromDevice(QIODevice *dev, KisResourcesInterfaceSP resourcesInterface)
0167 {
0168     QImageReader reader(dev, "PNG");
0169 
0170     d->version = reader.text("version");
0171     QString preset = reader.text("preset");
0172     int resourceCount = reader.text("embedded_resources").toInt();
0173 
0174     if (!(d->version == "2.2" || d->version == "5.0")) {
0175         return false;
0176     }
0177 
0178     QImage img;
0179     if (!reader.read(&img)) {
0180         dbgImage << "Fail to decode PNG";
0181         return false;
0182     }
0183 
0184     //Workaround for broken presets
0185     //Presets was saved with nested cdata section
0186     preset.replace("<curve><![CDATA[", "<curve>");
0187     preset.replace("]]></curve>", "</curve>");
0188 
0189     QDomDocument doc;
0190     if (!doc.setContent(preset)) {
0191         return false;
0192     }
0193 
0194     QDomElement root = doc.documentElement();
0195 
0196     if (d->version == "5.0" && resourceCount > 0) {
0197         // Load any embedded resources
0198         QDomElement e = root.firstChildElement("resources");
0199         for (e = e.firstChildElement("resource"); !e.isNull(); e = e.nextSiblingElement("resource")) {
0200             QString name = e.attribute("name");
0201             QString filename = e.attribute("filename");
0202             QString resourceType = e.attribute("type");
0203             QString md5sum = e.attribute("md5sum");
0204 
0205             KoResourceSP existingResource = resourcesInterface
0206                 ->source(resourceType)
0207                 .bestMatch(md5sum, filename, name);
0208 
0209             if (existingResource) {
0210                 continue;
0211             }
0212 
0213             QByteArray ba = QByteArray::fromBase64(e.text().toLatin1());
0214             QBuffer buf(&ba);
0215             buf.open(QBuffer::ReadOnly);
0216 
0217             /// HACK ALERT: Calling importResource()
0218             /// here is technically undefined
0219             /// behavior, because this code is
0220             /// called from inside the storage's
0221             /// loadVersionedResource(). Basically
0222             /// we change underlying storage's
0223             /// storage while it is reading from
0224             /// it.
0225 
0226             KisResourceModel model(resourceType);
0227             model.importResource(filename, &buf, false, "memory");
0228         }
0229     }
0230 
0231     fromXML(root, resourcesInterface);
0232 
0233     if (!d->settings) {
0234         return false;
0235     }
0236 
0237     setValid(d->settings->isValid());
0238 
0239     setImage(img);
0240 
0241     return true;
0242 }
0243 
0244 void KisPaintOpPreset::toXML(QDomDocument& doc, QDomElement& elt) const
0245 {
0246     QString paintopid = d->settings->getString("paintop", QString());
0247 
0248     elt.setAttribute("paintopid", paintopid);
0249     elt.setAttribute("name", name());
0250 
0251 
0252     QList<KoResourceLoadResult> linkedResources = this->linkedResources(resourcesInterface());
0253 
0254     elt.setAttribute("embedded_resources", linkedResources.count());
0255 
0256     if (!linkedResources.isEmpty()) {
0257         QDomElement resourcesElement = doc.createElement("resources");
0258         elt.appendChild(resourcesElement);
0259         Q_FOREACH(KoResourceLoadResult linkedResource, linkedResources) {
0260             // we have requested linked resources, how can it be an embedded one?
0261             KIS_SAFE_ASSERT_RECOVER(linkedResource.type() != KoResourceLoadResult::EmbeddedResource) { continue; }
0262 
0263             KoResourceSP resource = linkedResource.resource();
0264 
0265             if (!resource) {
0266                 qWarning() << "WARNING: KisPaintOpPreset::toXML couldn't fetch a linked resource" << linkedResource.signature();
0267                 continue;
0268             }
0269 
0270             //KIS_SAFE_ASSERT_RECOVER_NOOP(resource->isSerializable() && "embedding non-serializable resources is not yet implemented");
0271             if (!resource->isSerializable()) {
0272                 qWarning() << "embedding non-serializable resources is not yet implemented. Resource: " << filename() << name()
0273                            << "cannot embed" << resource->filename() << resource->name() << resource->resourceType().first << resource->resourceType().second;
0274                 continue;
0275             }
0276 
0277             QBuffer buf;
0278             buf.open(QBuffer::WriteOnly);
0279             KisResourceModel model(resource->resourceType().first);
0280             bool r = model.exportResource(resource, &buf);
0281             buf.close();
0282             if (r) {
0283                 QDomText text = doc.createCDATASection(QString::fromLatin1(buf.data().toBase64()));
0284                 QDomElement e = doc.createElement("resource");
0285                 e.setAttribute("type", resource->resourceType().first);
0286                 e.setAttribute("md5sum", resource->md5Sum());
0287                 e.setAttribute("name", resource->name());
0288                 e.setAttribute("filename", resource->filename());
0289                 e.appendChild(text);
0290                 resourcesElement.appendChild(e);
0291 
0292             }
0293         }
0294     }
0295 
0296     // sanitize the settings
0297     bool hasTexture = d->settings->getBool("Texture/Pattern/Enabled");
0298     if (!hasTexture) {
0299         Q_FOREACH (const QString & key, d->settings->getProperties().keys()) {
0300             if (key.startsWith("Texture") && key != "Texture/Pattern/Enabled") {
0301                 d->settings->removeProperty(key);
0302             }
0303         }
0304     }
0305 
0306     d->settings->toXML(doc, elt);
0307 }
0308 
0309 void KisPaintOpPreset::fromXML(const QDomElement& presetElt, KisResourcesInterfaceSP resourcesInterface)
0310 {
0311     setName(presetElt.attribute("name"));
0312     QString paintopid = presetElt.attribute("paintopid");
0313 
0314     if (!metadata().contains("paintopid")) {
0315         addMetaData("paintopid", paintopid);
0316     }
0317 
0318     if (paintopid.isEmpty()) {
0319         dbgImage << "No paintopid attribute";
0320         setValid(false);
0321         return;
0322     }
0323 
0324     if (KisPaintOpRegistry::instance()->get(paintopid) == 0) {
0325         dbgImage << "No paintop " << paintopid;
0326         setValid(false);
0327         return;
0328     }
0329 
0330     KoID id(paintopid, QString());
0331 
0332     KisPaintOpSettingsSP settings = KisPaintOpRegistry::instance()->createSettings(id, resourcesInterface);
0333     if (!settings) {
0334         setValid(false);
0335         warnKrita << "Could not load settings for preset" << paintopid;
0336         return;
0337     }
0338 
0339     settings->fromXML(presetElt);
0340 
0341     // sanitize the settings
0342     bool hasTexture = settings->getBool("Texture/Pattern/Enabled");
0343     if (!hasTexture) {
0344         Q_FOREACH (const QString & key, settings->getProperties().keys()) {
0345             if (key.startsWith("Texture") && key != "Texture/Pattern/Enabled") {
0346                 settings->removeProperty(key);
0347             }
0348         }
0349     }
0350     setSettings(settings);
0351 
0352 }
0353 
0354 bool KisPaintOpPreset::saveToDevice(QIODevice *dev) const
0355 {
0356     QImageWriter writer(dev, "PNG");
0357 
0358     QDomDocument doc;
0359     QDomElement root = doc.createElement("Preset");
0360 
0361     toXML(doc, root);
0362 
0363     doc.appendChild(root);
0364 
0365     /**
0366      * HACK ALERT: We update the version of the resource format on
0367      * the first save operation, even though there is no guarantee
0368      * that it was "save" operation, but not "export" operation.
0369      *
0370      * The only point it affects now is whether we need to check
0371      * for the presence of the linkedResources() in
0372      * updateLinkedResourcesMetaData(). The new version of the
0373      * preset format ("5.0") has all the linked resources embedded
0374      * outside KisPaintOpSettings, which are automatically
0375      * loaded on the resource activation. We we shouldn't
0376      * add them into metaData()["dependent_resources_filenames"].
0377      */
0378     d->version = "5.0";
0379 
0380     writer.setText("version", d->version);
0381     writer.setText("preset", doc.toString());
0382 
0383     QImage img;
0384 
0385     if (image().isNull()) {
0386         img = QImage(1, 1, QImage::Format_RGB32);
0387     } else {
0388         img = image();
0389     }
0390 
0391     return writer.write(img);
0392 
0393 }
0394 
0395 void KisPaintOpPreset::updateLinkedResourcesMetaData(KisResourcesInterfaceSP resourcesInterface)
0396 {
0397     /**
0398      * The new preset format embeds all the linked resources outside
0399      * KisPaintOpSettings and loads them on activation, therefore we
0400      * shouldn't add them into "dependent_resources_filenames".
0401      */
0402 
0403     if (d->version == "2.2") {
0404         QList<KoResourceLoadResult> dependentResources = this->linkedResources(resourcesInterface);
0405 
0406         QStringList resourceFileNames;
0407 
0408         Q_FOREACH (KoResourceLoadResult resource, dependentResources) {
0409             const QString filename = resource.signature().filename;
0410 
0411             if (!filename.isEmpty()) {
0412                 resourceFileNames.append(filename);
0413             }
0414         }
0415 
0416         KritaUtils::makeContainerUnique(resourceFileNames);
0417 
0418         if (!resourceFileNames.isEmpty()) {
0419             addMetaData("dependent_resources_filenames", resourceFileNames);
0420         }
0421     }
0422 }
0423 
0424 QPointer<KisPaintOpPresetUpdateProxy> KisPaintOpPreset::updateProxy() const
0425 {
0426     if (!d->updateProxy) {
0427         d->updateProxy.reset(new KisPaintOpPresetUpdateProxy());
0428     }
0429     return d->updateProxy.data();
0430 }
0431 
0432 QPointer<KisPaintOpPresetUpdateProxy> KisPaintOpPreset::updateProxyNoCreate() const
0433 {
0434     return d->updateProxy.data();
0435 }
0436 
0437 QList<KisUniformPaintOpPropertySP> KisPaintOpPreset::uniformProperties()
0438 {
0439     /// we pass a shared pointer to settings explicitly,
0440     /// because the settings will not be able to wrap
0441     /// itself into a shared pointer
0442     return d->settings->uniformProperties(d->settings, updateProxy());
0443 }
0444 
0445 bool KisPaintOpPreset::hasMaskingPreset() const
0446 {
0447     return d->settings && d->settings->hasMaskingSettings();
0448 }
0449 
0450 KisPaintOpPresetSP KisPaintOpPreset::createMaskingPreset() const
0451 {
0452     KisPaintOpPresetSP result;
0453 
0454     if (d->settings && d->settings->hasMaskingSettings()) {
0455         result.reset(new KisPaintOpPreset());
0456         result->setSettings(d->settings->createMaskingSettings());
0457         if (!result->valid()) {
0458             result.clear();
0459         }
0460     }
0461 
0462     return result;
0463 }
0464 
0465 KisResourcesInterfaceSP KisPaintOpPreset::resourcesInterface() const
0466 {
0467     return d->settings ? d->settings->resourcesInterface() : nullptr;
0468 }
0469 
0470 void KisPaintOpPreset::setResourcesInterface(KisResourcesInterfaceSP resourcesInterface)
0471 {
0472     KIS_SAFE_ASSERT_RECOVER_RETURN(d->settings);
0473     d->settings->setResourcesInterface(resourcesInterface);
0474 }
0475 
0476 KoCanvasResourcesInterfaceSP KisPaintOpPreset::canvasResourcesInterface() const
0477 {
0478     return d->settings ? d->settings->canvasResourcesInterface() : nullptr;
0479 }
0480 
0481 void KisPaintOpPreset::setCanvasResourcesInterface(KoCanvasResourcesInterfaceSP canvasResourcesInterface)
0482 {
0483     KIS_SAFE_ASSERT_RECOVER_RETURN(d->settings);
0484     d->settings->setCanvasResourcesInterface(canvasResourcesInterface);
0485 }
0486 
0487 bool KisPaintOpPreset::hasLocalResourcesSnapshot() const
0488 {
0489     return KisRequiredResourcesOperators::hasLocalResourcesSnapshot(this);
0490 }
0491 
0492 KisPaintOpPresetSP KisPaintOpPreset::cloneWithResourcesSnapshot(KisResourcesInterfaceSP globalResourcesInterface, KoCanvasResourcesInterfaceSP canvasResourcesInterface, KoResourceCacheInterfaceSP cacheInterface) const
0493 {
0494     KisPaintOpPresetSP result =
0495             KisRequiredResourcesOperators::cloneWithResourcesSnapshot<KisPaintOpPresetSP>(this, globalResourcesInterface);
0496 
0497     const QList<int> canvasResources = result->requiredCanvasResources();
0498     if (!canvasResources.isEmpty()) {
0499         KoLocalStrokeCanvasResourcesSP storage(new KoLocalStrokeCanvasResources());
0500         Q_FOREACH (int key, canvasResources) {
0501             storage->storeResource(key, canvasResourcesInterface->resource(key));
0502         }
0503         result->setCanvasResourcesInterface(storage);
0504     }
0505 
0506     if (cacheInterface) {
0507         result->setResourceCacheInterface(cacheInterface);
0508     } else if (!canvasResources.isEmpty()) {
0509         /**
0510          * If the preset depends on any canvas resources, then we don't trust
0511          * the caches that are stored inside. Instead we just reset them. If the
0512          * preset is independent of the canvas resources, then its caches are,
0513          * most probably valid and we can reuse them.
0514          */
0515         result->setResourceCacheInterface(nullptr);
0516     }
0517 
0518     return result;
0519 }
0520 
0521 QList<KoResourceLoadResult> KisPaintOpPreset::linkedResources(KisResourcesInterfaceSP globalResourcesInterface) const
0522 {
0523     QList<KoResourceLoadResult> resources;
0524 
0525     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(d->settings, resources);
0526 
0527     KisPaintOpFactory* f = KisPaintOpRegistry::instance()->value(paintOp().id());
0528     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(f, resources);
0529     resources << f->prepareLinkedResources(d->settings, globalResourcesInterface);
0530 
0531     if (hasMaskingPreset()) {
0532         KisPaintOpPresetSP maskingPreset = createMaskingPreset();
0533         Q_ASSERT(maskingPreset);
0534 
0535         KisPaintOpFactory* f = KisPaintOpRegistry::instance()->value(maskingPreset->paintOp().id());
0536         KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(f, resources);
0537         resources << f->prepareLinkedResources(maskingPreset->settings(), globalResourcesInterface);
0538 
0539     }
0540 
0541     return resources;
0542 }
0543 
0544 QList<KoResourceLoadResult> KisPaintOpPreset::embeddedResources(KisResourcesInterfaceSP globalResourcesInterface) const
0545 {
0546     QList<KoResourceLoadResult> resources;
0547 
0548     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(d->settings, resources);
0549 
0550     KisPaintOpFactory* f = KisPaintOpRegistry::instance()->value(paintOp().id());
0551     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(f, resources);
0552     resources << f->prepareEmbeddedResources(d->settings, globalResourcesInterface);
0553 
0554     if (hasMaskingPreset()) {
0555         KisPaintOpPresetSP maskingPreset = createMaskingPreset();
0556         Q_ASSERT(maskingPreset);
0557         KisPaintOpFactory* f = KisPaintOpRegistry::instance()->value(maskingPreset->paintOp().id());
0558         KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(f, resources);
0559         resources << f->prepareEmbeddedResources(maskingPreset->settings(), globalResourcesInterface);
0560 
0561     }
0562 
0563     return resources;
0564 }
0565 
0566 QList<int> KisPaintOpPreset::requiredCanvasResources() const
0567 {
0568     return d->settings ? d->settings->requiredCanvasResources() : QList<int>();
0569 }
0570 
0571 void KisPaintOpPreset::setResourceCacheInterface(KoResourceCacheInterfaceSP cacheInterface)
0572 {
0573     KIS_SAFE_ASSERT_RECOVER_RETURN(d->settings);
0574     d->settings->setResourceCacheInterface(cacheInterface);
0575 }
0576 
0577 KoResourceCacheInterfaceSP KisPaintOpPreset::resourceCacheInterface() const
0578 {
0579     return d->settings ? d->settings->resourceCacheInterface() : KoResourceCacheInterfaceSP();
0580 }
0581 
0582 void KisPaintOpPreset::regenerateResourceCache(KoResourceCacheInterfaceSP cacheInterface)
0583 {
0584     KIS_SAFE_ASSERT_RECOVER_RETURN(d->settings);
0585 
0586     d->settings->regenerateResourceCache(cacheInterface);
0587     cacheInterface->setRelatedResourceCookie(d->settings->sanityVersionCookie());
0588 }
0589 
0590 bool KisPaintOpPreset::sanityCheckResourceCacheIsValid(KoResourceCacheInterfaceSP cacheInterface) const
0591 {
0592     return d->settings->sanityVersionCookie() == cacheInterface->relatedResourceCookie();
0593 }
0594 
0595 KisPaintOpPreset::UpdatedPostponer::UpdatedPostponer(KisPaintOpPresetSP preset)
0596     : m_updateProxy(preset->updateProxyNoCreate())
0597 {
0598     if (m_updateProxy) {
0599         m_updateProxy->postponeSettingsChanges();
0600     }
0601 }
0602 
0603 KisPaintOpPreset::UpdatedPostponer::~UpdatedPostponer()
0604 {
0605     if (m_updateProxy) {
0606         m_updateProxy->unpostponeSettingsChanges();
0607     }
0608 }