aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEirik Aavitsland <eirik.aavitsland@qt.io>2026-08-31 11:34:53 +0200
committerEirik Aavitsland <eirik.aavitsland@qt.io>2026-09-14 21:22:28 +0000
commit71ee6061b73ef9c0312684d3d49f2e4f75fe4f6b (patch)
treecd9fada494c2837a9f57c5fd02b873b3e8affa7c
parent2fe514c09d656584d8dc3f005e2a8338f4d7853b (diff)
Vectorimage generator: handle SVG group opacity
In Qt Quick, semi-opacity on a parent item is implemented by rendering each child item individually with that opacity setting. In case of overlapping children, this gives a different result than what SVG expects. Work around this by using layered (intermediate texture) rendering on such items. To handle item transforms correctly, a wrapping item is introduced. As an optimization, layering is only enabled if there is more than one graphic child item. Fixes: QTBUG-149247 Pick-to: 6.12 6.11 6.8 Change-Id: Iaebb8cae0f950a68906a45901345cb8c3181b947 Reviewed-by: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>
-rw-r--r--src/quickvectorimage/generator/qquicknodeinfo_p.h2
-rw-r--r--src/quickvectorimage/generator/qquickqmlgenerator.cpp65
-rw-r--r--src/quickvectorimage/generator/qquickqmlgenerator_p.h9
-rw-r--r--src/quickvectorimage/generator/qsvgvisitorimpl.cpp2
-rw-r--r--tests/baseline/scenegraph/data/shared/svg/opacity_group_1.svg7
-rw-r--r--tests/baseline/scenegraph/data/shared/svg/opacity_group_2.svg8
-rw-r--r--tests/baseline/scenegraph/data/vectorimages/groupOpacity.qml31
7 files changed, 124 insertions, 0 deletions
diff --git a/src/quickvectorimage/generator/qquicknodeinfo_p.h b/src/quickvectorimage/generator/qquicknodeinfo_p.h
index 6e877144c0..008580fbdc 100644
--- a/src/quickvectorimage/generator/qquicknodeinfo_p.h
+++ b/src/quickvectorimage/generator/qquicknodeinfo_p.h
@@ -178,6 +178,8 @@ struct StructureNodeInfo : NodeInfo
QRectF viewBox;
QSize size;
QRectF clipBox;
+ QRectF opacityGroupBounds;
+ QString opacityGroupBoundsReferenceId;
bool isPathContainer = false;
std::optional<TimelineInfo> timelineInfo;
};
diff --git a/src/quickvectorimage/generator/qquickqmlgenerator.cpp b/src/quickvectorimage/generator/qquickqmlgenerator.cpp
index 3b385a80c7..fa3e0f8054 100644
--- a/src/quickvectorimage/generator/qquickqmlgenerator.cpp
+++ b/src/quickvectorimage/generator/qquickqmlgenerator.cpp
@@ -588,6 +588,7 @@ void QQuickQmlGenerator::generateImageNode(const ImageNodeInfo &info)
const QFileInfo assetFileInfo(filePath);
+ registerGraphicItem();
stream() << "Image {";
m_indentLevel++;
@@ -1035,6 +1036,7 @@ void QQuickQmlGenerator::outputShapePath(const PathNodeInfo &info, const QPainte
if (noPen && noFill)
return;
auto fillRule = QQuickShapePath::FillRule(painterPath ? painterPath->fillRule() : quadPath->fillRule());
+ registerGraphicItem();
stream() << "ShapePath {";
m_indentLevel++;
@@ -1267,6 +1269,7 @@ void QQuickQmlGenerator::generateTextNode(const TextNodeInfo &info)
stream() << "Item {";
m_indentLevel++;
generateNodeBase(info);
+ registerGraphicItem();
if (!info.isTextArea)
stream() << "Item { id: textAlignItem_" << m_textNodeCounter << "; x: " << info.position.x() << "; y: " << info.position.y() << "}";
@@ -1755,6 +1758,68 @@ void QQuickQmlGenerator::generateTransformTimeline(const QString &targetName, co
bool QQuickQmlGenerator::generateStructureNode(const StructureNodeInfo &info)
{
+ const bool isStartStage = (info.stage == StructureNodeStage::Start);
+ bool doWrap = false;
+ if (isStartStage)
+ doWrap = !info.opacityGroupBounds.isNull() || !info.opacityGroupBoundsReferenceId.isEmpty();
+ else
+ doWrap = (!m_opacityWrapperIds.isEmpty() && m_opacityWrapperIds.top() == info.id);
+ bool res = false;
+ if (!doWrap) {
+ res = generateStructureNode_helper(info);
+ } else {
+ StructureNodeInfo wrappedInfo = info;
+ wrappedInfo.isDefaultOpacity = true;
+ wrappedInfo.opacity = QQuickAnimatedProperty(QVariant::fromValue(1.0));
+ const QString wrapperId = info.id + "_opacityWrapper"_L1;
+ if (isStartStage) {
+ stream() << "Item { // Opacity group wrapper";
+ m_indentLevel++;
+ stream() << "id: " << wrapperId;
+ stream() << "layer.sourceRect: ";
+ if (!info.opacityGroupBoundsReferenceId.isEmpty()) {
+ stream(SameLine) << info.opacityGroupBoundsReferenceId << ".originalBounds";
+ } else {
+ stream(SameLine) << "Qt.rect("
+ << info.opacityGroupBounds.x() << ", "
+ << info.opacityGroupBounds.y() << ", "
+ << info.opacityGroupBounds.width() << ", "
+ << info.opacityGroupBounds.height() << ")";
+ }
+ stream() << "opacity: " << info.opacity.defaultValue().toReal();
+ generatePropertyAnimation(info.opacity, wrapperId, "opacity"_L1);
+ m_opacityWrapperIds.push(info.id);
+ m_opacityWrapperShapeCounts.push(0);
+ }
+
+ res = generateStructureNode_helper(wrappedInfo);
+
+ if (!isStartStage) {
+ m_opacityWrapperIds.pop();
+ if (m_opacityWrapperShapeCounts.pop() > 1) {
+ const QString itemSpyId = wrapperId + "_itemspy"_L1;
+ stream() << "layer.enabled: true";
+ stream() << "x: layer.sourceRect.x";
+ stream() << "y: layer.sourceRect.y";
+ stream() << "width: layer.sourceRect.width";
+ stream() << "height: layer.sourceRect.height";
+ stream() << "layer.textureSize: " << itemSpyId << ".requiredTextureSize";
+ stream() << "ItemSpy {";
+ m_indentLevel++;
+ stream() << "id: " << itemSpyId;
+ stream() << "anchors.fill: parent";
+ m_indentLevel--;
+ stream() << "}";
+ }
+ m_indentLevel--;
+ stream() << "}";
+ }
+ }
+ return res;
+}
+
+bool QQuickQmlGenerator::generateStructureNode_helper(const StructureNodeInfo &info)
+{
if (Q_UNLIKELY(errorState() || !isNodeVisible(info)))
return false;
diff --git a/src/quickvectorimage/generator/qquickqmlgenerator_p.h b/src/quickvectorimage/generator/qquickqmlgenerator_p.h
index 3a78a0fb38..ce01a1ba06 100644
--- a/src/quickvectorimage/generator/qquickqmlgenerator_p.h
+++ b/src/quickvectorimage/generator/qquickqmlgenerator_p.h
@@ -153,6 +153,13 @@ private:
void generateMarkers(const PathNodeInfo &info);
qsizetype generateFilterStep(const FilterNodeInfo &info, qsizetype stepIndex);
+ bool generateStructureNode_helper(const StructureNodeInfo &info);
+ void registerGraphicItem()
+ {
+ if (!m_opacityWrapperShapeCounts.isEmpty())
+ m_opacityWrapperShapeCounts.top()++;
+ }
+
bool usingTimelineAnimation() const
{
return m_flags.testFlag(QQuickVectorImageGenerator::TimelineAnimation);
@@ -200,6 +207,8 @@ private:
QString m_defsSuffix;
QString m_indentString;
QSet<QString> m_contentRelativeMasks;
+ QStack<QString> m_opacityWrapperIds;
+ QStack<int> m_opacityWrapperShapeCounts;
};
QT_END_NAMESPACE
diff --git a/src/quickvectorimage/generator/qsvgvisitorimpl.cpp b/src/quickvectorimage/generator/qsvgvisitorimpl.cpp
index f7c0f78222..428efc3a57 100644
--- a/src/quickvectorimage/generator/qsvgvisitorimpl.cpp
+++ b/src/quickvectorimage/generator/qsvgvisitorimpl.cpp
@@ -1587,6 +1587,8 @@ bool QSvgVisitorImpl::visitStructureNodeStart(const QSvgStructureNode *node)
fillAnimationInfo(node, info);
info.forceSeparatePaths = forceSeparatePaths;
info.isPathContainer = isPathContainer(m_doc, node);
+ if (!info.isDefaultOpacity || info.opacity.isAnimated())
+ info.opacityGroupBounds = node->bounds(m_doc);
info.stage = StructureNodeStage::Start;
return m_generator->generateStructureNode(info);
diff --git a/tests/baseline/scenegraph/data/shared/svg/opacity_group_1.svg b/tests/baseline/scenegraph/data/shared/svg/opacity_group_1.svg
new file mode 100644
index 0000000000..1aba4e067e
--- /dev/null
+++ b/tests/baseline/scenegraph/data/shared/svg/opacity_group_1.svg
@@ -0,0 +1,7 @@
+<svg viewBox="0 0 380 380" xmlns="http://www.w3.org/2000/svg">
+ <rect width="380" height="300" fill="green"/>
+ <g transform="translate(40, 40)" opacity="0.5">
+ <rect width="300" height="200" fill="blue"/>
+ <text x="10" y="125" style="font-size: 48px" fill="white">Qt Rocks!</text>
+ </g>
+</svg>
diff --git a/tests/baseline/scenegraph/data/shared/svg/opacity_group_2.svg b/tests/baseline/scenegraph/data/shared/svg/opacity_group_2.svg
new file mode 100644
index 0000000000..8e5c25fdb2
--- /dev/null
+++ b/tests/baseline/scenegraph/data/shared/svg/opacity_group_2.svg
@@ -0,0 +1,8 @@
+<svg viewBox="0 0 380 380" xmlns="http://www.w3.org/2000/svg">
+ <text x="20" y="175" style="font-size: 48px; font-weight: bold;" fill="black">BACKGROUND</text>
+ <g transform="translate(40, 75)" opacity="0.8">
+ <circle cx="150" cy="25" r="75" fill="red" />
+ <circle cx="100" cy="100" r="75" fill="green" />
+ <circle cx="200" cy="100" r="75" fill="blue" />
+ </g>
+</svg>
diff --git a/tests/baseline/scenegraph/data/vectorimages/groupOpacity.qml b/tests/baseline/scenegraph/data/vectorimages/groupOpacity.qml
new file mode 100644
index 0000000000..f254a6a619
--- /dev/null
+++ b/tests/baseline/scenegraph/data/vectorimages/groupOpacity.qml
@@ -0,0 +1,31 @@
+import QtQuick
+import QtQuick.VectorImage
+
+Rectangle{
+ id: topLevelItem
+ width: 800
+ height: 400
+
+ ListModel {
+ id: files
+ ListElement { src: "../shared/svg/opacity_group_1.svg" }
+ ListElement { src: "../shared/svg/opacity_group_2.svg" }
+ }
+
+ Grid {
+ spacing: 10
+ columns: 2
+ anchors.fill: parent
+ Repeater {
+ model: files
+
+ VectorImage {
+ source: src
+ preferredRendererType: VectorImage.CurveRenderer
+ width: 400
+ height: 400
+ fillMode: VectorImage.PreserveAspectFit
+ }
+ }
+ }
+}