]> git.sesse.net Git - kdenlive/blob - src/documentchecker.cpp
Warn about missing images and fonts in title clips when opening a document:
[kdenlive] / src / documentchecker.cpp
1 /***************************************************************************
2  *   Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org)        *
3  *                                                                         *
4  *   This program is free software; you can redistribute it and/or modify  *
5  *   it under the terms of the GNU General Public License as published by  *
6  *   the Free Software Foundation; either version 2 of the License, or     *
7  *   (at your option) any later version.                                   *
8  *                                                                         *
9  *   This program is distributed in the hope that it will be useful,       *
10  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
11  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
12  *   GNU General Public License for more details.                          *
13  *                                                                         *
14  *   You should have received a copy of the GNU General Public License     *
15  *   along with this program; if not, write to the                         *
16  *   Free Software Foundation, Inc.,                                       *
17  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA          *
18  ***************************************************************************/
19
20
21 #include "documentchecker.h"
22 #include "kthumb.h"
23 #include "docclipbase.h"
24 #include "titlewidget.h"
25 #include "definitions.h"
26 #include "kdenlivesettings.h"
27
28 #include <KDebug>
29 #include <KGlobalSettings>
30 #include <KFileItem>
31 #include <KIO/NetAccess>
32 #include <KFileDialog>
33 #include <KApplication>
34 #include <KUrlRequesterDialog>
35 #include <KMessageBox>
36
37 #include <QTreeWidgetItem>
38 #include <QFile>
39 #include <QHeaderView>
40 #include <QIcon>
41 #include <QPixmap>
42 #include <QTimer>
43 #include <QCryptographicHash>
44
45 const int hashRole = Qt::UserRole;
46 const int sizeRole = Qt::UserRole + 1;
47 const int idRole = Qt::UserRole + 2;
48 const int statusRole = Qt::UserRole + 3;
49 const int typeRole = Qt::UserRole + 4;
50 const int typeOriginalResource = Qt::UserRole + 5;
51
52 const int CLIPMISSING = 0;
53 const int CLIPOK = 1;
54 const int CLIPPLACEHOLDER = 2;
55 const int LUMAMISSING = 10;
56 const int LUMAOK = 11;
57 const int LUMAPLACEHOLDER = 12;
58
59 enum TITLECLIPTYPE { TITLE_IMAGE_ELEMENT = 20, TITLE_FONT_ELEMENT = 21 };
60
61 DocumentChecker::DocumentChecker(QDomNodeList infoproducers, QDomDocument doc):
62         m_info(infoproducers), m_doc(doc), m_dialog(NULL)
63 {
64
65 }
66
67
68 bool DocumentChecker::hasMissingClips()
69 {
70     int clipType;
71     QDomElement e;
72     QString id;
73     QString resource;
74     QList <QDomElement> missingClips;
75     for (int i = 0; i < m_info.count(); i++) {
76         e = m_info.item(i).toElement();
77         clipType = e.attribute("type").toInt();
78         if (clipType == COLOR) continue;
79         if (clipType == TEXT) {
80             //TODO: Check is clip template is missing (xmltemplate) or hash changed
81             QStringList images = TitleWidget::extractImageList(e.attribute("xmldata"));
82             QStringList fonts = TitleWidget::extractFontList(e.attribute("xmldata"));
83             checkMissingImages(missingClips, images, fonts, e.attribute("id"), e.attribute("name"));
84             continue;
85         }
86         id = e.attribute("id");
87         resource = e.attribute("resource");
88         if (clipType == SLIDESHOW) resource = KUrl(resource).directory();
89         if (!KIO::NetAccess::exists(KUrl(resource), KIO::NetAccess::SourceSide, 0)) {
90             // Missing clip found
91             missingClips.append(e);
92         } else {
93             // Check if the clip has changed
94             if (clipType != SLIDESHOW && e.hasAttribute("file_hash")) {
95                 if (e.attribute("file_hash") != DocClipBase::getHash(e.attribute("resource")))
96                     e.removeAttribute("file_hash");
97             }
98         }
99     }
100     if (missingClips.isEmpty()) {
101         return false;
102     }
103     m_dialog = new QDialog();
104     m_dialog->setFont(KGlobalSettings::toolBarFont());
105     m_ui.setupUi(m_dialog);
106
107     QStringList missingLumas;
108     QDomNodeList trans = m_doc.elementsByTagName("transition");
109     for (int i = 0; i < trans.count(); i++) {
110         QString luma = getProperty(trans.at(i).toElement(), "luma");
111         if (!luma.isEmpty() && !QFile::exists(luma)) {
112             if (!missingLumas.contains(luma)) {
113                 missingLumas.append(luma);
114                 QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Luma file") << luma);
115                 item->setIcon(0, KIcon("dialog-close"));
116                 item->setData(0, idRole, luma);
117                 item->setData(0, statusRole, LUMAMISSING);
118             }
119         }
120     }
121
122     m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
123     for (int i = 0; i < missingClips.count(); i++) {
124         e = missingClips.at(i).toElement();
125         QString clipType;
126         int t = e.attribute("type").toInt();
127         switch (t) {
128         case AV:
129             clipType = i18n("Video clip");
130             break;
131         case VIDEO:
132             clipType = i18n("Mute video clip");
133             break;
134         case AUDIO:
135             clipType = i18n("Audio clip");
136             break;
137         case PLAYLIST:
138             clipType = i18n("Playlist clip");
139             break;
140         case IMAGE:
141             clipType = i18n("Image clip");
142             break;
143         case SLIDESHOW:
144             clipType = i18n("Slideshow clip");
145             break;
146         case TITLE_IMAGE_ELEMENT:
147             clipType = i18n("Title Image");
148             break;
149         case TITLE_FONT_ELEMENT:
150             clipType = i18n("Title Font");
151             break;
152         default:
153             clipType = i18n("Video clip");
154         }
155         QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << clipType);
156         if (t == TITLE_IMAGE_ELEMENT) {
157             item->setIcon(0, KIcon("dialog-warning"));
158             item->setToolTip(1, e.attribute("name"));
159             item->setText(1, e.attribute("resource"));
160             item->setData(0, statusRole, CLIPPLACEHOLDER);
161             item->setData(0, typeOriginalResource, e.attribute("resource"));
162         } else if (t == TITLE_FONT_ELEMENT) {
163             item->setIcon(0, KIcon("dialog-warning"));
164             item->setToolTip(1, e.attribute("name"));
165             QString ft = e.attribute("resource");
166             QString newft = QFontInfo(QFont(ft)).family();
167             item->setText(1, i18n("%1, will be replaced by %2", ft, newft));
168             item->setData(0, statusRole, CLIPPLACEHOLDER);
169         } else {
170             item->setIcon(0, KIcon("dialog-close"));
171             item->setText(1, e.attribute("resource"));
172             item->setData(0, hashRole, e.attribute("file_hash"));
173             item->setData(0, sizeRole, e.attribute("file_size"));
174             item->setData(0, statusRole, CLIPMISSING);
175         }
176         item->setData(0, typeRole, t);
177         item->setData(0, idRole, e.attribute("id"));
178     }
179     connect(m_ui.recursiveSearch, SIGNAL(pressed()), this, SLOT(slotSearchClips()));
180     connect(m_ui.usePlaceholders, SIGNAL(pressed()), this, SLOT(slotPlaceholders()));
181     connect(m_ui.removeSelected, SIGNAL(pressed()), this, SLOT(slotDeleteSelected()));
182     connect(m_ui.treeWidget, SIGNAL(itemDoubleClicked(QTreeWidgetItem *, int)), this, SLOT(slotEditItem(QTreeWidgetItem *, int)));
183     connect(m_ui.treeWidget, SIGNAL(itemSelectionChanged()), this, SLOT(slotCheckButtons()));
184     //adjustSize();
185     if (m_ui.treeWidget->topLevelItem(0)) m_ui.treeWidget->setCurrentItem(m_ui.treeWidget->topLevelItem(0));
186     checkStatus();
187     int acceptMissing = m_dialog->exec();
188     if (acceptMissing == QDialog::Accepted) acceptDialog();
189     return (acceptMissing != QDialog::Accepted);
190 }
191
192 DocumentChecker::~DocumentChecker()
193 {
194     if (m_dialog) delete m_dialog;
195 }
196
197
198 QString DocumentChecker::getProperty(QDomElement effect, const QString &name)
199 {
200     QDomNodeList params = effect.elementsByTagName("property");
201     for (int i = 0; i < params.count(); i++) {
202         QDomElement e = params.item(i).toElement();
203         if (e.attribute("name") == name) {
204             return e.firstChild().nodeValue();
205         }
206     }
207     return QString();
208 }
209
210 void DocumentChecker::setProperty(QDomElement effect, const QString &name, const QString value)
211 {
212     QDomNodeList params = effect.elementsByTagName("property");
213     for (int i = 0; i < params.count(); i++) {
214         QDomElement e = params.item(i).toElement();
215         if (e.attribute("name") == name) {
216             e.firstChild().setNodeValue(value);
217         }
218     }
219 }
220
221 void DocumentChecker::slotSearchClips()
222 {
223     QString newpath = KFileDialog::getExistingDirectory(KUrl("kfiledialog:///clipfolder"), kapp->activeWindow(), i18n("Clips folder"));
224     if (newpath.isEmpty()) return;
225     int ix = 0;
226     m_ui.recursiveSearch->setEnabled(false);
227     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
228     while (child) {
229         if (child->data(0, statusRole).toInt() == CLIPMISSING) {
230             QString clipPath = searchFileRecursively(QDir(newpath), child->data(0, sizeRole).toString(), child->data(0, hashRole).toString());
231             if (!clipPath.isEmpty()) {
232                 child->setText(1, clipPath);
233                 child->setIcon(0, KIcon("dialog-ok"));
234                 child->setData(0, statusRole, CLIPOK);
235             }
236         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
237             QString fileName = searchLuma(child->data(0, idRole).toString());
238             if (!fileName.isEmpty()) {
239                 child->setText(1, fileName);
240                 child->setIcon(0, KIcon("dialog-ok"));
241                 child->setData(0, statusRole, LUMAOK);
242             }
243         }
244         ix++;
245         child = m_ui.treeWidget->topLevelItem(ix);
246     }
247     m_ui.recursiveSearch->setEnabled(true);
248     checkStatus();
249 }
250
251
252 QString DocumentChecker::searchLuma(QString file) const
253 {
254     KUrl searchPath(KdenliveSettings::mltpath());
255     if (file.contains("PAL"))
256         searchPath.cd("../lumas/PAL");
257     else
258         searchPath.cd("../lumas/NTSC");
259     QString result = searchPath.path(KUrl::AddTrailingSlash) + KUrl(file).fileName();
260     if (QFile::exists(result))
261         return result;
262     return QString();
263 }
264
265
266 QString DocumentChecker::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const
267 {
268     QString foundFileName;
269     QByteArray fileData;
270     QByteArray fileHash;
271     QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable);
272     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
273         QFile file(dir.absoluteFilePath(filesAndDirs.at(i)));
274         if (QString::number(file.size()) == matchSize) {
275             if (file.open(QIODevice::ReadOnly)) {
276                 /*
277                 * 1 MB = 1 second per 450 files (or faster)
278                 * 10 MB = 9 seconds per 450 files (or faster)
279                 */
280                 if (file.size() > 1000000*2) {
281                     fileData = file.read(1000000);
282                     if (file.seek(file.size() - 1000000))
283                         fileData.append(file.readAll());
284                 } else
285                     fileData = file.readAll();
286                 file.close();
287                 fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
288                 if (QString(fileHash.toHex()) == matchHash)
289                     return file.fileName();
290             }
291         }
292         //kDebug() << filesAndDirs.at(i) << file.size() << fileHash.toHex();
293     }
294     filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
295     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
296         foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash);
297         if (!foundFileName.isEmpty())
298             break;
299     }
300     return foundFileName;
301 }
302
303 void DocumentChecker::slotEditItem(QTreeWidgetItem *item, int)
304 {
305     int t = item->data(0, typeRole).toInt();
306     if (t == TITLE_FONT_ELEMENT) return;
307     //|| t == TITLE_IMAGE_ELEMENT) {
308
309     KUrl url = KUrlRequesterDialog::getUrl(item->text(1), m_dialog, i18n("Enter new location for file"));
310     if (url.isEmpty()) return;
311     item->setText(1, url.path());
312     if (KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, 0)) {
313         item->setIcon(0, KIcon("dialog-ok"));
314         int id = item->data(0, statusRole).toInt();
315         if (id < 10) item->setData(0, statusRole, CLIPOK);
316         else item->setData(0, statusRole, LUMAOK);
317         checkStatus();
318     } else {
319         item->setIcon(0, KIcon("dialog-close"));
320         int id = item->data(0, statusRole).toInt();
321         if (id < 10) item->setData(0, statusRole, CLIPMISSING);
322         else item->setData(0, statusRole, LUMAMISSING);
323         checkStatus();
324     }
325 }
326
327
328 void DocumentChecker::acceptDialog()
329 {
330     QDomElement e, property;
331     QDomNodeList producers = m_doc.elementsByTagName("producer");
332     QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
333     QDomNodeList properties;
334     int ix = 0;
335
336     // prepare transitions
337     QDomNodeList trans = m_doc.elementsByTagName("transition");
338
339     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
340     while (child) {
341         int t = child->data(0, typeRole).toInt();
342         if (child->data(0, statusRole).toInt() == CLIPOK) {
343             QString id = child->data(0, idRole).toString();
344             if (t == TITLE_IMAGE_ELEMENT) {
345                 // edit images embedded in titles
346                 for (int i = 0; i < infoproducers.count(); i++) {
347                     e = infoproducers.item(i).toElement();
348                     if (e.attribute("id") == id) {
349                         // Fix clip
350                         QString xml = e.attribute("xmldata");
351                         xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
352                         e.setAttribute("xmldata", xml);
353                         break;
354                     }
355                 }
356                 for (int i = 0; i < producers.count(); i++) {
357                     e = producers.item(i).toElement();
358                     if (e.attribute("id").section('_', 0, 0) == id) {
359                         // Fix clip
360                         properties = e.childNodes();
361                         for (int j = 0; j < properties.count(); ++j) {
362                             property = properties.item(j).toElement();
363                             if (property.attribute("name") == "xmldata") {
364                                 QString xml = property.firstChild().nodeValue();
365                                 xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
366                                 property.firstChild().setNodeValue(xml);
367                                 break;
368                             }
369                         }
370                     }
371                 }
372             } else {
373                 // edit clip url
374                 for (int i = 0; i < infoproducers.count(); i++) {
375                     e = infoproducers.item(i).toElement();
376                     if (e.attribute("id") == id) {
377                         // Fix clip
378                         e.setAttribute("resource", child->text(1));
379                         e.setAttribute("name", KUrl(child->text(1)).fileName());
380                         break;
381                     }
382                 }
383                 for (int i = 0; i < producers.count(); i++) {
384                     e = producers.item(i).toElement();
385                     if (e.attribute("id").section('_', 0, 0) == id) {
386                         // Fix clip
387                         properties = e.childNodes();
388                         for (int j = 0; j < properties.count(); ++j) {
389                             property = properties.item(j).toElement();
390                             if (property.attribute("name") == "resource") {
391                                 property.firstChild().setNodeValue(child->text(1));
392                                 break;
393                             }
394                         }
395                     }
396                 }
397             }
398         } else if (child->data(0, statusRole).toInt() == CLIPPLACEHOLDER && t != TITLE_FONT_ELEMENT && t != TITLE_IMAGE_ELEMENT) {
399             QString id = child->data(0, idRole).toString();
400             for (int i = 0; i < infoproducers.count(); i++) {
401                 e = infoproducers.item(i).toElement();
402                 if (e.attribute("id") == id) {
403                     // Fix clip
404                     e.setAttribute("placeholder", '1');
405                     break;
406                 }
407             }
408         } else if (child->data(0, statusRole).toInt() == LUMAOK) {
409             for (int i = 0; i < trans.count(); i++) {
410                 QString luma = getProperty(trans.at(i).toElement(), "luma");
411                 kDebug() << "luma: " << luma;
412                 if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
413                     setProperty(trans.at(i).toElement(), "luma", child->text(1));
414                     kDebug() << "replace with; " << child->text(1);
415                 }
416             }
417         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
418             for (int i = 0; i < trans.count(); i++) {
419                 QString luma = getProperty(trans.at(i).toElement(), "luma");
420                 if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
421                     setProperty(trans.at(i).toElement(), "luma", QString());
422                 }
423             }
424         }
425         ix++;
426         child = m_ui.treeWidget->topLevelItem(ix);
427     }
428     //QDialog::accept();
429 }
430
431 void DocumentChecker::slotPlaceholders()
432 {
433     int ix = 0;
434     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
435     while (child) {
436         if (child->data(0, statusRole).toInt() == CLIPMISSING) {
437             child->setData(0, statusRole, CLIPPLACEHOLDER);
438             child->setIcon(0, KIcon("dialog-ok"));
439         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
440             child->setData(0, statusRole, LUMAPLACEHOLDER);
441             child->setIcon(0, KIcon("dialog-ok"));
442         }
443         ix++;
444         child = m_ui.treeWidget->topLevelItem(ix);
445     }
446     checkStatus();
447 }
448
449
450 void DocumentChecker::checkStatus()
451 {
452     bool status = true;
453     int ix = 0;
454     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
455     while (child) {
456         if (child->data(0, statusRole).toInt() == CLIPMISSING || child->data(0, statusRole).toInt() == LUMAMISSING) {
457             status = false;
458             break;
459         }
460         ix++;
461         child = m_ui.treeWidget->topLevelItem(ix);
462     }
463     m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status);
464 }
465
466
467 void DocumentChecker::slotDeleteSelected()
468 {
469     if (KMessageBox::warningContinueCancel(m_dialog, i18np("This will remove the selected clip from this project", "This will remove the selected clips from this project", m_ui.treeWidget->selectedItems().count()), i18n("Remove clips")) == KMessageBox::Cancel) return;
470     int ix = 0;
471     QStringList deletedIds;
472     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
473     QDomNodeList playlists = m_doc.elementsByTagName("playlist");
474
475     while (child) {
476         int id = child->data(0, statusRole).toInt();
477         if (child->isSelected() && id < 10) {
478             QString id = child->data(0, idRole).toString();
479             deletedIds.append(id);
480             for (int j = 0; j < playlists.count(); j++)
481                 deletedIds.append(id + '_' + QString::number(j));
482             delete child;
483         } else ix++;
484         child = m_ui.treeWidget->topLevelItem(ix);
485     }
486     kDebug() << "// Clips to delete: " << deletedIds;
487
488     if (!deletedIds.isEmpty()) {
489         QDomElement e;
490         QDomNodeList producers = m_doc.elementsByTagName("producer");
491         QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
492
493         QDomElement mlt = m_doc.firstChildElement("mlt");
494         QDomElement kdenlivedoc = mlt.firstChildElement("kdenlivedoc");
495
496         for (int i = 0; i < infoproducers.count(); i++) {
497             e = infoproducers.item(i).toElement();
498             if (deletedIds.contains(e.attribute("id"))) {
499                 // Remove clip
500                 kdenlivedoc.removeChild(e);
501                 break;
502             }
503         }
504
505         for (int i = 0; i < producers.count(); i++) {
506             e = producers.item(i).toElement();
507             if (deletedIds.contains(e.attribute("id"))) {
508                 // Remove clip
509                 mlt.removeChild(e);
510                 break;
511             }
512         }
513
514         for (int i = 0; i < playlists.count(); i++) {
515             QDomNodeList entries = playlists.at(i).toElement().elementsByTagName("entry");
516             for (int j = 0; j < playlists.count(); j++) {
517                 e = entries.item(j).toElement();
518                 if (deletedIds.contains(e.attribute("producer"))) {
519                     // Replace clip with blank
520                     e.setTagName("blank");
521                     e.removeAttribute("producer");
522                     int length = e.attribute("out").toInt() - e.attribute("in").toInt();
523                     e.setAttribute("length", length);
524                 }
525             }
526         }
527         checkStatus();
528     }
529 }
530
531 void DocumentChecker::checkMissingImages(QList <QDomElement>&missingClips, QStringList images, QStringList fonts, QString id, QString baseClip)
532 {
533     QDomDocument doc;
534     foreach(const QString &img, images) {
535         if (!KIO::NetAccess::exists(KUrl(img), KIO::NetAccess::SourceSide, 0)) {
536             QDomElement e = doc.createElement("missingclip");
537             e.setAttribute("type", TITLE_IMAGE_ELEMENT);
538             e.setAttribute("resource", img);
539             e.setAttribute("id", id);
540             e.setAttribute("name", baseClip);
541             missingClips.append(e);
542         }
543     }
544     kDebug() << "/ / / CHK FONTS: " << fonts;
545     foreach(const QString &fontelement, fonts) {
546         QFont f(fontelement);
547         kDebug() << "/ / / CHK FONTS: " << fontelement << " = " << QFontInfo(f).family();
548         if (fontelement != QFontInfo(f).family()) {
549             QDomElement e = doc.createElement("missingclip");
550             e.setAttribute("type", TITLE_FONT_ELEMENT);
551             e.setAttribute("resource", fontelement);
552             e.setAttribute("id", id);
553             e.setAttribute("name", baseClip);
554             missingClips.append(e);
555         }
556     }
557 }
558
559
560 void DocumentChecker::slotCheckButtons()
561 {
562     if (m_ui.treeWidget->currentItem()) {
563         QTreeWidgetItem *item = m_ui.treeWidget->currentItem();
564         int t = item->data(0, typeRole).toInt();
565         if (t == TITLE_FONT_ELEMENT || t == TITLE_IMAGE_ELEMENT) {
566             m_ui.removeSelected->setEnabled(false);
567         } else m_ui.removeSelected->setEnabled(true);
568     }
569
570 }
571
572 #include "documentchecker.moc"
573
574