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 <eskil.abrahamsen-blomfeldt@qt.io>
bb10
Hatem ElKharashy 2024-05-08 11:12:32 +03:00
parent 90c2cde691
commit 20521c3163
6 changed files with 313 additions and 15 deletions

View File

@ -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<qreal> Declaration::dashArray() const
{
if (d->propertyId != Property::QtStrokeDashArray || d->values.empty())
return QList<qreal>();
bool isValid = true;
QList<qreal> 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<qreal>();
}
QIcon Declaration::iconValue() const
{
if (d->parsed.isValid())

View File

@ -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<qreal> dashArray() const;
};
QT_CSS_DECLARE_TYPEINFO(Declaration, Q_RELOCATABLE_TYPE)

View File

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

View File

@ -1422,6 +1422,61 @@ void QTextHtmlParserNode::applyCssDeclarations(const QList<QCss::Declaration> &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<qreal> 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();

View File

@ -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<QString>("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<QString>("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"

View File

@ -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<qreal> 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<qreal> pattern;
const int dash = 2;
const int gap = 4;
pattern << dash << gap << dash << gap << dash << gap;
QCOMPARE(pen.dashPattern(), pattern);
}
}
}