1 /***************************************************************************
2 * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
14 * You should have received a copy of the GNU General Public License *
15 * along with this program; if not, write to the *
16 * Free Software Foundation, Inc., *
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
18 ***************************************************************************/
21 #include "documentvalidator.h"
22 #include "definitions.h"
23 #include "initeffects.h"
24 #include "mainwindow.h"
27 #include <KMessageBox>
28 #include <KApplication>
31 #include <KStandardDirs>
37 #include <QScriptEngine>
39 #include <mlt++/Mlt.h>
44 DocumentValidator::DocumentValidator(QDomDocument doc):
49 bool DocumentValidator::validate(const double currentVersion)
51 QDomElement mlt = m_doc.firstChildElement("mlt");
52 // At least the root element must be there
56 QDomElement kdenliveDoc = mlt.firstChildElement("kdenlivedoc");
57 // Check if we're validating a Kdenlive project
58 if (kdenliveDoc.isNull())
61 // Previous MLT / Kdenlive versions used C locale by default
62 QLocale documentLocale = QLocale::c();
64 if (mlt.hasAttribute("LC_NUMERIC")) {
65 // Set locale for the document
66 // WARNING: what should be done in case the locale does not exist on the system?
67 setlocale(LC_NUMERIC, mlt.attribute("LC_NUMERIC").toUtf8().constData());
68 documentLocale = QLocale(mlt.attribute("LC_NUMERIC"));
71 documentLocale.setNumberOptions(QLocale::OmitGroupSeparator);
72 if (documentLocale.decimalPoint() != QLocale().decimalPoint()) {
73 // If loading an older MLT file without LC_NUMERIC, set locale to C which was previously the default
74 if (!mlt.hasAttribute("LC_NUMERIC")) setlocale(LC_NUMERIC, "C");
76 QLocale::setDefault(documentLocale);
77 // locale conversion might need to be redone
78 initEffects::parseEffectFiles();
81 // TODO: remove after string freeze
83 KMessageBox::sorry(kapp->activeWindow(), i18n("The document you are opening uses a different locale (%1) than your system. You can only open and render it, no editing is supported unless you change your system's locale.", mlt.attribute("LC_NUMERIC")), i18n("Read only project"));
85 // Upgrade the document to the latest version
86 if (!upgrade(documentLocale.toDouble(kdenliveDoc.attribute("version")), currentVersion))
90 * Check the syntax (this will be replaced by XSD validation with Qt 4.6)
91 * and correct some errors
94 // Return (or create) the tractor
95 QDomElement tractor = mlt.firstChildElement("tractor");
96 if (tractor.isNull()) {
98 tractor = m_doc.createElement("tractor");
99 tractor.setAttribute("global_feed", "1");
100 tractor.setAttribute("in", "0");
101 tractor.setAttribute("out", "-1");
102 tractor.setAttribute("id", "maintractor");
103 mlt.appendChild(tractor);
107 * Make sure at least one track exists, and they're equal in number to
108 * to the maximum between MLT and Kdenlive playlists and tracks
110 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
111 int tracksMax = playlists.count() - 1; // Remove the black track
112 QDomNodeList tracks = tractor.elementsByTagName("track");
113 tracksMax = qMax(tracks.count() - 1, tracksMax);
114 QDomNodeList tracksinfo = kdenliveDoc.elementsByTagName("trackinfo");
115 tracksMax = qMax(tracksinfo.count(), tracksMax);
116 tracksMax = qMax(1, tracksMax); // Force existance of one track
117 if (playlists.count() - 1 < tracksMax ||
118 tracks.count() - 1 < tracksMax ||
119 tracksinfo.count() < tracksMax) {
120 kDebug() << "//// WARNING, PROJECT IS CORRUPTED, MISSING TRACK";
123 // use the MLT tracks as reference
124 if (tracks.count() - 1 < tracksMax) {
125 // Looks like one MLT track is missing, remove the extra Kdenlive track if there is one.
126 if (tracksinfo.count() != tracks.count() - 1) {
127 // The Kdenlive tracks are not ok, clear and rebuild them
128 QDomNode tinfo = kdenliveDoc.firstChildElement("tracksinfo");
129 QDomNode tnode = tinfo.firstChild();
130 while (!tnode.isNull()) {
131 tinfo.removeChild(tnode);
132 tnode = tinfo.firstChild();
135 for (int i = 1; i < tracks.count(); i++) {
136 QString hide = tracks.at(i).toElement().attribute("hide");
137 QDomElement newTrack = m_doc.createElement("trackinfo");
138 if (hide == "video") {
140 newTrack.setAttribute("type", "audio");
141 newTrack.setAttribute("blind", 1);
142 newTrack.setAttribute("mute", 0);
143 newTrack.setAttribute("lock", 0);
145 newTrack.setAttribute("blind", 0);
146 newTrack.setAttribute("mute", 0);
147 newTrack.setAttribute("lock", 0);
149 tinfo.appendChild(newTrack);
154 if (playlists.count() - 1 < tracksMax) {
155 difference = tracksMax - (playlists.count() - 1);
156 for (int i = 0; i < difference; ++i) {
157 QDomElement playlist = m_doc.createElement("playlist");
158 mlt.appendChild(playlist);
161 if (tracks.count() - 1 < tracksMax) {
162 difference = tracksMax - (tracks.count() - 1);
163 for (int i = 0; i < difference; ++i) {
164 QDomElement track = m_doc.createElement("track");
165 tractor.appendChild(track);
168 if (tracksinfo.count() < tracksMax) {
169 QDomElement tracksinfoElm = kdenliveDoc.firstChildElement("tracksinfo");
170 if (tracksinfoElm.isNull()) {
171 tracksinfoElm = m_doc.createElement("tracksinfo");
172 kdenliveDoc.appendChild(tracksinfoElm);
174 difference = tracksMax - tracksinfo.count();
175 for (int i = 0; i < difference; ++i) {
176 QDomElement trackinfo = m_doc.createElement("trackinfo");
177 trackinfo.setAttribute("mute", "0");
178 trackinfo.setAttribute("locked", "0");
179 tracksinfoElm.appendChild(trackinfo);
184 // TODO: check the tracks references
185 // TODO: check internal mix transitions
194 bool DocumentValidator::upgrade(double version, const double currentVersion)
196 kDebug() << "Opening a document with version " << version;
198 // No conversion needed
199 if (version == currentVersion) {
203 // The document is too new
204 if (version > currentVersion) {
205 kDebug() << "Unable to open document with version " << version;
206 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"));
210 // Unsupported document versions
211 if (version == 0.5 || version == 0.7) {
212 kDebug() << "Unable to open document with version " << version;
213 KMessageBox::sorry(kapp->activeWindow(), i18n("This project type is unsupported (version %1) and can't be loaded.", version), i18n("Unable to open project"));
218 QDomNode infoXmlNode = m_doc.elementsByTagName("kdenlivedoc").at(0);
219 QDomElement infoXml = infoXmlNode.toElement();
220 infoXml.setAttribute("upgraded", "1");
222 if (version <= 0.6) {
223 QDomElement infoXml_old = infoXmlNode.cloneNode(true).toElement(); // Needed for folders
224 QDomNode westley = m_doc.elementsByTagName("westley").at(1);
225 QDomNode tractor = m_doc.elementsByTagName("tractor").at(0);
226 QDomNode multitrack = m_doc.elementsByTagName("multitrack").at(0);
227 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
229 QDomNode props = m_doc.elementsByTagName("properties").at(0).toElement();
230 QString profile = props.toElement().attribute("videoprofile");
231 int startPos = props.toElement().attribute("timeline_position").toInt();
232 if (profile == "dv_wide")
233 profile = "dv_pal_wide";
235 // move playlists outside of tractor and add the tracks instead
236 int max = playlists.count();
237 if (westley.isNull()) {
238 westley = m_doc.createElement("westley");
239 m_doc.documentElement().appendChild(westley);
241 if (tractor.isNull()) {
242 kDebug() << "// NO MLT PLAYLIST, building empty one";
243 QDomElement blank_tractor = m_doc.createElement("tractor");
244 westley.appendChild(blank_tractor);
245 QDomElement blank_playlist = m_doc.createElement("playlist");
246 blank_playlist.setAttribute("id", "black_track");
247 westley.insertBefore(blank_playlist, QDomNode());
248 QDomElement blank_track = m_doc.createElement("track");
249 blank_track.setAttribute("producer", "black_track");
250 blank_tractor.appendChild(blank_track);
252 QDomNodeList kdenlivetracks = m_doc.elementsByTagName("kdenlivetrack");
253 for (int i = 0; i < kdenlivetracks.count(); i++) {
254 blank_playlist = m_doc.createElement("playlist");
255 blank_playlist.setAttribute("id", "playlist" + QString::number(i));
256 westley.insertBefore(blank_playlist, QDomNode());
257 blank_track = m_doc.createElement("track");
258 blank_track.setAttribute("producer", "playlist" + QString::number(i));
259 blank_tractor.appendChild(blank_track);
260 if (kdenlivetracks.at(i).toElement().attribute("cliptype") == "Sound") {
261 blank_playlist.setAttribute("hide", "video");
262 blank_track.setAttribute("hide", "video");
265 } else for (int i = 0; i < max; i++) {
266 QDomNode n = playlists.at(i);
267 westley.insertBefore(n, QDomNode());
268 QDomElement pl = n.toElement();
269 QDomElement track = m_doc.createElement("track");
270 QString trackType = pl.attribute("hide");
271 if (!trackType.isEmpty())
272 track.setAttribute("hide", trackType);
273 QString playlist_id = pl.attribute("id");
274 if (playlist_id.isEmpty()) {
275 playlist_id = "black_track";
276 pl.setAttribute("id", playlist_id);
278 track.setAttribute("producer", playlist_id);
279 //tractor.appendChild(track);
280 #define KEEP_TRACK_ORDER 1
281 #ifdef KEEP_TRACK_ORDER
282 tractor.insertAfter(track, QDomNode());
284 // Insert the new track in an order that hopefully matches the 3 video, then 2 audio tracks of Kdenlive 0.7.0
285 // insertion sort - O( tracks*tracks )
286 // Note, this breaks _all_ transitions - but you can move them up and down afterwards.
287 QDomElement tractor_elem = tractor.toElement();
288 if (! tractor_elem.isNull()) {
289 QDomNodeList tracks = tractor_elem.elementsByTagName("track");
290 int size = tracks.size();
292 tractor.insertAfter(track, QDomNode());
294 bool inserted = false;
295 for (int i = 0; i < size; ++i) {
296 QDomElement track_elem = tracks.at(i).toElement();
297 if (track_elem.isNull()) {
298 tractor.insertAfter(track, QDomNode());
302 kDebug() << "playlist_id: " << playlist_id << " producer:" << track_elem.attribute("producer");
303 if (playlist_id < track_elem.attribute("producer")) {
304 tractor.insertBefore(track, track_elem);
310 // Reach here, no insertion, insert last
312 tractor.insertAfter(track, QDomNode());
316 kWarning() << "tractor was not a QDomElement";
317 tractor.insertAfter(track, QDomNode());
321 tractor.removeChild(multitrack);
323 // audio track mixing transitions should not be added to track view, so add required attribute
324 QDomNodeList transitions = m_doc.elementsByTagName("transition");
325 max = transitions.count();
326 for (int i = 0; i < max; i++) {
327 QDomElement tr = transitions.at(i).toElement();
328 if (tr.attribute("combine") == "1" && tr.attribute("mlt_service") == "mix") {
329 QDomElement property = m_doc.createElement("property");
330 property.setAttribute("name", "internal_added");
331 QDomText value = m_doc.createTextNode("237");
332 property.appendChild(value);
333 tr.appendChild(property);
334 property = m_doc.createElement("property");
335 property.setAttribute("name", "mlt_service");
336 value = m_doc.createTextNode("mix");
337 property.appendChild(value);
338 tr.appendChild(property);
340 // convert transition
341 QDomNamedNodeMap attrs = tr.attributes();
342 for (int j = 0; j < attrs.count(); j++) {
343 QString attrName = attrs.item(j).nodeName();
344 if (attrName != "in" && attrName != "out" && attrName != "id") {
345 QDomElement property = m_doc.createElement("property");
346 property.setAttribute("name", attrName);
347 QDomText value = m_doc.createTextNode(attrs.item(j).nodeValue());
348 property.appendChild(value);
349 tr.appendChild(property);
355 // move transitions after tracks
356 for (int i = 0; i < max; i++) {
357 tractor.insertAfter(transitions.at(0), QDomNode());
360 // Fix filters format
361 QDomNodeList entries = m_doc.elementsByTagName("entry");
362 max = entries.count();
363 for (int i = 0; i < max; i++) {
366 QDomNode m = entries.at(i).firstChild();
367 while (!m.isNull()) {
368 if (m.toElement().tagName() == "filter") {
369 QDomElement filt = m.toElement();
370 QDomNamedNodeMap attrs = filt.attributes();
371 QString current_id = filt.attribute("kdenlive_id");
372 if (current_id != last_id) {
374 last_id = current_id;
376 QDomElement e = m_doc.createElement("property");
377 e.setAttribute("name", "kdenlive_ix");
378 QDomText value = m_doc.createTextNode(QString::number(effectix));
379 e.appendChild(value);
381 for (int j = 0; j < attrs.count(); j++) {
382 QDomAttr a = attrs.item(j).toAttr();
384 kDebug() << " FILTER; adding :" << a.name() << ":" << a.value();
385 QDomElement e = m_doc.createElement("property");
386 e.setAttribute("name", a.name());
387 QDomText value = m_doc.createTextNode(a.value());
388 e.appendChild(value);
399 QDomNodeList filters = m_doc.elementsByTagName("filter");
400 max = filters.count();
403 for (int i = 0; i < max; i++) {
404 QDomElement filt = filters.at(i).toElement();
405 QDomNamedNodeMap attrs = filt.attributes();
406 QString current_id = filt.attribute("kdenlive_id");
407 if (current_id != last_id) {
409 last_id = current_id;
411 QDomElement e = m_doc.createElement("property");
412 e.setAttribute("name", "kdenlive_ix");
413 QDomText value = m_doc.createTextNode(QString::number(1));
414 e.appendChild(value);
416 for (int j = 0; j < attrs.count(); j++) {
417 QDomAttr a = attrs.item(j).toAttr();
419 kDebug() << " FILTER; adding :" << a.name() << ":" << a.value();
420 QDomElement e = m_doc.createElement("property");
421 e.setAttribute("name", a.name());
422 QDomText value = m_doc.createTextNode(a.value());
423 e.appendChild(value);
430 QDomNodeList producers = westley.toElement().elementsByTagName("producer");
431 max = producers.count();
432 for (int i = 0; i < max; i++) {
433 QDomElement prod = producers.at(i).toElement();
434 if (prod.attribute("mlt_service") == "framebuffer") {
435 QString slowmotionprod = prod.attribute("resource");
436 slowmotionprod.replace(':', '?');
437 kDebug() << "// FOUND WRONG SLOWMO, new: " << slowmotionprod;
438 prod.setAttribute("resource", slowmotionprod);
441 // move producers to correct place, markers to a global list, fix clip descriptions
442 QDomElement markers = m_doc.createElement("markers");
443 // This will get the xml producers:
444 producers = m_doc.elementsByTagName("producer");
445 max = producers.count();
446 for (int i = 0; i < max; i++) {
447 QDomElement prod = producers.at(0).toElement();
448 // add resource also as a property (to allow path correction in setNewResource())
449 // TODO: will it work with slowmotion? needs testing
450 /*if (!prod.attribute("resource").isEmpty()) {
451 QDomElement prop_resource = m_doc.createElement("property");
452 prop_resource.setAttribute("name", "resource");
453 QDomText resource = m_doc.createTextNode(prod.attribute("resource"));
454 prop_resource.appendChild(resource);
455 prod.appendChild(prop_resource);
457 QDomNode m = prod.firstChild();
459 if (m.toElement().tagName() == "markers") {
460 QDomNodeList prodchilds = m.childNodes();
461 int maxchild = prodchilds.count();
462 for (int k = 0; k < maxchild; k++) {
463 QDomElement mark = prodchilds.at(0).toElement();
464 mark.setAttribute("id", prod.attribute("id"));
465 markers.insertAfter(mark, QDomNode());
468 } else if (prod.attribute("type").toInt() == TEXT) {
469 // convert title clip
470 if (m.toElement().tagName() == "textclip") {
472 QDomElement titleclip = m.toElement();
473 QDomElement title = tdoc.createElement("kdenlivetitle");
474 tdoc.appendChild(title);
475 QDomNodeList objects = titleclip.childNodes();
476 int maxchild = objects.count();
477 for (int k = 0; k < maxchild; k++) {
479 QDomElement ob = objects.at(k).toElement();
480 if (ob.attribute("type") == "3") {
481 // text object - all of this goes into "xmldata"...
482 QDomElement item = tdoc.createElement("item");
483 item.setAttribute("z-index", ob.attribute("z"));
484 item.setAttribute("type", "QGraphicsTextItem");
485 QDomElement position = tdoc.createElement("position");
486 position.setAttribute("x", ob.attribute("x"));
487 position.setAttribute("y", ob.attribute("y"));
488 QDomElement content = tdoc.createElement("content");
489 content.setAttribute("font", ob.attribute("font_family"));
490 content.setAttribute("font-size", ob.attribute("font_size"));
491 content.setAttribute("font-bold", ob.attribute("bold"));
492 content.setAttribute("font-italic", ob.attribute("italic"));
493 content.setAttribute("font-underline", ob.attribute("underline"));
494 QString col = ob.attribute("color");
496 content.setAttribute("font-color", colorToString(c));
497 // todo: These fields are missing from the newly generated xmldata:
498 // transform, startviewport, endviewport, background
500 QDomText conttxt = tdoc.createTextNode(ob.attribute("text"));
501 content.appendChild(conttxt);
502 item.appendChild(position);
503 item.appendChild(content);
504 title.appendChild(item);
505 } else if (ob.attribute("type") == "5") {
507 QDomElement item = tdoc.createElement("item");
508 item.setAttribute("z-index", ob.attribute("z"));
509 item.setAttribute("type", "QGraphicsRectItem");
510 QDomElement position = tdoc.createElement("position");
511 position.setAttribute("x", ob.attribute("x"));
512 position.setAttribute("y", ob.attribute("y"));
513 QDomElement content = tdoc.createElement("content");
514 QString col = ob.attribute("color");
516 content.setAttribute("brushcolor", colorToString(c));
517 QString rect = "0,0,";
518 rect.append(ob.attribute("width"));
520 rect.append(ob.attribute("height"));
521 content.setAttribute("rect", rect);
522 item.appendChild(position);
523 item.appendChild(content);
524 title.appendChild(item);
527 prod.setAttribute("xmldata", tdoc.toString());
528 // mbd todo: This clearly does not work, as every title gets the same name - trying to leave it empty
529 // QStringList titleInfo = TitleWidget::getFreeTitleInfo(projectFolder());
530 // prod.setAttribute("titlename", titleInfo.at(0));
531 // prod.setAttribute("resource", titleInfo.at(1));
532 //kDebug()<<"TITLE DATA:\n"<<tdoc.toString();
534 } // End conversion of title clips.
536 } else if (m.isText()) {
537 QString comment = m.nodeValue();
538 if (!comment.isEmpty()) {
539 prod.setAttribute("description", comment);
544 int duration = prod.attribute("duration").toInt();
545 if (duration > 0) prod.setAttribute("out", QString::number(duration));
546 // The clip goes back in, but text clips should not go back in, at least not modified
547 westley.insertBefore(prod, QDomNode());
550 QDomNode westley0 = m_doc.elementsByTagName("westley").at(0);
551 if (!markers.firstChild().isNull()) westley0.appendChild(markers);
554 * Convert as much of the kdenlivedoc as possible. Use the producer in
555 * westley. First, remove the old stuff from westley, and add a new
556 * empty one. Also, track the max id in order to use it for the adding
559 int max_kproducer_id = 0;
560 westley0.removeChild(infoXmlNode);
561 QDomElement infoXml_new = m_doc.createElement("kdenlivedoc");
562 infoXml_new.setAttribute("profile", profile);
563 infoXml.setAttribute("position", startPos);
565 // Add all the producers that has a resource in westley
566 QDomElement westley_element = westley0.toElement();
567 if (westley_element.isNull()) {
568 kWarning() << "westley0 element in document was not a QDomElement - unable to add producers to new kdenlivedoc";
570 QDomNodeList wproducers = westley_element.elementsByTagName("producer");
571 int kmax = wproducers.count();
572 for (int i = 0; i < kmax; i++) {
573 QDomElement wproducer = wproducers.at(i).toElement();
574 if (wproducer.isNull()) {
575 kWarning() << "Found producer in westley0, that was not a QDomElement";
578 if (wproducer.attribute("id") == "black") continue;
579 // We have to do slightly different things, depending on the type
580 kDebug() << "Converting producer element with type" << wproducer.attribute("type");
581 if (wproducer.attribute("type").toInt() == TEXT) {
582 kDebug() << "Found TEXT element in producer" << endl;
583 QDomElement kproducer = wproducer.cloneNode(true).toElement();
584 kproducer.setTagName("kdenlive_producer");
585 infoXml_new.appendChild(kproducer);
587 * TODO: Perhaps needs some more changes here to
588 * "frequency", aspect ratio as a float, frame_size,
589 * channels, and later, resource and title name
592 QDomElement kproducer = m_doc.createElement("kdenlive_producer");
593 kproducer.setAttribute("id", wproducer.attribute("id"));
594 if (!wproducer.attribute("description").isEmpty())
595 kproducer.setAttribute("description", wproducer.attribute("description"));
596 kproducer.setAttribute("resource", wproducer.attribute("resource"));
597 kproducer.setAttribute("type", wproducer.attribute("type"));
598 // Testing fix for 358
599 if (!wproducer.attribute("aspect_ratio").isEmpty()) {
600 kproducer.setAttribute("aspect_ratio", wproducer.attribute("aspect_ratio"));
602 if (!wproducer.attribute("source_fps").isEmpty()) {
603 kproducer.setAttribute("fps", wproducer.attribute("source_fps"));
605 if (!wproducer.attribute("length").isEmpty()) {
606 kproducer.setAttribute("duration", wproducer.attribute("length"));
608 infoXml_new.appendChild(kproducer);
610 if (wproducer.attribute("id").toInt() > max_kproducer_id) {
611 max_kproducer_id = wproducer.attribute("id").toInt();
615 #define LOOKUP_FOLDER 1
618 * Look through all the folder elements of the old doc, for each folder,
619 * for each producer, get the id, look it up in the new doc, set the
620 * groupname and groupid. Note, this does not work at the moment - at
621 * least one folder shows up missing, and clips with no folder does not
624 //QDomElement infoXml_old = infoXmlNode.toElement();
625 if (!infoXml_old.isNull()) {
626 QDomNodeList folders = infoXml_old.elementsByTagName("folder");
627 int fsize = folders.size();
628 int groupId = max_kproducer_id + 1; // Start at +1 of max id of the kdenlive_producers
629 for (int i = 0; i < fsize; ++i) {
630 QDomElement folder = folders.at(i).toElement();
631 if (!folder.isNull()) {
632 QString groupName = folder.attribute("name");
633 kDebug() << "groupName: " << groupName << " with groupId: " << groupId;
634 QDomNodeList fproducers = folder.elementsByTagName("producer");
635 int psize = fproducers.size();
636 for (int j = 0; j < psize; ++j) {
637 QDomElement fproducer = fproducers.at(j).toElement();
638 if (!fproducer.isNull()) {
639 QString id = fproducer.attribute("id");
640 // This is not very effective, but compared to loading the clips, its a breeze
641 QDomNodeList kdenlive_producers = infoXml_new.elementsByTagName("kdenlive_producer");
642 int kpsize = kdenlive_producers.size();
643 for (int k = 0; k < kpsize; ++k) {
644 QDomElement kproducer = kdenlive_producers.at(k).toElement(); // Its an element for sure
645 if (id == kproducer.attribute("id")) {
646 // We do not check that it already is part of a folder
647 kproducer.setAttribute("groupid", groupId);
648 kproducer.setAttribute("groupname", groupName);
659 QDomNodeList elements = westley.childNodes();
660 max = elements.count();
661 for (int i = 0; i < max; i++) {
662 QDomElement prod = elements.at(0).toElement();
663 westley0.insertAfter(prod, QDomNode());
666 westley0.appendChild(infoXml_new);
668 westley0.removeChild(westley);
670 // adds <avfile /> information to <kdenlive_producer />
671 QDomNodeList kproducers = m_doc.elementsByTagName("kdenlive_producer");
672 QDomNodeList avfiles = infoXml_old.elementsByTagName("avfile");
673 kDebug() << "found" << avfiles.count() << "<avfile />s and" << kproducers.count() << "<kdenlive_producer />s";
674 for (int i = 0; i < avfiles.count(); ++i) {
675 QDomElement avfile = avfiles.at(i).toElement();
676 QDomElement kproducer;
678 kWarning() << "found an <avfile /> that is not a QDomElement";
680 QString id = avfile.attribute("id");
681 // this is horrible, must be rewritten, it's just for test
682 for (int j = 0; j < kproducers.count(); ++j) {
683 //kDebug() << "checking <kdenlive_producer /> with id" << kproducers.at(j).toElement().attribute("id");
684 if (kproducers.at(j).toElement().attribute("id") == id) {
685 kproducer = kproducers.at(j).toElement();
689 if (kproducer == QDomElement())
690 kWarning() << "no match for <avfile /> with id =" << id;
692 //kDebug() << "ready to set additional <avfile />'s attributes (id =" << id << ")";
693 kproducer.setAttribute("channels", avfile.attribute("channels"));
694 kproducer.setAttribute("duration", avfile.attribute("duration"));
695 kproducer.setAttribute("frame_size", avfile.attribute("width") + 'x' + avfile.attribute("height"));
696 kproducer.setAttribute("frequency", avfile.attribute("frequency"));
697 if (kproducer.attribute("description").isEmpty() && !avfile.attribute("description").isEmpty())
698 kproducer.setAttribute("description", avfile.attribute("description"));
702 infoXml = infoXml_new;
705 if (version <= 0.81) {
706 // Add the tracks information
707 QString tracksOrder = infoXml.attribute("tracks");
708 if (tracksOrder.isEmpty()) {
709 QDomNodeList tracks = m_doc.elementsByTagName("track");
710 for (int i = 0; i < tracks.count(); i++) {
711 QDomElement track = tracks.at(i).toElement();
712 if (track.attribute("producer") != "black_track") {
713 if (track.attribute("hide") == "video")
714 tracksOrder.append('a');
716 tracksOrder.append('v');
720 QDomElement tracksinfo = m_doc.createElement("tracksinfo");
721 for (int i = 0; i < tracksOrder.size(); i++) {
722 QDomElement trackinfo = m_doc.createElement("trackinfo");
723 if (tracksOrder.data()[i] == 'a') {
724 trackinfo.setAttribute("type", "audio");
725 trackinfo.setAttribute("blind", true);
727 trackinfo.setAttribute("blind", false);
728 trackinfo.setAttribute("mute", false);
729 trackinfo.setAttribute("locked", false);
730 tracksinfo.appendChild(trackinfo);
732 infoXml.appendChild(tracksinfo);
735 if (version <= 0.82) {
736 // Convert <westley />s in <mlt />s (MLT extreme makeover)
737 QDomNodeList westleyNodes = m_doc.elementsByTagName("westley");
738 for (int i = 0; i < westleyNodes.count(); i++) {
739 QDomElement westley = westleyNodes.at(i).toElement();
740 westley.setTagName("mlt");
744 if (version <= 0.83) {
745 // Replace point size with pixel size in text titles
746 if (m_doc.toString().contains("font-size")) {
747 KMessageBox::ButtonCode convert = KMessageBox::Continue;
748 QDomNodeList kproducerNodes = m_doc.elementsByTagName("kdenlive_producer");
749 for (int i = 0; i < kproducerNodes.count() && convert != KMessageBox::No; ++i) {
750 QDomElement kproducer = kproducerNodes.at(i).toElement();
751 if (kproducer.attribute("type").toInt() == TEXT) {
753 data.setContent(kproducer.attribute("xmldata"));
754 QDomNodeList items = data.firstChild().childNodes();
755 for (int j = 0; j < items.count() && convert != KMessageBox::No; ++j) {
756 if (items.at(j).attributes().namedItem("type").nodeValue() == "QGraphicsTextItem") {
757 QDomNamedNodeMap textProperties = items.at(j).namedItem("content").attributes();
758 if (textProperties.namedItem("font-pixel-size").isNull() && !textProperties.namedItem("font-size").isNull()) {
759 // Ask the user if he wants to convert
760 if (convert != KMessageBox::Yes && convert != KMessageBox::No)
761 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"));
762 if (convert == KMessageBox::Yes) {
764 font.setPointSize(textProperties.namedItem("font-size").nodeValue().toInt());
765 QDomElement content = items.at(j).namedItem("content").toElement();
766 content.setAttribute("font-pixel-size", QFontInfo(font).pixelSize());
767 content.removeAttribute("font-size");
768 kproducer.setAttribute("xmldata", data.toString());
770 * You may be tempted to delete the preview file
771 * to force its recreation: bad idea (see
772 * http://www.kdenlive.org/mantis/view.php?id=749)
782 // Fill the <documentproperties /> element
783 QDomElement docProperties = infoXml.firstChildElement("documentproperties");
784 if (docProperties.isNull()) {
785 docProperties = m_doc.createElement("documentproperties");
786 docProperties.setAttribute("zonein", infoXml.attribute("zonein"));
787 docProperties.setAttribute("zoneout", infoXml.attribute("zoneout"));
788 docProperties.setAttribute("zoom", infoXml.attribute("zoom"));
789 docProperties.setAttribute("position", infoXml.attribute("position"));
790 infoXml.appendChild(docProperties);
794 if (version <= 0.84) {
795 // update the title clips to use the new MLT kdenlivetitle producer
796 QDomNodeList kproducerNodes = m_doc.elementsByTagName("kdenlive_producer");
797 for (int i = 0; i < kproducerNodes.count(); ++i) {
798 QDomElement kproducer = kproducerNodes.at(i).toElement();
799 if (kproducer.attribute("type").toInt() == TEXT) {
800 QString data = kproducer.attribute("xmldata");
801 QString datafile = kproducer.attribute("resource");
802 if (!datafile.endsWith(".kdenlivetitle")) {
803 datafile = QString();
804 kproducer.setAttribute("resource", QString());
806 QString id = kproducer.attribute("id");
807 QDomNodeList mltproducers = m_doc.elementsByTagName("producer");
808 bool foundData = false;
809 bool foundResource = false;
810 bool foundService = false;
811 for (int j = 0; j < mltproducers.count(); j++) {
812 QDomElement wproducer = mltproducers.at(j).toElement();
813 if (wproducer.attribute("id") == id) {
814 QDomNodeList props = wproducer.childNodes();
815 for (int k = 0; k < props.count(); k++) {
816 if (props.at(k).toElement().attribute("name") == "xmldata") {
817 props.at(k).firstChild().setNodeValue(data);
819 } else if (props.at(k).toElement().attribute("name") == "mlt_service") {
820 props.at(k).firstChild().setNodeValue("kdenlivetitle");
822 } else if (props.at(k).toElement().attribute("name") == "resource") {
823 props.at(k).firstChild().setNodeValue(datafile);
824 foundResource = true;
828 QDomElement e = m_doc.createElement("property");
829 e.setAttribute("name", "xmldata");
830 QDomText value = m_doc.createTextNode(data);
831 e.appendChild(value);
832 wproducer.appendChild(e);
835 QDomElement e = m_doc.createElement("property");
836 e.setAttribute("name", "mlt_service");
837 QDomText value = m_doc.createTextNode("kdenlivetitle");
838 e.appendChild(value);
839 wproducer.appendChild(e);
841 if (!foundResource) {
842 QDomElement e = m_doc.createElement("property");
843 e.setAttribute("name", "resource");
844 QDomText value = m_doc.createTextNode(datafile);
845 e.appendChild(value);
846 wproducer.appendChild(e);
854 if (version <= 0.85) {
855 // update the LADSPA effects to use the new ladspa.id format instead of external xml file
856 QDomNodeList effectNodes = m_doc.elementsByTagName("filter");
857 for (int i = 0; i < effectNodes.count(); ++i) {
858 QDomElement effect = effectNodes.at(i).toElement();
859 if (EffectsList::property(effect, "mlt_service") == "ladspa") {
860 // Needs to be converted
861 QStringList info = getInfoFromEffectName(EffectsList::property(effect, "kdenlive_id"));
862 if (info.isEmpty()) continue;
863 // info contains the correct ladspa.id from kdenlive effect name, and a list of parameter's old and new names
864 EffectsList::setProperty(effect, "kdenlive_id", info.at(0));
865 EffectsList::setProperty(effect, "tag", info.at(0));
866 EffectsList::setProperty(effect, "mlt_service", info.at(0));
867 EffectsList::removeProperty(effect, "src");
868 for (int j = 1; j < info.size(); j++) {
869 QString value = EffectsList::property(effect, info.at(j).section('=', 0, 0));
870 if (!value.isEmpty()) {
871 // update parameter name
872 EffectsList::renameProperty(effect, info.at(j).section('=', 0, 0), info.at(j).section('=', 1, 1));
879 if (version <= 0.86) {
880 // Make sure we don't have avformat-novalidate producers, since it caused crashes
881 QDomNodeList producers = m_doc.elementsByTagName("producer");
882 int max = producers.count();
883 for (int i = 0; i < max; i++) {
884 QDomElement prod = producers.at(i).toElement();
885 if (EffectsList::property(prod, "mlt_service") == "avformat-novalidate")
886 EffectsList::setProperty(prod, "mlt_service", "avformat");
889 // 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
891 // Get profile info (width / height)
894 QDomElement profile = m_doc.firstChildElement("profile");
895 if (profile.isNull()) profile = infoXml.firstChildElement("profileinfo");
896 if (profile.isNull()) {
897 // could not find profile info, set PAL
902 profileWidth = profile.attribute("width").toInt();
903 profileHeight = profile.attribute("height").toInt();
905 QDomNodeList transitions = m_doc.elementsByTagName("transition");
906 max = transitions.count();
908 for (int i = 0; i < max; i++) {
909 QDomElement trans = transitions.at(i).toElement();
910 out = trans.attribute("out").toInt() - trans.attribute("in").toInt();
911 QString geom = EffectsList::property(trans, "geometry");
912 Mlt::Geometry *g = new Mlt::Geometry(geom.toUtf8().data(), out, profileWidth, profileHeight);
913 Mlt::GeometryItem item;
914 if (g->next_key(&item, out) == 0) {
915 // We have a keyframe just after last frame, try to move it to last frame
916 if (item.frame() == out + 1) {
920 EffectsList::setProperty(trans, "geometry", g->serialise());
927 if (version <= 0.87) {
928 if (!m_doc.firstChildElement("mlt").hasAttribute("LC_NUMERIC")) {
929 m_doc.firstChildElement("mlt").setAttribute("LC_NUMERIC", "C");
933 // The document has been converted: mark it as modified
934 infoXml.setAttribute("version", currentVersion);
939 QStringList DocumentValidator::getInfoFromEffectName(const QString oldName)
942 // Returns a list to convert old Kdenlive ladspa effects
943 if (oldName == "pitch_shift") {
944 info << "ladspa.1433";
947 else if (oldName == "vinyl") {
948 info << "ladspa.1905";
955 else if (oldName == "room_reverb") {
956 info << "ladspa.1216";
961 else if (oldName == "reverb") {
962 info << "ladspa.1423";
966 else if (oldName == "rate_scale") {
967 info << "ladspa.1417";
970 else if (oldName == "pitch_scale") {
971 info << "ladspa.1193";
974 else if (oldName == "phaser") {
975 info << "ladspa.1217";
978 info << "feedback=2";
981 else if (oldName == "limiter") {
982 info << "ladspa.1913";
987 else if (oldName == "equalizer_15") {
988 info << "ladspa.1197";
1005 else if (oldName == "equalizer") {
1006 info << "ladspa.1901";
1008 info << "midgain=1";
1011 else if (oldName == "declipper") {
1012 info << "ladspa.1195";
1017 QString DocumentValidator::colorToString(const QColor& c)
1019 QString ret = "%1,%2,%3,%4";
1020 ret = ret.arg(c.red()).arg(c.green()).arg(c.blue()).arg(c.alpha());
1024 bool DocumentValidator::isProject() const
1026 QDomNode infoXmlNode = m_doc.elementsByTagName("kdenlivedoc").at(0);
1027 return !infoXmlNode.isNull();
1030 bool DocumentValidator::isModified() const
1035 void DocumentValidator::updateEffects()
1037 // WARNING: order by findDirs will determine which js file to use (in case multiple scripts for the same filter exist)
1038 QMap <QString, KUrl> paths;
1039 #if QT_VERSION >= 0x040700
1040 QMap <QString, QScriptProgram> scripts;
1042 QMap <QString, QString> scripts;
1044 QStringList directories = KGlobal::dirs()->findDirs("appdata", "effects/update");
1045 foreach (const QString &directoryName, directories) {
1046 QDir directory(directoryName);
1047 QStringList fileList = directory.entryList(QStringList() << "*.js", QDir::Files);
1048 foreach (const QString &fileName, fileList) {
1049 QString identifier = fileName;
1050 // remove extension (".js")
1052 paths.insert(identifier, KUrl(directoryName + fileName));
1056 QDomNodeList effects = m_doc.elementsByTagName("filter");
1057 int max = effects.count();
1058 QStringList safeEffects;
1059 for(int i = 0; i < max; ++i) {
1060 QDomElement effect = effects.at(i).toElement();
1061 QString effectId = EffectsList::property(effect, "kdenlive_id");
1062 if (safeEffects.contains(effectId)) {
1063 // Do not check the same effect twice if it is at the correct version
1064 // (assume we don't have different versions of the same effect in a project file)
1067 QString effectTag = EffectsList::property(effect, "tag");
1068 QString effectVersionStr = EffectsList::property(effect, "version");
1069 double effectVersion = effectVersionStr.isNull() ? -1 : effectVersionStr.toDouble();
1071 QDomElement effectDescr = MainWindow::customEffects.getEffectByTag(QString(), effectId);
1072 if (effectDescr.isNull()) {
1073 effectDescr = MainWindow::videoEffects.getEffectByTag(effectTag, effectId);
1075 if (effectDescr.isNull()) {
1076 effectDescr = MainWindow::audioEffects.getEffectByTag(effectTag, effectId);
1078 if (!effectDescr.isNull()) {
1079 double serviceVersion = -1;
1080 QDomElement serviceVersionElem = effectDescr.firstChildElement("version");
1081 if (!serviceVersionElem.isNull()) {
1082 serviceVersion = serviceVersionElem.text().toDouble();
1084 if (serviceVersion != effectVersion && paths.contains(effectId)) {
1085 if (!scripts.contains(effectId)) {
1086 QFile scriptFile(paths.value(effectId).path());
1087 if (!scriptFile.open(QIODevice::ReadOnly)) {
1090 #if QT_VERSION >= 0x040700
1091 QScriptProgram scriptProgram(scriptFile.readAll());
1093 QString scriptProgram = scriptFile.readAll();
1096 scripts.insert(effectId, scriptProgram);
1099 QScriptEngine scriptEngine;
1100 scriptEngine.importExtension("qt.core");
1101 scriptEngine.importExtension("qt.xml");
1102 scriptEngine.evaluate(scripts.value(effectId));
1103 QScriptValue updateRules = scriptEngine.globalObject().property("update");
1104 if (!updateRules.isValid())
1106 if (updateRules.isFunction()) {
1107 QDomDocument scriptDoc;
1108 scriptDoc.appendChild(scriptDoc.importNode(effect, true));
1110 QString effectString = updateRules.call(QScriptValue(), QScriptValueList() << serviceVersion << effectVersion << scriptDoc.toString()).toString();
1112 if (!effectString.isEmpty() && !scriptEngine.hasUncaughtException()) {
1113 scriptDoc.setContent(effectString);
1114 QDomNode updatedEffect = effect.ownerDocument().importNode(scriptDoc.documentElement(), true);
1115 effect.parentNode().replaceChild(updatedEffect, effect);
1119 m_modified = updateEffectParameters(effect.childNodes(), &updateRules, serviceVersion, effectVersion);
1122 // set version number since MLT won't change it (only initially set it)
1123 QDomElement versionElem = effect.firstChildElement("version");
1124 if (EffectsList::property(effect, "version").isNull()) {
1125 versionElem = effect.ownerDocument().createTextNode(QLocale().toString(serviceVersion)).toElement();
1126 versionElem.setTagName("property");
1127 versionElem.setAttribute("name", "version");
1128 effect.appendChild(versionElem);
1130 EffectsList::setProperty(effect, "version", QLocale().toString(serviceVersion));
1133 else safeEffects.append(effectId);
1138 bool DocumentValidator::updateEffectParameters(QDomNodeList parameters, const QScriptValue* updateRules, const double serviceVersion, const double effectVersion)
1140 bool updated = false;
1141 bool isDowngrade = serviceVersion < effectVersion;
1142 for (int i = 0; i < parameters.count(); ++i) {
1143 QDomElement parameter = parameters.at(i).toElement();
1144 QScriptValue rules = updateRules->property(parameter.attribute("name"));
1145 if (rules.isValid() && rules.isArray()) {
1146 int rulesCount = rules.property("length").toInt32();
1148 // start with the highest version and downgrade step by step
1149 for (int j = rulesCount - 1; j >= 0; --j) {
1150 double version = rules.property(j).property(0).toNumber();
1151 if (version <= effectVersion && version > serviceVersion) {
1152 parameter.firstChild().setNodeValue(rules.property(j).property(1).call(QScriptValue(), QScriptValueList() << parameter.text() << isDowngrade).toString());
1157 for (int j = 0; j < rulesCount; ++j) {
1158 double version = rules.property(j).property(0).toNumber();
1159 if (version > effectVersion && version <= serviceVersion) {
1160 parameter.firstChild().setNodeValue(rules.property(j).property(1).call(QScriptValue(), QScriptValueList() << parameter.text() << isDowngrade).toString());