1 /***************************************************************************
2 titledocument.h - description
5 copyright : (C) 2008 by Marco Gittler
6 email : g.marco@freenet.de
7 ***************************************************************************/
9 /***************************************************************************
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
16 ***************************************************************************/
18 #include "titledocument.h"
21 #include <KTemporaryFile>
22 #include <kio/netaccess.h>
23 #include <KApplication>
24 #include <KStandardDirs>
26 #include <KMessageBox>
28 #include <QGraphicsScene>
29 #include <QDomElement>
30 #include <QGraphicsItem>
31 #include <QGraphicsRectItem>
32 #include <QGraphicsTextItem>
33 #include <QGraphicsSvgItem>
34 #include <QCryptographicHash>
35 #include <QSvgRenderer>
38 #include <QTextCursor>
42 #if QT_VERSION >= 0x040600
43 #include <QGraphicsEffect>
44 #include <QGraphicsBlurEffect>
45 #include <QGraphicsDropShadowEffect>
48 QByteArray fileToByteArray(const QString& filename)
52 if (file.open(QIODevice::ReadOnly)) {
53 while (!file.atEnd()) {
54 ret.append(file.readLine());
60 TitleDocument::TitleDocument()
67 void TitleDocument::setScene(QGraphicsScene* _scene, int width, int height)
74 int TitleDocument::base64ToUrl(QGraphicsItem* item, QDomElement& content, bool embed)
77 if (!item->data(Qt::UserRole + 1).toString().isEmpty()) {
78 content.setAttribute("base64", item->data(Qt::UserRole + 1).toString());
79 } else if (!item->data(Qt::UserRole).toString().isEmpty()) {
80 content.setAttribute("base64", fileToByteArray(item->data(Qt::UserRole).toString()).toBase64().data());
82 content.removeAttribute("url");
84 // save for project files to disk
85 QString base64 = item->data(Qt::UserRole + 1).toString();
86 if (!base64.isEmpty()) {
88 if (!m_projectPath.isEmpty()) {
89 titlePath = m_projectPath;
91 titlePath = "/tmp/titles";
93 QString filename = extractBase64Image(titlePath, base64);
94 if (!filename.isEmpty()) {
95 content.setAttribute("url", filename);
96 content.removeAttribute("base64");
108 const QString TitleDocument::extractBase64Image(const QString &titlePath, const QString &data)
110 QString filename = titlePath + QString(QCryptographicHash::hash(data.toAscii(), QCryptographicHash::Md5).toHex().append(".titlepart"));
111 KStandardDirs::makeDir(titlePath);
113 if (f.open(QIODevice::WriteOnly)) {
114 f.write(QByteArray::fromBase64(data.toAscii())) ;
121 QDomDocument TitleDocument::xml(QGraphicsRectItem* startv, QGraphicsRectItem* endv, bool embed)
125 QDomElement main = doc.createElement("kdenlivetitle");
126 main.setAttribute("width", m_width);
127 main.setAttribute("height", m_height);
129 const char *locale = setlocale(LC_NUMERIC, NULL);
130 main.setAttribute("LC_NUMERIC", locale);
131 doc.appendChild(main);
133 foreach(QGraphicsItem * item, m_scene->items()) {
134 QDomElement e = doc.createElement("item");
135 QDomElement content = doc.createElement("content");
137 QGraphicsTextItem *t;
139 switch (item->type()) {
141 e.setAttribute("type", "QGraphicsPixmapItem");
142 content.setAttribute("url", item->data(Qt::UserRole).toString());
143 base64ToUrl(item, content, embed);
146 e.setAttribute("type", "QGraphicsSvgItem");
147 content.setAttribute("url", item->data(Qt::UserRole).toString());
148 base64ToUrl(item, content, embed);
151 e.setAttribute("type", "QGraphicsRectItem");
152 content.setAttribute("rect", rectFToString(((QGraphicsRectItem*)item)->rect().normalized()));
153 content.setAttribute("pencolor", colorToString(((QGraphicsRectItem*)item)->pen().color()));
154 content.setAttribute("penwidth", ((QGraphicsRectItem*)item)->pen().width());
155 content.setAttribute("brushcolor", colorToString(((QGraphicsRectItem*)item)->brush().color()));
158 e.setAttribute("type", "QGraphicsTextItem");
159 t = static_cast<QGraphicsTextItem *>(item);
160 // Don't save empty text nodes
161 if (t->toPlainText().simplified().isEmpty()) continue;
162 //content.appendChild(doc.createTextNode(((QGraphicsTextItem*)item)->toHtml()));
163 content.appendChild(doc.createTextNode(t->toPlainText()));
165 content.setAttribute("font", font.family());
166 content.setAttribute("font-weight", font.weight());
167 content.setAttribute("font-pixel-size", font.pixelSize());
168 content.setAttribute("font-italic", font.italic());
169 content.setAttribute("font-underline", font.underline());
171 QTextCursor cursor(t->document());
172 cursor.select(QTextCursor::Document);
173 QColor fontcolor = cursor.charFormat().foreground().color();
174 content.setAttribute("font-color", colorToString(fontcolor));
175 if (!t->data(101).isNull()) content.setAttribute("font-outline", t->data(101).toDouble());
176 if (!t->data(102).isNull()) {
177 QVariant variant = t->data(102);
178 QColor outlineColor = variant.value<QColor>();
179 content.setAttribute("font-outline-color", colorToString(outlineColor));
182 if (!t->data(100).isNull()) {
183 QStringList effectParams = t->data(100).toStringList();
184 QString effectName = effectParams.takeFirst();
185 content.setAttribute("textwidth", t->sceneBoundingRect().width());
186 content.setAttribute(effectName, effectParams.join(";"));
189 // Only save when necessary.
190 if (t->data(OriginXLeft).toInt() == AxisInverted) {
191 content.setAttribute("kdenlive-axis-x-inverted", t->data(OriginXLeft).toInt());
193 if (t->data(OriginYTop).toInt() == AxisInverted) {
194 content.setAttribute("kdenlive-axis-y-inverted", t->data(OriginYTop).toInt());
196 if (t->textWidth() != -1) {
197 content.setAttribute("alignment", t->textCursor().blockFormat().alignment());
205 QDomElement pos = doc.createElement("position");
206 pos.setAttribute("x", item->pos().x());
207 pos.setAttribute("y", item->pos().y());
208 QTransform transform = item->transform();
209 QDomElement tr = doc.createElement("transform");
210 if (!item->data(ZOOMFACTOR).isNull()) {
211 tr.setAttribute("zoom", QString::number(item->data(ZOOMFACTOR).toInt()));
213 if (!item->data(ROTATEFACTOR).isNull()) {
214 QList<QVariant> rotlist = item->data(ROTATEFACTOR).toList();
215 tr.setAttribute("rotation", QString("%1,%2,%3").arg(rotlist[0].toDouble()).arg(rotlist[1].toDouble()).arg(rotlist[2].toDouble()));
217 tr.appendChild(doc.createTextNode(
218 QString("%1,%2,%3,%4,%5,%6,%7,%8,%9").arg(
219 transform.m11()).arg(transform.m12()).arg(transform.m13()).arg(transform.m21()).arg(transform.m22()).arg(transform.m23()).arg(transform.m31()).arg(transform.m32()).arg(transform.m33())
222 e.setAttribute("z-index", item->zValue());
225 #if QT_VERSION >= 0x040600
227 QGraphicsEffect *eff = item->graphicsEffect();
229 QGraphicsBlurEffect *blur = static_cast <QGraphicsBlurEffect *>(eff);
230 QDomElement effect = doc.createElement("effect");
232 effect.setAttribute("type", "blur");
233 effect.setAttribute("blurradius", blur->blurRadius());
235 //WARNING:those effects are anyways broken because they use QPixmaps which are not safe for MLT's threaded workflow
236 QGraphicsDropShadowEffect *shadow = static_cast <QGraphicsDropShadowEffect *>(eff);
238 effect.setAttribute("type", "shadow");
239 effect.setAttribute("blurradius", shadow->blurRadius());
240 effect.setAttribute("xoffset", shadow->xOffset());
241 effect.setAttribute("yoffset", shadow->yOffset());
244 e.appendChild(effect);
249 e.appendChild(content);
250 if (item->zValue() > -1000) main.appendChild(e);
252 if (startv && endv) {
253 QDomElement endp = doc.createElement("endviewport");
254 QDomElement startp = doc.createElement("startviewport");
255 QRectF r(endv->pos().x(), endv->pos().y(), endv->rect().width(), endv->rect().height());
256 endp.setAttribute("rect", rectFToString(r));
257 QRectF r2(startv->pos().x(), startv->pos().y(), startv->rect().width(), startv->rect().height());
258 startp.setAttribute("rect", rectFToString(r2));
260 main.appendChild(startp);
261 main.appendChild(endp);
263 QDomElement backgr = doc.createElement("background");
264 QColor color = getBackgroundColor();
265 backgr.setAttribute("color", colorToString(color));
266 main.appendChild(backgr);
271 /** \brief Get the background color (incl. alpha) from the document, if possibly
272 * \returns The background color of the document, inclusive alpha. If none found, returns (0,0,0,0) */
273 QColor TitleDocument::getBackgroundColor() const
275 QColor color(0, 0, 0, 0);
277 QList<QGraphicsItem *> items = m_scene->items();
278 for (int i = 0; i < items.size(); ++i) {
279 if (items.at(i)->zValue() == -1100) {
280 color = ((QGraphicsRectItem *)items.at(i))->brush().color();
289 bool TitleDocument::saveDocument(const KUrl& url, QGraphicsRectItem* startv, QGraphicsRectItem* endv, int duration, bool embed)
294 QDomDocument doc = xml(startv, endv, embed);
295 doc.documentElement().setAttribute("duration", duration);
296 // keep some time for backwards compatibility (opening projects with older versions) - 26/12/12
297 doc.documentElement().setAttribute("out", duration);
298 KTemporaryFile tmpfile;
299 if (!tmpfile.open()) {
300 kWarning() << "///// CANNOT CREATE TMP FILE in: " << tmpfile.fileName();
303 QFile xmlf(tmpfile.fileName());
304 if (!xmlf.open(QIODevice::WriteOnly))
306 xmlf.write(doc.toString().toUtf8());
307 if (xmlf.error() != QFile::NoError) {
312 return KIO::NetAccess::upload(tmpfile.fileName(), url, 0);
315 int TitleDocument::loadFromXml(const QDomDocument& doc, QGraphicsRectItem* startv, QGraphicsRectItem* endv, int *duration, const QString& projectpath)
317 m_projectPath = projectpath;
318 QDomNodeList titles = doc.elementsByTagName("kdenlivetitle");
319 //TODO: Check if the opened title size is equal to project size, otherwise warn user and rescale
320 if (doc.documentElement().hasAttribute("width") && doc.documentElement().hasAttribute("height")) {
321 int doc_width = doc.documentElement().attribute("width").toInt();
322 int doc_height = doc.documentElement().attribute("height").toInt();
323 if (doc_width != m_width || doc_height != m_height) {
324 KMessageBox::information(kapp->activeWindow(), i18n("This title clip was created with a different frame size."), i18n("Title Profile"));
325 //TODO: convert using QTransform
327 m_height = doc_height;
330 // Document has no size info, it is likely an old version title, so ignore viewport data
331 QDomNodeList viewportlist = doc.documentElement().elementsByTagName("startviewport");
332 if (!viewportlist.isEmpty()) {
333 doc.documentElement().removeChild(viewportlist.at(0));
335 viewportlist = doc.documentElement().elementsByTagName("endviewport");
336 if (!viewportlist.isEmpty()) {
337 doc.documentElement().removeChild(viewportlist.at(0));
340 //TODO: get default title duration instead of hardcoded one
341 if (doc.documentElement().hasAttribute("duration"))
342 *duration = doc.documentElement().attribute("duration").toInt();
343 else if (doc.documentElement().hasAttribute("out"))
344 *duration = doc.documentElement().attribute("out").toInt();
351 QDomNodeList items = titles.item(0).childNodes();
352 for (int i = 0; i < items.count(); ++i) {
353 QGraphicsItem *gitem = NULL;
354 kDebug() << items.item(i).attributes().namedItem("type").nodeValue();
355 int zValue = items.item(i).attributes().namedItem("z-index").nodeValue().toInt();
356 if (zValue > -1000) {
357 if (items.item(i).attributes().namedItem("type").nodeValue() == "QGraphicsTextItem") {
358 QDomNamedNodeMap txtProperties = items.item(i).namedItem("content").attributes();
359 QFont font(txtProperties.namedItem("font").nodeValue());
361 QDomNode node = txtProperties.namedItem("font-bold");
362 if (!node.isNull()) {
363 // Old: Bold/Not bold.
364 font.setBold(node.nodeValue().toInt());
366 // New: Font weight (QFont::)
367 font.setWeight(txtProperties.namedItem("font-weight").nodeValue().toInt());
369 //font.setBold(txtProperties.namedItem("font-bold").nodeValue().toInt());
370 font.setItalic(txtProperties.namedItem("font-italic").nodeValue().toInt());
371 font.setUnderline(txtProperties.namedItem("font-underline").nodeValue().toInt());
372 // Older Kdenlive version did not store pixel size but point size
373 if (txtProperties.namedItem("font-pixel-size").isNull()) {
374 KMessageBox::information(kapp->activeWindow(), i18n("Some of your text clips were saved with size in points, which means different sizes on different displays. They will be converted to pixel size, making them portable, but you could have to adjust their size."), i18n("Text Clips Updated"));
376 f2.setPointSize(txtProperties.namedItem("font-size").nodeValue().toInt());
377 font.setPixelSize(QFontInfo(f2).pixelSize());
379 font.setPixelSize(txtProperties.namedItem("font-pixel-size").nodeValue().toInt());
380 QColor col(stringToColor(txtProperties.namedItem("font-color").nodeValue()));
381 QGraphicsTextItem *txt = m_scene->addText(items.item(i).namedItem("content").firstChild().nodeValue(), font);
382 QTextCursor cursor(txt->document());
383 cursor.select(QTextCursor::Document);
384 QTextCharFormat format;
385 if (txtProperties.namedItem("font-outline").nodeValue().toDouble() > 0.0) {
386 txt->setData(101, txtProperties.namedItem("font-outline").nodeValue().toDouble());
387 txt->setData(102, stringToColor(txtProperties.namedItem("font-outline-color").nodeValue()));
388 format.setTextOutline(
389 QPen(QColor(stringToColor(txtProperties.namedItem("font-outline-color").nodeValue())),
390 txtProperties.namedItem("font-outline").nodeValue().toDouble(),
391 Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)
395 format.setForeground(QBrush(col));
396 cursor.mergeCharFormat(format);
397 txt->setTextInteractionFlags(Qt::NoTextInteraction);
398 if (txtProperties.namedItem("alignment").isNull() == false) {
399 txt->setTextWidth(txt->boundingRect().width());
400 QTextCursor cur = txt->textCursor();
401 QTextBlockFormat format = cur.blockFormat();
402 format.setAlignment((Qt::Alignment) txtProperties.namedItem("alignment").nodeValue().toInt());
403 cur.select(QTextCursor::Document);
404 cur.setBlockFormat(format);
405 txt->setTextCursor(cur);
406 cur.clearSelection();
407 txt->setTextCursor(cur);
410 if (!txtProperties.namedItem("kdenlive-axis-x-inverted").isNull()) {
411 txt->setData(OriginXLeft, txtProperties.namedItem("kdenlive-axis-x-inverted").nodeValue().toInt());
413 if (!txtProperties.namedItem("kdenlive-axis-y-inverted").isNull()) {
414 txt->setData(OriginYTop, txtProperties.namedItem("kdenlive-axis-y-inverted").nodeValue().toInt());
418 if (!txtProperties.namedItem("typewriter").isNull()) {
419 QStringList effData = QStringList() << "typewriter" << txtProperties.namedItem("typewriter").nodeValue();
420 txt->setData(100, effData);
424 } else if (items.item(i).attributes().namedItem("type").nodeValue() == "QGraphicsRectItem") {
425 QString rect = items.item(i).namedItem("content").attributes().namedItem("rect").nodeValue();
426 QString br_str = items.item(i).namedItem("content").attributes().namedItem("brushcolor").nodeValue();
427 QString pen_str = items.item(i).namedItem("content").attributes().namedItem("pencolor").nodeValue();
428 double penwidth = items.item(i).namedItem("content").attributes().namedItem("penwidth").nodeValue().toDouble();
429 QGraphicsRectItem *rec = m_scene->addRect(stringToRect(rect), QPen(QBrush(stringToColor(pen_str)), penwidth, Qt::SolidLine, Qt::SquareCap, Qt::RoundJoin), QBrush(stringToColor(br_str)));
431 } else if (items.item(i).attributes().namedItem("type").nodeValue() == "QGraphicsPixmapItem") {
432 QString url = items.item(i).namedItem("content").attributes().namedItem("url").nodeValue();
433 QString base64 = items.item(i).namedItem("content").attributes().namedItem("base64").nodeValue();
435 if (base64.isEmpty()) {
438 pix.loadFromData(QByteArray::fromBase64(base64.toAscii()));
440 QGraphicsPixmapItem *rec = m_scene->addPixmap(pix);
441 rec->setShapeMode(QGraphicsPixmapItem::BoundingRectShape);
442 rec->setData(Qt::UserRole, url);
443 if (!base64.isEmpty()) {
444 rec->setData(Qt::UserRole + 1, base64);
447 } else if (items.item(i).attributes().namedItem("type").nodeValue() == "QGraphicsSvgItem") {
448 QString url = items.item(i).namedItem("content").attributes().namedItem("url").nodeValue();
449 QString base64 = items.item(i).namedItem("content").attributes().namedItem("base64").nodeValue();
450 QGraphicsSvgItem *rec = NULL;
451 if (base64.isEmpty()) {
452 rec = new QGraphicsSvgItem(url);
454 rec = new QGraphicsSvgItem();
455 QSvgRenderer *renderer = new QSvgRenderer(QByteArray::fromBase64(base64.toAscii()), rec);
456 rec->setSharedRenderer(renderer);
457 //QString elem=rec->elementId();
458 //QRectF bounds = renderer->boundsOnElement(elem);
461 m_scene->addItem(rec);
462 rec->setData(Qt::UserRole, url);
463 if (!base64.isEmpty()) {
464 rec->setData(Qt::UserRole + 1, base64);
472 QPointF p(items.item(i).namedItem("position").attributes().namedItem("x").nodeValue().toDouble(),
473 items.item(i).namedItem("position").attributes().namedItem("y").nodeValue().toDouble());
475 QDomElement trans = items.item(i).namedItem("position").firstChild().toElement();
476 gitem->setTransform(stringToTransform(trans.firstChild().nodeValue()));
477 QString rotate = trans.attribute("rotation");
478 if (!rotate.isEmpty()) gitem->setData(ROTATEFACTOR, stringToList(rotate));
479 QString zoom = trans.attribute("zoom");
480 if (!zoom.isEmpty()) gitem->setData(ZOOMFACTOR, zoom.toInt());
481 int zValue = items.item(i).attributes().namedItem("z-index").nodeValue().toInt();
482 if (zValue > maxZValue) maxZValue = zValue;
483 gitem->setZValue(zValue);
484 gitem->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable);
486 #if QT_VERSION >= 0x040600
488 QDomNode eff = items.item(i).namedItem("effect");
490 QDomElement e = eff.toElement();
491 if (e.attribute("type") == "blur") {
492 QGraphicsBlurEffect *blur = new QGraphicsBlurEffect();
493 blur->setBlurRadius(e.attribute("blurradius").toInt());
494 gitem->setGraphicsEffect(blur);
495 } else if (e.attribute("type") == "shadow") {
496 QGraphicsDropShadowEffect *shadow = new QGraphicsDropShadowEffect();
497 shadow->setBlurRadius(e.attribute("blurradius").toInt());
498 shadow->setOffset(e.attribute("xoffset").toInt(), e.attribute("yoffset").toInt());
499 gitem->setGraphicsEffect(shadow);
505 if (items.item(i).nodeName() == "background") {
506 kDebug() << items.item(i).attributes().namedItem("color").nodeValue();
507 QColor color = QColor(stringToColor(items.item(i).attributes().namedItem("color").nodeValue()));
508 //color.setAlpha(items.item(i).attributes().namedItem("alpha").nodeValue().toInt());
509 QList<QGraphicsItem *> items = m_scene->items();
510 for (int i = 0; i < items.size(); ++i) {
511 if (items.at(i)->zValue() == -1100) {
512 ((QGraphicsRectItem *)items.at(i))->setBrush(QBrush(color));
516 } else if (items.item(i).nodeName() == "startviewport" && startv) {
517 QString rect = items.item(i).attributes().namedItem("rect").nodeValue();
518 QRectF r = stringToRect(rect);
519 startv->setRect(0, 0, r.width(), r.height());
520 startv->setPos(r.topLeft());
521 } else if (items.item(i).nodeName() == "endviewport" && endv) {
522 QString rect = items.item(i).attributes().namedItem("rect").nodeValue();
523 QRectF r = stringToRect(rect);
524 endv->setRect(0, 0, r.width(), r.height());
525 endv->setPos(r.topLeft());
532 QString TitleDocument::colorToString(const QColor& c)
534 QString ret = "%1,%2,%3,%4";
535 ret = ret.arg(c.red()).arg(c.green()).arg(c.blue()).arg(c.alpha());
539 QString TitleDocument::rectFToString(const QRectF& c)
541 QString ret = "%1,%2,%3,%4";
542 ret = ret.arg(c.left()).arg(c.top()).arg(c.width()).arg(c.height());
546 QRectF TitleDocument::stringToRect(const QString & s)
549 QStringList l = s.split(',');
552 return QRectF(l.at(0).toDouble(), l.at(1).toDouble(), l.at(2).toDouble(), l.at(3).toDouble()).normalized();
555 QColor TitleDocument::stringToColor(const QString & s)
557 QStringList l = s.split(',');
560 return QColor(l.at(0).toInt(), l.at(1).toInt(), l.at(2).toInt(), l.at(3).toInt());;
563 QTransform TitleDocument::stringToTransform(const QString& s)
565 QStringList l = s.split(',');
569 l.at(0).toDouble(), l.at(1).toDouble(), l.at(2).toDouble(),
570 l.at(3).toDouble(), l.at(4).toDouble(), l.at(5).toDouble(),
571 l.at(6).toDouble(), l.at(7).toDouble(), l.at(8).toDouble()
575 QList<QVariant> TitleDocument::stringToList(const QString & s)
577 QStringList l = s.split(',');
579 return QList<QVariant>();
580 return QList<QVariant>() << QVariant(l.at(0).toDouble()) << QVariant(l.at(1).toDouble()) << QVariant(l.at(2).toDouble());
583 int TitleDocument::frameWidth() const
588 int TitleDocument::frameHeight() const