File indexing completed on 2024-05-12 15:59:55

0001 /*
0002  *  SPDX-FileCopyrightText: 2014 Victor Lafon metabolic.ewilan @hotmail.fr
0003  *
0004  * SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 #include "KoResourceBundle.h"
0008 
0009 #include <QBuffer>
0010 #include <QByteArray>
0011 #include <QCryptographicHash>
0012 #include <QDate>
0013 #include <QDir>
0014 #include <QMessageBox>
0015 #include <QPainter>
0016 #include <QProcessEnvironment>
0017 #include <QScopedPointer>
0018 #include <QStringList>
0019 
0020 #include <klocalizedstring.h>
0021 
0022 #include <KisMimeDatabase.h>
0023 #include "KoResourceBundleManifest.h"
0024 #include <KoMD5Generator.h>
0025 #include <KoResourcePaths.h>
0026 #include <KoStore.h>
0027 #include <KoXmlWriter.h>
0028 #include "KisStoragePlugin.h"
0029 #include "KisResourceLoaderRegistry.h"
0030 #include <KisResourceModelProvider.h>
0031 #include <KisResourceModel.h>
0032 #include <KoMD5Generator.h>
0033 
0034 #include <KritaVersionWrapper.h>
0035 
0036 #include <kis_debug.h>
0037 #include <KisGlobalResourcesInterface.h>
0038 
0039 
0040 KoResourceBundle::KoResourceBundle(QString const& fileName)
0041     : m_filename(fileName),
0042       m_bundleVersion("1")
0043 {
0044     m_metadata[KisResourceStorage::s_meta_generator] = "Krita (" + KritaVersionWrapper::versionString(true) + ")";
0045 }
0046 
0047 KoResourceBundle::~KoResourceBundle()
0048 {
0049 }
0050 
0051 QString KoResourceBundle::defaultFileExtension() const
0052 {
0053     return QString(".bundle");
0054 }
0055 
0056 bool KoResourceBundle::load()
0057 {
0058     if (m_filename.isEmpty()) return false;
0059     QScopedPointer<KoStore> resourceStore(KoStore::createStore(m_filename, KoStore::Read, "application/x-krita-resourcebundle", KoStore::Zip));
0060 
0061     if (!resourceStore || resourceStore->bad()) {
0062         qWarning() << "Could not open store on bundle" << m_filename;
0063         return false;
0064 
0065     }
0066     else {
0067 
0068         m_metadata.clear();
0069 
0070         if (resourceStore->open("META-INF/manifest.xml")) {
0071             if (!m_manifest.load(resourceStore->device())) {
0072                 qWarning() << "Could not open manifest for bundle" << m_filename;
0073                 return false;
0074             }
0075             resourceStore->close();
0076 
0077             Q_FOREACH (KoResourceBundleManifest::ResourceReference ref, m_manifest.files()) {
0078                 if (!resourceStore->hasFile(ref.resourcePath)) {
0079                     m_manifest.removeResource(ref);
0080                     qWarning() << "Bundle" << filename() <<  "is broken. File" << ref.resourcePath << "is missing";
0081                 }
0082             }
0083 
0084         } else {
0085             qWarning() << "Could not load META-INF/manifest.xml";
0086             return false;
0087         }
0088 
0089         bool versionFound = false;
0090         if (!readMetaData(resourceStore.data())) {
0091             qWarning() << "Could not load meta.xml";
0092             return false;
0093         }
0094 
0095         if (resourceStore->open("preview.png")) {
0096             // Workaround for some OS (Debian, Ubuntu), where loading directly from the QIODevice
0097             // fails with "libpng error: IDAT: CRC error"
0098             QByteArray data = resourceStore->device()->readAll();
0099             QBuffer buffer(&data);
0100             m_thumbnail.load(&buffer, "PNG");
0101             resourceStore->close();
0102         }
0103         else {
0104             qWarning() << "Could not open preview.png";
0105         }
0106 
0107         /*
0108          * If no version is found it's an old bundle with md5 hashes to fix, or if some manifest resource entry
0109          * doesn't not correspond to a file the bundle is "broken", in both cases we need to recreate the bundle.
0110          */
0111         if (!versionFound) {
0112             m_metadata.insert(KisResourceStorage::s_meta_version, "1");
0113         }
0114 
0115     }
0116 
0117     return true;
0118 }
0119 
0120 bool KoResourceBundle::loadFromDevice(QIODevice *)
0121 {
0122     return false;
0123 }
0124 
0125 bool saveResourceToStore(const QString &filename, KoResourceSP resource, KoStore *store, const QString &resType, KisResourceModel &model)
0126 {
0127     if (!resource) {
0128         qWarning() << "No Resource";
0129         return false;
0130     }
0131 
0132     if (!resource->valid()) {
0133         qWarning() << "Resource is not valid";
0134         return false;
0135     }
0136     if (!store || store->bad()) {
0137         qWarning() << "No Store or Store is Bad";
0138         return false;
0139     }
0140 
0141     QBuffer buf;
0142     buf.open(QFile::WriteOnly);
0143 
0144     bool response = model.exportResource(resource, &buf);
0145     if (!response) {
0146         qWarning() << "Cannot save to device";
0147         return false;
0148     }
0149 
0150     if (!store->open(resType + "/" + filename)) {
0151         qWarning() << "Could not open file in store for resource";
0152         return false;
0153     }
0154 
0155     qint64 size = store->write(buf.data());
0156     store->close();
0157     buf.close();
0158     if (size != buf.size()) {
0159         qWarning() << "Cannot save resource to the store" << size << buf.size();
0160         return false;
0161     }
0162 
0163     if (!resource->thumbnailPath().isEmpty()) {
0164         // hack for MyPaint brush presets previews
0165         const QImage thumbnail = resource->thumbnail();
0166 
0167         // clone resource to find out the file path for its preview
0168         KoResourceSP clonedResource = resource->clone();
0169         clonedResource->setFilename(filename);
0170 
0171         if (!store->open(resType + "/" + clonedResource->thumbnailPath())) {
0172             qWarning() << "Could not open file in store for resource thumbnail";
0173             return false;
0174         }
0175         QBuffer buf;
0176         buf.open(QFile::ReadWrite);
0177         thumbnail.save(&buf, "PNG");
0178 
0179         int size2 = store->write(buf.data());
0180         if (size2 != buf.size()) {
0181             qWarning() << "Cannot save thumbnail to the store" << size << buf.size();
0182         }
0183         store->close();
0184         buf.close();
0185     }
0186 
0187 
0188     return size == buf.size();
0189 }
0190 
0191 bool KoResourceBundle::save()
0192 {
0193     if (m_filename.isEmpty()) return false;
0194 
0195     if (metaData(KisResourceStorage::s_meta_creation_date, "").isEmpty()) {
0196         setMetaData(KisResourceStorage::s_meta_creation_date, QLocale::c().toString(QDate::currentDate(), QStringLiteral("dd/MM/yyyy")));
0197     }
0198     setMetaData(KisResourceStorage::s_meta_dc_date, QLocale::c().toString(QDate::currentDate(), QStringLiteral("dd/MM/yyyy")));
0199 
0200     QDir bundleDir = KoResourcePaths::saveLocation("data", "bundles");
0201     bundleDir.cdUp();
0202 
0203     QScopedPointer<KoStore> store(KoStore::createStore(m_filename, KoStore::Write, "application/x-krita-resourcebundle", KoStore::Zip));
0204 
0205     if (!store || store->bad()) return false;
0206 
0207     Q_FOREACH (const QString &resType, m_manifest.types()) {
0208         KisResourceModel model(resType);
0209         model.setResourceFilter(KisResourceModel::ShowAllResources);
0210         Q_FOREACH (const KoResourceBundleManifest::ResourceReference &ref, m_manifest.files(resType)) {
0211             KoResourceSP res;
0212             if (ref.resourceId >= 0) res = model.resourceForId(ref.resourceId);
0213             if (!res) res = model.resourcesForMD5(ref.md5sum).first();
0214             if (!res) res = model.resourcesForFilename(QFileInfo(ref.resourcePath).fileName()).first();
0215             if (!res) {
0216                 qWarning() << "Could not find resource" << resType << ref.resourceId << ref.md5sum << ref.resourcePath;
0217                 continue;
0218             }
0219 
0220             if (!saveResourceToStore(ref.filenameInBundle, res, store.data(), resType, model)) {
0221                 qWarning() << "Could not save resource" << resType << res->name();
0222             }
0223         }
0224     }
0225 
0226     if (!m_thumbnail.isNull()) {
0227         QByteArray byteArray;
0228         QBuffer buffer(&byteArray);
0229         m_thumbnail.save(&buffer, "PNG");
0230         if (!store->open("preview.png")) qWarning() << "Could not open preview.png";
0231         if (store->write(byteArray) != buffer.size()) qWarning() << "Could not write preview.png";
0232         store->close();
0233     }
0234 
0235     saveManifest(store);
0236 
0237     saveMetadata(store);
0238 
0239     store->finalize();
0240 
0241     return true;
0242 }
0243 
0244 bool KoResourceBundle::saveToDevice(QIODevice */*dev*/) const
0245 {
0246     return false;
0247 }
0248 
0249 void KoResourceBundle::setMetaData(const QString &key, const QString &value)
0250 {
0251     m_metadata.insert(key, value);
0252 }
0253 
0254 const QString KoResourceBundle::metaData(const QString &key, const QString &defaultValue) const
0255 {
0256     if (m_metadata.contains(key)) {
0257         return m_metadata[key];
0258     }
0259     else {
0260         return defaultValue;
0261     }
0262 }
0263 
0264 void KoResourceBundle::addResource(QString resourceType, QString filePath, QVector<KisTagSP> fileTagList, const QString md5sum, const int resourceId, const QString filenameInBundle)
0265 {
0266     QStringList tags;
0267     Q_FOREACH(KisTagSP tag, fileTagList) {
0268         tags << tag->url();
0269     }
0270     m_manifest.addResource(resourceType, filePath, tags, md5sum, resourceId, filenameInBundle);
0271 }
0272 
0273 QList<QString> KoResourceBundle::getTagsList()
0274 {
0275 #if QT_VERSION >= QT_VERSION_CHECK(5,14,0)
0276     return QList<QString>(m_bundletags.begin(), m_bundletags.end());
0277 #else
0278     return QList<QString>::fromSet(m_bundletags);
0279 #endif
0280 }
0281 
0282 QStringList KoResourceBundle::resourceTypes() const
0283 {
0284     return m_manifest.types();
0285 }
0286 
0287 void KoResourceBundle::setThumbnail(QString filename)
0288 {
0289     if (QFileInfo(filename).exists()) {
0290         m_thumbnail = QImage(filename);
0291         m_thumbnail = m_thumbnail.scaled(256, 256, Qt::KeepAspectRatio, Qt::SmoothTransformation);
0292     }
0293     else {
0294         m_thumbnail = QImage(256, 256, QImage::Format_ARGB32);
0295         QPainter gc(&m_thumbnail);
0296         gc.fillRect(0, 0, 256, 256, Qt::red);
0297         gc.end();
0298     }
0299 }
0300 
0301 void KoResourceBundle::writeMeta(const QString &metaTag, KoXmlWriter *writer)
0302 {
0303     if (m_metadata.contains(metaTag)) {
0304         QByteArray mt = metaTag.toUtf8();
0305         QByteArray tx = m_metadata[metaTag].toUtf8();
0306         writer->startElement(mt);
0307         writer->addTextNode(tx);
0308         writer->endElement();
0309     }
0310 }
0311 
0312 void KoResourceBundle::writeUserDefinedMeta(const QString &metaTag, KoXmlWriter *writer)
0313 {
0314     if (m_metadata.contains(metaTag)) {
0315         writer->startElement("meta:meta-userdefined");
0316         writer->addAttribute("meta:name", metaTag);
0317         writer->addAttribute("meta:value", m_metadata[metaTag]);
0318         writer->endElement();
0319     }
0320 }
0321 
0322 bool KoResourceBundle::readMetaData(KoStore *resourceStore)
0323 {
0324     if (resourceStore->open("meta.xml")) {
0325         QDomDocument doc;
0326         if (!doc.setContent(resourceStore->device())) {
0327             qWarning() << "Could not parse meta.xml for" << m_filename;
0328             return false;
0329         }
0330         // First find the manifest:manifest node.
0331         QDomNode n = doc.firstChild();
0332         for (; !n.isNull(); n = n.nextSibling()) {
0333             if (!n.isElement()) {
0334                 continue;
0335             }
0336             if (n.toElement().tagName() == "meta:meta") {
0337                 break;
0338             }
0339         }
0340 
0341         if (n.isNull()) {
0342             qWarning() << "Could not find manifest node for bundle" << m_filename;
0343             return false;
0344         }
0345 
0346         const QDomElement  metaElement = n.toElement();
0347         for (n = metaElement.firstChild(); !n.isNull(); n = n.nextSibling()) {
0348             if (n.isElement()) {
0349                 QDomElement e = n.toElement();
0350                 if (e.tagName() == "meta:meta-userdefined") {
0351                     if (e.attribute("meta:name") == "tag") {
0352                         m_bundletags << e.attribute("meta:value");
0353                     }
0354                     else {
0355                         QString metaName = e.attribute("meta:name");
0356                         if (!metaName.startsWith("meta:") && !metaName.startsWith("dc:")) {
0357                             if (metaName == "email" || metaName == "license" || metaName == "website") { // legacy metadata options
0358                                 if (!m_metadata.contains("meta:" + metaName)) {
0359                                     m_metadata.insert("meta:" + metaName, e.attribute("meta:value"));
0360                                 }
0361                             } else {
0362                                 qWarning() << "Unrecognized metadata: " << e.tagName() << e.attribute("meta:name") << e.attribute("meta:value");
0363                             }
0364                         }
0365                         m_metadata.insert(e.attribute("meta:name"), e.attribute("meta:value"));
0366                     }
0367                 }
0368                 else {
0369                     if (!m_metadata.contains(e.tagName())) {
0370                         m_metadata.insert(e.tagName(), e.firstChild().toText().data());
0371                     }
0372                 }
0373             }
0374         }
0375         resourceStore->close();
0376         return true;
0377     }
0378     return false;
0379 }
0380 
0381 void KoResourceBundle::saveMetadata(QScopedPointer<KoStore> &store)
0382 {
0383     QBuffer buf;
0384 
0385     store->open("meta.xml");
0386     buf.open(QBuffer::WriteOnly);
0387 
0388     KoXmlWriter metaWriter(&buf);
0389     metaWriter.startDocument("office:document-meta");
0390     metaWriter.startElement("meta:meta");
0391 
0392     writeMeta(KisResourceStorage::s_meta_generator, &metaWriter);
0393 
0394     QByteArray ba1 = KisResourceStorage::s_meta_version.toUtf8();
0395     metaWriter.startElement(ba1);
0396     QByteArray ba2  = m_bundleVersion.toUtf8();
0397     metaWriter.addTextNode(ba2);
0398     metaWriter.endElement();
0399 
0400     writeMeta(KisResourceStorage::s_meta_author, &metaWriter);
0401     writeMeta(KisResourceStorage::s_meta_title,  &metaWriter);
0402     writeMeta(KisResourceStorage::s_meta_description, &metaWriter);
0403     writeMeta(KisResourceStorage::s_meta_initial_creator,  &metaWriter);
0404     writeMeta(KisResourceStorage::s_meta_creator, &metaWriter);
0405     writeMeta(KisResourceStorage::s_meta_creation_date, &metaWriter);
0406     writeMeta(KisResourceStorage::s_meta_dc_date, &metaWriter);
0407     writeMeta(KisResourceStorage::s_meta_email, &metaWriter);
0408     writeMeta(KisResourceStorage::s_meta_license, &metaWriter);
0409     writeMeta(KisResourceStorage::s_meta_website, &metaWriter);
0410 
0411     // For compatibility
0412     writeUserDefinedMeta("email", &metaWriter);
0413     writeUserDefinedMeta("license", &metaWriter);
0414     writeUserDefinedMeta("website", &metaWriter);
0415 
0416 
0417     Q_FOREACH (const QString &tag, m_bundletags) {
0418         QByteArray ba1 = KisResourceStorage::s_meta_user_defined.toUtf8();
0419         QByteArray ba2 = KisResourceStorage::s_meta_name.toUtf8();
0420         QByteArray ba3 = KisResourceStorage::s_meta_value.toUtf8();
0421         metaWriter.startElement(ba1);
0422         metaWriter.addAttribute(ba2, "tag");
0423         metaWriter.addAttribute(ba3, tag);
0424         metaWriter.endElement();
0425     }
0426 
0427     metaWriter.endElement(); // meta:meta
0428     metaWriter.endDocument();
0429 
0430     buf.close();
0431     store->write(buf.data());
0432     store->close();
0433 }
0434 
0435 void KoResourceBundle::saveManifest(QScopedPointer<KoStore> &store)
0436 {
0437     store->open("META-INF/manifest.xml");
0438     QBuffer buf;
0439     buf.open(QBuffer::WriteOnly);
0440     m_manifest.save(&buf);
0441     buf.close();
0442     store->write(buf.data());
0443     store->close();
0444 }
0445 
0446 int KoResourceBundle::resourceCount() const
0447 {
0448     return m_manifest.files().count();
0449 }
0450 
0451 KoResourceBundleManifest &KoResourceBundle::manifest()
0452 {
0453     return m_manifest;
0454 }
0455 
0456 KoResourceSP KoResourceBundle::resource(const QString &resourceType, const QString &filepath)
0457 {
0458     QString mime = KisMimeDatabase::mimeTypeForSuffix(filepath);
0459     KisResourceLoaderBase *loader = KisResourceLoaderRegistry::instance()->loader(resourceType, mime);
0460     if (!loader) {
0461         qWarning() << "Could not create loader for" << resourceType << filepath << mime;
0462         return 0;
0463     }
0464 
0465     QStringList parts = filepath.split('/', QString::SkipEmptyParts);
0466     Q_ASSERT(parts.size() == 2);
0467 
0468     KoResourceSP resource = loader->create(parts[1]);
0469     return loadResource(resource) ? resource : 0;
0470 }
0471 
0472 bool KoResourceBundle::exportResource(const QString &resourceType, const QString &fileName, QIODevice *device)
0473 {
0474     if (m_filename.isEmpty()) return false;
0475 
0476     QScopedPointer<KoStore> resourceStore(KoStore::createStore(m_filename, KoStore::Read, "application/x-krita-resourcebundle", KoStore::Zip));
0477 
0478     if (!resourceStore || resourceStore->bad()) {
0479         qWarning() << "Could not open store on bundle" << m_filename;
0480         return false;
0481     }
0482     const QString filePath = QString("%1/%2").arg(resourceType).arg(fileName);
0483 
0484     if (!resourceStore->open(filePath)) {
0485         qWarning() << "Could not open file in bundle" << filePath;
0486         return false;
0487     }
0488 
0489     device->write(resourceStore->device()->readAll());
0490 
0491     return true;
0492 }
0493 
0494 bool KoResourceBundle::loadResource(KoResourceSP resource)
0495 {
0496     if (m_filename.isEmpty()) return false;
0497 
0498     const QString resourceType = resource->resourceType().first;
0499 
0500     QScopedPointer<KoStore> resourceStore(KoStore::createStore(m_filename, KoStore::Read, "application/x-krita-resourcebundle", KoStore::Zip));
0501 
0502     if (!resourceStore || resourceStore->bad()) {
0503         qWarning() << "Could not open store on bundle" << m_filename;
0504         return false;
0505     }
0506     const QString fileName = QString("%1/%2").arg(resourceType).arg(resource->filename());
0507 
0508     if (!resourceStore->open(fileName)) {
0509         qWarning() << "Could not open file in bundle" << fileName;
0510         return false;
0511     }
0512 
0513     if (!resource->loadFromDevice(resourceStore->device(),
0514                                   KisGlobalResourcesInterface::instance())) {
0515         qWarning() << "Could not load the resource from the bundle" << resourceType << fileName << m_filename;
0516         return false;
0517     }
0518 
0519     resourceStore->close();
0520 
0521     if ((resource->image().isNull() || resource->thumbnail().isNull()) && !resource->thumbnailPath().isNull()) {
0522 
0523         if (!resourceStore->open(resourceType + '/' + resource->thumbnailPath())) {
0524             qWarning() << "Could not open thumbnail in bundle" << resource->thumbnailPath();
0525             return false;
0526         }
0527 
0528         QImage img;
0529         img.load(resourceStore->device(), QFileInfo(resource->thumbnailPath()).completeSuffix().toLatin1());
0530         resource->setImage(img);
0531         resource->updateThumbnail();
0532 
0533         resourceStore->close();
0534     }
0535 
0536     return true;
0537 }
0538 
0539 QString KoResourceBundle::resourceMd5(const QString &url)
0540 {
0541     QString result;
0542 
0543     if (m_filename.isEmpty()) return result;
0544 
0545     QScopedPointer<KoStore> resourceStore(KoStore::createStore(m_filename, KoStore::Read, "application/x-krita-resourcebundle", KoStore::Zip));
0546 
0547     if (!resourceStore || resourceStore->bad()) {
0548         qWarning() << "Could not open store on bundle" << m_filename;
0549         return result;
0550     }
0551     if (!resourceStore->open(url)) {
0552         qWarning() << "Could not open file in bundle" << url;
0553         return result;
0554     }
0555 
0556     result = KoMD5Generator::generateHash(resourceStore->device());
0557     resourceStore->close();
0558 
0559     return result;
0560 }
0561 
0562 QImage KoResourceBundle::image() const
0563 {
0564     return m_thumbnail;
0565 }
0566 
0567 QString KoResourceBundle::filename() const
0568 {
0569     return m_filename;
0570 }