Skip to content

Commit 1ef7ed5

Browse files
authored
Merge pull request #3703 from aaime/svg_params
Export parametric SVG, will fallback symbols for the systems that cannot understand them
2 parents cc0b2e6 + 701d444 commit 1ef7ed5

File tree

6 files changed

+180
-19
lines changed

6 files changed

+180
-19
lines changed

python/core/symbology-ng/qgssymbollayerutils.sip

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,4 +501,20 @@ class QgsSymbolLayerUtils
501501
*/
502502
static void mergeScaleDependencies( int mScaleMinDenom, int mScaleMaxDenom, QgsStringMap& props );
503503

504+
/**
505+
* Encodes a reference to a parametric SVG into SLD, as a succession of parametric SVG using URL parameters,
506+
* a fallback SVG without parameters, and a final fallback as a mark with the right colors and outline for systems
507+
* that cannot do SVG at all
508+
* @note added in 3.0
509+
*/
510+
static void parametricSvgToSld( QDomDocument &doc, QDomElement &graphicElem,
511+
const QString& path,
512+
const QColor& fillColor, double size, const QColor& outlineColor, double outlineWidth );
513+
514+
/**
515+
* Encodes a reference to a parametric SVG into a path with parameters according to the SVG Parameters spec
516+
* @note added in 3.0
517+
*/
518+
static QString getSvgParametricPath( const QString& basePath, const QColor& fillColor, const QColor& borderColor, double borderWidth );
519+
504520
};

