Commit 15a5d687 authored by Dmitry Kazakov's avatar Dmitry Kazakov
Browse files

Implemented saving the animation into mkv, ogv, gif and mp4

No configuration GUI yet. It just saves with the default options :)

Ref T116
parent c9e711c1
/*
* Copyright (c) 2016 Dmitry Kazakov <dimula73@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef __KIS_IMAGE_LOCK_HIJACKER_H
#define __KIS_IMAGE_LOCK_HIJACKER_H
#include <QApplication>
#include <QThread>
#include "kis_image.h"
#include "kis_debug.h"
/**
* This class removes all the locks from the image on construction and
* resets them back on destruction. Never use this class unless you
* really know what you are doing!
*
* Never!
*/
class KisImageLockHijacker
{
public:
KisImageLockHijacker(KisImageSP image)
: m_image(image),
m_count(0)
{
QThread *currentThread = QThread::currentThread();
KIS_ASSERT_RECOVER_RETURN(image->thread() == currentThread);
KIS_ASSERT_RECOVER_RETURN(qApp->thread() == currentThread);
while(image->locked()) {
m_count++;
m_image->unlock();
}
}
~KisImageLockHijacker() {
while (m_count--) {
m_image->barrierLock();
}
}
private:
KisImageSP m_image;
int m_count;
};
#endif /* __KIS_IMAGE_LOCK_HIJACKER_H */
......@@ -19,7 +19,7 @@
#include "kis_animation_exporter.h"
#include <QDesktopServices>
#include <QWaitCondition>
#include <QProgressDialog>
#include <KisMimeDatabase.h>
#include <QEventLoop>
......@@ -29,15 +29,20 @@
#include "KisImportExportManager.h"
#include "kis_image_animation_interface.h"
#include "KisPart.h"
#include "KisMainWindow.h"
#include "kis_paint_layer.h"
#include "kis_group_layer.h"
#include "kis_time_range.h"
#include "kis_painter.h"
#include "kis_image_lock_hijacker.h"
struct KisAnimationExporterUI::Private
{
QWidget *parentWidget;
KisAnimationExporter *exporter;
KisAnimationExportSaver *exporter;
Private(QWidget *parent)
: parentWidget(parent),
......@@ -72,7 +77,7 @@ KisImportExportFilter::ConversionStatus KisAnimationExporterUI::exportSequence(K
int firstFrame = fullClipRange.start();
int lastFrame = fullClipRange.end();
m_d->exporter = new KisAnimationExporter(document, filename, firstFrame, lastFrame);
m_d->exporter = new KisAnimationExportSaver(document, filename, firstFrame, lastFrame);
return m_d->exporter->exportAnimation();
}
......@@ -82,115 +87,102 @@ struct KisAnimationExporter::Private
KisDocument *document;
KisImageWSP image;
QString filenamePrefix;
QString filenameSuffix;
int firstFrame;
int currentFrame;
int lastFrame;
int currentFrame;
QScopedPointer<KisDocument> tmpDoc;
KisImageWSP tmpImage;
KisPaintDeviceSP tmpDevice;
bool exporting;
bool batchMode;
bool isCancelled;
KisImportExportFilter::ConversionStatus status;
QMutex mutex;
QWaitCondition exportFinished;
SaveFrameCallback saveFrameCallback;
KisPaintDeviceSP tmpDevice;
Private(KisDocument *document, int fromTime, int toTime)
: document(document),
image(document->image()),
firstFrame(fromTime),
lastFrame(toTime),
tmpDoc(KisPart::instance()->createDocument()),
exporting(false),
batchMode(false)
currentFrame(-1),
batchMode(document->fileBatchMode()),
isCancelled(false),
status(KisImportExportFilter::OK),
tmpDevice(new KisPaintDevice(image->colorSpace()))
{
tmpDoc->setAutoSave(0);
tmpImage = new KisImage(tmpDoc->createUndoStore(),
image->bounds().width(),
image->bounds().height(),
image->colorSpace(),
QString());
tmpImage->setResolution(image->xRes(), image->yRes());
tmpDoc->setCurrentImage(tmpImage);
KisPaintLayer* paintLayer = new KisPaintLayer(tmpImage, "paint device", 255);
tmpImage->addNode(paintLayer, tmpImage->rootLayer(), KisLayerSP(0));
tmpDevice = paintLayer->paintDevice();
}
};
KisAnimationExporter::KisAnimationExporter(KisDocument *document, const QString &baseFilename, int fromTime, int toTime)
KisAnimationExporter::KisAnimationExporter(KisDocument *document, int fromTime, int toTime)
: m_d(new Private(document, fromTime, toTime))
{
int baseLength = baseFilename.lastIndexOf(".");
if (baseLength > -1) {
m_d->filenamePrefix = baseFilename.left(baseLength);
m_d->filenameSuffix = baseFilename.right(baseFilename.length() - baseLength);
} else {
m_d->filenamePrefix = baseFilename;
}
m_d->batchMode = document->fileBatchMode();
connect(m_d->image->animationInterface(), SIGNAL(sigFrameReady(int)),
this, SLOT(frameReadyToCopy(int)), Qt::DirectConnection);
QString mimefilter = KisMimeDatabase::mimeTypeForFile(baseFilename);
m_d->tmpDoc->setOutputMimeType(mimefilter.toLatin1());
m_d->tmpDoc->setFileBatchMode(true);
connect(this, SIGNAL(sigFrameReadyToSave()), this, SLOT(frameReadyToSave()), Qt::QueuedConnection);
connect(this, SIGNAL(sigFrameReadyToSave()),
this, SLOT(frameReadyToSave()), Qt::QueuedConnection);
}
KisAnimationExporter::~KisAnimationExporter()
{
}
void KisAnimationExporter::setSaveFrameCallback(SaveFrameCallback func)
{
m_d->saveFrameCallback = func;
}
KisImportExportFilter::ConversionStatus KisAnimationExporter::exportAnimation()
{
QScopedPointer<QProgressDialog> progress;
if (!m_d->batchMode) {
emit m_d->document->statusBarMessage(i18n("Export frames"));
QString message = i18n("Export frames...");
progress.reset(new QProgressDialog(message, "", 0, 0, KisPart::instance()->currentMainwindow()));
progress->setWindowModality(Qt::ApplicationModal);
progress->setCancelButton(0);
progress->setMinimumDuration(0);
progress->setValue(0);
emit m_d->document->statusBarMessage(message);
emit m_d->document->sigProgress(0);
connect(m_d->document, SIGNAL(sigProgressCanceled()), this, SLOT(cancel()));
}
/**
* HACK ALERT: Here we remove the image lock! We do it in a GUI
* thread under the barrier lock held, so it is
* guaranteed no other stroke will accidentally be
* started by this. And showing an app-modal dialog to
* the user will prevent him from doing anything
* nasty.
*/
KisImageLockHijacker badGuy(m_d->image);
KIS_ASSERT_RECOVER(!m_d->image->locked()) { return KisImportExportFilter::InternalError; }
m_d->status = KisImportExportFilter::OK;
m_d->exporting = true;
m_d->currentFrame = m_d->firstFrame;
connect(m_d->image->animationInterface(), SIGNAL(sigFrameReady(int)), this, SLOT(frameReadyToCopy(int)), Qt::DirectConnection);
m_d->image->animationInterface()->requestFrameRegeneration(m_d->currentFrame, m_d->image->bounds());
QEventLoop loop;
loop.connect(this, SIGNAL(sigFinished()), SLOT(quit()));
loop.exec();
return m_d->status;
}
void KisAnimationExporter::stopExport()
{
if (!m_d->exporting) return;
m_d->exporting = false;
disconnect(m_d->image->animationInterface(), 0, this, 0);
if (!m_d->batchMode) {
disconnect(m_d->document, SIGNAL(sigProgressCanceled()), this, SLOT(cancel()));
emit m_d->document->sigProgress(100);
emit m_d->document->clearStatusBarMessage();
progress.reset();
}
emit sigFinished();
return m_d->status;
}
void KisAnimationExporter::cancel()
{
m_d->status = KisImportExportFilter::ProgressCancelled;
stopExport();
m_d->isCancelled = true;
}
void KisAnimationExporter::frameReadyToCopy(int time)
......@@ -205,23 +197,125 @@ void KisAnimationExporter::frameReadyToCopy(int time)
void KisAnimationExporter::frameReadyToSave()
{
QString frameNumber = QString("%1").arg(m_d->currentFrame, 4, 10, QChar('0'));
QString filename = m_d->filenamePrefix + frameNumber + m_d->filenameSuffix;
KIS_ASSERT_RECOVER(m_d->saveFrameCallback) {
m_d->status = KisImportExportFilter::InternalError;
emit sigFinished();
return;
}
if (m_d->isCancelled) {
m_d->status = KisImportExportFilter::UserCancelled;
emit sigFinished();
return;
}
KisImportExportFilter::ConversionStatus result =
KisImportExportFilter::OK;
int time = m_d->currentFrame;
if (m_d->tmpDoc->exportDocument(QUrl::fromLocalFile(filename))) {
if (m_d->exporting && m_d->currentFrame < m_d->lastFrame) {
if (!m_d->batchMode) {
emit m_d->document->sigProgress((m_d->currentFrame - m_d->firstFrame) * 100 /
(m_d->lastFrame - m_d->firstFrame));
}
m_d->currentFrame++;
m_d->image->animationInterface()->requestFrameRegeneration(m_d->currentFrame, m_d->image->bounds());
return; //continue
}
result = m_d->saveFrameCallback(time, m_d->tmpDevice);
if (!m_d->batchMode) {
emit m_d->document->sigProgress((time - m_d->firstFrame) * 100 /
(m_d->lastFrame - m_d->firstFrame));
}
if (result == KisImportExportFilter::OK &&
time < m_d->lastFrame) {
m_d->currentFrame = time + 1;
m_d->image->animationInterface()->requestFrameRegeneration(m_d->currentFrame, m_d->image->bounds());
} else {
//error
m_d->status = KisImportExportFilter::InternalError;
emit sigFinished();
}
}
struct KisAnimationExportSaver::Private
{
Private(KisDocument *document, int fromTime, int toTime)
: document(document),
image(document->image()),
firstFrame(fromTime),
lastFrame(toTime),
tmpDoc(KisPart::instance()->createDocument()),
exporter(document, fromTime, toTime)
{
tmpDoc->setAutoSave(0);
tmpImage = new KisImage(tmpDoc->createUndoStore(),
image->bounds().width(),
image->bounds().height(),
image->colorSpace(),
QString());
tmpImage->setResolution(image->xRes(), image->yRes());
tmpDoc->setCurrentImage(tmpImage);
KisPaintLayer* paintLayer = new KisPaintLayer(tmpImage, "paint device", 255);
tmpImage->addNode(paintLayer, tmpImage->rootLayer(), KisLayerSP(0));
tmpDevice = paintLayer->paintDevice();
}
KisDocument *document;
KisImageWSP image;
int firstFrame;
int lastFrame;
QScopedPointer<KisDocument> tmpDoc;
KisImageSP tmpImage;
KisPaintDeviceSP tmpDevice;
KisAnimationExporter exporter;
QString filenamePrefix;
QString filenameSuffix;
};
KisAnimationExportSaver::KisAnimationExportSaver(KisDocument *document, const QString &baseFilename, int fromTime, int toTime)
: m_d(new Private(document, fromTime, toTime))
{
int baseLength = baseFilename.lastIndexOf(".");
if (baseLength > -1) {
m_d->filenamePrefix = baseFilename.left(baseLength);
m_d->filenameSuffix = baseFilename.right(baseFilename.length() - baseLength);
} else {
m_d->filenamePrefix = baseFilename;
}
stopExport(); //finish
QString mimefilter = KisMimeDatabase::mimeTypeForFile(baseFilename);
m_d->tmpDoc->setOutputMimeType(mimefilter.toLatin1());
m_d->tmpDoc->setFileBatchMode(true);
using namespace std::placeholders; // For _1 placeholder
m_d->exporter.setSaveFrameCallback(std::bind(&KisAnimationExportSaver::saveFrameCallback, this, _1, _2));
}
KisAnimationExportSaver::~KisAnimationExportSaver()
{
}
KisImportExportFilter::ConversionStatus KisAnimationExportSaver::exportAnimation()
{
return m_d->exporter.exportAnimation();
}
KisImportExportFilter::ConversionStatus KisAnimationExportSaver::saveFrameCallback(int time, KisPaintDeviceSP frame)
{
KisImportExportFilter::ConversionStatus status =
KisImportExportFilter::OK;
QString frameNumber = QString("%1").arg(time, 4, 10, QChar('0'));
QString filename = m_d->filenamePrefix + frameNumber + m_d->filenameSuffix;
QRect rc = m_d->image->bounds();
KisPainter::copyAreaOptimized(rc.topLeft(), frame, m_d->tmpDevice, rc);
if (!m_d->tmpDoc->exportDocument(QUrl::fromLocalFile(filename))) {
status = KisImportExportFilter::InternalError;
}
return status;
}
......@@ -23,6 +23,9 @@
#include "kritaui_export.h"
#include <KisImportExportFilter.h>
#include <functional>
class KisDocument;
class KRITAUI_EXPORT KisAnimationExporterUI : public QObject
......@@ -43,13 +46,15 @@ private:
class KRITAUI_EXPORT KisAnimationExporter : public QObject
{
Q_OBJECT
public:
KisAnimationExporter(KisDocument *document, const QString &baseFilename, int fromTime, int toTime);
typedef std::function<KisImportExportFilter::ConversionStatus (int , KisPaintDeviceSP)> SaveFrameCallback;
public:
KisAnimationExporter(KisDocument *document, int fromTime, int toTime);
~KisAnimationExporter();
KisImportExportFilter::ConversionStatus exportAnimation();
void stopExport();
void setSaveFrameCallback(SaveFrameCallback func);
Q_SIGNALS:
// Internal, used for getting back to main thread
......@@ -66,5 +71,22 @@ private:
QScopedPointer<Private> m_d;
};
class KRITAUI_EXPORT KisAnimationExportSaver : public QObject
{
Q_OBJECT
public:
KisAnimationExportSaver(KisDocument *document, const QString &baseFilename, int fromTime, int toTime);
~KisAnimationExportSaver();
KisImportExportFilter::ConversionStatus exportAnimation();
private:
KisImportExportFilter::ConversionStatus saveFrameCallback(int time, KisPaintDeviceSP frame);
private:
struct Private;
const QScopedPointer<Private> m_d;
};
#endif
......@@ -34,12 +34,14 @@ void KisAnimationExporterTest::testAnimationExport()
{
KisDocument *document = KisPart::instance()->createDocument();
QRect rect(0,0,512,512);
QRect fillRect(10,0,502,512);
TestUtil::MaskParent p(rect);
document->setCurrentImage(p.image);
const KoColorSpace *cs = p.image->colorSpace();
KUndo2Command parentCommand;
p.layer->enableAnimation();
KisKeyframeChannel *rasterChannel = p.layer->getKeyframeChannel(KisKeyframeChannel::Content.id());
rasterChannel->addKeyframe(1, &parentCommand);
......@@ -48,22 +50,21 @@ void KisAnimationExporterTest::testAnimationExport()
KisPaintDeviceSP dev = p.layer->paintDevice();
dev->fill(rect, KoColor(Qt::red, cs));
QImage frame0 = dev->createThumbnail(50, 50);
dev->fill(fillRect, KoColor(Qt::red, cs));
QImage frame0 = dev->convertToQImage(0, rect);
p.image->animationInterface()->switchCurrentTimeAsync(1);
p.image->waitForDone();
dev->fill(rect, KoColor(Qt::green, cs));
QImage frame1 = dev->createThumbnail(50, 50);
dev->fill(fillRect, KoColor(Qt::green, cs));
QImage frame1 = dev->convertToQImage(0, rect);
p.image->animationInterface()->switchCurrentTimeAsync(2);
p.image->waitForDone();
dev->fill(rect, KoColor(Qt::blue, cs));
QImage frame2 = dev->createThumbnail(50, 50);
KisAnimationExporter exporter(document, "export-test.png", 0, 2);
QSignalSpy spy(&exporter, SIGNAL(sigExportProgress(int)));
dev->fill(fillRect, KoColor(Qt::blue, cs));
QImage frame2 = dev->convertToQImage(0, rect);
KisAnimationExportSaver exporter(document, "export-test.png", 0, 2);
QSignalSpy spy(document, SIGNAL(sigProgress(int)));
QVERIFY(spy.isValid());
exporter.exportAnimation();
......@@ -76,23 +77,23 @@ void KisAnimationExporterTest::testAnimationExport()
QTest::qWait(1000);
QCOMPARE(spy.count(), 3);
QCOMPARE(spy.at(0).at(0).value<int>(), 0);
QCOMPARE(spy.at(1).at(0).value<int>(), 1);
QCOMPARE(spy.at(2).at(0).value<int>(), 2);
//QCOMPARE(spy.count(), 3);
//QCOMPARE(spy.at(0).at(0).value<int>(), 0);
//QCOMPARE(spy.at(1).at(0).value<int>(), 1);
//QCOMPARE(spy.at(2).at(0).value<int>(), 2);
// FIXME: Export doesn't seem to work from unit tests
/*
QImage exported;
exported.load("export-test0.png");
QVERIFY(exported == frame0);
exported.load("export-test1.png");
QVERIFY(exported == frame1);
exported.load("export-test0000.png");
QCOMPARE(exported, frame0);
exported.load("export-test0001.png");
QCOMPARE(exported, frame1);
exported.load("export-test2.png");
QVERIFY(exported == frame2);
*/
exported.load("export-test0002.png");
QCOMPARE(exported, frame2);
}
QTEST_MAIN(KisAnimationExporterTest)
......@@ -120,7 +120,7 @@ KisImportExportFilter::ConversionStatus KisPNGExport::convert(const QByteArray&
}
} while (it.nextPixel());
if (qApp->applicationName() != "qttest") {
if (!qApp->applicationName().toLower().contains("test")) {
bool sRGB = (cs->profile()->name().contains(QLatin1String("srgb"), Qt::CaseInsensitive)
&& !cs->profile()->name().contains(QLatin1String("g10")));
......
......@@ -65,16 +65,13 @@ KisImportExportFilter::ConversionStatus KisVideoExport::convert(const QByteArray
if (filename.isEmpty()) return KisImportExportFilter::FileNotFound;
VideoSaver kpc(input, getBatchMode());
KisImageBuilder_Result res;
KisImageBuilder_Result res = kpc.encode(filename);
// if ((res = kpc.buildAnimation(filename)) == KisImageBuilder_RESULT_OK) {
// dbgFile <<"success !";
// return KisImportExportFilter::OK;
// }
// dbgFile <<" Result =" << res;
if (res == KisImageBuilder_RESULT_CANCEL)
if (res == KisImageBuilder_RESULT_OK) {
return KisImportExportFilter::OK;
} else if (res == KisImageBuilder_RESULT_CANCEL) {
return KisImportExportFilter::ProgressCancelled;
}
return KisImportExportFilter::InternalError;
}
......
......@@ -2,12 +2,12 @@
"Id": "Krita Video Export Filter",
"NoDisplay": "true",
"Type": "Service",
"X-KDE-Export": "video/x-matroska"
"X-KDE-Export": "video/x-matroska,image/gif,video/ogg,video/mp4",
"X-KDE-Import": "application/x-krita",
"X-KDE-Library": "kritavideoexport",
"X-KDE-ServiceTypes": [
"Krita/FileFilter"
],
"X-KDE-Weight": "1",
"X-KDE-Extensions" : "mkv"
"X-KDE-Extensions" : "mkv,gif,ogg,mp4"
}
......@@ -42,7 +42,8 @@ void KisVideoPluginTest::testFiles()
VideoSaver saver(doc, false);
saver.encode("testfile.mkv");
//saver.encode("testfile.gif");