[graphics/krita] /: Add Image->Purge Unused Image Data

Dmitry Kazakov null at kde.org
Wed Jan 28 12:24:42 GMT 2026


Git commit a059548b007f99e32533c77e26f0d25650998027 by Dmitry Kazakov.
Committed on 28/01/2026 at 12:24.
Pushed by dkazakov into branch 'master'.

Add Image->Purge Unused Image Data

Under some conditions it might happen that some layers and/or
projections will contain vast areas of fully transparent data. It can
cause a lot of undesirable side-effects, like layer thumbnails
recalculating too slowly or image compositing being extremely
slow.

This patch adds a new user-faced action Image->Purge Unused
Image Data, which iterated through all existing layers and removing
all fully transparent areas.

Just a mere calling to this action can workaround the "slow thumbnail
generation" issue as reported in bug 510627. Hence calling to that also
switches all the layers back into "precise" thumbnail generation mode.

WARNING: calling this action can potentially break Undo/Redo for your
image. Please take that into account until the feature is fully tested.

CCBUG:510627
CC:kimageshop at kde.org

M  +2    -1    krita/krita5.xmlgui
M  +12   -0    krita/kritamenu.action
M  +68   -13   libs/image/kis_image.cc
M  +14   -0    libs/image/kis_transaction.h
M  +11   -0    libs/ui/kis_layer_manager.cc
M  +1    -0    libs/ui/kis_layer_manager.h
M  +2    -1    libs/ui/kis_statusbar.cc

https://invent.kde.org/graphics/krita/-/commit/a059548b007f99e32533c77e26f0d25650998027

diff --git a/krita/krita5.xmlgui b/krita/krita5.xmlgui
index 6b16e34070a..d8fcac0e1bc 100644
--- a/krita/krita5.xmlgui
+++ b/krita/krita5.xmlgui
@@ -2,7 +2,7 @@
 <kpartgui xmlns="http://www.kde.org/standards/kxmlgui/1.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 name="Krita"
-version="542"
+version="543"
 xsi:schemaLocation="http://www.kde.org/standards/kxmlgui/1.0  http://www.kde.org/standards/kxmlgui/1.0/kxmlgui.xsd">
   <MenuBar>
     <Menu name="file">
@@ -163,6 +163,7 @@ xsi:schemaLocation="http://www.kde.org/standards/kxmlgui/1.0  http://www.kde.org
       <Action name="trim_to_image"/>
       <Action name="resizeimagetolayer"/>
       <Action name="resizeimagetoselection"/>
+      <Action name="purge_unused_image_data"/>
       <Separator/>
       <Action name="imagesize" group="image_transform_merge"/>
       <Action name="imageresolution"/>
diff --git a/krita/kritamenu.action b/krita/kritamenu.action
index 0e2f271335e..7ff6ef2c5b5 100644
--- a/krita/kritamenu.action
+++ b/krita/kritamenu.action
@@ -1192,6 +1192,18 @@
       <isCheckable>false</isCheckable>
       <statusTip></statusTip>
     </Action>
+    <Action name="purge_unused_image_data">
+      <icon></icon>
+      <text>Purge Unused Image Data</text>
+      <whatsThis></whatsThis>
+      <toolTip>Removes all fully transparent pixels from the image storage</toolTip>
+      <iconText></iconText>
+      <activationFlags>1</activationFlags>
+      <activationConditions>0</activationConditions>
+      <shortcut></shortcut>
+      <isCheckable>false</isCheckable>
+      <statusTip></statusTip>
+    </Action>
     <Action name="resizeimagetolayer">
       <icon></icon>
       <text>Trim to Current &Layer</text>
