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 }