[vlc-commits] [Git][videolan/vlc][master] 3 commits: qt/roundimage: unify loading and round image generation stage

Jean-Baptiste Kempf gitlab at videolan.org
Sat May 22 19:57:16 UTC 2021



Jean-Baptiste Kempf pushed to branch master at VideoLAN / VLC


Commits:
ecbe4ea6 by Prince Gupta at 2021-05-22T19:04:05+00:00
qt/roundimage: unify loading and round image generation stage

they were seperated to optimize conditions when the width and height
changes too frequently, but they occur seldomly, plus caching both
source image and round image costs double memory

- - - - -
a2afa3a6 by Prince Gupta at 2021-05-22T19:04:05+00:00
qt/roundimage: use QImageReader

- - - - -
a0d14a30 by Prince Gupta at 2021-05-22T19:04:05+00:00
qt/roundimage: implement caching

- - - - -


5 changed files:

- modules/gui/qt/medialibrary/qml/MusicAlbumsGridExpandDelegate.qml
- modules/gui/qt/medialibrary/qml/VideoInfoExpandPanel.qml
- modules/gui/qt/widgets/native/roundimage.cpp
- modules/gui/qt/widgets/native/roundimage.hpp
- modules/gui/qt/widgets/qml/MediaCover.qml


Changes:

=====================================
modules/gui/qt/medialibrary/qml/MusicAlbumsGridExpandDelegate.qml
=====================================
@@ -111,7 +111,6 @@ Widgets.NavigableFocusScope {
                         width: VLCStyle.expandCover_music_width
                         radius: VLCStyle.expandCover_music_radius
                         source: model.cover || VLCStyle.noArtAlbum
-                        sourceSize: Qt.size(width, height)
                     }
 
                     Widgets.ListCoverShadow {


=====================================
modules/gui/qt/medialibrary/qml/VideoInfoExpandPanel.qml
=====================================
@@ -100,7 +100,6 @@ Widgets.NavigableFocusScope {
 
                             anchors.fill: parent
                             source: model.thumbnail || VLCStyle.noArtCover
-                            sourceSize: Qt.size(width, height)
                             radius: VLCStyle.gridCover_radius
                         }
 


=====================================
modules/gui/qt/widgets/native/roundimage.cpp
=====================================
@@ -27,12 +27,15 @@
 
 #include "roundimage.hpp"
 
-#include <QBrush>
+#include <qhashfunctions.h>
+
+#include <QBuffer>
+#include <QCache>
 #include <QImage>
+#include <QImageReader>
+#include <QPainter>
 #include <QPainterPath>
-#include <QPen>
 #include <QQuickWindow>
-#include <QSvgRenderer>
 #include <QGuiApplication>
 
 #ifdef QT_NETWORK_LIB
@@ -43,6 +46,31 @@
 
 namespace
 {
+    struct ImageCacheKey
+    {
+        QUrl url;
+        QSize size;
+        qreal radius;
+    };
+
+    bool operator ==(const ImageCacheKey &lhs, const ImageCacheKey &rhs)
+    {
+        return lhs.radius == rhs.radius && lhs.size == rhs.size && lhs.url == rhs.url;
+    }
+
+    uint qHash(const ImageCacheKey &key, uint seed)
+    {
+        QtPrivate::QHashCombine hash;
+        seed = hash(seed, key.url);
+        seed = hash(seed, key.size.width());
+        seed = hash(seed, key.size.height());
+        seed = hash(seed, key.radius);
+        return seed;
+    }
+
+    // images are cached (result of RoundImageGenerator) with the cost calculated from QImage::sizeInBytes
+    QCache<ImageCacheKey, QImage> imageCache(2 * 1024 * 1024); // 2 MiB
+
     QString getPath(const QUrl &url)
     {
         QString path = url.isLocalFile() ? url.toLocalFile() : url.toString();
@@ -51,7 +79,7 @@ namespace
         return path;
     }
 
-    QByteArray readFile(const QUrl &url)
+    std::unique_ptr<QIODevice> getReadable(const QUrl &url)
     {
 #ifdef QT_NETWORK_LIB
         if (url.scheme() == "http" || url.scheme() == "https")
@@ -62,16 +90,34 @@ namespace
             QEventLoop loop;
             QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
             loop.exec();
-            return reply->readAll();
+
+            class DataOwningBuffer : public QBuffer
+            {
+            public:
+                DataOwningBuffer(const QByteArray &data) : m_data {data}
+                {
+                    setBuffer(&m_data);
+                }
+
+                ~DataOwningBuffer()
+                {
+                    close();
+                    setBuffer(nullptr);
+                }
+
+            private:
+                QByteArray m_data;
+            };
+
+            auto file = std::make_unique<DataOwningBuffer>(reply->readAll());
+            file->open(QIODevice::ReadOnly);
+            return file;
         }
 #endif
 
-        QByteArray data;
-        QString path = getPath(url);
-        QFile file(path);
-        if (file.open(QIODevice::ReadOnly))
-            data = file.readAll();
-        return data;
+        auto file = std::make_unique<QFile>(getPath(url));
+        file->open(QIODevice::ReadOnly);
+        return file;
     }
 }
 
@@ -86,6 +132,8 @@ RoundImage::RoundImage(QQuickItem *parent) : QQuickPaintedItem {parent}
 
 void RoundImage::paint(QPainter *painter)
 {
+    if (m_roundImage.isNull())
+        return;
     painter->drawImage(QPointF {0., 0.}, m_roundImage, m_roundImage.rect());
 }
 
@@ -103,7 +151,9 @@ void RoundImage::componentComplete()
     Q_ASSERT(!m_isComponentComplete); // classBegin is not called?
     m_isComponentComplete = true;
     if (!m_source.isEmpty())
-        updateSource();
+        regenerateRoundImage();
+    else
+        m_roundImage = {};
 }
 
 QUrl RoundImage::source() const
@@ -116,11 +166,6 @@ qreal RoundImage::radius() const
     return m_radius;
 }
 
-QSizeF RoundImage::sourceSize() const
-{
-    return m_sourceSize;
-}
-
 void RoundImage::setSource(QUrl source)
 {
     if (m_source == source)
@@ -128,7 +173,7 @@ void RoundImage::setSource(QUrl source)
 
     m_source = source;
     emit sourceChanged(m_source);
-    updateSource();
+    regenerateRoundImage();
 }
 
 void RoundImage::setRadius(qreal radius)
@@ -141,16 +186,6 @@ void RoundImage::setRadius(qreal radius)
     regenerateRoundImage();
 }
 
-void RoundImage::setSourceSize(QSizeF sourceSize)
-{
-    if (m_sourceSize == sourceSize)
-        return;
-
-    m_sourceSize = sourceSize;
-    emit sourceSizeChanged(m_sourceSize);
-    updateSource();
-}
-
 void RoundImage::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
 {
     if (change == QQuickItem::ItemDevicePixelRatioHasChanged)
@@ -165,35 +200,12 @@ void RoundImage::setDPR(const qreal value)
         return;
 
     m_dpr = value;
-    if (m_sourceSize.isValid())
-        updateSource(); // "effectiveSourceSize" is changed
-    else
-        regenerateRoundImage();
-}
-
-void RoundImage::updateSource()
-{
-    if (!m_isComponentComplete)
-        return;
-
-    const QSizeF effectiveSourceSize = m_sourceSize.isValid() ? m_sourceSize * m_dpr : QSize {};
-    m_loader.reset(new Loader({m_source, effectiveSourceSize}));
-    connect(m_loader.get(), &BaseAsyncTask::result, this, [this]()
-    {
-        m_sourceImage = m_loader->takeResult();
-        m_loader.reset();
-
-        regenerateRoundImage();
-    });
-
-    m_loader->start(*QThreadPool::globalInstance());
+    regenerateRoundImage();
 }
 
 void RoundImage::regenerateRoundImage()
 {
-    if (!m_isComponentComplete
-            || m_enqueuedGeneration
-            || m_loader /* when loader ends it will call regenerateRoundImage */)
+    if (!m_isComponentComplete || m_enqueuedGeneration)
         return;
 
     // use Qt::QueuedConnection to delay generation, so that dependent properties
@@ -204,15 +216,30 @@ void RoundImage::regenerateRoundImage()
     {
         m_enqueuedGeneration = false;
 
+        const qreal scaleWidth = this->width() * m_dpr;
+        const qreal scaledHeight = this->height() * m_dpr;
+        const qreal scaledRadius = this->radius() * m_dpr;
+
+        const ImageCacheKey key {source(), QSizeF {scaleWidth, scaledHeight}.toSize(), scaledRadius};
+        if (auto image = imageCache.object(key)) // should only by called in mainthread
+        {
+            m_roundImage = *image;
+            update();
+            return;
+        }
+
         // Image is generated in size factor of `m_dpr` to avoid scaling artefacts when
         // generated image is set with device pixel ratio
-        m_roundImageGenerator.reset(new RoundImageGenerator({width() * m_dpr, height() * m_dpr, radius() * m_dpr, m_sourceImage}));
-        connect(m_roundImageGenerator.get(), &BaseAsyncTask::result, this, [this]()
+        m_roundImageGenerator.reset(new RoundImageGenerator(m_source, scaleWidth, scaledHeight, scaledRadius));
+        connect(m_roundImageGenerator.get(), &BaseAsyncTask::result, this, [this, key]()
         {
             m_roundImage = m_roundImageGenerator->takeResult();
             m_roundImage.setDevicePixelRatio(m_dpr);
             m_roundImageGenerator.reset();
 
+            if (!m_roundImage.isNull())
+                imageCache.insert(key, new QImage(m_roundImage), m_roundImage.sizeInBytes());
+
             update();
         });
 
@@ -220,27 +247,41 @@ void RoundImage::regenerateRoundImage()
     }, Qt::QueuedConnection);
 }
 
