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(const QDomDocument &doc, const 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);
116 // Last try: replace comma with a dot
117 QString versionString = kdenliveDoc.attribute("version");
118 if (versionString.contains(',')) versionString.replace(',', '.');
119 version = versionString.toDouble(&ok);
120 if (!ok) kDebug()<<"// CANNOT PARSE VERSION NUMBER, ERROR!";
124 // Upgrade the document to the latest version
125 if (!upgrade(version, currentVersion))
129 * Check the syntax (this will be replaced by XSD validation with Qt 4.6)
130 * and correct some errors
133 // Return (or create) the tractor
134 QDomElement tractor = mlt.firstChildElement("tractor");
135 if (tractor.isNull()) {
137 tractor = m_doc.createElement("tractor");
138 tractor.setAttribute("global_feed", "1");
139 tractor.setAttribute("in", "0");
140 tractor.setAttribute("out", "-1");
141 tractor.setAttribute("id", "maintractor");
142 mlt.appendChild(tractor);
146 * Make sure at least one track exists, and they're equal in number to
147 * to the maximum between MLT and Kdenlive playlists and tracks
149 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
150 int tracksMax = playlists.count() - 1; // Remove the black track
151 QDomNodeList tracks = tractor.elementsByTagName("track");
152 tracksMax = qMax(tracks.count() - 1, tracksMax);
153 QDomNodeList tracksinfo = kdenliveDoc.elementsByTagName("trackinfo");
154 tracksMax = qMax(tracksinfo.count(), tracksMax);
155 tracksMax = qMax(1, tracksMax); // Force existance of one track
156 if (playlists.count() - 1 < tracksMax ||
157 tracks.count() - 1 < tracksMax ||
158 tracksinfo.count() < tracksMax) {
159 kDebug() << "//// WARNING, PROJECT IS CORRUPTED, MISSING TRACK";
162 // use the MLT tracks as reference
163 if (tracks.count() - 1 < tracksMax) {
164 // Looks like one MLT track is missing, remove the extra Kdenlive track if there is one.
165 if (tracksinfo.count() != tracks.count() - 1) {
166 // The Kdenlive tracks are not ok, clear and rebuild them
167 QDomNode tinfo = kdenliveDoc.firstChildElement("tracksinfo");
168 QDomNode tnode = tinfo.firstChild();
169 while (!tnode.isNull()) {
170 tinfo.removeChild(tnode);
171 tnode = tinfo.firstChild();
174 for (int i = 1; i < tracks.count(); ++i) {
175 QString hide = tracks.at(i).toElement().attribute("hide");
176 QDomElement newTrack = m_doc.createElement("trackinfo");
177 if (hide == "video") {
179 newTrack.setAttribute("type", "audio");
180 newTrack.setAttribute("blind", 1);
181 newTrack.setAttribute("mute", 0);
182 newTrack.setAttribute("lock", 0);
184 newTrack.setAttribute("blind", 0);
185 newTrack.setAttribute("mute", 0);
186 newTrack.setAttribute("lock", 0);
188 tinfo.appendChild(newTrack);
193 if (playlists.count() - 1 < tracksMax) {
194 difference = tracksMax - (playlists.count() - 1);
195 for (int i = 0; i < difference; ++i) {
196 QDomElement playlist = m_doc.createElement("playlist");
197 mlt.appendChild(playlist);
200 if (tracks.count() - 1 < tracksMax) {
201 difference = tracksMax - (tracks.count() - 1);
202 for (int i = 0; i < difference; ++i) {
203 QDomElement track = m_doc.createElement("track");
204 tractor.appendChild(track);
207 if (tracksinfo.count() < tracksMax) {
208 QDomElement tracksinfoElm = kdenliveDoc.firstChildElement("tracksinfo");
209 if (tracksinfoElm.isNull()) {
210 tracksinfoElm = m_doc.createElement("tracksinfo");
211 kdenliveDoc.appendChild(tracksinfoElm);
213 difference = tracksMax - tracksinfo.count();
214 for (int i = 0; i < difference; ++i) {
215 QDomElement trackinfo = m_doc.createElement("trackinfo");
216 trackinfo.setAttribute("mute", "0");
217 trackinfo.setAttribute("locked", "0");
218 tracksinfoElm.appendChild(trackinfo);
223 // TODO: check the tracks references
224 // TODO: check internal mix transitions
233 bool DocumentValidator::upgrade(double version, const double currentVersion)
235 kDebug() << "Opening a document with version " << version;
237 // No conversion needed
238 if (version == currentVersion) {
242 // The document is too new
243 if (version > currentVersion) {
244 kDebug() << "Unable to open document with version " << version;
245 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"));
249 // Unsupported document versions
250 if (version == 0.5 || version == 0.7) {
251 kDebug() << "Unable to open document with version " << version;
252 KMessageBox::sorry(kapp->activeWindow(), i18n("This project type is unsupported (version %1) and can't be loaded.", version), i18n("Unable to open project"));
257 QDomNode infoXmlNode = m_doc.elementsByTagName("kdenlivedoc").at(0);
258 QDomElement infoXml = infoXmlNode.toElement();
259 infoXml.setAttribute("upgraded", "1");
261 if (version <= 0.6) {
262 QDomElement infoXml_old = infoXmlNode.cloneNode(true).toElement(); // Needed for folders
263 QDomNode westley = m_doc.elementsByTagName("westley").at(1);
264 QDomNode tractor = m_doc.elementsByTagName("tractor").at(0);
265 QDomNode multitrack = m_doc.elementsByTagName("multitrack").at(0);
266 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
268 QDomNode props = m_doc.elementsByTagName("properties").at(0).toElement();
269 QString profile = props.toElement().attribute("videoprofile");
270 int startPos = props.toElement().attribute("timeline_position").toInt();
271 if (profile == "dv_wide")
272 profile = "dv_pal_wide";
274 // move playlists outside of tractor and add the tracks instead
275 int max = playlists.count();
276 if (westley.isNull()) {
277 westley = m_doc.createElement("westley");
278 m_doc.documentElement().appendChild(westley);
280 if (tractor.isNull()) {
281 kDebug() << "// NO MLT PLAYLIST, building empty one";
282 QDomElement blank_tractor = m_doc.createElement("tractor");
283 westley.appendChild(blank_tractor);
284 QDomElement blank_playlist = m_doc.createElement("playlist");
285 blank_playlist.setAttribute("id", "black_track");
286 westley.insertBefore(blank_playlist, QDomNode());
287 QDomElement blank_track = m_doc.createElement("track");
288 blank_track.setAttribute("producer", "black_track");
289 blank_tractor.appendChild(blank_track);
291 QDomNodeList kdenlivetracks = m_doc.elementsByTagName("kdenlivetrack");
292 for (int i = 0; i < kdenlivetracks.count(); ++i) {
293 blank_playlist = m_doc.createElement("playlist");
294 blank_playlist.setAttribute("id", "playlist" + QString::number(i));
295 westley.insertBefore(blank_playlist, QDomNode());
296 blank_track = m_doc.createElement("track");
297 blank_track.setAttribute("producer", "playlist" + QString::number(i));
298 blank_tractor.appendChild(blank_track);
299 if (kdenlivetracks.at(i).toElement().attribute("cliptype") == "Sound") {
300 blank_playlist.setAttribute("hide", "video");
301 blank_track.setAttribute("hide", "video");
304 } else for (int i = 0; i < max; ++i) {
305 QDomNode n = playlists.at(i);
306 westley.insertBefore(n, QDomNode());
307 QDomElement pl = n.toElement();
308 QDomElement track = m_doc.createElement("track");
309 QString trackType = pl.attribute("hide");
310 if (!trackType.isEmpty())
311 track.setAttribute("hide", trackType);
312 QString playlist_id = pl.attribute("id");
313 if (playlist_id.isEmpty()) {
314 playlist_id = "black_track";
315 pl.setAttribute("id", playlist_id);
317 track.setAttribute("producer", playlist_id);
318 //tractor.appendChild(track);
319 #define KEEP_TRACK_ORDER 1
320 #ifdef KEEP_TRACK_ORDER
321 tractor.insertAfter(track, QDomNode());
323 // Insert the new track in an order that hopefully matches the 3 video, then 2 audio tracks of Kdenlive 0.7.0
324 // insertion sort - O( tracks*tracks )
325 // Note, this breaks _all_ transitions - but you can move them up and down afterwards.
326 QDomElement tractor_elem = tractor.toElement();
327 if (! tractor_elem.isNull()) {
328 QDomNodeList tracks = tractor_elem.elementsByTagName("track");
329 int size = tracks.size();
331 tractor.insertAfter(track, QDomNode());
333 bool inserted = false;
334 for (int i = 0; i < size; ++i) {
335 QDomElement track_elem = tracks.at(i).toElement();
336 if (track_elem.isNull()) {
337 tractor.insertAfter(track, QDomNode());
341 kDebug() << "playlist_id: " << playlist_id << " producer:" << track_elem.attribute("producer");
342 if (playlist_id < track_elem.attribute("producer")) {
343 tractor.insertBefore(track, track_elem);
349 // Reach here, no insertion, insert last
351 tractor.insertAfter(track, QDomNode());
355 kWarning() << "tractor was not a QDomElement";
356 tractor.insertAfter(track, QDomNode());
360 tractor.removeChild(multitrack);
362 // audio track mixing transitions should not be added to track view, so add required attribute
363 QDomNodeList transitions = m_doc.elementsByTagName("transition");
364 max = transitions.count();
365 for (int i = 0; i < max; ++i) {
366 QDomElement tr = transitions.at(i).toElement();
367 if (tr.attribute("combine") == "1" && tr.attribute("mlt_service") == "mix") {
368 QDomElement property = m_doc.createElement("property");
369 property.setAttribute("name", "internal_added");
370 QDomText value = m_doc.createTextNode("237");
371 property.appendChild(value);
372 tr.appendChild(property);
373 property = m_doc.createElement("property");
374 property.setAttribute("name", "mlt_service");
375 value = m_doc.createTextNode("mix");
376 property.appendChild(value);
377 tr.appendChild(property);
379 // convert transition
380 QDomNamedNodeMap attrs = tr.attributes();
381 for (int j = 0; j < attrs.count(); j++) {
382 QString attrName = attrs.item(j).nodeName();
383 if (attrName != "in" && attrName != "out" && attrName != "id") {
384 QDomElement property = m_doc.createElement("property");
385 property.setAttribute("name", attrName);
386 QDomText value = m_doc.createTextNode(attrs.item(j).nodeValue());
387 property.appendChild(value);
388 tr.appendChild(property);
394 // move transitions after tracks
395 for (int i = 0; i < max; ++i) {
396 tractor.insertAfter(transitions.at(0), QDomNode());
399 // Fix filters format
400 QDomNodeList entries = m_doc.elementsByTagName("entry");
401 max = entries.count();
402 for (int i = 0; i < max; ++i) {
405 QDomNode m = entries.at(i).firstChild();
406 while (!m.isNull()) {
407 if (m.toElement().tagName() == "filter") {
408 QDomElement filt = m.toElement();
409 QDomNamedNodeMap attrs = filt.attributes();
410 QString current_id = filt.attribute("kdenlive_id");
411 if (current_id != last_id) {
413 last_id = current_id;
415 QDomElement e = m_doc.createElement("property");
416 e.setAttribute("name", "kdenlive_ix");
417 QDomText value = m_doc.createTextNode(QString::number(effectix));
418 e.appendChild(value);
420 for (int j = 0; j < attrs.count(); j++) {
421 QDomAttr a = attrs.item(j).toAttr();
423 kDebug() << " FILTER; adding :" << a.name() << ":" << a.value();
424 QDomElement e = m_doc.createElement("property");
425 e.setAttribute("name", a.name());
426 QDomText value = m_doc.createTextNode(a.value());
427 e.appendChild(value);
438 QDomNodeList filters = m_doc.elementsByTagName("filter");
439 max = filters.count();
442 for (int i = 0; i < max; ++i) {
443 QDomElement filt = filters.at(i).toElement();
444 QDomNamedNodeMap attrs = filt.attributes();
445 QString current_id = filt.attribute("kdenlive_id");
446 if (current_id != last_id) {
448 last_id = current_id;
450 QDomElement e = m_doc.createElement("property");
451 e.setAttribute("name", "kdenlive_ix");
452 QDomText value = m_doc.createTextNode(QString::number(1));
453 e.appendChild(value);
455 for (int j = 0; j < attrs.count(); j++) {
456 QDomAttr a = attrs.item(j).toAttr();
458 kDebug() << " FILTER; adding :" << a.name() << ":" << a.value();
459 QDomElement e = m_doc.createElement("property");
460 e.setAttribute("name", a.name());
461 QDomText value = m_doc.createTextNode(a.value());
462 e.appendChild(value);
469 QDomNodeList producers = westley.toElement().elementsByTagName("producer");
470 max = producers.count();
471 for (int i = 0; i < max; ++i) {
472 QDomElement prod = producers.at(i).toElement();
473 if (prod.attribute("mlt_service") == "framebuffer") {
474 QString slowmotionprod = prod.attribute("resource");
475 slowmotionprod.replace(':', '?');
476 kDebug() << "// FOUND WRONG SLOWMO, new: " << slowmotionprod;
477 prod.setAttribute("resource", slowmotionprod);
480 // move producers to correct place, markers to a global list, fix clip descriptions
481 QDomElement markers = m_doc.createElement("markers");
482 // This will get the xml producers:
483 producers = m_doc.elementsByTagName("producer");
484 max = producers.count();
485 for (int i = 0; i < max; ++i) {
486 QDomElement prod = producers.at(0).toElement();
487 // add resource also as a property (to allow path correction in setNewResource())
488 // TODO: will it work with slowmotion? needs testing
489 /*if (!prod.attribute("resource").isEmpty()) {
490 QDomElement prop_resource = m_doc.createElement("property");
491 prop_resource.setAttribute("name", "resource");
492 QDomText resource = m_doc.createTextNode(prod.attribute("resource"));
493 prop_resource.appendChild(resource);
494 prod.appendChild(prop_resource);
496 QDomNode m = prod.firstChild();
498 if (m.toElement().tagName() == "markers") {
499 QDomNodeList prodchilds = m.childNodes();
500 int maxchild = prodchilds.count();
501 for (int k = 0; k < maxchild; k++) {
502 QDomElement mark = prodchilds.at(0).toElement();
503 mark.setAttribute("id", prod.attribute("id"));
504 markers.insertAfter(mark, QDomNode());
507 } else if (prod.attribute("type").toInt() == TEXT) {
508 // convert title clip
509 if (m.toElement().tagName() == "textclip") {
511 QDomElement titleclip = m.toElement();
512 QDomElement title = tdoc.createElement("kdenlivetitle");
513 tdoc.appendChild(title);
514 QDomNodeList objects = titleclip.childNodes();
515 int maxchild = objects.count();
516 for (int k = 0; k < maxchild; k++) {
518 QDomElement ob = objects.at(k).toElement();
519 if (ob.attribute("type") == "3") {
520 // text object - all of this goes into "xmldata"...
521 QDomElement item = tdoc.createElement("item");
522 item.setAttribute("z-index", ob.attribute("z"));
523 item.setAttribute("type", "QGraphicsTextItem");
524 QDomElement position = tdoc.createElement("position");
525 position.setAttribute("x", ob.attribute("x"));
526 position.setAttribute("y", ob.attribute("y"));
527 QDomElement content = tdoc.createElement("content");
528 content.setAttribute("font", ob.attribute("font_family"));
529 content.setAttribute("font-size", ob.attribute("font_size"));
530 content.setAttribute("font-bold", ob.attribute("bold"));
531 content.setAttribute("font-italic", ob.attribute("italic"));
532 content.setAttribute("font-underline", ob.attribute("underline"));
533 QString col = ob.attribute("color");
535 content.setAttribute("font-color", colorToString(c));
536 // todo: These fields are missing from the newly generated xmldata:
537 // transform, startviewport, endviewport, background
539 QDomText conttxt = tdoc.createTextNode(ob.attribute("text"));
540 content.appendChild(conttxt);
541 item.appendChild(position);
542 item.appendChild(content);
543 title.appendChild(item);
544 } else if (ob.attribute("type") == "5") {
546 QDomElement item = tdoc.createElement("item");
547 item.setAttribute("z-index", ob.attribute("z"));
548 item.setAttribute("type", "QGraphicsRectItem");
549 QDomElement position = tdoc.createElement("position");
550 position.setAttribute("x", ob.attribute("x"));
551 position.setAttribute("y", ob.attribute("y"));
552 QDomElement content = tdoc.createElement("content");
553 QString col = ob.attribute("color");
555 content.setAttribute("brushcolor", colorToString(c));
556 QString rect = "0,0,";
557 rect.append(ob.attribute("width"));
559 rect.append(ob.attribute("height"));
560 content.setAttribute("rect", rect);
561 item.appendChild(position);
562 item.appendChild(content);
563 title.appendChild(item);
566 prod.setAttribute("xmldata", tdoc.toString());
567 // mbd todo: This clearly does not work, as every title gets the same name - trying to leave it empty
568 // QStringList titleInfo = TitleWidget::getFreeTitleInfo(projectFolder());
569 // prod.setAttribute("titlename", titleInfo.at(0));
570 // prod.setAttribute("resource", titleInfo.at(1));
571 //kDebug()<<"TITLE DATA:\n"<<tdoc.toString();
573 } // End conversion of title clips.
575 } else if (m.isText()) {
576 QString comment = m.nodeValue();
577 if (!comment.isEmpty()) {
578 prod.setAttribute("description", comment);
583 int duration = prod.attribute("duration").toInt();
584 if (duration > 0) prod.setAttribute("out", QString::number(duration));
585 // The clip goes back in, but text clips should not go back in, at least not modified
586 westley.insertBefore(prod, QDomNode());
589 QDomNode westley0 = m_doc.elementsByTagName("westley").at(0);
590 if (!markers.firstChild().isNull()) westley0.appendChild(markers);
593 * Convert as much of the kdenlivedoc as possible. Use the producer in
594 * westley. First, remove the old stuff from westley, and add a new
595 * empty one. Also, track the max id in order to use it for the adding
598 int max_kproducer_id = 0;
599 westley0.removeChild(infoXmlNode);
600 QDomElement infoXml_new = m_doc.createElement("kdenlivedoc");
601 infoXml_new.setAttribute("profile", profile);
602 infoXml.setAttribute("position", startPos);
604 // Add all the producers that has a resource in westley
605 QDomElement westley_element = westley0.toElement();
606 if (westley_element.isNull()) {
607 kWarning() << "westley0 element in document was not a QDomElement - unable to add producers to new kdenlivedoc";
609 QDomNodeList wproducers = westley_element.elementsByTagName("producer");
610 int kmax = wproducers.count();
611 for (int i = 0; i < kmax; ++i) {
612 QDomElement wproducer = wproducers.at(i).toElement();
613 if (wproducer.isNull()) {
614 kWarning() << "Found producer in westley0, that was not a QDomElement";
617 if (wproducer.attribute("id") == "black") continue;
618 // We have to do slightly different things, depending on the type
619 kDebug() << "Converting producer element with type" << wproducer.attribute("type");
620 if (wproducer.attribute("type").toInt() == TEXT) {
621 kDebug() << "Found TEXT element in producer" << endl;
622 QDomElement kproducer = wproducer.cloneNode(true).toElement();
623 kproducer.setTagName("kdenlive_producer");
624 infoXml_new.appendChild(kproducer);
626 * TODO: Perhaps needs some more changes here to
627 * "frequency", aspect ratio as a float, frame_size,
628 * channels, and later, resource and title name
631 QDomElement kproducer = m_doc.createElement("kdenlive_producer");
632 kproducer.setAttribute("id", wproducer.attribute("id"));
633 if (!wproducer.attribute("description").isEmpty())
634 kproducer.setAttribute("description", wproducer.attribute("description"));
635 kproducer.setAttribute("resource", wproducer.attribute("resource"));
636 kproducer.setAttribute("type", wproducer.attribute("type"));
637 // Testing fix for 358
638 if (!wproducer.attribute("aspect_ratio").isEmpty()) {
639 kproducer.setAttribute("aspect_ratio", wproducer.attribute("aspect_ratio"));
641 if (!wproducer.attribute("source_fps").isEmpty()) {
642 kproducer.setAttribute("fps", wproducer.attribute("source_fps"));
644 if (!wproducer.attribute("length").isEmpty()) {
645 kproducer.setAttribute("duration", wproducer.attribute("length"));
647 infoXml_new.appendChild(kproducer);
649 if (wproducer.attribute("id").toInt() > max_kproducer_id) {
650 max_kproducer_id = wproducer.attribute("id").toInt();
654 #define LOOKUP_FOLDER 1
657 * Look through all the folder elements of the old doc, for each folder,
658 * for each producer, get the id, look it up in the new doc, set the
659 * groupname and groupid. Note, this does not work at the moment - at
660 * least one folder shows up missing, and clips with no folder does not
663 //QDomElement infoXml_old = infoXmlNode.toElement();
664 if (!infoXml_old.isNull()) {
665 QDomNodeList folders = infoXml_old.elementsByTagName("folder");
666 int fsize = folders.size();
667 int groupId = max_kproducer_id + 1; // Start at +1 of max id of the kdenlive_producers
668 for (int i = 0; i < fsize; ++i) {
669 QDomElement folder = folders.at(i).toElement();
670 if (!folder.isNull()) {
671 QString groupName = folder.attribute("name");
672 kDebug() << "groupName: " << groupName << " with groupId: " << groupId;
673 QDomNodeList fproducers = folder.elementsByTagName("producer");
674 int psize = fproducers.size();
675 for (int j = 0; j < psize; ++j) {
676 QDomElement fproducer = fproducers.at(j).toElement();
677 if (!fproducer.isNull()) {
678 QString id = fproducer.attribute("id");
679 // This is not very effective, but compared to loading the clips, its a breeze
680 QDomNodeList kdenlive_producers = infoXml_new.elementsByTagName("kdenlive_producer");
681 int kpsize = kdenlive_producers.size();
682 for (int k = 0; k < kpsize; ++k) {
683 QDomElement kproducer = kdenlive_producers.at(k).toElement(); // Its an element for sure
684 if (id == kproducer.attribute("id")) {
685 // We do not check that it already is part of a folder
686 kproducer.setAttribute("groupid", groupId);
687 kproducer.setAttribute("groupname", groupName);
698 QDomNodeList elements = westley.childNodes();
699 max = elements.count();
700 for (int i = 0; i < max; ++i) {
701 QDomElement prod = elements.at(0).toElement();
702 westley0.insertAfter(prod, QDomNode());
705 westley0.appendChild(infoXml_new);
707 westley0.removeChild(westley);
709 // adds <avfile /> information to <kdenlive_producer />
710 QDomNodeList kproducers = m_doc.elementsByTagName("kdenlive_producer");
711 QDomNodeList avfiles = infoXml_old.elementsByTagName("avfile");
712 kDebug() << "found" << avfiles.count() << "<avfile />s and" << kproducers.count() << "<kdenlive_producer />s";
713 for (int i = 0; i < avfiles.count(); ++i) {
714 QDomElement avfile = avfiles.at(i).toElement();
715 QDomElement kproducer;
717 kWarning() << "found an <avfile /> that is not a QDomElement";
719 QString id = avfile.attribute("id");
720 // this is horrible, must be rewritten, it's just for test
721 for (int j = 0; j < kproducers.count(); ++j) {
722 //kDebug() << "checking <kdenlive_producer /> with id" << kproducers.at(j).toElement().attribute("id");
723 if (kproducers.at(j).toElement().attribute("id") == id) {
724 kproducer = kproducers.at(j).toElement();
728 if (kproducer == QDomElement())
729 kWarning() << "no match for <avfile /> with id =" << id;
731 //kDebug() << "ready to set additional <avfile />'s attributes (id =" << id << ")";
732 kproducer.setAttribute("channels", avfile.attribute("channels"));
733 kproducer.setAttribute("duration", avfile.attribute("duration"));
734 kproducer.setAttribute("frame_size", avfile.attribute("width") + 'x' + avfile.attribute("height"));
735 kproducer.setAttribute("frequency", avfile.attribute("frequency"));
736 if (kproducer.attribute("description").isEmpty() && !avfile.attribute("description").isEmpty())
737 kproducer.setAttribute("description", avfile.attribute("description"));
741 infoXml = infoXml_new;
744 if (version <= 0.81) {
745 // Add the tracks information
746 QString tracksOrder = infoXml.attribute("tracks");
747 if (tracksOrder.isEmpty()) {
748 QDomNodeList tracks = m_doc.elementsByTagName("track");
749 for (int i = 0; i < tracks.count(); ++i) {
750 QDomElement track = tracks.at(i).toElement();
751 if (track.attribute("producer") != "black_track") {
752 if (track.attribute("hide") == "video")
753 tracksOrder.append('a');
755 tracksOrder.append('v');
759 QDomElement tracksinfo = m_doc.createElement("tracksinfo");
760 for (int i = 0; i < tracksOrder.size(); ++i) {
761 QDomElement trackinfo = m_doc.createElement("trackinfo");
762 if (tracksOrder.data()[i] == 'a') {
763 trackinfo.setAttribute("type", "audio");
764 trackinfo.setAttribute("blind", true);
766 trackinfo.setAttribute("blind", false);
767 trackinfo.setAttribute("mute", false);
768 trackinfo.setAttribute("locked", false);
769 tracksinfo.appendChild(trackinfo);
771 infoXml.appendChild(tracksinfo);
774 if (version <= 0.82) {
775 // Convert <westley />s in <mlt />s (MLT extreme makeover)
776 QDomNodeList westleyNodes = m_doc.elementsByTagName("westley");
777 for (int i = 0; i < westleyNodes.count(); ++i) {
778 QDomElement westley = westleyNodes.at(i).toElement();
779 westley.setTagName("mlt");
783 if (version <= 0.83) {
784 // Replace point size with pixel size in text titles
785 if (m_doc.toString().contains("font-size")) {
786 KMessageBox::ButtonCode convert = KMessageBox::Continue;
787 QDomNodeList kproducerNodes = m_doc.elementsByTagName("kdenlive_producer");
788 for (int i = 0; i < kproducerNodes.count() && convert != KMessageBox::No; ++i) {
789 QDomElement kproducer = kproducerNodes.at(i).toElement();
790 if (kproducer.attribute("type").toInt() == TEXT) {
792 data.setContent(kproducer.attribute("xmldata"));
793 QDomNodeList items = data.firstChild().childNodes();
794 for (int j = 0; j < items.count() && convert != KMessageBox::No; ++j) {
795 if (items.at(j).attributes().namedItem("type").nodeValue() == "QGraphicsTextItem") {
796 QDomNamedNodeMap textProperties = items.at(j).namedItem("content").attributes();
797 if (textProperties.namedItem("font-pixel-size").isNull() && !textProperties.namedItem("font-size").isNull()) {
798 // Ask the user if he wants to convert
799 if (convert != KMessageBox::Yes && convert != KMessageBox::No)
800 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"));
801 if (convert == KMessageBox::Yes) {
803 font.setPointSize(textProperties.namedItem("font-size").nodeValue().toInt());
804 QDomElement content = items.at(j).namedItem("content").toElement();
805 content.setAttribute("font-pixel-size", QFontInfo(font).pixelSize());
806 content.removeAttribute("font-size");
807 kproducer.setAttribute("xmldata", data.toString());
809 * You may be tempted to delete the preview file
810 * to force its recreation: bad idea (see
811 * http://www.kdenlive.org/mantis/view.php?id=749)
821 // Fill the <documentproperties /> element
822 QDomElement docProperties = infoXml.firstChildElement("documentproperties");
823 if (docProperties.isNull()) {
824 docProperties = m_doc.createElement("documentproperties");
825 docProperties.setAttribute("zonein", infoXml.attribute("zonein"));
826 docProperties.setAttribute("zoneout", infoXml.attribute("zoneout"));
827 docProperties.setAttribute("zoom", infoXml.attribute("zoom"));
828 docProperties.setAttribute("position", infoXml.attribute("position"));
829 infoXml.appendChild(docProperties);
833 if (version <= 0.84) {
834 // update the title clips to use the new MLT kdenlivetitle producer
835 QDomNodeList kproducerNodes = m_doc.elementsByTagName("kdenlive_producer");
836 for (int i = 0; i < kproducerNodes.count(); ++i) {
837 QDomElement kproducer = kproducerNodes.at(i).toElement();
838 if (kproducer.attribute("type").toInt() == TEXT) {
839 QString data = kproducer.attribute("xmldata");
840 QString datafile = kproducer.attribute("resource");
841 if (!datafile.endsWith(".kdenlivetitle")) {
842 datafile = QString();
843 kproducer.setAttribute("resource", QString());
845 QString id = kproducer.attribute("id");
846 QDomNodeList mltproducers = m_doc.elementsByTagName("producer");
847 bool foundData = false;
848 bool foundResource = false;
849 bool foundService = false;
850 for (int j = 0; j < mltproducers.count(); j++) {
851 QDomElement wproducer = mltproducers.at(j).toElement();
852 if (wproducer.attribute("id") == id) {
853 QDomNodeList props = wproducer.childNodes();
854 for (int k = 0; k < props.count(); k++) {
855 if (props.at(k).toElement().attribute("name") == "xmldata") {
856 props.at(k).firstChild().setNodeValue(data);
858 } else if (props.at(k).toElement().attribute("name") == "mlt_service") {
859 props.at(k).firstChild().setNodeValue("kdenlivetitle");
861 } else if (props.at(k).toElement().attribute("name") == "resource") {
862 props.at(k).firstChild().setNodeValue(datafile);
863 foundResource = true;
867 QDomElement e = m_doc.createElement("property");
868 e.setAttribute("name", "xmldata");
869 QDomText value = m_doc.createTextNode(data);
870 e.appendChild(value);
871 wproducer.appendChild(e);
874 QDomElement e = m_doc.createElement("property");
875 e.setAttribute("name", "mlt_service");
876 QDomText value = m_doc.createTextNode("kdenlivetitle");
877 e.appendChild(value);
878 wproducer.appendChild(e);
880 if (!foundResource) {
881 QDomElement e = m_doc.createElement("property");
882 e.setAttribute("name", "resource");
883 QDomText value = m_doc.createTextNode(datafile);
884 e.appendChild(value);
885 wproducer.appendChild(e);
893 if (version <= 0.85) {
894 // update the LADSPA effects to use the new ladspa.id format instead of external xml file
895 QDomNodeList effectNodes = m_doc.elementsByTagName("filter");
896 for (int i = 0; i < effectNodes.count(); ++i) {
897 QDomElement effect = effectNodes.at(i).toElement();
898 if (EffectsList::property(effect, "mlt_service") == "ladspa") {
899 // Needs to be converted
900 QStringList info = getInfoFromEffectName(EffectsList::property(effect, "kdenlive_id"));
901 if (info.isEmpty()) continue;
902 // info contains the correct ladspa.id from kdenlive effect name, and a list of parameter's old and new names
903 EffectsList::setProperty(effect, "kdenlive_id", info.at(0));
904 EffectsList::setProperty(effect, "tag", info.at(0));
905 EffectsList::setProperty(effect, "mlt_service", info.at(0));
906 EffectsList::removeProperty(effect, "src");
907 for (int j = 1; j < info.size(); j++) {
908 QString value = EffectsList::property(effect, info.at(j).section('=', 0, 0));
909 if (!value.isEmpty()) {
910 // update parameter name
911 EffectsList::renameProperty(effect, info.at(j).section('=', 0, 0), info.at(j).section('=', 1, 1));
918 if (version <= 0.86) {
919 // Make sure we don't have avformat-novalidate producers, since it caused crashes
920 QDomNodeList producers = m_doc.elementsByTagName("producer");
921 int max = producers.count();
922 for (int i = 0; i < max; ++i) {
923 QDomElement prod = producers.at(i).toElement();
924 if (EffectsList::property(prod, "mlt_service") == "avformat-novalidate")
925 EffectsList::setProperty(prod, "mlt_service", "avformat");
928 // 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
930 // Get profile info (width / height)
933 QDomElement profile = m_doc.firstChildElement("profile");
934 if (profile.isNull()) profile = infoXml.firstChildElement("profileinfo");
935 if (profile.isNull()) {
936 // could not find profile info, set PAL
941 profileWidth = profile.attribute("width").toInt();
942 profileHeight = profile.attribute("height").toInt();
944 QDomNodeList transitions = m_doc.elementsByTagName("transition");
945 max = transitions.count();
947 for (int i = 0; i < max; ++i) {
948 QDomElement trans = transitions.at(i).toElement();
949 out = trans.attribute("out").toInt() - trans.attribute("in").toInt();
950 QString geom = EffectsList::property(trans, "geometry");
951 Mlt::Geometry *g = new Mlt::Geometry(geom.toUtf8().data(), out, profileWidth, profileHeight);
952 Mlt::GeometryItem item;
953 if (g->next_key(&item, out) == 0) {
954 // We have a keyframe just after last frame, try to move it to last frame
955 if (item.frame() == out + 1) {
959 EffectsList::setProperty(trans, "geometry", g->serialise());
966 if (version <= 0.87) {
967 if (!m_doc.firstChildElement("mlt").hasAttribute("LC_NUMERIC")) {
968 m_doc.firstChildElement("mlt").setAttribute("LC_NUMERIC", "C");
972 // The document has been converted: mark it as modified
973 infoXml.setAttribute("version", currentVersion);
978 QStringList DocumentValidator::getInfoFromEffectName(const QString &oldName)
981 // Returns a list to convert old Kdenlive ladspa effects
982 if (oldName == "pitch_shift") {
983 info << "ladspa.1433";
986 else if (oldName == "vinyl") {
987 info << "ladspa.1905";
994 else if (oldName == "room_reverb") {
995 info << "ladspa.1216";
1000 else if (oldName == "reverb") {
1001 info << "ladspa.1423";
1005 else if (oldName == "rate_scale") {
1006 info << "ladspa.1417";
1009 else if (oldName == "pitch_scale") {
1010 info << "ladspa.1193";
1013 else if (oldName == "phaser") {
1014 info << "ladspa.1217";
1017 info << "feedback=2";
1020 else if (oldName == "limiter") {
1021 info << "ladspa.1913";
1024 info << "release=2";
1026 else if (oldName == "equalizer_15") {
1027 info << "ladspa.1197";
1044 else if (oldName == "equalizer") {
1045 info << "ladspa.1901";
1047 info << "midgain=1";
1050 else if (oldName == "declipper") {
1051 info << "ladspa.1195";
1056 QString DocumentValidator::colorToString(const QColor& c)
1058 QString ret = "%1,%2,%3,%4";
1059 ret = ret.arg(c.red()).arg(c.green()).arg(c.blue()).arg(c.alpha());
1063 bool DocumentValidator::isProject() const
1065 QDomNode infoXmlNode = m_doc.elementsByTagName("kdenlivedoc").at(0);
1066 return !infoXmlNode.isNull();
1069 bool DocumentValidator::isModified() const
1074 void DocumentValidator::updateEffects()
1076 // WARNING: order by findDirs will determine which js file to use (in case multiple scripts for the same filter exist)
1077 QMap <QString, KUrl> paths;
1078 #if QT_VERSION >= 0x040700
1079 QMap <QString, QScriptProgram> scripts;
1081 QMap <QString, QString> scripts;
1083 QStringList directories = KGlobal::dirs()->findDirs("appdata", "effects/update");
1084 foreach (const QString &directoryName, directories) {
1085 QDir directory(directoryName);
1086 QStringList fileList = directory.entryList(QStringList() << "*.js", QDir::Files);
1087 foreach (const QString &fileName, fileList) {
1088 QString identifier = fileName;
1089 // remove extension (".js")
1091 paths.insert(identifier, KUrl(directoryName + fileName));
1095 QDomNodeList effects = m_doc.elementsByTagName("filter");
1096 int max = effects.count();
1097 QStringList safeEffects;
1098 for(int i = 0; i < max; ++i) {
1099 QDomElement effect = effects.at(i).toElement();
1100 QString effectId = EffectsList::property(effect, "kdenlive_id");
1101 if (safeEffects.contains(effectId)) {
1102 // Do not check the same effect twice if it is at the correct version
1103 // (assume we don't have different versions of the same effect in a project file)
1106 QString effectTag = EffectsList::property(effect, "tag");
1107 QString effectVersionStr = EffectsList::property(effect, "version");
1108 double effectVersion = effectVersionStr.isNull() ? -1 : effectVersionStr.toDouble();
1110 QDomElement effectDescr = MainWindow::customEffects.getEffectByTag(QString(), effectId);
1111 if (effectDescr.isNull()) {
1112 effectDescr = MainWindow::videoEffects.getEffectByTag(effectTag, effectId);
1114 if (effectDescr.isNull()) {
1115 effectDescr = MainWindow::audioEffects.getEffectByTag(effectTag, effectId);
1117 if (!effectDescr.isNull()) {
1118 double serviceVersion = -1;
1119 QDomElement serviceVersionElem = effectDescr.firstChildElement("version");
1120 if (!serviceVersionElem.isNull()) {
1121 serviceVersion = serviceVersionElem.text().toDouble();
1123 if (serviceVersion != effectVersion && paths.contains(effectId)) {
1124 if (!scripts.contains(effectId)) {
1125 QFile scriptFile(paths.value(effectId).path());
1126 if (!scriptFile.open(QIODevice::ReadOnly)) {
1129 #if QT_VERSION >= 0x040700
1130 QScriptProgram scriptProgram(scriptFile.readAll());
1132 QString scriptProgram = scriptFile.readAll();
1135 scripts.insert(effectId, scriptProgram);
1138 QScriptEngine scriptEngine;
1139 scriptEngine.importExtension("qt.core");
1140 scriptEngine.importExtension("qt.xml");
1141 scriptEngine.evaluate(scripts.value(effectId));
1142 QScriptValue updateRules = scriptEngine.globalObject().property("update");
1143 if (!updateRules.isValid())
1145 if (updateRules.isFunction()) {
1146 QDomDocument scriptDoc;
1147 scriptDoc.appendChild(scriptDoc.importNode(effect, true));
1149 QString effectString = updateRules.call(QScriptValue(), QScriptValueList() << serviceVersion << effectVersion << scriptDoc.toString()).toString();
1151 if (!effectString.isEmpty() && !scriptEngine.hasUncaughtException()) {
1152 scriptDoc.setContent(effectString);
1153 QDomNode updatedEffect = effect.ownerDocument().importNode(scriptDoc.documentElement(), true);
1154 effect.parentNode().replaceChild(updatedEffect, effect);
1158 m_modified = updateEffectParameters(effect.childNodes(), &updateRules, serviceVersion, effectVersion);
1161 // set version number since MLT won't change it (only initially set it)
1162 QDomElement versionElem = effect.firstChildElement("version");
1163 if (EffectsList::property(effect, "version").isNull()) {
1164 versionElem = effect.ownerDocument().createTextNode(QLocale().toString(serviceVersion)).toElement();
1165 versionElem.setTagName("property");
1166 versionElem.setAttribute("name", "version");
1167 effect.appendChild(versionElem);
1169 EffectsList::setProperty(effect, "version", QLocale().toString(serviceVersion));
1172 else safeEffects.append(effectId);
1177 bool DocumentValidator::updateEffectParameters(const QDomNodeList ¶meters, const QScriptValue* updateRules, const double serviceVersion, const double effectVersion)
1179 bool updated = false;
1180 bool isDowngrade = serviceVersion < effectVersion;
1181 for (int i = 0; i < parameters.count(); ++i) {
1182 QDomElement parameter = parameters.at(i).toElement();
1183 QScriptValue rules = updateRules->property(parameter.attribute("name"));
1184 if (rules.isValid() && rules.isArray()) {
1185 int rulesCount = rules.property("length").toInt32();
1187 // start with the highest version and downgrade step by step
1188 for (int j = rulesCount - 1; j >= 0; --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());
1196 for (int j = 0; j < rulesCount; ++j) {
1197 double version = rules.property(j).property(0).toNumber();
1198 if (version > effectVersion && version <= serviceVersion) {
1199 parameter.firstChild().setNodeValue(rules.property(j).property(1).call(QScriptValue(), QScriptValueList() << parameter.text() << isDowngrade).toString());