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>
This commit is contained in:
Hatem ElKharashy 2024-03-25 13:24:51 +02:00 committed by Eskil Abrahamsen Blomfeldt
parent de436b8525
commit 645e1ee76b
10 changed files with 224 additions and 82 deletions

View File

@ -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)

View File

@ -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;
};

View File

@ -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\"";

View File

@ -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());

View File

@ -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);

View File

@ -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

View File

@ -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>

After

Width:  |  Height:  |  Size: 492 B

View File

@ -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>

After

Width:  |  Height:  |  Size: 586 B

View File

@ -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>

After

Width:  |  Height:  |  Size: 736 B

View File

@ -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>

After

Width:  |  Height:  |  Size: 482 B