[FEATURE][layouts] Export project metadata as SVG RDF metadata

Adds an option to include project metadata into SVG exports
generated from layouts, using the SVG RDF standard.

Developed for Arpa Piemonte (Dipartimento Tematico Geologia e Dissesto)
within ERIKUS project
This commit is contained in:
Nyall Dawson 2018-03-20 12:24:00 +10:00
parent 18408fa2c1
commit a600b51bad
6 changed files with 203 additions and 28 deletions

View File

@ -276,6 +276,8 @@ Constructor for SvgExportSettings
bool exportAsLayers;
bool exportMetadata;
QgsLayoutRenderContext::Flags flags;
};

View File

@ -3816,6 +3816,7 @@ bool QgsLayoutDesignerDialog::getSvgExportSettings( QgsLayoutExporter::SvgExport
double rightMargin = 0.0;
double bottomMargin = 0.0;
double leftMargin = 0.0;
bool includeMetadata = true;
if ( mLayout )
{
mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool();
@ -3825,6 +3826,7 @@ bool QgsLayoutDesignerDialog::getSvgExportSettings( QgsLayoutExporter::SvgExport
rightMargin = mLayout->customProperty( QStringLiteral( "svgCropMarginRight" ), 0 ).toInt();
bottomMargin = mLayout->customProperty( QStringLiteral( "svgCropMarginBottom" ), 0 ).toInt();
leftMargin = mLayout->customProperty( QStringLiteral( "svgCropMarginLeft" ), 0 ).toInt();
includeMetadata = mLayout->customProperty( QStringLiteral( "svgIncludeMetadata" ), 1 ).toBool();
}
// open options dialog
@ -3839,6 +3841,7 @@ bool QgsLayoutDesignerDialog::getSvgExportSettings( QgsLayoutExporter::SvgExport
options.mRightMarginSpinBox->setValue( rightMargin );
options.mBottomMarginSpinBox->setValue( bottomMargin );
options.mLeftMarginSpinBox->setValue( leftMargin );
options.mIncludeMetadataCheckbox->setChecked( includeMetadata );
if ( dialog.exec() != QDialog::Accepted )
return false;
@ -3849,6 +3852,7 @@ bool QgsLayoutDesignerDialog::getSvgExportSettings( QgsLayoutExporter::SvgExport
marginRight = options.mRightMarginSpinBox->value();
marginBottom = options.mBottomMarginSpinBox->value();
marginLeft = options.mLeftMarginSpinBox->value();
includeMetadata = options.mIncludeMetadataCheckbox->isChecked();
if ( mLayout )
{
@ -3859,12 +3863,14 @@ bool QgsLayoutDesignerDialog::getSvgExportSettings( QgsLayoutExporter::SvgExport
mLayout->setCustomProperty( QStringLiteral( "svgCropMarginRight" ), marginRight );
mLayout->setCustomProperty( QStringLiteral( "svgCropMarginBottom" ), marginBottom );
mLayout->setCustomProperty( QStringLiteral( "svgCropMarginLeft" ), marginLeft );
mLayout->setCustomProperty( QStringLiteral( "svgIncludeMetadata" ), includeMetadata ? 1 : 0 );
}
settings.cropToContents = clipToContent;
settings.cropMargins = QgsMargins( marginLeft, marginTop, marginRight, marginBottom );
settings.forceVectorOutput = options.mForceVectorCheckBox->isChecked();
settings.exportAsLayers = groupLayers;
settings.exportMetadata = includeMetadata;
exportAsText = options.chkTextAsOutline->isChecked();
return true;

View File

@ -849,7 +849,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
}
}
ExportResult result = renderToLayeredSvg( settings, width, height, i, bounds, fileName, svgLayerId, layerName, svg, svgDocRoot );
ExportResult result = renderToLayeredSvg( settings, width, height, i, bounds, fileName, svgLayerId, layerName, svg, svgDocRoot, settings.exportMetadata );
if ( result != Success )
return result;
@ -861,6 +861,9 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
}
}
if ( settings.exportMetadata )
appendMetadataToSvg( svg );
QFile out( fileName );
bool openOk = out.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate );
if ( !openOk )
@ -872,10 +875,16 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
out.write( svg.toByteArray() );
}
else
{
QBuffer svgBuffer;
{
QSvgGenerator generator;
generator.setTitle( mLayout->project()->title() );
generator.setFileName( fileName );
if ( settings.exportMetadata )
{
generator.setTitle( mLayout->project()->metadata().title() );
generator.setDescription( mLayout->project()->metadata().abstract() );
}
generator.setOutputDevice( &svgBuffer );
generator.setSize( QSize( width, height ) );
generator.setViewBox( QRect( 0, 0, width, height ) );
generator.setResolution( settings.dpi );
@ -895,6 +904,32 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
p.end();
}
{
svgBuffer.close();
svgBuffer.open( QIODevice::ReadOnly );
QDomDocument svg;
QString errorMsg;
int errorLine;
if ( ! svg.setContent( &svgBuffer, false, &errorMsg, &errorLine ) )
{
mErrorFileName = fileName;
return SvgLayerError;
}
if ( settings.exportMetadata )
appendMetadataToSvg( svg );
QFile out( fileName );
bool openOk = out.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate );
if ( !openOk )
{
mErrorFileName = fileName;
return FileError;
}
out.write( svg.toByteArray() );
}
}
}
return Success;
@ -1067,15 +1102,18 @@ void QgsLayoutExporter::updatePrinterPageSize( QgsLayout *layout, QPrinter &prin
printer.setPaperSize( pageSizeMM.toQSizeF(), QPrinter::Millimeter );
}
QgsLayoutExporter::ExportResult QgsLayoutExporter::renderToLayeredSvg( const SvgExportSettings &settings, double width, double height, int page, QRectF bounds, const QString &filename, int svgLayerId, const QString &layerName, QDomDocument &svg, QDomNode &svgDocRoot ) const
QgsLayoutExporter::ExportResult QgsLayoutExporter::renderToLayeredSvg( const SvgExportSettings &settings, double width, double height, int page, QRectF bounds, const QString &filename, int svgLayerId, const QString &layerName, QDomDocument &svg, QDomNode &svgDocRoot, bool includeMetadata ) const
{
QBuffer svgBuffer;
{
QSvgGenerator generator;
if ( includeMetadata )
{
if ( const QgsMasterLayoutInterface *l = dynamic_cast< const QgsMasterLayoutInterface * >( mLayout.data() ) )
generator.setTitle( l->name() );
else if ( mLayout->project() )
generator.setTitle( mLayout->project()->title() );
}
generator.setOutputDevice( &svgBuffer );
generator.setSize( QSize( width, height ) );
@ -1122,6 +1160,68 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::renderToLayeredSvg( const Svg
return Success;
}
void QgsLayoutExporter::appendMetadataToSvg( QDomDocument &svg ) const
{
const QgsProjectMetadata &metadata = mLayout->project()->metadata();
QDomElement metadataElement = svg.createElement( QStringLiteral( "metadata" ) );
metadataElement.setAttribute( QStringLiteral( "id" ), QStringLiteral( "qgismetadata" ) );
QDomElement rdfElement = svg.createElement( QStringLiteral( "rdf:RDF" ) );
QDomElement workElement = svg.createElement( QStringLiteral( "cc:Work" ) );
auto addTextNode = [&workElement, &svg]( const QString & tag, const QString & value )
{
QDomElement element = svg.createElement( tag );
QDomText t = svg.createTextNode( value );
element.appendChild( t );
workElement.appendChild( element );
};
addTextNode( QStringLiteral( "dc:format" ), QStringLiteral( "image/svg+xml" ) );
addTextNode( QStringLiteral( "dc:title" ), metadata.title() );
addTextNode( QStringLiteral( "dc:date" ), metadata.creationDateTime().toString( Qt::ISODate ) );
addTextNode( QStringLiteral( "dc:identifier" ), metadata.identifier() );
addTextNode( QStringLiteral( "dc:description" ), metadata.abstract() );
auto addAgentNode = [&workElement, &svg]( const QString & tag, const QString & value )
{
QDomElement element = svg.createElement( tag );
QDomElement agentElement = svg.createElement( QStringLiteral( "cc:Agent" ) );
QDomElement titleElement = svg.createElement( QStringLiteral( "dc:title" ) );
QDomText t = svg.createTextNode( value );
titleElement.appendChild( t );
agentElement.appendChild( titleElement );
element.appendChild( agentElement );
workElement.appendChild( element );
};
addAgentNode( QStringLiteral( "dc:creator" ), metadata.author() );
addAgentNode( QStringLiteral( "dc:publisher" ), QStringLiteral( "QGIS %1" ).arg( Qgis::QGIS_VERSION ) );
// keywords
{
QDomElement element = svg.createElement( QStringLiteral( "dc:subject" ) );
QDomElement bagElement = svg.createElement( QStringLiteral( "rdf:Bag" ) );
QgsAbstractMetadataBase::KeywordMap keywords = metadata.keywords();
for ( auto it = keywords.constBegin(); it != keywords.constEnd(); ++it )
{
const QStringList words = it.value();
for ( const QString &keyword : words )
{
QDomElement liElement = svg.createElement( QStringLiteral( "rdf:li" ) );
QDomText t = svg.createTextNode( keyword );
liElement.appendChild( t );
bagElement.appendChild( liElement );
}
}
element.appendChild( bagElement );
workElement.appendChild( element );
}
rdfElement.appendChild( workElement );
metadataElement.appendChild( rdfElement );
svg.documentElement().appendChild( metadataElement );
}
std::unique_ptr<double[]> QgsLayoutExporter::computeGeoTransform( const QgsLayoutItemMap *map, const QRectF &region, double dpi ) const
{
if ( !map )

View File

@ -384,6 +384,14 @@ class CORE_EXPORT QgsLayoutExporter
*/
bool exportAsLayers = false;
/**
* Indicates whether SVG export should include RDF metadata generated
* from the layout's project's metadata.
*
* \since QGIS 3.2
*/
bool exportMetadata = true;
/**
* Layout context flags, which control how the export will be created.
*/
@ -519,7 +527,9 @@ class CORE_EXPORT QgsLayoutExporter
ExportResult renderToLayeredSvg( const SvgExportSettings &settings, double width, double height, int page, QRectF bounds,
const QString &filename, int svgLayerId, const QString &layerName,
QDomDocument &svg, QDomNode &svgDocRoot ) const;
QDomDocument &svg, QDomNode &svgDocRoot, bool includeMetadata ) const;
void appendMetadataToSvg( QDomDocument &svg ) const;
friend class TestQgsLayout;

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>489</width>
<height>319</height>
<height>351</height>
</rect>
</property>
<property name="windowTitle">
@ -23,7 +23,7 @@
<item>
<widget class="QCheckBox" name="chkMapLayersAsGroup">
<property name="text">
<string>Export map layers as svg groups (may affect label placement)</string>
<string>Export map layers as SVG groups (may affect label placement)</string>
</property>
<property name="checked">
<bool>false</bool>
@ -56,6 +56,19 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="mIncludeMetadataCheckbox">
<property name="toolTip">
<string>If checked, the layout will always be kept as vector objects when exported to a compatible format, even if the appearance of the resultant file does not match the layouts settings. If unchecked, some elements in the layout may be rasterized in order to keep their appearance intact.</string>
</property>
<property name="text">
<string>Export RDF metadata</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -205,8 +218,9 @@
<tabstops>
<tabstop>chkMapLayersAsGroup</tabstop>
<tabstop>chkTextAsOutline</tabstop>
<tabstop>mClipToContentGroupBox</tabstop>
<tabstop>mForceVectorCheckBox</tabstop>
<tabstop>mIncludeMetadataCheckbox</tabstop>
<tabstop>mClipToContentGroupBox</tabstop>
<tabstop>mTopMarginSpinBox</tabstop>
<tabstop>mLeftMarginSpinBox</tabstop>
<tabstop>mRightMarginSpinBox</tabstop>

View File

@ -18,6 +18,7 @@ import tempfile
import shutil
import os
import subprocess
from xml.dom import minidom
from qgis.core import (QgsMultiRenderChecker,
QgsLayoutExporter,
@ -39,7 +40,7 @@ from qgis.core import (QgsMultiRenderChecker,
QgsPrintLayout,
QgsSingleSymbolRenderer,
QgsReport)
from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt
from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt, QDateTime, QDate, QTime
from qgis.PyQt.QtGui import QImage, QPainter
from qgis.PyQt.QtPrintSupport import QPrinter
from qgis.PyQt.QtSvg import QSvgRenderer, QSvgGenerator
@ -418,6 +419,14 @@ class TestQgsLayoutExporter(unittest.TestCase):
self.assertTrue(self.checkImage('exporttopdfdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
def testExportToSvg(self):
md = QgsProject.instance().metadata()
md.setTitle('proj title')
md.setAuthor('proj author')
md.setCreationDateTime(QDateTime(QDate(2011, 5, 3), QTime(9, 4, 5)))
md.setIdentifier('proj identifier')
md.setAbstract('proj abstract')
md.setKeywords({'kw': ['kw1', 'kw2']})
QgsProject.instance().setMetadata(md)
l = QgsLayout(QgsProject.instance())
l.initializeDefaults()
@ -453,6 +462,7 @@ class TestQgsLayoutExporter(unittest.TestCase):
settings = QgsLayoutExporter.SvgExportSettings()
settings.dpi = 80
settings.forceVectorOutput = False
settings.exportMetadata = True
svg_file_path = os.path.join(self.basetestpath, 'test_exporttosvgdpi.svg')
svg_file_path_2 = os.path.join(self.basetestpath, 'test_exporttosvgdpi_2.svg')
@ -460,6 +470,22 @@ class TestQgsLayoutExporter(unittest.TestCase):
self.assertTrue(os.path.exists(svg_file_path))
self.assertTrue(os.path.exists(svg_file_path_2))
# metadata
def checkMetadata(f, expected):
# ideally we'd check the path too - but that's very complex given that
# the output from Qt svg generator isn't valid XML, and no Python standard library
# xml parser handles invalid xml...
self.assertEqual('proj title' in open(f).read(), expected)
self.assertEqual('proj author' in open(f).read(), expected)
self.assertEqual('proj identifier' in open(f).read(), expected)
self.assertEqual('2011-05-03' in open(f).read(), expected)
self.assertEqual('proj abstract' in open(f).read(), expected)
self.assertEqual('kw1' in open(f).read(), expected)
self.assertEqual('kw2' in open(f).read(), expected)
for f in [svg_file_path, svg_file_path_2]:
checkMetadata(f, True)
rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttosvgdpi.png')
svgToPng(svg_file_path, rendered_page_1, width=936)
rendered_page_2 = os.path.join(self.basetestpath, 'test_exporttosvgdpi2.png')
@ -468,8 +494,15 @@ class TestQgsLayoutExporter(unittest.TestCase):
self.assertTrue(self.checkImage('exporttosvgdpi_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
self.assertTrue(self.checkImage('exporttosvgdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
# no metadata
settings.exportMetadata = False
self.assertEqual(exporter.exportToSvg(svg_file_path, settings), QgsLayoutExporter.Success)
for f in [svg_file_path, svg_file_path_2]:
checkMetadata(f, False)
# layered
settings.exportAsLayers = True
settings.exportMetadata = True
svg_file_path = os.path.join(self.basetestpath, 'test_exporttosvglayered.svg')
svg_file_path_2 = os.path.join(self.basetestpath, 'test_exporttosvglayered_2.svg')
@ -485,6 +518,16 @@ class TestQgsLayoutExporter(unittest.TestCase):
self.assertTrue(self.checkImage('exporttosvglayered_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
self.assertTrue(self.checkImage('exporttosvglayered_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
for f in [svg_file_path, svg_file_path_2]:
checkMetadata(f, True)
# layered no metadata
settings.exportAsLayers = True
settings.exportMetadata = False
self.assertEqual(exporter.exportToSvg(svg_file_path, settings), QgsLayoutExporter.Success)
for f in [svg_file_path, svg_file_path_2]:
checkMetadata(f, False)
def testPrint(self):
l = QgsLayout(QgsProject.instance())
l.initializeDefaults()