src/core/symbology-ng/qgsfillsymbollayer.cpp

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2083,8 +2083,10 @@ void QgsSVGFillSymbolLayer::toSld( QDomDocument &doc, QDomElement &element, cons
20832083

20842084
if ( !mSvgFilePath.isEmpty() )
20852085
{
2086-
double partternWidth = QgsSymbolLayerUtils::rescaleUom( mPatternWidth, mPatternWidthUnit, props );
2087-
QgsSymbolLayerUtils::externalGraphicToSld( doc, graphicElem, mSvgFilePath, QStringLiteral( "image/svg+xml" ), mColor, partternWidth );
2086+
// encode a parametric SVG reference
2087+
double patternWidth = QgsSymbolLayerUtils::rescaleUom( mPatternWidth, mPatternWidthUnit, props );
2088+
double outlineWidth = QgsSymbolLayerUtils::rescaleUom( mSvgOutlineWidth, mSvgOutlineWidthUnit, props );
2089+
QgsSymbolLayerUtils::parametricSvgToSld( doc, graphicElem, mSvgFilePath, mColor, patternWidth, mSvgOutlineColor, outlineWidth );
20882090
}
20892091
else
20902092
{
@@ -2093,12 +2095,6 @@ void QgsSVGFillSymbolLayer::toSld( QDomDocument &doc, QDomElement &element, cons
20932095
symbolizerElem.appendChild( doc.createComment( QStringLiteral( "SVG from data not implemented yet" ) ) );
20942096
}
20952097

2096-
if ( mSvgOutlineColor.isValid() || mSvgOutlineWidth >= 0 )
2097-
{
2098-
double svgOutlineWidth = QgsSymbolLayerUtils::rescaleUom( mSvgOutlineWidth, mSvgOutlineWidthUnit, props );
2099-
QgsSymbolLayerUtils::lineToSld( doc, graphicElem, Qt::SolidLine, mSvgOutlineColor, svgOutlineWidth );
2100-
}
2101-
21022098
// <Rotation>
21032099
QString angleFunc;
21042100
bool ok;

src/core/symbology-ng/qgsmarkersymbollayer.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2194,8 +2194,10 @@ void QgsSvgMarkerSymbolLayer::writeSldMarker( QDomDocument &doc, QDomElement &el
21942194
QDomElement graphicElem = doc.createElement( QStringLiteral( "se:Graphic" ) );
21952195
element.appendChild( graphicElem );
21962196

2197+
// encode a parametric SVG reference
21972198
double size = QgsSymbolLayerUtils::rescaleUom( mSize, mSizeUnit, props );
2198-
QgsSymbolLayerUtils::externalGraphicToSld( doc, graphicElem, mPath, QStringLiteral( "image/svg+xml" ), mColor, size );
2199+
double outlineWidth = QgsSymbolLayerUtils::rescaleUom( mOutlineWidth, mOutlineWidthUnit, props );
2200+
QgsSymbolLayerUtils::parametricSvgToSld( doc, graphicElem, mPath, mColor, size, mOutlineColor, outlineWidth );
21992201

22002202
// <Rotation>
22012203
QString angleFunc;

src/core/symbology-ng/qgssymbollayerutils.cpp

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ QColor QgsSymbolLayerUtils::decodeColor( const QString& str )
6868

6969
QString QgsSymbolLayerUtils::encodeSldAlpha( int alpha )
7070
{
71-
return QString::number( alpha / 255.0, 'f', 2 );
71+
QString result;
72+
result.sprintf( "%.2g", alpha / 255.0 );
73+
return result;
7274
}
7375

7476
int QgsSymbolLayerUtils::decodeSldAlpha( const QString& str )
@@ -1953,6 +1955,69 @@ void QgsSymbolLayerUtils::externalGraphicToSld( QDomDocument &doc, QDomElement &
19531955
}
19541956
}
19551957

1958+
void QgsSymbolLayerUtils::parametricSvgToSld( QDomDocument &doc, QDomElement &graphicElem,
1959+
const QString& path, const QColor& fillColor, double size, const QColor& outlineColor, double outlineWidth )
1960+
{
1961+
// Parametric SVG paths are an extension that few systems will understand, but se:Graphic allows for fallback
1962+
// symbols, this encodes the full parametric path first, the pure shape second, and a mark with the right colors as
1963+
// a last resort for systems that cannot do SVG at all
1964+
1965+
// encode parametric version with all coloring details (size is going to be encoded by the last fallback)
1966+
graphicElem.appendChild( doc.createComment( QStringLiteral( "Parametric SVG" ) ) );
1967+
QString parametricPath = getSvgParametricPath( path, fillColor, outlineColor, outlineWidth );
1968+
QgsSymbolLayerUtils::externalGraphicToSld( doc, graphicElem, parametricPath, QStringLiteral( "image/svg+xml" ), fillColor, -1 );
1969+
// also encode a fallback version without parameters, in case a renderer gets confused by the parameters
1970+
graphicElem.appendChild( doc.createComment( QStringLiteral( "Plain SVG fallback, no parameters" ) ) );
1971+
QgsSymbolLayerUtils::externalGraphicToSld( doc, graphicElem, path, QStringLiteral( "image/svg+xml" ), fillColor, -1 );
1972+
// finally encode a simple mark with the right colors/outlines for renderers that cannot do SVG at all
1973+
graphicElem.appendChild( doc.createComment( QStringLiteral( "Well known marker fallback" ) ) );
1974+
QgsSymbolLayerUtils::wellKnownMarkerToSld( doc, graphicElem, QStringLiteral( "square" ), fillColor, outlineColor, Qt::PenStyle::SolidLine, outlineWidth, -1 );
1975+
1976+
// size is encoded here, it's part of se:Graphic, not attached to the single symbol
1977+
if ( size >= 0 )
1978+
{
1979+
QDomElement sizeElem = doc.createElement( QStringLiteral( "se:Size" ) );
1980+
sizeElem.appendChild( doc.createTextNode( qgsDoubleToString( size ) ) );
1981+
graphicElem.appendChild( sizeElem );
1982+
}
1983+
}
1984+
1985+
1986+
QString QgsSymbolLayerUtils::getSvgParametricPath( const QString& basePath, const QColor& fillColor, const QColor& borderColor, double borderWidth )
1987+
{
1988+
QUrl url = QUrl();
1989+
if ( fillColor.isValid() )
1990+
{
1991+
url.addQueryItem( QStringLiteral( "fill" ), fillColor.name() );
1992+
url.addQueryItem( QStringLiteral( "fill-opacity" ), encodeSldAlpha( fillColor.alpha() ) );
1993+
}
1994+
else
1995+
{
1996+
url.addQueryItem( "fill", QStringLiteral( "#000000" ) );
1997+
url.addQueryItem( "fill-opacity", QStringLiteral( "1" ) );
1998+
}
1999+
if ( borderColor.isValid() )
2000+
{
2001+
url.addQueryItem( QStringLiteral( "outline" ), borderColor.name() );
2002+
url.addQueryItem( QStringLiteral( "outline-opacity" ), encodeSldAlpha( borderColor.alpha() ) );
2003+
}
2004+
else
2005+
{
2006+
url.addQueryItem( QStringLiteral( "outline" ), QStringLiteral( "#000000" ) );
2007+
url.addQueryItem( QStringLiteral( "outline-opacity" ), QStringLiteral( "1" ) );
2008+
}
2009+
url.addQueryItem( QStringLiteral( "outline-width" ), QString::number( borderWidth ) );
2010+
QString params = url.encodedQuery();
2011+
if ( params.isEmpty() )
2012+
{
2013+
return basePath;
2014+
}
2015+
else
2016+
{
2017+
return basePath + "?" + params;
2018+
}
2019+
}
2020+
19562021
bool QgsSymbolLayerUtils::externalGraphicFromSld( QDomElement &element,
19572022
QString &path, QString &mime,
19582023
QColor &color, double &size )

src/core/symbology-ng/qgssymbollayerutils.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,22 @@ class CORE_EXPORT QgsSymbolLayerUtils
588588
*/
589589
static void mergeScaleDependencies( int mScaleMinDenom, int mScaleMaxDenom, QgsStringMap& props );
590590

591+
/**
592+
* Encodes a reference to a parametric SVG into SLD, as a succession of parametric SVG using URL parameters,
593+
* a fallback SVG without parameters, and a final fallback as a mark with the right colors and outline for systems
594+
* that cannot do SVG at all
595+
* @note added in 3.0
596+
*/
597+
static void parametricSvgToSld( QDomDocument &doc, QDomElement &graphicElem,
598+
const QString& path,
599+
const QColor& fillColor, double size, const QColor& outlineColor, double outlineWidth );
600+
601+
/**
602+
* Encodes a reference to a parametric SVG into a path with parameters according to the SVG Parameters spec
603+
* @note added in 3.0
604+
*/
605+
static QString getSvgParametricPath( const QString& basePath, const QColor& fillColor, const QColor& borderColor, double borderWidth );
606+
591607
};
592608

593609
class QPolygonF;

tests/src/python/test_qgssymbollayer_createsld.py

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ def testSimpleMarkerRotation(self):
6060

6161
self.assertStaticRotation(root, '50')
6262

63-
def assertStaticRotation(self, root, expectedValue):
63+
def assertStaticRotation(self, root, expectedValue, index=0):
6464
# Check the rotation element is a literal, not a
65-
rotation = root.elementsByTagName('se:Rotation').item(0)
65+
rotation = root.elementsByTagName('se:Rotation').item(index)
6666
literal = rotation.firstChild()
6767
self.assertEqual("ogc:Literal", literal.nodeName())
6868
self.assertEqual(expectedValue, literal.firstChild().nodeValue())
@@ -127,11 +127,21 @@ def testSimpleMarkerUnitPixels(self):
127127

128128
def testSvgMarkerUnitDefault(self):
129129
symbol = QgsSvgMarkerSymbolLayer('symbols/star.svg', 10, 90)
130+
symbol.setFillColor(QColor("blue"))
131+
symbol.setOutlineWidth(1)
132+
symbol.setOutlineColor(QColor('red'))
133+
symbol.setPath('symbols/star.svg')
130134
symbol.setOffset(QPointF(5, 10))
131135

132136
dom, root = self.symbolToSld(symbol)
133137
# print("Svg marker mm: " + dom.toString())
134138

139+
self.assertExternalGraphic(root, 0,
140+
'symbols/star.svg?fill=%230000ff&fill-opacity=1&outline=%23ff0000&outline-opacity=1&outline-width=4', 'image/svg+xml')
141+
self.assertExternalGraphic(root, 1,
142+
'symbols/star.svg', 'image/svg+xml')
143+
self.assertWellKnownMark(root, 0, 'square', '#0000ff', '#ff0000', 4)
144+
135145
# Check the size has been rescaled
136146
self.assertStaticSize(root, '36')
137147

@@ -141,11 +151,21 @@ def testSvgMarkerUnitDefault(self):
141151

142152
def testSvgMarkerUnitPixels(self):
143153
symbol = QgsSvgMarkerSymbolLayer('symbols/star.svg', 10, 0)
154+
symbol.setFillColor(QColor("blue"))
155+
symbol.setOutlineWidth(1)
156+
symbol.setOutlineColor(QColor('red'))
157+
symbol.setPath('symbols/star.svg')
144158
symbol.setOffset(QPointF(5, 10))
145159
symbol.setOutputUnit(QgsUnitTypes.RenderPixels)
146160
dom, root = self.symbolToSld(symbol)
147161
# print("Svg marker unit px: " + dom.toString())
148162

163+
self.assertExternalGraphic(root, 0,
164+
'symbols/star.svg?fill=%230000ff&fill-opacity=1&outline=%23ff0000&outline-opacity=1&outline-width=1', 'image/svg+xml')
165+
self.assertExternalGraphic(root, 1,
166+
'symbols/star.svg', 'image/svg+xml')
167+
self.assertWellKnownMark(root, 0, 'square', '#0000ff', '#ff0000', 1)
168+
149169
# Check the size has not been rescaled
150170
self.assertStaticSize(root, '10')
151171
self.assertStaticDisplacement(root, 5, 10)
@@ -154,7 +174,7 @@ def testFontMarkerUnitDefault(self):
154174
symbol = QgsFontMarkerSymbolLayer('sans', ',', 10, QColor('black'), 45)
155175
symbol.setOffset(QPointF(5, 10))
156176
dom, root = self.symbolToSld(symbol)
157-
# print "Font marker unit mm: " + dom.toString()
177+
# print("Font marker unit mm: " + dom.toString())
158178

159179
# Check the size has been rescaled
160180
self.assertStaticSize(root, '36')
@@ -300,32 +320,47 @@ def testSimpleFillPixels(self):
300320

301321
def testSvgFillDefault(self):
302322
symbol = QgsSVGFillSymbolLayer('test/star.svg', 10, 45)
323+
symbol.setSvgFillColor(QColor('blue'))
303324
symbol.setSvgOutlineWidth(3)
325+
symbol.setSvgOutlineColor(QColor('yellow'))
326+
symbol.subSymbol().setWidth(10)
304327

305328
dom, root = self.symbolToSld(symbol)
306329
# print ("Svg fill mm: \n" + dom.toString())
307330

331+
self.assertExternalGraphic(root, 0,
332+
'test/star.svg?fill=%230000ff&fill-opacity=1&outline=%23ffff00&outline-opacity=1&outline-width=11', 'image/svg+xml')
333+
self.assertExternalGraphic(root, 1,
334+
'test/star.svg', 'image/svg+xml')
335+
self.assertWellKnownMark(root, 0, 'square', '#0000ff', '#ffff00', 11)
336+
308337
self.assertStaticRotation(root, '45')
309338
self.assertStaticSize(root, '36')
310-
# width of the svg outline
311-
self.assertStrokeWidth(root, 1, 11)
312339
# width of the polygon outline
313-
self.assertStrokeWidth(root, 3, 1)
340+
lineSymbolizer = root.elementsByTagName('se:LineSymbolizer').item(0).toElement()
341+
self.assertStrokeWidth(lineSymbolizer, 1, 36)
314342

315343
def testSvgFillPixel(self):
316344
symbol = QgsSVGFillSymbolLayer('test/star.svg', 10, 45)
345+
symbol.setSvgFillColor(QColor('blue'))
317346
symbol.setSvgOutlineWidth(3)
318347
symbol.setOutputUnit(QgsUnitTypes.RenderPixels)
348+
symbol.subSymbol().setWidth(10)
319349

320350
dom, root = self.symbolToSld(symbol)
321351
# print ("Svg fill px: \n" + dom.toString())
322352

353+
self.assertExternalGraphic(root, 0,
354+
'test/star.svg?fill=%230000ff&fill-opacity=1&outline=%23000000&outline-opacity=1&outline-width=3', 'image/svg+xml')
355+
self.assertExternalGraphic(root, 1,
356+
'test/star.svg', 'image/svg+xml')
357+
self.assertWellKnownMark(root, 0, 'square', '#0000ff', '#000000', 3)
358+
323359
self.assertStaticRotation(root, '45')
324360
self.assertStaticSize(root, '10')
325-
# width of the svg outline
326-
self.assertStrokeWidth(root, 1, 3)
327361
# width of the polygon outline
328-
self.assertStrokeWidth(root, 3, 0.26)
362+
lineSymbolizer = root.elementsByTagName('se:LineSymbolizer').item(0).toElement()
363+
self.assertStrokeWidth(lineSymbolizer, 1, 10)
329364

330365
def testLineFillDefault(self):
331366
symbol = QgsLinePatternFillSymbolLayer()
@@ -497,10 +532,41 @@ def assertStaticSize(self, root, expectedValue):
497532
size = root.elementsByTagName('se:Size').item(0)
498533
self.assertEqual(expectedValue, size.firstChild().nodeValue())
499534

535+
def assertExternalGraphic(self, root, index, expectedLink, expectedFormat):
536+
graphic = root.elementsByTagName('se:ExternalGraphic').item(index)
537+
onlineResource = graphic.firstChildElement('se:OnlineResource')
538+
self.assertEqual(expectedLink, onlineResource.attribute('xlink:href'))
539+
format = graphic.firstChildElement('se:Format')
540+
self.assertEqual(expectedFormat, format.firstChild().nodeValue())
541+
500542
def assertStaticPerpendicularOffset(self, root, expectedValue):
501543
offset = root.elementsByTagName('se:PerpendicularOffset').item(0)
502544
self.assertEqual(expectedValue, offset.firstChild().nodeValue())
503545

546+
def assertWellKnownMark(self, root, index, expectedName, expectedFill, expectedStroke, expectedStrokeWidth):
547+
mark = root.elementsByTagName('se:Mark').item(index)
548+
wkn = mark.firstChildElement('se:WellKnownName')
549+
self.assertEqual(expectedName, wkn.text())
550+
551+
fill = mark.firstChildElement('se:Fill')
552+
if expectedFill is None:
553+
self.assertTrue(fill.isNull())
554+
else:
555+
parameter = fill.firstChildElement('se:SvgParameter')
556+
self.assertEqual('fill', parameter.attribute('name'))
557+
self.assertEqual(expectedFill, parameter.text())
558+
559+
stroke = mark.firstChildElement('se:Stroke')
560+
if expectedStroke is None:
561+
self.assertTrue(stroke.isNull())
562+
else:
563+
parameter = stroke.firstChildElement('se:SvgParameter')
564+
self.assertEqual('stroke', parameter.attribute('name'))
565+
self.assertEqual(expectedStroke, parameter.text())
566+
parameter = parameter.nextSiblingElement('se:SvgParameter')
567+
self.assertEqual('stroke-width', parameter.attribute('name'))
568+
self.assertEqual(str(expectedStrokeWidth), parameter.text())
569+
504570
def symbolToSld(self, symbolLayer):
505571
dom = QDomDocument()
506572
root = dom.createElement("FakeRoot")

0 commit comments

Comments
 (0)