[graphics/krita/krita/6.0] /: [android] Implement animation video rendering
Carsten Hartenfels <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 3cf46b57a785c44c5a1d911c5eefb7ea0cd88a0c by Carsten Hartenfels.
Committed on 27/07/2026 at 17:19.
Pushed by hartenfels into branch 'krita/6.0'.
[android] Implement animation video rendering
Through the Android Media Encoder API and libav, like it is done for
timelapse exports. Does not yet support audio.
M +2 -1 libs/ui/animation/KisAndroidMediaEncoderRunnable.cpp
M +110 -26 libs/ui/animation/KisAnimationRender.cpp
M +1 -1 libs/ui/animation/KisAnimationRender.h
M +22 -8 libs/ui/animation/KisAnimationRenderingOptions.cpp
M +16 -5 libs/ui/animation/KisAnimationRenderingOptions.h
M +185 -65 libs/ui/animation/KisDlgAnimationRenderer.cpp
M +30 -8 libs/ui/animation/KisDlgAnimationRenderer.h
M +19 -0 libs/ui/animation/KisLibavEncoderContext.h
M +2 -1 libs/ui/animation/KisLibavMediaEncoderRunnable.cpp
M +81 -0 libs/ui/animation/KisMediaEncoderWrapper.cpp
M +4 -0 libs/ui/animation/KisMediaEncoderWrapper.h
M +68 -4 libs/ui/animation/KisVideoSaver.cpp
M +12 -12 libs/ui/animation/KisVideoSaver.h
M +17 -10 libs/ui/animation/wdg_animationrenderer.ui
M +23 -0 plugins/dockers/compositiondocker/compositiondocker_dock.cpp
M +1 -0 plugins/dockers/recorder/recorder_export.cpp
https://invent.kde.org/graphics/krita/-/commit/3cf46b57a785c44c5a1d911c5eefb7ea0cd88a0c
diff --git a/libs/ui/animation/KisAndroidMediaEncoderRunnable.cpp b/libs/ui/animation/KisAndroidMediaEncoderRunnable.cpp
index 06ed3515e4e..9bea0460824 100644
--- a/libs/ui/animation/KisAndroidMediaEncoderRunnable.cpp
+++ b/libs/ui/animation/KisAndroidMediaEncoderRunnable.cpp
@@ -440,6 +440,7 @@ KisMediaEncoderRunnable::EncodeResult KisAndroidMediaEncoderRunnable::encode(QSt
// Encode the frames.
Frame frame;
+ int swsFlags = ctx.getSwsFlags(settings().scaleFilter);
while (nextFrame(frame)) {
if (isCancelled()) {
return EncodeResult::Cancelled;
@@ -516,7 +517,7 @@ KisMediaEncoderRunnable::EncodeResult KisAndroidMediaEncoderRunnable::encode(QSt
outputWidth,
outputHeight,
outputPixelFormat,
- SWS_FAST_BILINEAR);
+ swsFlags);
if (!swsContext) {
ctx.setInternalErrorMessage(QStringLiteral("sws_getCachedContext"));
return EncodeResult::Failed;
diff --git a/libs/ui/animation/KisAnimationRender.cpp b/libs/ui/animation/KisAnimationRender.cpp
index 78a45dd57fd..b820646ee80 100644
--- a/libs/ui/animation/KisAnimationRender.cpp
+++ b/libs/ui/animation/KisAnimationRender.cpp
@@ -24,13 +24,74 @@
#include "KisVideoSaver.h"
+#ifdef Q_OS_ANDROID
+#include <QTemporaryDir>
+#include <memory>
+#endif
+
+namespace
+{
+
+bool looksLikeMp4(const QString &videoType)
+{
+#ifdef Q_OS_ANDROID
+ return videoType.contains(QStringLiteral("mp4"));
+#else
+ return videoType == QStringLiteral("video/mp4");
+#endif
+}
+
+bool looksLikeMatroska(const QString &videoType)
+{
+#ifdef Q_OS_ANDROID
+ return videoType.contains(QStringLiteral("matroska"));
+#else
+ return videoType == QStringLiteral("video/x-matroska");
+#endif
+}
+
+} // namespace
+
bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, KisAnimationRenderingOptions encoderOptions) {
+#ifdef Q_OS_ANDROID
+ // The user may cancel the dialog prompting them for a video file, so bail
+ // out if we don't have one here. We can't create an implicit one next to
+ // the document on Android because of file system restrictions.
+ if (encoderOptions.shouldEncodeVideo && encoderOptions.videoFileName.isEmpty()) {
+ return false;
+ }
+#endif
+
+ bool isTemporaryFramesDirectory = false;
+ QString framesDirectory;
+#ifdef Q_OS_ANDROID
+ // Android uses weird content URIs instead of file paths and isn't allowed
+ // to scribble around in the file system without asking the user for access.
+ // We'll have to take the frames directory as it is given and if we don't
+ // get one then we'll create a temporary directory to stick our frames into.
+ std::unique_ptr<QTemporaryDir> tempDir;
+ if (encoderOptions.shouldDeleteSequence || encoderOptions.directory.isEmpty()) {
+ tempDir = std::make_unique<QTemporaryDir>();
+ KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(tempDir->isValid(), false);
+ framesDirectory = tempDir->path();
+ isTemporaryFramesDirectory = true;
+ } else {
+ framesDirectory = encoderOptions.directory;
+ }
+#else
+ framesDirectory = encoderOptions.resolveAbsoluteFramesDirectory();
+#endif
+
const QString frameMimeType = encoderOptions.frameMimeType;
- const QString framesDirectory = encoderOptions.resolveAbsoluteFramesDirectory();
const QString extension = KisMimeDatabase::suffixesForMimeType(frameMimeType).first();
const QString baseFileName = QString("%1/%2.%3").arg(framesDirectory, encoderOptions.basename, extension);
- if (mustHaveEvenDimensions(encoderOptions.videoMimeType, encoderOptions.renderMode())) {
+#ifdef Q_OS_ANDROID
+ QString videoType = encoderOptions.videoFormatKey;
+#else
+ QString videoType = encoderOptions.videoMimeType;
+#endif
+ if (mustHaveEvenDimensions(videoType, encoderOptions.renderMode())) {
if (hasEvenDimensions(encoderOptions.width, encoderOptions.height) != true) {
encoderOptions.width = encoderOptions.width + (encoderOptions.width & 0x1);
encoderOptions.height = encoderOptions.height + (encoderOptions.height & 0x1);
@@ -39,9 +100,9 @@ bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, K
const QSize scaledSize = doc->image()->bounds().size().scaled(encoderOptions.width, encoderOptions.height, Qt::IgnoreAspectRatio);
- if (mustHaveEvenDimensions(encoderOptions.videoMimeType, encoderOptions.renderMode())) {
+ if (mustHaveEvenDimensions(videoType, encoderOptions.renderMode())) {
if (hasEvenDimensions(scaledSize.width(), scaledSize.height()) != true) {
- QString type = encoderOptions.videoMimeType == "video/mp4" ? "Mpeg4 (.mp4) " : "Matroska (.mkv) ";
+ QString type = looksLikeMp4(videoType) ? "Mpeg4 (.mp4) " : "Matroska (.mkv) ";
qWarning() << type <<"requires width and height to be even, resize and try again!";
doc->setErrorMessage(i18n("%1 requires width and height to be even numbers. Please resize or crop the image before exporting.", type));
@@ -73,6 +134,12 @@ bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, K
const QString savedFilesMask = exporter.savedFilesMask();
if (encoderOptions.shouldEncodeVideo) {
+ bool videoFileWriteAllowed = true;
+ // Android's weird file system doesn't work this way, the target
+ // file path is going to be a sandbox URL. Making it absolute,
+ // creating a directory above it or checking for its existence
+ // neither make sense nor are they necessary.
+#ifndef Q_OS_ANDROID
const QString videoOutputFilePath = encoderOptions.resolveAbsoluteVideoFilePath();
KIS_SAFE_ASSERT_RECOVER_NOOP(QFileInfo(videoOutputFilePath).isAbsolute());
@@ -85,7 +152,6 @@ bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, K
KIS_SAFE_ASSERT_RECOVER_NOOP(outputDir.exists());
// If file exists at output path, prompt user for overwrite..
- bool videoFileWriteAllowed = true;
if (videoOutputFile.exists()) {
QMessageBox videoOverwritePrompt;
@@ -96,11 +162,16 @@ bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, K
videoFileWriteAllowed = videoOverwritePrompt.exec() == QMessageBox::Ok ? true : false;
}
+#endif
// Write the video..
if (videoFileWriteAllowed) {
KisImportExportErrorCode exportResult = ImportExportCodes::OK;
+ // Let's not mess with the file on Android like this, it's slow
+ // and could cause weird behavior depending on the provider.
+ // We'll notice that the file can't be opened later anyway.
+#ifndef Q_OS_ANDROID
QFile videoFile(videoOutputFilePath);
if (!videoFile.open(QIODevice::WriteOnly)) {
qWarning() << "Could not open" << videoFile.fileName() << "for writing! Do you have permission to write to this file?";
@@ -108,10 +179,16 @@ bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, K
} else {
videoFile.close();
}
+#endif
if (exportResult.isOk()) {
QScopedPointer<KisAnimationVideoSaver> encoder(new KisAnimationVideoSaver(doc, batchMode));
- exportResult = encoder->convert(doc, savedFilesMask, encoderOptions, batchMode);
+ exportResult = encoder->convert(doc,
+ framesDirectory,
+ savedFilesMask,
+ exporter.savedFiles(),
+ encoderOptions,
+ batchMode);
}
if (!exportResult.isOk()) {
@@ -123,31 +200,36 @@ bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, K
}
//File cleanup
- QDir d(framesDirectory);
+ if (!isTemporaryFramesDirectory) {
+ QDir d(framesDirectory);
- if (encoderOptions.shouldDeleteSequence || delayReturnSuccess == false) {
- QStringList savedFiles = exporter.savedFiles();
+ if (encoderOptions.shouldDeleteSequence || !delayReturnSuccess) {
+ QStringList savedFiles = exporter.savedFiles();
- Q_FOREACH(const QString &f, savedFiles) {
- if (d.exists(f)) {
- d.remove(f);
+ Q_FOREACH(const QString &f, savedFiles) {
+ if (d.exists(f)) {
+ d.remove(f);
+ }
}
- }
- } else if(encoderOptions.wantsOnlyUniqueFrameSequence) {
- const QStringList fileNames = exporter.savedFiles();
- const QStringList uniqueFrameNames = exporter.savedUniqueFiles();
-
- Q_FOREACH(const QString &f, fileNames) {
- if (!uniqueFrameNames.contains(f)) {
- d.remove(f);
+ } else if(encoderOptions.wantsOnlyUniqueFrameSequence) {
+ const QStringList fileNames = exporter.savedFiles();
+ const QStringList uniqueFrameNames = exporter.savedUniqueFiles();
+
+ Q_FOREACH(const QString &f, fileNames) {
+ if (!uniqueFrameNames.contains(f)) {
+ d.remove(f);
+ }
}
}
- }
- QStringList paletteFiles = d.entryList(QStringList() << "KritaTempPalettegen_*.png", QDir::Files);
+ // We don't generate palette files on Android, that's done in memory.
+#ifndef Q_OS_ANDROID
+ QStringList paletteFiles = d.entryList(QStringList() << "KritaTempPalettegen_*.png", QDir::Files);
- Q_FOREACH(const QString &f, paletteFiles) {
- d.remove(f);
+ Q_FOREACH(const QString &f, paletteFiles) {
+ d.remove(f);
+ }
+#endif
}
} else if (result == KisAsyncAnimationFramesSaveDialog::RenderTimedOut) {
QMessageBox::critical(qApp->activeWindow(), i18nc("@title:window", "Rendering error"), "Animation frame rendering has timed out. Output files are incomplete.\nTry to increase \"Frame Rendering Timeout\" or reduce \"Frame Rendering Clones Limit\" in Krita settings");
@@ -158,9 +240,11 @@ bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, K
return delayReturnSuccess;
}
-bool KisAnimationRender::mustHaveEvenDimensions(const QString &mimeType, KisAnimationRenderingOptions::RenderMode renderMode)
+bool KisAnimationRender::mustHaveEvenDimensions(const QString &videoType,
+ KisAnimationRenderingOptions::RenderMode renderMode)
{
- return (mimeType == "video/mp4" || mimeType == "video/x-matroska") && renderMode != KisAnimationRenderingOptions::RENDER_FRAMES_ONLY;
+ return renderMode != KisAnimationRenderingOptions::RENDER_FRAMES_ONLY
+ && (looksLikeMp4(videoType) || looksLikeMatroska(videoType));
}
bool KisAnimationRender::hasEvenDimensions(int width, int height)
diff --git a/libs/ui/animation/KisAnimationRender.h b/libs/ui/animation/KisAnimationRender.h
index 086465ea5d6..c5c9337e9c6 100644
--- a/libs/ui/animation/KisAnimationRender.h
+++ b/libs/ui/animation/KisAnimationRender.h
@@ -20,7 +20,7 @@ namespace KisAnimationRender {
**/
KRITAUI_EXPORT bool render(KisDocument *doc, KisViewManager* viewManager, KisAnimationRenderingOptions encoderOptions);
- bool mustHaveEvenDimensions(const QString &mimeType, KisAnimationRenderingOptions::RenderMode renderMode);
+ bool mustHaveEvenDimensions(const QString &videoType, KisAnimationRenderingOptions::RenderMode renderMode);
bool hasEvenDimensions(int width, int height);
}
diff --git a/libs/ui/animation/KisAnimationRenderingOptions.cpp b/libs/ui/animation/KisAnimationRenderingOptions.cpp
index bd45474797f..d02a5a3407d 100644
--- a/libs/ui/animation/KisAnimationRenderingOptions.cpp
+++ b/libs/ui/animation/KisAnimationRenderingOptions.cpp
@@ -11,15 +11,16 @@
#include <KisFileUtils.h>
+#ifdef Q_OS_ANDROID
+#include <QJsonDocument>
+#endif
+
KisAnimationRenderingOptions::KisAnimationRenderingOptions()
- : videoMimeType("video/mp4"),
- frameMimeType("image/png"),
- basename("frame"),
- directory("")
{
}
+#ifndef Q_OS_ANDROID
QString KisAnimationRenderingOptions::resolveAbsoluteDocumentFilePath(const QString &documentPath) const
{
return
@@ -53,6 +54,7 @@ QString KisAnimationRenderingOptions::resolveAbsoluteFramesDirectory() const
{
return resolveAbsoluteFramesDirectory(lastDocumentPath);
}
+#endif
KisAnimationRenderingOptions::RenderMode KisAnimationRenderingOptions::renderMode() const
{
@@ -77,20 +79,26 @@ KisPropertiesConfigurationSP KisAnimationRenderingOptions::toProperties() const
config->setProperty("first_frame", firstFrame);
config->setProperty("last_frame", lastFrame);
config->setProperty("sequence_start", sequenceStart);
- config->setProperty("video_mimetype", videoMimeType);
config->setProperty("frame_mimetype", frameMimeType);
config->setProperty("encode_video", shouldEncodeVideo);
config->setProperty("delete_sequence", shouldDeleteSequence);
config->setProperty("only_unique_frames", wantsOnlyUniqueFrameSequence);
- config->setProperty("ffmpeg_path", ffmpegPath);
config->setProperty("framerate", frameRate);
config->setProperty("height", height);
config->setProperty("width", width);
config->setProperty("include_audio", includeAudio);
config->setProperty("filename", videoFileName);
+
+#ifdef Q_OS_ANDROID
+ config->setProperty("video_format_key", videoFormatKey);
+ config->setProperty("video_format_preferences_json", videoFormatPreferencesJson);
+#else
+ config->setProperty("video_mimetype", videoMimeType);
+ config->setProperty("ffmpeg_path", ffmpegPath);
config->setProperty("custom_ffmpeg_options", customFFMpegOptions);
+#endif
config->setPrefixedProperties("frame_export/", frameExportConfig);
@@ -105,20 +113,26 @@ void KisAnimationRenderingOptions::fromProperties(KisPropertiesConfigurationSP c
firstFrame = config->getPropertyLazy("first_frame", 0);
lastFrame = config->getPropertyLazy("last_frame", 0);
sequenceStart = config->getPropertyLazy("sequence_start", 0);
- videoMimeType = config->getPropertyLazy("video_mimetype", videoMimeType);
frameMimeType = config->getPropertyLazy("frame_mimetype", frameMimeType);
shouldEncodeVideo = config->getPropertyLazy("encode_video", false);
shouldDeleteSequence = config->getPropertyLazy("delete_sequence", false);
wantsOnlyUniqueFrameSequence = config->getPropertyLazy("only_unique_frames", false);
- ffmpegPath = config->getPropertyLazy("ffmpeg_path", "");
frameRate = config->getPropertyLazy("framerate", 25);
height = config->getPropertyLazy("height", 0);
width = config->getPropertyLazy("width", 0);
includeAudio = config->getPropertyLazy("include_audio", true);
videoFileName = config->getPropertyLazy("filename", "");
+
+#ifdef Q_OS_ANDROID
+ videoFormatKey = config->getPropertyLazy("video_format_key", QString());
+ videoFormatPreferencesJson = config->getPropertyLazy("video_format_preferences_json", QString());
+#else
+ videoMimeType = config->getPropertyLazy("video_mimetype", videoMimeType);
+ ffmpegPath = config->getPropertyLazy("ffmpeg_path", "");
customFFMpegOptions = config->getPropertyLazy("custom_ffmpeg_options", "");
+#endif
frameExportConfig = new KisPropertiesConfiguration();
config->getPrefixedProperties("frame_export/", frameExportConfig);
diff --git a/libs/ui/animation/KisAnimationRenderingOptions.h b/libs/ui/animation/KisAnimationRenderingOptions.h
index 5e40598c769..0a247a4320e 100644
--- a/libs/ui/animation/KisAnimationRenderingOptions.h
+++ b/libs/ui/animation/KisAnimationRenderingOptions.h
@@ -18,11 +18,16 @@ public:
KisAnimationRenderingOptions();
QString lastDocumentPath;
- QString videoMimeType;
- QString frameMimeType;
-
- QString basename;
- QString directory;
+#ifdef Q_OS_ANDROID
+ QString videoFormatKey;
+ QString videoFormatPreferencesJson;
+#else
+ QString videoMimeType = QStringLiteral("video/mp4");
+#endif
+ QString frameMimeType = QStringLiteral("image/png");
+
+ QString basename = QStringLiteral("frame");
+ QString directory = QStringLiteral("");
int firstFrame = 0;
int lastFrame = 0;
int sequenceStart = 0;
@@ -32,22 +37,28 @@ public:
bool includeAudio = false;
bool wantsOnlyUniqueFrameSequence = false;
+#ifndef Q_OS_ANDROID
QString ffmpegPath;
+#endif
int frameRate = 25;
int width = 0;
int height = 0;
QString scaleFilter;
QString videoFileName;
+#ifndef Q_OS_ANDROID
QString customFFMpegOptions;
+#endif
KisPropertiesConfigurationSP frameExportConfig;
+#ifndef Q_OS_ANDROID
QString resolveAbsoluteDocumentFilePath(const QString &documentPath) const;
QString resolveAbsoluteVideoFilePath(const QString &documentPath) const;
QString resolveAbsoluteFramesDirectory(const QString &documentPath) const;
QString resolveAbsoluteVideoFilePath() const;
QString resolveAbsoluteFramesDirectory() const;
+#endif
enum RenderMode {
diff --git a/libs/ui/animation/KisDlgAnimationRenderer.cpp b/libs/ui/animation/KisDlgAnimationRenderer.cpp
index 9603683610c..f226b16d8a2 100644
--- a/libs/ui/animation/KisDlgAnimationRenderer.cpp
+++ b/libs/ui/animation/KisDlgAnimationRenderer.cpp
@@ -24,6 +24,7 @@
#include <KoJsonTrader.h>
#include <KisImportExportFilter.h>
#include <krita_container_utils.h>
+#include <kis_icon_utils.h>
#include <kis_image.h>
#include <kis_image_animation_interface.h>
#include <kis_time_span.h>
@@ -39,10 +40,17 @@
#include "kis_acyclic_signal_connector.h"
#include "KisVideoSaver.h"
#include "KisAnimationRenderingOptions.h"
-#include "animation/KisFFMpegWrapper.h"
-#include "VideoExportOptionsDialog.h"
#include "kis_image_config.h"
+#ifdef Q_OS_ANDROID
+#include <QJsonDocument>
+#include "animation/KisMediaEncoderFormatPreferencesDialog.h"
+#include "animation/KisMediaEncoderWrapper.h"
+#else
+#include "VideoExportOptionsDialog.h"
+#include "animation/KisFFMpegWrapper.h"
+#endif
+
KisDlgAnimationRenderer::KisDlgAnimationRenderer(KisDocument *doc, QWidget *parent)
: KoDialog(parent)
@@ -58,6 +66,22 @@ KisDlgAnimationRenderer::KisDlgAnimationRenderer(KisDocument *doc, QWidget *pare
m_page = new WdgAnimationRenderer(this);
m_page->layout()->setContentsMargins(0, 0, 0, 0);
+ {
+ QIcon editIcon = KisIconUtils::loadIcon("document-edit");
+ m_page->bnExportOptions->setIcon(editIcon);
+ m_page->bnRenderOptions->setIcon(editIcon);
+ }
+
+#ifdef Q_OS_ANDROID
+ m_page->lblVideoFilenameTitle->hide();
+ m_page->videoFilename->hide();
+ m_page->dirRequester->setReadOnlyText(true);
+ m_page->lblFFMpegLocationTitle->hide();
+ m_page->ffmpegLocation->hide();
+ m_page->lblFFMpegVersionTitle->hide();
+ m_page->lblFFMpegVersion->hide();
+#endif
+
m_page->dirRequester->setMode(KoFileDialog::OpenDirectory);
m_page->intStart->setMinimum(0);
@@ -91,16 +115,26 @@ KisDlgAnimationRenderer::KisDlgAnimationRenderer(KisDocument *doc, QWidget *pare
m_page->cmbMimetype->setCurrentIndex(m_page->cmbMimetype->count() - 1);
}
}
-
+
+#ifdef Q_OS_ANDROID
+ // Set up video export formats on Android. No ffmpeg here.
+ m_videoFormatPreferences = loadVideoFormatPreferences();
+ const QVector<KisMediaEncoderFormat *> videoFormats = KisMediaEncoderWrapper::getSupportedFormats();
+ for (KisMediaEncoderFormat *videoFormat : videoFormats) {
+ m_page->cmbRenderType->addItem(videoFormat->title(), QVariant(videoFormat->key()));
+ }
+#endif
+
m_page->cmbScaleFilter->addItem(i18nc("bicubic filtering", "bicubic"), "bicubic");
m_page->cmbScaleFilter->addItem(i18nc("bilinear filtering", "bilinear"), "bilinear");
m_page->cmbScaleFilter->addItem(i18nc("lanczos3 filtering", "lanczos3"), "lanczos");
m_page->cmbScaleFilter->addItem(i18nc("nearest neighbor filtering", "neighbor"), "neighbor");
m_page->cmbScaleFilter->addItem(i18nc("spline filtering", "spline"), "spline");
-
- m_page->videoFilename->setMode(KoFileDialog::SaveFile);
+#ifndef Q_OS_ANDROID
+ m_page->videoFilename->setMode(KoFileDialog::SaveFile);
m_page->ffmpegLocation->setMode(KoFileDialog::OpenFile);
+#endif
m_page->cmbRenderType->setPlaceholderText(i18nc("Not applicable. No render types without valid ffmpeg path.", "N/A"));
@@ -113,7 +147,9 @@ KisDlgAnimationRenderer::KisDlgAnimationRenderer(KisDocument *doc, QWidget *pare
connect(m_page->intFramesPerSecond, SIGNAL(valueChanged(int)), SLOT(frameRateChanged(int)));
+#ifndef Q_OS_ANDROID
connect(m_page->ffmpegLocation, SIGNAL(fileSelected(QString)), SLOT(setFFmpegPath(QString)));
+#endif
connect(this, SIGNAL(accepted()), SLOT(slotDialogAccepted()));
}
@@ -142,6 +178,7 @@ KisDlgAnimationRenderer::~KisDlgAnimationRenderer()
void KisDlgAnimationRenderer::initializeRenderSettings(const KisDocument &doc, const KisAnimationRenderingOptions &lastUsedOptions)
{
+#ifndef Q_OS_ANDROID
// Initialize FFmpeg location... (!)
KisConfig cfg(false);
QString cfgFFmpegPath = cfg.ffmpegLocation();
@@ -178,6 +215,7 @@ void KisDlgAnimationRenderer::initializeRenderSettings(const KisDocument &doc, c
if (!likelyFFmpegPath.isEmpty() && QFileInfo(likelyFFmpegPath).isExecutable()) {
setFFmpegPath(likelyFFmpegPath);
}
+#endif
const QString documentPath = m_doc->localFilePath();
@@ -192,20 +230,27 @@ void KisDlgAnimationRenderer::initializeRenderSettings(const KisDocument &doc, c
m_page->intWidth->setValue(lastUsedOptions.width);
m_page->intHeight->setValue(lastUsedOptions.height);
+#ifdef Q_OS_ANDROID
+ m_page->dirRequester->setStartDir(documentPath);
+#else
m_page->videoFilename->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
m_page->videoFilename->setFileName(lastUsedOptions.videoFileName);
-
m_page->dirRequester->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
+#endif
m_page->dirRequester->setFileName(lastUsedOptions.directory);
+
} else {
m_page->sequenceStart->setValue(m_image->animationInterface()->activePlaybackRange().start());
m_page->intWidth->setValue(m_image->width());
m_page->intHeight->setValue(m_image->height());
+#ifdef Q_OS_ANDROID
+ m_page->dirRequester->setStartDir(documentPath);
+#else
m_page->videoFilename->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
m_page->videoFilename->setFileName(defaultVideoFileName(m_doc, lastUsedOptions.videoMimeType));
-
m_page->dirRequester->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
+#endif
m_page->dirRequester->setFileName(lastUsedOptions.directory);
}
@@ -225,28 +270,25 @@ void KisDlgAnimationRenderer::initializeRenderSettings(const KisDocument &doc, c
}
// Initialize VIDEO render format...
+#ifdef Q_OS_ANDROID
+ const QString &lastVideoType = lastUsedOptions.videoFormatKey;
+#else
+ const QString &lastVideoType = lastUsedOptions.videoMimeType;
+#endif
for (int i = 0; i < m_page->cmbRenderType->count(); ++i) {
- if (m_page->cmbRenderType->itemData(i).toString() == lastUsedOptions.videoMimeType) {
+ if (m_page->cmbRenderType->itemData(i).toString() == lastVideoType) {
m_page->cmbRenderType->setCurrentIndex(i);
break;
}
}
m_page->chkOnlyUniqueFrames->setChecked(lastUsedOptions.wantsOnlyUniqueFrameSequence);
-
- if constexpr (PLATFORM_SUPPORTS_FFMPEG) {
- m_page->shouldExportOnlyVideo->setChecked(lastUsedOptions.shouldEncodeVideo);
- m_page->shouldExportOnlyImageSequence->setChecked(!lastUsedOptions.shouldDeleteSequence);
- } else {
- m_page->shouldExportOnlyVideo->setChecked(false);
- m_page->shouldExportOnlyVideo->setEnabled(false);
- m_page->shouldExportOnlyVideo->setVisible(false);
- m_page->shouldExportOnlyImageSequence->setChecked(true);
- m_page->shouldExportOnlyImageSequence->setCheckable(false);
- }
+ m_page->shouldExportOnlyVideo->setChecked(lastUsedOptions.shouldEncodeVideo);
+ m_page->shouldExportOnlyImageSequence->setChecked(!lastUsedOptions.shouldDeleteSequence);
slotExportTypeChanged();
+#ifndef Q_OS_ANDROID
{
KisPropertiesConfigurationSP settings = loadLastConfiguration("VIDEO_ENCODER");
@@ -269,6 +311,7 @@ void KisDlgAnimationRenderer::initializeRenderSettings(const KisDocument &doc, c
m_page->ffmpegLocation->setFileName(likelyFFmpegPath);
m_page->ffmpegLocation->setStartDir(QFileInfo(m_doc->localFilePath()).path());
m_page->ffmpegLocation->setReadOnlyText(true);
+#endif
// Initialize these settings based on the current document context..
m_page->intStart->setValue(doc.image()->animationInterface()->activePlaybackRange().start());
@@ -293,6 +336,7 @@ void KisDlgAnimationRenderer::initializeRenderSettings(const KisDocument &doc, c
m_page->chkIncludeAudio->setChecked(hasAudioLoaded);
}
+#ifndef Q_OS_ANDROID
void KisDlgAnimationRenderer::getDefaultVideoEncoderOptions(const QString &mimeType,
KisPropertiesConfigurationSP cfg,
const QStringList &availableEncoders,
@@ -311,6 +355,7 @@ void KisDlgAnimationRenderer::getDefaultVideoEncoderOptions(const QString &mimeT
*customFFMpegOptionsString = encoderConfigWidget->customUserOptionsString();
*renderHDR = encoderConfigWidget->videoConfiguredForHDR();
}
+#endif
void KisDlgAnimationRenderer::filterSequenceMimeTypes(QStringList &mimeTypes)
{
@@ -321,6 +366,7 @@ void KisDlgAnimationRenderer::filterSequenceMimeTypes(QStringList &mimeTypes)
});
}
+#ifndef Q_OS_ANDROID
QStringList KisDlgAnimationRenderer::makeVideoMimeTypesList()
{
QStringList supportedMimeTypes = QStringList();
@@ -394,12 +440,36 @@ QStringList KisDlgAnimationRenderer::filterMimeTypeListByAvailableEncoders(const
return retValue;
}
+#endif
bool KisDlgAnimationRenderer::imageMimeSupportsHDR(QString &mime)
{
return (mime == "image/png");
}
+#ifdef Q_OS_ANDROID
+QVariantMap KisDlgAnimationRenderer::loadVideoFormatPreferences()
+{
+ KisPropertiesConfigurationSP settings = loadLastConfiguration(QStringLiteral("VIDEO_ENCODER"));
+ QString s = settings->getString(QStringLiteral("format_preferences"));
+ if (!s.isEmpty()) {
+ QJsonDocument doc = QJsonDocument::fromJson(s.toUtf8());
+ if (doc.isObject()) {
+ return doc.toVariant().toMap();
+ }
+ }
+ return QVariantMap();
+}
+
+void KisDlgAnimationRenderer::saveVideoFormatPreferences(const QVariantMap &value)
+{
+ KisPropertiesConfigurationSP settings = new KisPropertiesConfiguration();
+ settings->setProperty(QStringLiteral("format_preferences"),
+ QString::fromUtf8(QJsonDocument::fromVariant(value).toJson(QJsonDocument::Compact)));
+ saveLastUsedConfiguration(QStringLiteral("VIDEO_ENCODER"), settings);
+}
+#endif
+
KisPropertiesConfigurationSP KisDlgAnimationRenderer::loadLastConfiguration(QString configurationID) {
KisConfig globalConfig(true);
return globalConfig.exportConfiguration(configurationID);
@@ -411,6 +481,16 @@ void KisDlgAnimationRenderer::saveLastUsedConfiguration(QString configurationID,
globalConfig.setExportConfiguration(configurationID, config);
}
+bool KisDlgAnimationRenderer::looksLikeGif(const QString &videoType)
+{
+#ifdef Q_OS_ANDROID
+ return videoType.contains(QStringLiteral(":gif"));
+#else
+ return videoType == QStringLiteral("image/gif");
+#endif
+}
+
+#ifndef Q_OS_ANDROID
void KisDlgAnimationRenderer::setFFmpegPath(const QString& path) {
// Let's START with the assumption that user-specified ffmpeg path is invalid
// and clear out all of the ffmpeg-specific fields to fill post-validation...
@@ -493,25 +573,28 @@ void KisDlgAnimationRenderer::setFFmpegPath(const QString& path) {
// Store configuration..
cfg.setFFMpegLocation(ffmpegJsonObj["path"].toString());
- ffmpegWarningCheck();
+ checkWarnings();
}
}
+#endif
+
+void KisDlgAnimationRenderer::checkWarnings()
+{
+ QStringList warnings;
-void KisDlgAnimationRenderer::ffmpegWarningCheck() {
- const QString mimeType = m_page->cmbRenderType->itemData(m_page->cmbRenderType->currentIndex()).toString();
+ QString videoType = m_page->cmbRenderType->itemData(m_page->cmbRenderType->currentIndex()).toString();
+ bool gif = looksLikeGif(videoType);
+#ifndef Q_OS_ANDROID
const QRegularExpression minVerFFMpegRX(R"(^n{0,1}(?:[0-3]|4\.[01])[\.\-])");
const QRegularExpressionMatch minVerFFMpegMatch = minVerFFMpegRX.match(ffmpegVersion);
- QStringList warnings;
-
- if (mimeType == "image/gif" && minVerFFMpegMatch.hasMatch()) {
+ if (gif && minVerFFMpegMatch.hasMatch()) {
warnings << i18nc("ffmpeg warning checks", "FFmpeg must be at least version 4.2+ for GIF transparency to work");
}
+#endif
- // m_page->bnRenderOptions->setEnabled(mimeType != "image/gif" && mimeType != "image/webp" && mimeType !=
- // "image/png" );
- if (mimeType == "image/gif" && m_page->intFramesPerSecond->value() > 50) {
+ if (gif && m_page->intFramesPerSecond->value() > 50) {
warnings << i18nc("ffmpeg warning checks",
"Animated GIF images cannot have a framerate higher than 50. The framerate will be reduced "
"to 50 frames per second");
@@ -537,6 +620,7 @@ void KisDlgAnimationRenderer::ffmpegWarningCheck() {
m_page->adjustSize();
}
+#ifndef Q_OS_ANDROID
QString KisDlgAnimationRenderer::defaultVideoFileName(KisDocument *doc, const QString &mimeType)
{
const QString docFileName = !doc->localFilePath().isEmpty() ? doc->localFilePath() : i18n("Untitled");
@@ -555,11 +639,7 @@ void KisDlgAnimationRenderer::selectRenderType(int index)
const QString mimeType = m_page->cmbRenderType->itemData(index).toString();
- /*
- m_page->bnRenderOptions->setEnabled(mimeType != "image/gif" && mimeType != "image/webp" && mimeType != "image/png");
- */
-
- ffmpegWarningCheck();
+ checkWarnings();
QString videoFileName = defaultVideoFileName(m_doc, mimeType);
@@ -592,9 +672,21 @@ void KisDlgAnimationRenderer::selectRenderType(int index)
&m_wantsRenderWithHDR);
}
}
+#endif
void KisDlgAnimationRenderer::selectRenderOptions()
{
+#ifdef Q_OS_ANDROID
+ QString key = m_page->cmbRenderType->currentData().toString();
+ KisMediaEncoderFormat *format = KisMediaEncoderWrapper::getFormatByKey(key);
+ KIS_SAFE_ASSERT_RECOVER_RETURN(format);
+
+ KisMediaEncoderPreferencesDialog dlg(format, m_videoFormatPreferences.value(key).toMap(), this);
+ if (dlg.exec() == QDialog::Accepted) {
+ m_videoFormatPreferences.insert(key, dlg.preferences());
+ saveVideoFormatPreferences(m_videoFormatPreferences);
+ }
+#else
const int index = m_page->cmbRenderType->currentIndex();
const QString mimetype = m_page->cmbRenderType->itemData(index).toString();
@@ -623,12 +715,15 @@ void KisDlgAnimationRenderer::selectRenderOptions()
dlg.setButtons(KoDialog::Ok | KoDialog::Cancel);
if (dlg.exec() == QDialog::Accepted) {
saveLastUsedConfiguration("VIDEO_ENCODER", encoderConfigWidget->configuration());
+#ifndef Q_OS_ANDROID
m_customFFMpegOptionsString = encoderConfigWidget->customUserOptionsString();
m_wantsRenderWithHDR = encoderConfigWidget->videoConfiguredForHDR();
+#endif
}
dlg.setMainWidget(0);
encoderConfigWidget->deleteLater();
+#endif
}
void KisDlgAnimationRenderer::sequenceMimeTypeOptionsClicked()
@@ -650,19 +745,23 @@ void KisDlgAnimationRenderer::sequenceMimeTypeOptionsClicked()
}
//Important -- m_useHDR allows the synchronization of both the video and image render settings.
+#ifndef Q_OS_ANDROID
if(imageMimeSupportsHDR(mimetype)) {
exportConfig->setProperty("saveAsHDR", m_wantsRenderWithHDR);
if (m_wantsRenderWithHDR) {
exportConfig->setProperty("forceSRGB", false);
}
}
+#endif
frameExportConfigWidget->setConfiguration(exportConfig);
KoDialog dlg(this);
dlg.setMainWidget(frameExportConfigWidget);
dlg.setButtons(KoDialog::Ok | KoDialog::Cancel);
if (dlg.exec() == QDialog::Accepted) {
+#ifndef Q_OS_ANDROID
m_wantsRenderWithHDR = frameExportConfigWidget->configuration()->getPropertyLazy("saveAsHDR", false);
+#endif
saveLastUsedConfiguration("img_sequence/" + mimetype, frameExportConfigWidget->configuration());
}
@@ -681,7 +780,21 @@ KisAnimationRenderingOptions KisDlgAnimationRenderer::getEncoderOptions() const
KisAnimationRenderingOptions options;
options.lastDocumentPath = m_doc->localFilePath();
- options.videoMimeType = m_page->cmbRenderType->currentData().toString();
+ QString videoType = m_page->cmbRenderType->currentData().toString();
+#ifdef Q_OS_ANDROID
+ options.videoFormatKey = videoType;
+ QVariantMap videoFormatPreferences = m_videoFormatPreferences.value(videoType).toMap();
+ if (!videoFormatPreferences.isEmpty()) {
+ options.videoFormatPreferencesJson =
+ QString::fromUtf8(QJsonDocument::fromVariant(videoFormatPreferences).toJson(QJsonDocument::Compact));
+ }
+ options.videoFileName = m_videoFileName;
+#else
+ options.videoMimeType = videoType;
+ options.videoFileName = m_page->videoFilename->fileName();
+ options.ffmpegPath = m_page->ffmpegLocation->fileName();
+ options.customFFMpegOptions = m_customFFMpegOptionsString;
+#endif
options.frameMimeType = m_page->cmbMimetype->currentData().toString();
options.scaleFilter = m_page->cmbScaleFilter->currentData().toString();
@@ -691,28 +804,18 @@ KisAnimationRenderingOptions KisDlgAnimationRenderer::getEncoderOptions() const
options.lastFrame = m_page->intEnd->value();
options.sequenceStart = m_page->sequenceStart->value();
- if constexpr (PLATFORM_SUPPORTS_FFMPEG) {
- options.shouldEncodeVideo = m_page->shouldExportOnlyVideo->isChecked();
- options.shouldDeleteSequence = !m_page->shouldExportOnlyImageSequence->isChecked();
- } else {
- options.shouldEncodeVideo = false;
- options.shouldDeleteSequence = false;
- }
+ options.shouldEncodeVideo = m_page->shouldExportOnlyVideo->isChecked();
+ options.shouldDeleteSequence = !m_page->shouldExportOnlyImageSequence->isChecked();
options.includeAudio = m_page->chkIncludeAudio->isChecked();
options.wantsOnlyUniqueFrameSequence = m_page->chkOnlyUniqueFrames->isChecked();
- options.ffmpegPath = m_page->ffmpegLocation->fileName();
options.frameRate = m_page->intFramesPerSecond->value();
-
- if (options.frameRate > 50 && options.videoMimeType == "image/gif") {
+ if (options.frameRate > 50 && looksLikeGif(videoType)) {
options.frameRate = 50;
}
options.width = m_page->intWidth->value();
options.height = m_page->intHeight->value();
- options.videoFileName = m_page->videoFilename->fileName();
-
- options.customFFMpegOptions = m_customFFMpegOptionsString;
{
KisPropertiesConfigurationSP cfg = loadLastConfiguration("img_sequence/" + options.frameMimeType);
@@ -720,12 +823,14 @@ KisAnimationRenderingOptions KisDlgAnimationRenderer::getEncoderOptions() const
KisImportExportManager::fillStaticExportConfigurationProperties(cfg, m_image);
}
+#ifndef Q_OS_ANDROID
const bool forceNecessaryHDRSettings = m_wantsRenderWithHDR && imageMimeSupportsHDR(options.frameMimeType);
if (forceNecessaryHDRSettings) {
KIS_SAFE_ASSERT_RECOVER_NOOP(options.frameMimeType == "image/png");
cfg->setProperty("forceSRGB", false);
cfg->setProperty("saveAsHDR", true);
}
+#endif
options.frameExportConfig = cfg;
}
@@ -733,6 +838,7 @@ KisAnimationRenderingOptions KisDlgAnimationRenderer::getEncoderOptions() const
return options;
}
+#ifndef Q_OS_ANDROID
KisDlgAnimationRenderer::FFmpegValidationResult KisDlgAnimationRenderer::validateFFmpeg(const QString &ffmpegPath)
{
if (!ffmpegPath.isEmpty()) {
@@ -751,10 +857,12 @@ KisDlgAnimationRenderer::FFmpegValidationResult KisDlgAnimationRenderer::validat
}
return FFmpegValidationResult::INVALID;
}
+#endif
+#ifndef Q_OS_ANDROID
void KisDlgAnimationRenderer::slotButtonClicked(int button)
{
- if (button == KoDialog::Ok && (PLATFORM_SUPPORTS_FFMPEG && !m_page->shouldExportOnlyImageSequence->isChecked())) {
+ if (button == KoDialog::Ok && !m_page->shouldExportOnlyImageSequence->isChecked()) {
QString fileName = m_page->videoFilename->fileName();
if (fileName.isEmpty()) {
@@ -776,9 +884,26 @@ void KisDlgAnimationRenderer::slotButtonClicked(int button)
}
KoDialog::slotButtonClicked(button);
}
+#endif
void KisDlgAnimationRenderer::slotDialogAccepted()
{
+#ifdef Q_OS_ANDROID
+ if (m_page->shouldExportOnlyVideo) {
+ KisMediaEncoderFormat *format = KisMediaEncoderWrapper::getFormatByKey(m_page->cmbRenderType->currentData().toString());
+ KIS_SAFE_ASSERT_RECOVER_RETURN(format);
+ KoFileDialog dialog(this, KoFileDialog::SaveFile, QStringLiteral("ExportAnimation"));
+ dialog.setMimeTypeFilters(QStringList(KisMimeDatabase::mimeTypeForSuffix(format->extension())));
+ dialog.setDefaultDir(m_doc->localFilePath());
+ m_videoFileName = dialog.filename();
+ if (m_videoFileName.isEmpty()) {
+ return;
+ }
+ } else {
+ m_videoFileName.clear();
+ }
+#endif
+
KisConfig cfg(false);
KisAnimationRenderingOptions options = getEncoderOptions();
saveLastUsedConfiguration("ANIMATION_EXPORT", options.toProperties());
@@ -790,24 +915,19 @@ void KisDlgAnimationRenderer::slotDialogAccepted()
void KisDlgAnimationRenderer::slotExportTypeChanged()
{
- if constexpr (PLATFORM_SUPPORTS_FFMPEG) {
- // if a video format needs to be outputted
- if (m_page->shouldExportOnlyVideo->isChecked()) {
- // videos always uses PNG for creating video, so disable the ability to change the format
- m_page->cmbMimetype->setEnabled(false);
- m_page->cmbMimetype->setCurrentIndex(m_page->cmbMimetype->findData("image/png"));
- }
+ // if a video format needs to be outputted
+ if (m_page->shouldExportOnlyVideo->isChecked()) {
+ // videos always uses PNG for creating video, so disable the ability to change the format
+ m_page->cmbMimetype->setEnabled(false);
+ m_page->cmbMimetype->setCurrentIndex(m_page->cmbMimetype->findData("image/png"));
+ }
- /**
- * A fallback fix for a case when both checkboxes are unchecked
- */
- if (!m_page->shouldExportOnlyVideo->isChecked() &&
- !m_page->shouldExportOnlyImageSequence->isChecked()) {
+ /**
+ * A fallback fix for a case when both checkboxes are unchecked
+ */
+ if (!m_page->shouldExportOnlyVideo->isChecked() &&
+ !m_page->shouldExportOnlyImageSequence->isChecked()) {
- KisSignalsBlocker b(m_page->shouldExportOnlyImageSequence);
- m_page->shouldExportOnlyImageSequence->setChecked(true);
- }
- } else {
KisSignalsBlocker b(m_page->shouldExportOnlyImageSequence);
m_page->shouldExportOnlyImageSequence->setChecked(true);
}
@@ -816,7 +936,7 @@ void KisDlgAnimationRenderer::slotExportTypeChanged()
void KisDlgAnimationRenderer::frameRateChanged(int framerate)
{
Q_UNUSED(framerate);
- ffmpegWarningCheck();
+ checkWarnings();
}
void KisDlgAnimationRenderer::slotLockAspectRatioDimensionsWidth(int width)
diff --git a/libs/ui/animation/KisDlgAnimationRenderer.h b/libs/ui/animation/KisDlgAnimationRenderer.h
index 9e2f0e80be8..be295b2e248 100644
--- a/libs/ui/animation/KisDlgAnimationRenderer.h
+++ b/libs/ui/animation/KisDlgAnimationRenderer.h
@@ -13,6 +13,9 @@
#include <kis_types.h>
+#ifdef Q_OS_ANDROID
+#include <QVariantMap>
+#endif
#include "kritaui_export.h"
@@ -48,7 +51,9 @@ public:
KisAnimationRenderingOptions getEncoderOptions() const;
private Q_SLOTS:
+#ifndef Q_OS_ANDROID
void selectRenderType(int i);
+#endif
void selectRenderOptions();
/**
* @brief sequenceMimeTypeSelected
@@ -60,32 +65,33 @@ private Q_SLOTS:
void slotLockAspectRatioDimensionsHeight(int height);
void slotExportTypeChanged();
+#ifndef Q_OS_ANDROID
void setFFmpegPath(const QString& path);
+#endif
void frameRateChanged(int framerate);
protected Q_SLOTS:
-
+#ifndef Q_OS_ANDROID
void slotButtonClicked(int button) override;
+#endif
void slotDialogAccepted();
private:
-#ifdef Q_OS_ANDROID
- static constexpr bool PLATFORM_SUPPORTS_FFMPEG = false;
-#else
- static constexpr bool PLATFORM_SUPPORTS_FFMPEG = true;
-#endif
-
+#ifndef Q_OS_ANDROID
enum FFmpegValidationResult {
VALID = 1,
INVALID = 0,
NOT_A_BINARY = -1,
COMPRESSED_FORMAT = -2
};
+#endif
void initializeRenderSettings(const KisDocument &doc, const KisAnimationRenderingOptions &lastUsedOptions);
- void ffmpegWarningCheck();
+
+ void checkWarnings();
+#ifndef Q_OS_ANDROID
FFmpegValidationResult validateFFmpeg(const QString &ffmpegPath);
static QString defaultVideoFileName(KisDocument *doc, const QString &mimeType);
@@ -95,19 +101,34 @@ private:
const QStringList &availableEncoders,
QString *customFFMpegOptionsString,
bool *forceHDRVideo);
+#endif
static void filterSequenceMimeTypes(QStringList &mimeTypes);
+
+#ifndef Q_OS_ANDROID
static QStringList makeVideoMimeTypesList();
QStringList filterMimeTypeListByAvailableEncoders(const QStringList &mimeTypes);
+#endif
static bool imageMimeSupportsHDR(QString &hdr);
+#ifdef Q_OS_ANDROID
+ static QVariantMap loadVideoFormatPreferences();
+ static void saveVideoFormatPreferences(const QVariantMap &value);
+#endif
+
static KisPropertiesConfigurationSP loadLastConfiguration(QString configurationID);
static void saveLastUsedConfiguration(QString configurationID, KisPropertiesConfigurationSP config);
+ static bool looksLikeGif(const QString &videoType);
+
private:
KisImageSP m_image;
KisDocument *m_doc;
+#ifdef Q_OS_ANDROID
+ QString m_videoFileName;
+ QVariantMap m_videoFormatPreferences;
+#else
QString m_customFFMpegOptionsString;
QString ffmpegVersion = "None";
@@ -115,6 +136,7 @@ private:
QMap<QString, QStringList> ffmpegEncoderTypes; // Maps supported output format to available list of encoder(s)
bool m_wantsRenderWithHDR = false;
+#endif
WdgAnimationRenderer *m_page {0};
};
diff --git a/libs/ui/animation/KisLibavEncoderContext.h b/libs/ui/animation/KisLibavEncoderContext.h
index 01536d4c7c8..67086591d16 100644
--- a/libs/ui/animation/KisLibavEncoderContext.h
+++ b/libs/ui/animation/KisLibavEncoderContext.h
@@ -54,6 +54,25 @@ public:
return true;
}
+ int getSwsFlags(const QString &scaleFilter) const
+ {
+ // These are the values provided by KisDlgAnimationRenderer.
+ if (scaleFilter.isEmpty() || scaleFilter == QStringLiteral("bilinear")) {
+ return SWS_FAST_BILINEAR;
+ } else if (scaleFilter == QStringLiteral("bicubic")) {
+ return SWS_BICUBIC;
+ } else if (scaleFilter == QStringLiteral("lanczos")) {
+ return SWS_LANCZOS;
+ } else if (scaleFilter == QStringLiteral("neighbor")) {
+ return 0;
+ } else if (scaleFilter == QStringLiteral("spline")) {
+ return SWS_SPLINE;
+ } else {
+ warnFile.nospace() << "Unhandled scale filter '" << scaleFilter << "'";
+ return SWS_FAST_BILINEAR;
+ }
+ }
+
SwsContext *getSwsContextFor(int inputWidth,
int inputHeight,
AVPixelFormat inputFormat,
diff --git a/libs/ui/animation/KisLibavMediaEncoderRunnable.cpp b/libs/ui/animation/KisLibavMediaEncoderRunnable.cpp
index 1dbf3e9578b..eee0053c7ff 100644
--- a/libs/ui/animation/KisLibavMediaEncoderRunnable.cpp
+++ b/libs/ui/animation/KisLibavMediaEncoderRunnable.cpp
@@ -664,6 +664,7 @@ public:
m_frame->pts = 0;
Frame inputFrame;
+ int swsFlags = getSwsFlags(settings.scaleFilter);
while (m_runnable->nextFrame(inputFrame)) {
if (isCancelled()) {
return EncodeResult::Cancelled;
@@ -701,7 +702,7 @@ public:
outputWidth,
outputHeight,
AVPixelFormat(m_frame->format),
- SWS_FAST_BILINEAR);
+ swsFlags);
if (!swsContext) {
setInternalErrorMessage(QStringLiteral("sws_getCachedContext failed"));
return EncodeResult::Failed;
diff --git a/libs/ui/animation/KisMediaEncoderWrapper.cpp b/libs/ui/animation/KisMediaEncoderWrapper.cpp
index 9d0c84c3cf8..599ed54f2d5 100644
--- a/libs/ui/animation/KisMediaEncoderWrapper.cpp
+++ b/libs/ui/animation/KisMediaEncoderWrapper.cpp
@@ -2,12 +2,15 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include <QAtomicInt>
+#include <QCoreApplication>
#include <QImage>
#include <QImageReader>
+#include <QProgressDialog>
#include <QRunnable>
#include <QThreadPool>
#include <functional>
+#include <memory>
#include <klocalizedstring.h>
@@ -160,6 +163,84 @@ KisMediaEncoderWrapper::~KisMediaEncoderWrapper()
reset();
}
+KisImportExportErrorCode KisMediaEncoderWrapper::start(const KisMediaEncoderWrapperSettings &settings, bool batchMode)
+{
+ reset();
+
+ KisMediaEncoderRunnable *runnable = makeSupportedRunnable(settings);
+ ImportExportCodes::ErrorCodeID resultCode = ImportExportCodes::InternalError;
+ if (runnable) {
+ connect(
+ runnable,
+ &KisMediaEncoderRunnable::sigCompleted,
+ this,
+ [&resultCode] {
+ resultCode = ImportExportCodes::OK;
+ },
+ Qt::DirectConnection);
+ connect(
+ runnable,
+ &KisMediaEncoderRunnable::sigCancelled,
+ this,
+ [&resultCode] {
+ resultCode = ImportExportCodes::Cancelled;
+ },
+ Qt::DirectConnection);
+ connect(
+ runnable,
+ &KisMediaEncoderRunnable::sigFailed,
+ this,
+ [&resultCode](const QString &errorMessage) {
+ warnFile << "Media encoder export error:" << errorMessage;
+ resultCode = ImportExportCodes::OK;
+ },
+ Qt::DirectConnection);
+ connect(this,
+ &KisMediaEncoderWrapper::sigCancelRequested,
+ runnable,
+ &KisMediaEncoderRunnable::slotHandleCancelRequested,
+ Qt::DirectConnection);
+
+ QProgressDialog *progressDlg;
+ if (batchMode) {
+ progressDlg = nullptr;
+ } else {
+ progressDlg = new QProgressDialog;
+ progressDlg->setLabelText(i18n("Rendering animation..."));
+ connect(
+ runnable,
+ &KisMediaEncoderRunnable::sigProgressUpdated,
+ this,
+ [progressDlg, frameCount = settings.inputFiles.size()](int frameNo) {
+ int progress;
+ if (frameNo < 0) {
+ progress = 0;
+ } else if (frameNo >= frameCount) {
+ progress = 100;
+ } else {
+ progress = qRound(qreal(frameNo) / qreal(frameCount) * 100.0);
+ }
+ progressDlg->setValue(progress);
+ QCoreApplication::processEvents();
+ },
+ Qt::DirectConnection);
+ connect(progressDlg,
+ &QProgressDialog::canceled,
+ this,
+ &KisMediaEncoderWrapper::sigCancelRequested,
+ Qt::DirectConnection);
+ progressDlg->show();
+ QCoreApplication::processEvents();
+ }
+
+ m_runnable = runnable;
+ runnable->run();
+ delete runnable;
+ delete progressDlg;
+ }
+ return KisImportExportErrorCode(resultCode);
+}
+
void KisMediaEncoderWrapper::startNonBlocking(const KisMediaEncoderWrapperSettings &settings)
{
reset();
diff --git a/libs/ui/animation/KisMediaEncoderWrapper.h b/libs/ui/animation/KisMediaEncoderWrapper.h
index 539eba6c1b0..1c7a481271d 100644
--- a/libs/ui/animation/KisMediaEncoderWrapper.h
+++ b/libs/ui/animation/KisMediaEncoderWrapper.h
@@ -14,6 +14,8 @@
#include <kritaui_export.h>
+#include "KisImportExportErrorCode.h"
+
class QImage;
class QWidget;
@@ -45,6 +47,7 @@ struct KRITAUI_EXPORT KisMediaEncoderWrapperSettings {
QStringList inputFiles;
KisMediaEncoderFormat *format;
QVariantMap formatPreferences;
+ QString scaleFilter;
QSize outputSize;
int inputFps;
int outputFps;
@@ -141,6 +144,7 @@ public:
explicit KisMediaEncoderWrapper(QObject *parent = nullptr);
~KisMediaEncoderWrapper() override;
+ KisImportExportErrorCode start(const KisMediaEncoderWrapperSettings &settings, bool batchMode);
void startNonBlocking(const KisMediaEncoderWrapperSettings &settings);
void reset();
diff --git a/libs/ui/animation/KisVideoSaver.cpp b/libs/ui/animation/KisVideoSaver.cpp
index 9116d127f68..c26cf038506 100644
--- a/libs/ui/animation/KisVideoSaver.cpp
+++ b/libs/ui/animation/KisVideoSaver.cpp
@@ -26,10 +26,16 @@
#include <KoResourcePaths.h>
#include "kis_config.h"
#include "KisAnimationRenderingOptions.h"
-#include "animation/KisFFMpegWrapper.h"
#include "KisPart.h"
+#ifdef Q_OS_ANDROID
+#include "animation/KisMediaEncoderWrapper.h"
+#include <QJsonDocument>
+#else
+#include "animation/KisFFMpegWrapper.h"
+#endif
+
KisAnimationVideoSaver::KisAnimationVideoSaver(KisDocument *doc, bool batchMode)
: m_image(doc->image())
, m_doc(doc)
@@ -46,8 +52,60 @@ KisImageSP KisAnimationVideoSaver::image()
return m_image;
}
-KisImportExportErrorCode KisAnimationVideoSaver::encode(const QString &savedFilesMask, const KisAnimationRenderingOptions &options)
+KisImportExportErrorCode KisAnimationVideoSaver::encode(const QString &framesDirectory,
+ const QString &savedFilesMask,
+ const QStringList &savedFiles,
+ const KisAnimationRenderingOptions &options)
{
+#ifdef Q_OS_ANDROID
+ Q_UNUSED(savedFilesMask);
+
+ KisMediaEncoderFormat *format = KisMediaEncoderWrapper::getFormatByKey(options.videoFormatKey);
+ KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(format, ImportExportCodes::InternalError);
+
+ QVariantMap formatPreferences;
+ if (!options.videoFormatPreferencesJson.isEmpty()) {
+ formatPreferences = QJsonDocument::fromJson(options.videoFormatPreferencesJson.toUtf8()).toVariant().toMap();
+ }
+
+ KisMediaEncoderWrapperSettings settings = {
+ options.videoFileName,
+ QStringList(),
+ format,
+ formatPreferences,
+ options.scaleFilter,
+ QSize(options.width, options.height),
+ options.frameRate,
+ options.frameRate,
+ 0,
+ 0,
+ };
+
+ QString inputDir = framesDirectory;
+ if (!inputDir.endsWith(QStringLiteral("/"))) {
+ inputDir.append(QStringLiteral("/"));
+ }
+
+ settings.inputFiles.reserve(savedFiles.size());
+ for (const QString &savedFile : savedFiles) {
+ settings.inputFiles.append(inputDir + savedFile);
+ }
+
+ // This whole batchMode business is either a vestige or was never actually
+ // implemented to completion. It's *supposed* to not show dialogs if Krita
+ // is run in batch mode via the command line, but it's not actually wired up
+ // properly. KisAnimationRender always sets it to false with a TODO to fetch
+ // it correctly and the ffmpeg code here never sets it either, meaning it'll
+ // always be false. For consistency, I've made KisMediaEncoderWrapper behave
+ // the same, even if it's kind of pointless. But if you're working on this
+ // in the future, you should be able to do the same thing for this code path
+ // as you're doing for the ffmpeg one.
+ bool batchMode = false;
+ return KisMediaEncoderWrapper().start(settings, batchMode);
+#else
+ Q_UNUSED(framesDirectory);
+ Q_UNUSED(savedFiles);
+
if (!QFileInfo(options.ffmpegPath).exists()) {
m_doc->setErrorMessage(i18n("ffmpeg could not be found at %1", options.ffmpegPath));
return ImportExportCodes::Failure;
@@ -202,11 +260,17 @@ KisImportExportErrorCode KisAnimationVideoSaver::encode(const QString &savedFile
return resultOuter;
+#endif
}
-KisImportExportErrorCode KisAnimationVideoSaver::convert(KisDocument *document, const QString &savedFilesMask, const KisAnimationRenderingOptions &options, bool batchMode)
+KisImportExportErrorCode KisAnimationVideoSaver::convert(KisDocument *document,
+ const QString &framesDirectory,
+ const QString &savedFilesMask,
+ const QStringList &savedFiles,
+ const KisAnimationRenderingOptions &options,
+ bool batchMode)
{
KisAnimationVideoSaver videoSaver(document, batchMode);
- KisImportExportErrorCode res = videoSaver.encode(savedFilesMask, options);
+ KisImportExportErrorCode res = videoSaver.encode(framesDirectory, savedFilesMask, savedFiles, options);
return res;
}
diff --git a/libs/ui/animation/KisVideoSaver.h b/libs/ui/animation/KisVideoSaver.h
index 9afea47a751..743dd4f933d 100644
--- a/libs/ui/animation/KisVideoSaver.h
+++ b/libs/ui/animation/KisVideoSaver.h
@@ -24,9 +24,8 @@ public:
/**
* @brief KisAnimationVideoSaver
* This is the object that takes an animation document and config and tells ffmpeg
- * to render it via KisFFMpegWrapper.
+ * to render it via KisFFMpegWrapper or uses KisMediaEncoderWrapper on Android.
* @param doc the document to use for rendering.
- * @param ffmpegPath the path to the ffmpeg executable.
* @param batchMode whether Krita is in batchmode and we can thus not show gui widgets.
*/
KisAnimationVideoSaver(KisDocument* doc, bool batchMode);
@@ -38,16 +37,17 @@ public:
*/
KisImageSP image();
- /**
- * @brief encode the main encoding function.
- * This in turn calls runFFMpeg, which is a private function inside this class.
- * @param filename the filename to which to render the animation.
- * @param configuration the configuration
- * @return whether it is successful or had another failure.
- */
- KisImportExportErrorCode encode(const QString &savedFilesMask, const KisAnimationRenderingOptions &options);
-
- static KisImportExportErrorCode convert(KisDocument *document, const QString &savedFilesMask, const KisAnimationRenderingOptions &options, bool batchMode);
+ KisImportExportErrorCode encode(const QString &framesDirectory,
+ const QString &savedFilesMask,
+ const QStringList &savedFiles,
+ const KisAnimationRenderingOptions &options);
+
+ static KisImportExportErrorCode convert(KisDocument *document,
+ const QString &framesDirectory,
+ const QString &savedFilesMask,
+ const QStringList &savedFiles,
+ const KisAnimationRenderingOptions &options,
+ bool batchMode);
private:
KisImageSP m_image;
diff --git a/libs/ui/animation/wdg_animationrenderer.ui b/libs/ui/animation/wdg_animationrenderer.ui
index 278cb14e97e..45322d93374 100644
--- a/libs/ui/animation/wdg_animationrenderer.ui
+++ b/libs/ui/animation/wdg_animationrenderer.ui
@@ -9,7 +9,7 @@
<property name="windowTitle">
<string>Animation Renderer Image</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_3">
+ <layout class="QVBoxLayout" name="verticalLayout_3" stretch="0,0,0,1,0">
<item>
<widget class="QGroupBox" name="grpGeneralOptions">
<property name="title">
@@ -73,9 +73,6 @@
<property name="toolTip">
<string>Select the frame export options</string>
</property>
- <property name="text">
- <string>...</string>
- </property>
</widget>
</item>
</layout>
@@ -209,15 +206,12 @@
<property name="toolTip">
<string>Select the FFmpeg render options.</string>
</property>
- <property name="text">
- <string>...</string>
- </property>
</widget>
</item>
</layout>
</item>
<item row="5" column="0">
- <widget class="QLabel" name="label_7">
+ <widget class="QLabel" name="lblVideoFilenameTitle">
<property name="text">
<string>Video location:</string>
</property>
@@ -234,7 +228,7 @@
</widget>
</item>
<item row="7" column="0">
- <widget class="QLabel" name="label_9">
+ <widget class="QLabel" name="lblFFMpegLocationTitle">
<property name="text">
<string>FFmpeg location:</string>
</property>
@@ -244,7 +238,7 @@
<widget class="KisFileNameRequester" name="ffmpegLocation" native="true"/>
</item>
<item row="8" column="0">
- <widget class="QLabel" name="label_10">
+ <widget class="QLabel" name="lblFFMpegVersionTitle">
<property name="text">
<string>FFmpeg version:</string>
</property>
@@ -273,6 +267,19 @@
</property>
</widget>
</item>
+ <item>
+ <spacer name="bottomSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
</layout>
</widget>
<customwidgets>
diff --git a/plugins/dockers/compositiondocker/compositiondocker_dock.cpp b/plugins/dockers/compositiondocker/compositiondocker_dock.cpp
index c7ceeff900a..856d68d89c6 100644
--- a/plugins/dockers/compositiondocker/compositiondocker_dock.cpp
+++ b/plugins/dockers/compositiondocker/compositiondocker_dock.cpp
@@ -45,6 +45,10 @@
#include <kis_time_span.h>
#include <KisMimeDatabase.h>
+#ifdef Q_OS_ANDROID
+#include <animation/KisAndroidMediaEncoderRunnable.h>
+#endif
+
#include "compositionmodel.h"
@@ -338,6 +342,21 @@ void CompositionDockerDock::exportAnimationClicked()
KisAnimationRenderingOptions exportOptions;
exportOptions.fromProperties(settings);
+#ifdef Q_OS_ANDROID
+ KisMediaEncoderFormat *format = KisMediaEncoderWrapper::getFormatByKey(exportOptions.videoFormatKey);
+ // If there's no format configured, try to grab the first available one.
+ if (!format) {
+ const QVector<KisMediaEncoderFormat *> &supportedFormats = KisMediaEncoderWrapper::getSupportedFormats();
+ if (!supportedFormats.isEmpty()) {
+ format = supportedFormats.first();
+ }
+ }
+ // If there's still none, bail out.
+ if (!format) {
+ return;
+ }
+#endif
+
if (m_canvas &&
m_canvas->viewManager() &&
m_canvas->viewManager()->image() &&
@@ -367,7 +386,11 @@ void CompositionDockerDock::exportAnimationClicked()
KisLayerCompositionSP currentComposition = toQShared(new KisLayerComposition(image, "temp"));
currentComposition->store();
+#ifdef Q_OS_ANDROID
+ const QString videoExtension = format->extension();
+#else
const QString videoExtension = KisMimeDatabase::suffixesForMimeType(exportOptions.videoMimeType).first();
+#endif
Q_FOREACH (KisLayerCompositionSP composition, image->compositions()) {
if(!composition->isExportEnabled())
diff --git a/plugins/dockers/recorder/recorder_export.cpp b/plugins/dockers/recorder/recorder_export.cpp
index 302f96d54fe..9210446abed 100644
--- a/plugins/dockers/recorder/recorder_export.cpp
+++ b/plugins/dockers/recorder/recorder_export.cpp
@@ -300,6 +300,7 @@ public:
settings->inputFilePaths,
KisMediaEncoderWrapper::getFormatByKey(settings->selectedFormat),
settings->formatPreferences.value(settings->selectedFormat).toMap(),
+ QString(),
settings->resize ? settings->size : settings->imageSize,
settings->inputFps,
settings->fps,