1 /***************************************************************************
2 * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) *
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. *
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. *
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 ***************************************************************************/
21 #include "documentvalidator.h"
22 #include "definitions.h"
23 #include "initeffects.h"
24 #include "mainwindow.h"
27 #include <KMessageBox>
28 #include <KApplication>
30 #include <KStandardDirs>
36 #include <QScriptEngine>
38 #include <mlt++/Mlt.h>
43 DocumentValidator::DocumentValidator(QDomDocument doc, KUrl documentUrl):
49 bool DocumentValidator::validate(const double currentVersion)
51 QDomElement mlt = m_doc.firstChildElement("mlt");
52 // At least the root element must be there
56 QDomElement kdenliveDoc = mlt.firstChildElement("kdenlivedoc");
57 // Check if we're validating a Kdenlive project
58 if (kdenliveDoc.isNull())
61 QString rootDir = mlt.attribute("root");
62 if (rootDir == "$CURRENTPATH") {
63 // The document was extracted from a Kdenlive archived project, fix root directory$
64 QString playlist = m_doc.toString();
65 playlist.replace("$CURRENTPATH", m_url.directory(KUrl::IgnoreTrailingSlash));
66 m_doc.setContent(playlist);
67 mlt = m_doc.firstChildElement("mlt");
68 kdenliveDoc = mlt.firstChildElement("kdenlivedoc");
71 // Previous MLT / Kdenlive versions used C locale by default
72 QLocale documentLocale = QLocale::c();
74 if (mlt.hasAttribute("LC_NUMERIC")) {
75 // Set locale for the document
76 QString newLocale = setlocale(LC_NUMERIC, mlt.attribute("LC_NUMERIC").toUtf8().constData());
77 documentLocale = QLocale(mlt.attribute("LC_NUMERIC"));
79 // Make sure Qt locale and C++ locale have the same numeric separator, might not be the case
80 // With some locales since C++ and Qt use a different database for locales
81 char *separator = localeconv()->decimal_point;
82 if (separator != documentLocale.decimalPoint()) {
83 if (newLocale.isEmpty()) {
84 // Requested locale not available, ask for install
85 KMessageBox::sorry(kapp->activeWindow(), i18n("The document was created in \"%1\" locale, which is not installed on your system. Please install that language pack. Until then, Kdenlive might not be able to correctly open the document.", mlt.attribute("LC_NUMERIC")));
88 else KMessageBox::sorry(kapp->activeWindow(), i18n("There is a locale conflict on your system. The document uses locale %1 which uses a \"%2\" as numeric separator (in system libraries) but Qt expects \"%3\". You might not be able to correctly open the project.", mlt.attribute("LC_NUMERIC"), separator, documentLocale.decimalPoint()));
89 kDebug()<<"------\n!!! system locale is not similar to Qt's locale... be prepared for bugs!!!\n------";
90 // HACK: There is a locale conflict, so set locale to at least have correct decimal point
91 if (strncmp(separator, ".", 1) == 0) documentLocale = QLocale::c();
92 else if (strncmp(separator, ",", 1) == 0) documentLocale = QLocale("fr_FR.UTF-8");
96 documentLocale.setNumberOptions(QLocale::OmitGroupSeparator);
97 if (documentLocale.decimalPoint() != QLocale().decimalPoint()) {
98 // If loading an older MLT file without LC_NUMERIC, set locale to C which was previously the default
99 if (!mlt.hasAttribute("LC_NUMERIC")) setlocale(LC_NUMERIC, "C");
101 QLocale::setDefault(documentLocale);
102 // locale conversion might need to be redone
103 initEffects::parseEffectFiles();
106 // Upgrade the document to the latest version
107 if (!upgrade(documentLocale.toDouble(kdenliveDoc.attribute("version")), currentVersion))
111 * Check the syntax (this will be replaced by XSD validation with Qt 4.6)
112 * and correct some errors
115 // Return (or create) the tractor
116 QDomElement tractor = mlt.firstChildElement("tractor");
117 if (tractor.isNull()) {
119 tractor = m_doc.createElement("tractor");
120 tractor.setAttribute("global_feed", "1");
121 tractor.setAttribute("in", "0");
122 tractor.setAttribute("out", "-1");
123 tractor.setAttribute("id", "maintractor");
124 mlt.appendChild(tractor);
128 * Make sure at least one track exists, and they're equal in number to
129 * to the maximum between MLT and Kdenlive playlists and tracks
131 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
132 int tracksMax = playlists.count() - 1; // Remove the black track
133 QDomNodeList tracks = tractor.elementsByTagName("track");
134 tracksMax = qMax(tracks.count() - 1, tracksMax);
135 QDomNodeList tracksinfo = kdenliveDoc.elementsByTagName("trackinfo");
136 tracksMax = qMax(tracksinfo.count(), tracksMax);
137 tracksMax = qMax(1, tracksMax); // Force existance of one track
138 if (playlists.count() - 1 < tracksMax ||
139 tracks.count() - 1 < tracksMax ||
140 tracksinfo.count() < tracksMax) {
141 kDebug() << "//// WARNING, PROJECT IS CORRUPTED, MISSING TRACK";
144 // use the MLT tracks as reference
145 if (tracks.count() - 1 < tracksMax) {
146 // Looks like one MLT track is missing, remove the extra Kdenlive track if there is one.
147 if (tracksinfo.count() != tracks.count() - 1) {
148 // The Kdenlive tracks are not ok, clear and rebuild them
149 QDomNode tinfo = kdenliveDoc.firstChildElement("tracksinfo");
150 QDomNode tnode = tinfo.firstChild();
151 while (!tnode.isNull()) {
152 tinfo.removeChild(tnode);
153 tnode = tinfo.firstChild();
156 for (int i = 1; i < tracks.count(); i++) {
157 QString hide = tracks.at(i).toElement().attribute("hide");
158 QDomElement newTrack = m_doc.createElement("trackinfo");
159 if (hide == "video") {
161 newTrack.setAttribute("type", "audio");
162 newTrack.setAttribute("blind", 1);
163 newTrack.setAttribute("mute", 0);
164 newTrack.setAttribute("lock", 0);
166 newTrack.setAttribute("blind", 0);
167 newTrack.setAttribute("mute", 0);
168 newTrack.setAttribute("lock", 0);
170 tinfo.appendChild(newTrack);
175 if (playlists.count() - 1 < tracksMax) {
176 difference = tracksMax - (playlists.count() - 1);
177 for (int i = 0; i < difference; ++i) {
178 QDomElement playlist = m_doc.createElement("playlist");
179 mlt.appendChild(playlist);
182 if (tracks.count() - 1 < tracksMax) {
183 difference = tracksMax - (tracks.count() - 1);
184 for (int i = 0; i < difference; ++i) {
185 QDomElement track = m_doc.createElement("track");
186 tractor.appendChild(track);
189 if (tracksinfo.count() < tracksMax) {
190 QDomElement tracksinfoElm = kdenliveDoc.firstChildElement("tracksinfo");
191 if (tracksinfoElm.isNull()) {
192 tracksinfoElm = m_doc.createElement("tracksinfo");
193 kdenliveDoc.appendChild(tracksinfoElm);
195 difference = tracksMax - tracksinfo.count();
196 for (int i = 0; i < difference; ++i) {
197 QDomElement trackinfo = m_doc.createElement("trackinfo");
198 trackinfo.setAttribute("mute", "0");
199 trackinfo.setAttribute("locked", "0");
200 tracksinfoElm.appendChild(trackinfo);
205 // TODO: check the tracks references
206 // TODO: check internal mix transitions
215 bool DocumentValidator::upgrade(double version, const double currentVersion)
217 kDebug() << "Opening a document with version " << version;
219 // No conversion needed
220 if (version == currentVersion) {
224 // The document is too new
225 if (version > currentVersion) {
226 kDebug() << "Unable to open document with version " << version;
227 KMessageBox::sorry(kapp->activeWindow(), i18n("This project type is unsupported (version %1) and can't be loaded.\nPlease consider upgrading your Kdenlive version.", version), i18n("Unable to open project"));
231 // Unsupported document versions
232 if (version == 0.5 || version == 0.7) {
233 kDebug() << "Unable to open document with version " << version;
234 KMessageBox::sorry(kapp->activeWindow(), i18n("This project type is unsupported (version %1) and can't be loaded.", version), i18n("Unable to open project"));
239 QDomNode infoXmlNode = m_doc.elementsByTagName("kdenlivedoc").at(0);
240 QDomElement infoXml = infoXmlNode.toElement();
241 infoXml.setAttribute("upgraded", "1");
243 if (version <= 0.6) {
244 QDomElement infoXml_old = infoXmlNode.cloneNode(true).toElement(); // Needed for folders
245 QDomNode westley = m_doc.elementsByTagName("westley").at(1);
246 QDomNode tractor = m_doc.elementsByTagName("tractor").at(0);
247 QDomNode multitrack = m_doc.elementsByTagName("multitrack").at(0);
248 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
250 QDomNode props = m_doc.elementsByTagName("properties").at(0).toElement();
251 QString profile = props.toElement().attribute("videoprofile");
252 int startPos = props.toElement().attribute("timeline_position").toInt();
253 if (profile == "dv_wide")
254 profile = "dv_pal_wide";
256 // move playlists outside of tractor and add the tracks instead
257 int max = playlists.count();
258 if (westley.isNull()) {
259 westley = m_doc.createElement("westley");
260 m_doc.documentElement().appendChild(westley);
262 if (tractor.isNull()) {
263 kDebug() << "// NO MLT PLAYLIST, building empty one";
264 QDomElement blank_tractor = m_doc.createElement("tractor");
265 westley.appendChild(blank_tractor);
266 QDomElement blank_playlist = m_doc.createElement("playlist");
267 blank_playlist.setAttribute("id", "black_track");
268 westley.insertBefore(blank_playlist, QDomNode());
269 QDomElement blank_track = m_doc.createElement("track");
270 blank_track.setAttribute("producer", "black_track");
271 blank_tractor.appendChild(blank_track);
273 QDomNodeList kdenlivetracks = m_doc.elementsByTagName("kdenlivetrack");
274 for (int i = 0; i < kdenlivetracks.count(); i++) {
275 blank_playlist = m_doc.createElement("playlist");
276 blank_playlist.setAttribute("id", "playlist" + QString::number(i));
277 westley.insertBefore(blank_playlist, QDomNode());
278 blank_track = m_doc.createElement("track");
279 blank_track.setAttribute("producer", "playlist" + QString::number(i));
280 blank_tractor.appendChild(blank_track);
281 if (kdenlivetracks.at(i).toElement().attribute("cliptype") == "Sound") {
282 blank_playlist.setAttribute("hide", "video");
283 blank_track.setAttribute("hide", "video");
286 } else for (int i = 0; i < max; i++) {
287 QDomNode n = playlists.at(i);
288 westley.insertBefore(n, QDomNode());
289 QDomElement pl = n.toElement();
290 QDomElement track = m_doc.createElement("track");
291 QString trackType = pl.attribute("hide");
292 if (!trackType.isEmpty())
293 track.setAttribute("hide", trackType);
294 QString playlist_id = pl.attribute("id");
295 if (playlist_id.isEmpty()) {
296 playlist_id = "black_track";
297 pl.setAttribute("id", playlist_id);
299 track.setAttribute("producer", playlist_id);
300 //tractor.appendChild(track);
301 #define KEEP_TRACK_ORDER 1
302 #ifdef KEEP_TRACK_ORDER
303 tractor.insertAfter(track, QDomNode());
305 // Insert the new track in an order that hopefully matches the 3 video, then 2 audio tracks of Kdenlive 0.7.0
306 // insertion sort - O( tracks*tracks )
307 // Note, this breaks _all_ transitions - but you can move them up and down afterwards.
308 QDomElement tractor_elem = tractor.toElement();
309 if (! tractor_elem.isNull()) {
310 QDomNodeList tracks = tractor_elem.elementsByTagName("track");
311 int size = tracks.size();
313 tractor.insertAfter(track, QDomNode());
315 bool inserted = false;
316 for (int i = 0; i < size; ++i) {
317 QDomElement track_elem = tracks.at(i).toElement();
318 if (track_elem.isNull()) {
319 tractor.insertAfter(track, QDomNode());
323 kDebug() << "playlist_id: " << playlist_id << " producer:" << track_elem.attribute("producer");
324 if (playlist_id < track_elem.attribute("producer")) {
325 tractor.insertBefore(track, track_elem);
331 // Reach here, no insertion, insert last
333 tractor.insertAfter(track, QDomNode());
337 kWarning() << "tractor was not a QDomElement";
338 tractor.insertAfter(track, QDomNode());
342 tractor.removeChild(multitrack);
344 // audio track mixing transitions should not be added to track view, so add required attribute
345 QDomNodeList transitions = m_doc.elementsByTagName("transition");
346 max = transitions.count();
347 for (int i = 0; i < max; i++) {
348 QDomElement tr = transitions.at(i).toElement();
349 if (tr.attribute("combine") == "1" && tr.attribute("mlt_service") == "mix") {
350 QDomElement property = m_doc.createElement("property");
351 property.setAttribute("name", "internal_added");
352 QDomText value = m_doc.createTextNode("237");
353 property.appendChild(value);
354 tr.appendChild(property);
355 property = m_doc.createElement("property");
356 property.setAttribute("name", "mlt_service");
357 value = m_doc.createTextNode("mix");
358 property.appendChild(value);
359 tr.appendChild(property);
361 // convert transition
362 QDomNamedNodeMap attrs = tr.attributes();
363 for (int j = 0; j < attrs.count(); j++) {
364 QString attrName = attrs.item(j).nodeName();
365 if (attrName != "in" && attrName != "out" && attrName != "id") {
366 QDomElement property = m_doc.createElement("property");
367 property.setAttribute("name", attrName);
368 QDomText value = m_doc.createTextNode(attrs.item(j).nodeValue());
369 property.appendChild(value);
370 tr.appendChild(property);
376 // move transitions after tracks
377 for (int i = 0; i < max; i++) {
378 tractor.insertAfter(transitions.at(0), QDomNode());
381 // Fix filters format
382 QDomNodeList entries = m_doc.elementsByTagName("entry");
383 max = entries.count();
384 for (int i = 0; i < max; i++) {
387 QDomNode m = entries.at(i).firstChild();
388 while (!m.isNull()) {
389 if (m.toElement().tagName() == "filter") {
390 QDomElement filt = m.toElement();
391 QDomNamedNodeMap attrs = filt.attributes();
392 QString current_id = filt.attribute("kdenlive_id");
393 if (current_id != last_id) {
395 last_id = current_id;
397 QDomElement e = m_doc.createElement("property");
398 e.setAttribute("name", "kdenlive_ix");
399 QDomText value = m_doc.createTextNode(QString::number(effectix));
400 e.appendChild(value);
402 for (int j = 0; j < attrs.count(); j++) {
403 QDomAttr a = attrs.item(j).toAttr();
405 kDebug() << " FILTER; adding :" << a.name() << ":" << a.value();
406 QDomElement e = m_doc.createElement("property");
407 e.setAttribute("name", a.name());
408 QDomText value = m_doc.createTextNode(a.value());
409 e.appendChild(value);
420 QDomNodeList filters = m_doc.elementsByTagName("filter");
421 max = filters.count();
424 for (int i = 0; i < max; i++) {
425 QDomElement filt = filters.at(i).toElement();
426 QDomNamedNodeMap attrs = filt.attributes();
427 QString current_id = filt.attribute("kdenlive_id");
428 if (current_id != last_id) {
430 last_id = current_id;
432 QDomElement e = m_doc.createElement("property");
433 e.setAttribute("name", "kdenlive_ix");
434 QDomText value = m_doc.createTextNode(QString::number(1));
435 e.appendChild(value);
437 for (int j = 0; j < attrs.count(); j++) {
438 QDomAttr a = attrs.item(j).toAttr();
440 kDebug() << " FILTER; adding :" << a.name() << ":" << a.value();
441 QDomElement e = m_doc.createElement("property");
442 e.setAttribute("name", a.name());
443 QDomText value = m_doc.createTextNode(a.value());
444 e.appendChild(value);
451 QDomNodeList producers = westley.toElement().elementsByTagName("producer");
452 max = producers.count();
453 for (int i = 0; i < max; i++) {
454 QDomElement prod = producers.at(i).toElement();
455 if (prod.attribute("mlt_service") == "framebuffer") {
456 QString slowmotionprod = prod.attribute("resource");
457 slowmotionprod.replace(':', '?');
458 kDebug() << "// FOUND WRONG SLOWMO, new: " << slowmotionprod;
459 prod.setAttribute("resource", slowmotionprod);
462 // move producers to correct place, markers to a global list, fix clip descriptions
463 QDomElement markers = m_doc.createElement("markers");
464 // This will get the xml producers:
465 producers = m_doc.elementsByTagName("producer");
466 max = producers.count();
467 for (int i = 0; i < max; i++) {
468 QDomElement prod = producers.at(0).toElement();
469 // add resource also as a property (to allow path correction in setNewResource())
470 // TODO: will it work with slowmotion? needs testing
471 /*if (!prod.attribute("resource").isEmpty()) {
472 QDomElement prop_resource = m_doc.createElement("property");
473 prop_resource.setAttribute("name", "resource");
474 QDomText resource = m_doc.createTextNode(prod.attribute("resource"));
475 prop_resource.appendChild(resource);
476 prod.appendChild(prop_resource);
478 QDomNode m = prod.firstChild();
480 if (m.toElement().tagName() == "markers") {
481 QDomNodeList prodchilds = m.childNodes();
482 int maxchild = prodchilds.count();
483 for (int k = 0; k < maxchild; k++) {
484 QDomElement mark = prodchilds.at(0).toElement();
485 mark.setAttribute("id", prod.attribute("id"));
486 markers.insertAfter(mark, QDomNode());
489 } else if (prod.attribute("type").toInt() == TEXT) {
490 // convert title clip
491 if (m.toElement().tagName() == "textclip") {
493 QDomElement titleclip = m.toElement();
494 QDomElement title = tdoc.createElement("kdenlivetitle");
495 tdoc.appendChild(title);
496 QDomNodeList objects = titleclip.childNodes();
497 int maxchild = objects.count();
498 for (int k = 0; k < maxchild; k++) {
500 QDomElement ob = objects.at(k).toElement();
501 if (ob.attribute("type") == "3") {
502 // text object - all of this goes into "xmldata"...
503 QDomElement item = tdoc.createElement("item");
504 item.setAttribute("z-index", ob.attribute("z"));
505 item.setAttribute("type", "QGraphicsTextItem");
506 QDomElement position = tdoc.createElement("position");
507 position.setAttribute("x", ob.attribute("x"));
508 position.setAttribute("y", ob.attribute("y"));
509 QDomElement content = tdoc.createElement("content");
510 content.setAttribute("font", ob.attribute("font_family"));
511 content.setAttribute("font-size", ob.attribute("font_size"));
512 content.setAttribute("font-bold", ob.attribute("bold"));
513 content.setAttribute("font-italic", ob.attribute("italic"));
514 content.setAttribute("font-underline", ob.attribute("underline"));
515 QString col = ob.attribute("color");
517 content.setAttribute("font-color", colorToString(c));
518 // todo: These fields are missing from the newly generated xmldata:
519 // transform, startviewport, endviewport, background
521 QDomText conttxt = tdoc.createTextNode(ob.attribute("text"));
522 content.appendChild(conttxt);
523 item.appendChild(position);
524 item.appendChild(content);
525 title.appendChild(item);
526 } else if (ob.attribute("type") == "5") {
528 QDomElement item = tdoc.createElement("item");
529 item.setAttribute("z-index", ob.attribute("z"));
530 item.setAttribute("type", "QGraphicsRectItem");
531 QDomElement position = tdoc.createElement("position");
532 position.setAttribute("x", ob.attribute("x"));
533 position.setAttribute("y", ob.attribute("y"));
534 QDomElement content = tdoc.createElement("content");
535 QString col = ob.attribute("color");
537 content.setAttribute("brushcolor", colorToString(c));
538 QString rect = "0,0,";
539 rect.append(ob.attribute("width"));
541 rect.append(ob.attribute("height"));
542 content.setAttribute("rect", rect);
543 item.appendChild(position);
544 item.appendChild(content);
545 title.appendChild(item);
548 prod.setAttribute("xmldata", tdoc.toString());
549 // mbd todo: This clearly does not work, as every title gets the same name - trying to leave it empty
550 // QStringList titleInfo = TitleWidget::getFreeTitleInfo(projectFolder());
551 // prod.setAttribute("titlename", titleInfo.at(0));
552 // prod.setAttribute("resource", titleInfo.at(1));
553 //kDebug()<<"TITLE DATA:\n"<<tdoc.toString();
555 } // End conversion of title clips.
557 } else if (m.isText()) {
558 QString comment = m.nodeValue();
559 if (!comment.isEmpty()) {
560 prod.setAttribute("description", comment);
565 int duration = prod.attribute("duration").toInt();
566 if (duration > 0) prod.setAttribute("out", QString::number(duration));
567 // The clip goes back in, but text clips should not go back in, at least not modified
568 westley.insertBefore(prod, QDomNode());
571 QDomNode westley0 = m_doc.elementsByTagName("westley").at(0);
572 if (!markers.firstChild().isNull()) westley0.appendChild(markers);
575 * Convert as much of the kdenlivedoc as possible. Use the producer in
576 * westley. First, remove the old stuff from westley, and add a new
577 * empty one. Also, track the max id in order to use it for the adding
580 int max_kproducer_id = 0;
581 westley0.removeChild(infoXmlNode);
582 QDomElement infoXml_new = m_doc.createElement("kdenlivedoc");
583 infoXml_new.setAttribute("profile", profile);
584 infoXml.setAttribute("position", startPos);
586 // Add all the producers that has a resource in westley
587 QDomElement westley_element = westley0.toElement();
588 if (westley_element.isNull()) {
589 kWarning() << "westley0 element in document was not a QDomElement - unable to add producers to new kdenlivedoc";
591 QDomNodeList wproducers = westley_element.elementsByTagName("producer");
592 int kmax = wproducers.count();
593 for (int i = 0; i < kmax; i++) {
594 QDomElement wproducer = wproducers.at(i).toElement();
595 if (wproducer.isNull()) {
596 kWarning() << "Found producer in westley0, that was not a QDomElement";
599 if (wproducer.attribute("id") == "black") continue;
600 // We have to do slightly different things, depending on the type
601 kDebug() << "Converting producer element with type" << wproducer.attribute("type");
602 if (wproducer.attribute("type").toInt() == TEXT) {
603 kDebug() << "Found TEXT element in producer" << endl;
604 QDomElement kproducer = wproducer.cloneNode(true).toElement();
605 kproducer.setTagName("kdenlive_producer");
606 infoXml_new.appendChild(kproducer);
608 * TODO: Perhaps needs some more changes here to
609 * "frequency", aspect ratio as a float, frame_size,
610 * channels, and later, resource and title name
613 QDomElement kproducer = m_doc.createElement("kdenlive_producer");
614 kproducer.setAttribute("id", wproducer.attribute("id"));
615 if (!wproducer.attribute("description").isEmpty())
616 kproducer.setAttribute("description", wproducer.attribute("description"));
617 kproducer.setAttribute("resource", wproducer.attribute("resource"));
618 kproducer.setAttribute("type", wproducer.attribute("type"));
619 // Testing fix for 358
620 if (!wproducer.attribute("aspect_ratio").isEmpty()) {
621 kproducer.setAttribute("aspect_ratio", wproducer.attribute("aspect_ratio"));
623 if (!wproducer.attribute("source_fps").isEmpty()) {
624 kproducer.setAttribute("fps", wproducer.attribute("source_fps"));
626 if (!wproducer.attribute("length").isEmpty()) {
627 kproducer.setAttribute("duration", wproducer.attribute("length"));
629 infoXml_new.appendChild(kproducer);
631 if (wproducer.attribute("id").toInt() > max_kproducer_id) {
632 max_kproducer_id = wproducer.attribute("id").toInt();
636 #define LOOKUP_FOLDER 1
639 * Look through all the folder elements of the old doc, for each folder,
640 * for each producer, get the id, look it up in the new doc, set the
641 * groupname and groupid. Note, this does not work at the moment - at
642 * least one folder shows up missing, and clips with no folder does not
645 //QDomElement infoXml_old = infoXmlNode.toElement();
646 if (!infoXml_old.isNull()) {
647 QDomNodeList folders = infoXml_old.elementsByTagName("folder");
648 int fsize = folders.size();
649 int groupId = max_kproducer_id + 1; // Start at +1 of max id of the kdenlive_producers
650 for (int i = 0; i < fsize; ++i) {
651 QDomElement folder = folders.at(i).toElement();
652 if (!folder.isNull()) {
653 QString groupName = folder.attribute("name");
654 kDebug() << "groupName: " << groupName << " with groupId: " << groupId;
655 QDomNodeList fproducers = folder.elementsByTagName("producer");
656 int psize = fproducers.size();
657 for (int j = 0; j < psize; ++j) {
658 QDomElement fproducer = fproducers.at(j).toElement();
659 if (!fproducer.isNull()) {
660 QString id = fproducer.attribute("id");
661 // This is not very effective, but compared to loading the clips, its a breeze
662 QDomNodeList kdenlive_producers = infoXml_new.elementsByTagName("kdenlive_producer");
663 int kpsize = kdenlive_producers.size();
664 for (int k = 0; k < kpsize; ++k) {
665 QDomElement kproducer = kdenlive_producers.at(k).toElement(); // Its an element for sure
666 if (id == kproducer.attribute("id")) {
667 // We do not check that it already is part of a folder
668 kproducer.setAttribute("groupid", groupId);
669 kproducer.setAttribute("groupname", groupName);
680 QDomNodeList elements = westley.childNodes();
681 max = elements.count();
682 for (int i = 0; i < max; i++) {
683 QDomElement prod = elements.at(0).toElement();
684 westley0.insertAfter(prod, QDomNode());
687 westley0.appendChild(infoXml_new);
689 westley0.removeChild(westley);
691 // adds <avfile /> information to <kdenlive_producer />
692 QDomNodeList kproducers = m_doc.elementsByTagName("kdenlive_producer");
693 QDomNodeList avfiles = infoXml_old.elementsByTagName("avfile");
694 kDebug() << "found" << avfiles.count() << "<avfile />s and" << kproducers.count() << "<kdenlive_producer />s";
695 for (int i = 0; i < avfiles.count(); ++i) {
696 QDomElement avfile = avfiles.at(i).toElement();
697 QDomElement kproducer;
699 kWarning() << "found an <avfile /> that is not a QDomElement";
701 QString id = avfile.attribute("id");
702 // this is horrible, must be rewritten, it's just for test
703 for (int j = 0; j < kproducers.count(); ++j) {
704 //kDebug() << "checking <kdenlive_producer /> with id" << kproducers.at(j).toElement().attribute("id");
705 if (kproducers.at(j).toElement().attribute("id") == id) {
706 kproducer = kproducers.at(j).toElement();
710 if (kproducer == QDomElement())
711 kWarning() << "no match for <avfile /> with id =" << id;
713 //kDebug() << "ready to set additional <avfile />'s attributes (id =" << id << ")";
714 kproducer.setAttribute("channels", avfile.attribute("channels"));
715 kproducer.setAttribute("duration", avfile.attribute("duration"));
716 kproducer.setAttribute("frame_size", avfile.attribute("width") + 'x' + avfile.attribute("height"));
717 kproducer.setAttribute("frequency", avfile.attribute("frequency"));
718 if (kproducer.attribute("description").isEmpty() && !avfile.attribute("description").isEmpty())
719 kproducer.setAttribute("description", avfile.attribute("description"));
723 infoXml = infoXml_new;
726 if (version <= 0.81) {
727 // Add the tracks information
728 QString tracksOrder = infoXml.attribute("tracks");
729 if (tracksOrder.isEmpty()) {
730 QDomNodeList tracks = m_doc.elementsByTagName("track");
731 for (int i = 0; i < tracks.count(); i++) {
732 QDomElement track = tracks.at(i).toElement();
733 if (track.attribute("producer") != "black_track") {
734 if (track.attribute("hide") == "video")
735 tracksOrder.append('a');
737 tracksOrder.append('v');
741 QDomElement tracksinfo = m_doc.createElement("tracksinfo");
742 for (int i = 0; i < tracksOrder.size(); i++) {
743 QDomElement trackinfo = m_doc.createElement("trackinfo");
744 if (tracksOrder.data()[i] == 'a') {
745 trackinfo.setAttribute("type", "audio");
746 trackinfo.setAttribute("blind", true);
748 trackinfo.setAttribute("blind", false);
749 trackinfo.setAttribute("mute", false);
750 trackinfo.setAttribute("locked", false);
751 tracksinfo.appendChild(trackinfo);
753 infoXml.appendChild(tracksinfo);
756 if (version <= 0.82) {
757 // Convert <westley />s in <mlt />s (MLT extreme makeover)
758 QDomNodeList westleyNodes = m_doc.elementsByTagName("westley");
759 for (int i = 0; i < westleyNodes.count(); i++) {
760 QDomElement westley = westleyNodes.at(i).toElement();
761 westley.setTagName("mlt");
765 if (version <= 0.83) {
766 // Replace point size with pixel size in text titles
767 if (m_doc.toString().contains("font-size")) {
768 KMessageBox::ButtonCode convert = KMessageBox::Continue;
769 QDomNodeList kproducerNodes = m_doc.elementsByTagName("kdenlive_producer");
770 for (int i = 0; i < kproducerNodes.count() && convert != KMessageBox::No; ++i) {
771 QDomElement kproducer = kproducerNodes.at(i).toElement();
772 if (kproducer.attribute("type").toInt() == TEXT) {
774 data.setContent(kproducer.attribute("xmldata"));
775 QDomNodeList items = data.firstChild().childNodes();
776 for (int j = 0; j < items.count() && convert != KMessageBox::No; ++j) {
777 if (items.at(j).attributes().namedItem("type").nodeValue() == "QGraphicsTextItem") {
778 QDomNamedNodeMap textProperties = items.at(j).namedItem("content").attributes();
779 if (textProperties.namedItem("font-pixel-size").isNull() && !textProperties.namedItem("font-size").isNull()) {
780 // Ask the user if he wants to convert
781 if (convert != KMessageBox::Yes && convert != KMessageBox::No)
782 convert = (KMessageBox::ButtonCode)KMessageBox::warningYesNo(kapp->activeWindow(), i18n("Some of your text clips were saved with size in points, which means different sizes on different displays. Do you want to convert them to pixel size, making them portable? It is recommended you do this on the computer they were first created on, or you could have to adjust their size."), i18n("Update Text Clips"));
783 if (convert == KMessageBox::Yes) {
785 font.setPointSize(textProperties.namedItem("font-size").nodeValue().toInt());
786 QDomElement content = items.at(j).namedItem("content").toElement();
787 content.setAttribute("font-pixel-size", QFontInfo(font).pixelSize());
788 content.removeAttribute("font-size");
789 kproducer.setAttribute("xmldata", data.toString());
791 * You may be tempted to delete the preview file
792 * to force its recreation: bad idea (see
793 * http://www.kdenlive.org/mantis/view.php?id=749)
803 // Fill the <documentproperties /> element
804 QDomElement docProperties = infoXml.firstChildElement("documentproperties");
805 if (docProperties.isNull()) {
806 docProperties = m_doc.createElement("documentproperties");
807 docProperties.setAttribute("zonein", infoXml.attribute("zonein"));
808 docProperties.setAttribute("zoneout", infoXml.attribute("zoneout"));
809 docProperties.setAttribute("zoom", infoXml.attribute("zoom"));
810 docProperties.setAttribute("position", infoXml.attribute("position"));
811 infoXml.appendChild(docProperties);
815 if (version <= 0.84) {
816 // update the title clips to use the new MLT kdenlivetitle producer
817 QDomNodeList kproducerNodes = m_doc.elementsByTagName("kdenlive_producer");
818 for (int i = 0; i < kproducerNodes.count(); ++i) {
819 QDomElement kproducer = kproducerNodes.at(i).toElement();
820 if (kproducer.attribute("type").toInt() == TEXT) {
821 QString data = kproducer.attribute("xmldata");
822 QString datafile = kproducer.attribute("resource");
823 if (!datafile.endsWith(".kdenlivetitle")) {
824 datafile = QString();
825 kproducer.setAttribute("resource", QString());
827 QString id = kproducer.attribute("id");
828 QDomNodeList mltproducers = m_doc.elementsByTagName("producer");
829 bool foundData = false;
830 bool foundResource = false;
831 bool foundService = false;
832 for (int j = 0; j < mltproducers.count(); j++) {
833 QDomElement wproducer = mltproducers.at(j).toElement();
834 if (wproducer.attribute("id") == id) {
835 QDomNodeList props = wproducer.childNodes();
836 for (int k = 0; k < props.count(); k++) {
837 if (props.at(k).toElement().attribute("name") == "xmldata") {
838 props.at(k).firstChild().setNodeValue(data);
840 } else if (props.at(k).toElement().attribute("name") == "mlt_service") {
841 props.at(k).firstChild().setNodeValue("kdenlivetitle");
843 } else if (props.at(k).toElement().attribute("name") == "resource") {
844 props.at(k).firstChild().setNodeValue(datafile);
845 foundResource = true;
849 QDomElement e = m_doc.createElement("property");
850 e.setAttribute("name", "xmldata");
851 QDomText value = m_doc.createTextNode(data);
852 e.appendChild(value);
853 wproducer.appendChild(e);
856 QDomElement e = m_doc.createElement("property");
857 e.setAttribute("name", "mlt_service");
858 QDomText value = m_doc.createTextNode("kdenlivetitle");
859 e.appendChild(value);
860 wproducer.appendChild(e);
862 if (!foundResource) {
863 QDomElement e = m_doc.createElement("property");
864 e.setAttribute("name", "resource");
865 QDomText value = m_doc.createTextNode(datafile);
866 e.appendChild(value);
867 wproducer.appendChild(e);
875 if (version <= 0.85) {
876 // update the LADSPA effects to use the new ladspa.id format instead of external xml file
877 QDomNodeList effectNodes = m_doc.elementsByTagName("filter");
878 for (int i = 0; i < effectNodes.count(); ++i) {
879 QDomElement effect = effectNodes.at(i).toElement();
880 if (EffectsList::property(effect, "mlt_service") == "ladspa") {
881 // Needs to be converted
882 QStringList info = getInfoFromEffectName(EffectsList::property(effect, "kdenlive_id"));
883 if (info.isEmpty()) continue;
884 // info contains the correct ladspa.id from kdenlive effect name, and a list of parameter's old and new names
885 EffectsList::setProperty(effect, "kdenlive_id", info.at(0));
886 EffectsList::setProperty(effect, "tag", info.at(0));
887 EffectsList::setProperty(effect, "mlt_service", info.at(0));
888 EffectsList::removeProperty(effect, "src");
889 for (int j = 1; j < info.size(); j++) {
890 QString value = EffectsList::property(effect, info.at(j).section('=', 0, 0));
891 if (!value.isEmpty()) {
892 // update parameter name
893 EffectsList::renameProperty(effect, info.at(j).section('=', 0, 0), info.at(j).section('=', 1, 1));
900 if (version <= 0.86) {
901 // Make sure we don't have avformat-novalidate producers, since it caused crashes
902 QDomNodeList producers = m_doc.elementsByTagName("producer");
903 int max = producers.count();
904 for (int i = 0; i < max; i++) {
905 QDomElement prod = producers.at(i).toElement();
906 if (EffectsList::property(prod, "mlt_service") == "avformat-novalidate")
907 EffectsList::setProperty(prod, "mlt_service", "avformat");
910 // There was a mistake in Geometry transitions where the last keyframe was created one frame after the end of transition, so fix it and move last keyframe to real end of transition
912 // Get profile info (width / height)
915 QDomElement profile = m_doc.firstChildElement("profile");
916 if (profile.isNull()) profile = infoXml.firstChildElement("profileinfo");
917 if (profile.isNull()) {
918 // could not find profile info, set PAL
923 profileWidth = profile.attribute("width").toInt();
924 profileHeight = profile.attribute("height").toInt();
926 QDomNodeList transitions = m_doc.elementsByTagName("transition");
927 max = transitions.count();
929 for (int i = 0; i < max; i++) {
930 QDomElement trans = transitions.at(i).toElement();
931 out = trans.attribute("out").toInt() - trans.attribute("in").toInt();
932 QString geom = EffectsList::property(trans, "geometry");
933 Mlt::Geometry *g = new Mlt::Geometry(geom.toUtf8().data(), out, profileWidth, profileHeight);
934 Mlt::GeometryItem item;
935 if (g->next_key(&item, out) == 0) {
936 // We have a keyframe just after last frame, try to move it to last frame
937 if (item.frame() == out + 1) {
941 EffectsList::setProperty(trans, "geometry", g->serialise());
948 if (version <= 0.87) {
949 if (!m_doc.firstChildElement("mlt").hasAttribute("LC_NUMERIC")) {
950 m_doc.firstChildElement("mlt").setAttribute("LC_NUMERIC", "C");
954 // The document has been converted: mark it as modified
955 infoXml.setAttribute("version", currentVersion);
960 QStringList DocumentValidator::getInfoFromEffectName(const QString oldName)
963 // Returns a list to convert old Kdenlive ladspa effects
964 if (oldName == "pitch_shift") {
965 info << "ladspa.1433";
968 else if (oldName == "vinyl") {
969 info << "ladspa.1905";
976 else if (oldName == "room_reverb") {
977 info << "ladspa.1216";
982 else if (oldName == "reverb") {
983 info << "ladspa.1423";
987 else if (oldName == "rate_scale") {
988 info << "ladspa.1417";
991 else if (oldName == "pitch_scale") {
992 info << "ladspa.1193";
995 else if (oldName == "phaser") {
996 info << "ladspa.1217";
999 info << "feedback=2";
1002 else if (oldName == "limiter") {
1003 info << "ladspa.1913";
1006 info << "release=2";
1008 else if (oldName == "equalizer_15") {
1009 info << "ladspa.1197";
1026 else if (oldName == "equalizer") {
1027 info << "ladspa.1901";
1029 info << "midgain=1";
1032 else if (oldName == "declipper") {
1033 info << "ladspa.1195";
1038 QString DocumentValidator::colorToString(const QColor& c)
1040 QString ret = "%1,%2,%3,%4";
1041 ret = ret.arg(c.red()).arg(c.green()).arg(c.blue()).arg(c.alpha());
1045 bool DocumentValidator::isProject() const
1047 QDomNode infoXmlNode = m_doc.elementsByTagName("kdenlivedoc").at(0);
1048 return !infoXmlNode.isNull();
1051 bool DocumentValidator::isModified() const
1056 void DocumentValidator::updateEffects()
1058 // WARNING: order by findDirs will determine which js file to use (in case multiple scripts for the same filter exist)
1059 QMap <QString, KUrl> paths;
1060 #if QT_VERSION >= 0x040700
1061 QMap <QString, QScriptProgram> scripts;
1063 QMap <QString, QString> scripts;
1065 QStringList directories = KGlobal::dirs()->findDirs("appdata", "effects/update");
1066 foreach (const QString &directoryName, directories) {
1067 QDir directory(directoryName);
1068 QStringList fileList = directory.entryList(QStringList() << "*.js", QDir::Files);
1069 foreach (const QString &fileName, fileList) {
1070 QString identifier = fileName;
1071 // remove extension (".js")
1073 paths.insert(identifier, KUrl(directoryName + fileName));
1077 QDomNodeList effects = m_doc.elementsByTagName("filter");
1078 int max = effects.count();
1079 QStringList safeEffects;
1080 for(int i = 0; i < max; ++i) {
1081 QDomElement effect = effects.at(i).toElement();
1082 QString effectId = EffectsList::property(effect, "kdenlive_id");
1083 if (safeEffects.contains(effectId)) {
1084 // Do not check the same effect twice if it is at the correct version
1085 // (assume we don't have different versions of the same effect in a project file)
1088 QString effectTag = EffectsList::property(effect, "tag");
1089 QString effectVersionStr = EffectsList::property(effect, "version");
1090 double effectVersion = effectVersionStr.isNull() ? -1 : effectVersionStr.toDouble();
1092 QDomElement effectDescr = MainWindow::customEffects.getEffectByTag(QString(), effectId);
1093 if (effectDescr.isNull()) {
1094 effectDescr = MainWindow::videoEffects.getEffectByTag(effectTag, effectId);
1096 if (effectDescr.isNull()) {
1097 effectDescr = MainWindow::audioEffects.getEffectByTag(effectTag, effectId);
1099 if (!effectDescr.isNull()) {
1100 double serviceVersion = -1;
1101 QDomElement serviceVersionElem = effectDescr.firstChildElement("version");
1102 if (!serviceVersionElem.isNull()) {
1103 serviceVersion = serviceVersionElem.text().toDouble();
1105 if (serviceVersion != effectVersion && paths.contains(effectId)) {
1106 if (!scripts.contains(effectId)) {
1107 QFile scriptFile(paths.value(effectId).path());
1108 if (!scriptFile.open(QIODevice::ReadOnly)) {
1111 #if QT_VERSION >= 0x040700
1112 QScriptProgram scriptProgram(scriptFile.readAll());
1114 QString scriptProgram = scriptFile.readAll();
1117 scripts.insert(effectId, scriptProgram);
1120 QScriptEngine scriptEngine;
1121 scriptEngine.importExtension("qt.core");
1122 scriptEngine.importExtension("qt.xml");
1123 scriptEngine.evaluate(scripts.value(effectId));
1124 QScriptValue updateRules = scriptEngine.globalObject().property("update");
1125 if (!updateRules.isValid())
1127 if (updateRules.isFunction()) {
1128 QDomDocument scriptDoc;
1129 scriptDoc.appendChild(scriptDoc.importNode(effect, true));
1131 QString effectString = updateRules.call(QScriptValue(), QScriptValueList() << serviceVersion << effectVersion << scriptDoc.toString()).toString();
1133 if (!effectString.isEmpty() && !scriptEngine.hasUncaughtException()) {
1134 scriptDoc.setContent(effectString);
1135 QDomNode updatedEffect = effect.ownerDocument().importNode(scriptDoc.documentElement(), true);
1136 effect.parentNode().replaceChild(updatedEffect, effect);
1140 m_modified = updateEffectParameters(effect.childNodes(), &updateRules, serviceVersion, effectVersion);
1143 // set version number since MLT won't change it (only initially set it)
1144 QDomElement versionElem = effect.firstChildElement("version");
1145 if (EffectsList::property(effect, "version").isNull()) {
1146 versionElem = effect.ownerDocument().createTextNode(QLocale().toString(serviceVersion)).toElement();
1147 versionElem.setTagName("property");
1148 versionElem.setAttribute("name", "version");
1149 effect.appendChild(versionElem);
1151 EffectsList::setProperty(effect, "version", QLocale().toString(serviceVersion));
1154 else safeEffects.append(effectId);
1159 bool DocumentValidator::updateEffectParameters(QDomNodeList parameters, const QScriptValue* updateRules, const double serviceVersion, const double effectVersion)
1161 bool updated = false;
1162 bool isDowngrade = serviceVersion < effectVersion;
1163 for (int i = 0; i < parameters.count(); ++i) {
1164 QDomElement parameter = parameters.at(i).toElement();
1165 QScriptValue rules = updateRules->property(parameter.attribute("name"));
1166 if (rules.isValid() && rules.isArray()) {
1167 int rulesCount = rules.property("length").toInt32();
1169 // start with the highest version and downgrade step by step
1170 for (int j = rulesCount - 1; j >= 0; --j) {
1171 double version = rules.property(j).property(0).toNumber();
1172 if (version <= effectVersion && version > serviceVersion) {
1173 parameter.firstChild().setNodeValue(rules.property(j).property(1).call(QScriptValue(), QScriptValueList() << parameter.text() << isDowngrade).toString());
1178 for (int j = 0; j < rulesCount; ++j) {
1179 double version = rules.property(j).property(0).toNumber();
1180 if (version > effectVersion && version <= serviceVersion) {
1181 parameter.firstChild().setNodeValue(rules.property(j).property(1).call(QScriptValue(), QScriptValueList() << parameter.text() << isDowngrade).toString());