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 const 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")) {
100 setlocale(LC_NUMERIC, "C");
103 QLocale::setDefault(documentLocale);
104 // locale conversion might need to be redone
105 initEffects::parseEffectFiles(setlocale(LC_NUMERIC, NULL));
109 double version = documentLocale.toDouble(kdenliveDoc.attribute("version"), &ok);
111 // Could not parse version number, there is probably a conflict in decimal separator
112 QLocale tempLocale = QLocale(mlt.attribute("LC_NUMERIC"));
113 version = tempLocale.toDouble(kdenliveDoc.attribute("version"), &ok);
114 if (!ok) version = kdenliveDoc.attribute("version").toDouble(&ok);
115 if (!ok) kDebug()<<"// CANNOT PARSE VERSION NUMBER, ERROR!";
118 // Upgrade the document to the latest version
119 if (!upgrade(version, currentVersion))
123 * Check the syntax (this will be replaced by XSD validation with Qt 4.6)
124 * and correct some errors
127 // Return (or create) the tractor
128 QDomElement tractor = mlt.firstChildElement("tractor");
129 if (tractor.isNull()) {
131 tractor = m_doc.createElement("tractor");
132 tractor.setAttribute("global_feed", "1");
133 tractor.setAttribute("in", "0");
134 tractor.setAttribute("out", "-1");
135 tractor.setAttribute("id", "maintractor");
136 mlt.appendChild(tractor);
140 * Make sure at least one track exists, and they're equal in number to
141 * to the maximum between MLT and Kdenlive playlists and tracks
143 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
144 int tracksMax = playlists.count() - 1; // Remove the black track
145 QDomNodeList tracks = tractor.elementsByTagName("track");
146 tracksMax = qMax(tracks.count() - 1, tracksMax);
147 QDomNodeList tracksinfo = kdenliveDoc.elementsByTagName("trackinfo");
148 tracksMax = qMax(tracksinfo.count(), tracksMax);
149 tracksMax = qMax(1, tracksMax); // Force existance of one track
150 if (playlists.count() - 1 < tracksMax ||
151 tracks.count() - 1 < tracksMax ||
152 tracksinfo.count() < tracksMax) {
153 kDebug() << "//// WARNING, PROJECT IS CORRUPTED, MISSING TRACK";
156 // use the MLT tracks as reference
157 if (tracks.count() - 1 < tracksMax) {
158 // Looks like one MLT track is missing, remove the extra Kdenlive track if there is one.
159 if (tracksinfo.count() != tracks.count() - 1) {
160 // The Kdenlive tracks are not ok, clear and rebuild them
161 QDomNode tinfo = kdenliveDoc.firstChildElement("tracksinfo");
162 QDomNode tnode = tinfo.firstChild();
163 while (!tnode.isNull()) {
164 tinfo.removeChild(tnode);
165 tnode = tinfo.firstChild();
168 for (int i = 1; i < tracks.count(); i++) {
169 QString hide = tracks.at(i).toElement().attribute("hide");
170 QDomElement newTrack = m_doc.createElement("trackinfo");
171 if (hide == "video") {
173 newTrack.setAttribute("type", "audio");
174 newTrack.setAttribute("blind", 1);
175 newTrack.setAttribute("mute", 0);
176 newTrack.setAttribute("lock", 0);
178 newTrack.setAttribute("blind", 0);
179 newTrack.setAttribute("mute", 0);
180 newTrack.setAttribute("lock", 0);
182 tinfo.appendChild(newTrack);
187 if (playlists.count() - 1 < tracksMax) {
188 difference = tracksMax - (playlists.count() - 1);
189 for (int i = 0; i < difference; ++i) {
190 QDomElement playlist = m_doc.createElement("playlist");
191 mlt.appendChild(playlist);
194 if (tracks.count() - 1 < tracksMax) {
195 difference = tracksMax - (tracks.count() - 1);
196 for (int i = 0; i < difference; ++i) {
197 QDomElement track = m_doc.createElement("track");
198 tractor.appendChild(track);
201 if (tracksinfo.count() < tracksMax) {
202 QDomElement tracksinfoElm = kdenliveDoc.firstChildElement("tracksinfo");
203 if (tracksinfoElm.isNull()) {
204 tracksinfoElm = m_doc.createElement("tracksinfo");
205 kdenliveDoc.appendChild(tracksinfoElm);
207 difference = tracksMax - tracksinfo.count();
208 for (int i = 0; i < difference; ++i) {
209 QDomElement trackinfo = m_doc.createElement("trackinfo");
210 trackinfo.setAttribute("mute", "0");
211 trackinfo.setAttribute("locked", "0");
212 tracksinfoElm.appendChild(trackinfo);
217 // TODO: check the tracks references
218 // TODO: check internal mix transitions
227 bool DocumentValidator::upgrade(double version, const double currentVersion)
229 kDebug() << "Opening a document with version " << version;
231 // No conversion needed
232 if (version == currentVersion) {
236 // The document is too new
237 if (version > currentVersion) {
238 kDebug() << "Unable to open document with version " << version;
239 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"));
243 // Unsupported document versions
244 if (version == 0.5 || version == 0.7) {
245 kDebug() << "Unable to open document with version " << version;
246 KMessageBox::sorry(kapp->activeWindow(), i18n("This project type is unsupported (version %1) and can't be loaded.", version), i18n("Unable to open project"));
251 QDomNode infoXmlNode = m_doc.elementsByTagName("kdenlivedoc").at(0);
252 QDomElement infoXml = infoXmlNode.toElement();
253 infoXml.setAttribute("upgraded", "1");
255 if (version <= 0.6) {
256 QDomElement infoXml_old = infoXmlNode.cloneNode(true).toElement(); // Needed for folders
257 QDomNode westley = m_doc.elementsByTagName("westley").at(1);
258 QDomNode tractor = m_doc.elementsByTagName("tractor").at(0);
259 QDomNode multitrack = m_doc.elementsByTagName("multitrack").at(0);
260 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
262 QDomNode props = m_doc.elementsByTagName("properties").at(0).toElement();
263 QString profile = props.toElement().attribute("videoprofile");
264 int startPos = props.toElement().attribute("timeline_position").toInt();
265 if (profile == "dv_wide")
266 profile = "dv_pal_wide";
268 // move playlists outside of tractor and add the tracks instead
269 int max = playlists.count();
270 if (westley.isNull()) {
271 westley = m_doc.createElement("westley");
272 m_doc.documentElement().appendChild(westley);
274 if (tractor.isNull()) {
275 kDebug() << "// NO MLT PLAYLIST, building empty one";
276 QDomElement blank_tractor = m_doc.createElement("tractor");
277 westley.appendChild(blank_tractor);
278 QDomElement blank_playlist = m_doc.createElement("playlist");
279 blank_playlist.setAttribute("id", "black_track");
280 westley.insertBefore(blank_playlist, QDomNode());
281 QDomElement blank_track = m_doc.createElement("track");
282 blank_track.setAttribute("producer", "black_track");
283 blank_tractor.appendChild(blank_track);
285 QDomNodeList kdenlivetracks = m_doc.elementsByTagName("kdenlivetrack");
286 for (int i = 0; i < kdenlivetracks.count(); i++) {
287 blank_playlist = m_doc.createElement("playlist");
288 blank_playlist.setAttribute("id", "playlist" + QString::number(i));
289 westley.insertBefore(blank_playlist, QDomNode());
290 blank_track = m_doc.createElement("track");
291 blank_track.setAttribute("producer", "playlist" + QString::number(i));
292 blank_tractor.appendChild(blank_track);
293 if (kdenlivetracks.at(i).toElement().attribute("cliptype") == "Sound") {
294 blank_playlist.setAttribute("hide", "video");
295 blank_track.setAttribute("hide", "video");
298 } else for (int i = 0; i < max; i++) {
299 QDomNode n = playlists.at(i);
300 westley.insertBefore(n, QDomNode());
301 QDomElement pl = n.toElement();
302 QDomElement track = m_doc.createElement("track");
303 QString trackType = pl.attribute("hide");
304 if (!trackType.isEmpty())
305 track.setAttribute("hide", trackType);
306 QString playlist_id = pl.attribute("id");
307 if (playlist_id.isEmpty()) {
308 playlist_id = "black_track";
309 pl.setAttribute("id", playlist_id);
311 track.setAttribute("producer", playlist_id);
312 //tractor.appendChild(track);
313 #define KEEP_TRACK_ORDER 1
314 #ifdef KEEP_TRACK_ORDER
315 tractor.insertAfter(track, QDomNode());
317 // Insert the new track in an order that hopefully matches the 3 video, then 2 audio tracks of Kdenlive 0.7.0
318 // insertion sort - O( tracks*tracks )
319 // Note, this breaks _all_ transitions - but you can move them up and down afterwards.
320 QDomElement tractor_elem = tractor.toElement();
321 if (! tractor_elem.isNull()) {
322 QDomNodeList tracks = tractor_elem.elementsByTagName("track");
323 int size = tracks.size();
325 tractor.insertAfter(track, QDomNode());
327 bool inserted = false;
328 for (int i = 0; i < size; ++i) {
329 QDomElement track_elem = tracks.at(i).toElement();
330 if (track_elem.isNull()) {
331 tractor.insertAfter(track, QDomNode());
335 kDebug() << "playlist_id: " << playlist_id << " producer:" << track_elem.attribute("producer");
336 if (playlist_id < track_elem.attribute("producer")) {
337 tractor.insertBefore(track, track_elem);
343 // Reach here, no insertion, insert last
345 tractor.insertAfter(track, QDomNode());
349 kWarning() << "tractor was not a QDomElement";
350 tractor.insertAfter(track, QDomNode());
354 tractor.removeChild(multitrack);
356 // audio track mixing transitions should not be added to track view, so add required attribute
357 QDomNodeList transitions = m_doc.elementsByTagName("transition");
358 max = transitions.count();
359 for (int i = 0; i < max; i++) {
360 QDomElement tr = transitions.at(i).toElement();
361 if (tr.attribute("combine") == "1" && tr.attribute("mlt_service") == "mix") {
362 QDomElement property = m_doc.createElement("property");
363 property.setAttribute("name", "internal_added");
364 QDomText value = m_doc.createTextNode("237");
365 property.appendChild(value);
366 tr.appendChild(property);
367 property = m_doc.createElement("property");
368 property.setAttribute("name", "mlt_service");
369 value = m_doc.createTextNode("mix");
370 property.appendChild(value);
371 tr.appendChild(property);
373 // convert transition
374 QDomNamedNodeMap attrs = tr.attributes();
375 for (int j = 0; j < attrs.count(); j++) {
376 QString attrName = attrs.item(j).nodeName();
377 if (attrName != "in" && attrName != "out" && attrName != "id") {
378 QDomElement property = m_doc.createElement("property");
379 property.setAttribute("name", attrName);
380 QDomText value = m_doc.createTextNode(attrs.item(j).nodeValue());
381 property.appendChild(value);
382 tr.appendChild(property);
388 // move transitions after tracks
389 for (int i = 0; i < max; i++) {
390 tractor.insertAfter(transitions.at(0), QDomNode());
393 // Fix filters format
394 QDomNodeList entries = m_doc.elementsByTagName("entry");
395 max = entries.count();
396 for (int i = 0; i < max; i++) {
399 QDomNode m = entries.at(i).firstChild();
400 while (!m.isNull()) {
401 if (m.toElement().tagName() == "filter") {
402 QDomElement filt = m.toElement();
403 QDomNamedNodeMap attrs = filt.attributes();
404 QString current_id = filt.attribute("kdenlive_id");
405 if (current_id != last_id) {
407 last_id = current_id;
409 QDomElement e = m_doc.createElement("property");
410 e.setAttribute("name", "kdenlive_ix");
411 QDomText value = m_doc.createTextNode(QString::number(effectix));
412 e.appendChild(value);
414 for (int j = 0; j < attrs.count(); j++) {
415 QDomAttr a = attrs.item(j).toAttr();
417 kDebug() << " FILTER; adding :" << a.name() << ":" << a.value();
418 QDomElement e = m_doc.createElement("property");
419 e.setAttribute("name", a.name());
420 QDomText value = m_doc.createTextNode(a.value());
421 e.appendChild(value);
432 QDomNodeList filters = m_doc.elementsByTagName("filter");
433 max = filters.count();
436 for (int i = 0; i < max; i++) {
437 QDomElement filt = filters.at(i).toElement();
438 QDomNamedNodeMap attrs = filt.attributes();
439 QString current_id = filt.attribute("kdenlive_id");
440 if (current_id != last_id) {
442 last_id = current_id;
444 QDomElement e = m_doc.createElement("property");
445 e.setAttribute("name", "kdenlive_ix");
446 QDomText value = m_doc.createTextNode(QString::number(1));
447 e.appendChild(value);
449 for (int j = 0; j < attrs.count(); j++) {
450 QDomAttr a = attrs.item(j).toAttr();
452 kDebug() << " FILTER; adding :" << a.name() << ":" << a.value();
453 QDomElement e = m_doc.createElement("property");
454 e.setAttribute("name", a.name());
455 QDomText value = m_doc.createTextNode(a.value());
456 e.appendChild(value);
463 QDomNodeList producers = westley.toElement().elementsByTagName("producer");
464 max = producers.count();
465 for (int i = 0; i < max; i++) {
466 QDomElement prod = producers.at(i).toElement();
467 if (prod.attribute("mlt_service") == "framebuffer") {
468 QString slowmotionprod = prod.attribute("resource");
469 slowmotionprod.replace(':', '?');
470 kDebug() << "// FOUND WRONG SLOWMO, new: " << slowmotionprod;
471 prod.setAttribute("resource", slowmotionprod);
474 // move producers to correct place, markers to a global list, fix clip descriptions
475 QDomElement markers = m_doc.createElement("markers");
476 // This will get the xml producers:
477 producers = m_doc.elementsByTagName("producer");
478 max = producers.count();
479 for (int i = 0; i < max; i++) {
480 QDomElement prod = producers.at(0).toElement();
481 // add resource also as a property (to allow path correction in setNewResource())
482 // TODO: will it work with slowmotion? needs testing
483 /*if (!prod.attribute("resource").isEmpty()) {
484 QDomElement prop_resource = m_doc.createElement("property");
485 prop_resource.setAttribute("name", "resource");
486 QDomText resource = m_doc.createTextNode(prod.attribute("resource"));
487 prop_resource.appendChild(resource);
488 prod.appendChild(prop_resource);
490 QDomNode m = prod.firstChild();
492 if (m.toElement().tagName() == "markers") {
493 QDomNodeList prodchilds = m.childNodes();
494 int maxchild = prodchilds.count();
495 for (int k = 0; k < maxchild; k++) {
496 QDomElement mark = prodchilds.at(0).toElement();
497 mark.setAttribute("id", prod.attribute("id"));
498 markers.insertAfter(mark, QDomNode());
501 } else if (prod.attribute("type").toInt() == TEXT) {
502 // convert title clip
503 if (m.toElement().tagName() == "textclip") {
505 QDomElement titleclip = m.toElement();
506 QDomElement title = tdoc.createElement("kdenlivetitle");
507 tdoc.appendChild(title);
508 QDomNodeList objects = titleclip.childNodes();
509 int maxchild = objects.count();
510 for (int k = 0; k < maxchild; k++) {
512 QDomElement ob = objects.at(k).toElement();
513 if (ob.attribute("type") == "3") {
514 // text object - all of this goes into "xmldata"...
515 QDomElement item = tdoc.createElement("item");
516 item.setAttribute("z-index", ob.attribute("z"));
517 item.setAttribute("type", "QGraphicsTextItem");
518 QDomElement position = tdoc.createElement("position");
519 position.setAttribute("x", ob.attribute("x"));
520 position.setAttribute("y", ob.attribute("y"));
521 QDomElement content = tdoc.createElement("content");
522 content.setAttribute("font", ob.attribute("font_family"));
523 content.setAttribute("font-size", ob.attribute("font_size"));
524 content.setAttribute("font-bold", ob.attribute("bold"));
525 content.setAttribute("font-italic", ob.attribute("italic"));
526 content.setAttribute("font-underline", ob.attribute("underline"));
527 QString col = ob.attribute("color");
529 content.setAttribute("font-color", colorToString(c));
530 // todo: These fields are missing from the newly generated xmldata:
531 // transform, startviewport, endviewport, background
533 QDomText conttxt = tdoc.createTextNode(ob.attribute("text"));
534 content.appendChild(conttxt);
535 item.appendChild(position);
536 item.appendChild(content);
537 title.appendChild(item);
538 } else if (ob.attribute("type") == "5") {
540 QDomElement item = tdoc.createElement("item");
541 item.setAttribute("z-index", ob.attribute("z"));
542 item.setAttribute("type", "QGraphicsRectItem");
543 QDomElement position = tdoc.createElement("position");
544 position.setAttribute("x", ob.attribute("x"));
545 position.setAttribute("y", ob.attribute("y"));
546 QDomElement content = tdoc.createElement("content");
547 QString col = ob.attribute("color");
549 content.setAttribute("brushcolor", colorToString(c));
550 QString rect = "0,0,";
551 rect.append(ob.attribute("width"));
553 rect.append(ob.attribute("height"));
554 content.setAttribute("rect", rect);
555 item.appendChild(position);
556 item.appendChild(content);
557 title.appendChild(item);
560 prod.setAttribute("xmldata", tdoc.toString());
561 // mbd todo: This clearly does not work, as every title gets the same name - trying to leave it empty
562 // QStringList titleInfo = TitleWidget::getFreeTitleInfo(projectFolder());
563 // prod.setAttribute("titlename", titleInfo.at(0));
564 // prod.setAttribute("resource", titleInfo.at(1));
565 //kDebug()<<"TITLE DATA:\n"<<tdoc.toString();
567 } // End conversion of title clips.
569 } else if (m.isText()) {
570 QString comment = m.nodeValue();
571 if (!comment.isEmpty()) {
572 prod.setAttribute("description", comment);
577 int duration = prod.attribute("duration").toInt();
578 if (duration > 0) prod.setAttribute("out", QString::number(duration));
579 // The clip goes back in, but text clips should not go back in, at least not modified
580 westley.insertBefore(prod, QDomNode());
583 QDomNode westley0 = m_doc.elementsByTagName("westley").at(0);
584 if (!markers.firstChild().isNull()) westley0.appendChild(markers);
587 * Convert as much of the kdenlivedoc as possible. Use the producer in
588 * westley. First, remove the old stuff from westley, and add a new
589 * empty one. Also, track the max id in order to use it for the adding
592 int max_kproducer_id = 0;
593 westley0.removeChild(infoXmlNode);
594 QDomElement infoXml_new = m_doc.createElement("kdenlivedoc");
595 infoXml_new.setAttribute("profile", profile);
596 infoXml.setAttribute("position", startPos);
598 // Add all the producers that has a resource in westley
599 QDomElement westley_element = westley0.toElement();
600 if (westley_element.isNull()) {
601 kWarning() << "westley0 element in document was not a QDomElement - unable to add producers to new kdenlivedoc";
603 QDomNodeList wproducers = westley_element.elementsByTagName("producer");
604 int kmax = wproducers.count();
605 for (int i = 0; i < kmax; i++) {
606 QDomElement wproducer = wproducers.at(i).toElement();
607 if (wproducer.isNull()) {
608 kWarning() << "Found producer in westley0, that was not a QDomElement";
611 if (wproducer.attribute("id") == "black") continue;
612 // We have to do slightly different things, depending on the type
613 kDebug() << "Converting producer element with type" << wproducer.attribute("type");
614 if (wproducer.attribute("type").toInt() == TEXT) {
615 kDebug() << "Found TEXT element in producer" << endl;
616 QDomElement kproducer = wproducer.cloneNode(true).toElement();
617 kproducer.setTagName("kdenlive_producer");
618 infoXml_new.appendChild(kproducer);
620 * TODO: Perhaps needs some more changes here to
621 * "frequency", aspect ratio as a float, frame_size,
622 * channels, and later, resource and title name
625 QDomElement kproducer = m_doc.createElement("kdenlive_producer");
626 kproducer.setAttribute("id", wproducer.attribute("id"));
627 if (!wproducer.attribute("description").isEmpty())
628 kproducer.setAttribute("description", wproducer.attribute("description"));
629 kproducer.setAttribute("resource", wproducer.attribute("resource"));
630 kproducer.setAttribute("type", wproducer.attribute("type"));
631 // Testing fix for 358
632 if (!wproducer.attribute("aspect_ratio").isEmpty()) {
633 kproducer.setAttribute("aspect_ratio", wproducer.attribute("aspect_ratio"));
635 if (!wproducer.attribute("source_fps").isEmpty()) {
636 kproducer.setAttribute("fps", wproducer.attribute("source_fps"));
638 if (!wproducer.attribute("length").isEmpty()) {
639 kproducer.setAttribute("duration", wproducer.attribute("length"));
641 infoXml_new.appendChild(kproducer);
643 if (wproducer.attribute("id").toInt() > max_kproducer_id) {
644 max_kproducer_id = wproducer.attribute("id").toInt();
648 #define LOOKUP_FOLDER 1
651 * Look through all the folder elements of the old doc, for each folder,
652 * for each producer, get the id, look it up in the new doc, set the
653 * groupname and groupid. Note, this does not work at the moment - at
654 * least one folder shows up missing, and clips with no folder does not
657 //QDomElement infoXml_old = infoXmlNode.toElement();
658 if (!infoXml_old.isNull()) {
659 QDomNodeList folders = infoXml_old.elementsByTagName("folder");
660 int fsize = folders.size();
661 int groupId = max_kproducer_id + 1; // Start at +1 of max id of the kdenlive_producers
662 for (int i = 0; i < fsize; ++i) {
663 QDomElement folder = folders.at(i).toElement();
664 if (!folder.isNull()) {
665 QString groupName = folder.attribute("name");
666 kDebug() << "groupName: " << groupName << " with groupId: " << groupId;
667 QDomNodeList fproducers = folder.elementsByTagName("producer");
668 int psize = fproducers.size();
669 for (int j = 0; j < psize; ++j) {
670 QDomElement fproducer = fproducers.at(j).toElement();
671 if (!fproducer.isNull()) {
672 QString id = fproducer.attribute("id");
673 // This is not very effective, but compared to loading the clips, its a breeze
674 QDomNodeList kdenlive_producers = infoXml_new.elementsByTagName("kdenlive_producer");
675 int kpsize = kdenlive_producers.size();
676 for (int k = 0; k < kpsize; ++k) {
677 QDomElement kproducer = kdenlive_producers.at(k).toElement(); // Its an element for sure
678 if (id == kproducer.attribute("id")) {
679 // We do not check that it already is part of a folder
680 kproducer.setAttribute("groupid", groupId);
681 kproducer.setAttribute("groupname", groupName);
692 QDomNodeList elements = westley.childNodes();
693 max = elements.count();
694 for (int i = 0; i < max; i++) {
695 QDomElement prod = elements.at(0).toElement();
696 westley0.insertAfter(prod, QDomNode());
699 westley0.appendChild(infoXml_new);
701 westley0.removeChild(westley);
703 // adds <avfile /> information to <kdenlive_producer />
704 QDomNodeList kproducers = m_doc.elementsByTagName("kdenlive_producer");
705 QDomNodeList avfiles = infoXml_old.elementsByTagName("avfile");
706 kDebug() << "found" << avfiles.count() << "<avfile />s and" << kproducers.count() << "<kdenlive_producer />s";
707 for (int i = 0; i < avfiles.count(); ++i) {
708 QDomElement avfile = avfiles.at(i).toElement();
709 QDomElement kproducer;
711 kWarning() << "found an <avfile /> that is not a QDomElement";
713 QString id = avfile.attribute("id");
714 // this is horrible, must be rewritten, it's just for test
715 for (int j = 0; j < kproducers.count(); ++j) {
716 //kDebug() << "checking <kdenlive_producer /> with id" << kproducers.at(j).toElement().attribute("id");
717 if (kproducers.at(j).toElement().attribute("id") == id) {
718 kproducer = kproducers.at(j).toElement();
722 if (kproducer == QDomElement())
723 kWarning() << "no match for <avfile /> with id =" << id;
725 //kDebug() << "ready to set additional <avfile />'s attributes (id =" << id << ")";
726 kproducer.setAttribute("channels", avfile.attribute("channels"));
727 kproducer.setAttribute("duration", avfile.attribute("duration"));
728 kproducer.setAttribute("frame_size", avfile.attribute("width") + 'x' + avfile.attribute("height"));
729 kproducer.setAttribute("frequency", avfile.attribute("frequency"));
730 if (kproducer.attribute("description").isEmpty() && !avfile.attribute("description").isEmpty())
731 kproducer.setAttribute("description", avfile.attribute("description"));
735 infoXml = infoXml_new;
738 if (version <= 0.81) {
739 // Add the tracks information
740 QString tracksOrder = infoXml.attribute("tracks");
741 if (tracksOrder.isEmpty()) {
742 QDomNodeList tracks = m_doc.elementsByTagName("track");
743 for (int i = 0; i < tracks.count(); i++) {
744 QDomElement track = tracks.at(i).toElement();
745 if (track.attribute("producer") != "black_track") {
746 if (track.attribute("hide") == "video")
747 tracksOrder.append('a');
749 tracksOrder.append('v');
753 QDomElement tracksinfo = m_doc.createElement("tracksinfo");
754 for (int i = 0; i < tracksOrder.size(); i++) {
755 QDomElement trackinfo = m_doc.createElement("trackinfo");
756 if (tracksOrder.data()[i] == 'a') {
757 trackinfo.setAttribute("type", "audio");
758 trackinfo.setAttribute("blind", true);
760 trackinfo.setAttribute("blind", false);
761 trackinfo.setAttribute("mute", false);
762 trackinfo.setAttribute("locked", false);
763 tracksinfo.appendChild(trackinfo);
765 infoXml.appendChild(tracksinfo);
768 if (version <= 0.82) {
769 // Convert <westley />s in <mlt />s (MLT extreme makeover)
770 QDomNodeList westleyNodes = m_doc.elementsByTagName("westley");
771 for (int i = 0; i < westleyNodes.count(); i++) {
772 QDomElement westley = westleyNodes.at(i).toElement();
773 westley.setTagName("mlt");
777 if (version <= 0.83) {
778 // Replace point size with pixel size in text titles
779 if (m_doc.toString().contains("font-size")) {
780 KMessageBox::ButtonCode convert = KMessageBox::Continue;
781 QDomNodeList kproducerNodes = m_doc.elementsByTagName("kdenlive_producer");
782 for (int i = 0; i < kproducerNodes.count() && convert != KMessageBox::No; ++i) {
783 QDomElement kproducer = kproducerNodes.at(i).toElement();
784 if (kproducer.attribute("type").toInt() == TEXT) {
786 data.setContent(kproducer.attribute("xmldata"));
787 QDomNodeList items = data.firstChild().childNodes();
788 for (int j = 0; j < items.count() && convert != KMessageBox::No; ++j) {
789 if (items.at(j).attributes().namedItem("type").nodeValue() == "QGraphicsTextItem") {
790 QDomNamedNodeMap textProperties = items.at(j).namedItem("content").attributes();
791 if (textProperties.namedItem("font-pixel-size").isNull() && !textProperties.namedItem("font-size").isNull()) {
792 // Ask the user if he wants to convert
793 if (convert != KMessageBox::Yes && convert != KMessageBox::No)
794 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"));
795 if (convert == KMessageBox::Yes) {
797 font.setPointSize(textProperties.namedItem("font-size").nodeValue().toInt());
798 QDomElement content = items.at(j).namedItem("content").toElement();
799 content.setAttribute("font-pixel-size", QFontInfo(font).pixelSize());
800 content.removeAttribute("font-size");
801 kproducer.setAttribute("xmldata", data.toString());
803 * You may be tempted to delete the preview file
804 * to force its recreation: bad idea (see
805 * http://www.kdenlive.org/mantis/view.php?id=749)
815 // Fill the <documentproperties /> element
816 QDomElement docProperties = infoXml.firstChildElement("documentproperties");
817 if (docProperties.isNull()) {
818 docProperties = m_doc.createElement("documentproperties");
819 docProperties.setAttribute("zonein", infoXml.attribute("zonein"));
820 docProperties.setAttribute("zoneout", infoXml.attribute("zoneout"));
821 docProperties.setAttribute("zoom", infoXml.attribute("zoom"));
822 docProperties.setAttribute("position", infoXml.attribute("position"));
823 infoXml.appendChild(docProperties);
827 if (version <= 0.84) {
828 // update the title clips to use the new MLT kdenlivetitle producer
829 QDomNodeList kproducerNodes = m_doc.elementsByTagName("kdenlive_producer");
830 for (int i = 0; i < kproducerNodes.count(); ++i) {
831 QDomElement kproducer = kproducerNodes.at(i).toElement();
832 if (kproducer.attribute("type").toInt() == TEXT) {
833 QString data = kproducer.attribute("xmldata");
834 QString datafile = kproducer.attribute("resource");
835 if (!datafile.endsWith(".kdenlivetitle")) {
836 datafile = QString();
837 kproducer.setAttribute("resource", QString());
839 QString id = kproducer.attribute("id");
840 QDomNodeList mltproducers = m_doc.elementsByTagName("producer");
841 bool foundData = false;
842 bool foundResource = false;
843 bool foundService = false;
844 for (int j = 0; j < mltproducers.count(); j++) {
845 QDomElement wproducer = mltproducers.at(j).toElement();
846 if (wproducer.attribute("id") == id) {
847 QDomNodeList props = wproducer.childNodes();
848 for (int k = 0; k < props.count(); k++) {
849 if (props.at(k).toElement().attribute("name") == "xmldata") {
850 props.at(k).firstChild().setNodeValue(data);
852 } else if (props.at(k).toElement().attribute("name") == "mlt_service") {
853 props.at(k).firstChild().setNodeValue("kdenlivetitle");
855 } else if (props.at(k).toElement().attribute("name") == "resource") {
856 props.at(k).firstChild().setNodeValue(datafile);
857 foundResource = true;
861 QDomElement e = m_doc.createElement("property");
862 e.setAttribute("name", "xmldata");
863 QDomText value = m_doc.createTextNode(data);
864 e.appendChild(value);
865 wproducer.appendChild(e);
868 QDomElement e = m_doc.createElement("property");
869 e.setAttribute("name", "mlt_service");
870 QDomText value = m_doc.createTextNode("kdenlivetitle");
871 e.appendChild(value);
872 wproducer.appendChild(e);
874 if (!foundResource) {
875 QDomElement e = m_doc.createElement("property");
876 e.setAttribute("name", "resource");
877 QDomText value = m_doc.createTextNode(datafile);
878 e.appendChild(value);
879 wproducer.appendChild(e);
887 if (version <= 0.85) {
888 // update the LADSPA effects to use the new ladspa.id format instead of external xml file
889 QDomNodeList effectNodes = m_doc.elementsByTagName("filter");
890 for (int i = 0; i < effectNodes.count(); ++i) {
891 QDomElement effect = effectNodes.at(i).toElement();
892 if (EffectsList::property(effect, "mlt_service") == "ladspa") {
893 // Needs to be converted
894 QStringList info = getInfoFromEffectName(EffectsList::property(effect, "kdenlive_id"));
895 if (info.isEmpty()) continue;
896 // info contains the correct ladspa.id from kdenlive effect name, and a list of parameter's old and new names
897 EffectsList::setProperty(effect, "kdenlive_id", info.at(0));
898 EffectsList::setProperty(effect, "tag", info.at(0));
899 EffectsList::setProperty(effect, "mlt_service", info.at(0));
900 EffectsList::removeProperty(effect, "src");
901 for (int j = 1; j < info.size(); j++) {
902 QString value = EffectsList::property(effect, info.at(j).section('=', 0, 0));
903 if (!value.isEmpty()) {
904 // update parameter name
905 EffectsList::renameProperty(effect, info.at(j).section('=', 0, 0), info.at(j).section('=', 1, 1));
912 if (version <= 0.86) {
913 // Make sure we don't have avformat-novalidate producers, since it caused crashes
914 QDomNodeList producers = m_doc.elementsByTagName("producer");
915 int max = producers.count();
916 for (int i = 0; i < max; i++) {
917 QDomElement prod = producers.at(i).toElement();
918 if (EffectsList::property(prod, "mlt_service") == "avformat-novalidate")
919 EffectsList::setProperty(prod, "mlt_service", "avformat");
922 // 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
924 // Get profile info (width / height)
927 QDomElement profile = m_doc.firstChildElement("profile");
928 if (profile.isNull()) profile = infoXml.firstChildElement("profileinfo");
929 if (profile.isNull()) {
930 // could not find profile info, set PAL
935 profileWidth = profile.attribute("width").toInt();
936 profileHeight = profile.attribute("height").toInt();
938 QDomNodeList transitions = m_doc.elementsByTagName("transition");
939 max = transitions.count();
941 for (int i = 0; i < max; i++) {
942 QDomElement trans = transitions.at(i).toElement();
943 out = trans.attribute("out").toInt() - trans.attribute("in").toInt();
944 QString geom = EffectsList::property(trans, "geometry");
945 Mlt::Geometry *g = new Mlt::Geometry(geom.toUtf8().data(), out, profileWidth, profileHeight);
946 Mlt::GeometryItem item;
947 if (g->next_key(&item, out) == 0) {
948 // We have a keyframe just after last frame, try to move it to last frame
949 if (item.frame() == out + 1) {
953 EffectsList::setProperty(trans, "geometry", g->serialise());
960 if (version <= 0.87) {
961 if (!m_doc.firstChildElement("mlt").hasAttribute("LC_NUMERIC")) {
962 m_doc.firstChildElement("mlt").setAttribute("LC_NUMERIC", "C");
966 // The document has been converted: mark it as modified
967 infoXml.setAttribute("version", currentVersion);
972 QStringList DocumentValidator::getInfoFromEffectName(const QString oldName)
975 // Returns a list to convert old Kdenlive ladspa effects
976 if (oldName == "pitch_shift") {
977 info << "ladspa.1433";
980 else if (oldName == "vinyl") {
981 info << "ladspa.1905";
988 else if (oldName == "room_reverb") {
989 info << "ladspa.1216";
994 else if (oldName == "reverb") {
995 info << "ladspa.1423";
999 else if (oldName == "rate_scale") {
1000 info << "ladspa.1417";
1003 else if (oldName == "pitch_scale") {
1004 info << "ladspa.1193";
1007 else if (oldName == "phaser") {
1008 info << "ladspa.1217";
1011 info << "feedback=2";
1014 else if (oldName == "limiter") {
1015 info << "ladspa.1913";
1018 info << "release=2";
1020 else if (oldName == "equalizer_15") {
1021 info << "ladspa.1197";
1038 else if (oldName == "equalizer") {
1039 info << "ladspa.1901";
1041 info << "midgain=1";
1044 else if (oldName == "declipper") {
1045 info << "ladspa.1195";
1050 QString DocumentValidator::colorToString(const QColor& c)
1052 QString ret = "%1,%2,%3,%4";
1053 ret = ret.arg(c.red()).arg(c.green()).arg(c.blue()).arg(c.alpha());
1057 bool DocumentValidator::isProject() const
1059 QDomNode infoXmlNode = m_doc.elementsByTagName("kdenlivedoc").at(0);
1060 return !infoXmlNode.isNull();
1063 bool DocumentValidator::isModified() const
1068 void DocumentValidator::updateEffects()
1070 // WARNING: order by findDirs will determine which js file to use (in case multiple scripts for the same filter exist)
1071 QMap <QString, KUrl> paths;
1072 #if QT_VERSION >= 0x040700
1073 QMap <QString, QScriptProgram> scripts;
1075 QMap <QString, QString> scripts;
1077 QStringList directories = KGlobal::dirs()->findDirs("appdata", "effects/update");
1078 foreach (const QString &directoryName, directories) {
1079 QDir directory(directoryName);
1080 QStringList fileList = directory.entryList(QStringList() << "*.js", QDir::Files);
1081 foreach (const QString &fileName, fileList) {
1082 QString identifier = fileName;
1083 // remove extension (".js")
1085 paths.insert(identifier, KUrl(directoryName + fileName));
1089 QDomNodeList effects = m_doc.elementsByTagName("filter");
1090 int max = effects.count();
1091 QStringList safeEffects;
1092 for(int i = 0; i < max; ++i) {
1093 QDomElement effect = effects.at(i).toElement();
1094 QString effectId = EffectsList::property(effect, "kdenlive_id");
1095 if (safeEffects.contains(effectId)) {
1096 // Do not check the same effect twice if it is at the correct version
1097 // (assume we don't have different versions of the same effect in a project file)
1100 QString effectTag = EffectsList::property(effect, "tag");
1101 QString effectVersionStr = EffectsList::property(effect, "version");
1102 double effectVersion = effectVersionStr.isNull() ? -1 : effectVersionStr.toDouble();
1104 QDomElement effectDescr = MainWindow::customEffects.getEffectByTag(QString(), effectId);
1105 if (effectDescr.isNull()) {
1106 effectDescr = MainWindow::videoEffects.getEffectByTag(effectTag, effectId);
1108 if (effectDescr.isNull()) {
1109 effectDescr = MainWindow::audioEffects.getEffectByTag(effectTag, effectId);
1111 if (!effectDescr.isNull()) {
1112 double serviceVersion = -1;
1113 QDomElement serviceVersionElem = effectDescr.firstChildElement("version");
1114 if (!serviceVersionElem.isNull()) {
1115 serviceVersion = serviceVersionElem.text().toDouble();
1117 if (serviceVersion != effectVersion && paths.contains(effectId)) {
1118 if (!scripts.contains(effectId)) {
1119 QFile scriptFile(paths.value(effectId).path());
1120 if (!scriptFile.open(QIODevice::ReadOnly)) {
1123 #if QT_VERSION >= 0x040700
1124 QScriptProgram scriptProgram(scriptFile.readAll());
1126 QString scriptProgram = scriptFile.readAll();
1129 scripts.insert(effectId, scriptProgram);
1132 QScriptEngine scriptEngine;
1133 scriptEngine.importExtension("qt.core");
1134 scriptEngine.importExtension("qt.xml");
1135 scriptEngine.evaluate(scripts.value(effectId));
1136 QScriptValue updateRules = scriptEngine.globalObject().property("update");
1137 if (!updateRules.isValid())
1139 if (updateRules.isFunction()) {
1140 QDomDocument scriptDoc;
1141 scriptDoc.appendChild(scriptDoc.importNode(effect, true));
1143 QString effectString = updateRules.call(QScriptValue(), QScriptValueList() << serviceVersion << effectVersion << scriptDoc.toString()).toString();
1145 if (!effectString.isEmpty() && !scriptEngine.hasUncaughtException()) {
1146 scriptDoc.setContent(effectString);
1147 QDomNode updatedEffect = effect.ownerDocument().importNode(scriptDoc.documentElement(), true);
1148 effect.parentNode().replaceChild(updatedEffect, effect);
1152 m_modified = updateEffectParameters(effect.childNodes(), &updateRules, serviceVersion, effectVersion);
1155 // set version number since MLT won't change it (only initially set it)
1156 QDomElement versionElem = effect.firstChildElement("version");
1157 if (EffectsList::property(effect, "version").isNull()) {
1158 versionElem = effect.ownerDocument().createTextNode(QLocale().toString(serviceVersion)).toElement();
1159 versionElem.setTagName("property");
1160 versionElem.setAttribute("name", "version");
1161 effect.appendChild(versionElem);
1163 EffectsList::setProperty(effect, "version", QLocale().toString(serviceVersion));
1166 else safeEffects.append(effectId);
1171 bool DocumentValidator::updateEffectParameters(QDomNodeList parameters, const QScriptValue* updateRules, const double serviceVersion, const double effectVersion)
1173 bool updated = false;
1174 bool isDowngrade = serviceVersion < effectVersion;
1175 for (int i = 0; i < parameters.count(); ++i) {
1176 QDomElement parameter = parameters.at(i).toElement();
1177 QScriptValue rules = updateRules->property(parameter.attribute("name"));
1178 if (rules.isValid() && rules.isArray()) {
1179 int rulesCount = rules.property("length").toInt32();
1181 // start with the highest version and downgrade step by step
1182 for (int j = rulesCount - 1; j >= 0; --j) {
1183 double version = rules.property(j).property(0).toNumber();
1184 if (version <= effectVersion && version > serviceVersion) {
1185 parameter.firstChild().setNodeValue(rules.property(j).property(1).call(QScriptValue(), QScriptValueList() << parameter.text() << isDowngrade).toString());
1190 for (int j = 0; j < rulesCount; ++j) {
1191 double version = rules.property(j).property(0).toNumber();
1192 if (version > effectVersion && version <= serviceVersion) {
1193 parameter.firstChild().setNodeValue(rules.property(j).property(1).call(QScriptValue(), QScriptValueList() << parameter.text() << isDowngrade).toString());