[network/neochat] src: Optimize images by default before sending them

Joshua Goins <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 1da94250097f2152f71e02974307daa1a9de566e by Joshua Goins.
Committed on 30/07/2026 at 00:05.
Pushed by redstrate into branch 'master'.

Optimize images by default before sending them

By default, we send the images in their source quality. While this might
work for some users, a common thing I see in the KDE rooms is:

Developer takes a screenshot of their desktop or a window, and if they
happen to have a high resolution (and also take screenshots in PNG) the
file is easily multiple megabytes. This file is then plopped right into
NeoChat which sends it as-is, even if all the quality isn't really
needed. Users and servers take *ages* to download the image they'll look
at with a tiny portion of their screen and the image's original
resolution.

So now by default, NeoChat will optimize images to save everyone's
bandwidth and server's storage space. Currently, this means converting
all images to JPEG and limiting them to a resolution of 3000x3000.
Optimization can be turned off if not desired, such as transporting
transparent or source quality PNGs to somebody.

In a simple test, a normal screenshot of my desktop is 2.3 MiB and at a
ridiculous 4K resolution. Our optimization now brings that down to ~0.23
MiB - a 10x size reduction!

M  +14   -1    src/libneochat/block.cpp
M  +9    -0    src/libneochat/block.h
M  +3    -1    src/libneochat/blockcache.cpp
M  +3    -1    src/libneochat/blockcache.h
M  +39   -2    src/libneochat/chatbarcache.cpp
M  +1    -1    src/libneochat/eventhandler.cpp
M  +1    -1    src/libneochat/filepreview.cpp
M  +16   -1    src/messagecontent/ImageComponent.qml
M  +16   -1    src/messagecontent/models/chatbarmessagecontentmodel.cpp
M  +1    -0    src/messagecontent/models/chatbarmessagecontentmodel.h

https://invent.kde.org/network/neochat/-/commit/1da94250097f2152f71e02974307daa1a9de566e

diff --git a/src/libneochat/block.cpp b/src/libneochat/block.cpp
index 931e7a28e..c758d8bd1 100644
--- a/src/libneochat/block.cpp
+++ b/src/libneochat/block.cpp
@@ -251,12 +251,14 @@ ImageBlock::ImageBlock(Type type,
                        const ImageInfo &info,
                        const QUrl &thumbnailSource,
                        const ImageInfo &thumbnailInfo,
+                       const bool optimize,
                        QObject *parent)
     : UrlBlock(type, source, parent)
     , m_filename(filename)
     , m_info(info)
     , m_thumbnailSource(thumbnailSource)
     , m_thumbnailInfo(thumbnailInfo)
+    , m_optimize(optimize)
 {
 }
 
@@ -266,6 +268,7 @@ ImageBlock::ImageBlock(ImageCacheItem *item, QObject *parent)
     , m_info(item->info)
     , m_thumbnailSource(item->thumbnailSource)
     , m_thumbnailInfo(item->thumbnailInfo)
+    , m_optimize(item->optimize)
 {
 }
 
@@ -289,9 +292,19 @@ const ImageInfo &ImageBlock::thumbnailInfo() const
     return m_thumbnailInfo;
 }
 
+bool ImageBlock::optimize() const
+{
+    return m_optimize;
+}
+
+void ImageBlock::setOptimize(const bool optimize)
+{
+    m_optimize = optimize;
+}
+
 CacheItemPtr ImageBlock::toCacheItem() const
 {
-    return std::make_unique<ImageCacheItem>(type(), source(), filename(), info(), thumbnailSource(), thumbnailInfo());
+    return std::make_unique<ImageCacheItem>(type(), source(), filename(), info(), thumbnailSource(), thumbnailInfo(), optimize());
 }
 
 VideoBlock::VideoBlock(Type type,
diff --git a/src/libneochat/block.h b/src/libneochat/block.h
index ee131ce29..5e79202b1 100644
--- a/src/libneochat/block.h
+++ b/src/libneochat/block.h
@@ -289,6 +289,11 @@ class ImageBlock : public UrlBlock
      */
     Q_PROPERTY(ImageInfo thumbnailInfo READ thumbnailInfo CONSTANT)
 
+    /**
+     * @brief Whether the image should be optimized before uploading.
+     */
+    Q_PROPERTY(bool optimize READ optimize CONSTANT)
+
 public:
     ImageBlock(Type type,
                const QUrl &source,
@@ -296,6 +301,7 @@ public:
                const ImageInfo &info,
                const QUrl &thumbnailSource,
                const ImageInfo &thumbnailInfo,
+               bool optimize,
                QObject *parent);
     ImageBlock(ImageCacheItem *item, QObject *parent);
 
@@ -303,6 +309,8 @@ public:
     const ImageInfo &info() const;
     QUrl thumbnailSource() const;
     const ImageInfo &thumbnailInfo() const;
+    bool optimize() const;
+    void setOptimize(bool optimize);
 
     [[nodiscard]] CacheItemPtr toCacheItem() const override;
 
@@ -311,6 +319,7 @@ private:
     ImageInfo m_info;
     QUrl m_thumbnailSource;
     ImageInfo m_thumbnailInfo;
+    bool m_optimize;
 };
 
 /**
diff --git a/src/libneochat/blockcache.cpp b/src/libneochat/blockcache.cpp
index 1b578c772..5e552a4de 100644
--- a/src/libneochat/blockcache.cpp
+++ b/src/libneochat/blockcache.cpp
@@ -165,12 +165,14 @@ ImageCacheItem::ImageCacheItem(Type type,
                                const QString &filename,
                                const ImageInfo &info,
                                const QUrl &thumbnailSource,
-                               const ImageInfo &thumbnailInfo)
+                               const ImageInfo &thumbnailInfo,
+                               const bool optimize)
     : UrlCacheItem(type, source)
     , filename(filename)
     , info(info)
     , thumbnailSource(thumbnailSource)
     , thumbnailInfo(thumbnailInfo)
+    , optimize(optimize)
 {
 }
 
diff --git a/src/libneochat/blockcache.h b/src/libneochat/blockcache.h
index a61b61c16..fe54e1769 100644
--- a/src/libneochat/blockcache.h
+++ b/src/libneochat/blockcache.h
@@ -133,12 +133,14 @@ public:
                    const QString &filename,
                    const ImageInfo &info,
                    const QUrl &thumbnailSource = {},
-                   const ImageInfo &thumbnailInfo = {});
+                   const ImageInfo &thumbnailInfo = {},
+                   bool optimize = true);
 
     QString filename;
     ImageInfo info;
     QUrl thumbnailSource;
     ImageInfo thumbnailInfo;
+    bool optimize;
 };
 
 /**
diff --git a/src/libneochat/chatbarcache.cpp b/src/libneochat/chatbarcache.cpp
index 1908863ef..a8f8231f9 100644
--- a/src/libneochat/chatbarcache.cpp
+++ b/src/libneochat/chatbarcache.cpp
@@ -21,6 +21,8 @@
 
 #include "chatbarlogging.h"
 
+#include <QStandardPaths>
+
 using namespace Qt::StringLiterals;
 
 ChatBarCache::ChatBarCache(NeoChatRoom *room)
@@ -174,8 +176,43 @@ void ChatBarCache::postMessage(const QString &threadRootId)
     }
 
     if (Blocks::isFileType(m_cache.at(0)->type)) {
-        const auto fileCacheItem = dynamic_cast<const Blocks::UrlCacheItem *>(m_cache.at(0));
-        m_room->uploadFile(fileCacheItem->source, m_cache.toString(), relatesTo);
+        QUrl source;
+        QString filename;
+        if (const auto imageItem = dynamic_cast<const Blocks::ImageCacheItem *>(m_cache.at(0)); imageItem != nullptr) {
+            if (imageItem->optimize) {
+                QImage image(imageItem->source.toLocalFile());
+
+                // Maximum resolution we want to send in standard quality.
+                constexpr QSize maximumResolution(3000, 3000);
+                // The file format we want standard quality images to be in.
+                const auto fileExtension = QStringLiteral("jpg");
+
+                if (image.size().width() > maximumResolution.width() || image.size().height() > maximumResolution.height()) {
+                    image = image.scaled(maximumResolution, Qt::AspectRatioMode::KeepAspectRatio);
+                }
+
+                QString imageDir(u"%1/optimized"_s.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)));
+                if (!QDir().exists(imageDir)) {
+                    QDir().mkdir(imageDir);
+                }
+                filename = u"%1.%3"_s.arg(QDateTime::currentDateTime().toString(u"yyyy-MM-dd-hh-mm-ss"_s), fileExtension);
+                source = QUrl(u"file://%1/%2"_s.arg(imageDir, filename));
+                if (!image.save(source.toLocalFile())) {
+                    qCWarning(ChatBar) << "Failed to save optimized image to" << source << "falling back to the actual source file";
+                    source = imageItem->source;
+                    filename = imageItem->filename;
+                }
+            } else {
+                source = imageItem->source;
+                filename = imageItem->toString();
+            }
+        } else {
+            const auto fileCacheItem = dynamic_cast<const Blocks::UrlCacheItem *>(m_cache.at(0));
+            source = fileCacheItem->source;
+            filename = fileCacheItem->toString();
+        }
+
+        m_room->uploadFile(source, filename, relatesTo);
         clearCache();
         return;
     }
diff --git a/src/libneochat/eventhandler.cpp b/src/libneochat/eventhandler.cpp
index 3fb51e61b..5b42431a8 100644
--- a/src/libneochat/eventhandler.cpp
+++ b/src/libneochat/eventhandler.cpp
@@ -905,7 +905,7 @@ Blocks::Block *EventHandler::fileBlockFromFileContent(QObject *parent,
             }
         }
         const auto thumbnailInfo = getTumbnailInfo(imageContent->thumbnail);
-        return new Blocks::ImageBlock(Blocks::Image, source, filename, imageInfo, thumbnailSource, thumbnailInfo, parent);
+        return new Blocks::ImageBlock(Blocks::Image, source, filename, imageInfo, thumbnailSource, thumbnailInfo, true, parent);
     }
     if (mimeType.name().contains(u"video"_s)) {
         const auto videoContent = dynamic_cast<const EventContent::VideoContent *>(fileContent);
diff --git a/src/libneochat/filepreview.cpp b/src/libneochat/filepreview.cpp
index c10a4b569..861f277da 100644
--- a/src/libneochat/filepreview.cpp
+++ b/src/libneochat/filepreview.cpp
@@ -82,7 +82,7 @@ void FilePreviewBlockLoader::blockForFile()
         QImageReader reader(m_source.path());
         Blocks::ImageInfo info;
         info.pixelSize = reader.size();
-        m_previewBlock = new Blocks::ImageBlock(Blocks::Pdf, m_source, m_source.fileName(), info, QUrl(), Blocks::ImageInfo(), parent());
+        m_previewBlock = new Blocks::ImageBlock(Blocks::Pdf, m_source, m_source.fileName(), info, QUrl(), Blocks::ImageInfo(), true, parent());
         m_state = Available;
         Q_EMIT blockAvailable();
         return;
diff --git a/src/messagecontent/ImageComponent.qml b/src/messagecontent/ImageComponent.qml
index 4104f7639..44fb46bd0 100644
--- a/src/messagecontent/ImageComponent.qml
+++ b/src/messagecontent/ImageComponent.qml
@@ -76,13 +76,28 @@ Item {
                 icon.name: "view-hidden"
                 text: i18nc("@action:button", "Hide Image")
                 display: QQC2.Button.IconOnly
-                z: 10
                 onClicked: Message.contentModel?.hideMedia()
 
                 QQC2.ToolTip.text: text
                 QQC2.ToolTip.visible: hovered
                 QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
             }
+            QQC2.Button {
+                id: qualityButton
+
+                visible: root.editable
+                icon.name: "quickwizard-symbolic"
+                text:  i18nc("@action:button", "Optimize Image")
+                display: QQC2.Button.IconOnly
+                checkable: true
+                checked: root.block.optimize
+
+                onCheckedChanged: root.Message.contentModel?.setImageOptimization(checked)
+
+                QQC2.ToolTip.text: text
+                QQC2.ToolTip.visible: hovered
+                QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
+            }
             QQC2.Button {
                 id: editImageButton
                 visible: root.editable
diff --git a/src/messagecontent/models/chatbarmessagecontentmodel.cpp b/src/messagecontent/models/chatbarmessagecontentmodel.cpp
index 545f42ec0..7165851b5 100644
--- a/src/messagecontent/models/chatbarmessagecontentmodel.cpp
+++ b/src/messagecontent/models/chatbarmessagecontentmodel.cpp
@@ -547,7 +547,7 @@ Blocks::Block *ChatBarMessageContentModel::blockForFile(const QUrl &path)
 
         // TODO: Images in certain formats (e.g. WebP) will be erroneously marked as animated, even if they are static.
         imageInfo.isAnimated = QMovie::supportedFormats().contains(mime.preferredSuffix().toUtf8());
-        return new Blocks::ImageBlock(Blocks::Image, path, path.fileName(), imageInfo, QUrl(), Blocks::ImageInfo(), this);
+        return new Blocks::ImageBlock(Blocks::Image, path, path.fileName(), imageInfo, QUrl(), Blocks::ImageInfo(), true, this);
     }
     if (mime.name().contains(u"video"_s)) {
         Blocks::VideoInfo videoInfo;
@@ -694,6 +694,21 @@ void ChatBarMessageContentModel::removeAttachment()
     Q_EMIT hasAttachmentChanged();
 }
 
+void ChatBarMessageContentModel::setImageOptimization(const bool optimize) const
+{
+    if (!hasComponentType(Blocks::Image)) {
+        return;
+    }
+
+    auto mediaRow = 0;
+    if (Blocks::isFileType(m_components[1]->type())) {
+        mediaRow = 1;
+    }
+
+    dynamic_cast<Blocks::ImageBlock *>(m_components[mediaRow])->setOptimize(optimize);
+    updateCache(); // so the optimization flag is actually reflected
+}
+
 bool ChatBarMessageContentModel::sendMessageWithEnter() const
 {
     return m_sendMessageWithEnter;
diff --git a/src/messagecontent/models/chatbarmessagecontentmodel.h b/src/messagecontent/models/chatbarmessagecontentmodel.h
index 8c3fe0f59..761fa1f57 100644
--- a/src/messagecontent/models/chatbarmessagecontentmodel.h
+++ b/src/messagecontent/models/chatbarmessagecontentmodel.h
@@ -120,6 +120,7 @@ public:
     Q_INVOKABLE void removeComponent(int row, bool removeLast = false);
 
     Q_INVOKABLE void removeAttachment();
+    Q_INVOKABLE void setImageOptimization(bool optimize) const;
 
     bool sendMessageWithEnter() const;
     void setSendMessageWithEnter(bool sendMessageWithEnter);
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.