-RoundImage::Loader::Loader(const Params &params) : params {params} {}
-
-RoundImage::ImagePtr RoundImage::Loader::execute()
+RoundImage::RoundImageGenerator::RoundImageGenerator(const QUrl &source, qreal width, qreal height, qreal radius)
+    : source(source)
+    , width(width)
+    , height(height)
+    , radius(radius)
 {
-    return Image::getImage(params.source, params.sourceSize);
 }
 
-RoundImage::RoundImageGenerator::RoundImageGenerator(const RoundImage::RoundImageGenerator::Params &params) : params {params} {}
-
 QImage RoundImage::RoundImageGenerator::execute()
 {
-    if (params.width <= 0 || params.height <= 0)
+    if (width <= 0 || height <= 0)
+        return {};
+
+    auto file = getReadable(source);
+    if (!file || !file->isOpen())
         return {};
 
-    QImage target(params.width, params.height, QImage::Format_ARGB32);
+    QImageReader sourceReader(file.get());
+
+    // do PreserveAspectCrop
+    const QSizeF size {width, height};
+    QSizeF defaultSize = sourceReader.size();
+    if (!defaultSize.isValid())
+        defaultSize = size;
+
+    const qreal ratio = std::max(size.width() / defaultSize.width(), size.height() / defaultSize.height());
+    const QSizeF targetSize = defaultSize * ratio;
+    const QPointF alignedCenteredTopLeft {(size.width() - targetSize.width()) / 2., (size.height() - targetSize.height()) / 2.};
+    sourceReader.setScaledSize(targetSize.toSize());
+
+    QImage target(width, height, QImage::Format_ARGB32);
     if (target.isNull())
         return target;
 
     target.fill(Qt::transparent);
-    if (Q_UNLIKELY(!params.image))
-        return target;
 
     QPainter painter;
     painter.begin(&target);
@@ -248,77 +289,11 @@ QImage RoundImage::RoundImageGenerator::execute()
     painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
 
     QPainterPath path;
-    path.addRoundedRect(0, 0, params.width, params.height, params.radius, params.radius);
+    path.addRoundedRect(0, 0, width, height, radius, radius);
     painter.setClipPath(path);
 
-    params.image->paint(&painter, {params.width, params.height});
+    painter.drawImage({alignedCenteredTopLeft, targetSize}, sourceReader.read());
     painter.end();
 
     return target;
 }
