diff options
author | Hatem ElKharashy <hatem.elkharashy@qt.io> | 2024-03-25 13:24:51 +0200 |
---|---|---|
committer | Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io> | 2024-04-25 10:04:21 +0300 |
commit | 645e1ee76b56fe351170bd76d54dd08d56bb917f (patch) | |
tree | cd3c2deab1232c41b799b379dca05197a05ab218 | |
parent | de436b852586ab03f93418ddf55f90082e93ae03 (diff) |
VectorImage: support stroke styling for paths
SVG different stroke attibutes can be easily mapped to QQuickShapePath
properties.
Task-number: QTBUG-121650
Change-Id: Id52f3e7d99a81c84851b7a7645f75fdee1efbaeb
Reviewed-by: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>
10 files changed, 224 insertions, 82 deletions
diff --git a/src/quickvectorimage/generator/qquickitemgenerator.cpp b/src/quickvectorimage/generator/qquickitemgenerator.cpp index cc46ba943c..121af73130 100644 --- a/src/quickvectorimage/generator/qquickitemgenerator.cpp +++ b/src/quickvectorimage/generator/qquickitemgenerator.cpp @@ -123,7 +123,7 @@ void QQuickItemGenerator::outputShapePath(const PathNodeInfo &info, const QPaint Q_UNUSED(pathSelector) Q_ASSERT(painterPath || quadPath); - const bool noPen = info.strokeColor == QColorConstants::Transparent; + const bool noPen = info.strokeStyle.color == QColorConstants::Transparent; if (pathSelector == QQuickVectorImageGenerator::StrokePath && noPen) return; @@ -145,12 +145,18 @@ void QQuickItemGenerator::outputShapePath(const PathNodeInfo &info, const QPaint if (noPen || !(pathSelector & QQuickVectorImageGenerator::StrokePath)) { shapePath->setStrokeColor(Qt::transparent); } else { - shapePath->setStrokeColor(info.strokeColor); - shapePath->setStrokeWidth(info.strokeWidth); + shapePath->setStrokeColor(info.strokeStyle.color); + shapePath->setStrokeWidth(info.strokeStyle.width); + shapePath->setCapStyle(QQuickShapePath::CapStyle(info.strokeStyle.lineCapStyle)); + shapePath->setJoinStyle(QQuickShapePath::JoinStyle(info.strokeStyle.lineJoinStyle)); + shapePath->setMiterLimit(info.strokeStyle.miterLimit); + if (info.strokeStyle.dashArray.length() != 0) { + shapePath->setStrokeStyle(QQuickShapePath::DashLine); + shapePath->setDashPattern(info.strokeStyle.dashArray.toVector()); + shapePath->setDashOffset(info.strokeStyle.dashOffset); + } } - shapePath->setCapStyle(QQuickShapePath::CapStyle(info.capStyle)); - if (!(pathSelector & QQuickVectorImageGenerator::FillPath)) shapePath->setFillColor(Qt::transparent); else if (info.grad.type() != QGradient::NoGradient) diff --git a/src/quickvectorimage/generator/qquicknodeinfo_p.h b/src/quickvectorimage/generator/qquicknodeinfo_p.h index 5d2d92bc40..b7123f53e0 100644 --- a/src/quickvectorimage/generator/qquicknodeinfo_p.h +++ b/src/quickvectorimage/generator/qquicknodeinfo_p.h @@ -44,14 +44,23 @@ struct ImageNodeInfo : NodeInfo QRectF rect; }; +struct StrokeStyle +{ + Qt::PenCapStyle lineCapStyle = Qt::SquareCap; + Qt::PenJoinStyle lineJoinStyle = Qt::MiterJoin; + qreal miterLimit = 4; + qreal dashOffset = 0; + QList<qreal> dashArray; + QColor color = QColorConstants::Transparent; + qreal width = 1.0; +}; + struct PathNodeInfo : NodeInfo { QPainterPath painterPath; Qt::FillRule fillRule = Qt::FillRule::WindingFill; - Qt::PenCapStyle capStyle = Qt::SquareCap; - QColor strokeColor; - qreal strokeWidth; QColor fillColor; + StrokeStyle strokeStyle; QGradient grad; }; diff --git a/src/quickvectorimage/generator/qquickqmlgenerator.cpp b/src/quickvectorimage/generator/qquickqmlgenerator.cpp index e10e6166bd..7313109f0d 100644 --- a/src/quickvectorimage/generator/qquickqmlgenerator.cpp +++ b/src/quickvectorimage/generator/qquickqmlgenerator.cpp @@ -246,7 +246,7 @@ void QQuickQmlGenerator::outputShapePath(const PathNodeInfo &info, const QPainte Q_UNUSED(pathSelector) Q_ASSERT(painterPath || quadPath); - const bool noPen = info.strokeColor == QColorConstants::Transparent; + const bool noPen = info.strokeStyle.color == QColorConstants::Transparent; if (pathSelector == QQuickVectorImageGenerator::StrokePath && noPen) return; @@ -275,11 +275,17 @@ void QQuickQmlGenerator::outputShapePath(const PathNodeInfo &info, const QPainte if (noPen || !(pathSelector & QQuickVectorImageGenerator::StrokePath)) { stream() << "strokeColor: \"transparent\""; } else { - stream() << "strokeColor: \"" << info.strokeColor.name(QColor::HexArgb) << "\""; - stream() << "strokeWidth: " << info.strokeWidth; + stream() << "strokeColor: \"" << info.strokeStyle.color.name(QColor::HexArgb) << "\""; + stream() << "strokeWidth: " << info.strokeStyle.width; + stream() << "capStyle: " << QQuickVectorImageGenerator::Utils::strokeCapStyleString(info.strokeStyle.lineCapStyle); + stream() << "joinStyle: " << QQuickVectorImageGenerator::Utils::strokeJoinStyleString(info.strokeStyle.lineJoinStyle); + stream() << "miterLimit: " << info.strokeStyle.miterLimit; + if (info.strokeStyle.dashArray.length() != 0) { + stream() << "strokeStyle: " << "ShapePath.DashLine"; + stream() << "dashPattern: " << QQuickVectorImageGenerator::Utils::listString(info.strokeStyle.dashArray); + stream() << "dashOffset: " << info.strokeStyle.dashOffset; + } } - if (info.capStyle == Qt::FlatCap) - stream() << "capStyle: ShapePath.FlatCap"; //### TODO Add the rest of the styles, as well as join styles etc. if (!(pathSelector & QQuickVectorImageGenerator::FillPath)) { stream() << "fillColor: \"transparent\""; diff --git a/src/quickvectorimage/generator/qsvgvisitorimpl.cpp b/src/quickvectorimage/generator/qsvgvisitorimpl.cpp index c0fae484f7..603c68ea33 100644 --- a/src/quickvectorimage/generator/qsvgvisitorimpl.cpp +++ b/src/quickvectorimage/generator/qsvgvisitorimpl.cpp @@ -34,52 +34,6 @@ using namespace Qt::StringLiterals; Q_DECLARE_LOGGING_CATEGORY(lcQuickVectorImage) -static inline bool isPathContainer(const QSvgStructureNode *node) -{ - bool foundPath = false; - for (const auto *child : node->renderers()) { - switch (child->type()) { - // nodes that shouldn't go inside Shape{} - case QSvgNode::Switch: - case QSvgNode::Doc: - case QSvgNode::Group: - case QSvgNode::Animation: - case QSvgNode::Use: - case QSvgNode::Video: - //qCDebug(lcQuickVectorGraphics) << "NOT path container because" << node->typeName() ; - return false; - - // nodes that could go inside Shape{} - case QSvgNode::Defs: - case QSvgNode::Image: - case QSvgNode::Textarea: - case QSvgNode::Text: - case QSvgNode::Tspan: - break; - - // nodes that are done as pure ShapePath{} - case QSvgNode::Rect: - case QSvgNode::Circle: - case QSvgNode::Ellipse: - case QSvgNode::Line: - case QSvgNode::Path: - case QSvgNode::Polygon: - case QSvgNode::Polyline: - if (!child->style().transform.isDefault()) { - //qCDebug(lcQuickVectorGraphics) << "NOT path container because local transform"; - return false; - } - foundPath = true; - break; - default: - qCDebug(lcQuickVectorImage) << "Unhandled type in switch" << child->type(); - break; - } - } - //qCDebug(lcQuickVectorGraphics) << "Container" << node->nodeId() << node->typeName() << "is" << foundPath; - return foundPath; -} - class QSvgStyleResolver { public: @@ -141,17 +95,6 @@ public: return strokeColor; } - qreal currentStrokeOpacity() const - { - return m_svgState.strokeOpacity; - } - - float currentStrokeWidth() const - { - float penWidth = m_dummyPainter.pen().widthF(); - return penWidth ? penWidth : 1; - } - static QGradient applyOpacityToGradient(const QGradient &gradient, float opacity) { QGradient grad = gradient; @@ -166,6 +109,17 @@ public: return grad; } + float currentStrokeWidth() const + { + float penWidth = m_dummyPainter.pen().widthF(); + return penWidth ? penWidth : 1; + } + + QPen currentStroke() const + { + return m_dummyPainter.pen(); + } + protected: QPainter m_dummyPainter; QImage m_dummyImage; @@ -175,6 +129,67 @@ protected: Q_GLOBAL_STATIC(QSvgStyleResolver, styleResolver) +namespace { +inline bool isPathContainer(const QSvgStructureNode *node) +{ + bool foundPath = false; + for (const auto *child : node->renderers()) { + switch (child->type()) { + // nodes that shouldn't go inside Shape{} + case QSvgNode::Switch: + case QSvgNode::Doc: + case QSvgNode::Group: + case QSvgNode::Animation: + case QSvgNode::Use: + case QSvgNode::Video: + //qCDebug(lcQuickVectorGraphics) << "NOT path container because" << node->typeName() ; + return false; + + // nodes that could go inside Shape{} + case QSvgNode::Defs: + case QSvgNode::Image: + case QSvgNode::Textarea: + case QSvgNode::Text: + case QSvgNode::Tspan: + break; + + // nodes that are done as pure ShapePath{} + case QSvgNode::Rect: + case QSvgNode::Circle: + case QSvgNode::Ellipse: + case QSvgNode::Line: + case QSvgNode::Path: + case QSvgNode::Polygon: + case QSvgNode::Polyline: + if (!child->style().transform.isDefault()) { + //qCDebug(lcQuickVectorGraphics) << "NOT path container because local transform"; + return false; + } + foundPath = true; + break; + default: + qCDebug(lcQuickVectorImage) << "Unhandled type in switch" << child->type(); + break; + } + } + //qCDebug(lcQuickVectorGraphics) << "Container" << node->nodeId() << node->typeName() << "is" << foundPath; + return foundPath; +} + +void populateStrokeStyle(StrokeStyle &srokeStyle) +{ + QPen p = styleResolver->currentStroke(); + srokeStyle.lineCapStyle = p.capStyle(); + srokeStyle.lineJoinStyle = p.joinStyle() == Qt::SvgMiterJoin ? Qt::MiterJoin : p.joinStyle(); //TODO support SvgMiterJoin + srokeStyle.miterLimit = p.miterLimit(); + srokeStyle.dashOffset = p.dashOffset(); + srokeStyle.dashArray = p.dashPattern(); + srokeStyle.color = styleResolver->currentStrokeColor(); + srokeStyle.width = p.widthF(); +} + +}; + QSvgVisitorImpl::QSvgVisitorImpl(const QString svgFileName, QQuickGenerator *generator) : m_svgFileName(svgFileName) , m_generator(generator) @@ -277,11 +292,10 @@ void QSvgVisitorImpl::visitPathNode(const QSvgPath *node) void QSvgVisitorImpl::visitLineNode(const QSvgLine *node) { - // TODO: proper end caps (should be flat by default?) QPainterPath p; p.moveTo(node->line().p1()); p.lineTo(node->line().p2()); - handlePathNode(node, p, Qt::FlatCap); + handlePathNode(node, p); } void QSvgVisitorImpl::visitPolygonNode(const QSvgPolygon *node) @@ -293,7 +307,7 @@ void QSvgVisitorImpl::visitPolygonNode(const QSvgPolygon *node) void QSvgVisitorImpl::visitPolylineNode(const QSvgPolyline *node) { QPainterPath p = QQuickVectorImageGenerator::Utils::polygonToPath(node->polygon(), false); - handlePathNode(node, p, Qt::FlatCap); + handlePathNode(node, p); } QString QSvgVisitorImpl::gradientCssDescription(const QGradient *gradient) @@ -526,11 +540,11 @@ void QSvgVisitorImpl::visitTextNode(const QSvgText *node) info.painterPath = p; if (fmt.hasProperty(QTextCharFormat::TextOutline)) { - info.strokeWidth = fmt.textOutline().widthF(); - info.strokeColor = fmt.textOutline().color(); + info.strokeStyle.width = fmt.textOutline().widthF(); + info.strokeStyle.color = fmt.textOutline().color(); } else { - info.strokeColor = styleResolver->currentStrokeColor(); - info.strokeWidth = styleResolver->currentStrokeWidth(); + info.strokeStyle.color = styleResolver->currentStrokeColor(); + info.strokeStyle.width = styleResolver->currentStrokeWidth(); } if (info.grad.type() == QGradient::NoGradient && styleResolver->currentFillGradient() != nullptr) @@ -738,7 +752,7 @@ void QSvgVisitorImpl::handleBaseNodeEnd(const QSvgNode *node) } -void QSvgVisitorImpl::handlePathNode(const QSvgNode *node, const QPainterPath &path, Qt::PenCapStyle capStyle) +void QSvgVisitorImpl::handlePathNode(const QSvgNode *node, const QPainterPath &path) { handleBaseNodeSetup(node); @@ -749,10 +763,8 @@ void QSvgVisitorImpl::handlePathNode(const QSvgNode *node, const QPainterPath &p info.fillRule = fillStyle->fillRule(); info.painterPath = path; - info.capStyle = capStyle; info.fillColor = styleResolver->currentFillColor(); - info.strokeColor = styleResolver->currentStrokeColor(); - info.strokeWidth = styleResolver->currentStrokeWidth(); + populateStrokeStyle(info.strokeStyle); if (styleResolver->currentFillGradient() != nullptr) info.grad = styleResolver->applyOpacityToGradient(*styleResolver->currentFillGradient(), styleResolver->currentFillOpacity()); diff --git a/src/quickvectorimage/generator/qsvgvisitorimpl_p.h b/src/quickvectorimage/generator/qsvgvisitorimpl_p.h index af631f387d..af8a8d386e 100644 --- a/src/quickvectorimage/generator/qsvgvisitorimpl_p.h +++ b/src/quickvectorimage/generator/qsvgvisitorimpl_p.h @@ -54,7 +54,7 @@ private: void handleBaseNodeSetup(const QSvgNode *node); void handleBaseNode(const QSvgNode *node); void handleBaseNodeEnd(const QSvgNode *node); - void handlePathNode(const QSvgNode *node, const QPainterPath &path, Qt::PenCapStyle capStyle = Qt::SquareCap); + void handlePathNode(const QSvgNode *node, const QPainterPath &path); void outputShapePath(QPainterPath pathCopy, const PathNodeInfo &info); static QString gradientCssDescription(const QGradient *gradient); static QString colorCssDescription(QColor color); diff --git a/src/quickvectorimage/generator/utils_p.h b/src/quickvectorimage/generator/utils_p.h index 9673dbbe65..0c25f3d7d3 100644 --- a/src/quickvectorimage/generator/utils_p.h +++ b/src/quickvectorimage/generator/utils_p.h @@ -193,6 +193,70 @@ inline QString toSvgString(const QQuadPath &path) return svgPathString; } +inline QString strokeCapStyleString(Qt::PenCapStyle strokeCapStyle) +{ + QString capStyle; + switch (strokeCapStyle) { + case Qt::FlatCap: + capStyle = QStringLiteral("ShapePath.FlatCap"); + break; + case Qt::SquareCap: + capStyle = QStringLiteral("ShapePath.SquareCap"); + break; + case Qt::RoundCap: + capStyle = QStringLiteral("ShapePath.RoundCap"); + break; + default: + Q_UNREACHABLE(); + break; + } + + return capStyle; +} + +inline QString strokeJoinStyleString(Qt::PenJoinStyle strokeJoinStyle) +{ + QString joinStyle; + switch (strokeJoinStyle) { + case Qt::MiterJoin: + joinStyle = QStringLiteral("ShapePath.MiterJoin"); + break; + case Qt::BevelJoin: + joinStyle = QStringLiteral("ShapePath.BevelJoin"); + break; + case Qt::RoundJoin: + joinStyle = QStringLiteral("ShapePath.RoundJoin"); + break; + default: + //TODO: Add support for SvgMiter case + Q_UNREACHABLE(); + break; + } + + return joinStyle; +} + +template<typename T> +inline QString listString(QList<T> list) +{ + if (list.isEmpty()) + return QStringLiteral("[]"); + + QString l; + QTextStream stream(&l); + stream << "["; + + if (list.length() > 1) { + for (int i = 0; i < list.length() - 1; i++) { + T v = list[i]; + stream << v << ", "; + } + } + + stream << list.last() << "]"; + return l; +} + } QT_END_NAMESPACE diff --git a/tests/manual/svg/data/styling/stroking_capStyle_shapes_1.svg b/tests/manual/svg/data/styling/stroking_capStyle_shapes_1.svg new file mode 100644 index 0000000000..9448fcb527 --- /dev/null +++ b/tests/manual/svg/data/styling/stroking_capStyle_shapes_1.svg @@ -0,0 +1,11 @@ +<svg width="900" height="600" viewBox="0 0 90 60" xmlns="http://www.w3.org/2000/svg"> + <defs> + <g id="capTest"> + <polyline points="10,10 80,10" fill="none" stroke-width="8" stroke="black"/> + <polyline points="10,10 80,10" fill="none" stroke-width="0.5" stroke-linecap="butt" stroke="yellow"/> + </g> + </defs> + <use href="#capTest" stroke-linecap="butt"/> + <use href="#capTest" y="20" stroke-linecap="round"/> + <use href="#capTest" y=" 40" stroke-linecap="square"/> +</svg> diff --git a/tests/manual/svg/data/styling/stroking_capStyle_shapes_2.svg b/tests/manual/svg/data/styling/stroking_capStyle_shapes_2.svg new file mode 100644 index 0000000000..1d52d2d247 --- /dev/null +++ b/tests/manual/svg/data/styling/stroking_capStyle_shapes_2.svg @@ -0,0 +1,12 @@ +<svg width="900" height="300" viewBox="0 0 130 40" xmlns="http://www.w3.org/2000/svg"> + <defs> + <polyline id="letterz" points="10,10 40,10 10,30 40,30" fill="none" stroke-width="2.5" stroke="black"/> + <polyline id="highlight" points="10,10 40,10 10,30 40,30" fill="none" stroke-width="0.1" stroke="yellow"/> + </defs> + <use href="#letterz" stroke-linecap="butt"/> + <use href="#highlight"/> + <use href="#letterz" x="40" stroke-linecap="round"/> + <use href="#highlight" x="40"/> + <use href="#letterz" x=" 80" stroke-linecap="square"/> + <use href="#highlight" x="80"/> +</svg> diff --git a/tests/manual/svg/data/styling/stroking_dash.svg b/tests/manual/svg/data/styling/stroking_dash.svg new file mode 100644 index 0000000000..5fdefa4cce --- /dev/null +++ b/tests/manual/svg/data/styling/stroking_dash.svg @@ -0,0 +1,10 @@ +<svg width="460" height="200" viewBox="0 0 30 13" xmlns="http://www.w3.org/2000/svg"> + <line x1="0" y1="1" x2="30" y2="1" stroke="black" stroke-dashoffset="2"/> + <line x1="0" y1="3" x2="30" y2="3" stroke="black" stroke-dasharray="4 2"/> +<!--positive dashoffset pulls the dashes--> + <line x1="0" y1="5" x2="30" y2="5" stroke="black" stroke-dasharray="4 2" stroke-dashoffset="2"/> + <line x1="0" y1="7" x2="30" y2="7" stroke="black" stroke-dasharray="4 2" stroke-dashoffset="4"/> + <!--negative dashoffset pushs the dashes--> + <line x1="0" y1="9" x2="30" y2="9" stroke="black" stroke-dasharray="4 2" stroke-dashoffset="-2"/> + <line x1="0" y1="11" x2="30" y2="11" stroke="black" stroke-dasharray="4 2" stroke-dashoffset="-4"/> +</svg> diff --git a/tests/manual/svg/data/styling/stroking_joinStyle_shapes_1.svg b/tests/manual/svg/data/styling/stroking_joinStyle_shapes_1.svg new file mode 100644 index 0000000000..1da775a97e --- /dev/null +++ b/tests/manual/svg/data/styling/stroking_joinStyle_shapes_1.svg @@ -0,0 +1,12 @@ +<svg width="600" height="200" viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg"> + <defs> + <g id="capTest"> + <polyline points="1,8 1,2 7,2" fill="none" stroke-width="1" stroke="black"/> + <polyline points="1,8 1,2 7,2" fill="none" stroke-width="0.03" stroke="yellow"/> + </g> + </defs> + + <use href="#capTest" x="1" stroke-linejoin="bevel"/> + <use href="#capTest" x="11" stroke-linejoin="round"/> + <use href="#capTest" x="21" stroke-linejoin="miter"/> +</svg> |