File indexing completed on 2024-04-21 04:52:02
0001 /* 0002 SPDX-FileCopyrightText: 2017 Nicolas Carion 0003 SPDX-FileCopyrightText: 2022 Julius Künzel <jk.kdedev@smartlab.uber.space> 0004 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 #include "renderpresetrepository.hpp" 0008 #include "kdenlive_debug.h" 0009 #include "kdenlivesettings.h" 0010 #include "renderpresetmodel.hpp" 0011 #include "xml/xml.hpp" 0012 #include <KLocalizedString> 0013 #include <KMessageBox> 0014 #include <QDir> 0015 #include <QInputDialog> 0016 #include <QStandardPaths> 0017 #include <algorithm> 0018 #include <mlt++/MltConsumer.h> 0019 #include <mlt++/MltProfile.h> 0020 0021 std::unique_ptr<RenderPresetRepository> RenderPresetRepository::instance; 0022 std::once_flag RenderPresetRepository::m_onceFlag; 0023 std::vector<std::pair<int, QString>> RenderPresetRepository::colorProfiles{{601, QStringLiteral("ITU-R BT.601")}, 0024 {709, QStringLiteral("ITU-R BT.709")}, 0025 {240, QStringLiteral("SMPTE ST240")}, 0026 {9, QStringLiteral("ITU-R BT.2020")}, 0027 {10, QStringLiteral("ITU-R BT.2020")}}; 0028 QStringList RenderPresetRepository::m_acodecsList; 0029 QStringList RenderPresetRepository::m_vcodecsList; 0030 QStringList RenderPresetRepository::m_supportedFormats; 0031 0032 RenderPresetRepository::RenderPresetRepository() 0033 { 0034 refresh(); 0035 } 0036 0037 std::unique_ptr<RenderPresetRepository> &RenderPresetRepository::get() 0038 { 0039 std::call_once(m_onceFlag, [] { instance.reset(new RenderPresetRepository()); }); 0040 return instance; 0041 } 0042 0043 // static 0044 void RenderPresetRepository::checkCodecs(bool forceRefresh) 0045 { 0046 if (!(m_acodecsList.isEmpty() || m_vcodecsList.isEmpty() || m_supportedFormats.isEmpty() || forceRefresh)) { 0047 return; 0048 } 0049 Mlt::Profile p; 0050 auto *consumer = new Mlt::Consumer(p, "avformat"); 0051 if (consumer) { 0052 consumer->set("vcodec", "list"); 0053 consumer->set("acodec", "list"); 0054 consumer->set("f", "list"); 0055 consumer->start(); 0056 consumer->stop(); 0057 m_vcodecsList.clear(); 0058 Mlt::Properties vcodecs(mlt_properties(consumer->get_data("vcodec"))); 0059 m_vcodecsList.reserve(vcodecs.count()); 0060 for (int i = 0; i < vcodecs.count(); ++i) { 0061 m_vcodecsList << QString(vcodecs.get(i)); 0062 } 0063 m_acodecsList.clear(); 0064 Mlt::Properties acodecs(mlt_properties(consumer->get_data("acodec"))); 0065 m_acodecsList.reserve(acodecs.count()); 0066 for (int i = 0; i < acodecs.count(); ++i) { 0067 m_acodecsList << QString(acodecs.get(i)); 0068 } 0069 m_supportedFormats.clear(); 0070 Mlt::Properties formats(mlt_properties(consumer->get_data("f"))); 0071 m_supportedFormats.reserve(formats.count()); 0072 for (int i = 0; i < formats.count(); ++i) { 0073 m_supportedFormats << QString(formats.get(i)); 0074 } 0075 delete consumer; 0076 } 0077 } 0078 0079 void RenderPresetRepository::refresh(bool fullRefresh) 0080 { 0081 QWriteLocker locker(&m_mutex); 0082 0083 if (fullRefresh) { 0084 // Reset all profiles 0085 m_profiles.clear(); 0086 m_groups.clear(); 0087 } 0088 0089 // Profiles downloaded by KNewStuff 0090 QString exportFolder = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + QStringLiteral("/export/"); 0091 QDir directory(exportFolder); 0092 QStringList fileList = directory.entryList({QStringLiteral("*.xml")}, QDir::Files); 0093 0094 // Parse customprofiles.xml always first so custom profiles always override 0095 // profiles downloaded with KNewStuff 0096 if (directory.exists(QStringLiteral("customprofiles.xml"))) { 0097 parseFile(directory.absoluteFilePath(QStringLiteral("customprofiles.xml")), true); 0098 // no need to parse this again 0099 fileList.removeAll(QStringLiteral("customprofiles.xml")); 0100 } 0101 // Parse files downloaded with KNewStuff 0102 for (const QString &filename : qAsConst(fileList)) { 0103 parseFile(directory.absoluteFilePath(filename), true); 0104 } 0105 0106 // Parse some MLT's profiles 0107 parseMltPresets(); 0108 0109 // Parse our xml profile 0110 QString exportFile = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("export/profiles.xml")); 0111 parseFile(exportFile, false); 0112 0113 // focusFirstVisibleItem(selectedProfile); 0114 } 0115 0116 void RenderPresetRepository::parseFile(const QString &exportFile, bool editable) 0117 { 0118 QDomDocument doc; 0119 if (!Xml::docContentFromFile(doc, exportFile, false)) { 0120 return; 0121 } 0122 QDomElement documentElement; 0123 QDomNodeList groups = doc.elementsByTagName(QStringLiteral("group")); 0124 0125 if (editable || groups.isEmpty()) { 0126 QDomElement profiles = doc.documentElement(); 0127 if (editable && profiles.attribute(QStringLiteral("version"), nullptr).toInt() < 1) { 0128 // this is an old profile version, update it 0129 QDomDocument newdoc; 0130 QDomElement newprofiles = newdoc.createElement(QStringLiteral("profiles")); 0131 newprofiles.setAttribute(QStringLiteral("version"), 1); 0132 newdoc.appendChild(newprofiles); 0133 QDomNodeList profilelist = doc.elementsByTagName(QStringLiteral("profile")); 0134 for (int i = 0; i < profilelist.count(); ++i) { 0135 QString category = i18nc("Category Name", "Custom"); 0136 QString ext; 0137 QDomNode parent = profilelist.at(i).parentNode(); 0138 if (!parent.isNull()) { 0139 QDomElement parentNode = parent.toElement(); 0140 if (parentNode.hasAttribute(QStringLiteral("name"))) { 0141 category = parentNode.attribute(QStringLiteral("name")); 0142 } 0143 ext = parentNode.attribute(QStringLiteral("extension")); 0144 } 0145 if (!profilelist.at(i).toElement().hasAttribute(QStringLiteral("category"))) { 0146 profilelist.at(i).toElement().setAttribute(QStringLiteral("category"), category); 0147 } 0148 if (!ext.isEmpty()) { 0149 profilelist.at(i).toElement().setAttribute(QStringLiteral("extension"), ext); 0150 } 0151 QDomNode n = profilelist.at(i).cloneNode(); 0152 newprofiles.appendChild(newdoc.importNode(n, true)); 0153 } 0154 QFile file(exportFile); 0155 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { 0156 KMessageBox::error(nullptr, i18n("Unable to write to file %1", exportFile)); 0157 return; 0158 } 0159 QTextStream out(&file); 0160 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0161 out.setCodec("UTF-8"); 0162 #endif 0163 out << newdoc.toString(); 0164 file.close(); 0165 // now that we fixed the file, run this function again 0166 parseFile(exportFile, editable); 0167 return; 0168 } 0169 0170 QDomNode node = doc.elementsByTagName(QStringLiteral("profile")).at(0); 0171 if (node.isNull()) { 0172 return; 0173 } 0174 int count = 1; 0175 while (!node.isNull()) { 0176 QDomElement profile = node.toElement(); 0177 0178 std::unique_ptr<RenderPresetModel> model(new RenderPresetModel(profile, exportFile, editable)); 0179 0180 if (m_profiles.count(model->name()) == 0) { 0181 m_groups.append(model->groupName()); 0182 m_groups.removeDuplicates(); 0183 m_profiles.insert(std::make_pair(model->name(), std::move(model))); 0184 } 0185 0186 node = doc.elementsByTagName(QStringLiteral("profile")).at(count); 0187 count++; 0188 } 0189 return; 0190 } 0191 0192 int i = 0; 0193 0194 while (!groups.item(i).isNull()) { 0195 documentElement = groups.item(i).toElement(); 0196 QString groupName = documentElement.attribute(QStringLiteral("name"), i18nc("Attribute Name", "Custom")); 0197 QString extension = documentElement.attribute(QStringLiteral("extension"), QString()); 0198 QString renderer = documentElement.attribute(QStringLiteral("renderer"), QString()); 0199 0200 QDomNode n = groups.item(i).firstChild(); 0201 while (!n.isNull()) { 0202 if (n.toElement().tagName() != QLatin1String("profile")) { 0203 n = n.nextSibling(); 0204 continue; 0205 } 0206 QDomElement profile = n.toElement(); 0207 0208 std::unique_ptr<RenderPresetModel> model(new RenderPresetModel(profile, exportFile, editable, groupName, renderer)); 0209 if (m_profiles.count(model->name()) == 0) { 0210 m_groups.append(model->groupName()); 0211 m_groups.removeDuplicates(); 0212 m_profiles.insert(std::make_pair(model->name(), std::move(model))); 0213 } 0214 n = n.nextSibling(); 0215 } 0216 0217 ++i; 0218 } 0219 } 0220 0221 void RenderPresetRepository::parseMltPresets() 0222 { 0223 QDir root(KdenliveSettings::mltpath()); 0224 if (!root.cd(QStringLiteral("../presets/consumer/avformat"))) { 0225 // Cannot find MLT's presets directory 0226 qCWarning(KDENLIVE_LOG) << " / / / WARNING, cannot find MLT's preset folder"; 0227 return; 0228 } 0229 if (root.cd(QStringLiteral("lossless"))) { 0230 QString groupName = i18n("Lossless/HQ"); 0231 const QStringList profiles = root.entryList(QDir::Files, QDir::Name); 0232 for (const QString &prof : profiles) { 0233 std::unique_ptr<RenderPresetModel> model( 0234 new RenderPresetModel(groupName, root.absoluteFilePath(prof), prof, QString("properties=lossless/" + prof), true)); 0235 if (m_profiles.count(model->name()) == 0) { 0236 m_groups.append(model->groupName()); 0237 m_groups.removeDuplicates(); 0238 m_profiles.insert(std::make_pair(model->name(), std::move(model))); 0239 } 0240 } 0241 } 0242 if (root.cd(QStringLiteral("../stills"))) { 0243 QString groupName = i18nc("Category Name", "Images sequence"); 0244 QStringList profiles = root.entryList(QDir::Files, QDir::Name); 0245 for (const QString &prof : qAsConst(profiles)) { 0246 std::unique_ptr<RenderPresetModel> model( 0247 new RenderPresetModel(groupName, root.absoluteFilePath(prof), prof, QString("properties=stills/" + prof), false)); 0248 m_groups.append(model->groupName()); 0249 m_groups.removeDuplicates(); 0250 m_profiles.insert(std::make_pair(model->name(), std::move(model))); 0251 } 0252 // Add GIF as image sequence 0253 root.cdUp(); 0254 std::unique_ptr<RenderPresetModel> model( 0255 new RenderPresetModel(groupName, root.absoluteFilePath(QStringLiteral("GIF")), QStringLiteral("GIF"), QStringLiteral("properties=GIF"), false)); 0256 if (m_profiles.count(model->name()) == 0) { 0257 m_groups.append(model->groupName()); 0258 m_groups.removeDuplicates(); 0259 m_profiles.insert(std::make_pair(model->name(), std::move(model))); 0260 } 0261 } 0262 } 0263 0264 QVector<QString> RenderPresetRepository::getAllPresets() const 0265 { 0266 QReadLocker locker(&m_mutex); 0267 0268 QVector<QString> list; 0269 std::transform(m_profiles.begin(), m_profiles.end(), std::inserter(list, list.begin()), 0270 [&](decltype(*m_profiles.begin()) corresp) { return corresp.first; }); 0271 std::sort(list.begin(), list.end()); 0272 return list; 0273 } 0274 0275 std::unique_ptr<RenderPresetModel> &RenderPresetRepository::getPreset(const QString &name) 0276 { 0277 QReadLocker locker(&m_mutex); 0278 if (!presetExists(name)) { 0279 // TODO 0280 // qCWarning(KDENLIVE_LOG) << "//// WARNING: profile not found: " << path << ". Returning default profile instead."; 0281 /*QString default_profile = KdenliveSettings::default_profile(); 0282 if (default_profile.isEmpty()) { 0283 default_profile = QStringLiteral("dv_pal"); 0284 } 0285 if (m_profiles.count(default_profile) == 0) { 0286 qCWarning(KDENLIVE_LOG) << "//// WARNING: default profile not found: " << default_profile << ". Returning random profile instead."; 0287 return (*(m_profiles.begin())).second; 0288 } 0289 return m_profiles.at(default_profile);*/ 0290 } 0291 return m_profiles.at(name); 0292 } 0293 0294 bool RenderPresetRepository::presetExists(const QString &name) const 0295 { 0296 QReadLocker locker(&m_mutex); 0297 return m_profiles.count(name) > 0; 0298 } 0299 0300 const QString RenderPresetRepository::savePreset(RenderPresetModel *preset, bool editMode, const QString &oldName) 0301 { 0302 0303 QDomElement newPreset = preset->toXml(); 0304 0305 QDomDocument doc; 0306 0307 QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + QStringLiteral("/export/")); 0308 if (!dir.exists()) { 0309 dir.mkpath(QStringLiteral(".")); 0310 } 0311 QString fileName(dir.absoluteFilePath(QStringLiteral("customprofiles.xml"))); 0312 if (dir.exists(QStringLiteral("customprofiles.xml")) && !Xml::docContentFromFile(doc, fileName, false)) { 0313 KMessageBox::error(nullptr, i18n("Cannot read file %1", fileName)); 0314 return {}; 0315 } 0316 0317 QDomElement documentElement; 0318 QDomElement profiles = doc.documentElement(); 0319 if (profiles.isNull() || profiles.tagName() != QLatin1String("profiles")) { 0320 doc.clear(); 0321 profiles = doc.createElement(QStringLiteral("profiles")); 0322 profiles.setAttribute(QStringLiteral("version"), 1); 0323 doc.appendChild(profiles); 0324 } 0325 int version = profiles.attribute(QStringLiteral("version"), nullptr).toInt(); 0326 if (version < 1) { 0327 doc.clear(); 0328 profiles = doc.createElement(QStringLiteral("profiles")); 0329 profiles.setAttribute(QStringLiteral("version"), 1); 0330 doc.appendChild(profiles); 0331 } 0332 0333 QDomNodeList profilelist = doc.elementsByTagName(QStringLiteral("profile")); 0334 // Check existing profiles 0335 QStringList existingProfileNames; 0336 int i = 0; 0337 while (!profilelist.item(i).isNull()) { 0338 documentElement = profilelist.item(i).toElement(); 0339 QString profileName = documentElement.attribute(QStringLiteral("name")); 0340 existingProfileNames << profileName; 0341 i++; 0342 } 0343 0344 QString newPresetName = preset->name(); 0345 while (existingProfileNames.contains(newPresetName)) { 0346 QString updatedPresetName = newPresetName; 0347 if (!editMode) { 0348 bool ok; 0349 updatedPresetName = QInputDialog::getText(nullptr, i18n("Preset already exists"), 0350 i18n("This preset name already exists. Change the name if you do not want to overwrite it."), 0351 QLineEdit::Normal, newPresetName, &ok); 0352 if (!ok) { 0353 return {}; 0354 } 0355 } 0356 0357 if (updatedPresetName == newPresetName) { 0358 // remove previous profile 0359 int ix = existingProfileNames.indexOf(newPresetName); 0360 profiles.removeChild(profilelist.item(ix)); 0361 existingProfileNames.removeAt(ix); 0362 break; 0363 } 0364 newPresetName = updatedPresetName; 0365 newPreset.setAttribute(QStringLiteral("name"), newPresetName); 0366 } 0367 if (editMode && !oldName.isEmpty() && existingProfileNames.contains(oldName)) { 0368 profiles.removeChild(profilelist.item(existingProfileNames.indexOf(oldName))); 0369 } 0370 0371 profiles.appendChild(newPreset); 0372 QFile file(fileName); 0373 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { 0374 KMessageBox::error(nullptr, i18n("Cannot open file %1", file.fileName())); 0375 return {}; 0376 } 0377 QTextStream out(&file); 0378 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0379 out.setCodec("UTF-8"); 0380 #endif 0381 out << doc.toString(); 0382 if (file.error() != QFile::NoError) { 0383 KMessageBox::error(nullptr, i18n("Cannot write to file %1", file.fileName())); 0384 file.close(); 0385 return {}; 0386 } 0387 file.close(); 0388 refresh(true); 0389 return newPresetName; 0390 } 0391 0392 bool RenderPresetRepository::deletePreset(const QString &name, bool dontRefresh) 0393 { 0394 // TODO: delete a profile installed by KNewStuff the easy way 0395 /* 0396 QString edit = m_view.formats->currentItem()->data(EditableRole).toString(); 0397 if (!edit.endsWith(QLatin1String("customprofiles.xml"))) { 0398 // This is a KNewStuff installed file, process through KNS 0399 KNS::Engine engine(0); 0400 if (engine.init("kdenlive_render.knsrc")) { 0401 KNS::Entry::List entries; 0402 } 0403 return; 0404 }*/ 0405 0406 if (!getPreset(name)->editable()) { 0407 return false; 0408 } 0409 0410 QString exportFile = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + QStringLiteral("/export/customprofiles.xml"); 0411 QDomDocument doc; 0412 QFile file(exportFile); 0413 doc.setContent(&file, false); 0414 file.close(); 0415 0416 QDomElement documentElement; 0417 QDomNodeList profiles = doc.elementsByTagName(QStringLiteral("profile")); 0418 if (profiles.isEmpty()) { 0419 return false; 0420 } 0421 int i = 0; 0422 QString profileName; 0423 while (!profiles.item(i).isNull()) { 0424 documentElement = profiles.item(i).toElement(); 0425 profileName = documentElement.attribute(QStringLiteral("name")); 0426 if (profileName == name) { 0427 doc.documentElement().removeChild(profiles.item(i)); 0428 break; 0429 } 0430 ++i; 0431 } 0432 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { 0433 KMessageBox::error(nullptr, i18n("Unable to write to file %1", exportFile)); 0434 return false; 0435 } 0436 QTextStream out(&file); 0437 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0438 out.setCodec("UTF-8"); 0439 #endif 0440 out << doc.toString(); 0441 if (file.error() != QFile::NoError) { 0442 KMessageBox::error(nullptr, i18n("Cannot write to file %1", exportFile)); 0443 file.close(); 0444 return false; 0445 } 0446 file.close(); 0447 0448 if (!dontRefresh) { 0449 refresh(true); 0450 } 0451 return true; 0452 }