Skip to content

Commit

Permalink
Merge pull request #3703 from aaime/svg_params
Browse files Browse the repository at this point in the history
Export parametric SVG, will fallback symbols for the systems that cannot understand them
  • Loading branch information
nyalldawson committed Nov 11, 2016
2 parents cc0b2e6 + 701d444 commit 1ef7ed5
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 19 deletions.
16 changes: 16 additions & 0 deletions python/core/symbology-ng/qgssymbollayerutils.sip
Expand Up @@ -501,4 +501,20 @@ class QgsSymbolLayerUtils
*/
static void mergeScaleDependencies( int mScaleMinDenom, int mScaleMaxDenom, QgsStringMap& props );

/**
* Encodes a reference to a parametric SVG into SLD, as a succession of parametric SVG using URL parameters,
* a fallback SVG without parameters, and a final fallback as a mark with the right colors and outline for systems
* that cannot do SVG at all
* @note added in 3.0
*/
static void parametricSvgToSld( QDomDocument &doc, QDomElement &graphicElem,
const QString& path,
const QColor& fillColor, double size, const QColor& outlineColor, double outlineWidth );

/**
* Encodes a reference to a parametric SVG into a path with parameters according to the SVG Parameters spec
* @note added in 3.0
*/
static QString getSvgParametricPath( const QString& basePath, const QColor& fillColor, const QColor& borderColor, double borderWidth );

};
12 changes: 4 additions & 8 deletions src/core/symbology-ng/qgsfillsymbollayer.cpp
Expand Up @@ -2083,8 +2083,10 @@ void QgsSVGFillSymbolLayer::toSld( QDomDocument &doc, QDomElement &element, cons

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

if ( mSvgOutlineColor.isValid() || mSvgOutlineWidth >= 0 )
{
double svgOutlineWidth = QgsSymbolLayerUtils::rescaleUom( mSvgOutlineWidth, mSvgOutlineWidthUnit, props );
QgsSymbolLayerUtils::lineToSld( doc, graphicElem, Qt::SolidLine, mSvgOutlineColor, svgOutlineWidth );
}

// <Rotation>
QString angleFunc;
bool ok;
Expand Down
4 changes: 3 additions & 1 deletion src/core/symbology-ng/qgsmarkersymbollayer.cpp
Expand Up @@ -2194,8 +2194,10 @@ void QgsSvgMarkerSymbolLayer::writeSldMarker( QDomDocument &doc, QDomElement &el
QDomElement graphicElem = doc.createElement( QStringLiteral( "se:Graphic" ) );
element.appendChild( graphicElem );

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

// <Rotation>
QString angleFunc;
Expand Down
67 changes: 66 additions & 1 deletion src/core/symbology-ng/qgssymbollayerutils.cpp
Expand Up @@ -68,7 +68,9 @@ QColor QgsSymbolLayerUtils::decodeColor( const QString& str )

QString QgsSymbolLayerUtils::encodeSldAlpha( int alpha )
{
return QString::number( alpha / 255.0, 'f', 2 );
QString result;
result.sprintf( "%.2g", alpha / 255.0 );
return result;
}

int QgsSymbolLayerUtils::decodeSldAlpha( const QString& str )
Expand Down Expand Up @@ -1953,6 +1955,69 @@ void QgsSymbolLayerUtils::externalGraphicToSld( QDomDocument &doc, QDomElement &
}
}

void QgsSymbolLayerUtils::parametricSvgToSld( QDomDocument &doc, QDomElement &graphicElem,
const QString& path, const QColor& fillColor, double size, const QColor& outlineColor, double outlineWidth )
{
// Parametric SVG paths are an extension that few systems will understand, but se:Graphic allows for fallback
// symbols, this encodes the full parametric path first, the pure shape second, and a mark with the right colors as
// a last resort for systems that cannot do SVG at all

// encode parametric version with all coloring details (size is going to be encoded by the last fallback)
graphicElem.appendChild( doc.createComment( QStringLiteral( "Parametric SVG" ) ) );
QString parametricPath = getSvgParametricPath( path, fillColor, outlineColor, outlineWidth );
QgsSymbolLayerUtils::externalGraphicToSld( doc, graphicElem, parametricPath, QStringLiteral( "image/svg+xml" ), fillColor, -1 );
// also encode a fallback version without parameters, in case a renderer gets confused by the parameters
graphicElem.appendChild( doc.createComment( QStringLiteral( "Plain SVG fallback, no parameters" ) ) );
QgsSymbolLayerUtils::externalGraphicToSld( doc, graphicElem, path, QStringLiteral( "image/svg+xml" ), fillColor, -1 );
// finally encode a simple mark with the right colors/outlines for renderers that cannot do SVG at all
graphicElem.appendChild( doc.createComment( QStringLiteral( "Well known marker fallback" ) ) );
QgsSymbolLayerUtils::wellKnownMarkerToSld( doc, graphicElem, QStringLiteral( "square" ), fillColor, outlineColor, Qt::PenStyle::SolidLine, outlineWidth, -1 );

// size is encoded here, it's part of se:Graphic, not attached to the single symbol
if ( size >= 0 )
{
QDomElement sizeElem = doc.createElement( QStringLiteral( "se:Size" ) );
sizeElem.appendChild( doc.createTextNode( qgsDoubleToString( size ) ) );
graphicElem.appendChild( sizeElem );
}
}


QString QgsSymbolLayerUtils::getSvgParametricPath( const QString& basePath, const QColor& fillColor, const QColor& borderColor, double borderWidth )
{
QUrl url = QUrl();
if ( fillColor.isValid() )
{
url.addQueryItem( QStringLiteral( "fill" ), fillColor.name() );
url.addQueryItem( QStringLiteral( "fill-opacity" ), encodeSldAlpha( fillColor.alpha() ) );
}
else
{
url.addQueryItem( "fill", QStringLiteral( "#000000" ) );
url.addQueryItem( "fill-opacity", QStringLiteral( "1" ) );
}
if ( borderColor.isValid() )
{
url.addQueryItem( QStringLiteral( "outline" ), borderColor.name() );
url.addQueryItem( QStringLiteral( "outline-opacity" ), encodeSldAlpha( borderColor.alpha() ) );
}
else
{
url.addQueryItem( QStringLiteral( "outline" ), QStringLiteral( "#000000" ) );
url.addQueryItem( QStringLiteral( "outline-opacity" ), QStringLiteral( "1" ) );
}
url.addQueryItem( QStringLiteral( "outline-width" ), QString::number( borderWidth ) );
QString params = url.encodedQuery();
if ( params.isEmpty() )
{
return basePath;
}
else
{
return basePath + "?" + params;
}
}

bool QgsSymbolLayerUtils::externalGraphicFromSld( QDomElement &element,
QString &path, QString &mime,
QColor &color, double &size )
Expand Down
16 changes: 16 additions & 0 deletions src/core/symbology-ng/qgssymbollayerutils.h
Expand Up @@ -588,6 +588,22 @@ class CORE_EXPORT QgsSymbolLayerUtils
*/
static void mergeScaleDependencies( int mScaleMinDenom, int mScaleMaxDenom, QgsStringMap& props );

/**
* Encodes a reference to a parametric SVG into SLD, as a succession of parametric SVG using URL parameters,
* a fallback SVG without parameters, and a final fallback as a mark with the right colors and outline for systems
* that cannot do SVG at all
* @note added in 3.0
*/
static void parametricSvgToSld( QDomDocument &doc, QDomElement &graphicElem,
const QString& path,
const QColor& fillColor, double size, const QColor& outlineColor, double outlineWidth );

/**
* Encodes a reference to a parametric SVG into a path with parameters according to the SVG Parameters spec
* @note added in 3.0
*/
static QString getSvgParametricPath( const QString& basePath, const QColor& fillColor, const QColor& borderColor, double borderWidth );

};

