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"));
71 documentLocale.setNumberOptions(QLocale::OmitGroupSeparator);
72 if (documentLocale.decimalPoint() != QLocale().decimalPoint()) {
73 // If loading an older MLT file without LC_NUMERIC, set locale to C which was previously the default
74 if (!mlt.hasAttribute("LC_NUMERIC")) setlocale(LC_NUMERIC, "C");
76 QLocale::setDefault(documentLocale);
77 // locale conversion might need to be redone
78 initEffects::parseEffectFiles();
81 // Upgrade the document to the latest version
82 if (!upgrade(documentLocale.toDouble(kdenliveDoc.attribute("version")), currentVersion))
86 * Check the syntax (this will be replaced by XSD validation with Qt 4.6)
87 * and correct some errors
90 // Return (or create) the tractor
91 QDomElement tractor = mlt.firstChildElement("tractor");
92 if (tractor.isNull()) {
94 tractor = m_doc.createElement("tractor");
95 tractor.setAttribute("global_feed", "1");
96 tractor.setAttribute("in", "0");
97 tractor.setAttribute("out", "-1");
98 tractor.setAttribute("id", "maintractor");
99 mlt.appendChild(tractor);
103 * Make sure at least one track exists, and they're equal in number to
104 * to the maximum between MLT and Kdenlive playlists and tracks
106 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
107 int tracksMax = playlists.count() - 1; // Remove the black track
108 QDomNodeList tracks = tractor.elementsByTagName("track");
109 tracksMax = qMax(tracks.count() - 1, tracksMax);
110 QDomNodeList tracksinfo = kdenliveDoc.elementsByTagName("trackinfo");
111 tracksMax = qMax(tracksinfo.count(), tracksMax);
112 tracksMax = qMax(1, tracksMax); // Force existance of one track
113 if (playlists.count() - 1 < tracksMax ||
114 tracks.count() - 1 < tracksMax ||
115 tracksinfo.count() < tracksMax) {
116 kDebug() << "//// WARNING, PROJECT IS CORRUPTED, MISSING TRACK";
119 // use the MLT tracks as reference
120 if (tracks.count() - 1 < tracksMax) {
121 // Looks like one MLT track is missing, remove the extra Kdenlive track if there is one.
122 if (tracksinfo.count() != tracks.count() - 1) {
123 // The Kdenlive tracks are not ok, clear and rebuild them
124 QDomNode tinfo = kdenliveDoc.firstChildElement("tracksinfo");
125 QDomNode tnode = tinfo.firstChild();
126 while (!tnode.isNull()) {
127 tinfo.removeChild(tnode);
128 tnode = tinfo.firstChild();
131 for (int i = 1; i < tracks.count(); i++) {
132 QString hide = tracks.at(i).toElement().attribute("hide");
133 QDomElement newTrack = m_doc.createElement("trackinfo");
134 if (hide == "video") {
136 newTrack.setAttribute("type", "audio");
137 newTrack.setAttribute("blind", 1);
138 newTrack.setAttribute("mute", 0);
139 newTrack.setAttribute("lock", 0);
141 newTrack.setAttribute("blind", 0);
142 newTrack.setAttribute("mute", 0);
143 newTrack.setAttribute("lock", 0);
145 tinfo.appendChild(newTrack);
150 if (playlists.count() - 1 < tracksMax) {
151 difference = tracksMax - (playlists.count() - 1);
152 for (int i = 0; i < difference; ++i) {
153 QDomElement playlist = m_doc.createElement("playlist");
154 mlt.appendChild(playlist);
157 if (tracks.count() - 1 < tracksMax) {
158 difference = tracksMax - (tracks.count() - 1);
159 for (int i = 0; i < difference; ++i) {
160 QDomElement track = m_doc.createElement("track");
161 tractor.appendChild(track);
164 if (tracksinfo.count() < tracksMax) {
165 QDomElement tracksinfoElm = kdenliveDoc.firstChildElement("tracksinfo");
166 if (tracksinfoElm.isNull()) {
167 tracksinfoElm = m_doc.createElement("tracksinfo");
168 kdenliveDoc.appendChild(tracksinfoElm);
170 difference = tracksMax - tracksinfo.count();
171 for (int i = 0; i < difference; ++i) {
172 QDomElement trackinfo = m_doc.createElement("trackinfo");
173 trackinfo.setAttribute("mute", "0");
174 trackinfo.setAttribute("locked", "0");
175 tracksinfoElm.appendChild(trackinfo);
180 // TODO: check the tracks references
181 // TODO: check internal mix transitions
190 bool DocumentValidator::upgrade(double version, const double currentVersion)
192 kDebug() << "Opening a document with version " << version;
194 // No conversion needed
195 if (version == currentVersion) {
199 // The document is too new
200 if (version > currentVersion) {
201 kDebug() << "Unable to open document with version " << version;
202 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"));
206 // Unsupported document versions
207 if (version == 0.5 || version == 0.7) {
208 kDebug() << "Unable to open document with version " << version;
209 KMessageBox::sorry(kapp->activeWindow(), i18n("This project type is unsupported (version %1) and can't be loaded.", version), i18n("Unable to open project"));
214 QDomNode infoXmlNode = m_doc.elementsByTagName("kdenlivedoc").at(0);
215 QDomElement infoXml = infoXmlNode.toElement();
216 infoXml.setAttribute("upgraded", "1");
218 if (version <= 0.6) {
219 QDomElement infoXml_old = infoXmlNode.cloneNode(true).toElement(); // Needed for folders
220 QDomNode westley = m_doc.elementsByTagName("westley").at(1);
221 QDomNode tractor = m_doc.elementsByTagName("tractor").at(0);
222 QDomNode multitrack = m_doc.elementsByTagName("multitrack").at(0);
223 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
225 QDomNode props = m_doc.elementsByTagName("properties").at(0).toElement();
226 QString profile = props.toElement().attribute("videoprofile");
227 int startPos = props.toElement().attribute("timeline_position").toInt();
228 if (profile == "dv_wide")
229 profile = "dv_pal_wide";
231 // move playlists outside of tractor and add the tracks instead
232 int max = playlists.count();
233 if (westley.isNull()) {
234 westley = m_doc.createElement("westley");
235 m_doc.documentElement().appendChild(westley);
237 if (tractor.isNull()) {
238 kDebug() << "// NO MLT PLAYLIST, building empty one";
239 QDomElement blank_tractor = m_doc.createElement("tractor");
240 westley.appendChild(blank_tractor);
241 QDomElement blank_playlist = m_doc.createElement("playlist");
242 blank_playlist.setAttribute("id", "black_track");
243 westley.insertBefore(blank_playlist, QDomNode());
244 QDomElement blank_track = m_doc.createElement("track");
245 blank_track.setAttribute("producer", "black_track");
246 blank_tractor.appendChild(blank_track);
248 QDomNodeList kdenlivetracks = m_doc.elementsByTagName("kdenlivetrack");
249 for (int i = 0; i < kdenlivetracks.count(); i++) {
250 blank_playlist = m_doc.createElement("playlist");
251 blank_playlist.setAttribute("id", "playlist" + QString::number(i));
252 westley.insertBefore(blank_playlist, QDomNode());
253 blank_track = m_doc.createElement("track");
254 blank_track.setAttribute("producer", "playlist" + QString::number(i));
255 blank_tractor.appendChild(blank_track);
256 if (kdenlivetracks.at(i).toElement().attribute("cliptype") == "Sound") {
257 blank_playlist.setAttribute("hide", "video");
258 blank_track.setAttribute("hide", "video");
261 } else for (int i = 0; i < max; i++) {
262 QDomNode n = playlists.at(i);
263 westley.insertBefore(n, QDomNode());
264 QDomElement pl = n.toElement();
265 QDomElement track = m_doc.createElement("track");
266 QString trackType = pl.attribute("hide");
267 if (!trackType.isEmpty())
268 track.setAttribute("hide", trackType);
269 QString playlist_id = pl.attribute("id");
270 if (playlist_id.isEmpty()) {
271 playlist_id = "black_track";
272 pl.setAttribute("id", playlist_id);
274 track.setAttribute("producer", playlist_id);
275 //tractor.appendChild(track);
276 #define KEEP_TRACK_ORDER 1
277 #ifdef KEEP_TRACK_ORDER
278 tractor.insertAfter(track, QDomNode());
280 // Insert the new track in an order that hopefully matches the 3 video, then 2 audio tracks of Kdenlive 0.7.0
281 // insertion sort - O( tracks*tracks )
282 // Note, this breaks _all_ transitions - but you can move them up and down afterwards.
283 QDomElement tractor_elem = tractor.toElement();
284 if (! tractor_elem.isNull()) {
285 QDomNodeList tracks = tractor_elem.elementsByTagName("track");
286 int size = tracks.size();
288 tractor.insertAfter(track, QDomNode());
290 bool inserted = false;
291 for (int i = 0; i < size; ++i) {
292 QDomElement track_elem = tracks.at(i).toElement();
293 if (track_elem.isNull()) {
294 tractor.insertAfter(track, QDomNode());
298 kDebug() << "playlist_id: " << playlist_id << " producer:" << track_elem.attribute("producer");
299 if (playlist_id < track_elem.attribute("producer")) {
300 tractor.insertBefore(track, track_elem);
306 // Reach here, no insertion, insert last
308 tractor.insertAfter(track, QDomNode());
312 kWarning() << "tractor was not a QDomElement";
313 tractor.insertAfter(track, QDomNode());
317 tractor.removeChild(multitrack);
319 // audio track mixing transitions should not be added to track view, so add required attribute
320 QDomNodeList transitions = m_doc.elementsByTagName("transition");
321 max = transitions.count();
322 for (int i = 0; i < max; i++) {
323 QDomElement tr = transitions.at(i).toElement();
324 if (tr.attribute("combine") == "1" && tr.attribute("mlt_service") == "mix") {
325 QDomElement property = m_doc.createElement("property");
326 property.setAttribute("name", "internal_added");
327 QDomText value = m_doc.createTextNode("237");
328 property.appendChild(value);
329 tr.appendChild(property);
330 property = m_doc.createElement("property");
331 property.setAttribute("name", "mlt_service");
332 value = m_doc.createTextNode("mix");
333 property.appendChild(value);
334 tr.appendChild(property);
336 // convert transition
337 QDomNamedNodeMap attrs = tr.attributes();
338 for (int j = 0; j < attrs.count(); j++) {
339 QString attrName = attrs.item(j).nodeName();
340 if (attrName != "in" && attrName != "out" && attrName != "id") {
341 QDomElement property = m_doc.createElement("property");
342 property.setAttribute("name", attrName);
343 QDomText value = m_doc.createTextNode(attrs.item(j).nodeValue());
344 property.appendChild(value);
345 tr.appendChild(property);
351 // move transitions after tracks
352 for (int i = 0; i < max; i++) {
353 tractor.insertAfter(transitions.at(0), QDomNode());
356 // Fix filters format
357 QDomNodeList entries = m_doc.elementsByTagName("entry");
358 max = entries.count();
359 for (int i = 0; i < max; i++) {
362 QDomNode m = entries.at(i).firstChild();
363 while (!m.isNull()) {
364 if (m.toElement().tagName() == "filter") {
365 QDomElement filt = m.toElement();
366 QDomNamedNodeMap attrs = filt.attributes();
367 QString current_id = filt.attribute("kdenlive_id");
368 if (current_id != last_id) {
370 last_id = current_id;
372 QDomElement e = m_doc.createElement("property");
373 e.setAttribute("name", "kdenlive_ix");
374 QDomText value = m_doc.createTextNode(QString::number(effectix));
375 e.appendChild(value);
377 for (int j = 0; j < attrs.count(); j++) {
378 QDomAttr a = attrs.item(j).toAttr();
380 kDebug() << " FILTER; adding :" << a.name() << ":" << a.value();
381 QDomElement e = m_doc.createElement("property");
382 e.setAttribute("name", a.name());
383 QDomText value = m_doc.createTextNode(a.value());
384 e.appendChild(value);
395 QDomNodeList filters = m_doc.elementsByTagName("filter");
396 max = filters.count();
399 for (int i = 0; i < max; i++) {
400 QDomElement filt = filters.at(i).toElement();
401 QDomNamedNodeMap attrs = filt.attributes();
402 QString current_id = filt.attribute("kdenlive_id");
403 if (current_id != last_id) {
405 last_id = current_id;
407 QDomElement e = m_doc.createElement("property");
408 e.setAttribute("name", "kdenlive_ix");
409 QDomText value = m_doc.createTextNode(QString::number(1));
410 e.appendChild(value);
412 for (int j = 0; j < attrs.count(); j++) {
413 QDomAttr a = attrs.item(j).toAttr();
415 kDebug() << " FILTER; adding :" << a.name() << ":" << a.value();
416 QDomElement e = m_doc.createElement("property");
417 e.setAttribute("name", a.name());
418 QDomText value = m_doc.createTextNode(a.value());
419 e.appendChild(value);
426 QDomNodeList producers = westley.toElement().elementsByTagName("producer");
427 max = producers.count();
428 for (int i = 0; i < max; i++) {
429 QDomElement prod = producers.at(i).toElement();
430 if (prod.attribute("mlt_service") == "framebuffer") {
431 QString slowmotionprod = prod.attribute("resource");
432 slowmotionprod.replace(':', '?');
433 kDebug() << "// FOUND WRONG SLOWMO, new: " << slowmotionprod;
434 prod.setAttribute("resource", slowmotionprod);
437 // move producers to correct place, markers to a global list, fix clip descriptions
438 QDomElement markers = m_doc.createElement("markers");
439 // This will get the xml producers:
440 producers = m_doc.elementsByTagName("producer");
441 max = producers.count();
442 for (int i = 0; i < max; i++) {
443 QDomElement prod = producers.at(0).toElement();
444 // add resource also as a property (to allow path correction in setNewResource())
445 // TODO: will it work with slowmotion? needs testing
446 /*if (!prod.attribute("resource").isEmpty()) {
447 QDomElement prop_resource = m_doc.createElement("property");
448 prop_resource.setAttribute("name", "resource");
449 QDomText resource = m_doc.createTextNode(prod.attribute("resource"));
450 prop_resource.appendChild(resource);
451 prod.appendChild(prop_resource);
453 QDomNode m = prod.firstChild();
455 if (m.toElement().tagName() == "markers") {
456 QDomNodeList prodchilds = m.childNodes();
457 int maxchild = prodchilds.count();
458 for (int k = 0; k < maxchild; k++) {
459 QDomElement mark = prodchilds.at(0).toElement();
460 mark.setAttribute("id", prod.attribute("id"));
461 markers.insertAfter(mark, QDomNode());
464 } else if (prod.attribute("type").toInt() == TEXT) {
465 // convert title clip
466 if (m.toElement().tagName() == "textclip") {
468 QDomElement titleclip = m.toElement();
469 QDomElement title = tdoc.createElement("kdenlivetitle");
470 tdoc.appendChild(title);
471 QDomNodeList objects = titleclip.childNodes();
472 int maxchild = objects.count();
473 for (int k = 0; k < maxchild; k++) {
475 QDomElement ob = objects.at(k).toElement();
476 if (ob.attribute("type") == "3") {
477 // text object - all of this goes into "xmldata"...
478 QDomElement item = tdoc.createElement("item");
479 item.setAttribute("z-index", ob.attribute("z"));
480 item.setAttribute("type", "QGraphicsTextItem");
481 QDomElement position = tdoc.createElement("position");
482 position.setAttribute("x", ob.attribute("x"));
483 position.setAttribute("y", ob.attribute("y"));
484 QDomElement content = tdoc.createElement("content");
485 content.setAttribute("font", ob.attribute("font_family"));
486 content.setAttribute("font-size", ob.attribute("font_size"));
487 content.setAttribute("font-bold", ob.attribute("bold"));
488 content.setAttribute("font-italic", ob.attribute("italic"));
489 content.setAttribute("font-underline", ob.attribute("underline"));
490 QString col = ob.attribute("color");
492 content.setAttribute("font-color", colorToString(c));
493 // todo: These fields are missing from the newly generated xmldata:
494 // transform, startviewport, endviewport, background
496 QDomText conttxt = tdoc.createTextNode(ob.attribute("text"));
497 content.appendChild(conttxt);
498 item.appendChild(position);
499 item.appendChild(content);
500 title.appendChild(item);
501 } else if (ob.attribute("type") == "5") {
503 QDomElement item = tdoc.createElement("item");
504 item.setAttribute("z-index", ob.attribute("z"));
505 item.setAttribute("type", "QGraphicsRectItem");
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 QString col = ob.attribute("color");
512 content.setAttribute("brushcolor", colorToString(c));
513 QString rect = "0,0,";
514 rect.append(ob.attribute("width"));
516 rect.append(ob.attribute("height"));
517 content.setAttribute("rect", rect);
518 item.appendChild(position);
519 item.appendChild(content);
520 title.appendChild(item);
523 prod.setAttribute("xmldata", tdoc.toString());
524 // mbd todo: This clearly does not work, as every title gets the same name - trying to leave it empty
525 // QStringList titleInfo = TitleWidget::getFreeTitleInfo(projectFolder());
526 // prod.setAttribute("titlename", titleInfo.at(0));
527 // prod.setAttribute("resource", titleInfo.at(1));
528 //kDebug()<<"TITLE DATA:\n"<<tdoc.toString();
530 } // End conversion of title clips.
532 } else if (m.isText()) {
533 QString comment = m.nodeValue();
534 if (!comment.isEmpty()) {
535 prod.setAttribute("description", comment);
540 int duration = prod.attribute("duration").toInt();
541 if (duration > 0) prod.setAttribute("out", QString::number(duration));
542 // The clip goes back in, but text clips should not go back in, at least not modified
543 westley.insertBefore(prod, QDomNode());
546 QDomNode westley0 = m_doc.elementsByTagName("westley").at(0);
547 if (!markers.firstChild().isNull()) westley0.appendChild(markers);
550 * Convert as much of the kdenlivedoc as possible. Use the producer in
551 * westley. First, remove the old stuff from westley, and add a new
552 * empty one. Also, track the max id in order to use it for the adding
555 int max_kproducer_id = 0;
556 westley0.removeChild(infoXmlNode);
557 QDomElement infoXml_new = m_doc.createElement("kdenlivedoc");
558 infoXml_new.setAttribute("profile", profile);
559 infoXml.setAttribute("position", startPos);
561 // Add all the producers that has a resource in westley
562 QDomElement westley_element = westley0.toElement();
563 if (westley_element.isNull()) {
564 kWarning() << "westley0 element in document was not a QDomElement - unable to add producers to new kdenlivedoc";
566 QDomNodeList wproducers = westley_element.elementsByTagName("producer");
567 int kmax = wproducers.count();
568 for (int i = 0; i < kmax; i++) {
569 QDomElement wproducer = wproducers.at(i).toElement();
570 if (wproducer.isNull()) {
571 kWarning() << "Found producer in westley0, that was not a QDomElement";
574 if (wproducer.attribute("id") == "black") continue;
575 // We have to do slightly different things, depending on the type
576 kDebug() << "Converting producer element with type" << wproducer.attribute("type");
577 if (wproducer.attribute("type").toInt() == TEXT) {
578 kDebug() << "Found TEXT element in producer" << endl;
579 QDomElement kproducer = wproducer.cloneNode(true).toElement();
580 kproducer.setTagName("kdenlive_producer");
581 infoXml_new.appendChild(kproducer);
583 * TODO: Perhaps needs some more changes here to
584 * "frequency", aspect ratio as a float, frame_size,
585 * channels, and later, resource and title name
588 QDomElement kproducer = m_doc.createElement("kdenlive_producer");
589 kproducer.setAttribute("id", wproducer.attribute("id"));
590 if (!wproducer.attribute("description").isEmpty())
591 kproducer.setAttribute("description", wproducer.attribute("description"));
592 kproducer.setAttribute("resource", wproducer.attribute("resource"));
593 kproducer.setAttribute("type", wproducer.attribute("type"));
594 // Testing fix for 358
595 if (!wproducer.attribute("aspect_ratio").isEmpty()) {
596 kproducer.setAttribute("aspect_ratio", wproducer.attribute("aspect_ratio"));
598 if (!wproducer.attribute("source_fps").isEmpty()) {
599 kproducer.setAttribute("fps", wproducer.attribute("source_fps"));
601 if (!wproducer.attribute("length").isEmpty()) {
602 kproducer.setAttribute("duration", wproducer.attribute("length"));
604 infoXml_new.appendChild(kproducer);
606 if (wproducer.attribute("id").toInt() > max_kproducer_id) {
607 max_kproducer_id = wproducer.attribute("id").toInt();
611 #define LOOKUP_FOLDER 1
614 * Look through all the folder elements of the old doc, for each folder,
615 * for each producer, get the id, look it up in the new doc, set the
616 * groupname and groupid. Note, this does not work at the moment - at
617 * least one folder shows up missing, and clips with no folder does not
620 //QDomElement infoXml_old = infoXmlNode.toElement();
621 if (!infoXml_old.isNull()) {
622 QDomNodeList folders = infoXml_old.elementsByTagName("folder");
623 int fsize = folders.size();
624 int groupId = max_kproducer_id + 1; // Start at +1 of max id of the kdenlive_producers
625 for (int i = 0; i < fsize; ++i) {
626 QDomElement folder = folders.at(i).toElement();
627 if (!folder.isNull()) {
628 QString groupName = folder.attribute("name");
629 kDebug() << "groupName: " << groupName << " with groupId: " << groupId;
630 QDomNodeList fproducers = folder.elementsByTagName("producer");
631 int psize = fproducers.size();
632 for (int j = 0; j < psize; ++j) {
633 QDomElement fproducer = fproducers.at(j).toElement();
634 if (!fproducer.isNull()) {
635 QString id = fproducer.attribute("id");
636 // This is not very effective, but compared to loading the clips, its a breeze
637 QDomNodeList kdenlive_producers = infoXml_new.elementsByTagName("kdenlive_producer");
638 int kpsize = kdenlive_producers.size();
639 for (int k = 0; k < kpsize; ++k) {
640 QDomElement kproducer = kdenlive_producers.at(k).toElement(); // Its an element for sure
641 if (id == kproducer.attribute("id")) {
642 // We do not check that it already is part of a folder
643 kproducer.setAttribute("groupid", groupId);
644 kproducer.setAttribute("groupname", groupName);
655 QDomNodeList elements = westley.childNodes();
656 max = elements.count();
657 for (int i = 0; i < max; i++) {
658 QDomElement prod = elements.at(0).toElement();
659 westley0.insertAfter(prod, QDomNode());
662 westley0.appendChild(infoXml_new);
664 westley0.removeChild(westley);
666 // adds <avfile /> information to <kdenlive_producer />
667 QDomNodeList kproducers = m_doc.elementsByTagName("kdenlive_producer");
668 QDomNodeList avfiles = infoXml_old.elementsByTagName("avfile");
669 kDebug() << "found" << avfiles.count() << "<avfile />s and" << kproducers.count() << "<kdenlive_producer />s";
670 for (int i = 0; i < avfiles.count(); ++i) {
671 QDomElement avfile = avfiles.at(i).toElement();
672 QDomElement kproducer;
674 kWarning() << "found an <avfile /> that is not a QDomElement";
676 QString id = avfile.attribute("id");
677 // this is horrible, must be rewritten, it's just for test
678 for (int j = 0; j < kproducers.count(); ++j) {
679 //kDebug() << "checking <kdenlive_producer /> with id" << kproducers.at(j).toElement().attribute("id");
680 if (kproducers.at(j).toElement().attribute("id") == id) {
681 kproducer = kproducers.at(j).toElement();
685 if (kproducer == QDomElement())
686 kWarning() << "no match for <avfile /> with id =" << id;
688 //kDebug() << "ready to set additional <avfile />'s attributes (id =" << id << ")";
689 kproducer.setAttribute("channels", avfile.attribute("channels"));
690 kproducer.setAttribute("duration", avfile.attribute("duration"));
691 kproducer.setAttribute("frame_size", avfile.attribute("width") + 'x' + avfile.attribute("height"));
692 kproducer.setAttribute("frequency", avfile.attribute("frequency"));
693 if (kproducer.attribute("description").isEmpty() && !avfile.attribute("description").isEmpty())
694 kproducer.setAttribute("description", avfile.attribute("description"));
698 infoXml = infoXml_new;
701 if (version <= 0.81) {
702 // Add the tracks information
703 QString tracksOrder = infoXml.attribute("tracks");
704 if (tracksOrder.isEmpty()) {
705 QDomNodeList tracks = m_doc.elementsByTagName("track");
706 for (int i = 0; i < tracks.count(); i++) {
707 QDomElement track = tracks.at(i).toElement();
708 if (track.attribute("producer") != "black_track") {
709 if (track.attribute("hide") == "video")
710 tracksOrder.append('a');
712 tracksOrder.append('v');
716 QDomElement tracksinfo = m_doc.createElement("tracksinfo");
717 for (int i = 0; i < tracksOrder.size(); i++) {
718 QDomElement trackinfo = m_doc.createElement("trackinfo");
719 if (tracksOrder.data()[i] == 'a') {
720 trackinfo.setAttribute("type", "audio");
721 trackinfo.setAttribute("blind", true);
723 trackinfo.setAttribute("blind", false);
724 trackinfo.setAttribute("mute", false);
725 trackinfo.setAttribute("locked", false);
726 tracksinfo.appendChild(trackinfo);
728 infoXml.appendChild(tracksinfo);
731 if (version <= 0.82) {
732 // Convert <westley />s in <mlt />s (MLT extreme makeover)
733 QDomNodeList westleyNodes = m_doc.elementsByTagName("westley");
734 for (int i = 0; i < westleyNodes.count(); i++) {
735 QDomElement westley = westleyNodes.at(i).toElement();
736 westley.setTagName("mlt");
740 if (version <= 0.83) {
741 // Replace point size with pixel size in text titles
742 if (m_doc.toString().contains("font-size")) {
743 KMessageBox::ButtonCode convert = KMessageBox::Continue;
744 QDomNodeList kproducerNodes = m_doc.elementsByTagName("kdenlive_producer");
745 for (int i = 0; i < kproducerNodes.count() && convert != KMessageBox::No; ++i) {
746 QDomElement kproducer = kproducerNodes.at(i).toElement();
747 if (kproducer.attribute("type").toInt() == TEXT) {
749 data.setContent(kproducer.attribute("xmldata"));
750 QDomNodeList items = data.firstChild().childNodes();
751 for (int j = 0; j < items.count() && convert != KMessageBox::No; ++j) {
752 if (items.at(j).attributes().namedItem("type").nodeValue() == "QGraphicsTextItem") {
753 QDomNamedNodeMap textProperties = items.at(j).namedItem("content").attributes();
754 if (textProperties.namedItem("font-pixel-size").isNull() && !textProperties.namedItem("font-size").isNull()) {
755 // Ask the user if he wants to convert
756 if (convert != KMessageBox::Yes && convert != KMessageBox::No)
757 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"));
758 if (convert == KMessageBox::Yes) {
760 font.setPointSize(textProperties.namedItem("font-size").nodeValue().toInt());
761 QDomElement content = items.at(j).namedItem("content").toElement();
762 content.setAttribute("font-pixel-size", QFontInfo(font).pixelSize());
763 content.removeAttribute("font-size");
764 kproducer.setAttribute("xmldata", data.toString());
766 * You may be tempted to delete the preview file
767 * to force its recreation: bad idea (see
768 * http://www.kdenlive.org/mantis/view.php?id=749)
778 // Fill the <documentproperties /> element
779 QDomElement docProperties = infoXml.firstChildElement("documentproperties");
780 if (docProperties.isNull()) {
781 docProperties = m_doc.createElement("documentproperties");
782 docProperties.setAttribute("zonein", infoXml.attribute("zonein"));
783 docProperties.setAttribute("zoneout", infoXml.attribute("zoneout"));
784 docProperties.setAttribute("zoom", infoXml.attribute("zoom"));
785 docProperties.setAttribute("position", infoXml.attribute("position"));
786 infoXml.appendChild(docProperties);
790 if (version <= 0.84) {
791 // update the title clips to use the new MLT kdenlivetitle producer
792 QDomNodeList kproducerNodes = m_doc.elementsByTagName("kdenlive_producer");
793 for (int i = 0; i < kproducerNodes.count(); ++i) {
794 QDomElement kproducer = kproducerNodes.at(i).toElement();
795 if (kproducer.attribute("type").toInt() == TEXT) {
796 QString data = kproducer.attribute("xmldata");
797 QString datafile = kproducer.attribute("resource");
798 if (!datafile.endsWith(".kdenlivetitle")) {
799 datafile = QString();
800 kproducer.setAttribute("resource", QString());
802 QString id = kproducer.attribute("id");
803 QDomNodeList mltproducers = m_doc.elementsByTagName("producer");
804 bool foundData = false;
805 bool foundResource = false;
806 bool foundService = false;
807 for (int j = 0; j < mltproducers.count(); j++) {
808 QDomElement wproducer = mltproducers.at(j).toElement();
809 if (wproducer.attribute("id") == id) {
810 QDomNodeList props = wproducer.childNodes();
811 for (int k = 0; k < props.count(); k++) {
812 if (props.at(k).toElement().attribute("name") == "xmldata") {
813 props.at(k).firstChild().setNodeValue(data);
815 } else if (props.at(k).toElement().attribute("name") == "mlt_service") {
816 props.at(k).firstChild().setNodeValue("kdenlivetitle");
818 } else if (props.at(k).toElement().attribute("name") == "resource") {
819 props.at(k).firstChild().setNodeValue(datafile);
820 foundResource = true;
824 QDomElement e = m_doc.createElement("property");
825 e.setAttribute("name", "xmldata");
826 QDomText value = m_doc.createTextNode(data);
827 e.appendChild(value);
828 wproducer.appendChild(e);
831 QDomElement e = m_doc.createElement("property");
832 e.setAttribute("name", "mlt_service");
833 QDomText value = m_doc.createTextNode("kdenlivetitle");
834 e.appendChild(value);
835 wproducer.appendChild(e);
837 if (!foundResource) {
838 QDomElement e = m_doc.createElement("property");
839 e.setAttribute("name", "resource");
840 QDomText value = m_doc.createTextNode(datafile);
841 e.appendChild(value);
842 wproducer.appendChild(e);
850 if (version <= 0.85) {
851 // update the LADSPA effects to use the new ladspa.id format instead of external xml file
852 QDomNodeList effectNodes = m_doc.elementsByTagName("filter");
853 for (int i = 0; i < effectNodes.count(); ++i) {
854 QDomElement effect = effectNodes.at(i).toElement();
855 if (EffectsList::property(effect, "mlt_service") == "ladspa") {
856 // Needs to be converted
857 QStringList info = getInfoFromEffectName(EffectsList::property(effect, "kdenlive_id"));
858 if (info.isEmpty()) continue;
859 // info contains the correct ladspa.id from kdenlive effect name, and a list of parameter's old and new names
860 EffectsList::setProperty(effect, "kdenlive_id", info.at(0));
861 EffectsList::setProperty(effect, "tag", info.at(0));
862 EffectsList::setProperty(effect, "mlt_service", info.at(0));
863 EffectsList::removeProperty(effect, "src");
864 for (int j = 1; j < info.size(); j++) {
865 QString value = EffectsList::property(effect, info.at(j).section('=', 0, 0));
866 if (!value.isEmpty()) {
867 // update parameter name
868 EffectsList::renameProperty(effect, info.at(j).section('=', 0, 0), info.at(j).section('=', 1, 1));
875 if (version <= 0.86) {
876 // Make sure we don't have avformat-novalidate producers, since it caused crashes
877 QDomNodeList producers = m_doc.elementsByTagName("producer");
878 int max = producers.count();
879 for (int i = 0; i < max; i++) {
880 QDomElement prod = producers.at(i).toElement();
881 if (EffectsList::property(prod, "mlt_service") == "avformat-novalidate")
882 EffectsList::setProperty(prod, "mlt_service", "avformat");
885 // 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
887 // Get profile info (width / height)
890 QDomElement profile = m_doc.firstChildElement("profile");
891 if (profile.isNull()) profile = infoXml.firstChildElement("profileinfo");
892 if (profile.isNull()) {
893 // could not find profile info, set PAL
898 profileWidth = profile.attribute("width").toInt();
899 profileHeight = profile.attribute("height").toInt();
901 QDomNodeList transitions = m_doc.elementsByTagName("transition");
902 max = transitions.count();
904 for (int i = 0; i < max; i++) {
905 QDomElement trans = transitions.at(i).toElement();
906 out = trans.attribute("out").toInt() - trans.attribute("in").toInt();
907 QString geom = EffectsList::property(trans, "geometry");
908 Mlt::Geometry *g = new Mlt::Geometry(geom.toUtf8().data(), out, profileWidth, profileHeight);
909 Mlt::GeometryItem item;
910 if (g->next_key(&item, out) == 0) {
911 // We have a keyframe just after last frame, try to move it to last frame
912 if (item.frame() == out + 1) {
916 EffectsList::setProperty(trans, "geometry", g->serialise());
923 if (version <= 0.87) {
924 if (!m_doc.firstChildElement("mlt").hasAttribute("LC_NUMERIC")) {
925 m_doc.firstChildElement("mlt").setAttribute("LC_NUMERIC", "C");
929 // The document has been converted: mark it as modified
930 infoXml.setAttribute("version", currentVersion);
935 QStringList DocumentValidator::getInfoFromEffectName(const QString oldName)
938 // Returns a list to convert old Kdenlive ladspa effects
939 if (oldName == "pitch_shift") {
940 info << "ladspa.1433";
943 else if (oldName == "vinyl") {
944 info << "ladspa.1905";
951 else if (oldName == "room_reverb") {
952 info << "ladspa.1216";
957 else if (oldName == "reverb") {
958 info << "ladspa.1423";
962 else if (oldName == "rate_scale") {
963 info << "ladspa.1417";
966 else if (oldName == "pitch_scale") {
967 info << "ladspa.1193";
970 else if (oldName == "phaser") {
971 info << "ladspa.1217";
974 info << "feedback=2";
977 else if (oldName == "limiter") {
978 info << "ladspa.1913";
983 else if (oldName == "equalizer_15") {
984 info << "ladspa.1197";
1001 else if (oldName == "equalizer") {
1002 info << "ladspa.1901";
1004 info << "midgain=1";
1007 else if (oldName == "declipper") {
1008 info << "ladspa.1195";
1013 QString DocumentValidator::colorToString(const QColor& c)
1015 QString ret = "%1,%2,%3,%4";
1016 ret = ret.arg(c.red()).arg(c.green()).arg(c.blue()).arg(c.alpha());
1020 bool DocumentValidator::isProject() const
1022 QDomNode infoXmlNode = m_doc.elementsByTagName("kdenlivedoc").at(0);
1023 return !infoXmlNode.isNull();
1026 bool DocumentValidator::isModified() const
1031 void DocumentValidator::updateEffects()
1033 // WARNING: order by findDirs will determine which js file to use (in case multiple scripts for the same filter exist)
1034 QMap <QString, KUrl> paths;
1035 #if QT_VERSION >= 0x040700
1036 QMap <QString, QScriptProgram> scripts;
1038 QMap <QString, QString> scripts;
1040 QStringList directories = KGlobal::dirs()->findDirs("appdata", "effects/update");
1041 foreach (const QString &directoryName, directories) {
1042 QDir directory(directoryName);
1043 QStringList fileList = directory.entryList(QStringList() << "*.js", QDir::Files);
1044 foreach (const QString &fileName, fileList) {
1045 QString identifier = fileName;
1046 // remove extension (".js")
1048 paths.insert(identifier, KUrl(directoryName + fileName));
1052 QDomNodeList effects = m_doc.elementsByTagName("filter");
1053 int max = effects.count();
1054 QStringList safeEffects;
1055 for(int i = 0; i < max; ++i) {
1056 QDomElement effect = effects.at(i).toElement();
1057 QString effectId = EffectsList::property(effect, "kdenlive_id");
1058 if (safeEffects.contains(effectId)) {
1059 // Do not check the same effect twice if it is at the correct version
1060 // (assume we don't have different versions of the same effect in a project file)
1063 QString effectTag = EffectsList::property(effect, "tag");
1064 QString effectVersionStr = EffectsList::property(effect, "version");
1065 double effectVersion = effectVersionStr.isNull() ? -1 : effectVersionStr.toDouble();
1067 QDomElement effectDescr = MainWindow::customEffects.getEffectByTag(QString(), effectId);
1068 if (effectDescr.isNull()) {
1069 effectDescr = MainWindow::videoEffects.getEffectByTag(effectTag, effectId);
1071 if (effectDescr.isNull()) {
1072 effectDescr = MainWindow::audioEffects.getEffectByTag(effectTag, effectId);
1074 if (!effectDescr.isNull()) {
1075 double serviceVersion = -1;
1076 QDomElement serviceVersionElem = effectDescr.firstChildElement("version");
1077 if (!serviceVersionElem.isNull()) {
1078 serviceVersion = serviceVersionElem.text().toDouble();
1080 if (serviceVersion != effectVersion && paths.contains(effectId)) {
1081 if (!scripts.contains(effectId)) {
1082 QFile scriptFile(paths.value(effectId).path());
1083 if (!scriptFile.open(QIODevice::ReadOnly)) {
1086 #if QT_VERSION >= 0x040700
1087 QScriptProgram scriptProgram(scriptFile.readAll());
1089 QString scriptProgram = scriptFile.readAll();
1092 scripts.insert(effectId, scriptProgram);
1095 QScriptEngine scriptEngine;
1096 scriptEngine.importExtension("qt.core");
1097 scriptEngine.importExtension("qt.xml");
1098 scriptEngine.evaluate(scripts.value(effectId));
1099 QScriptValue updateRules = scriptEngine.globalObject().property("update");
1100 if (!updateRules.isValid())
1102 if (updateRules.isFunction()) {
1103 QDomDocument scriptDoc;
1104 scriptDoc.appendChild(scriptDoc.importNode(effect, true));
1106 QString effectString = updateRules.call(QScriptValue(), QScriptValueList() << serviceVersion << effectVersion << scriptDoc.toString()).toString();
1108 if (!effectString.isEmpty() && !scriptEngine.hasUncaughtException()) {
1109 scriptDoc.setContent(effectString);
1110 QDomNode updatedEffect = effect.ownerDocument().importNode(scriptDoc.documentElement(), true);
1111 effect.parentNode().replaceChild(updatedEffect, effect);
1115 m_modified = updateEffectParameters(effect.childNodes(), &updateRules, serviceVersion, effectVersion);
1118 // set version number since MLT won't change it (only initially set it)
1119 QDomElement versionElem = effect.firstChildElement("version");
1120 if (EffectsList::property(effect, "version").isNull()) {
1121 versionElem = effect.ownerDocument().createTextNode(QLocale().toString(serviceVersion)).toElement();
1122 versionElem.setTagName("property");
1123 versionElem.setAttribute("name", "version");
1124 effect.appendChild(versionElem);
1126 EffectsList::setProperty(effect, "version", QLocale().toString(serviceVersion));
1129 else safeEffects.append(effectId);
1134 bool DocumentValidator::updateEffectParameters(QDomNodeList parameters, const QScriptValue* updateRules, const double serviceVersion, const double effectVersion)
1136 bool updated = false;
1137 bool isDowngrade = serviceVersion < effectVersion;
1138 for (int i = 0; i < parameters.count(); ++i) {
1139 QDomElement parameter = parameters.at(i).toElement();
1140 QScriptValue rules = updateRules->property(parameter.attribute("name"));
1141 if (rules.isValid() && rules.isArray()) {
1142 int rulesCount = rules.property("length").toInt32();
1144 // start with the highest version and downgrade step by step
1145 for (int j = rulesCount - 1; j >= 0; --j) {
1146 double version = rules.property(j).property(0).toNumber();
1147 if (version <= effectVersion && version > serviceVersion) {
1148 parameter.firstChild().setNodeValue(rules.property(j).property(1).call(QScriptValue(), QScriptValueList() << parameter.text() << isDowngrade).toString());
1153 for (int j = 0; j < rulesCount; ++j) {
1154 double version = rules.property(j).property(0).toNumber();
1155 if (version > effectVersion && version <= serviceVersion) {
1156 parameter.firstChild().setNodeValue(rules.property(j).property(1).call(QScriptValue(), QScriptValueList() << parameter.text() << isDowngrade).toString());