[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 ¶ms) : 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 ¶ms) : 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 ¶ms);
-
- 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 ¶ms);
+ 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