Revision control

Copy as Markdown

// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qcanvas2dcontext_p.h"
#include "qcanvas2ditem_p.h"
#include "qcanvas2ditemrenderer_p.h"
#include <QtQuick/private/qquickitem_p.h>
#include <QtQuick/private/qsgadaptationlayer_p.h>
#include <QtQuick/qsgtextureprovider.h>
#include <QtQuick/private/qquickpixmap_p.h>
#include <QtCore/QBuffer>
#include <QtCore/qdatetime.h>
#include <QtCore/qcoreapplication.h>
#include <QtQml/qqmlinfo.h>
#include <QtQml/private/qqmlengine_p.h>
#include <QtQml/private/qv4value_p.h>
#include <QtQml/private/qv4functionobject_p.h>
#include <QtQml/private/qv4scopedvalue_p.h>
#include <QtQml/private/qv4jscall_p.h>
#include <QtQml/private/qv4qobjectwrapper_p.h>
#include <QtQml/private/qjsvalue_p.h>
QT_BEGIN_NAMESPACE
QCanvas2DPixmap::QCanvas2DPixmap(const QImage& image)
: m_pixmap(nullptr)
, m_image(image)
{
}
QCanvas2DPixmap::QCanvas2DPixmap(QQuickPixmap *pixmap)
: m_pixmap(pixmap)
{
}
QCanvas2DPixmap::~QCanvas2DPixmap()
{
delete m_pixmap;
}
qreal QCanvas2DPixmap::width() const
{
if (m_pixmap)
return m_pixmap->width();
return m_image.width();
}
qreal QCanvas2DPixmap::height() const
{
if (m_pixmap)
return m_pixmap->height();
return m_image.height();
}
bool QCanvas2DPixmap::isValid() const
{
if (m_pixmap)
return m_pixmap->isReady();
return !m_image.isNull();
}
QImage QCanvas2DPixmap::image()
{
if (m_image.isNull() && m_pixmap)
m_image = m_pixmap->image();
return m_image;
}
QCanvas2DPainterItem::QCanvas2DPainterItem(QCanvas2DItem *canvas)
: QCanvasPainterItem(canvas)
, m_canvas(canvas)
{
setFlag(ItemHasContents);
}
QCanvasPainterItemRenderer* QCanvas2DPainterItem::createItemRenderer() const
{
auto *renderer = new QCanvas2DItemRenderer();
QObject::connect(renderer, &QCanvas2DItemRenderer::painted,
m_canvas, &QCanvas2DItem::painted);
return renderer;
}
class QCanvas2DItemPrivate : public QQuickItemPrivate
{
public:
QCanvas2DItemPrivate();
~QCanvas2DItemPrivate();
QCanvas2DPainterItem *painterItem = nullptr;
QCanvas2DContext *context = nullptr;
QString contextType;
QHash<QUrl, QQmlRefPointer<QCanvas2DPixmap> > pixmaps;
QUrl baseUrl;
QMap<int, QV4::PersistentValue> animationCallbacks;
QCanvas2DCommandBuffer *ccb = nullptr;
bool available = false;
QHash<uint, QCanvas2DItem::ImageData> imageData;
};
QCanvas2DItemPrivate::QCanvas2DItemPrivate()
: QQuickItemPrivate()
{
implicitAntialiasing = true;
}
QCanvas2DItemPrivate::~QCanvas2DItemPrivate()
{
pixmaps.clear();
}
/*!
\qmltype Canvas2D
\inqmlmodule QtCanvas2D
\since 6.12
\brief Provides a 2D canvas item enabling drawing via JavaScript.
The Canvas2D item allows drawing of straight and curved lines, simple and
complex shapes, graphs, and referenced graphic images. It can also add
text, colors, gradients, and patterns.
Rendering to the Canvas is done using a Canvas2DContext object, usually as a
result of the \l paint signal.
To define a drawing area in the Canvas2D item set the \c width and \c height
properties. For example, the following code creates a Canvas2D item which
has a drawing area with a height of 100 pixels and width of 200 pixels:
\qml
import QtCanvas2D
Canvas2D {
id: mycanvas
width: 100
height: 200
onPaint: {
var ctx = getContext("2d");
ctx.fillStyle = Qt.rgba(1, 0, 0, 1);
ctx.fillRect(0, 0, width, height);
}
}
\endqml
\section1 Tips for Porting Existing HTML5 Canvas Applications
Although the Canvas item provides an HTML5-like API, HTML5 canvas
applications need to be modified to run in the Canvas item:
\list
\li Replace all DOM API calls with QML property bindings or Canvas item methods.
\li Replace all HTML event handlers with the MouseArea item.
\li Change setInterval/setTimeout function calls with the \l FrameAnimation item or
the use of requestAnimationFrame().
\li Place painting code into the \c onPaint handler and trigger
painting by calling the markDirty() or requestPaint() methods.
\li To draw images, load them by calling the Canvas's loadImage() method and then request to paint
them in the \c onImageLoaded handler.
\endlist
\sa Canvas2DContext, QCanvasPainterItem, QCanvasPainter
*/
QCanvas2DItem::QCanvas2DItem(QQuickItem *parent)
: QQuickItem(*(new QCanvas2DItemPrivate), parent)
{
Q_D(QCanvas2DItem);
d->painterItem = new QCanvas2DPainterItem(this);
d->painterItem->setAntialiasing(antialiasing());
connect(this, &QQuickItem::antialiasingChanged, d->painterItem, [this]() {
d_func()->painterItem->setAntialiasing(antialiasing());
});
// TODO: Should we default to performance like QCanvasPainterItem
// and remove these, or compatibity with HTLM/Quick Canvas and
// enable these to be the default?
//setAlphaBlending(true);
//setFillColor(Qt::transparent);
}
QCanvas2DItem::~QCanvas2DItem()
{
Q_D(QCanvas2DItem);
delete d->context;
}
/*!
\qmlproperty color Canvas2D::fillColor
The color to use for filling the item ie. the item background.
The default color is \c black. To not have background for Canvas2D,
set this to 'transparent' and \l alphaBlending to \c true.
\sa alphaBlending, QCanvasPainterItem::fillColor
*/
QColor QCanvas2DItem::fillColor() const
{
Q_D(const QCanvas2DItem);
return d->painterItem->fillColor();
}
void QCanvas2DItem::setFillColor(QColor color)
{
Q_D(QCanvas2DItem);
if (fillColor() == color)
return;
d->painterItem->setFillColor(color);
Q_EMIT fillColorChanged();
}
/*!
\qmlproperty bool Canvas2D::alphaBlending
Controls if blending is always enabled when drawing the Canvas2D item.
The default value is \c false. This is for performance reasons: if
semi-transparency is not involved, because the \l fillColor clears
to an opaque color, then there is no point in enabling blending.
If the \l fillColor has semi-transparency, set this property to \c true.
\sa fillColor, QQuickRhiItem::alphaBlending
*/
bool QCanvas2DItem::alphaBlending() const
{
Q_D(const QCanvas2DItem);
return d->painterItem->alphaBlending();
}
void QCanvas2DItem::setAlphaBlending(bool enable)
{
Q_D(QCanvas2DItem);
if (alphaBlending() == enable)
return;
d->painterItem->setAlphaBlending(enable);
Q_EMIT alphaBlendingChanged();
}
/*!
\qmlproperty bool Canvas2D::available
Indicates when Canvas is able to provide a drawing context to operate on.
*/
bool QCanvas2DItem::isAvailable() const
{
return d_func()->available;
}
/*!
\qmlproperty string Canvas2D::contextType
The type of drawing context to use.
This property is set to the name of the active context type.
If set explicitly the canvas will attempt to create a context of the
named type after becoming available.
The type name is the same as used in the getContext() call, for the 2d
canvas the value will be "2d".
\sa getContext(), available
*/
QString QCanvas2DItem::contextType() const
{
return d_func()->contextType;
}
void QCanvas2DItem::setContextType(const QString &contextType)
{
Q_D(QCanvas2DItem);
if (contextType.compare(d->contextType, Qt::CaseInsensitive) == 0)
return;
if (d->context) {
qmlWarning(this) << "Canvas already initialized with a different context type";
return;
}
d->contextType = contextType;
if (d->available)
createContext(contextType);
emit contextTypeChanged();
}
/*!
\qmlproperty object Canvas2D::context
Holds the active drawing context.
If the canvas is ready and there has been a successful call to getContext()
or the contextType property has been set with a supported context type,
this property will contain the current drawing context, otherwise null.
*/
QJSValue QCanvas2DItem::context() const
{
Q_D(const QCanvas2DItem);
return d->context ? QJSValuePrivate::fromReturnedValue(d->context->v4value()) : QJSValue();
}
bool QCanvas2DItem::isPaintConnected()
{
IS_SIGNAL_CONNECTED(this, QCanvas2DItem, paint, ());
}
void QCanvas2DItem::sceneGraphInitialized()
{
Q_D(QCanvas2DItem);
d->available = true;
connect(this, SIGNAL(visibleChanged()), SLOT(checkAnimationCallbacks()));
QMetaObject::invokeMethod(this, "availableChanged", Qt::QueuedConnection);
if (!d->contextType.isNull())
QMetaObject::invokeMethod(this, "delayedCreate", Qt::QueuedConnection);
else if (isPaintConnected())
QMetaObject::invokeMethod(this, "requestPaint", Qt::QueuedConnection);
}
void QCanvas2DItem::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
{
Q_D(QCanvas2DItem);
QQuickItem::geometryChange(newGeometry, oldGeometry);
// Due to indirect recursion, newGeometry may be outdated
// after this call, so we use width and height instead.
QSizeF newSize = QSizeF(width(), height());
d->painterItem->setSize(newSize);
if (d->available && newSize != oldGeometry.size()) {
if (isVisible() || (d->extra.isAllocated() && d->extra->effectRefCount > 0))
requestPaint();
}
}
void QCanvas2DItem::releaseResources()
{
Q_D(QCanvas2DItem);
if (d->context) {
delete d->context;
d->context = nullptr;
}
}
bool QCanvas2DItem::event(QEvent *event)
{
switch (event->type()) {
case QEvent::PolishRequest:
polish();
return true;
default:
return QQuickItem::event(event);
}
}
bool QCanvas2DItem::isTextureProvider() const
{
Q_D(const QCanvas2DItem);
return d->painterItem->isTextureProvider();
}
QSGTextureProvider *QCanvas2DItem::textureProvider() const
{
Q_D(const QCanvas2DItem);
return d->painterItem->textureProvider();
}
void QCanvas2DItem::invalidateSceneGraph()
{
Q_D(QCanvas2DItem);
if (d->context)
d->context->deleteLater();
d->context = nullptr;
// As we can expect(/hope) that the SG will be "good again", we can requestPaint
// Otherwise this Canvas will be "blank" when SG comes back
requestPaint();
}
void QCanvas2DItem::schedulePolish()
{
auto polishRequestEvent = new QEvent(QEvent::PolishRequest);
QCoreApplication::postEvent(this, polishRequestEvent);
}
void QCanvas2DItem::componentComplete()
{
QQuickItem::componentComplete();
Q_D(QCanvas2DItem);
d->baseUrl = qmlEngine(this)->contextForObject(this)->baseUrl();
}
void QCanvas2DItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
{
QQuickItem::itemChange(change, value);
if (change != QQuickItem::ItemSceneChange)
return;
Q_D(QCanvas2DItem);
if (d->available) {
if (d->dirtyAttributes & QQuickItemPrivate::ContentUpdateMask)
requestPaint();
return;
}
if (value.window == nullptr)
return;
d->window = value.window;
QSGRenderContext *context = QQuickWindowPrivate::get(d->window)->context;
if (context) {
// Defer the call. In some (arguably incorrect) cases we get here due
// to ItemSceneChange with the user-supplied property values not yet
// set. Work this around by a deferred invoke. (QTBUG-49692)
QMetaObject::invokeMethod(this, "sceneGraphInitialized", Qt::QueuedConnection);
} else {
connect(d->window, SIGNAL(sceneGraphInitialized()), SLOT(sceneGraphInitialized()));
}
}
void QCanvas2DItem::updatePolish()
{
QQuickItem::updatePolish();
Q_D(QCanvas2DItem);
if (d->animationCallbacks.size() > 0 && isVisible()) {
QMap<int, QV4::PersistentValue> animationCallbacks = d->animationCallbacks;
d->animationCallbacks.clear();
QV4::ExecutionEngine *v4 = qmlEngine(this)->handle();
QV4::Scope scope(v4);
QV4::ScopedFunctionObject function(scope);
QV4::JSCallArguments jsCall(scope, 1);
*jsCall.thisObject = QV4::QObjectWrapper::wrap(v4, this);
for (auto it = animationCallbacks.cbegin(), end = animationCallbacks.cend(); it != end; ++it) {
function = it.value().value();
jsCall.args[0] = QV4::Value::fromUInt32(QDateTime::currentMSecsSinceEpoch());
function->call(jsCall);
}
} else {
// TODO: Always call paint()?
emit paint();
}
if (d->context)
d->context->flush();
}
QCanvas2DCommandBuffer *QCanvas2DItem::ccb() const
{
Q_D(const QCanvas2DItem);
return d->ccb;
}
void QCanvas2DItem::setCcb(QCanvas2DCommandBuffer *ccb)
{
Q_D(QCanvas2DItem);
d->ccb = ccb;
d->painterItem->update();
}
/*!
\qmlmethod object Canvas2D::getContext(string contextId, ... args)
Returns a drawing context, or \c null if no context is available.
The \a contextId parameter names the required context. The Canvas2D item
will return a context that implements the required drawing mode. After the
first call to getContext, any subsequent call to getContext with the same
contextId will return the same context object. Any additional arguments
(\a args) are currently ignored.
If the context type is not supported or the canvas has previously been
requested to provide a different and incompatible context type, \c null
will be returned.
Canvas2D only supports a 2d context.
*/
void QCanvas2DItem::getContext(QQmlV4FunctionPtr args)
{
Q_D(QCanvas2DItem);
QV4::Scope scope(args->v4engine());
QV4::ScopedString str(scope, (*args)[0]);
if (!str) {
qmlWarning(this) << "getContext should be called with a string naming the required context type";
args->setReturnValue(QV4::Encode::null());
return;
}
if (!d->available) {
qmlWarning(this) << "Unable to use getContext() at this time, please wait for available: true";
args->setReturnValue(QV4::Encode::null());
return;
}
QString contextId = str->toQString();
if (d->context != nullptr) {
if (d->context->contextNames().contains(contextId, Qt::CaseInsensitive)) {
args->setReturnValue(d->context->v4value());
return;
}
qmlWarning(this) << "Canvas already initialized with a different context type";
args->setReturnValue(QV4::Encode::null());
return;
}
if (createContext(contextId))
args->setReturnValue(d->context->v4value());
else
args->setReturnValue(QV4::Encode::null());
}
/*!
\qmlmethod int Canvas2D::requestAnimationFrame(callback)
This function schedules \a callback to be invoked before composing the Qt Quick
scene.
*/
void QCanvas2DItem::requestAnimationFrame(QQmlV4FunctionPtr args)
{
QV4::Scope scope(args->v4engine());
QV4::ScopedFunctionObject f(scope, (*args)[0]);
if (!f) {
qmlWarning(this) << "requestAnimationFrame should be called with an animation callback function";
args->setReturnValue(QV4::Encode::null());
return;
}
Q_D(QCanvas2DItem);
static int id = 0;
d->animationCallbacks.insert(++id, QV4::PersistentValue(scope.engine, f->asReturnedValue()));
// QTBUG-55778: Calling polish directly here can lead to a polish loop
if (isVisible())
schedulePolish();
args->setReturnValue(QV4::Encode(id));
}
/*!
\qmlmethod void Canvas2D::cancelRequestAnimationFrame(int handle)
This function will cancel the animation callback referenced by \a handle.
*/
void QCanvas2DItem::cancelRequestAnimationFrame(QQmlV4FunctionPtr args)
{
QV4::Scope scope(args->v4engine());
QV4::ScopedValue v(scope, (*args)[0]);
if (!v->isInteger()) {
qmlWarning(this) << "cancelRequestAnimationFrame should be called with an animation callback id";
args->setReturnValue(QV4::Encode::null());
return;
}
d_func()->animationCallbacks.remove(v->integerValue());
}
/*!
\qmlmethod void Canvas2D::requestPaint()
Request the entire visible region be re-drawn.
\sa markDirty()
*/
void QCanvas2DItem::requestPaint()
{
markDirty();
}
/*!
\qmlmethod void Canvas2D::markDirty()
Marks the canvas as dirty. This will trigger the \c paint signal.
\sa paint, requestPaint()
*/
void QCanvas2DItem::markDirty()
{
Q_D(QCanvas2DItem);
if (!d->available)
return;
polish();
d->painterItem->polish();
}
void QCanvas2DItem::checkAnimationCallbacks()
{
if (d_func()->animationCallbacks.size() > 0 && isVisible())
polish();
}
QQmlRefPointer<QCanvas2DPixmap> QCanvas2DItem::loadedPixmap(const QUrl& url, QSizeF sourceSize)
{
Q_D(QCanvas2DItem);
QUrl fullPathUrl = d->baseUrl.resolved(url);
if (!d->pixmaps.contains(fullPathUrl)) {
loadImage(url, sourceSize);
}
return d->pixmaps.value(fullPathUrl);
}
/*!
\qmlsignal Canvas2D::imageLoaded()
This signal is emitted when an image has been loaded.
\sa loadImage()
*/
/*!
\qmlmethod void Canvas2D::loadImage(url image, size sourceSize = undefined)
Loads the given \a image asynchronously.
Once the image is ready, imageLoaded() signal will be emitted.
The loaded image can be unloaded with the unloadImage() method.
\note Only loaded images can be painted on the Canvas2D item.
If \a sourceSize is specified, the image will be scaled to that size during loading. This is
useful for loading scalable (vector) images (eg. SVGs) at their intended display size.
\sa unloadImage(), imageLoaded(), isImageLoaded(),
Context2D::createImageData(), Context2D::drawImage()
*/
void QCanvas2DItem::loadImage(const QUrl& url, QSizeF sourceSize)
{
Q_D(QCanvas2DItem);
QUrl fullPathUrl = d->baseUrl.resolved(url);
if (!d->pixmaps.contains(fullPathUrl)) {
QQuickPixmap* pix = new QQuickPixmap();
QQmlRefPointer<QCanvas2DPixmap> canvasPix;
canvasPix.adopt(new QCanvas2DPixmap(pix));
d->pixmaps.insert(fullPathUrl, canvasPix);
pix->load(qmlEngine(this)
, fullPathUrl
, QRect()
, sourceSize.toSize()
, QQuickPixmap::Cache | QQuickPixmap::Asynchronous);
if (pix->isLoading())
pix->connectFinished(this, SIGNAL(imageLoaded()));
}
}
/*!
\qmlmethod void Canvas2D::unloadImage(url image)
Unloads the \a image.
Once an image is unloaded, it cannot be painted by the canvas context
unless it is loaded again.
\sa loadImage(), imageLoaded(), isImageLoaded(),
Context2D::createImageData(), Context2D::drawImage
*/
void QCanvas2DItem::unloadImage(const QUrl& url)
{
Q_D(QCanvas2DItem);
d->pixmaps.remove(d->baseUrl.resolved(url));
}
/*!
\qmlmethod bool Canvas2D::isImageError(url image)
Returns \c true if the \a image failed to load, \c false otherwise.
\sa loadImage()
*/
bool QCanvas2DItem::isImageError(const QUrl& url) const
{
Q_D(const QCanvas2DItem);
QUrl fullPathUrl = d->baseUrl.resolved(url);
return d->pixmaps.contains(fullPathUrl)
&& d->pixmaps.value(fullPathUrl)->pixmap()->isError();
}
/*!
\qmlmethod bool Canvas2D::isImageLoading(url image)
Returns true if the \a image is currently loading.
\sa loadImage()
*/
bool QCanvas2DItem::isImageLoading(const QUrl& url) const
{
Q_D(const QCanvas2DItem);
QUrl fullPathUrl = d->baseUrl.resolved(url);
return d->pixmaps.contains(fullPathUrl)
&& d->pixmaps.value(fullPathUrl)->pixmap()->isLoading();
}
/*!
\qmlmethod bool Canvas2D::isImageLoaded(url image)
Returns true if the \a image is successfully loaded and ready to use.
\sa loadImage()
*/
bool QCanvas2DItem::isImageLoaded(const QUrl& url) const
{
Q_D(const QCanvas2DItem);
QUrl fullPathUrl = d->baseUrl.resolved(url);
return d->pixmaps.contains(fullPathUrl)
&& d->pixmaps.value(fullPathUrl)->pixmap()->isReady();
}
void QCanvas2DItem::delayedCreate()
{
Q_D(QCanvas2DItem);
if (!d->context && !d->contextType.isNull())
createContext(d->contextType);
requestPaint();
}
bool QCanvas2DItem::createContext(const QString &contextType)
{
Q_D(QCanvas2DItem);
if (!window())
return false;
if (contextType == QStringLiteral("2d")) {
if (d->contextType.compare(QStringLiteral("2d"), Qt::CaseInsensitive) != 0) {
d->contextType = QStringLiteral("2d");
emit contextTypeChanged(); // XXX: can't be in setContextType()
}
initializeContext(new QCanvas2DContext(this));
return true;
}
return false;
}
void QCanvas2DItem::initializeContext(QCanvas2DContext *context, const QVariantMap &args)
{
Q_D(QCanvas2DItem);
d->context = context;
d->context->init(this, args);
d->context->setV4Engine(qmlEngine(this)->handle());
emit contextChanged();
}
/*!
\qmlsignal Canvas2D::paint(rect region)
This signal is emitted when the \a region needs to be rendered. If a context
is active it can be referenced from the context property.
This signal can be triggered by markDirty(), requestPaint() or by changing
the current canvas window.
*/
/*!
\qmlsignal Canvas2D::painted()
This signal is emitted after all context painting commands are executed and
the Canvas has been rendered.
*/
void QCanvas2DItem::addImagePattern(const QCanvasImagePattern &pattern, const QString &url, const QImage &image, QCanvasPainter::ImageFlags flags)
{
Q_D(QCanvas2DItem);
// Add a new entry into imageData cache.
ImageData iData;
iData.pattern = pattern;
iData.image = image;
iData.flags = flags;
iData.url = url;
d->imageData.insert(pattern.serialNumber(), iData);
}
QHash<uint, QCanvas2DItem::ImageData> QCanvas2DItem::imageData() const
{
Q_D(const QCanvas2DItem);
return d->imageData;
}
void QCanvas2DItem::clearImageDataCache()
{
Q_D(QCanvas2DItem);
d->imageData.clear();
}
QT_END_NAMESPACE