File indexing completed on 2024-05-19 04:27:42

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