Skip to content
Snippets Groups Projects
Commit 4082222d authored by Prince Gupta's avatar Prince Gupta :speech_balloon: Committed by Jean-Baptiste Kempf
Browse files

qt: support image provider in RoundImage

parent 19cc1b35
No related branches found
No related tags found
1 merge request!1983qt: use in-memory caching for custom ml covers
......@@ -31,10 +31,12 @@
#include <QBuffer>
#include <QCache>
#include <QFile>
#include <QImage>
#include <QImageReader>
#include <QPainter>
#include <QPainterPath>
#include <QQuickImageProvider>
#include <QQuickWindow>
#include <QGuiApplication>
#include <QSGImageNode>
......@@ -116,6 +118,161 @@ namespace
qWarning() << "Could not load source image:" << url << error.what();
return {};
}
QRectF doPreserveAspectCrop(const QSizeF &sourceSize, const QSizeF &size)
{
const qreal ratio = std::max(size.width() / sourceSize.width(), size.height() / sourceSize.height());
const QSizeF imageSize = sourceSize * ratio;
const QPointF alignedCenteredTopLeft {(size.width() - imageSize.width()) / 2., (size.height() - imageSize.height()) / 2.};
return {alignedCenteredTopLeft, imageSize};
}
class ImageReader : public AsyncTask<QImage>
{
public:
// requestedSize is only taken as hint, the Image is resized with PreserveAspectCrop
ImageReader(const QUrl &url, QSize requestedSize)
: url {url}
, requestedSize {requestedSize}
{
}
QString errorString() const { return errorStr; }
QImage execute()
{
auto file = getReadable(url);
if (!file || !file->isOpen())
return {};
QImageReader reader;
reader.setDevice(file.get());
const QSize sourceSize = reader.size();
if (requestedSize.isValid())
reader.setScaledSize(doPreserveAspectCrop(sourceSize, requestedSize).size().toSize());
auto img = reader.read();
errorStr = reader.errorString();
return img;
}
private:
QUrl url;
QSize requestedSize;
QString errorStr;
};
class LocalImageResponse : public QQuickImageResponse
{
public:
LocalImageResponse(const QUrl &url, const QSize &requestedSize)
{
reader.reset(new ImageReader(url, requestedSize));
connect(reader.get(), &ImageReader::result, this, &LocalImageResponse::handleImageRead);
reader->start(*QThreadPool::globalInstance());
}
QQuickTextureFactory *textureFactory() const override
{
return result.isNull() ? nullptr : QQuickTextureFactory::textureFactoryForImage(result);
}
QString errorString() const override
{
return errorStr;
}
private:
void handleImageRead()
{
result = reader->takeResult();
errorStr = reader->errorString();
reader.reset();
emit finished();
}
QImage result;
TaskHandle<ImageReader> reader;
QString errorStr;
};
class ImageProviderAsyncAdaptor : public QQuickImageResponse
{
public:
ImageProviderAsyncAdaptor(QQuickImageProvider *provider, const QString &id, const QSize &requestedSize)
{
task.reset(new ProviderImageGetter(provider, id, requestedSize));
connect(task.get(), &ProviderImageGetter::result, this, [this]()
{
result = task->takeResult();
task.reset();
emit finished();
});
task->start(*QThreadPool::globalInstance());
}
QQuickTextureFactory *textureFactory() const override
{
return result.isNull() ? nullptr : QQuickTextureFactory::textureFactoryForImage(result);
}
private:
class ProviderImageGetter : public AsyncTask<QImage>
{
public:
ProviderImageGetter(QQuickImageProvider *provider, const QString &id, const QSize &requestedSize)
: provider {provider}
, id{id}
, requestedSize{requestedSize}
{
}
QImage execute() override
{
return provider->requestImage(id, &sourceSize, requestedSize);
}
private:
QQuickImageProvider *provider;
QString id;
QSize requestedSize;
QSize sourceSize;
};
TaskHandle<ProviderImageGetter> task;
QImage result;
};
QQuickImageResponse *getAsyncImageResponse(const QUrl &url, const QSize &requestedSize, QQmlEngine *engine)
{
if (url.scheme() == QStringLiteral("image"))
{
auto provider = engine->imageProvider(url.host());
if (!provider)
return nullptr;
assert(provider->imageType() == QQmlImageProviderBase::Image
|| provider->imageType() == QQmlImageProviderBase::ImageResponse);
const auto imageId = url.toString(QUrl::RemoveScheme | QUrl::RemoveAuthority).mid(1);;
if (provider->imageType() == QQmlImageProviderBase::Image)
return new ImageProviderAsyncAdaptor(static_cast<QQuickImageProvider *>(provider), imageId, requestedSize);
if (provider->imageType() == QQmlImageProviderBase::ImageResponse)
return static_cast<QQuickAsyncImageProvider *>(provider)->requestImageResponse(imageId, requestedSize);
return nullptr;
}
else
{
return new LocalImageResponse(url, requestedSize);
}
}
}
RoundImage::RoundImage(QQuickItem *parent) : QQuickItem {parent}
......@@ -127,6 +284,11 @@ RoundImage::RoundImage(QQuickItem *parent) : QQuickItem {parent}
connect(this, &QQuickItem::widthChanged, this, &RoundImage::regenerateRoundImage);
}
RoundImage::~RoundImage()
{
resetImageRequest();
}
QSGNode *RoundImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
auto node = static_cast<QSGImageNode *>(oldNode);
......@@ -229,25 +391,33 @@ void RoundImage::setDPR(const qreal value)
regenerateRoundImage();
}
void RoundImage::load()
void RoundImage::handleImageRequestFinished()
{
m_enqueuedGeneration = false;
assert(!m_roundImageGenerator);
const QString error = m_activeImageRequest->errorString();
QImage image;
if (auto textureFactory = m_activeImageRequest->textureFactory())
{
image = textureFactory->image();
delete textureFactory;
}
resetImageRequest();
if (image.isNull())
{
qDebug() << "failed to get image, error" << error << source();
return;
}
const qreal scaledWidth = this->width() * m_dpr;
const qreal scaledHeight = this->height() * m_dpr;
const qreal scaledRadius = this->radius() * m_dpr;
const ImageCacheKey key {source(), QSizeF {scaledWidth, scaledHeight}.toSize(), scaledRadius};
if (auto image = imageCache.object(key)) // should only by called in mainthread
{
setRoundImage(*image);
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(m_source, scaledWidth, scaledHeight, scaledRadius));
m_roundImageGenerator.reset(new RoundImageGenerator(image, scaledWidth, scaledHeight, scaledRadius));
connect(m_roundImageGenerator.get(), &BaseAsyncTask::result, this, [this, key]()
{
const auto image = new QImage(m_roundImageGenerator->takeResult());
......@@ -270,6 +440,40 @@ void RoundImage::load()
m_roundImageGenerator->start(*QThreadPool::globalInstance());
}
void RoundImage::resetImageRequest()
{
if (!m_activeImageRequest)
return;
m_activeImageRequest->disconnect(this);
m_activeImageRequest->deleteLater();
m_activeImageRequest = nullptr;
}
void RoundImage::load()
{
m_enqueuedGeneration = false;
assert(!m_roundImageGenerator);
auto engine = qmlEngine(this);
if (!engine || m_source.isEmpty() || !size().isValid() || size().isEmpty())
return;
const qreal scaledWidth = this->width() * m_dpr;
const qreal scaledHeight = this->height() * m_dpr;
const qreal scaledRadius = this->radius() * m_dpr;
const ImageCacheKey key {source(), QSizeF {scaledWidth, scaledHeight}.toSize(), scaledRadius};
if (auto image = imageCache.object(key)) // should only by called in mainthread
{
setRoundImage(*image);
return;
}
m_activeImageRequest = getAsyncImageResponse(source(), QSizeF {scaledWidth, scaledHeight}.toSize(), engine);
connect(m_activeImageRequest, &QQuickImageResponse::finished, this, &RoundImage::handleImageRequestFinished);
}
void RoundImage::setRoundImage(QImage image)
{
m_dirty = true;
......@@ -292,6 +496,8 @@ void RoundImage::regenerateRoundImage()
// remove old contents
setRoundImage({});
resetImageRequest();
m_roundImageGenerator.reset();
// use Qt::QueuedConnection to delay generation, so that dependent properties
......@@ -301,8 +507,8 @@ void RoundImage::regenerateRoundImage()
QMetaObject::invokeMethod(this, &RoundImage::load, Qt::QueuedConnection);
}
RoundImage::RoundImageGenerator::RoundImageGenerator(const QUrl &source, qreal width, qreal height, qreal radius)
: source(source)
RoundImage::RoundImageGenerator::RoundImageGenerator(const QImage &sourceImage, qreal width, qreal height, qreal radius)
: sourceImage(sourceImage)
, width(width)
, height(height)
, radius(radius)
......@@ -311,34 +517,9 @@ RoundImage::RoundImageGenerator::RoundImageGenerator(const QUrl &source, qreal w
QImage RoundImage::RoundImageGenerator::execute()
{
if (width <= 0 || height <= 0)
return {};
if (source.isEmpty())
return {};
auto file = getReadable(source);
if (!file || !file->isOpen())
if (width <= 0 || height <= 0 || sourceImage.isNull())
return {};
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());
if (Q_UNLIKELY(radius <= 0))
{
return sourceReader.read();
}
QImage target(width, height, QImage::Format_ARGB32_Premultiplied);
if (target.isNull())
return target;
......@@ -354,7 +535,10 @@ QImage RoundImage::RoundImageGenerator::execute()
path.addRoundedRect(0, 0, width, height, radius, radius);
painter.setClipPath(path);
painter.drawImage({alignedCenteredTopLeft, targetSize}, sourceReader.read());
// do PreserveAspectCrop
const auto imageSize = sourceImage.size();
const QPointF alignedCenteredTopLeft {(width - imageSize.width()) / 2., (height - imageSize.height()) / 2.};
painter.drawImage(QRectF {alignedCenteredTopLeft, imageSize}, sourceImage);
}
return target;
......
......@@ -31,6 +31,8 @@
#include <QQuickItem>
#include <QUrl>
class QQuickImageResponse;
class RoundImage : public QQuickItem
{
Q_OBJECT
......@@ -42,6 +44,7 @@ class RoundImage : public QQuickItem
public:
RoundImage(QQuickItem *parent = nullptr);
~RoundImage();
void componentComplete() override;
......@@ -64,18 +67,20 @@ private:
class RoundImageGenerator : public AsyncTask<QImage>
{
public:
RoundImageGenerator(const QUrl &source, qreal width, qreal height, qreal radius);
RoundImageGenerator(const QImage &sourceImage, qreal width, qreal height, qreal radius);
QImage execute();
private:
QUrl source;
QImage sourceImage;
qreal width;
qreal height;
qreal radius;
};
void setDPR(qreal value);
void handleImageRequestFinished();
void resetImageRequest();
void load();
void setRoundImage(QImage image);
void regenerateRoundImage();
......@@ -88,6 +93,7 @@ private:
bool m_dirty = false;
TaskHandle<RoundImageGenerator> m_roundImageGenerator {};
QQuickImageResponse *m_activeImageRequest {};
bool m_enqueuedGeneration = false;
};
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment