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 (newLocale.isEmpty()) {
83 // Requested locale not available, ask for install
84 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")));
87 if (separator != documentLocale.decimalPoint()) {
88 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();
107 double version = documentLocale.toDouble(kdenliveDoc.attribute("version"), &ok);
109 // Could not parse version number, there is probably a conflict in decimal separator
110 QLocale tempLocale = QLocale(mlt.attribute("LC_NUMERIC"));
111 version = tempLocale.toDouble(kdenliveDoc.attribute("version"), &ok);
112 if (!ok) version = kdenliveDoc.attribute("version").toDouble(&ok);
113 if (!ok) kDebug()<<"// CANNOT PARSE VERSION NUMBER, ERROR!";
116 // Upgrade the document to the latest version
117 if (!upgrade(version, currentVersion))
121 * Check the syntax (this will be replaced by XSD validation with Qt 4.6)
122 * and correct some errors
125 // Return (or create) the tractor
126 QDomElement tractor = mlt.firstChildElement("tractor");
127 if (tractor.isNull()) {
129 tractor = m_doc.createElement("tractor");
130 tractor.setAttribute("global_feed", "1");
131 tractor.setAttribute("in", "0");
132 tractor.setAttribute("out", "-1");
133 tractor.setAttribute("id", "maintractor");
134 mlt.appendChild(tractor);
138 * Make sure at least one track exists, and they're equal in number to
139 * to the maximum between MLT and Kdenlive playlists and tracks
141 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
142 int tracksMax = playlists.count() - 1; // Remove the black track
143 QDomNodeList tracks = tractor.elementsByTagName("track");
144 tracksMax = qMax(tracks.count() - 1, tracksMax);
145 QDomNodeList tracksinfo = kdenliveDoc.elementsByTagName("trackinfo");
146 tracksMax = qMax(tracksinfo.count(), tracksMax);
147 tracksMax = qMax(1, tracksMax); // Force existance of one track
148 if (playlists.count() - 1 < tracksMax ||
149 tracks.count() - 1 < tracksMax ||
150 tracksinfo.count() < tracksMax) {
151 kDebug() << "//// WARNING, PROJECT IS CORRUPTED, MISSING TRACK";
154 // use the MLT tracks as reference
155 if (tracks.count() - 1 < tracksMax) {
156 // Looks like one MLT track is missing, remove the extra Kdenlive track if there is one.
157 if (tracksinfo.count() != tracks.count() - 1) {
158 // The Kdenlive tracks are not ok, clear and rebuild them
159 QDomNode tinfo = kdenliveDoc.firstChildElement("tracksinfo");
160 QDomNode tnode = tinfo.firstChild();
161 while (!tnode.isNull()) {
162 tinfo.removeChild(tnode);
163 tnode = tinfo.firstChild();
166 for (int i = 1; i < tracks.count(); i++) {
167 QString hide = tracks.at(i).toElement().attribute("hide");
168 QDomElement newTrack = m_doc.createElement("trackinfo");
169 if (hide == "video") {
171 newTrack.setAttribute("type", "audio");
172 newTrack.setAttribute("blind", 1);
173 newTrack.setAttribute("mute", 0);
174 newTrack.setAttribute("lock", 0);
176 newTrack.setAttribute("blind", 0);
177 newTrack.setAttribute("mute", 0);
178 newTrack.setAttribute("lock", 0);
180 tinfo.appendChild(newTrack);
185 if (playlists.count() - 1 < tracksMax) {
186 difference = tracksMax - (playlists.count() - 1);
187 for (int i = 0; i < difference; ++i) {
188 QDomElement playlist = m_doc.createElement("playlist");
189 mlt.appendChild(playlist);
192 if (tracks.count() - 1 < tracksMax) {
193 difference = tracksMax - (tracks.count() - 1);
194 for (int i = 0; i < difference; ++i) {
195 QDomElement track = m_doc.createElement("track");
196 tractor.appendChild(track);
199 if (tracksinfo.count() < tracksMax) {
200 QDomElement tracksinfoElm = kdenliveDoc.firstChildElement("tracksinfo");
201 if (tracksinfoElm.isNull()) {
202 tracksinfoElm = m_doc.createElement("tracksinfo");
203 kdenliveDoc.appendChild(tracksinfoElm);
205 difference = tracksMax - tracksinfo.count();
206 for (int i = 0; i < difference; ++i) {
207 QDomElement trackinfo = m_doc.createElement("trackinfo");
208 trackinfo.setAttribute("mute", "0");
209 trackinfo.setAttribute("locked", "0");
210 tracksinfoElm.appendChild(trackinfo);
215 // TODO: check the tracks references
216 // TODO: check internal mix transitions
225 bool DocumentValidator::upgrade(double version, const double currentVersion)
227 kDebug() << "Opening a document with version " << version;
229 // No conversion needed
230 if (version == currentVersion) {
234 // The document is too new
235 if (version > currentVersion) {
236 kDebug() << "Unable to open document with version " << version;
237 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"));
241 // Unsupported document versions
242 if (version == 0.5 || version == 0.7) {
243 kDebug() << "Unable to open document with version " << version;
244 KMessageBox::sorry(kapp->activeWindow(), i18n("This project type is unsupported (version %1) and can't be loaded.", version), i18n("Unable to open project"));
249 QDomNode infoXmlNode = m_doc.elementsByTagName("kdenlivedoc").at(0);
250 QDomElement infoXml = infoXmlNode.toElement();
251 infoXml.setAttribute("upgraded", "1");
253 if (version <= 0.6) {
254 QDomElement infoXml_old = infoXmlNode.cloneNode(true).toElement(); // Needed for folders
255 QDomNode westley = m_doc.elementsByTagName("westley").at(1);
256 QDomNode tractor = m_doc.elementsByTagName("tractor").at(0);
257 QDomNode multitrack = m_doc.elementsByTagName("multitrack").at(0);
258 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
260 QDomNode props = m_doc.elementsByTagName("properties").at(0).toElement();
261 QString profile = props.toElement().attribute("videoprofile");
262 int startPos = props.toElement().attribute("timeline_position").toInt();
263 if (profile == "dv_wide")
264 profile = "dv_pal_wide";
266 // move playlists outside of tractor and add the tracks instead
267 int max = playlists.count();
268 if (westley.isNull()) {
269 westley = m_doc.createElement("westley");
270 m_doc.documentElement().appendChild(westley);
272 if (tractor.isNull()) {
273 kDebug() << "// NO MLT PLAYLIST, building empty one";
274 QDomElement blank_tractor = m_doc.createElement("tractor");
275 westley.appendChild(blank_tractor);
276 QDomElement blank_playlist = m_doc.createElement("playlist");
277 blank_playlist.setAttribute("id", "black_track");
278 westley.insertBefore(blank_playlist, QDomNode());
279 QDomElement blank_track = m_doc.createElement("track");
280 blank_track.setAttribute("producer", "black_track");
281 blank_tractor.appendChild(blank_track);
283 QDomNodeList kdenlivetracks = m_doc.elementsByTagName("kdenlivetrack");
284 for (int i = 0; i < kdenlivetracks.count(); i++) {
285 blank_playlist = m_doc.createElement("playlist");
286 blank_playlist.setAttribute("id", "playlist" + QString::number(i));
287 westley.insertBefore(blank_playlist, QDomNode());
288 blank_track = m_doc.createElement("track");
289 blank_track.setAttribute("producer", "playlist" + QString::number(i));
290 blank_tractor.appendChild(blank_track);
291 if (kdenlivetracks.at(i).toElement().attribute("cliptype") == "Sound") {
292 blank_playlist.setAttribute("hide", "video");
293 blank_track.setAttribute("hide", "video");
296 } else for (int i = 0; i < max; i++) {
297 QDomNode n = playlists.at(i);
298 westley.insertBefore(n, QDomNode());
299 QDomElement pl = n.toElement();
300 QDomElement track = m_doc.createElement("track");
301 QString trackType = pl.attribute("hide");
302 if (!trackType.isEmpty())
303 track.setAttribute("hide", trackType);
304 QString playlist_id = pl.attribute("id");
305 if (playlist_id.isEmpty()) {
306 playlist_id = "black_track";
307 pl.setAttribute("id", playlist_id);
309 track.setAttribute("producer", playlist_id);
310 //tractor.appendChild(track);
311 #define KEEP_TRACK_ORDER 1
312 #ifdef KEEP_TRACK_ORDER
313 tractor.insertAfter(track, QDomNode());
315 // Insert the new track in an order that hopefully matches the 3 video, then 2 audio tracks of Kdenlive 0.7.0
316 // insertion sort - O( tracks*tracks )
317 // Note, this breaks _all_ transitions - but you can move them up and down afterwards.
318 QDomElement tractor_elem = tractor.toElement();
319 if (! tractor_elem.isNull()) {
320 QDomNodeList tracks = tractor_elem.elementsByTagName("track");
321 int size = tracks.size();
323 tractor.insertAfter(track, QDomNode());
325 bool inserted = false;
326 for (int i = 0; i < size; ++i) {
327 QDomElement track_elem = tracks.at(i).toElement();
328 if (track_elem.isNull()) {
329 tractor.insertAfter(track, QDomNode());
333 kDebug() << "playlist_id: " << playlist_id << " producer:" << track_elem.attribute("producer");
334 if (playlist_id < track_elem.attribute("producer")) {
335 tractor.insertBefore(track, track_elem);
341 // Reach here, no insertion, insert last
343 tractor.insertAfter(track, QDomNode());
347 kWarning() << "tractor was not a QDomElement";
348 tractor.insertAfter(track, QDomNode());
352 tractor.removeChild(multitrack);
354 // audio track mixing transitions should not be added to track view, so add required attribute
355 QDomNodeList transitions = m_doc.elementsByTagName("transition");
356 max = transitions.count();
357 for (int i = 0; i < max; i++) {
358 QDomElement tr = transitions.at(i).toElement();
359 if (tr.attribute("combine") == "1" && tr.attribute("mlt_service") == "mix") {
360 QDomElement property = m_doc.createElement("property");
361 property.setAttribute("name", "internal_added");
362 QDomText value = m_doc.createTextNode("237");
363 property.appendChild(value);
364 tr.appendChild(property);
365 property = m_doc.createElement("property");
366 property.setAttribute("name", "mlt_service");
367 value = m_doc.createTextNode("mix");
368 property.appendChild(value);
369 tr.appendChild(property);
371 // convert transition
372 QDomNamedNodeMap attrs = tr.attributes();
373 for (int j = 0; j < attrs.count(); j++) {
374 QString attrName = attrs.item(j).nodeName();
375 if (attrName != "in" && attrName != "out" && attrName != "id") {
376 QDomElement property = m_doc.createElement("property");
377 property.setAttribute("name", attrName);
378 QDomText value = m_doc.createTextNode(attrs.item(j).nodeValue());
379 property.appendChild(value);
380 tr.appendChild(property);
386 // move transitions after tracks
387 for (int i = 0; i < max; i++) {
388 tractor.insertAfter(transitions.at(0), QDomNode());
391 // Fix filters format
392 QDomNodeList entries = m_doc.elementsByTagName("entry");
393 max = entries.count();
394 for (int i = 0; i < max; i++) {
397 QDomNode m = entries.at(i).firstChild();
398 while (!m.isNull()) {
399 if (m.toElement().tagName() == "filter") {
400 QDomElement filt = m.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(effectix));
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);
430 QDomNodeList filters = m_doc.elementsByTagName("filter");
431 max = filters.count();
434 for (int i = 0; i < max; i++) {
435 QDomElement filt = filters.at(i).toElement();
436 QDomNamedNodeMap attrs = filt.attributes();
437 QString current_id = filt.attribute("kdenlive_id");
438 if (current_id != last_id) {
440 last_id = current_id;
442 QDomElement e = m_doc.createElement("property");
443 e.setAttribute("name", "kdenlive_ix");
444 QDomText value = m_doc.createTextNode(QString::number(1));
445 e.appendChild(value);
447 for (int j = 0; j < attrs.count(); j++) {
448 QDomAttr a = attrs.item(j).toAttr();
450 kDebug() << " FILTER; adding :" << a.name() << ":" << a.value();
451 QDomElement e = m_doc.createElement("property");
452 e.setAttribute("name", a.name());
453 QDomText value = m_doc.createTextNode(a.value());
454 e.appendChild(value);
461 QDomNodeList producers = westley.toElement().elementsByTagName("producer");
462 max = producers.count();
463 for (int i = 0; i < max; i++) {
464 QDomElement prod = producers.at(i).toElement();
465 if (prod.attribute("mlt_service") == "framebuffer") {
466 QString slowmotionprod = prod.attribute("resource");
467 slowmotionprod.replace(':', '?');
468 kDebug() << "// FOUND WRONG SLOWMO, new: " << slowmotionprod;
469 prod.setAttribute("resource", slowmotionprod);
472 // move producers to correct place, markers to a global list, fix clip descriptions
473 QDomElement markers = m_doc.createElement("markers");
474 // This will get the xml producers:
475 producers = m_doc.elementsByTagName("producer");
476 max = producers.count();
477 for (int i = 0; i < max; i++) {
478 QDomElement prod = producers.at(0).toElement();
479 // add resource also as a property (to allow path correction in setNewResource())
480 // TODO: will it work with slowmotion? needs testing
481 /*if (!prod.attribute("resource").isEmpty()) {
482 QDomElement prop_resource = m_doc.createElement("property");
483 prop_resource.setAttribute("name", "resource");
484 QDomText resource = m_doc.createTextNode(prod.attribute("resource"));
485 prop_resource.appendChild(resource);
486 prod.appendChild(prop_resource);
488 QDomNode m = prod.firstChild();
490 if (m.toElement().tagName() == "markers") {
491 QDomNodeList prodchilds = m.childNodes();
492 int maxchild = prodchilds.count();
493 for (int k = 0; k < maxchild; k++) {
494 QDomElement mark = prodchilds.at(0).toElement();
495 mark.setAttribute("id", prod.attribute("id"));
496 markers.insertAfter(mark, QDomNode());
499 } else if (prod.attribute("type").toInt() == TEXT) {
500 // convert title clip
501 if (m.toElement().tagName() == "textclip") {
503 QDomElement titleclip = m.toElement();
504 QDomElement title = tdoc.createElement("kdenlivetitle");
505 tdoc.appendChild(title);
506 QDomNodeList objects = titleclip.childNodes();
507 int maxchild = objects.count();
508 for (int k = 0; k < maxchild; k++) {
510 QDomElement ob = objects.at(k).toElement();
511 if (ob.attribute("type") == "3") {
512 // text object - all of this goes into "xmldata"...
513 QDomElement item = tdoc.createElement("item");
514 item.setAttribute("z-index", ob.attribute("z"));
515 item.setAttribute("type", "QGraphicsTextItem");
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 content.setAttribute("font", ob.attribute("font_family"));
521 content.setAttribute("font-size", ob.attribute("font_size"));
522 content.setAttribute("font-bold", ob.attribute("bold"));
523 content.setAttribute("font-italic", ob.attribute("italic"));
524 content.setAttribute("font-underline", ob.attribute("underline"));
525 QString col = ob.attribute("color");
527 content.setAttribute("font-color", colorToString(c));
528 // todo: These fields are missing from the newly generated xmldata:
529 // transform, startviewport, endviewport, background
531 QDomText conttxt = tdoc.createTextNode(ob.attribute("text"));
532 content.appendChild(conttxt);
533 item.appendChild(position);
534 item.appendChild(content);
535 title.appendChild(item);
536 } else if (ob.attribute("type") == "5") {
538 QDomElement item = tdoc.createElement("item");
539 item.setAttribute("z-index", ob.attribute("z"));
540 item.setAttribute("type", "QGraphicsRectItem");
541 QDomElement position = tdoc.createElement("position");
542 position.setAttribute("x", ob.attribute("x"));
543 position.setAttribute("y", ob.attribute("y"));
544 QDomElement content = tdoc.createElement("content");
545 QString col = ob.attribute("color");
547 content.setAttribute("brushcolor", colorToString(c));
548 QString rect = "0,0,";
549 rect.append(ob.attribute("width"));
551 rect.append(ob.attribute("height"));
552 content.setAttribute("rect", rect);
553 item.appendChild(position);
554 item.appendChild(content);
555 title.appendChild(item);
558 prod.setAttribute("xmldata", tdoc.toString());
559 // mbd todo: This clearly does not work, as every title gets the same name - trying to leave it empty
560 // QStringList titleInfo = TitleWidget::getFreeTitleInfo(projectFolder());
561 // prod.setAttribute("titlename", titleInfo.at(0));
562 // prod.setAttribute("resource", titleInfo.at(1));
563 //kDebug()<<"TITLE DATA:\n"<<tdoc.toString();
565 } // End conversion of title clips.
567 } else if (m.isText()) {
568 QString comment = m.nodeValue();
569 if (!comment.isEmpty()) {
570 prod.setAttribute("description", comment);
575 int duration = prod.attribute("duration").toInt();
576 if (duration > 0) prod.setAttribute("out", QString::number(duration));
577 // The clip goes back in, but text clips should not go back in, at least not modified
578 westley.insertBefore(prod, QDomNode());
581 QDomNode westley0 = m_doc.elementsByTagName("westley").at(0);
582 if (!markers.firstChild().isNull()) westley0.appendChild(markers);
585 * Convert as much of the kdenlivedoc as possible. Use the producer in
586 * westley. First, remove the old stuff from westley, and add a new
587 * empty one. Also, track the max id in order to use it for the adding
590 int max_kproducer_id = 0;
591 westley0.removeChild(infoXmlNode);
592 QDomElement infoXml_new = m_doc.createElement("kdenlivedoc");
593 infoXml_new.setAttribute("profile", profile);
594 infoXml.setAttribute("position", startPos);
596 // Add all the producers that has a resource in westley
597 QDomElement westley_element = westley0.toElement();
598 if (westley_element.isNull()) {
599 kWarning() << "westley0 element in document was not a QDomElement - unable to add producers to new kdenlivedoc";
601 QDomNodeList wproducers = westley_element.elementsByTagName("producer");
602 int kmax = wproducers.count();
603 for (int i = 0; i < kmax; i++) {
604 QDomElement wproducer = wproducers.at(i).toElement();
605 if (wproducer.isNull()) {
606 kWarning() << "Found producer in westley0, that was not a QDomElement";
609 if (wproducer.attribute("id") == "black") continue;
610 // We have to do slightly different things, depending on the type
611 kDebug() << "Converting producer element with type" << wproducer.attribute("type");
612 if (wproducer.attribute("type").toInt() == TEXT) {
613 kDebug() << "Found TEXT element in producer" << endl;
614 QDomElement kproducer = wproducer.cloneNode(true).toElement();
615 kproducer.setTagName("kdenlive_producer");
616 infoXml_new.appendChild(kproducer);
618 * TODO: Perhaps needs some more changes here to
619 * "frequency", aspect ratio as a float, frame_size,
620 * channels, and later, resource and title name
623 QDomElement kproducer = m_doc.createElement("kdenlive_producer");
624 kproducer.setAttribute("id", wproducer.attribute("id"));
625 if (!wproducer.attribute("description").isEmpty())
626 kproducer.setAttribute("description", wproducer.attribute("description"));
627 kproducer.setAttribute("resource", wproducer.attribute("resource"));
628 kproducer.setAttribute("type", wproducer.attribute("type"));
629 // Testing fix for 358
630 if (!wproducer.attribute("aspect_ratio").isEmpty()) {
631 kproducer.setAttribute("aspect_ratio", wproducer.attribute("aspect_ratio"));
633 if (!wproducer.attribute("source_fps").isEmpty()) {
634 kproducer.setAttribute("fps", wproducer.attribute("source_fps"));
636 if (!wproducer.attribute("length").isEmpty()) {
637 kproducer.setAttribute("duration", wproducer.attribute("length"));
639 infoXml_new.appendChild(kproducer);
641 if (wproducer.attribute("id").toInt() > max_kproducer_id) {
642 max_kproducer_id = wproducer.attribute("id").toInt();
646 #define LOOKUP_FOLDER 1
649 * Look through all the folder elements of the old doc, for each folder,
650 * for each producer, get the id, look it up in the new doc, set the
651 * groupname and groupid. Note, this does not work at the moment - at
652 * least one folder shows up missing, and clips with no folder does not
655 //QDomElement infoXml_old = infoXmlNode.toElement();
656 if (!infoXml_old.isNull()) {
657 QDomNodeList folders = infoXml_old.elementsByTagName("folder");
658 int fsize = folders.size();
659 int groupId = max_kproducer_id + 1; // Start at +1 of max id of the kdenlive_producers
660 for (int i = 0; i < fsize; ++i) {
661 QDomElement folder = folders.at(i).toElement();
662 if (!folder.isNull()) {
663 QString groupName = folder.attribute("name");
664 kDebug() << "groupName: " << groupName << " with groupId: " << groupId;
665 QDomNodeList fproducers = folder.elementsByTagName("producer");
666 int psize = fproducers.size();
667 for (int j = 0; j < psize; ++j) {
668 QDomElement fproducer = fproducers.at(j).toElement();
669 if (!fproducer.isNull()) {
670 QString id = fproducer.attribute("id");
671 // This is not very effective, but compared to loading the clips, its a breeze
672 QDomNodeList kdenlive_producers = infoXml_new.elementsByTagName("kdenlive_producer");
673 int kpsize = kdenlive_producers.size();
674 for (int k = 0; k < kpsize; ++k) {
675 QDomElement kproducer = kdenlive_producers.at(k).toElement(); // Its an element for sure
676 if (id == kproducer.attribute("id")) {
677 // We do not check that it already is part of a folder
678 kproducer.setAttribute("groupid", groupId);
679 kproducer.setAttribute("groupname", groupName);
690 QDomNodeList elements = westley.childNodes();
691 max = elements.count();
692 for (int i = 0; i < max; i++) {
693 QDomElement prod = elements.at(0).toElement();
694 westley0.insertAfter(prod, QDomNode());
697 westley0.appendChild(infoXml_new);
699 westley0.removeChild(westley);
701 // adds <avfile /> information to <kdenlive_producer />
702 QDomNodeList kproducers = m_doc.elementsByTagName("kdenlive_producer");
703 QDomNodeList avfiles = infoXml_old.elementsByTagName("avfile");
704 kDebug() << "found" << avfiles.count() << "<avfile />s and" << kproducers.count() << "<kdenlive_producer />s";
705 for (int i = 0; i < avfiles.count(); ++i) {
706 QDomElement avfile = avfiles.at(i).toElement();
707 QDomElement kproducer;
709 kWarning() << "found an <avfile /> that is not a QDomElement";
711 QString id = avfile.attribute("id");
712 // this is horrible, must be rewritten, it's just for test
713 for (int j = 0; j < kproducers.count(); ++j) {
714 //kDebug() << "checking <kdenlive_producer /> with id" << kproducers.at(j).toElement().attribute("id");
715 if (kproducers.at(j).toElement().attribute("id") == id) {
716 kproducer = kproducers.at(j).toElement();
720 if (kproducer == QDomElement())
721 kWarning() << "no match for <avfile /> with id =" << id;
723 //kDebug() << "ready to set additional <avfile />'s attributes (id =" << id << ")";
724 kproducer.setAttribute("channels", avfile.attribute("channels"));
725 kproducer.setAttribute("duration", avfile.attribute("duration"));
726 kproducer.setAttribute("frame_size", avfile.attribute("width") + 'x' + avfile.attribute("height"));
727 kproducer.setAttribute("frequency", avfile.attribute("frequency"));
728 if (kproducer.attribute("description").isEmpty() && !avfile.attribute("description").isEmpty())
729 kproducer.setAttribute("description", avfile.attribute("description"));
733 infoXml = infoXml_new;
736 if (version <= 0.81) {
737 // Add the tracks information
738 QString tracksOrder = infoXml.attribute("tracks");
739 if (tracksOrder.isEmpty()) {
740 QDomNodeList tracks = m_doc.elementsByTagName("track");
741 for (int i = 0; i < tracks.count(); i++) {
742 QDomElement track = tracks.at(i).toElement();
743 if (track.attribute("producer") != "black_track") {
744 if (track.attribute("hide") == "video")
745 tracksOrder.append('a');
747 tracksOrder.append('v');
751 QDomElement tracksinfo = m_doc.createElement("tracksinfo");
752 for (int i = 0; i < tracksOrder.size(); i++) {
753 QDomElement trackinfo = m_doc.createElement("trackinfo");
754 if (tracksOrder.data()[i] == 'a') {
755 trackinfo.setAttribute("type", "audio");
756 trackinfo.setAttribute("blind", true);
758 trackinfo.setAttribute("blind", false);
759 trackinfo.setAttribute("mute", false);
760 trackinfo.setAttribute("locked", false);
761 tracksinfo.appendChild(trackinfo);
763 infoXml.appendChild(tracksinfo);
766 if (version <= 0.82) {
767 // Convert <westley />s in <mlt />s (MLT extreme makeover)
768 QDomNodeList westleyNodes = m_doc.elementsByTagName("westley");
769 for (int i = 0; i < westleyNodes.count(); i++) {
770 QDomElement westley = westleyNodes.at(i).toElement();
771 westley.setTagName("mlt");
775 if (version <= 0.83) {
776 // Replace point size with pixel size in text titles
777 if (m_doc.toString().contains("font-size")) {
778 KMessageBox::ButtonCode convert = KMessageBox::Continue;
779 QDomNodeList kproducerNodes = m_doc.elementsByTagName("kdenlive_producer");
780 for (int i = 0; i < kproducerNodes.count() && convert != KMessageBox::No; ++i) {
781 QDomElement kproducer = kproducerNodes.at(i).toElement();
782 if (kproducer.attribute("type").toInt() == TEXT) {
784 data.setContent(kproducer.attribute("xmldata"));
785 QDomNodeList items = data.firstChild().childNodes();
786 for (int j = 0; j < items.count() && convert != KMessageBox::No; ++j) {
787 if (items.at(j).attributes().namedItem("type").nodeValue() == "QGraphicsTextItem") {
788 QDomNamedNodeMap textProperties = items.at(j).namedItem("content").attributes();
789 if (textProperties.namedItem("font-pixel-size").isNull() && !textProperties.namedItem("font-size").isNull()) {
790 // Ask the user if he wants to convert
791 if (convert != KMessageBox::Yes && convert != KMessageBox::No)
792 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"));
793 if (convert == KMessageBox::Yes) {
795 font.setPointSize(textProperties.namedItem("font-size").nodeValue().toInt());
796 QDomElement content = items.at(j).namedItem("content").toElement();
797 content.setAttribute("font-pixel-size", QFontInfo(font).pixelSize());
798 content.removeAttribute("font-size");
799 kproducer.setAttribute("xmldata", data.toString());
801 * You may be tempted to delete the preview file
802 * to force its recreation: bad idea (see
803 * http://www.kdenlive.org/mantis/view.php?id=749)
813 // Fill the <documentproperties /> element
814 QDomElement docProperties = infoXml.firstChildElement("documentproperties");
815 if (docProperties.isNull()) {
816 docProperties = m_doc.createElement("documentproperties");
817 docProperties.setAttribute("zonein", infoXml.attribute("zonein"));
818 docProperties.setAttribute("zoneout", infoXml.attribute("zoneout"));
819 docProperties.setAttribute("zoom", infoXml.attribute("zoom"));
820 docProperties.setAttribute("position", infoXml.attribute("position"));
821 infoXml.appendChild(docProperties);
825 if (version <= 0.84) {
826 // update the title clips to use the new MLT kdenlivetitle producer
827 QDomNodeList kproducerNodes = m_doc.elementsByTagName("kdenlive_producer");
828 for (int i = 0; i < kproducerNodes.count(); ++i) {
829 QDomElement kproducer = kproducerNodes.at(i).toElement();
830 if (kproducer.attribute("type").toInt() == TEXT) {
831 QString data = kproducer.attribute("xmldata");
832 QString datafile = kproducer.attribute("resource");
833 if (!datafile.endsWith(".kdenlivetitle")) {
834 datafile = QString();
835 kproducer.setAttribute("resource", QString());
837 QString id = kproducer.attribute("id");
838 QDomNodeList mltproducers = m_doc.elementsByTagName("producer");
839 bool foundData = false;
840 bool foundResource = false;
841 bool foundService = false;
842 for (int j = 0; j < mltproducers.count(); j++) {
843 QDomElement wproducer = mltproducers.at(j).toElement();
844 if (wproducer.attribute("id") == id) {
845 QDomNodeList props = wproducer.childNodes();
846 for (int k = 0; k < props.count(); k++) {
847 if (props.at(k).toElement().attribute("name") == "xmldata") {
848 props.at(k).firstChild().setNodeValue(data);
850 } else if (props.at(k).toElement().attribute("name") == "mlt_service") {
851 props.at(k).firstChild().setNodeValue("kdenlivetitle");
853 } else if (props.at(k).toElement().attribute("name") == "resource") {
854 props.at(k).firstChild().setNodeValue(datafile);
855 foundResource = true;
859 QDomElement e = m_doc.createElement("property");
860 e.setAttribute("name", "xmldata");
861 QDomText value = m_doc.createTextNode(data);
862 e.appendChild(value);
863 wproducer.appendChild(e);
866 QDomElement e = m_doc.createElement("property");
867 e.setAttribute("name", "mlt_service");
868 QDomText value = m_doc.createTextNode("kdenlivetitle");
869 e.appendChild(value);
870 wproducer.appendChild(e);
872 if (!foundResource) {
873 QDomElement e = m_doc.createElement("property");
874 e.setAttribute("name", "resource");
875 QDomText value = m_doc.createTextNode(datafile);
876 e.appendChild(value);
877 wproducer.appendChild(e);
885 if (version <= 0.85) {
886 // update the LADSPA effects to use the new ladspa.id format instead of external xml file
887 QDomNodeList effectNodes = m_doc.elementsByTagName("filter");
888 for (int i = 0; i < effectNodes.count(); ++i) {
889 QDomElement effect = effectNodes.at(i).toElement();
890 if (EffectsList::property(effect, "mlt_service") == "ladspa") {
891 // Needs to be converted
892 QStringList info = getInfoFromEffectName(EffectsList::property(effect, "kdenlive_id"));
893 if (info.isEmpty()) continue;
894 // info contains the correct ladspa.id from kdenlive effect name, and a list of parameter's old and new names
895 EffectsList::setProperty(effect, "kdenlive_id", info.at(0));
896 EffectsList::setProperty(effect, "tag", info.at(0));
897 EffectsList::setProperty(effect, "mlt_service", info.at(0));
898 EffectsList::removeProperty(effect, "src");
899 for (int j = 1; j < info.size(); j++) {
900 QString value = EffectsList::property(effect, info.at(j).section('=', 0, 0));
901 if (!value.isEmpty()) {
902 // update parameter name
903 EffectsList::renameProperty(effect, info.at(j).section('=', 0, 0), info.at(j).section('=', 1, 1));
910 if (version <= 0.86) {
911 // Make sure we don't have avformat-novalidate producers, since it caused crashes
912 QDomNodeList producers = m_doc.elementsByTagName("producer");
913 int max = producers.count();
914 for (int i = 0; i < max; i++) {
915 QDomElement prod = producers.at(i).toElement();
916 if (EffectsList::property(prod, "mlt_service") == "avformat-novalidate")
917 EffectsList::setProperty(prod, "mlt_service", "avformat");
920 // 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
922 // Get profile info (width / height)
925 QDomElement profile = m_doc.firstChildElement("profile");
926 if (profile.isNull()) profile = infoXml.firstChildElement("profileinfo");
927 if (profile.isNull()) {
928 // could not find profile info, set PAL
933 profileWidth = profile.attribute("width").toInt();
934 profileHeight = profile.attribute("height").toInt();
936 QDomNodeList transitions = m_doc.elementsByTagName("transition");
937 max = transitions.count();
939 for (int i = 0; i < max; i++) {
940 QDomElement trans = transitions.at(i).toElement();
941 out = trans.attribute("out").toInt() - trans.attribute("in").toInt();
942 QString geom = EffectsList::property(trans, "geometry");
943 Mlt::Geometry *g = new Mlt::Geometry(geom.toUtf8().data(), out, profileWidth, profileHeight);
944 Mlt::GeometryItem item;
945 if (g->next_key(&item, out) == 0) {
946 // We have a keyframe just after last frame, try to move it to last frame
947 if (item.frame() == out + 1) {
951 EffectsList::setProperty(trans, "geometry", g->serialise());
958 if (version <= 0.87) {
959 if (!m_doc.firstChildElement("mlt").hasAttribute("LC_NUMERIC")) {
960 m_doc.firstChildElement("mlt").setAttribute("LC_NUMERIC", "C");
964 // The document has been converted: mark it as modified
965 infoXml.setAttribute("version", currentVersion);
970 QStringList DocumentValidator::getInfoFromEffectName(const QString oldName)
973 // Returns a list to convert old Kdenlive ladspa effects
974 if (oldName == "pitch_shift") {
975 info << "ladspa.1433";
978 else if (oldName == "vinyl") {
979 info << "ladspa.1905";
986 else if (oldName == "room_reverb") {
987 info << "ladspa.1216";
992 else if (oldName == "reverb") {
993 info << "ladspa.1423";
997 else if (oldName == "rate_scale") {
998 info << "ladspa.1417";
1001 else if (oldName == "pitch_scale") {
1002 info << "ladspa.1193";
1005 else if (oldName == "phaser") {
1006 info << "ladspa.1217";
1009 info << "feedback=2";
1012 else if (oldName == "limiter") {
1013 info << "ladspa.1913";
1016 info << "release=2";
1018 else if (oldName == "equalizer_15") {
1019 info << "ladspa.1197";
1036 else if (oldName == "equalizer") {
1037 info << "ladspa.1901";
1039 info << "midgain=1";
1042 else if (oldName == "declipper") {
1043 info << "ladspa.1195";
1048 QString DocumentValidator::colorToString(const QColor& c)
1050 QString ret = "%1,%2,%3,%4";
1051 ret = ret.arg(c.red()).arg(c.green()).arg(c.blue()).arg(c.alpha());
1055 bool DocumentValidator::isProject() const
1057 QDomNode infoXmlNode = m_doc.elementsByTagName("kdenlivedoc").at(0);
1058 return !infoXmlNode.isNull();
1061 bool DocumentValidator::isModified() const
1066 void DocumentValidator::updateEffects()
1068 // WARNING: order by findDirs will determine which js file to use (in case multiple scripts for the same filter exist)
1069 QMap <QString, KUrl> paths;
1070 #if QT_VERSION >= 0x040700
1071 QMap <QString, QScriptProgram> scripts;
1073 QMap <QString, QString> scripts;
1075 QStringList directories = KGlobal::dirs()->findDirs("appdata", "effects/update");
1076 foreach (const QString &directoryName, directories) {
1077 QDir directory(directoryName);
1078 QStringList fileList = directory.entryList(QStringList() << "*.js", QDir::Files);
1079 foreach (const QString &fileName, fileList) {
1080 QString identifier = fileName;
1081 // remove extension (".js")
1083 paths.insert(identifier, KUrl(directoryName + fileName));
1087 QDomNodeList effects = m_doc.elementsByTagName("filter");
1088 int max = effects.count();
1089 QStringList safeEffects;
1090 for(int i = 0; i < max; ++i) {
1091 QDomElement effect = effects.at(i).toElement();
1092 QString effectId = EffectsList::property(effect, "kdenlive_id");
1093 if (safeEffects.contains(effectId)) {
1094 // Do not check the same effect twice if it is at the correct version
1095 // (assume we don't have different versions of the same effect in a project file)
1098 QString effectTag = EffectsList::property(effect, "tag");
1099 QString effectVersionStr = EffectsList::property(effect, "version");
1100 double effectVersion = effectVersionStr.isNull() ? -1 : effectVersionStr.toDouble();
1102 QDomElement effectDescr = MainWindow::customEffects.getEffectByTag(QString(), effectId);
1103 if (effectDescr.isNull()) {
1104 effectDescr = MainWindow::videoEffects.getEffectByTag(effectTag, effectId);
1106 if (effectDescr.isNull()) {
1107 effectDescr = MainWindow::audioEffects.getEffectByTag(effectTag, effectId);
1109 if (!effectDescr.isNull()) {
1110 double serviceVersion = -1;
1111 QDomElement serviceVersionElem = effectDescr.firstChildElement("version");
1112 if (!serviceVersionElem.isNull()) {
1113 serviceVersion = serviceVersionElem.text().toDouble();
1115 if (serviceVersion != effectVersion && paths.contains(effectId)) {
1116 if (!scripts.contains(effectId)) {
1117 QFile scriptFile(paths.value(effectId).path());
1118 if (!scriptFile.open(QIODevice::ReadOnly)) {
1121 #if QT_VERSION >= 0x040700
1122 QScriptProgram scriptProgram(scriptFile.readAll());
1124 QString scriptProgram = scriptFile.readAll();
1127 scripts.insert(effectId, scriptProgram);
1130 QScriptEngine scriptEngine;
1131 scriptEngine.importExtension("qt.core");
1132 scriptEngine.importExtension("qt.xml");
1133 scriptEngine.evaluate(scripts.value(effectId));
1134 QScriptValue updateRules = scriptEngine.globalObject().property("update");
1135 if (!updateRules.isValid())
1137 if (updateRules.isFunction()) {
1138 QDomDocument scriptDoc;
1139 scriptDoc.appendChild(scriptDoc.importNode(effect, true));
1141 QString effectString = updateRules.call(QScriptValue(), QScriptValueList() << serviceVersion << effectVersion << scriptDoc.toString()).toString();
1143 if (!effectString.isEmpty() && !scriptEngine.hasUncaughtException()) {
1144 scriptDoc.setContent(effectString);
1145 QDomNode updatedEffect = effect.ownerDocument().importNode(scriptDoc.documentElement(), true);
1146 effect.parentNode().replaceChild(updatedEffect, effect);
1150 m_modified = updateEffectParameters(effect.childNodes(), &updateRules, serviceVersion, effectVersion);
1153 // set version number since MLT won't change it (only initially set it)
1154 QDomElement versionElem = effect.firstChildElement("version");
1155 if (EffectsList::property(effect, "version").isNull()) {
1156 versionElem = effect.ownerDocument().createTextNode(QLocale().toString(serviceVersion)).toElement();
1157 versionElem.setTagName("property");
1158 versionElem.setAttribute("name", "version");
1159 effect.appendChild(versionElem);
1161 EffectsList::setProperty(effect, "version", QLocale().toString(serviceVersion));
1164 else safeEffects.append(effectId);
1169 bool DocumentValidator::updateEffectParameters(QDomNodeList parameters, const QScriptValue* updateRules, const double serviceVersion, const double effectVersion)
1171 bool updated = false;
1172 bool isDowngrade = serviceVersion < effectVersion;
1173 for (int i = 0; i < parameters.count(); ++i) {
1174 QDomElement parameter = parameters.at(i).toElement();
1175 QScriptValue rules = updateRules->property(parameter.attribute("name"));
1176 if (rules.isValid() && rules.isArray()) {
1177 int rulesCount = rules.property("length").toInt32();
1179 // start with the highest version and downgrade step by step
1180 for (int j = rulesCount - 1; j >= 0; --j) {
1181 double version = rules.property(j).property(0).toNumber();
1182 if (version <= effectVersion && version > serviceVersion) {
1183 parameter.firstChild().setNodeValue(rules.property(j).property(1).call(QScriptValue(), QScriptValueList() << parameter.text() << isDowngrade).toString());
1188 for (int j = 0; j < rulesCount; ++j) {
1189 double version = rules.property(j).property(0).toNumber();
1190 if (version > effectVersion && version <= serviceVersion) {
1191 parameter.firstChild().setNodeValue(rules.property(j).property(1).call(QScriptValue(), QScriptValueList() << parameter.text() << isDowngrade).toString());