Export parametric SVG parameters, will fallback symbols for the system that cannot understand them

This commit is contained in:
Andrea Aime 2016-10-09 11:57:38 +02:00
parent 5984b21852
commit 701d4440ac
6 changed files with 180 additions and 19 deletions

View File

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

View File

@ -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
{
@ -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;

View File

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

View File

@ -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 )
@ -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 )

View File

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

View File

@ -60,9 +60,9 @@ class TestQgsSymbolLayerCreateSld(unittest.TestCase):
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())
@ -127,11 +127,21 @@ class TestQgsSymbolLayerCreateSld(unittest.TestCase):
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')
@ -141,11 +151,21 @@ class TestQgsSymbolLayerCreateSld(unittest.TestCase):
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)
@ -154,7 +174,7 @@ class TestQgsSymbolLayerCreateSld(unittest.TestCase):
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')
@ -300,32 +320,47 @@ class TestQgsSymbolLayerCreateSld(unittest.TestCase):
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()
@ -497,10 +532,41 @@ class TestQgsSymbolLayerCreateSld(unittest.TestCase):
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")