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>
31 #include <KStandardDirs>
37 #include <QScriptEngine>
39 #include <mlt++/Mlt.h>
44 DocumentValidator::DocumentValidator(QDomDocument doc):
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 // Previous MLT / Kdenlive versions used C locale by default
62 QLocale documentLocale = QLocale::c();
64 if (mlt.hasAttribute("LC_NUMERIC")) {
65 // Set locale for the document
66 // WARNING: what should be done in case the locale does not exist on the system?
67 setlocale(LC_NUMERIC, mlt.attribute("LC_NUMERIC").toUtf8().constData());
68 documentLocale = QLocale(mlt.attribute("LC_NUMERIC"));
70 // Make sure Qt locale and C++ locale have the same numeric separator, might not be the case
71 // With some locales since C++ and Qt use a different database for locales
72 char *separator = localeconv()->decimal_point;
73 if (separator != documentLocale.decimalPoint()) {
74 kDebug()<<"------\n!!! system locale is not similar to Qt's locale... be prepared for bugs!!!\n------";
75 // HACK: There is a locale conflict, so set locale to at least have correct decimal point
76 if (strncmp(separator, ".", 1) == 0) documentLocale = QLocale::c();
77 else if (strncmp(separator, ",", 1) == 0) documentLocale = QLocale("fr_FR.UTF-8");
81 documentLocale.setNumberOptions(QLocale::OmitGroupSeparator);
82 if (documentLocale.decimalPoint() != QLocale().decimalPoint()) {
83 // If loading an older MLT file without LC_NUMERIC, set locale to C which was previously the default
84 if (!mlt.hasAttribute("LC_NUMERIC")) setlocale(LC_NUMERIC, "C");
86 QLocale::setDefault(documentLocale);
87 // locale conversion might need to be redone
88 initEffects::parseEffectFiles();
91 // Upgrade the document to the latest version
92 if (!upgrade(documentLocale.toDouble(kdenliveDoc.attribute("version")), currentVersion))
96 * Check the syntax (this will be replaced by XSD validation with Qt 4.6)
97 * and correct some errors
100 // Return (or create) the tractor
101 QDomElement tractor = mlt.firstChildElement("tractor");
102 if (tractor.isNull()) {
104 tractor = m_doc.createElement("tractor");
105 tractor.setAttribute("global_feed", "1");
106 tractor.setAttribute("in", "0");
107 tractor.setAttribute("out", "-1");
108 tractor.setAttribute("id", "maintractor");
109 mlt.appendChild(tractor);
113 * Make sure at least one track exists, and they're equal in number to
114 * to the maximum between MLT and Kdenlive playlists and tracks
116 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
117 int tracksMax = playlists.count() - 1; // Remove the black track
118 QDomNodeList tracks = tractor.elementsByTagName("track");
119 tracksMax = qMax(tracks.count() - 1, tracksMax);
120 QDomNodeList tracksinfo = kdenliveDoc.elementsByTagName("trackinfo");
121 tracksMax = qMax(tracksinfo.count(), tracksMax);
122 tracksMax = qMax(1, tracksMax); // Force existance of one track
123 if (playlists.count() - 1 < tracksMax ||
124 tracks.count() - 1 < tracksMax ||
125 tracksinfo.count() < tracksMax) {
126 kDebug() << "//// WARNING, PROJECT IS CORRUPTED, MISSING TRACK";
129 // use the MLT tracks as reference
130 if (tracks.count() - 1 < tracksMax) {
131 // Looks like one MLT track is missing, remove the extra Kdenlive track if there is one.
132 if (tracksinfo.count() != tracks.count() - 1) {
133 // The Kdenlive tracks are not ok, clear and rebuild them
134 QDomNode tinfo = kdenliveDoc.firstChildElement("tracksinfo");
135 QDomNode tnode = tinfo.firstChild();
136 while (!tnode.isNull()) {
137 tinfo.removeChild(tnode);
138 tnode = tinfo.firstChild();
141 for (int i = 1; i < tracks.count(); i++) {
142 QString hide = tracks.at(i).toElement().attribute("hide");
143 QDomElement newTrack = m_doc.createElement("trackinfo");
144 if (hide == "video") {
146 newTrack.setAttribute("type", "audio");
147 newTrack.setAttribute("blind", 1);
148 newTrack.setAttribute("mute", 0);
149 newTrack.setAttribute("lock", 0);
151 newTrack.setAttribute("blind", 0);
152 newTrack.setAttribute("mute", 0);
153 newTrack.setAttribute("lock", 0);
155 tinfo.appendChild(newTrack);
160 if (playlists.count() - 1 < tracksMax) {
161 difference = tracksMax - (playlists.count() - 1);
162 for (int i = 0; i < difference; ++i) {
163 QDomElement playlist = m_doc.createElement("playlist");
164 mlt.appendChild(playlist);
167 if (tracks.count() - 1 < tracksMax) {
168 difference = tracksMax - (tracks.count() - 1);
169 for (int i = 0; i < difference; ++i) {
170 QDomElement track = m_doc.createElement("track");
171 tractor.appendChild(track);
174 if (tracksinfo.count() < tracksMax) {
175 QDomElement tracksinfoElm = kdenliveDoc.firstChildElement("tracksinfo");
176 if (tracksinfoElm.isNull()) {
177 tracksinfoElm = m_doc.createElement("tracksinfo");
178 kdenliveDoc.appendChild(tracksinfoElm);
180 difference = tracksMax - tracksinfo.count();
181 for (int i = 0; i < difference; ++i) {
182 QDomElement trackinfo = m_doc.createElement("trackinfo");
183 trackinfo.setAttribute("mute", "0");
184 trackinfo.setAttribute("locked", "0");
185 tracksinfoElm.appendChild(trackinfo);
190 // TODO: check the tracks references
191 // TODO: check internal mix transitions
200 bool DocumentValidator::upgrade(double version, const double currentVersion)
202 kDebug() << "Opening a document with version " << version;
204 // No conversion needed
205 if (version == currentVersion) {
209 // The document is too new
210 if (version > currentVersion) {
211 kDebug() << "Unable to open document with version " << version;
212 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"));
216 // Unsupported document versions
217 if (version == 0.5 || version == 0.7) {
218 kDebug() << "Unable to open document with version " << version;
219 KMessageBox::sorry(kapp->activeWindow(), i18n("This project type is unsupported (version %1) and can't be loaded.", version), i18n("Unable to open project"));
224 QDomNode infoXmlNode = m_doc.elementsByTagName("kdenlivedoc").at(0);
225 QDomElement infoXml = infoXmlNode.toElement();
226 infoXml.setAttribute("upgraded", "1");
228 if (version <= 0.6) {
229 QDomElement infoXml_old = infoXmlNode.cloneNode(true).toElement(); // Needed for folders
230 QDomNode westley = m_doc.elementsByTagName("westley").at(1);
231 QDomNode tractor = m_doc.elementsByTagName("tractor").at(0);
232 QDomNode multitrack = m_doc.elementsByTagName("multitrack").at(0);
233 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
235 QDomNode props = m_doc.elementsByTagName("properties").at(0).toElement();
236 QString profile = props.toElement().attribute("videoprofile");
237 int startPos = props.toElement().attribute("timeline_position").toInt();
238 if (profile == "dv_wide")
239 profile = "dv_pal_wide";
241 // move playlists outside of tractor and add the tracks instead
242 int max = playlists.count();
243 if (westley.isNull()) {
244 westley = m_doc.createElement("westley");
245 m_doc.documentElement().appendChild(westley);
247 if (tractor.isNull()) {
248 kDebug() << "// NO MLT PLAYLIST, building empty one";
249 QDomElement blank_tractor = m_doc.createElement("tractor");
250 westley.appendChild(blank_tractor);
251 QDomElement blank_playlist = m_doc.createElement("playlist");
252 blank_playlist.setAttribute("id", "black_track");
253 westley.insertBefore(blank_playlist, QDomNode());
254 QDomElement blank_track = m_doc.createElement("track");
255 blank_track.setAttribute("producer", "black_track");
256 blank_tractor.appendChild(blank_track);
258 QDomNodeList kdenlivetracks = m_doc.elementsByTagName("kdenlivetrack");
259 for (int i = 0; i < kdenlivetracks.count(); i++) {
260 blank_playlist = m_doc.createElement("playlist");
261 blank_playlist.setAttribute("id", "playlist" + QString::number(i));
262 westley.insertBefore(blank_playlist, QDomNode());
263 blank_track = m_doc.createElement("track");
264 blank_track.setAttribute("producer", "playlist" + QString::number(i));
265 blank_tractor.appendChild(blank_track);
266 if (kdenlivetracks.at(i).toElement().attribute("cliptype") == "Sound") {
267 blank_playlist.setAttribute("hide", "video");
268 blank_track.setAttribute("hide", "video");
271 } else for (int i = 0; i < max; i++) {
272 QDomNode n = playlists.at(i);
273 westley.insertBefore(n, QDomNode());
274 QDomElement pl = n.toElement();
275 QDomElement track = m_doc.createElement("track");
276 QString trackType = pl.attribute("hide");
277 if (!trackType.isEmpty())
278 track.setAttribute("hide", trackType);
279 QString playlist_id = pl.attribute("id");
280 if (playlist_id.isEmpty()) {
281 playlist_id = "black_track";
282 pl.setAttribute("id", playlist_id);
284 track.setAttribute("producer", playlist_id);
285 //tractor.appendChild(track);
286 #define KEEP_TRACK_ORDER 1
287 #ifdef KEEP_TRACK_ORDER
288 tractor.insertAfter(track, QDomNode());
290 // Insert the new track in an order that hopefully matches the 3 video, then 2 audio tracks of Kdenlive 0.7.0
291 // insertion sort - O( tracks*tracks )
292 // Note, this breaks _all_ transitions - but you can move them up and down afterwards.
293 QDomElement tractor_elem = tractor.toElement();
294 if (! tractor_elem.isNull()) {
295 QDomNodeList tracks = tractor_elem.elementsByTagName("track");
296 int size = tracks.size();
298 tractor.insertAfter(track, QDomNode());
300 bool inserted = false;
301 for (int i = 0; i < size; ++i) {
302 QDomElement track_elem = tracks.at(i).toElement();
303 if (track_elem.isNull()) {
304 tractor.insertAfter(track, QDomNode());
308 kDebug() << "playlist_id: " << playlist_id << " producer:" << track_elem.attribute("producer");
309 if (playlist_id < track_elem.attribute("producer")) {
310 tractor.insertBefore(track, track_elem);
316 // Reach here, no insertion, insert last
318 tractor.insertAfter(track, QDomNode());
322 kWarning() << "tractor was not a QDomElement";
323 tractor.insertAfter(track, QDomNode());
327 tractor.removeChild(multitrack);
329 // audio track mixing transitions should not be added to track view, so add required attribute
330 QDomNodeList transitions = m_doc.elementsByTagName("transition");
331 max = transitions.count();
332 for (int i = 0; i < max; i++) {
333 QDomElement tr = transitions.at(i).toElement();
334 if (tr.attribute("combine") == "1" && tr.attribute("mlt_service") == "mix") {
335 QDomElement property = m_doc.createElement("property");
336 property.setAttribute("name", "internal_added");
337 QDomText value = m_doc.createTextNode("237");
338 property.appendChild(value);
339 tr.appendChild(property);
340 property = m_doc.createElement("property");
341 property.setAttribute("name", "mlt_service");
342 value = m_doc.createTextNode("mix");
343 property.appendChild(value);
344 tr.appendChild(property);
346 // convert transition
347 QDomNamedNodeMap attrs = tr.attributes();
348 for (int j = 0; j < attrs.count(); j++) {
349 QString attrName = attrs.item(j).nodeName();
350 if (attrName != "in" && attrName != "out" && attrName != "id") {
351 QDomElement property = m_doc.createElement("property");
352 property.setAttribute("name", attrName);
353 QDomText value = m_doc.createTextNode(attrs.item(j).nodeValue());
354 property.appendChild(value);
355 tr.appendChild(property);
361 // move transitions after tracks
362 for (int i = 0; i < max; i++) {
363 tractor.insertAfter(transitions.at(0), QDomNode());
366 // Fix filters format
367 QDomNodeList entries = m_doc.elementsByTagName("entry");
368 max = entries.count();
369 for (int i = 0; i < max; i++) {
372 QDomNode m = entries.at(i).firstChild();
373 while (!m.isNull()) {
374 if (m.toElement().tagName() == "filter") {
375 QDomElement filt = m.toElement();
376 QDomNamedNodeMap attrs = filt.attributes();
377 QString current_id = filt.attribute("kdenlive_id");
378 if (current_id != last_id) {
380 last_id = current_id;
382 QDomElement e = m_doc.createElement("property");
383 e.setAttribute("name", "kdenlive_ix");
384 QDomText value = m_doc.createTextNode(QString::number(effectix));
385 e.appendChild(value);
387 for (int j = 0; j < attrs.count(); j++) {
388 QDomAttr a = attrs.item(j).toAttr();
390 kDebug() << " FILTER; adding :" << a.name() << ":" << a.value();
391 QDomElement e = m_doc.createElement("property");
392 e.setAttribute("name", a.name());
393 QDomText value = m_doc.createTextNode(a.value());
394 e.appendChild(value);
405 QDomNodeList filters = m_doc.elementsByTagName("filter");
406 max = filters.count();
409 for (int i = 0; i < max; i++) {
410 QDomElement filt = filters.at(i).toElement();
411 QDomNamedNodeMap attrs = filt.attributes();
412 QString current_id = filt.attribute("kdenlive_id");
413 if (current_id != last_id) {
415 last_id = current_id;
417 QDomElement e = m_doc.createElement("property");
418 e.setAttribute("name", "kdenlive_ix");
419 QDomText value = m_doc.createTextNode(QString::number(1));
420 e.appendChild(value);
422 for (int j = 0; j < attrs.count(); j++) {
423 QDomAttr a = attrs.item(j).toAttr();
425 kDebug() << " FILTER; adding :" << a.name() << ":" << a.value();
426 QDomElement e = m_doc.createElement("property");
427 e.setAttribute("name", a.name());
428 QDomText value = m_doc.createTextNode(a.value());
429 e.appendChild(value);
436 QDomNodeList producers = westley.toElement().elementsByTagName("producer");
437 max = producers.count();
438 for (int i = 0; i < max; i++) {
439 QDomElement prod = producers.at(i).toElement();
440 if (prod.attribute("mlt_service") == "framebuffer") {
441 QString slowmotionprod = prod.attribute("resource");
442 slowmotionprod.replace(':', '?');
443 kDebug() << "// FOUND WRONG SLOWMO, new: " << slowmotionprod;
444 prod.setAttribute("resource", slowmotionprod);
447 // move producers to correct place, markers to a global list, fix clip descriptions
448 QDomElement markers = m_doc.createElement("markers");
449 // This will get the xml producers:
450 producers = m_doc.elementsByTagName("producer");
451 max = producers.count();
452 for (int i = 0; i < max; i++) {
453 QDomElement prod = producers.at(0).toElement();
454 // add resource also as a property (to allow path correction in setNewResource())
455 // TODO: will it work with slowmotion? needs testing
456 /*if (!prod.attribute("resource").isEmpty()) {
457 QDomElement prop_resource = m_doc.createElement("property");
458 prop_resource.setAttribute("name", "resource");
459 QDomText resource = m_doc.createTextNode(prod.attribute("resource"));
460 prop_resource.appendChild(resource);
461 prod.appendChild(prop_resource);
463 QDomNode m = prod.firstChild();
465 if (m.toElement().tagName() == "markers") {
466 QDomNodeList prodchilds = m.childNodes();
467 int maxchild = prodchilds.count();
468 for (int k = 0; k < maxchild; k++) {
469 QDomElement mark = prodchilds.at(0).toElement();
470 mark.setAttribute("id", prod.attribute("id"));
471 markers.insertAfter(mark, QDomNode());
474 } else if (prod.attribute("type").toInt() == TEXT) {
475 // convert title clip
476 if (m.toElement().tagName() == "textclip") {
478 QDomElement titleclip = m.toElement();
479 QDomElement title = tdoc.createElement("kdenlivetitle");
480 tdoc.appendChild(title);
481 QDomNodeList objects = titleclip.childNodes();
482 int maxchild = objects.count();
483 for (int k = 0; k < maxchild; k++) {
485 QDomElement ob = objects.at(k).toElement();
486 if (ob.attribute("type") == "3") {
487 // text object - all of this goes into "xmldata"...
488 QDomElement item = tdoc.createElement("item");
489 item.setAttribute("z-index", ob.attribute("z"));
490 item.setAttribute("type", "QGraphicsTextItem");
491 QDomElement position = tdoc.createElement("position");
492 position.setAttribute("x", ob.attribute("x"));
493 position.setAttribute("y", ob.attribute("y"));
494 QDomElement content = tdoc.createElement("content");
495 content.setAttribute("font", ob.attribute("font_family"));
496 content.setAttribute("font-size", ob.attribute("font_size"));
497 content.setAttribute("font-bold", ob.attribute("bold"));
498 content.setAttribute("font-italic", ob.attribute("italic"));
499 content.setAttribute("font-underline", ob.attribute("underline"));
500 QString col = ob.attribute("color");
502 content.setAttribute("font-color", colorToString(c));
503 // todo: These fields are missing from the newly generated xmldata:
504 // transform, startviewport, endviewport, background
506 QDomText conttxt = tdoc.createTextNode(ob.attribute("text"));
507 content.appendChild(conttxt);
508 item.appendChild(position);
509 item.appendChild(content);
510 title.appendChild(item);
511 } else if (ob.attribute("type") == "5") {
513 QDomElement item = tdoc.createElement("item");
514 item.setAttribute("z-index", ob.attribute("z"));
515 item.setAttribute("type", "QGraphicsRectItem");
516 QDomElement position = tdoc.createElement("position");
517 position.setAttribute("x", ob.attribute("x"));
518 position.setAttribute("y", ob.attribute("y"));
519 QDomElement content = tdoc.createElement("content");
520 QString col = ob.attribute("color");
522 content.setAttribute("brushcolor", colorToString(c));
523 QString rect = "0,0,";
524 rect.append(ob.attribute("width"));
526 rect.append(ob.attribute("height"));
527 content.setAttribute("rect", rect);
528 item.appendChild(position);
529 item.appendChild(content);
530 title.appendChild(item);
533 prod.setAttribute("xmldata", tdoc.toString());
534 // mbd todo: This clearly does not work, as every title gets the same name - trying to leave it empty
535 // QStringList titleInfo = TitleWidget::getFreeTitleInfo(projectFolder());
536 // prod.setAttribute("titlename", titleInfo.at(0));
537 // prod.setAttribute("resource", titleInfo.at(1));
538 //kDebug()<<"TITLE DATA:\n"<<tdoc.toString();
540 } // End conversion of title clips.
542 } else if (m.isText()) {
543 QString comment = m.nodeValue();
544 if (!comment.isEmpty()) {
545 prod.setAttribute("description", comment);
550 int duration = prod.attribute("duration").toInt();
551 if (duration > 0) prod.setAttribute("out", QString::number(duration));
552 // The clip goes back in, but text clips should not go back in, at least not modified
553 westley.insertBefore(prod, QDomNode());
556 QDomNode westley0 = m_doc.elementsByTagName("westley").at(0);
557 if (!markers.firstChild().isNull()) westley0.appendChild(markers);
560 * Convert as much of the kdenlivedoc as possible. Use the producer in
561 * westley. First, remove the old stuff from westley, and add a new
562 * empty one. Also, track the max id in order to use it for the adding
565 int max_kproducer_id = 0;
566 westley0.removeChild(infoXmlNode);
567 QDomElement infoXml_new = m_doc.createElement("kdenlivedoc");
568 infoXml_new.setAttribute("profile", profile);
569 infoXml.setAttribute("position", startPos);
571 // Add all the producers that has a resource in westley
572 QDomElement westley_element = westley0.toElement();
573 if (westley_element.isNull()) {
574 kWarning() << "westley0 element in document was not a QDomElement - unable to add producers to new kdenlivedoc";
576 QDomNodeList wproducers = westley_element.elementsByTagName("producer");
577 int kmax = wproducers.count();
578 for (int i = 0; i < kmax; i++) {
579 QDomElement wproducer = wproducers.at(i).toElement();
580 if (wproducer.isNull()) {
581 kWarning() << "Found producer in westley0, that was not a QDomElement";
584 if (wproducer.attribute("id") == "black") continue;
585 // We have to do slightly different things, depending on the type
586 kDebug() << "Converting producer element with type" << wproducer.attribute("type");
587 if (wproducer.attribute("type").toInt() == TEXT) {
588 kDebug() << "Found TEXT element in producer" << endl;
589 QDomElement kproducer = wproducer.cloneNode(true).toElement();
590 kproducer.setTagName("kdenlive_producer");
591 infoXml_new.appendChild(kproducer);
593 * TODO: Perhaps needs some more changes here to
594 * "frequency", aspect ratio as a float, frame_size,
595 * channels, and later, resource and title name
598 QDomElement kproducer = m_doc.createElement("kdenlive_producer");
599 kproducer.setAttribute("id", wproducer.attribute("id"));
600 if (!wproducer.attribute("description").isEmpty())
601 kproducer.setAttribute("description", wproducer.attribute("description"));
602 kproducer.setAttribute("resource", wproducer.attribute("resource"));
603 kproducer.setAttribute("type", wproducer.attribute("type"));
604 // Testing fix for 358
605 if (!wproducer.attribute("aspect_ratio").isEmpty()) {
606 kproducer.setAttribute("aspect_ratio", wproducer.attribute("aspect_ratio"));
608 if (!wproducer.attribute("source_fps").isEmpty()) {
609 kproducer.setAttribute("fps", wproducer.attribute("source_fps"));
611 if (!wproducer.attribute("length").isEmpty()) {
612 kproducer.setAttribute("duration", wproducer.attribute("length"));
614 infoXml_new.appendChild(kproducer);
616 if (wproducer.attribute("id").toInt() > max_kproducer_id) {
617 max_kproducer_id = wproducer.attribute("id").toInt();
621 #define LOOKUP_FOLDER 1
624 * Look through all the folder elements of the old doc, for each folder,
625 * for each producer, get the id, look it up in the new doc, set the
626 * groupname and groupid. Note, this does not work at the moment - at
627 * least one folder shows up missing, and clips with no folder does not
630 //QDomElement infoXml_old = infoXmlNode.toElement();
631 if (!infoXml_old.isNull()) {
632 QDomNodeList folders = infoXml_old.elementsByTagName("folder");
633 int fsize = folders.size();
634 int groupId = max_kproducer_id + 1; // Start at +1 of max id of the kdenlive_producers
635 for (int i = 0; i < fsize; ++i) {
636 QDomElement folder = folders.at(i).toElement();
637 if (!folder.isNull()) {
638 QString groupName = folder.attribute("name");
639 kDebug() << "groupName: " << groupName << " with groupId: " << groupId;
640 QDomNodeList fproducers = folder.elementsByTagName("producer");
641 int psize = fproducers.size();
642 for (int j = 0; j < psize; ++j) {
643 QDomElement fproducer = fproducers.at(j).toElement();
644 if (!fproducer.isNull()) {
645 QString id = fproducer.attribute("id");
646 // This is not very effective, but compared to loading the clips, its a breeze
647 QDomNodeList kdenlive_producers = infoXml_new.elementsByTagName("kdenlive_producer");
648 int kpsize = kdenlive_producers.size();
649 for (int k = 0; k < kpsize; ++k) {
650 QDomElement kproducer = kdenlive_producers.at(k).toElement(); // Its an element for sure
651 if (id == kproducer.attribute("id")) {
652 // We do not check that it already is part of a folder
653 kproducer.setAttribute("groupid", groupId);
654 kproducer.setAttribute("groupname", groupName);
665 QDomNodeList elements = westley.childNodes();
666 max = elements.count();
667 for (int i = 0; i < max; i++) {
668 QDomElement prod = elements.at(0).toElement();
669 westley0.insertAfter(prod, QDomNode());
672 westley0.appendChild(infoXml_new);
674 westley0.removeChild(westley);
676 // adds <avfile /> information to <kdenlive_producer />
677 QDomNodeList kproducers = m_doc.elementsByTagName("kdenlive_producer");
678 QDomNodeList avfiles = infoXml_old.elementsByTagName("avfile");
679 kDebug() << "found" << avfiles.count() << "<avfile />s and" << kproducers.count() << "<kdenlive_producer />s";
680 for (int i = 0; i < avfiles.count(); ++i) {
681 QDomElement avfile = avfiles.at(i).toElement();
682 QDomElement kproducer;
684 kWarning() << "found an <avfile /> that is not a QDomElement";
686 QString id = avfile.attribute("id");
687 // this is horrible, must be rewritten, it's just for test
688 for (int j = 0; j < kproducers.count(); ++j) {
689 //kDebug() << "checking <kdenlive_producer /> with id" << kproducers.at(j).toElement().attribute("id");
690 if (kproducers.at(j).toElement().attribute("id") == id) {
691 kproducer = kproducers.at(j).toElement();
695 if (kproducer == QDomElement())
696 kWarning() << "no match for <avfile /> with id =" << id;
698 //kDebug() << "ready to set additional <avfile />'s attributes (id =" << id << ")";
699 kproducer.setAttribute("channels", avfile.attribute("channels"));
700 kproducer.setAttribute("duration", avfile.attribute("duration"));
701 kproducer.setAttribute("frame_size", avfile.attribute("width") + 'x' + avfile.attribute("height"));
702 kproducer.setAttribute("frequency", avfile.attribute("frequency"));
703 if (kproducer.attribute("description").isEmpty() && !avfile.attribute("description").isEmpty())
704 kproducer.setAttribute("description", avfile.attribute("description"));
708 infoXml = infoXml_new;
711 if (version <= 0.81) {
712 // Add the tracks information
713 QString tracksOrder = infoXml.attribute("tracks");
714 if (tracksOrder.isEmpty()) {
715 QDomNodeList tracks = m_doc.elementsByTagName("track");
716 for (int i = 0; i < tracks.count(); i++) {
717 QDomElement track = tracks.at(i).toElement();
718 if (track.attribute("producer") != "black_track") {
719 if (track.attribute("hide") == "video")
720 tracksOrder.append('a');
722 tracksOrder.append('v');
726 QDomElement tracksinfo = m_doc.createElement("tracksinfo");
727 for (int i = 0; i < tracksOrder.size(); i++) {
728 QDomElement trackinfo = m_doc.createElement("trackinfo");
729 if (tracksOrder.data()[i] == 'a') {
730 trackinfo.setAttribute("type", "audio");
731 trackinfo.setAttribute("blind", true);
733 trackinfo.setAttribute("blind", false);
734 trackinfo.setAttribute("mute", false);
735 trackinfo.setAttribute("locked", false);
736 tracksinfo.appendChild(trackinfo);
738 infoXml.appendChild(tracksinfo);
741 if (version <= 0.82) {
742 // Convert <westley />s in <mlt />s (MLT extreme makeover)
743 QDomNodeList westleyNodes = m_doc.elementsByTagName("westley");
744 for (int i = 0; i < westleyNodes.count(); i++) {
745 QDomElement westley = westleyNodes.at(i).toElement();
746 westley.setTagName("mlt");
750 if (version <= 0.83) {
751 // Replace point size with pixel size in text titles
752 if (m_doc.toString().contains("font-size")) {
753 KMessageBox::ButtonCode convert = KMessageBox::Continue;
754 QDomNodeList kproducerNodes = m_doc.elementsByTagName("kdenlive_producer");
755 for (int i = 0; i < kproducerNodes.count() && convert != KMessageBox::No; ++i) {
756 QDomElement kproducer = kproducerNodes.at(i).toElement();
757 if (kproducer.attribute("type").toInt() == TEXT) {
759 data.setContent(kproducer.attribute("xmldata"));
760 QDomNodeList items = data.firstChild().childNodes();
761 for (int j = 0; j < items.count() && convert != KMessageBox::No; ++j) {
762 if (items.at(j).attributes().namedItem("type").nodeValue() == "QGraphicsTextItem") {
763 QDomNamedNodeMap textProperties = items.at(j).namedItem("content").attributes();
764 if (textProperties.namedItem("font-pixel-size").isNull() && !textProperties.namedItem("font-size").isNull()) {
765 // Ask the user if he wants to convert
766 if (convert != KMessageBox::Yes && convert != KMessageBox::No)
767 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"));
768 if (convert == KMessageBox::Yes) {
770 font.setPointSize(textProperties.namedItem("font-size").nodeValue().toInt());
771 QDomElement content = items.at(j).namedItem("content").toElement();
772 content.setAttribute("font-pixel-size", QFontInfo(font).pixelSize());
773 content.removeAttribute("font-size");
774 kproducer.setAttribute("xmldata", data.toString());
776 * You may be tempted to delete the preview file
777 * to force its recreation: bad idea (see
778 * http://www.kdenlive.org/mantis/view.php?id=749)
788 // Fill the <documentproperties /> element
789 QDomElement docProperties = infoXml.firstChildElement("documentproperties");
790 if (docProperties.isNull()) {
791 docProperties = m_doc.createElement("documentproperties");
792 docProperties.setAttribute("zonein", infoXml.attribute("zonein"));
793 docProperties.setAttribute("zoneout", infoXml.attribute("zoneout"));
794 docProperties.setAttribute("zoom", infoXml.attribute("zoom"));
795 docProperties.setAttribute("position", infoXml.attribute("position"));
796 infoXml.appendChild(docProperties);
800 if (version <= 0.84) {
801 // update the title clips to use the new MLT kdenlivetitle producer
802 QDomNodeList kproducerNodes = m_doc.elementsByTagName("kdenlive_producer");
803 for (int i = 0; i < kproducerNodes.count(); ++i) {
804 QDomElement kproducer = kproducerNodes.at(i).toElement();
805 if (kproducer.attribute("type").toInt() == TEXT) {
806 QString data = kproducer.attribute("xmldata");
807 QString datafile = kproducer.attribute("resource");
808 if (!datafile.endsWith(".kdenlivetitle")) {
809 datafile = QString();
810 kproducer.setAttribute("resource", QString());
812 QString id = kproducer.attribute("id");
813 QDomNodeList mltproducers = m_doc.elementsByTagName("producer");
814 bool foundData = false;
815 bool foundResource = false;
816 bool foundService = false;
817 for (int j = 0; j < mltproducers.count(); j++) {
818 QDomElement wproducer = mltproducers.at(j).toElement();
819 if (wproducer.attribute("id") == id) {
820 QDomNodeList props = wproducer.childNodes();
821 for (int k = 0; k < props.count(); k++) {
822 if (props.at(k).toElement().attribute("name") == "xmldata") {
823 props.at(k).firstChild().setNodeValue(data);
825 } else if (props.at(k).toElement().attribute("name") == "mlt_service") {
826 props.at(k).firstChild().setNodeValue("kdenlivetitle");
828 } else if (props.at(k).toElement().attribute("name") == "resource") {
829 props.at(k).firstChild().setNodeValue(datafile);
830 foundResource = true;
834 QDomElement e = m_doc.createElement("property");
835 e.setAttribute("name", "xmldata");
836 QDomText value = m_doc.createTextNode(data);
837 e.appendChild(value);
838 wproducer.appendChild(e);
841 QDomElement e = m_doc.createElement("property");
842 e.setAttribute("name", "mlt_service");
843 QDomText value = m_doc.createTextNode("kdenlivetitle");
844 e.appendChild(value);
845 wproducer.appendChild(e);
847 if (!foundResource) {
848 QDomElement e = m_doc.createElement("property");
849 e.setAttribute("name", "resource");
850 QDomText value = m_doc.createTextNode(datafile);
851 e.appendChild(value);
852 wproducer.appendChild(e);
860 if (version <= 0.85) {
861 // update the LADSPA effects to use the new ladspa.id format instead of external xml file
862 QDomNodeList effectNodes = m_doc.elementsByTagName("filter");
863 for (int i = 0; i < effectNodes.count(); ++i) {
864 QDomElement effect = effectNodes.at(i).toElement();
865 if (EffectsList::property(effect, "mlt_service") == "ladspa") {
866 // Needs to be converted
867 QStringList info = getInfoFromEffectName(EffectsList::property(effect, "kdenlive_id"));
868 if (info.isEmpty()) continue;
869 // info contains the correct ladspa.id from kdenlive effect name, and a list of parameter's old and new names
870 EffectsList::setProperty(effect, "kdenlive_id", info.at(0));
871 EffectsList::setProperty(effect, "tag", info.at(0));
872 EffectsList::setProperty(effect, "mlt_service", info.at(0));
873 EffectsList::removeProperty(effect, "src");
874 for (int j = 1; j < info.size(); j++) {
875 QString value = EffectsList::property(effect, info.at(j).section('=', 0, 0));
876 if (!value.isEmpty()) {
877 // update parameter name
878 EffectsList::renameProperty(effect, info.at(j).section('=', 0, 0), info.at(j).section('=', 1, 1));
885 if (version <= 0.86) {
886 // Make sure we don't have avformat-novalidate producers, since it caused crashes
887 QDomNodeList producers = m_doc.elementsByTagName("producer");
888 int max = producers.count();
889 for (int i = 0; i < max; i++) {
890 QDomElement prod = producers.at(i).toElement();
891 if (EffectsList::property(prod, "mlt_service") == "avformat-novalidate")
892 EffectsList::setProperty(prod, "mlt_service", "avformat");
895 // 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
897 // Get profile info (width / height)
900 QDomElement profile = m_doc.firstChildElement("profile");
901 if (profile.isNull()) profile = infoXml.firstChildElement("profileinfo");
902 if (profile.isNull()) {
903 // could not find profile info, set PAL
908 profileWidth = profile.attribute("width").toInt();
909 profileHeight = profile.attribute("height").toInt();
911 QDomNodeList transitions = m_doc.elementsByTagName("transition");
912 max = transitions.count();
914 for (int i = 0; i < max; i++) {
915 QDomElement trans = transitions.at(i).toElement();
916 out = trans.attribute("out").toInt() - trans.attribute("in").toInt();
917 QString geom = EffectsList::property(trans, "geometry");
918 Mlt::Geometry *g = new Mlt::Geometry(geom.toUtf8().data(), out, profileWidth, profileHeight);
919 Mlt::GeometryItem item;
920 if (g->next_key(&item, out) == 0) {
921 // We have a keyframe just after last frame, try to move it to last frame
922 if (item.frame() == out + 1) {
926 EffectsList::setProperty(trans, "geometry", g->serialise());
933 if (version <= 0.87) {
934 if (!m_doc.firstChildElement("mlt").hasAttribute("LC_NUMERIC")) {
935 m_doc.firstChildElement("mlt").setAttribute("LC_NUMERIC", "C");
939 // The document has been converted: mark it as modified
940 infoXml.setAttribute("version", currentVersion);
945 QStringList DocumentValidator::getInfoFromEffectName(const QString oldName)
948 // Returns a list to convert old Kdenlive ladspa effects
949 if (oldName == "pitch_shift") {
950 info << "ladspa.1433";
953 else if (oldName == "vinyl") {
954 info << "ladspa.1905";
961 else if (oldName == "room_reverb") {
962 info << "ladspa.1216";
967 else if (oldName == "reverb") {
968 info << "ladspa.1423";
972 else if (oldName == "rate_scale") {
973 info << "ladspa.1417";
976 else if (oldName == "pitch_scale") {
977 info << "ladspa.1193";
980 else if (oldName == "phaser") {
981 info << "ladspa.1217";
984 info << "feedback=2";
987 else if (oldName == "limiter") {
988 info << "ladspa.1913";
993 else if (oldName == "equalizer_15") {
994 info << "ladspa.1197";
1011 else if (oldName == "equalizer") {
1012 info << "ladspa.1901";
1014 info << "midgain=1";
1017 else if (oldName == "declipper") {
1018 info << "ladspa.1195";
1023 QString DocumentValidator::colorToString(const QColor& c)
1025 QString ret = "%1,%2,%3,%4";
1026 ret = ret.arg(c.red()).arg(c.green()).arg(c.blue()).arg(c.alpha());
1030 bool DocumentValidator::isProject() const
1032 QDomNode infoXmlNode = m_doc.elementsByTagName("kdenlivedoc").at(0);
1033 return !infoXmlNode.isNull();
1036 bool DocumentValidator::isModified() const
1041 void DocumentValidator::updateEffects()
1043 // WARNING: order by findDirs will determine which js file to use (in case multiple scripts for the same filter exist)
1044 QMap <QString, KUrl> paths;
1045 #if QT_VERSION >= 0x040700
1046 QMap <QString, QScriptProgram> scripts;
1048 QMap <QString, QString> scripts;
1050 QStringList directories = KGlobal::dirs()->findDirs("appdata", "effects/update");
1051 foreach (const QString &directoryName, directories) {
1052 QDir directory(directoryName);
1053 QStringList fileList = directory.entryList(QStringList() << "*.js", QDir::Files);
1054 foreach (const QString &fileName, fileList) {
1055 QString identifier = fileName;
1056 // remove extension (".js")
1058 paths.insert(identifier, KUrl(directoryName + fileName));
1062 QDomNodeList effects = m_doc.elementsByTagName("filter");
1063 int max = effects.count();
1064 QStringList safeEffects;
1065 for(int i = 0; i < max; ++i) {
1066 QDomElement effect = effects.at(i).toElement();
1067 QString effectId = EffectsList::property(effect, "kdenlive_id");
1068 if (safeEffects.contains(effectId)) {
1069 // Do not check the same effect twice if it is at the correct version
1070 // (assume we don't have different versions of the same effect in a project file)
1073 QString effectTag = EffectsList::property(effect, "tag");
1074 QString effectVersionStr = EffectsList::property(effect, "version");
1075 double effectVersion = effectVersionStr.isNull() ? -1 : effectVersionStr.toDouble();
1077 QDomElement effectDescr = MainWindow::customEffects.getEffectByTag(QString(), effectId);
1078 if (effectDescr.isNull()) {
1079 effectDescr = MainWindow::videoEffects.getEffectByTag(effectTag, effectId);
1081 if (effectDescr.isNull()) {
1082 effectDescr = MainWindow::audioEffects.getEffectByTag(effectTag, effectId);
1084 if (!effectDescr.isNull()) {
1085 double serviceVersion = -1;
1086 QDomElement serviceVersionElem = effectDescr.firstChildElement("version");
1087 if (!serviceVersionElem.isNull()) {
1088 serviceVersion = serviceVersionElem.text().toDouble();
1090 if (serviceVersion != effectVersion && paths.contains(effectId)) {
1091 if (!scripts.contains(effectId)) {
1092 QFile scriptFile(paths.value(effectId).path());
1093 if (!scriptFile.open(QIODevice::ReadOnly)) {
1096 #if QT_VERSION >= 0x040700
1097 QScriptProgram scriptProgram(scriptFile.readAll());
1099 QString scriptProgram = scriptFile.readAll();
1102 scripts.insert(effectId, scriptProgram);
1105 QScriptEngine scriptEngine;
1106 scriptEngine.importExtension("qt.core");
1107 scriptEngine.importExtension("qt.xml");
1108 scriptEngine.evaluate(scripts.value(effectId));
1109 QScriptValue updateRules = scriptEngine.globalObject().property("update");
1110 if (!updateRules.isValid())
1112 if (updateRules.isFunction()) {
1113 QDomDocument scriptDoc;
1114 scriptDoc.appendChild(scriptDoc.importNode(effect, true));
1116 QString effectString = updateRules.call(QScriptValue(), QScriptValueList() << serviceVersion << effectVersion << scriptDoc.toString()).toString();
1118 if (!effectString.isEmpty() && !scriptEngine.hasUncaughtException()) {
1119 scriptDoc.setContent(effectString);
1120 QDomNode updatedEffect = effect.ownerDocument().importNode(scriptDoc.documentElement(), true);
1121 effect.parentNode().replaceChild(updatedEffect, effect);
1125 m_modified = updateEffectParameters(effect.childNodes(), &updateRules, serviceVersion, effectVersion);
1128 // set version number since MLT won't change it (only initially set it)
1129 QDomElement versionElem = effect.firstChildElement("version");
1130 if (EffectsList::property(effect, "version").isNull()) {
1131 versionElem = effect.ownerDocument().createTextNode(QLocale().toString(serviceVersion)).toElement();
1132 versionElem.setTagName("property");
1133 versionElem.setAttribute("name", "version");
1134 effect.appendChild(versionElem);
1136 EffectsList::setProperty(effect, "version", QLocale().toString(serviceVersion));
1139 else safeEffects.append(effectId);
1144 bool DocumentValidator::updateEffectParameters(QDomNodeList parameters, const QScriptValue* updateRules, const double serviceVersion, const double effectVersion)
1146 bool updated = false;
1147 bool isDowngrade = serviceVersion < effectVersion;
1148 for (int i = 0; i < parameters.count(); ++i) {
1149 QDomElement parameter = parameters.at(i).toElement();
1150 QScriptValue rules = updateRules->property(parameter.attribute("name"));
1151 if (rules.isValid() && rules.isArray()) {
1152 int rulesCount = rules.property("length").toInt32();
1154 // start with the highest version and downgrade step by step
1155 for (int j = rulesCount - 1; j >= 0; --j) {
1156 double version = rules.property(j).property(0).toNumber();
1157 if (version <= effectVersion && version > serviceVersion) {
1158 parameter.firstChild().setNodeValue(rules.property(j).property(1).call(QScriptValue(), QScriptValueList() << parameter.text() << isDowngrade).toString());
1163 for (int j = 0; j < rulesCount; ++j) {
1164 double version = rules.property(j).property(0).toNumber();
1165 if (version > effectVersion && version <= serviceVersion) {
1166 parameter.firstChild().setNodeValue(rules.property(j).property(1).call(QScriptValue(), QScriptValueList() << parameter.text() << isDowngrade).toString());