diff --git a/libs/image/kis_image.cc b/libs/image/kis_image.cc
index 2125ab96599..db0caee3d81 100644
--- a/libs/image/kis_image.cc
+++ b/libs/image/kis_image.cc
@@ -875,42 +875,69 @@ void KisImage::cropImage(const QRect& newRect)
 void KisImage::purgeUnusedData(bool isCancellable)
 {
     /**
-     * WARNING: don't use this function unless you know what you are doing!
-     *
-     * It breaks undo on layers! Therefore, after calling it, KisImage is not
-     * undo-capable anymore!
+     * In cancellable mode, the action does **not** touch the paint
+     * devices of the image, only projections, because it can break
+     * undo/redo.
      */
 
     struct PurgeUnusedDataStroke : public KisRunnableBasedStrokeStrategy {
         PurgeUnusedDataStroke(KisImageSP image, bool isCancellable)
             : KisRunnableBasedStrokeStrategy(QLatin1String("purge-unused-data"),
-                                             kundo2_noi18n("purge-unused-data")),
-              m_image(image)
+                                             kundo2_i18n("Purge Unused Data")),
+              m_image(image),
+              m_finalCommand(new KUndo2Command(this->name()))
+
         {
             this->enableJob(JOB_INIT, true, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE);
             this->enableJob(JOB_DOSTROKE, true);
-            setClearsRedoOnStart(false);
+            this->enableJob(JOB_FINISH, true, KisStrokeJobData::SEQUENTIAL);
+
+            setClearsRedoOnStart(!isCancellable);
             setRequestsOtherStrokesToEnd(!isCancellable);
             setCanForgetAboutMe(isCancellable);
         }
 
         void initStrokeCallback() override
         {
-            KisPaintDeviceList deviceList;
+            KisPaintDeviceList paintDevicesList;
+            KisPaintDeviceList projectionsList;
             QVector<KisStrokeJobData*> jobsData;
 
             KisLayerUtils::recursiveApplyNodes(m_image->root(),
-                [&deviceList](KisNodeSP node) {
-                   deviceList << node->getLodCapableDevices();
+                [&paintDevicesList, &projectionsList, this](KisNodeSP node) {
+                    KisPaintDeviceList deviceList = node->getLodCapableDevices();
+
+                    Q_FOREACH (KisPaintDeviceSP dev, deviceList) {
+                        if (!dev) continue;
+
+                        // we do **not** strip paint devices in the forgettable
+                        // mode, since we should handle transactions for them
+                        if (dev == node->paintDevice() && !canForgetAboutMe()) {
+                            paintDevicesList << dev;
+                        } else {
+                            projectionsList << dev;
+                        }
+                    }
                  });
 
             /// make sure we deduplicate the list to avoid
             /// concurrent write access to the devices
-            KritaUtils::makeContainerUnique(deviceList);
+            KritaUtils::makeContainerUnique(paintDevicesList);
+            KritaUtils::makeContainerUnique(projectionsList);
+
+            Q_FOREACH(KisPaintDeviceSP dev, paintDevicesList) {
+                projectionsList.removeAll(dev);
 
-            Q_FOREACH (KisPaintDeviceSP device, deviceList) {
-                if (!device) continue;
+                // all transactions will be linked to the final command via the
+                // parent-child relationship
+                m_transactions.emplace_back(dev, m_finalCommand.data(), -1, nullptr, KisTransaction::None);
+            }
 
+            // now, when the transactions are started, we can merge the two lists
+            paintDevicesList << projectionsList;
+            projectionsList.clear();
+
+            Q_FOREACH (KisPaintDeviceSP device, paintDevicesList) {
                 KritaUtils::addJobConcurrent(jobsData,
                     [device] () {
                         const_cast<KisPaintDevice*>(device.data())->purgeDefaultPixels();
@@ -920,8 +947,36 @@ void KisImage::purgeUnusedData(bool isCancellable)
             addMutatedJobs(jobsData);
         }
 
+        void finishStrokeCallback() override {
+            for (auto it = m_transactions.begin(); it != m_transactions.end(); ++it) {
+                QScopedPointer<KUndo2Command> cmd(it->endAndTake());
+
+                // verify the transaction command is linked to m_finalCommand,
+                // if not, just delete on return
+                KIS_SAFE_ASSERT_RECOVER(cmd->hasParent()) { continue; }
+
+                // if has a parent, release...
+                (void)cmd.take();
+            }
+
+            m_transactions.clear();
+
+            m_finalCommand->redo();
+            m_image->postExecutionUndoAdapter()->addCommand(toQShared(m_finalCommand.take()));
+
+            // now reset the thumbnail generation limitation
+            KisLayerUtils::recursiveApplyNodes(m_image->root(),
+                [](KisNodeSP node) {
+                    if (node->preferredThumbnailBoundsMode() != KisThumbnailBoundsMode::Precise) {
+                        node->setPreferredThumbnailBoundsMode(KisThumbnailBoundsMode::Precise);
+                    }
+                });
+        }
+
     private:
         KisImageSP m_image;
+        QScopedPointer<KUndo2Command> m_finalCommand;
+        std::vector<KisTransaction> m_transactions;
     };
 
     KisStrokeId id = startStroke(new PurgeUnusedDataStroke(this, isCancellable));
diff --git a/libs/image/kis_transaction.h b/libs/image/kis_transaction.h
index 8d67116a054..e08385fe936 100644
--- a/libs/image/kis_transaction.h
+++ b/libs/image/kis_transaction.h
@@ -41,6 +41,20 @@ public:
     {
     }
 
+    KisTransaction(KisTransaction &&rhs)
+        : m_transactionData(rhs.m_transactionData)
+    {
+        rhs.m_transactionData = nullptr;
+    }
+
+    KisTransaction& operator=(KisTransaction &&rhs)
+    {
+        delete m_transactionData;
+        m_transactionData = rhs.m_transactionData;
+        rhs.m_transactionData = nullptr;
+        return *this;
+    }
+
     virtual ~KisTransaction() {
         delete m_transactionData;
     }
diff --git a/libs/ui/kis_layer_manager.cc b/libs/ui/kis_layer_manager.cc
index c02a8d4818c..c8d1101a564 100644
--- a/libs/ui/kis_layer_manager.cc
+++ b/libs/ui/kis_layer_manager.cc
@@ -169,6 +169,9 @@ void KisLayerManager::setup(KisActionManager* actionManager)
     KisAction *action = actionManager->createAction("trim_to_image");
     connect(action, SIGNAL(triggered()), this, SLOT(trimToImage()));
 
+    action = actionManager->createAction("purge_unused_image_data");
+    connect(action, SIGNAL(triggered()), this, SLOT(purgeUnusedImageData()));
+
     m_layerStyle  = actionManager->createAction("layer_style");
     connect(m_layerStyle, SIGNAL(triggered()), this, SLOT(layerStyle()));
 
@@ -228,6 +231,14 @@ void KisLayerManager::trimToImage()
     }
 }
 
+void KisLayerManager::purgeUnusedImageData()
+{
+    KisImageWSP image = m_view->image();
+    if (image) {
+        image->purgeUnusedData(false);
+    }
+}
+
 void KisLayerManager::layerProperties()
 {
     if (!m_view) return;
diff --git a/libs/ui/kis_layer_manager.h b/libs/ui/kis_layer_manager.h
index 6d7e4edff57..b4574a596fe 100644
--- a/libs/ui/kis_layer_manager.h
+++ b/libs/ui/kis_layer_manager.h
@@ -62,6 +62,7 @@ private Q_SLOTS:
 
     void imageResizeToActiveLayer();
     void trimToImage();
+    void purgeUnusedImageData();
 
     void layerProperties();
     void layerPropertiesDialogClosed();
diff --git a/libs/ui/kis_statusbar.cc b/libs/ui/kis_statusbar.cc
index e35509cce42..bec99a862fe 100644
--- a/libs/ui/kis_statusbar.cc
+++ b/libs/ui/kis_statusbar.cc
@@ -337,7 +337,8 @@ void KisStatusBar::updateMemoryStatus()
                 i18nc("tooltip on statusbar memory reporting button",
                       "\n\nWARNING:\tSome layers took too much time to calculate\n"
                       "\t\ttheir thumbnails. They were switched into low-quality\n"
-                      "\t\tthumbnails mode.\n"
+                      "\t\tthumbnails mode. Try purging unused image data with\n"
+                      "\t\tImage->Purge Unused Image Data action.\n"
                     "Slow layers: ");
 
             bool needsSeparator = false;


More information about the kimageshop mailing list