From 42d2b10d5d53ecc92531491fdf2e27399da08d84 Mon Sep 17 00:00:00 2001
From: Nicolas Werner <nicolas.werner@hotmail.de>
Date: Sat, 14 Aug 2021 17:17:50 +0200
Subject: [PATCH] Round images in the image provider

---
 resources/qml/Avatar.qml       | 16 +-----
 resources/qml/MessageView.qml  |  1 -
 resources/qml/Root.qml         |  1 -
 resources/qml/TimelineView.qml |  1 -
 src/MxcImageProvider.cpp       | 96 ++++++++++++++++++++++++++++------
 src/MxcImageProvider.h         |  7 ++-
 6 files changed, 87 insertions(+), 35 deletions(-)

diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml
index 9685dde1b..c3e8acdbc 100644
--- a/resources/qml/Avatar.qml
+++ b/resources/qml/Avatar.qml
@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 
 import "./ui"
-import QtGraphicalEffects 1.0
 import QtQuick 2.6
 import QtQuick.Controls 2.3
 import im.nheko 1.0
@@ -50,8 +49,7 @@ Rectangle {
         smooth: true
         sourceSize.width: avatar.width
         sourceSize.height: avatar.height
-        layer.enabled: true
-        source: avatar.url + ((avatar.crop || !avatar.url) ? "" : "?scale")
+        source: avatar.url ? (avatar.url + "?radius=" + radius + ((avatar.crop) ? "" : "&scale")) : ""
 
         MouseArea {
             id: mouseArea
@@ -65,18 +63,6 @@ Rectangle {
 
         }
 
-        layer.effect: OpacityMask {
-            cached: true
-
-            maskSource: Rectangle {
-                anchors.fill: parent
-                width: avatar.width
-                height: avatar.height
-                radius: Settings.avatarCircles ? height / 2 : 3
-            }
-
-        }
-
     }
 
     Rectangle {
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 79cbd700b..e5c6b4ecc 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -6,7 +6,6 @@ import "./delegates"
 import "./emoji"
 import "./ui"
 import Qt.labs.platform 1.1 as Platform
-import QtGraphicalEffects 1.0
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.2
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index 79f12bbfb..cc7d32ea4 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -8,7 +8,6 @@ import "./dialogs"
 import "./emoji"
 import "./voip"
 import Qt.labs.platform 1.1 as Platform
-import QtGraphicalEffects 1.0
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 104da160d..c8ac6bc7b 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -9,7 +9,6 @@ import "./emoji"
 import "./ui"
 import "./voip"
 import Qt.labs.platform 1.1 as Platform
-import QtGraphicalEffects 1.0
 import QtQuick 2.9
 import QtQuick.Controls 2.5
 import QtQuick.Layouts 1.3
diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp
index b86482698..58078a3b4 100644
--- a/src/MxcImageProvider.cpp
+++ b/src/MxcImageProvider.cpp
@@ -11,6 +11,8 @@
 #include <QByteArray>
 #include <QDir>
 #include <QFileInfo>
+#include <QPainter>
+#include <QPainterPath>
 #include <QStandardPaths>
 
 #include "Logging.h"
@@ -22,14 +24,26 @@ QHash<QString, mtx::crypto::EncryptedFile> infos;
 QQuickImageResponse *
 MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
 {
-        auto id_  = id;
-        bool crop = true;
-        if (id.endsWith("?scale")) {
-                crop = false;
-                id_.remove("?scale");
+        auto id_      = id;
+        bool crop     = true;
+        double radius = 0;
+
+        auto queryStart = id.lastIndexOf('?');
+        if (queryStart != -1) {
+                id_            = id.left(queryStart);
+                auto query     = id.midRef(queryStart + 1);
+                auto queryBits = query.split('&');
+
+                for (auto b : queryBits) {
+                        if (b == "scale") {
+                                crop = false;
+                        } else if (b.startsWith("radius=")) {
+                                radius = b.mid(7).toDouble();
+                        }
+                }
         }
 
-        MxcImageResponse *response = new MxcImageResponse(id_, crop, requestedSize);
+        MxcImageResponse *response = new MxcImageResponse(id_, crop, radius, requestedSize);
         pool.start(response);
         return response;
 }
@@ -53,14 +67,35 @@ MxcImageResponse::run()
                   }
                   emit finished();
           },
-          m_crop);
+          m_crop,
+          m_radius);
+}
+
+static QImage
+clipRadius(QImage img, double radius)
+{
+        QImage out(img.size(), QImage::Format_ARGB32_Premultiplied);
+        out.fill(Qt::transparent);
+
+        QPainter painter(&out);
+        painter.setRenderHint(QPainter::Antialiasing, true);
+        painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
+
+        QPainterPath ppath;
+        ppath.addRoundedRect(img.rect(), radius, radius);
+
+        painter.setClipPath(ppath);
+        painter.drawImage(img.rect(), img);
+
+        return out;
 }
 
 void
 MxcImageProvider::download(const QString &id,
                            const QSize &requestedSize,
                            std::function<void(QString, QSize, QImage, QString)> then,
-                           bool crop)
+                           bool crop,
+                           double radius)
 {
         std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
         auto temp = infos.find("mxc://" + id);
@@ -69,12 +104,13 @@ MxcImageProvider::download(const QString &id,
 
         if (requestedSize.isValid() && !encryptionInfo) {
                 QString fileName =
-                  QString("%1_%2x%3_%4")
+                  QString("%1_%2x%3_%4_radius%5")
                     .arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding |
                                                                 QByteArray::OmitTrailingEquals)))
                     .arg(requestedSize.width())
                     .arg(requestedSize.height())
