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 }