Skip to content

Commit

Permalink
Export parametric SVG parameters, will fallback symbols for the syste…
Browse files Browse the repository at this point in the history
…m that cannot understand them
  • Loading branch information
aaime committed Nov 5, 2016
1 parent 5984b21 commit 701d444
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 @@ -587,6 +587,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 701d444

Please sign in to comment.