aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorEskil Abrahamsen Blomfeldt <[email protected]>2025-11-21 13:11:52 +0100
committerEskil Abrahamsen Blomfeldt <[email protected]>2025-12-04 21:01:18 +0100
commitfdba000bcd0b34740b919d9d2d62eaf056aa434c (patch)
treecb78a9f2eafa36cbcbbf18188fa12658e99ba1b0 /src
parent1460f39ba98a590027bac012ea08caa9fa83b225 (diff)
VectorImage: Add support for more filters
This adds support for the feColorMatrix, feOffset and feFlood filters. We also need to change how we calculate the bounds of post-processed items to use the internal bounds. Basically, we need the bounds of the children of the item + the item itself before the node's transform is applied. This is because the bounding box is axis aligned, so we cannot get the original rectangle back just by applying the inverse transform in the case of e.g. rotations. Task-number: QTBUG-121541 Change-Id: I7a8863bef4397df4043c854256dca312b91f888a Reviewed-by: Eirik Aavitsland <[email protected]> Reviewed-by: Hatem ElKharashy <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/quickvectorimage/CMakeLists.txt1
-rw-r--r--src/quickvectorimage/generator/qquicknodeinfo_p.h18
-rw-r--r--src/quickvectorimage/generator/qquickqmlgenerator.cpp198
-rw-r--r--src/quickvectorimage/generator/qsvgvisitorimpl.cpp70
-rw-r--r--src/quickvectorimage/helpers/shaders_ng/fecolormatrix.frag77
5 files changed, 311 insertions, 53 deletions
diff --git a/src/quickvectorimage/CMakeLists.txt b/src/quickvectorimage/CMakeLists.txt
index ba2a671fdd..36e6d703d2 100644
--- a/src/quickvectorimage/CMakeLists.txt
+++ b/src/quickvectorimage/CMakeLists.txt
@@ -66,4 +66,5 @@ qt_internal_add_shaders(QuickVectorImageHelpers "vectorimage_shaders"
"/qt-project.org/quickvectorimage/"
FILES
"helpers/shaders_ng/genericmask.frag"
+ "helpers/shaders_ng/fecolormatrix.frag"
)
diff --git a/src/quickvectorimage/generator/qquicknodeinfo_p.h b/src/quickvectorimage/generator/qquicknodeinfo_p.h
index 100222668f..2f4f522ab0 100644
--- a/src/quickvectorimage/generator/qquicknodeinfo_p.h
+++ b/src/quickvectorimage/generator/qquicknodeinfo_p.h
@@ -164,16 +164,26 @@ struct FilterNodeInfo : NodeInfo
{
enum class Type {
None,
- GaussianBlur
+ GaussianBlur,
+ ColorMatrix,
+ Offset,
+ Flood
+ };
+
+ enum class CoordinateSystem {
+ Absolute,
+ Relative,
+ MatchFilterRect // Special case
};
QRectF filterRect;
- bool isFilterRectRelativeCoordinates = false;
- bool isFilterParameterRelative = false;
+ CoordinateSystem csFilterRect = CoordinateSystem::Absolute;
+ CoordinateSystem csFilterParameter = CoordinateSystem::Absolute;
QSGTexture::WrapMode wrapMode = QSGTexture::ClampToEdge;
Type filterType = Type::None;
- qreal filterParameter = 0.0;
+ QRectF filterPrimitiveRect;
+ QVariant filterParameter;
};
}
diff --git a/src/quickvectorimage/generator/qquickqmlgenerator.cpp b/src/quickvectorimage/generator/qquickqmlgenerator.cpp
index 05142b64d9..6faedf08f8 100644
--- a/src/quickvectorimage/generator/qquickqmlgenerator.cpp
+++ b/src/quickvectorimage/generator/qquickqmlgenerator.cpp
@@ -342,10 +342,10 @@ void QQuickQmlGenerator::generateShaderUse(const NodeInfo &info)
stream() << "property var filterSourceItem: " << seId;
stream() << "sourceComponent: " << info.filterId << "_container";
- stream() << "property real sourceX: " << seId << ".sourceRect.x";
- stream() << "property real sourceY: " << seId << ".sourceRect.y";
- stream() << "width: " << seId << ".sourceRect.width";
- stream() << "height: " << seId << ".sourceRect.height";
+ stream() << "property real sourceX: " << info.id << ".originalBounds.x";
+ stream() << "property real sourceY: " << info.id << ".originalBounds.y";
+ stream() << "width: " << info.id << ".originalBounds.width";
+ stream() << "height: " << info.id << ".originalBounds.height";
if (hasMask) {
m_indentLevel--;
@@ -372,7 +372,12 @@ void QQuickQmlGenerator::generateShaderUse(const NodeInfo &info)
stream() << "}";
stream() << "textureSize: " << maskId << "_itemspy.requiredTextureSize";
- stream() << "sourceRect: " << info.maskId << ".maskRect(" << info.id << ")";
+ stream() << "sourceRect: " << info.maskId << ".maskRect("
+ << info.id << ".originalBounds.x,"
+ << info.id << ".originalBounds.y,"
+ << info.id << ".originalBounds.width,"
+ << info.id << ".originalBounds.height)";
+
stream() << "width: sourceRect.width";
stream() << "height: sourceRect.height";
@@ -399,10 +404,17 @@ void QQuickQmlGenerator::generateShaderUse(const NodeInfo &info)
else
stream() << "sourceItem: " << info.id;
stream() << "textureSize: " << info.id << "_masked_se_itemspy.requiredTextureSize";
- if (!hasFilters)
- stream() << "sourceRect: " << info.maskId << ".maskRect(" << info.id << ")";
- else
- stream() << "sourceRect: Qt.rect(0, 0, " << effectId << ".width, " << effectId << ".height)";
+ if (!hasFilters) {
+ stream() << "sourceRect: " << info.maskId << ".maskRect("
+ << info.id << ".originalBounds.x,"
+ << info.id << ".originalBounds.y,"
+ << info.id << ".originalBounds.width,"
+ << info.id << ".originalBounds.height)";
+ } else {
+ stream() << "sourceRect: " << info.maskId << ".maskRect(0, 0,"
+ << info.id << ".originalBounds.width,"
+ << info.id << ".originalBounds.height)";
+ }
stream() << "width: sourceRect.width";
stream() << "height: sourceRect.height";
stream() << "smooth: false";
@@ -1490,17 +1502,17 @@ bool QQuickQmlGenerator::generateMaskNode(const MaskNodeInfo &info)
stream() << "property real maskWidth: " << info.maskRect.width();
stream() << "property real maskHeight: " << info.maskRect.height();
- stream() << "function maskRect(other) {";
+ stream() << "function maskRect(otherX, otherY, otherWidth, otherHeight) {";
m_indentLevel++;
stream() << "return ";
if (info.isMaskRectRelativeCoordinates) {
stream(SameLine)
<< "Qt.rect("
- << info.id << ".maskX * other.originalBounds.width + other.originalBounds.x,"
- << info.id << ".maskY * other.originalBounds.height + other.originalBounds.y,"
- << info.id << ".maskWidth * other.originalBounds.width,"
- << info.id << ".maskHeight * other.originalBounds.height)";
+ << info.id << ".maskX * otherWidth + otherX,"
+ << info.id << ".maskY * otherHeight + otherY,"
+ << info.id << ".maskWidth * otherWidth,"
+ << info.id << ".maskHeight * otherHeight)";
} else {
stream(SameLine)
<< "Qt.rect("
@@ -1533,21 +1545,22 @@ void QQuickQmlGenerator::generateFilterNode(const FilterNodeInfo &info)
else
stream(SameLine) << "ShaderEffectSource.ClampToEdge";
+ stream() << "property rect filterRect: Qt.rect("
+ << info.filterRect.x() << ", "
+ << info.filterRect.y() << ", "
+ << info.filterRect.width() << ", "
+ << info.filterRect.height() << ")";
+
stream() << "function adaptToFilterRect(sx, sy, sw, sh) {";
m_indentLevel++;
- if (!info.isFilterRectRelativeCoordinates) {
- stream() << "return Qt.rect("
- << info.filterRect.x() << ", "
- << info.filterRect.y() << ", "
- << info.filterRect.width() << ", "
- << info.filterRect.height() << ")";
+ // If the shader requires an offset to the source rect, we apply that here
+ if (info.csFilterRect == FilterNodeInfo::CoordinateSystem::Absolute) {
+ stream() << "return Qt.rect(filterRect.x, filterRect.y, filterRect.width, filterRect.height)";
} else {
stream() << "return Qt.rect("
- << "sx + sw * " << info.filterRect.x() << ", "
- << "sy + sh * " << info.filterRect.y() << ", "
- << "sw * " << info.filterRect.width() << ", "
- << "sh * " << info.filterRect.height() << ")";
+ << "sx + sw * filterRect.x, sy + sh * filterRect.y,"
+ << "sw * filterRect.width, sh * filterRect.height)";
}
m_indentLevel--;
@@ -1561,36 +1574,161 @@ void QQuickQmlGenerator::generateFilterNode(const FilterNodeInfo &info)
stream() << "id: " << info.id << "_container";
+ // Container for transform and effects
+ stream() << "Item {";
+ m_indentLevel++;
+
+ stream() << "property real originalWidth: filterSourceItem.sourceItem.originalBounds.width";
+ stream() << "property real originalHeight: filterSourceItem.sourceItem.originalBounds.height";
+ stream() << "property rect filterRect: " << info.id << "_filterParameters"
+ << ".adaptToFilterRect(0, 0, originalWidth, originalHeight)";
+
+ generateNodeBase(info);
+
+ QString primitiveId = info.id + QStringLiteral("_primitive");
+ QString offsetStr = QStringLiteral("Qt.vector2d(0, 0)");
+
switch (info.filterType) {
+ case FilterNodeInfo::Type::Flood:
+ {
+ stream() << "Rectangle {";
+ m_indentLevel++;
+
+ stream() << "id: " << primitiveId;
+
+ stream() << "width: filterSourceItem.sourceRect.width";
+ stream() << "height: filterSourceItem.sourceRect.height";
+
+ QColor floodColor = info.filterParameter.value<QColor>();
+ stream() << "color: \"" << floodColor.name(QColor::HexArgb) << "\"";
+
+ m_indentLevel--;
+ stream() << "}";
+
+ break;
+ }
+ case FilterNodeInfo::Type::ColorMatrix:
+ {
+ stream() << "ShaderEffect {";
+ m_indentLevel++;
+
+ stream() << "id: " << primitiveId;
+
+ stream() << "fragmentShader: \"qrc:/qt-project.org/quickvectorimage/helpers/shaders_ng/fecolormatrix.frag.qsb\"";
+ stream() << "property var source: filterSourceItem";
+ stream() << "width: filterSourceItem.sourceRect.width";
+ stream() << "height: filterSourceItem.sourceRect.height";
+
+ QGenericMatrix<5, 5, qreal> matrix = info.filterParameter.value<QGenericMatrix<5, 5, qreal> >();
+ for (int row = 0; row < 4; ++row) { // Last row is ignored
+
+ // Qt SVG stores rows as columns, so we flip the coordinates
+ for (int col = 0; col < 5; ++col)
+ stream() << "property real m_" << row << "_" << col << ": " << matrix(col, row);
+ }
+
+ m_indentLevel--;
+ stream() << "}";
+
+ break;
+ }
+
+ case FilterNodeInfo::Type::Offset:
+ {
+ QVector2D offset = info.filterParameter.value<QVector2D>();
+ offsetStr = (info.csFilterParameter == FilterNodeInfo::CoordinateSystem::Absolute
+ ? QStringLiteral("Qt.vector2d(%1 / filterSourceItem.width, %2 / filterSourceItem.height)")
+ : QStringLiteral("Qt.vector2d(%1, %2)")).arg(offset.x()).arg(offset.y());
+ primitiveId = QStringLiteral("filterSourceItem");
+
+ break;
+ }
+
case FilterNodeInfo::Type::GaussianBlur:
{
// Approximate blur effect with fast blur
stream() << "MultiEffect {";
m_indentLevel++;
- generateNodeBase(info);
+
+ stream() << "id: " << primitiveId;
stream() << "source: filterSourceItem";
stream() << "blurEnabled: true";
+ stream() << "width: filterSourceItem.sourceRect.width";
+ stream() << "height: filterSourceItem.sourceRect.height";
const qreal maxDeviation(12.0); // Decided experimentally
- if (info.isFilterParameterRelative)
- stream() << "blur: Math.min(1.0, " << info.filterParameter << " * filterSourceItem.width / " << maxDeviation << ")";
+ const qreal deviation = info.filterParameter.toReal();
+ if (info.csFilterParameter == FilterNodeInfo::CoordinateSystem::Relative)
+ stream() << "blur: Math.min(1.0, " << deviation << " * filterSourceItem.width / " << maxDeviation << ")";
else
- stream() << "blur: " << std::min(qreal(1.0), info.filterParameter / maxDeviation);
+ stream() << "blur: " << std::min(qreal(1.0), deviation / maxDeviation);
stream() << "blurMax: 64";
- generateNodeEnd(info);
+ m_indentLevel--;
+ stream() << "}";
+
break;
}
default:
qCWarning(lcQuickVectorImage) << "Unhandled filter type: " << int(info.filterType);
// Dummy item to avoid empty component
- stream() << "Item{}";
+ stream() << "Item { id: " << primitiveId << " }";
break;
}
+ // Sample correct part of primitive
+ stream() << "ShaderEffectSource {";
+ m_indentLevel++;
+
+ qreal x1, x2, y1, y2;
+ info.filterPrimitiveRect.getCoords(&x1, &y1, &x2, &y2);
+ if (info.csFilterParameter == FilterNodeInfo::CoordinateSystem::Absolute) {
+ stream() << "property real fpx1: " << x1;
+ stream() << "property real fpy1: " << y1;
+ stream() << "property real fpx2: " << x2;
+ stream() << "property real fpy2: " << y2;
+ } else if (info.csFilterParameter == FilterNodeInfo::CoordinateSystem::Relative) {
+ // If they are relative, they are actually in the coordinate system
+ // of the original bounds of the filtered item. This means we first have to convert
+ // them to the filter's coordinate system first.
+ stream() << "property real fpx1: " << x1 << " * filterSourceItem.sourceItem.originalBounds.width";
+ stream() << "property real fpy1: " << y1 << " * filterSourceItem.sourceItem.originalBounds.height";
+ stream() << "property real fpx2: " << x2 << " * filterSourceItem.sourceItem.originalBounds.width";
+ stream() << "property real fpy2: " << y2 << " * filterSourceItem.sourceItem.originalBounds.height";
+ } else { // Just match filter rect
+ stream() << "property real fpx1: filterRect.x";
+ stream() << "property real fpy1: filterRect.y";
+ stream() << "property real fpx2: filterRect.x + filterRect.width";
+ stream() << "property real fpy2: filterRect.y + filterRect.height";
+ }
+
+ stream() << "sourceItem: " << primitiveId;
+ stream() << "hideSource: true";
+ stream() << "property vector2d offset: " << offsetStr;
+ stream() << "sourceRect: Qt.rect(fpx1 - filterRect.x - offset.x, fpy1 - filterRect.y - offset.y, width, height)";
+
+ stream() << "x: fpx1";
+ stream() << "y: fpy1";
+ stream() << "width: " << "fpx2 - fpx1 + offset.x";
+ stream() << "height: " << "fpy2 - fpy1 + offset.y";
+
+ stream() << "ItemSpy {";
+ m_indentLevel++;
+ stream() << "id: " << primitiveId << "_itemspy";
+ stream() << "anchors.fill: parent";
+
+ m_indentLevel--;
+ stream() << "}";
+ stream() << "textureSize: " << primitiveId << "_itemspy.requiredTextureSize";
+
m_indentLevel--;
stream() << "}";
+
+ generateNodeEnd(info);
+
+ m_indentLevel--;
+ stream() << "}"; // End of Component
}
bool QQuickQmlGenerator::generateRootNode(const StructureNodeInfo &info)
diff --git a/src/quickvectorimage/generator/qsvgvisitorimpl.cpp b/src/quickvectorimage/generator/qsvgvisitorimpl.cpp
index ca94337463..8b4631395a 100644
--- a/src/quickvectorimage/generator/qsvgvisitorimpl.cpp
+++ b/src/quickvectorimage/generator/qsvgvisitorimpl.cpp
@@ -1033,12 +1033,15 @@ void QSvgVisitorImpl::visitUseNode(const QSvgUse *node)
handleBaseNodeSetup(node);
UseNodeInfo info;
+ QPointF startPos = node->start();
+
fillCommonNodeInfo(node, info);
fillAnimationInfo(node, info);
+ if (!info.bounds.isNull())
+ info.bounds.translate(-startPos);
info.stage = StructureNodeStage::Start;
- QPointF startPos = node->start();
if (!startPos.isNull()) {
QTransform xform;
if (!info.isDefaultTransform)
@@ -1201,12 +1204,39 @@ void QSvgVisitorImpl::visitFilterNodeEnd(const QSvgFilterContainer *node)
fillCommonNodeInfo(node, info);
info.filterRect = node->rect();
- info.isFilterRectRelativeCoordinates =
- node->rect().unitW() == QtSvg::UnitTypes::objectBoundingBox;
- info.isFilterParameterRelative =
- node->primitiveUnits() == QtSvg::UnitTypes::objectBoundingBox;
+ info.filterPrimitiveRect = filterPrimitive->rect();
+ if (node->filterUnits() == QtSvg::UnitTypes::objectBoundingBox)
+ info.csFilterRect = FilterNodeInfo::CoordinateSystem::Relative;
+ if (node->primitiveUnits() == QtSvg::UnitTypes::objectBoundingBox)
+ info.csFilterParameter = FilterNodeInfo::CoordinateSystem::Relative;
+
+ // We special-case the default filter primitive rect as is done in Qt Svg.
+ // The default is to match the filter's rect and this is represented by making
+ // the types of the filter primitive's rect QtSvg::UnitTypes::unknown. Since
+ // this is not generally handled in Qt Svg, we also just special case it here.
+ if (node->primitiveUnits() == QtSvg::UnitTypes::userSpaceOnUse
+ && filterPrimitive->rect().unitW() == QtSvg::UnitTypes::unknown) {
+ info.csFilterParameter = FilterNodeInfo::CoordinateSystem::MatchFilterRect;
+ }
switch (filterPrimitive->type()) {
+ case QSvgNode::FeOffset:
+ {
+ const QSvgFeOffset *offset = static_cast<const QSvgFeOffset *>(filterPrimitive);
+ info.filterType = FilterNodeInfo::Type::Offset;
+ info.filterParameter = QVariant::fromValue(QVector2D(offset->dx(), offset->dy()));
+ break;
+
+ }
+ case QSvgNode::FeColormatrix:
+ {
+ const QSvgFeColorMatrix *colorMatrix =
+ static_cast<const QSvgFeColorMatrix *>(filterPrimitive);
+ info.filterType = FilterNodeInfo::Type::ColorMatrix;
+ info.filterParameter = QVariant::fromValue(colorMatrix->matrix());
+ break;
+ }
+
case QSvgNode::FeGaussianblur:
{
const QSvgFeGaussianBlur *gaussianBlur =
@@ -1219,16 +1249,25 @@ void QSvgVisitorImpl::visitFilterNodeEnd(const QSvgFilterContainer *node)
qCWarning(lcQuickVectorImage) << "Separate X and Y deviations not supported for gaussian blur";
info.filterParameter = std::max(gaussianBlur->stdDeviationX(),
gaussianBlur->stdDeviationY());
- m_generator->generateFilterNode(info);
+ break;
+ }
+
+ case QSvgNode::FeFlood:
+ {
+ const QSvgFeFlood *flood =
+ static_cast<const QSvgFeFlood *>(filterPrimitive);
+
+ info.filterType = FilterNodeInfo::Type::Flood;
+ info.filterParameter = flood->color();
break;
}
default:
// Create a dummy filter node to make sure bindings still work for unsupported filters
info.filterType = FilterNodeInfo::Type::None;
- m_generator->generateFilterNode(info);
break;
}
+ m_generator->generateFilterNode(info);
m_filterPrimitives.clear();
}
@@ -1343,18 +1382,11 @@ void QSvgVisitorImpl::fillCommonNodeInfo(const QSvgNode *node, NodeInfo &info, c
info.isDisplayed = node->displayMode() != QSvgNode::DisplayMode::NoneMode;
if (node->hasFilter() || node->hasMask() || node->type() == QSvgNode::Type::Mask) {
- info.bounds = node->bounds();
-
- if (!xf.isIdentity()) {
- bool ok;
- xf = xf.inverted(&ok);
- if (ok) {
- info.bounds = xf.mapRect(info.bounds);
- } else {
- qCWarning(lcQuickVectorImage)
- << "Masked item with non-invertible transform currently not supported.";
- }
- }
+ QImage dummy(1, 1, QImage::Format_RGB32);
+ QPainter p(&dummy);
+ QSvgExtraStates states;
+ p.setPen(QPen(Qt::NoPen));
+ info.bounds = node->internalBounds(&p, states);
}
if (node->hasMask()) {
diff --git a/src/quickvectorimage/helpers/shaders_ng/fecolormatrix.frag b/src/quickvectorimage/helpers/shaders_ng/fecolormatrix.frag
new file mode 100644
index 0000000000..9589d6b771
--- /dev/null
+++ b/src/quickvectorimage/helpers/shaders_ng/fecolormatrix.frag
@@ -0,0 +1,77 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+#version 440
+
+layout(location = 0) in vec2 qt_TexCoord0;
+layout(location = 0) out vec4 fragColor;
+layout(std140, binding = 0) uniform buf {
+ // qt_Matrix and qt_Opacity must always be both present
+ // if the built-in vertex shader is used.
+ mat4 qt_Matrix;
+ float qt_Opacity;
+
+ // red
+ float m_0_0;
+ float m_0_1;
+ float m_0_2;
+ float m_0_3;
+ float m_0_4;
+
+ // green
+ float m_1_0;
+ float m_1_1;
+ float m_1_2;
+ float m_1_3;
+ float m_1_4;
+
+ // blue
+ float m_2_0;
+ float m_2_1;
+ float m_2_2;
+ float m_2_3;
+ float m_2_4;
+
+ // alpha
+ float m_3_0;
+ float m_3_1;
+ float m_3_2;
+ float m_3_3;
+ float m_3_4;
+};
+
+layout(binding = 1) uniform sampler2D source;
+
+void main()
+{
+ vec4 color = texture(source, qt_TexCoord0.st);
+
+ // Un-premultiply, avoiding divide by zero
+ float epsilon = 0.0000001;
+ color.rgb /= max(epsilon, color.a);
+
+ float red = color.r * m_0_0
+ + color.g * m_0_1
+ + color.b * m_0_2
+ + color.a * m_0_3
+ + m_0_4;
+
+ float green = color.r * m_1_0
+ + color.g * m_1_1
+ + color.b * m_1_2
+ + color.a * m_1_3
+ + m_1_4;
+
+ float blue = color.r * m_2_0
+ + color.g * m_2_1
+ + color.b * m_2_2
+ + color.a * m_2_3
+ + m_2_4;
+
+ float alpha = color.r * m_3_0
+ + color.g * m_3_1
+ + color.b * m_3_2
+ + color.a * m_3_3
+ + m_3_4;
+
+ fragColor = vec4(red, green, blue, 1.0) * alpha;
+}