[graphics/krita/krita/6.0] libs/ui/animation: [android] Don't allow combined image/video export
Carsten Hartenfels <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 3dbc1e37566e8b1e4f39cf6b0f46931d3a102361 by Carsten Hartenfels.
Committed on 27/07/2026 at 17:19.
Pushed by hartenfels into branch 'krita/6.0'.
[android] Don't allow combined image/video export
For animations, since it doesn't work well. For one, it makes the
animation export dialog way too tall, causing it to spill out of the
screen on some devices. Writing all those frames to external storage is
also incredibly slow on some devices, writing them to an internal
temporary directory is several orders of magnitude quicker, negating any
advantages that could be gained from exporting the frames only once.
M +19 -10 libs/ui/animation/KisAnimationRender.cpp
M +12 -0 libs/ui/animation/KisAnimationRenderingOptions.cpp
M +9 -0 libs/ui/animation/KisAnimationRenderingOptions.h
M +97 -18 libs/ui/animation/KisDlgAnimationRenderer.cpp
M +6 -0 libs/ui/animation/KisDlgAnimationRenderer.h
M +87 -2 libs/ui/animation/wdg_animationrenderer.ui
https://invent.kde.org/graphics/krita/-/commit/3dbc1e37566e8b1e4f39cf6b0f46931d3a102361
diff --git a/libs/ui/animation/KisAnimationRender.cpp b/libs/ui/animation/KisAnimationRender.cpp
index b820646ee80..ad18b96825c 100644
--- a/libs/ui/animation/KisAnimationRender.cpp
+++ b/libs/ui/animation/KisAnimationRender.cpp
@@ -53,24 +53,28 @@ bool looksLikeMatroska(const QString &videoType)
} // namespace
bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, KisAnimationRenderingOptions encoderOptions) {
+ bool isTemporaryFramesDirectory = false;
+ QString framesDirectory;
#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()) {
+ // The user may cancel the dialog prompting them for a video file or a frames
+ // directory and we can't implicitly create them next to the document like on
+ // desktop due to file system restrictions. So if we don't get those paths
+ // here, we just bail out. The user knows they pressed cancel on the file
+ // dialog, so no message dialog is necessary.
+ if (encoderOptions.shouldEncodeVideo) {
+ if (encoderOptions.videoFileName.isEmpty()) {
+ return false;
+ }
+ } else if (encoderOptions.directory.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()) {
+ if (encoderOptions.shouldEncodeVideo) {
tempDir = std::make_unique<QTemporaryDir>();
KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(tempDir->isValid(), false);
framesDirectory = tempDir->path();
@@ -203,7 +207,12 @@ bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, K
if (!isTemporaryFramesDirectory) {
QDir d(framesDirectory);
- if (encoderOptions.shouldDeleteSequence || !delayReturnSuccess) {
+#ifdef Q_OS_ANDROID
+ bool shouldDeleteSequence = false;
+#else
+ bool shouldDeleteSequence = encoderOptions.shouldDeleteSequence;
+#endif
+ if (shouldDeleteSequence || !delayReturnSuccess) {
QStringList savedFiles = exporter.savedFiles();
Q_FOREACH(const QString &f, savedFiles) {
diff --git a/libs/ui/animation/KisAnimationRenderingOptions.cpp b/libs/ui/animation/KisAnimationRenderingOptions.cpp
index d02a5a3407d..af441929b8e 100644
--- a/libs/ui/animation/KisAnimationRenderingOptions.cpp
+++ b/libs/ui/animation/KisAnimationRenderingOptions.cpp
@@ -58,6 +58,13 @@ QString KisAnimationRenderingOptions::resolveAbsoluteFramesDirectory() const
KisAnimationRenderingOptions::RenderMode KisAnimationRenderingOptions::renderMode() const
{
+#ifdef Q_OS_ANDROID
+ if (shouldEncodeVideo) {
+ return RENDER_VIDEO_ONLY;
+ } else {
+ return RENDER_FRAMES_ONLY;
+ }
+#else
if (shouldDeleteSequence) {
KIS_SAFE_ASSERT_RECOVER_NOOP(shouldEncodeVideo);
return RENDER_VIDEO_ONLY;
@@ -67,6 +74,7 @@ KisAnimationRenderingOptions::RenderMode KisAnimationRenderingOptions::renderMod
} else {
return RENDER_FRAMES_AND_VIDEO;
}
+#endif
}
KisPropertiesConfigurationSP KisAnimationRenderingOptions::toProperties() const
@@ -82,7 +90,9 @@ KisPropertiesConfigurationSP KisAnimationRenderingOptions::toProperties() const
config->setProperty("frame_mimetype", frameMimeType);
config->setProperty("encode_video", shouldEncodeVideo);
+#ifndef Q_OS_ANDROID
config->setProperty("delete_sequence", shouldDeleteSequence);
+#endif
config->setProperty("only_unique_frames", wantsOnlyUniqueFrameSequence);
config->setProperty("framerate", frameRate);
@@ -116,7 +126,9 @@ void KisAnimationRenderingOptions::fromProperties(KisPropertiesConfigurationSP c
frameMimeType = config->getPropertyLazy("frame_mimetype", frameMimeType);
shouldEncodeVideo = config->getPropertyLazy("encode_video", false);
+#ifndef Q_OS_ANDROID
shouldDeleteSequence = config->getPropertyLazy("delete_sequence", false);
+#endif
wantsOnlyUniqueFrameSequence = config->getPropertyLazy("only_unique_frames", false);
frameRate = config->getPropertyLazy("framerate", 25);
diff --git a/libs/ui/animation/KisAnimationRenderingOptions.h b/libs/ui/animation/KisAnimationRenderingOptions.h
index 0a247a4320e..22cc788d1c6 100644
--- a/libs/ui/animation/KisAnimationRenderingOptions.h
+++ b/libs/ui/animation/KisAnimationRenderingOptions.h
@@ -32,8 +32,15 @@ public:
int lastFrame = 0;
int sequenceStart = 0;
+ // On Android, we only allow either frame or video export, not both. Using
+ // a temporary directory instead of scribbling around in external storage is
+ // just orders of magnitude faster, doesn't spam the user's recent files
+ // with pointless image frames and also makes the export dialog not have to
+ // be so screen-escapingly tall. Hence we only have one boolean there.
bool shouldEncodeVideo = false;
+#ifndef Q_OS_ANDROID
bool shouldDeleteSequence = false;
+#endif
bool includeAudio = false;
bool wantsOnlyUniqueFrameSequence = false;
@@ -64,7 +71,9 @@ public:
enum RenderMode {
RENDER_FRAMES_ONLY,
RENDER_VIDEO_ONLY,
+#ifndef Q_OS_ANDROID
RENDER_FRAMES_AND_VIDEO
+#endif
};
RenderMode renderMode() const;
diff --git a/libs/ui/animation/KisDlgAnimationRenderer.cpp b/libs/ui/animation/KisDlgAnimationRenderer.cpp
index c0b7ee126d5..49949301273 100644
--- a/libs/ui/animation/KisDlgAnimationRenderer.cpp
+++ b/libs/ui/animation/KisDlgAnimationRenderer.cpp
@@ -43,6 +43,7 @@
#include "kis_image_config.h"
#ifdef Q_OS_ANDROID
+#include <QButtonGroup>
#include <QJsonDocument>
#include "animation/KisMediaEncoderFormatPreferencesDialog.h"
#include "animation/KisMediaEncoderWrapper.h"
@@ -76,13 +77,26 @@ KisDlgAnimationRenderer::KisDlgAnimationRenderer(KisDocument *doc, QWidget *pare
m_page->lblWarnings->setPixmap(KisIconUtils::loadIcon(QStringLiteral("dialog-warning")).pixmap(32, 32));
#ifdef Q_OS_ANDROID
+ m_exportButtonGroup = new QButtonGroup(this);
+ m_exportButtonGroup->addButton(m_page->bnExportImages);
+ m_exportButtonGroup->addButton(m_page->bnExportVideo);
+ m_page->shouldExportOnlyImageSequence->setChecked(false);
+ m_page->shouldExportOnlyVideo->setChecked(false);
+ m_page->shouldExportOnlyImageSequence->setCheckable(false);
+ m_page->shouldExportOnlyVideo->setCheckable(false);
+ m_page->pgImages->layout()->addWidget(m_page->shouldExportOnlyImageSequence);
+ m_page->pgVideo->layout()->addWidget(m_page->shouldExportOnlyVideo);
m_page->lblVideoFilenameTitle->hide();
m_page->videoFilename->hide();
- m_page->dirRequester->setReadOnlyText(true);
+ m_page->lblDirRequester->hide();
+ m_page->dirRequester->hide();
m_page->lblFFMpegLocationTitle->hide();
m_page->ffmpegLocation->hide();
m_page->lblFFMpegVersionTitle->hide();
m_page->lblFFMpegVersion->hide();
+#else
+ m_page->wdgExportButtons->hide();
+ m_page->stkExport->hide();
#endif
m_page->dirRequester->setMode(KoFileDialog::OpenDirectory);
@@ -145,8 +159,15 @@ KisDlgAnimationRenderer::KisDlgAnimationRenderer(KisDocument *doc, QWidget *pare
connect(m_page->bnExportOptions, SIGNAL(clicked()), this, SLOT(sequenceMimeTypeOptionsClicked()));
connect(m_page->bnRenderOptions, SIGNAL(clicked()), this, SLOT(selectRenderOptions()));
+#ifdef Q_OS_ANDROID
+ connect(m_exportButtonGroup,
+ QOverload<QAbstractButton *>::of(&QButtonGroup::buttonClicked),
+ this,
+ &KisDlgAnimationRenderer::slotExportTypeChanged);
+#else
connect(m_page->shouldExportOnlyImageSequence, SIGNAL(toggled(bool)), this, SLOT(slotExportTypeChanged()));
connect(m_page->shouldExportOnlyVideo, SIGNAL(toggled(bool)), this, SLOT(slotExportTypeChanged()));
+#endif
connect(m_page->cmbRenderType, SIGNAL(currentIndexChanged(int)), SLOT(slotRenderTypeChanged()));
connect(m_page->intFramesPerSecond, SIGNAL(valueChanged(int)), SLOT(slotCheckWarnings()));
@@ -290,8 +311,16 @@ void KisDlgAnimationRenderer::initializeRenderSettings(const KisDocument &doc, c
}
m_page->chkOnlyUniqueFrames->setChecked(lastUsedOptions.wantsOnlyUniqueFrameSequence);
+#ifdef Q_OS_ANDROID
+ if (lastUsedOptions.shouldEncodeVideo) {
+ m_page->bnExportVideo->setChecked(true);
+ } else {
+ m_page->bnExportImages->setChecked(true);
+ }
+#else
m_page->shouldExportOnlyVideo->setChecked(lastUsedOptions.shouldEncodeVideo);
m_page->shouldExportOnlyImageSequence->setChecked(!lastUsedOptions.shouldDeleteSequence);
+#endif
slotExportTypeChanged();
@@ -344,6 +373,24 @@ void KisDlgAnimationRenderer::initializeRenderSettings(const KisDocument &doc, c
slotRenderTypeChanged();
}
+bool KisDlgAnimationRenderer::wantImageSequenceExport() const
+{
+#ifdef Q_OS_ANDROID
+ return !wantVideoExport();
+#else
+ return m_page->shouldExportOnlyImageSequence->isChecked();
+#endif
+}
+
+bool KisDlgAnimationRenderer::wantVideoExport() const
+{
+#ifdef Q_OS_ANDROID
+ return m_page->bnExportVideo->isChecked();
+#else
+ return m_page->shouldExportOnlyVideo->isChecked();
+#endif
+}
+
#ifndef Q_OS_ANDROID
void KisDlgAnimationRenderer::getDefaultVideoEncoderOptions(const QString &mimeType,
KisPropertiesConfigurationSP cfg,
@@ -609,7 +656,7 @@ void KisDlgAnimationRenderer::updateWarnings()
QStringList warnings;
bool exportMayFail = false;
- if (m_page->shouldExportOnlyVideo->isChecked()) {
+ if (wantVideoExport()) {
QString videoType = m_page->cmbRenderType->itemData(m_page->cmbRenderType->currentIndex()).toString();
bool gif = looksLikeGif(videoType);
@@ -825,34 +872,45 @@ KisAnimationRenderingOptions KisDlgAnimationRenderer::getEncoderOptions() const
options.lastDocumentPath = m_doc->localFilePath();
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));
+ bool video = wantVideoExport();
+ options.shouldEncodeVideo = video;
+ options.includeAudio = video && supportsAudio(videoType) && m_page->chkIncludeAudio->isChecked();
+ options.wantsOnlyUniqueFrameSequence = !video && m_page->chkOnlyUniqueFrames->isChecked();
+
+ if (video) {
+ options.frameMimeType = QStringLiteral("image/png");
+ options.videoFileName = m_videoFileName;
+ options.videoFormatKey = videoType;
+ QVariantMap videoFormatPreferences = m_videoFormatPreferences.value(videoType).toMap();
+ if (!videoFormatPreferences.isEmpty()) {
+ options.videoFormatPreferencesJson =
+ QString::fromUtf8(QJsonDocument::fromVariant(videoFormatPreferences).toJson(QJsonDocument::Compact));
+ }
+ } else {
+ options.directory = m_imageDirectory;
+ options.frameMimeType = m_page->cmbMimetype->currentData().toString();
}
- 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.directory = m_page->dirRequester->fileName();
+ options.shouldEncodeVideo = wantVideoExport();
+ options.shouldDeleteSequence = !wantImageSequenceExport();
+ options.includeAudio = supportsAudio(videoType) && m_page->chkIncludeAudio->isChecked();
+ options.wantsOnlyUniqueFrameSequence = m_page->chkOnlyUniqueFrames->isChecked();
options.frameMimeType = m_page->cmbMimetype->currentData().toString();
+#endif
options.scaleFilter = m_page->cmbScaleFilter->currentData().toString();
options.basename = m_page->txtBasename->text();
- options.directory = m_page->dirRequester->fileName();
options.firstFrame = m_page->intStart->value();
options.lastFrame = m_page->intEnd->value();
options.sequenceStart = m_page->sequenceStart->value();
- options.shouldEncodeVideo = m_page->shouldExportOnlyVideo->isChecked();
- options.shouldDeleteSequence = !m_page->shouldExportOnlyImageSequence->isChecked();
- options.includeAudio = supportsAudio(videoType) && m_page->chkIncludeAudio->isChecked();
- options.wantsOnlyUniqueFrameSequence = m_page->chkOnlyUniqueFrames->isChecked();
-
options.frameRate = m_page->intFramesPerSecond->value();
if (options.frameRate > 50 && looksLikeGif(videoType)) {
options.frameRate = 50;
@@ -933,7 +991,10 @@ void KisDlgAnimationRenderer::slotButtonClicked(int button)
void KisDlgAnimationRenderer::slotDialogAccepted()
{
#ifdef Q_OS_ANDROID
- if (m_page->shouldExportOnlyVideo) {
+ m_imageDirectory.clear();
+ m_videoFileName.clear();
+
+ if (wantVideoExport()) {
KisMediaEncoderFormat *format = KisMediaEncoderWrapper::getFormatByKey(m_page->cmbRenderType->currentData().toString());
KIS_SAFE_ASSERT_RECOVER_RETURN(format);
KoFileDialog dialog(this, KoFileDialog::SaveFile, QStringLiteral("ExportAnimation"));
@@ -943,8 +1004,13 @@ void KisDlgAnimationRenderer::slotDialogAccepted()
if (m_videoFileName.isEmpty()) {
return;
}
+
} else {
- m_videoFileName.clear();
+ KoFileDialog dialog(this, KoFileDialog::OpenDirectory, QStringLiteral("ExportAnimation"));
+ m_imageDirectory = dialog.filename();
+ if (m_imageDirectory.isEmpty()) {
+ return;
+ }
}
#endif
@@ -959,6 +1025,16 @@ void KisDlgAnimationRenderer::slotDialogAccepted()
void KisDlgAnimationRenderer::slotExportTypeChanged()
{
+ setUpdatesEnabled(false);
+
+#ifdef Q_OS_ANDROID
+ if (wantVideoExport()) {
+ m_page->stkExport->setCurrentWidget(m_page->pgVideo);
+ } else {
+ m_page->lblWarnings->hide();
+ m_page->stkExport->setCurrentWidget(m_page->pgImages);
+ }
+#else
// 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
@@ -975,8 +1051,11 @@ void KisDlgAnimationRenderer::slotExportTypeChanged()
KisSignalsBlocker b(m_page->shouldExportOnlyImageSequence);
m_page->shouldExportOnlyImageSequence->setChecked(true);
}
+#endif
- slotCheckWarnings();
+ updateWarnings();
+ m_page->adjustSize();
+ setUpdatesEnabled(true);
}
void KisDlgAnimationRenderer::slotRenderTypeChanged()
diff --git a/libs/ui/animation/KisDlgAnimationRenderer.h b/libs/ui/animation/KisDlgAnimationRenderer.h
index a0fe9c00b5b..eb2208d1076 100644
--- a/libs/ui/animation/KisDlgAnimationRenderer.h
+++ b/libs/ui/animation/KisDlgAnimationRenderer.h
@@ -22,6 +22,7 @@
class KisDocument;
class KisImportExportFilter;
class KisConfigWidget;
+class QButtonGroup;
class QHBoxLayout;
class KisAnimationVideoSaver;
class KisAnimationRenderingOptions;
@@ -91,6 +92,9 @@ private:
void initializeRenderSettings(const KisDocument &doc, const KisAnimationRenderingOptions &lastUsedOptions);
+ bool wantImageSequenceExport() const;
+ bool wantVideoExport() const;
+
void updateWarnings();
#ifndef Q_OS_ANDROID
@@ -129,6 +133,8 @@ private:
KisDocument *m_doc;
#ifdef Q_OS_ANDROID
+ QButtonGroup *m_exportButtonGroup;
+ QString m_imageDirectory;
QString m_videoFileName;
QVariantMap m_videoFormatPreferences;
#else
diff --git a/libs/ui/animation/wdg_animationrenderer.ui b/libs/ui/animation/wdg_animationrenderer.ui
index 45322d93374..b1dbcc09f2f 100644
--- a/libs/ui/animation/wdg_animationrenderer.ui
+++ b/libs/ui/animation/wdg_animationrenderer.ui
@@ -6,10 +6,18 @@
</author>
<class>WdgAnimationRenderer</class>
<widget class="QWidget" name="WdgAnimationRenderer">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>299</width>
+ <height>694</height>
+ </rect>
+ </property>
<property name="windowTitle">
<string>Animation Renderer Image</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_3" stretch="0,0,0,1,0">
+ <layout class="QVBoxLayout" name="verticalLayout_3" stretch="0,0,0,0,0,1,0">
<item>
<widget class="QGroupBox" name="grpGeneralOptions">
<property name="title">
@@ -43,6 +51,83 @@
</layout>
</widget>
</item>
+ <item>
+ <widget class="QWidget" name="wdgExportButtons" native="true">
+ <layout class="QHBoxLayout" name="lytExportButtons">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="bnExportImages">
+ <property name="text">
+ <string>Images</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="bnExportVideo">
+ <property name="text">
+ <string>Video</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QStackedWidget" name="stkExport">
+ <widget class="QWidget" name="pgImages">
+ <layout class="QVBoxLayout" name="lytPgImages">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="pgVideo">
+ <layout class="QVBoxLayout" name="lytPgVideo">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ </layout>
+ </widget>
+ </widget>
+ </item>
<item>
<widget class="QGroupBox" name="shouldExportOnlyImageSequence">
<property name="title">
@@ -78,7 +163,7 @@
</layout>
</item>
<item row="1" column="0">
- <widget class="QLabel" name="label_2">
+ <widget class="QLabel" name="lblDirRequester">
<property name="text">
<string>Image location:</string>
</property>