diff options
| author | Eskil Abrahamsen Blomfeldt <[email protected]> | 2025-11-21 13:11:52 +0100 |
|---|---|---|
| committer | Eskil Abrahamsen Blomfeldt <[email protected]> | 2025-12-04 21:01:18 +0100 |
| commit | fdba000bcd0b34740b919d9d2d62eaf056aa434c (patch) | |
| tree | cb78a9f2eafa36cbcbbf18188fa12658e99ba1b0 /src | |
| parent | 1460f39ba98a590027bac012ea08caa9fa83b225 (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.txt | 1 | ||||
| -rw-r--r-- | src/quickvectorimage/generator/qquicknodeinfo_p.h | 18 | ||||
| -rw-r--r-- | src/quickvectorimage/generator/qquickqmlgenerator.cpp | 198 | ||||
| -rw-r--r-- | src/quickvectorimage/generator/qsvgvisitorimpl.cpp | 70 | ||||
| -rw-r--r-- | src/quickvectorimage/helpers/shaders_ng/fecolormatrix.frag | 77 |
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; +} |