-
-std::shared_ptr<RoundImage::Image> RoundImage::Image::getImage(const QUrl &source, const QSizeF &sourceSize)
-{
-    class QtImage : public Image
-    {
-    public:
-        QtImage(const QByteArray &data, const QSizeF &sourceSize)
-        {
-            m_image.loadFromData(data);
-
-            if (sourceSize.isValid())
-                m_image = m_image.scaled(sourceSize.toSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
-        }
-
-        void paint(QPainter *painter, const QSizeF &size) override
-        {
-            if (m_image.isNull() || !painter || !size.isValid())
-                return;
-
-            auto image = m_image;
-
-            // do PreserveAspectCrop
-            const qreal ratio = std::max(qreal(size.width()) / image.width(), qreal(size.height()) / image.height());
-            if (ratio != 0.0)
-                image = image.scaled(qRound(image.width() * ratio), qRound(image.height() * ratio),
-                                     Qt::IgnoreAspectRatio, // aspect ratio handled manually by using `ratio`
-                                     Qt::SmoothTransformation);
-
-            const QPointF alignedCenteredTopLeft {(size.width() - image.width()) / 2, (size.height() - image.height()) / 2};
-            painter->drawImage(alignedCenteredTopLeft, image);
-        }
-
-    private:
-        QImage m_image;
-    };
-
-    class SVGImage : public Image
-    {
-    public:
-        SVGImage(const QByteArray &data)
-        {
-            m_svg.load(data);
-        }
-
-        void paint(QPainter *painter, const QSizeF &size) override
-        {
-            if (!m_svg.isValid() || !painter || !size.isValid())
-                return;
-
-            // do PreserveAspectCrop
-            const QSizeF defaultSize = m_svg.defaultSize();
-            const qreal ratio = std::max(size.width() / defaultSize.width(), size.height() / defaultSize.height());
-            const QSizeF targetSize = defaultSize * ratio;
-            const QPointF alignedCenteredTopLeft {(size.width() - targetSize.width()) / 2., (size.height() - targetSize.height()) / 2.};
-            m_svg.render(painter, QRectF {alignedCenteredTopLeft, targetSize});
-        }
-
-    private:
-        QSvgRenderer m_svg;
-    };
-
-    const QByteArray data = readFile(source);
-    if (source.toString().endsWith(".svg"))
-        return std::make_shared<SVGImage>(data);
-    return std::make_shared<QtImage>(data, sourceSize);
-}


=====================================
modules/gui/qt/widgets/native/roundimage.hpp
=====================================
@@ -27,8 +27,7 @@
 
 #include "util/asynctask.hpp"
 
-#include <QPixmap>
-#include <QPainter>
+#include <QImage>
 #include <QQuickPaintedItem>
 #include <QUrl>
 
@@ -39,9 +38,6 @@ class RoundImage : public QQuickPaintedItem
     // url of the image
     Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
 
-    // sets the maximum number of pixels stored for the loaded image so that large images do not use more memory than necessary
-    Q_PROPERTY(QSizeF sourceSize READ sourceSize WRITE setSourceSize NOTIFY sourceSizeChanged)
-
     Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged)
 
 public:
@@ -54,12 +50,10 @@ public:
 
     QUrl source() const;
     qreal radius() const;
-    QSizeF sourceSize() const;
 
 public slots:
     void setSource(QUrl source);
     void setRadius(qreal radius);
-    void setSourceSize(QSizeF sourceSize);
 
 signals:
     void sourceChanged(QUrl source);
@@ -70,65 +64,27 @@ protected:
     void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) override;
 
 private:
-    class Image
-    {
-    public:
-        static std::shared_ptr<Image> getImage(const QUrl &source, const QSizeF &sourceSize);
-        virtual ~Image() = default;
-
-        // must be reentrant
-        virtual void paint(QPainter *painter, const QSizeF &size) = 0;
-    };
-
-    using ImagePtr = std::shared_ptr<Image>;
-
-    class Loader : public AsyncTask<ImagePtr>
-    {
-    public:
-        struct Params
-        {
-            QUrl source;
-            QSizeF sourceSize;
-        };
-
-        Loader(const Params &params);
-
-        ImagePtr execute();
-
-    private:
-        const Params params;
-    };
-
     class RoundImageGenerator : public AsyncTask<QImage>
     {
     public:
-        struct Params
-        {
-            qreal width;
-            qreal height;
-            qreal radius;
-            ImagePtr image;
-        };
-
-        RoundImageGenerator(const Params &params);
+        RoundImageGenerator(const QUrl &source, qreal width, qreal height, qreal radius);
 
         QImage execute();
 
     private:
-        const Params params;
+        QUrl source;
+        qreal width;
+        qreal height;
+        qreal radius;
     };
 
     void setDPR(qreal value);
-    void updateSource();
     void regenerateRoundImage();
 
     QUrl m_source;
-    ImagePtr m_sourceImage;
     qreal m_radius = 0.0;
-    QSizeF m_sourceSize;
     qreal m_dpr = 1.0; // device pixel ratio
     QImage m_roundImage;
-    TaskHandle<Loader> m_loader {};
     TaskHandle<RoundImageGenerator> m_roundImageGenerator {};
 
     bool m_enqueuedGeneration = false;


=====================================
modules/gui/qt/widgets/qml/MediaCover.qml
=====================================
@@ -37,7 +37,6 @@ RoundImage {
 
     height: VLCStyle.listAlbumCover_height
     width: VLCStyle.listAlbumCover_width
-    sourceSize: Qt.size(width, height)
 
     Loader {
         id: overlay



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/c2f86a25fdace335c401c4926751df92746ecba8...a0d14a303cf0ceb32ab6f25f684186c2669e9a05

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/c2f86a25fdace335c401c4926751df92746ecba8...a0d14a303cf0ceb32ab6f25f684186c2669e9a05
You're receiving this email because of your account on code.videolan.org.




More information about the vlc-commits mailing list