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 }