class QPolygonF;
Expand Down
84 changes: 75 additions & 9 deletions tests/src/python/test_qgssymbollayer_createsld.py
Expand Up @@ -60,9 +60,9 @@ def testSimpleMarkerRotation(self):

self.assertStaticRotation(root, '50')

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

def testSvgMarkerUnitDefault(self):
symbol = QgsSvgMarkerSymbolLayer('symbols/star.svg', 10, 90)
symbol.setFillColor(QColor("blue"))
symbol.setOutlineWidth(1)
symbol.setOutlineColor(QColor('red'))
symbol.setPath('symbols/star.svg')
symbol.setOffset(QPointF(5, 10))

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

self.assertExternalGraphic(root, 0,
'symbols/star.svg?fill=%230000ff&fill-opacity=1&outline=%23ff0000&outline-opacity=1&outline-width=4', 'image/svg+xml')
self.assertExternalGraphic(root, 1,
'symbols/star.svg', 'image/svg+xml')
self.assertWellKnownMark(root, 0, 'square', '#0000ff', '#ff0000', 4)

# Check the size has been rescaled
self.assertStaticSize(root, '36')

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

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

self.assertExternalGraphic(root, 0,
'symbols/star.svg?fill=%230000ff&fill-opacity=1&outline=%23ff0000&outline-opacity=1&outline-width=1', 'image/svg+xml')
self.assertExternalGraphic(root, 1,
'symbols/star.svg', 'image/svg+xml')
self.assertWellKnownMark(root, 0, 'square', '#0000ff', '#ff0000', 1)

