[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>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.