From 20521c31634b81543af68dc3166f9804c804f911 Mon Sep 17 00:00:00 2001 From: Hatem ElKharashy Date: Wed, 8 May 2024 11:12:32 +0300 Subject: [PATCH] Support SVG specific stroke styling properties SVG has special properties for stroke styling. Those are usually supported by different browsers, because SVG documents can be used inside HTML files. This kind of styling is already supported by QPen and all need to be done is some plumbing to save and retrieve those values in QTextDocument when it is stored as HTML. Change-Id: I291efab5483ac5e852d117e762e203257c64b47f Reviewed-by: Eskil Abrahamsen Blomfeldt --- src/gui/text/qcssparser.cpp | 50 +++++++- src/gui/text/qcssparser_p.h | 14 +++ src/gui/text/qtextdocument.cpp | 45 ++++++++ src/gui/text/qtexthtmlparser.cpp | 55 +++++++++ .../gui/text/qcssparser/tst_qcssparser.cpp | 55 +++++++++ .../text/qtextdocument/tst_qtextdocument.cpp | 109 ++++++++++++++++-- 6 files changed, 313 insertions(+), 15 deletions(-) diff --git a/src/gui/text/qcssparser.cpp b/src/gui/text/qcssparser.cpp index 916e96ea63..7886d9ba91 100644 --- a/src/gui/text/qcssparser.cpp +++ b/src/gui/text/qcssparser.cpp @@ -36,6 +36,7 @@ struct QCssKnownValue quint64 id; }; +// This array is sorted alphabetically. static const QCssKnownValue properties[NumProperties - 1] = { { "-qt-background-role", QtBackgroundRole }, { "-qt-block-indent", QtBlockIndent }, @@ -47,6 +48,11 @@ static const QCssKnownValue properties[NumProperties - 1] = { { "-qt-list-number-suffix", QtListNumberSuffix }, { "-qt-paragraph-type", QtParagraphType }, { "-qt-stroke-color", QtStrokeColor }, + { "-qt-stroke-dasharray", QtStrokeDashArray }, + { "-qt-stroke-dashoffset", QtStrokeDashOffset }, + { "-qt-stroke-linecap", QtStrokeLineCap }, + { "-qt-stroke-linejoin", QtStrokeLineJoin }, + { "-qt-stroke-miterlimit", QtStrokeMiterLimit }, { "-qt-stroke-width", QtStrokeWidth }, { "-qt-style-features", QtStyleFeatures }, { "-qt-table-type", QtTableType }, @@ -160,6 +166,7 @@ static const QCssKnownValue values[NumKnownValues - 1] = { { "always", Value_Always }, { "auto", Value_Auto }, { "base", Value_Base }, + { "beveljoin", Value_BevelJoin}, { "bold", Value_Bold }, { "bottom", Value_Bottom }, { "bright-text", Value_BrightText }, @@ -176,6 +183,7 @@ static const QCssKnownValue values[NumKnownValues - 1] = { { "dot-dot-dash", Value_DotDotDash }, { "dotted", Value_Dotted }, { "double", Value_Double }, + { "flatcap", Value_FlatCap}, { "groove", Value_Groove }, { "highlight", Value_Highlight }, { "highlighted-text", Value_HighlightedText }, @@ -194,6 +202,7 @@ static const QCssKnownValue values[NumKnownValues - 1] = { { "mid", Value_Mid }, { "middle", Value_Middle }, { "midlight", Value_Midlight }, + { "miterjoin", Value_MiterJoin}, { "native", Value_Native }, { "none", Value_None }, { "normal", Value_Normal }, @@ -208,14 +217,18 @@ static const QCssKnownValue values[NumKnownValues - 1] = { { "pre-wrap", Value_PreWrap }, { "ridge", Value_Ridge }, { "right", Value_Right }, + { "roundcap", Value_RoundCap}, + { "roundjoin", Value_RoundJoin}, { "selected", Value_Selected }, { "shadow", Value_Shadow }, { "small" , Value_Small }, { "small-caps", Value_SmallCaps }, { "solid", Value_Solid }, { "square", Value_Square }, + { "squarecap", Value_SquareCap}, { "sub", Value_Sub }, { "super", Value_Super }, + { "svgmiterjoin", Value_SvgMiterJoin}, { "text", Value_Text }, { "top", Value_Top }, { "transparent", Value_Transparent }, @@ -231,10 +244,10 @@ static const QCssKnownValue values[NumKnownValues - 1] = { }; //Map id to strings as they appears in the 'values' array above -static const short indexOfId[NumKnownValues] = { 0, 41, 48, 42, 49, 50, 55, 35, 26, 71, 72, 25, 43, 5, 64, 48, - 29, 59, 60, 27, 52, 62, 6, 10, 39, 56, 19, 13, 17, 18, 20, 21, 51, 24, 46, 68, 37, 3, 2, 40, 63, 16, - 11, 58, 14, 32, 65, 33, 66, 56, 67, 34, 70, 8, 28, 38, 12, 36, 61, 7, 9, 4, 69, 54, 22, 23, 30, 31, - 1, 15, 0, 53, 45, 44 }; +static const short indexOfId[NumKnownValues] = { 0, 44, 51, 45, 52, 53, 60, 37, 28, 78, 79, 27, 46, 6, 71, 50, + 31, 65, 66, 29, 55, 69, 7, 11, 42, 62, 20, 14, 18, 19, 21, 23, 54, 26, 49, 75, 39, 3, 2, 43, 70, 17, 12, + 63, 15, 34, 72, 35, 73, 61, 74, 36, 64, 22, 56, 41, 5, 57, 67, 77, 9, 30, 40, 13, 38, 68, 8, 10, 4, 76, + 59, 24, 25, 32, 33, 1, 16, 0, 58, 48, 47 }; QString Value::toString() const { @@ -1835,6 +1848,35 @@ bool Declaration::borderCollapseValue() const return d->values.at(0).toString() == "collapse"_L1; } +QList Declaration::dashArray() const +{ + if (d->propertyId != Property::QtStrokeDashArray || d->values.empty()) + return QList(); + + bool isValid = true; + QList dashes; + for (int i = 0; i < d->values.size(); i++) { + Value v = d->values[i]; + // Separators must be at odd indices and Numbers at even indices. + bool isValidSeparator = (i & 1) && v.type == Value::TermOperatorComma; + bool isValidNumber = !(i & 1) && v.type == Value::Number; + if (!isValidNumber && !isValidSeparator) { + isValid = false; + break; + } else if (isValidNumber) { + bool ok; + dashes.append(v.variant.toReal(&ok)); + if (!ok) { + isValid = false; + break; + } + } + } + + isValid &= !(dashes.size() & 1); + return isValid ? dashes : QList(); +} + QIcon Declaration::iconValue() const { if (d->parsed.isValid()) diff --git a/src/gui/text/qcssparser_p.h b/src/gui/text/qcssparser_p.h index 7742271e41..ba4a611df3 100644 --- a/src/gui/text/qcssparser_p.h +++ b/src/gui/text/qcssparser_p.h @@ -169,6 +169,11 @@ enum Property { QtAccent, QtStrokeWidth, QtStrokeColor, + QtStrokeLineCap, + QtStrokeLineJoin, + QtStrokeMiterLimit, + QtStrokeDashArray, + QtStrokeDashOffset, QtForeground, NumProperties }; @@ -226,6 +231,13 @@ enum KnownValue { Value_SmallCaps, Value_Uppercase, Value_Lowercase, + Value_SquareCap, + Value_FlatCap, + Value_RoundCap, + Value_MiterJoin, + Value_BevelJoin, + Value_RoundJoin, + Value_SvgMiterJoin, /* keep these in same order as QPalette::ColorRole */ Value_FirstColorRole, @@ -451,6 +463,8 @@ struct Q_GUI_EXPORT Declaration void borderImageValue(QString *image, int *cuts, TileMode *h, TileMode *v) const; bool borderCollapseValue() const; + + QList dashArray() const; }; QT_CSS_DECLARE_TYPEINFO(Declaration, Q_RELOCATABLE_TYPE) diff --git a/src/gui/text/qtextdocument.cpp b/src/gui/text/qtextdocument.cpp index ab788a5f9b..31cb3a526a 100644 --- a/src/gui/text/qtextdocument.cpp +++ b/src/gui/text/qtextdocument.cpp @@ -2757,6 +2757,51 @@ bool QTextHtmlExporter::emitCharFormatStyle(const QTextCharFormat &format) html += " -qt-stroke-width:"_L1; html += QString::number(outlinePen.widthF()); html += "px;"_L1; + + html += " -qt-stroke-linecap:"_L1; + if (outlinePen.capStyle() == Qt::SquareCap) + html += "squarecap;"_L1; + else if (outlinePen.capStyle() == Qt::FlatCap) + html += "flatcap;"_L1; + else if (outlinePen.capStyle() == Qt::RoundCap) + html += "roundcap;"_L1; + + html += " -qt-stroke-linejoin:"_L1; + if (outlinePen.joinStyle() == Qt::MiterJoin) + html += "miterjoin;"_L1; + else if (outlinePen.joinStyle() == Qt::SvgMiterJoin) + html += "svgmiterjoin;"_L1; + else if (outlinePen.joinStyle() == Qt::BevelJoin) + html += "beveljoin;"_L1; + else if (outlinePen.joinStyle() == Qt::RoundJoin) + html += "roundjoin;"_L1; + + if (outlinePen.joinStyle() == Qt::MiterJoin || + outlinePen.joinStyle() == Qt::SvgMiterJoin) { + html += " -qt-stroke-miterlimit:"_L1; + html += QString::number(outlinePen.miterLimit()); + html += u';'; + } + + if (outlinePen.style() == Qt::CustomDashLine && !outlinePen.dashPattern().empty()) { + html += " -qt-stroke-dasharray:"_L1; + QString dashArrayString; + QList dashes = outlinePen.dashPattern(); + + for (int i = 0; i < dashes.length() - 1; i++) { + qreal dash = dashes[i]; + dashArrayString += QString::number(dash) + u','; + } + + dashArrayString += QString::number(dashes.last()); + html += dashArrayString; + html += u';'; + + html += " -qt-stroke-dashoffset:"_L1; + html += QString::number(outlinePen.dashOffset()); + html += u';'; + } + attributesEmitted = true; } diff --git a/src/gui/text/qtexthtmlparser.cpp b/src/gui/text/qtexthtmlparser.cpp index bc2200697d..54c291b82e 100644 --- a/src/gui/text/qtexthtmlparser.cpp +++ b/src/gui/text/qtexthtmlparser.cpp @@ -1422,6 +1422,61 @@ void QTextHtmlParserNode::applyCssDeclarations(const QList &d } break; } + case QCss::QtStrokeLineCap: + { + QPen pen = charFormat.textOutline(); + switch (identifier) { + case QCss::Value_SquareCap: pen.setCapStyle(Qt::SquareCap); break; + case QCss::Value_FlatCap: pen.setCapStyle(Qt::FlatCap); break; + case QCss::Value_RoundCap: pen.setCapStyle(Qt::RoundCap); break; + default: break; + } + charFormat.setTextOutline(pen); + break; + } + case QCss::QtStrokeLineJoin: + { + QPen pen = charFormat.textOutline(); + switch (identifier) { + case QCss::Value_MiterJoin: pen.setJoinStyle(Qt::MiterJoin); break; + case QCss::Value_BevelJoin: pen.setJoinStyle(Qt::BevelJoin); break; + case QCss::Value_RoundJoin: pen.setJoinStyle(Qt::RoundJoin); break; + case QCss::Value_SvgMiterJoin: pen.setJoinStyle(Qt::SvgMiterJoin); break; + default: break; + } + charFormat.setTextOutline(pen); + break; + } + case QCss::QtStrokeMiterLimit: + { + qreal miterLimit; + if (decl.realValue(&miterLimit)) { + QPen pen = charFormat.textOutline(); + pen.setMiterLimit(miterLimit); + charFormat.setTextOutline(pen); + } + break; + } + case QCss::QtStrokeDashArray: + { + QList dashes = decl.dashArray(); + if (!dashes.empty()) { + QPen pen = charFormat.textOutline(); + pen.setDashPattern(dashes); + charFormat.setTextOutline(pen); + } + break; + } + case QCss::QtStrokeDashOffset: + { + qreal dashOffset; + if (decl.realValue(&dashOffset)) { + QPen pen = charFormat.textOutline(); + pen.setDashOffset(dashOffset); + charFormat.setTextOutline(pen); + } + break; + } case QCss::QtForeground: { QBrush brush = decl.brushValue(); diff --git a/tests/auto/gui/text/qcssparser/tst_qcssparser.cpp b/tests/auto/gui/text/qcssparser/tst_qcssparser.cpp index a438d7ebc8..203fe003a0 100644 --- a/tests/auto/gui/text/qcssparser/tst_qcssparser.cpp +++ b/tests/auto/gui/text/qcssparser/tst_qcssparser.cpp @@ -56,6 +56,10 @@ private slots: void quotedAndUnquotedIdentifiers(); void whitespaceValues_data(); void whitespaceValues(); + void strokeLineCapValues_data(); + void strokeLineCapValues(); + void strokeLineJoinValues_data(); + void strokeLineJoinValues(); }; void tst_QCssParser::scanner_data() @@ -1759,6 +1763,57 @@ void tst_QCssParser::whitespaceValues() QCOMPARE(rule.declarations.at(0).d->values.first().toString(), value); } +void tst_QCssParser::strokeLineCapValues_data() +{ + QTest::addColumn("value"); + + QTest::newRow("flatcap") << "flatcap"; + QTest::newRow("roundcap") << "roundcap"; + QTest::newRow("squarecap") << "squarecap"; +} + +void tst_QCssParser::strokeLineCapValues() +{ + QFETCH(QString, value); + QCss::Parser parser(QString("foo { -qt-stroke-linecap: %1 }").arg(value)); + QCss::StyleSheet sheet; + QVERIFY(parser.parse(&sheet)); + + QCss::StyleRule rule = (!sheet.styleRules.isEmpty()) ? + sheet.styleRules.at(0) : *sheet.nameIndex.begin(); + QCOMPARE(rule.declarations.size(), 1); + + QCOMPARE(rule.declarations.at(0).d->property, QLatin1String("-qt-stroke-linecap")); + QCOMPARE(rule.declarations.at(0).d->values.first().type, QCss::Value::KnownIdentifier); + QCOMPARE(rule.declarations.at(0).d->values.first().toString(), value); +} + +void tst_QCssParser::strokeLineJoinValues_data() +{ + QTest::addColumn("value"); + + QTest::newRow("beveljoin") << "beveljoin"; + QTest::newRow("miterjoin") << "miterjoin"; + QTest::newRow("roundjoin") << "roundjoin"; + QTest::newRow("svgmiterjoin") << "svgmiterjoin"; +} + +void tst_QCssParser::strokeLineJoinValues() +{ + QFETCH(QString, value); + QCss::Parser parser(QString("foo { -qt-stroke-linejoin: %1 }").arg(value)); + QCss::StyleSheet sheet; + QVERIFY(parser.parse(&sheet)); + + QCss::StyleRule rule = (!sheet.styleRules.isEmpty()) ? + sheet.styleRules.at(0) : *sheet.nameIndex.begin(); + QCOMPARE(rule.declarations.size(), 1); + + QCOMPARE(rule.declarations.at(0).d->property, QLatin1String("-qt-stroke-linejoin")); + QCOMPARE(rule.declarations.at(0).d->values.first().type, QCss::Value::KnownIdentifier); + QCOMPARE(rule.declarations.at(0).d->values.first().toString(), value); +} + QTEST_MAIN(tst_QCssParser) #include "tst_qcssparser.moc" diff --git a/tests/auto/gui/text/qtextdocument/tst_qtextdocument.cpp b/tests/auto/gui/text/qtextdocument/tst_qtextdocument.cpp index 335ee06e2f..600b45575f 100644 --- a/tests/auto/gui/text/qtextdocument/tst_qtextdocument.cpp +++ b/tests/auto/gui/text/qtextdocument/tst_qtextdocument.cpp @@ -4053,20 +4053,107 @@ void tst_QTextDocument::restoreStrokeFromHtml() QTextDocument document; QTextCursor textCursor(&document); QTextCharFormat textOutline; - textOutline.setTextOutline(QPen(Qt::red, 2.3)); - textCursor.insertText("Outlined text", textOutline); + + // Set stroke color and width + { + QPen pen(Qt::red, 2.3, Qt::SolidLine); + textOutline.setTextOutline(pen); + textCursor.insertText("Outlined text", textOutline); + } + + // Set Cap and Join styles + { + QPen pen; + pen.setCapStyle(Qt::FlatCap); + pen.setJoinStyle(Qt::RoundJoin); + textOutline.setTextOutline(pen); + textCursor.insertBlock(); + textCursor.insertText("Cap and Join Style", textOutline); + } + + // Set Miter limit + { + QPen pen; + pen.setJoinStyle(Qt::MiterJoin); + pen.setMiterLimit(4); + textOutline.setTextOutline(pen); + textCursor.insertBlock(); + textCursor.insertText("Miter Limit", textOutline); + } + + // Set Dash Array and Dash Offset + { + QPen pen; + QList pattern; + const int dash = 2; + const int gap = 4; + pattern << dash << gap << dash << gap << dash << gap; + pen.setDashPattern(pattern); + pen.setDashOffset(3); + textOutline.setTextOutline(pen); + textCursor.insertBlock(); + textCursor.insertText("Dash Pattern", textOutline); + } + { QTextDocument otherDocument; otherDocument.setHtml(document.toHtml()); - QCOMPARE(otherDocument.blockCount(), 1); - QTextBlock block = otherDocument.firstBlock(); - QTextFragment fragment = block.begin().fragment(); - QCOMPARE(fragment.text(), QStringLiteral("Outlined text")); - QTextCharFormat fmt = fragment.charFormat(); - QVERIFY(fmt.hasProperty(QTextCharFormat::TextOutline)); - QPen pen = fmt.textOutline(); - QCOMPARE(pen.color(), QColor(Qt::red)); - QCOMPARE(pen.widthF(), 2.3); + QCOMPARE(otherDocument.blockCount(), document.blockCount()); + + QTextBlock block; + QTextFragment fragment; + QTextCharFormat fmt; + QPen pen; + + { + block = otherDocument.findBlockByNumber(0); + fragment = block.begin().fragment(); + QCOMPARE(fragment.text(), QStringLiteral("Outlined text")); + fmt = fragment.charFormat(); + QVERIFY(fmt.hasProperty(QTextCharFormat::TextOutline)); + pen = fmt.textOutline(); + QCOMPARE(pen.color(), QColor(Qt::red)); + QCOMPARE(pen.widthF(), 2.3); + } + + { + block = otherDocument.findBlockByNumber(1); + qDebug() << block.text(); + fragment = block.begin().fragment(); + QCOMPARE(fragment.text(), QStringLiteral("Cap and Join Style")); + fmt = fragment.charFormat(); + QVERIFY(fmt.hasProperty(QTextCharFormat::TextOutline)); + pen = fmt.textOutline(); + QCOMPARE(pen.capStyle(), Qt::FlatCap); + QCOMPARE(pen.joinStyle(), Qt::RoundJoin); + } + + { + block = otherDocument.findBlockByNumber(2); + fragment = block.begin().fragment(); + QCOMPARE(fragment.text(), QStringLiteral("Miter Limit")); + fmt = fragment.charFormat(); + QVERIFY(fmt.hasProperty(QTextCharFormat::TextOutline)); + pen = fmt.textOutline(); + QCOMPARE(pen.joinStyle(), Qt::MiterJoin); + QCOMPARE(pen.miterLimit(), 4); + } + + + { + block = otherDocument.findBlockByNumber(3); + fragment = block.begin().fragment(); + QCOMPARE(fragment.text(), QStringLiteral("Dash Pattern")); + fmt = fragment.charFormat(); + QVERIFY(fmt.hasProperty(QTextCharFormat::TextOutline)); + pen = fmt.textOutline(); + QCOMPARE(pen.dashOffset(), 3); + QList pattern; + const int dash = 2; + const int gap = 4; + pattern << dash << gap << dash << gap << dash << gap; + QCOMPARE(pen.dashPattern(), pattern); + } } }