# Check the size has not been rescaled
self.assertStaticSize(root, '10')
self.assertStaticDisplacement(root, 5, 10)
Expand All @@ -154,7 +174,7 @@ def testFontMarkerUnitDefault(self):
symbol = QgsFontMarkerSymbolLayer('sans', ',', 10, QColor('black'), 45)
symbol.setOffset(QPointF(5, 10))
dom, root = self.symbolToSld(symbol)
# print "Font marker unit mm: " + dom.toString()
# print("Font marker unit mm: " + dom.toString())

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

def testSvgFillDefault(self):
symbol = QgsSVGFillSymbolLayer('test/star.svg', 10, 45)
symbol.setSvgFillColor(QColor('blue'))
symbol.setSvgOutlineWidth(3)
symbol.setSvgOutlineColor(QColor('yellow'))
symbol.subSymbol().setWidth(10)

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

self.assertExternalGraphic(root, 0,
'test/star.svg?fill=%230000ff&fill-opacity=1&outline=%23ffff00&outline-opacity=1&outline-width=11', 'image/svg+xml')
self.assertExternalGraphic(root, 1,
'test/star.svg', 'image/svg+xml')
self.assertWellKnownMark(root, 0, 'square', '#0000ff', '#ffff00', 11)

self.assertStaticRotation(root, '45')
self.assertStaticSize(root, '36')
# width of the svg outline
self.assertStrokeWidth(root, 1, 11)
# width of the polygon outline
self.assertStrokeWidth(root, 3, 1)
lineSymbolizer = root.elementsByTagName('se:LineSymbolizer').item(0).toElement()
self.assertStrokeWidth(lineSymbolizer, 1, 36)

def testSvgFillPixel(self):
symbol = QgsSVGFillSymbolLayer('test/star.svg', 10, 45)
symbol.setSvgFillColor(QColor('blue'))
symbol.setSvgOutlineWidth(3)
symbol.setOutputUnit(QgsUnitTypes.RenderPixels)
symbol.subSymbol().setWidth(10)

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

self.assertExternalGraphic(root, 0,
'test/star.svg?fill=%230000ff&fill-opacity=1&outline=%23000000&outline-opacity=1&outline-width=3', 'image/svg+xml')
self.assertExternalGraphic(root, 1,
'test/star.svg', 'image/svg+xml')
self.assertWellKnownMark(root, 0, 'square', '#0000ff', '#000000', 3)

self.assertStaticRotation(root, '45')
self.assertStaticSize(root, '10')
# width of the svg outline
self.assertStrokeWidth(root, 1, 3)
# width of the polygon outline
self.assertStrokeWidth(root, 3, 0.26)
lineSymbolizer = root.elementsByTagName('se:LineSymbolizer').item(0).toElement()
self.assertStrokeWidth(lineSymbolizer, 1, 10)

def testLineFillDefault(self):
symbol = QgsLinePatternFillSymbolLayer()
Expand Down Expand Up @@ -497,10 +532,41 @@ def assertStaticSize(self, root, expectedValue):
size = root.elementsByTagName('se:Size').item(0)
self.assertEqual(expectedValue, size.firstChild().nodeValue())

def assertExternalGraphic(self, root, index, expectedLink, expectedFormat):
graphic = root.elementsByTagName('se:ExternalGraphic').item(index)
onlineResource = graphic.firstChildElement('se:OnlineResource')
self.assertEqual(expectedLink, onlineResource.attribute('xlink:href'))
format = graphic.firstChildElement('se:Format')
self.assertEqual(expectedFormat, format.firstChild().nodeValue())

def assertStaticPerpendicularOffset(self, root, expectedValue):
offset = root.elementsByTagName('se:PerpendicularOffset').item(0)
self.assertEqual(expectedValue, offset.firstChild().nodeValue())

def assertWellKnownMark(self, root, index, expectedName, expectedFill, expectedStroke, expectedStrokeWidth):
mark = root.elementsByTagName('se:Mark').item(index)
wkn = mark.firstChildElement('se:WellKnownName')
self.assertEqual(expectedName, wkn.text())

fill = mark.firstChildElement('se:Fill')
if expectedFill is None:
self.assertTrue(fill.isNull())
else:
parameter = fill.firstChildElement('se:SvgParameter')
self.assertEqual('fill', parameter.attribute('name'))
self.assertEqual(expectedFill, parameter.text())

stroke = mark.firstChildElement('se:Stroke')
if expectedStroke is None:
self.assertTrue(stroke.isNull())
else:
parameter = stroke.firstChildElement('se:SvgParameter')
self.assertEqual('stroke', parameter.attribute('name'))
self.assertEqual(expectedStroke, parameter.text())
parameter = parameter.nextSiblingElement('se:SvgParameter')
self.assertEqual('stroke-width', parameter.attribute('name'))
self.assertEqual(str(expectedStrokeWidth), parameter.text())

def symbolToSld(self, symbolLayer):
dom = QDomDocument()
root = dom.createElement("FakeRoot")
Expand Down

0 comments on commit 1ef7ed5

Please sign in to comment.