1
0
mirror of https://github.com/qgis/QGIS.git synced 2025-04-25 00:03:06 -04:00

[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

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

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

@ -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 ) if ( result != Success )
return result; return result;
@ -861,6 +861,9 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
} }
} }
if ( settings.exportMetadata )
appendMetadataToSvg( svg );
QFile out( fileName ); QFile out( fileName );
bool openOk = out.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate ); bool openOk = out.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate );
if ( !openOk ) if ( !openOk )
@ -872,10 +875,16 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
out.write( svg.toByteArray() ); out.write( svg.toByteArray() );
} }
else else
{
QBuffer svgBuffer;
{ {
QSvgGenerator generator; QSvgGenerator generator;
generator.setTitle( mLayout->project()->title() ); if ( settings.exportMetadata )
generator.setFileName( fileName ); {
generator.setTitle( mLayout->project()->metadata().title() );
generator.setDescription( mLayout->project()->metadata().abstract() );
}
generator.setOutputDevice( &svgBuffer );
generator.setSize( QSize( width, height ) ); generator.setSize( QSize( width, height ) );
generator.setViewBox( QRect( 0, 0, width, height ) ); generator.setViewBox( QRect( 0, 0, width, height ) );
generator.setResolution( settings.dpi ); generator.setResolution( settings.dpi );
@ -895,6 +904,32 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
p.end(); 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; return Success;
@ -1067,15 +1102,18 @@ void QgsLayoutExporter::updatePrinterPageSize( QgsLayout *layout, QPrinter &prin
printer.setPaperSize( pageSizeMM.toQSizeF(), QPrinter::Millimeter ); 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; QBuffer svgBuffer;
{ {
QSvgGenerator generator; QSvgGenerator generator;
if ( includeMetadata )
{
if ( const QgsMasterLayoutInterface *l = dynamic_cast< const QgsMasterLayoutInterface * >( mLayout.data() ) ) if ( const QgsMasterLayoutInterface *l = dynamic_cast< const QgsMasterLayoutInterface * >( mLayout.data() ) )
generator.setTitle( l->name() ); generator.setTitle( l->name() );
else if ( mLayout->project() ) else if ( mLayout->project() )
generator.setTitle( mLayout->project()->title() ); generator.setTitle( mLayout->project()->title() );
}
generator.setOutputDevice( &svgBuffer ); generator.setOutputDevice( &svgBuffer );
generator.setSize( QSize( width, height ) ); generator.setSize( QSize( width, height ) );
@ -1122,6 +1160,68 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::renderToLayeredSvg( const Svg
return Success; 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 std::unique_ptr<double[]> QgsLayoutExporter::computeGeoTransform( const QgsLayoutItemMap *map, const QRectF &region, double dpi ) const
{ {
if ( !map ) if ( !map )

@ -384,6 +384,14 @@ class CORE_EXPORT QgsLayoutExporter
*/ */
bool exportAsLayers = false; 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. * 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, ExportResult renderToLayeredSvg( const SvgExportSettings &settings, double width, double height, int page, QRectF bounds,
const QString &filename, int svgLayerId, const QString &layerName, 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; friend class TestQgsLayout;

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>489</width> <width>489</width>
<height>319</height> <height>351</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -23,7 +23,7 @@
<item> <item>
<widget class="QCheckBox" name="chkMapLayersAsGroup"> <widget class="QCheckBox" name="chkMapLayersAsGroup">
<property name="text"> <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>
<property name="checked"> <property name="checked">
<bool>false</bool> <bool>false</bool>
@ -56,6 +56,19 @@
</property> </property>
</widget> </widget>
</item> </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> </layout>
</widget> </widget>
</item> </item>
@ -205,8 +218,9 @@
<tabstops> <tabstops>
<tabstop>chkMapLayersAsGroup</tabstop> <tabstop>chkMapLayersAsGroup</tabstop>
<tabstop>chkTextAsOutline</tabstop> <tabstop>chkTextAsOutline</tabstop>
<tabstop>mClipToContentGroupBox</tabstop>
<tabstop>mForceVectorCheckBox</tabstop> <tabstop>mForceVectorCheckBox</tabstop>
<tabstop>mIncludeMetadataCheckbox</tabstop>
<tabstop>mClipToContentGroupBox</tabstop>
<tabstop>mTopMarginSpinBox</tabstop> <tabstop>mTopMarginSpinBox</tabstop>
<tabstop>mLeftMarginSpinBox</tabstop> <tabstop>mLeftMarginSpinBox</tabstop>
<tabstop>mRightMarginSpinBox</tabstop> <tabstop>mRightMarginSpinBox</tabstop>

@ -18,6 +18,7 @@ import tempfile
import shutil import shutil
import os import os
import subprocess import subprocess
from xml.dom import minidom
from qgis.core import (QgsMultiRenderChecker, from qgis.core import (QgsMultiRenderChecker,
QgsLayoutExporter, QgsLayoutExporter,
@ -39,7 +40,7 @@ from qgis.core import (QgsMultiRenderChecker,
QgsPrintLayout, QgsPrintLayout,
QgsSingleSymbolRenderer, QgsSingleSymbolRenderer,
QgsReport) 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.QtGui import QImage, QPainter
from qgis.PyQt.QtPrintSupport import QPrinter from qgis.PyQt.QtPrintSupport import QPrinter
from qgis.PyQt.QtSvg import QSvgRenderer, QSvgGenerator 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)) self.assertTrue(self.checkImage('exporttopdfdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
def testExportToSvg(self): 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 = QgsLayout(QgsProject.instance())
l.initializeDefaults() l.initializeDefaults()
@ -453,6 +462,7 @@ class TestQgsLayoutExporter(unittest.TestCase):
settings = QgsLayoutExporter.SvgExportSettings() settings = QgsLayoutExporter.SvgExportSettings()
settings.dpi = 80 settings.dpi = 80
settings.forceVectorOutput = False settings.forceVectorOutput = False
settings.exportMetadata = True
svg_file_path = os.path.join(self.basetestpath, 'test_exporttosvgdpi.svg') svg_file_path = os.path.join(self.basetestpath, 'test_exporttosvgdpi.svg')
svg_file_path_2 = os.path.join(self.basetestpath, 'test_exporttosvgdpi_2.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))
self.assertTrue(os.path.exists(svg_file_path_2)) 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') rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttosvgdpi.png')
svgToPng(svg_file_path, rendered_page_1, width=936) svgToPng(svg_file_path, rendered_page_1, width=936)
rendered_page_2 = os.path.join(self.basetestpath, 'test_exporttosvgdpi2.png') 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_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
self.assertTrue(self.checkImage('exporttosvgdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, 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 # layered
settings.exportAsLayers = True settings.exportAsLayers = True
settings.exportMetadata = True
svg_file_path = os.path.join(self.basetestpath, 'test_exporttosvglayered.svg') svg_file_path = os.path.join(self.basetestpath, 'test_exporttosvglayered.svg')
svg_file_path_2 = os.path.join(self.basetestpath, 'test_exporttosvglayered_2.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_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
self.assertTrue(self.checkImage('exporttosvglayered_page2', 'exporttopdfdpi_page2', rendered_page_2, 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): def testPrint(self):
l = QgsLayout(QgsProject.instance()) l = QgsLayout(QgsProject.instance())
l.initializeDefaults() l.initializeDefaults()