Source code
Revision control
Copy as Markdown
// Copyright (C) 2016 Jolla Ltd, author: <gunnar.sletta@jollamobile.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include <private/qtquickglobal_p.h>
#include "qquickitemgrabresult.h"
#include "qquickrendercontrol.h"
#include "qquickwindow.h"
#include "qquickitem.h"
#if QT_CONFIG(quick_shadereffect)
#include "qquickshadereffectsource_p.h"
#endif
#include <QtQml/QQmlEngine>
#include <QtQml/QQmlInfo>
#include <private/qquickpixmap_p.h>
#include <private/qquickitem_p.h>
#include <private/qsgcontext_p.h>
#include <private/qsgadaptationlayer_p.h>
#include <QtCore/qmutex.h>
#include <QtCore/qpointer.h>
QT_BEGIN_NAMESPACE
const QEvent::Type Event_Grab_Completed = static_cast<QEvent::Type>(QEvent::User + 1);
class QQuickItemGrabResultPrivate : public QObjectPrivate
{
public:
QQuickItemGrabResultPrivate()
: cacheEntry(nullptr)
, qmlEngine(nullptr)
, texture(nullptr)
, devicePixelRatio(1.0)
{
}
~QQuickItemGrabResultPrivate()
{
// Remove the unmanaged heap size we've added when creating the image in
// QQuickItemGrabResult::render(). If render() was never called the image is empty and
// its size is 0.
if (hasCallback())
qmlEngine->handle()->memoryManager->changeUnmanagedHeapSizeUsage(-image.sizeInBytes());
delete cacheEntry;
}
// Should be called under a locked mutex!
bool hasCallback() const { return qmlEngine && callback.isCallable(); }
// Should be called under a locked mutex!
void ensureImageInCache() const {
if (url.isEmpty() && !image.isNull()) {
url.setScheme(QQuickPixmap::itemGrabberScheme);
url.setPath(QVariant::fromValue(item.data()).toString());
static uint counter = 0;
url.setFragment(QString::number(++counter));
cacheEntry = new QQuickPixmap(url, image);
}
}
static QQuickItemGrabResult *create(QQuickItem *item, const QSize &size);
mutable QBasicMutex mutex;
QImage image;
mutable QUrl url;
mutable QQuickPixmap *cacheEntry;
QQmlEngine *qmlEngine;
QJSValue callback;
QPointer<QQuickItem> item;
QPointer<QQuickWindow> window;
QSGLayer *texture;
QSizeF itemSize;
QSize textureSize;
qreal devicePixelRatio;
QMetaObject::Connection setupConnection;
QMetaObject::Connection renderConnection;
};
/*!
* \qmlproperty url QtQuick::ItemGrabResult::url
*
* This property holds a URL which can be used in conjunction with
* URL based image consumers, such as the QtQuick::Image type.
*
* The URL is valid while there is a reference in QML or JavaScript
* to the ItemGrabResult or while the image the URL references is
* actively used.
*
* The URL does not represent a valid file or location to read it from, it
* is primarily a key to access images through Qt Quick's image-based types.
*/
/*!
* \property QQuickItemGrabResult::url
*
* This property holds a URL which can be used in conjunction with
* URL based image consumers, such as the QtQuick::Image type.
*
* The URL is valid until the QQuickItemGrabResult object is deleted.
*
* The URL does not represent a valid file or location to read it from, it
* is primarily a key to access images through Qt Quick's image-based types.
*/
/*!
* \qmlproperty variant QtQuick::ItemGrabResult::image
*
* This property holds the pixel results from a grab in the
* form of a QImage.
*/
/*!
* \property QQuickItemGrabResult::image
*
* This property holds the pixel results from a grab.
*
* If the grab is not yet complete or if it failed,
* a null image is returned (\c {image.isNull()} will return \c true).
*/
/*!
\class QQuickItemGrabResult
\inmodule QtQuick
\brief The QQuickItemGrabResult contains the result from QQuickItem::grabToImage().
\sa QQuickItem::grabToImage()
*/
/*!
* \fn void QQuickItemGrabResult::ready()
*
* This signal is emitted when the grab has completed.
*/
/*!
* \qmltype ItemGrabResult
* \nativetype QQuickItemGrabResult
* \inherits QtObject
* \inqmlmodule QtQuick
* \ingroup qtquick-visual
* \brief Contains the results from a call to Item::grabToImage().
*
* The ItemGrabResult is a small container used to encapsulate
* the results from Item::grabToImage().
*
* \sa Item::grabToImage()
*/
QQuickItemGrabResult::QQuickItemGrabResult(QObject *parent)
: QObject(*new QQuickItemGrabResultPrivate, parent)
{
}
/*!
* \qmlmethod bool QtQuick::ItemGrabResult::saveToFile(fileName)
*
* Saves the grab result as an image to \a fileName. Returns \c true
* if successful; otherwise returns \c false.
*/
// ### Qt 7: remove and keep only QUrl overload
/*!
* Saves the grab result as an image to \a fileName. Returns \c true
* if successful; otherwise returns \c false.
*
* \note In Qt versions prior to 5.9, this function is marked as non-\c{const}.
*/
bool QQuickItemGrabResult::saveToFile(const QString &fileName) const
{
Q_D(const QQuickItemGrabResult);
if (fileName.startsWith(QLatin1String("file:/")))
return saveToFile(QUrl(fileName));
QMutexLocker locker(&d->mutex);
return d->image.save(fileName);
}
/*!
* \since 6.2
* Saves the grab result as an image to \a filePath, which must refer to a
* \l{QUrl::isLocalFile}{local file name} with a
* \l{QImageWriter::supportedImageFormats()}{supported image format} extension.
* Returns \c true if successful; otherwise returns \c false.
*/
bool QQuickItemGrabResult::saveToFile(const QUrl &filePath) const
{
Q_D(const QQuickItemGrabResult);
if (!filePath.isLocalFile()) {
qWarning() << "saveToFile can only save to a file on the local filesystem";
return false;
}
QMutexLocker locker(&d->mutex);
return d->image.save(filePath.toLocalFile());
}
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#if QT_DEPRECATED_SINCE(5, 15)
/*!
* \overload
* \internal
*/
bool QQuickItemGrabResult::saveToFile(const QString &fileName)
{
return std::as_const(*this).saveToFile(fileName);
}
#endif
#endif // < Qt 6
QUrl QQuickItemGrabResult::url() const
{
Q_D(const QQuickItemGrabResult);
QMutexLocker locker(&d->mutex);
d->ensureImageInCache();
return d->url;
}
QImage QQuickItemGrabResult::image() const
{
Q_D(const QQuickItemGrabResult);
QMutexLocker locker(&d->mutex);
return d->image;
}
/*!
* \internal
*/
bool QQuickItemGrabResult::event(QEvent *e)
{
Q_D(QQuickItemGrabResult);
if (e->type() == Event_Grab_Completed) {
// We cannot hold the mutex while emitting the signal or calling the
// callback, because they can immediately call various getters.
QJSValue callback;
QQmlEngine *engine = nullptr;
if (QMutexLocker locker(&d->mutex); d->hasCallback()) {
callback = d->callback;
engine = d->qmlEngine;
}
if (engine) {
// We have a JS callback. Transfer ownership to JavaScript to follow the documentation.
// The user should be able to store the object in QML, but we also want to GC it
// eventually.
QQmlEngine::setObjectOwnership(this, QQmlEngine::JavaScriptOwnership);
setParent(nullptr);
callback.call(QJSValueList() << engine->newQObject(this));
} else {
Q_EMIT ready();
}
return true;
}
return QObject::event(e);
}
/*!
\internal
This method is called from QSGRenderThread
*/
void QQuickItemGrabResult::setup()
{
Q_D(QQuickItemGrabResult);
QMutexLocker locker(&d->mutex);
if (!d->item) {
disconnect(d->setupConnection);
disconnect(d->renderConnection);
QCoreApplication::postEvent(this, new QEvent(Event_Grab_Completed));
return;
}
QSGRenderContext *rc = QQuickWindowPrivate::get(d->window.data())->context;
d->devicePixelRatio = d->window->effectiveDevicePixelRatio();
d->texture = rc->sceneGraphContext()->createLayer(rc);
d->texture->setDevicePixelRatio(d->devicePixelRatio);
d->texture->setItem(QQuickItemPrivate::get(d->item)->itemNode());
d->itemSize = QSizeF(d->item->width(), d->item->height());
}
/*!
\internal
This method is called from QSGRenderThread
*/
void QQuickItemGrabResult::render()
{
Q_D(QQuickItemGrabResult);
QMutexLocker locker(&d->mutex);
if (!d->texture)
return;
d->texture->setRect(QRectF(0, d->itemSize.height(), d->itemSize.width(), -d->itemSize.height()));
const QSize minSize = QQuickWindowPrivate::get(d->window.data())->context->sceneGraphContext()->minimumFBOSize();
const QSize effectiveTextureSize = d->textureSize.isEmpty()
? (d->itemSize * d->devicePixelRatio).toSize()
: d->textureSize * d->devicePixelRatio;
d->texture->setSize(QSize(qMax(minSize.width(), effectiveTextureSize.width()),
qMax(minSize.height(), effectiveTextureSize.height())));
d->texture->scheduleUpdate();
d->texture->updateTexture();
const qsizetype oldImageSize = d->image.sizeInBytes();
d->image = d->texture->toImage();
d->image.setDevicePixelRatio(d->devicePixelRatio);
const qsizetype newImageSize = d->image.sizeInBytes();
if (d->hasCallback()) {
// If we have a callback, we assume that it's going to be called, eventually. If you post
// an event that never arrives, you have worse problems. Based on that assumption, we track
// the (potentially large) image as unmanaged heap size already now. This is the only place
// where we can determine the size. In the dtor we need to "untrack" the image if it was
// tracked. So either we need to store an extra bit that tells us whether the callback was
// called or we need to live with stale unmanaged heap size in the unlikely case that it's
// never called. We choose the latter.
d->qmlEngine->handle()->memoryManager->changeUnmanagedHeapSizeUsage(
newImageSize - oldImageSize);
}
delete d->texture;
d->texture = nullptr;
disconnect(d->setupConnection);
disconnect(d->renderConnection);
QCoreApplication::postEvent(this, new QEvent(Event_Grab_Completed));
}
QQuickItemGrabResult *QQuickItemGrabResultPrivate::create(QQuickItem *item, const QSize &targetSize)
{
const QSize size = targetSize.isEmpty() ? QSize(item->width(), item->height()) : targetSize;
if (size.width() < 1 || size.height() < 1) {
qmlWarning(item) << "grabToImage: item's width and/or height are less than 1: " << size;
return nullptr;
}
if (!item->window()) {
qmlWarning(item) << "grabToImage: item is not attached to a window";
return nullptr;
}
QWindow *effectiveWindow = item->window();
if (QWindow *renderWindow = QQuickRenderControl::renderWindowFor(item->window()))
effectiveWindow = renderWindow;
if (!effectiveWindow->isVisible()) {
qmlWarning(item) << "grabToImage: item's window is not visible";
return nullptr;
}
// Initially parent to item so that we don't leak it if Event_Grab_Completed never arrives or
// the callback can't be called.
QQuickItemGrabResult *result = new QQuickItemGrabResult(item);
QQuickItemGrabResultPrivate *d = result->d_func();
// no need to lock here, since we didn't do the connection yet, so it cannot be
// accessed from multiple threads
d->item = item;
d->window = item->window();
d->textureSize = targetSize;
QQuickItemPrivate::get(item)->refFromEffectItem(false);
// trigger sync & render
item->window()->update();
return result;
}
/*!
* Grabs the item into an in-memory image.
*
* The grab happens asynchronously and the signal QQuickItemGrabResult::ready()
* is emitted when the grab has been completed.
*
* Use \a targetSize to specify the size of the target image. By default, the
* result will have the same size as item.
*
* If the grab could not be initiated, the function returns \c null.
*
* \note This function will render the item to an offscreen surface and
* copy that surface from the GPU's memory into the CPU's memory, which can
* be quite costly. For "live" preview, use \l {QtQuick::Item::layer.enabled} {layers}
* or ShaderEffectSource.
*
* \sa QQuickWindow::grabWindow()
*/
QSharedPointer<QQuickItemGrabResult> QQuickItem::grabToImage(const QSize &targetSize)
{
QQuickItemGrabResult *result = QQuickItemGrabResultPrivate::create(this, targetSize);
if (!result)
return QSharedPointer<QQuickItemGrabResult>();
QQuickItemGrabResultPrivate *d = result->d_func();
auto sharedResult = QSharedPointer<QQuickItemGrabResult>(result);
result->setParent(nullptr); // the smart pointer now manages the lifetime
auto weakResult = QWeakPointer(sharedResult);
QMutexLocker locker(&d->mutex);
d->setupConnection = connect(window(), &QQuickWindow::beforeSynchronizing,
result, [weakResult] {
if (auto strong = weakResult.toStrongRef(); strong)
strong->setup();
}, Qt::DirectConnection);
d->renderConnection = connect(window(), &QQuickWindow::afterRendering,
result, [weakResult] {
if (auto strong = weakResult.toStrongRef(); strong)
strong->render();
}, Qt::DirectConnection);
return sharedResult;
}
/*!
* \qmlmethod bool QtQuick::Item::grabToImage(callback, targetSize)
*
* Grabs the item into an in-memory image.
*
* The grab happens asynchronously and the JavaScript function \a callback is
* invoked when the grab is completed. The callback takes one argument, which
* is the result of the grab operation; an \l ItemGrabResult object.
*
* Use \a targetSize to specify the size of the target image. By default, the result
* will have the same size as the item.
*
* If the grab could not be initiated, the function returns \c false.
*
* The following snippet shows how to grab an item and store the results in
* a file:
*
* \snippet qml/item/itemGrab.qml grab-to-file
*
* The following snippet shows how to grab an item and use the results in
* another image element:
*
* \snippet qml/item/itemGrab.qml grab-to-image
*
* \note This function will render the item to an offscreen surface and
* copy that surface from the GPU's memory into the CPU's memory, which can
* be quite costly. For "live" preview, use \l {QtQuick::Item::layer.enabled} {layers}
* or ShaderEffectSource.
*/
/*!
* \internal
* Only visible from QML.
*/
bool QQuickItem::grabToImage(const QJSValue &callback, const QSize &targetSize)
{
QQmlEngine *engine = qmlEngine(this);
if (!engine) {
qmlWarning(this) << "grabToImage: item has no QML engine";
return false;
}
if (!callback.isCallable()) {
qmlWarning(this) << "grabToImage: 'callback' is not a function";
return false;
}
const QSize validationSize = targetSize.isEmpty() ? QSize(width(), height()) : targetSize;
if (validationSize.width() < 1 || validationSize.height() < 1) {
qmlWarning(this) << "grabToImage: item's width and/or height are less than 1: "
<< validationSize;
return false;
}
if (!window()) {
qmlWarning(this) << "grabToImage: item is not attached to a window";
return false;
}
QQuickItemGrabResult *result = QQuickItemGrabResultPrivate::create(this, targetSize);
if (!result)
return false;
// Do not transfer ownership to JavaScript here, yet. There is no reference to the object
// We don't want the GC to collect it while the grab is still in flight.
QQuickItemGrabResultPrivate *d = result->d_func();
QMutexLocker locker(&d->mutex);
d->qmlEngine = engine;
d->callback = callback;
d->setupConnection = connect(window(), &QQuickWindow::beforeSynchronizing,
result, &QQuickItemGrabResult::setup, Qt::DirectConnection);
d->renderConnection = connect(window(), &QQuickWindow::afterRendering,
result, &QQuickItemGrabResult::render, Qt::DirectConnection);
return true;
}
QT_END_NAMESPACE
#include "moc_qquickitemgrabresult.cpp"