-                    .arg(crop ? "crop" : "scale");
+                    .arg(crop ? "crop" : "scale")
+                    .arg(radius);
                 QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
                                      "/media_cache",
                                    fileName);
@@ -86,6 +122,10 @@ MxcImageProvider::download(const QString &id,
                                 image = image.scaled(
                                   requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
 
+                                if (radius != 0) {
+                                        image = clipRadius(std::move(image), radius);
+                                }
+
                                 if (!image.isNull()) {
                                         then(id, requestedSize, image, fileInfo.absoluteFilePath());
                                         return;
@@ -100,8 +140,8 @@ MxcImageProvider::download(const QString &id,
                 opts.method  = crop ? "crop" : "scale";
                 http::client()->get_thumbnail(
                   opts,
-                  [fileInfo, requestedSize, then, id](const std::string &res,
-                                                      mtx::http::RequestErr err) {
+                  [fileInfo, requestedSize, radius, then, id](const std::string &res,
+                                                              mtx::http::RequestErr err) {
                           if (err || res.empty()) {
                                   then(id, QSize(), {}, "");
 
@@ -113,6 +153,10 @@ MxcImageProvider::download(const QString &id,
                           if (!image.isNull()) {
                                   image = image.scaled(
                                     requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+
+                                  if (radius != 0) {
+                                          image = clipRadius(std::move(image), radius);
+                                  }
                           }
                           image.setText("mxc url", "mxc://" + id);
                           if (image.save(fileInfo.absoluteFilePath(), "png"))
@@ -126,8 +170,12 @@ MxcImageProvider::download(const QString &id,
                   });
         } else {
                 try {
-                        QString fileName = QString::fromUtf8(id.toUtf8().toBase64(
-                          QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
+                        QString fileName =
+                          QString("%1_radius%2")
+                            .arg(QString::fromUtf8(id.toUtf8().toBase64(
+                              QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)))
+                            .arg(radius);
+
                         QFileInfo fileInfo(
                           QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
                             "/media_cache",
@@ -148,6 +196,11 @@ MxcImageProvider::download(const QString &id,
                                         QImage image = utils::readImage(data);
                                         image.setText("mxc url", "mxc://" + id);
                                         if (!image.isNull()) {
+                                                if (radius != 0) {
+                                                        image =
+                                                          clipRadius(std::move(image), radius);
+                                                }
+
                                                 then(id,
                                                      requestedSize,
                                                      image,
@@ -158,6 +211,11 @@ MxcImageProvider::download(const QString &id,
                                         QImage image =
                                           utils::readImageFromFile(fileInfo.absoluteFilePath());
                                         if (!image.isNull()) {
+                                                if (radius != 0) {
+                                                        image =
+                                                          clipRadius(std::move(image), radius);
+                                                }
+
                                                 then(id,
                                                      requestedSize,
                                                      image,
@@ -169,7 +227,7 @@ MxcImageProvider::download(const QString &id,
 
                         http::client()->download(
                           "mxc://" + id.toStdString(),
-                          [fileInfo, requestedSize, then, id, encryptionInfo](
+                          [fileInfo, requestedSize, then, id, radius, encryptionInfo](
                             const std::string &res,
                             const std::string &,
                             const std::string &originalFilename,
@@ -195,6 +253,10 @@ MxcImageProvider::download(const QString &id,
                                           auto data =
                                             QByteArray(tempData.data(), (int)tempData.size());
                                           QImage image = utils::readImage(data);
+                                          if (radius != 0) {
+                                                  image = clipRadius(std::move(image), radius);
+                                          }
+
                                           image.setText("original filename",
                                                         QString::fromStdString(originalFilename));
                                           image.setText("mxc url", "mxc://" + id);
@@ -205,6 +267,10 @@ MxcImageProvider::download(const QString &id,
 
                                   QImage image =
                                     utils::readImageFromFile(fileInfo.absoluteFilePath());
+                                  if (radius != 0) {
+                                          image = clipRadius(std::move(image), radius);
+                                  }
+
                                   image.setText("original filename",
                                                 QString::fromStdString(originalFilename));
                                   image.setText("mxc url", "mxc://" + id);
diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h
index 61d828524..6de83c0e3 100644
--- a/src/MxcImageProvider.h
+++ b/src/MxcImageProvider.h
@@ -19,10 +19,11 @@ class MxcImageResponse
   , public QRunnable
 {
 public:
-        MxcImageResponse(const QString &id, bool crop, const QSize &requestedSize)
+        MxcImageResponse(const QString &id, bool crop, double radius, const QSize &requestedSize)
           : m_id(id)
           , m_requestedSize(requestedSize)
           , m_crop(crop)
+          , m_radius(radius)
         {
                 setAutoDelete(false);
         }
@@ -39,6 +40,7 @@ public:
         QSize m_requestedSize;
         QImage m_image;
         bool m_crop;
+        double m_radius;
 };
 
 class MxcImageProvider
@@ -54,7 +56,8 @@ public slots:
         static void download(const QString &id,
                              const QSize &requestedSize,
                              std::function<void(QString, QSize, QImage, QString)> then,
-                             bool crop = true);
+                             bool crop     = true,
+                             double radius = 0);
 
 private:
         QThreadPool pool;
-- 
GitLab