Skip to content

Commit a600b51

Browse files
committedMar 22, 2018
[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
1 parent 18408fa commit a600b51

File tree

6 files changed

+203
-28
lines changed

6 files changed

+203
-28
lines changed
 

‎python/core/layout/qgslayoutexporter.sip.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ Constructor for SvgExportSettings
276276

277277
bool exportAsLayers;
278278

279+
bool exportMetadata;
280+
279281
QgsLayoutRenderContext::Flags flags;
280282

281283
};

‎src/app/layout/qgslayoutdesignerdialog.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3816,6 +3816,7 @@ bool QgsLayoutDesignerDialog::getSvgExportSettings( QgsLayoutExporter::SvgExport
38163816
double rightMargin = 0.0;
38173817
double bottomMargin = 0.0;
38183818
double leftMargin = 0.0;
3819+
bool includeMetadata = true;
38193820
if ( mLayout )
38203821
{
38213822
mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool();
@@ -3825,6 +3826,7 @@ bool QgsLayoutDesignerDialog::getSvgExportSettings( QgsLayoutExporter::SvgExport
38253826
rightMargin = mLayout->customProperty( QStringLiteral( "svgCropMarginRight" ), 0 ).toInt();
38263827
bottomMargin = mLayout->customProperty( QStringLiteral( "svgCropMarginBottom" ), 0 ).toInt();
38273828
leftMargin = mLayout->customProperty( QStringLiteral( "svgCropMarginLeft" ), 0 ).toInt();
3829+
includeMetadata = mLayout->customProperty( QStringLiteral( "svgIncludeMetadata" ), 1 ).toBool();
38283830
}
38293831

38303832
// open options dialog
@@ -3839,6 +3841,7 @@ bool QgsLayoutDesignerDialog::getSvgExportSettings( QgsLayoutExporter::SvgExport
38393841
options.mRightMarginSpinBox->setValue( rightMargin );
38403842
options.mBottomMarginSpinBox->setValue( bottomMargin );
38413843
options.mLeftMarginSpinBox->setValue( leftMargin );
3844+
options.mIncludeMetadataCheckbox->setChecked( includeMetadata );
38423845

38433846
if ( dialog.exec() != QDialog::Accepted )
38443847
return false;
@@ -3849,6 +3852,7 @@ bool QgsLayoutDesignerDialog::getSvgExportSettings( QgsLayoutExporter::SvgExport
38493852
marginRight = options.mRightMarginSpinBox->value();
38503853
marginBottom = options.mBottomMarginSpinBox->value();
38513854
marginLeft = options.mLeftMarginSpinBox->value();
3855+
includeMetadata = options.mIncludeMetadataCheckbox->isChecked();
38523856

38533857
if ( mLayout )
38543858
{
@@ -3859,12 +3863,14 @@ bool QgsLayoutDesignerDialog::getSvgExportSettings( QgsLayoutExporter::SvgExport
38593863
mLayout->setCustomProperty( QStringLiteral( "svgCropMarginRight" ), marginRight );
38603864
mLayout->setCustomProperty( QStringLiteral( "svgCropMarginBottom" ), marginBottom );
38613865
mLayout->setCustomProperty( QStringLiteral( "svgCropMarginLeft" ), marginLeft );
3866+
mLayout->setCustomProperty( QStringLiteral( "svgIncludeMetadata" ), includeMetadata ? 1 : 0 );
38623867
}
38633868

38643869
settings.cropToContents = clipToContent;
38653870
settings.cropMargins = QgsMargins( marginLeft, marginTop, marginRight, marginBottom );
38663871
settings.forceVectorOutput = options.mForceVectorCheckBox->isChecked();
38673872
settings.exportAsLayers = groupLayers;
3873+
settings.exportMetadata = includeMetadata;
38683874

38693875
exportAsText = options.chkTextAsOutline->isChecked();
38703876
return true;

‎src/core/layout/qgslayoutexporter.cpp

Lines changed: 123 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -849,7 +849,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
849849
}
850850
}
851851

852-
ExportResult result = renderToLayeredSvg( settings, width, height, i, bounds, fileName, svgLayerId, layerName, svg, svgDocRoot );
852+
ExportResult result = renderToLayeredSvg( settings, width, height, i, bounds, fileName, svgLayerId, layerName, svg, svgDocRoot, settings.exportMetadata );
853853
if ( result != Success )
854854
return result;
855855

@@ -861,6 +861,9 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
861861
}
862862
}
863863

864+
if ( settings.exportMetadata )
865+
appendMetadataToSvg( svg );
866+
864867
QFile out( fileName );
865868
bool openOk = out.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate );
866869
if ( !openOk )
@@ -873,27 +876,59 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
873876
}
874877
else
875878
{
876-
QSvgGenerator generator;
877-
generator.setTitle( mLayout->project()->title() );
878-
generator.setFileName( fileName );
879-
generator.setSize( QSize( width, height ) );
880-
generator.setViewBox( QRect( 0, 0, width, height ) );
881-
generator.setResolution( settings.dpi );
882-
883-
QPainter p;
884-
bool createOk = p.begin( &generator );
885-
if ( !createOk )
879+
QBuffer svgBuffer;
886880
{
887-
mErrorFileName = fileName;
888-
return FileError;
881+
QSvgGenerator generator;
882+
if ( settings.exportMetadata )
883+
{
884+
generator.setTitle( mLayout->project()->metadata().title() );
885+
generator.setDescription( mLayout->project()->metadata().abstract() );
886+
}
887+
generator.setOutputDevice( &svgBuffer );
888+
generator.setSize( QSize( width, height ) );
889+
generator.setViewBox( QRect( 0, 0, width, height ) );
890+
generator.setResolution( settings.dpi );
891+
892+
QPainter p;
893+
bool createOk = p.begin( &generator );
894+
if ( !createOk )
895+
{
896+
mErrorFileName = fileName;
897+
return FileError;
898+
}
899+
900+
if ( settings.cropToContents )
901+
renderRegion( &p, bounds );
902+
else
903+
renderPage( &p, i );
904+
905+
p.end();
889906
}
907+
{
908+
svgBuffer.close();
909+
svgBuffer.open( QIODevice::ReadOnly );
910+
QDomDocument svg;
911+
QString errorMsg;
912+
int errorLine;
913+
if ( ! svg.setContent( &svgBuffer, false, &errorMsg, &errorLine ) )
914+
{
915+
mErrorFileName = fileName;
916+
return SvgLayerError;
917+
}
890918

891-
if ( settings.cropToContents )
892-
renderRegion( &p, bounds );
893-
else
894-
renderPage( &p, i );
919+
if ( settings.exportMetadata )
920+
appendMetadataToSvg( svg );
921+
922+
QFile out( fileName );
923+
bool openOk = out.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate );
924+
if ( !openOk )
925+
{
926+
mErrorFileName = fileName;
927+
return FileError;
928+
}
895929

896-
p.end();
930+
out.write( svg.toByteArray() );
931+
}
897932
}
898933
}
899934

@@ -1067,15 +1102,18 @@ void QgsLayoutExporter::updatePrinterPageSize( QgsLayout *layout, QPrinter &prin
10671102
printer.setPaperSize( pageSizeMM.toQSizeF(), QPrinter::Millimeter );
10681103
}
10691104

1070-
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
1105+
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
10711106
{
10721107
QBuffer svgBuffer;
10731108
{
10741109
QSvgGenerator generator;
1075-
if ( const QgsMasterLayoutInterface *l = dynamic_cast< const QgsMasterLayoutInterface * >( mLayout.data() ) )
1076-
generator.setTitle( l->name() );
1077-
else if ( mLayout->project() )
1078-
generator.setTitle( mLayout->project()->title() );
1110+
if ( includeMetadata )
1111+
{
1112+
if ( const QgsMasterLayoutInterface *l = dynamic_cast< const QgsMasterLayoutInterface * >( mLayout.data() ) )
1113+
generator.setTitle( l->name() );
1114+
else if ( mLayout->project() )
1115+
generator.setTitle( mLayout->project()->title() );
1116+
}
10791117

10801118
generator.setOutputDevice( &svgBuffer );
10811119
generator.setSize( QSize( width, height ) );
@@ -1122,6 +1160,68 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::renderToLayeredSvg( const Svg
11221160
return Success;
11231161
}
11241162

1163+
void QgsLayoutExporter::appendMetadataToSvg( QDomDocument &svg ) const
1164+
{
1165+
const QgsProjectMetadata &metadata = mLayout->project()->metadata();
1166+
QDomElement metadataElement = svg.createElement( QStringLiteral( "metadata" ) );
1167+
metadataElement.setAttribute( QStringLiteral( "id" ), QStringLiteral( "qgismetadata" ) );
1168+
QDomElement rdfElement = svg.createElement( QStringLiteral( "rdf:RDF" ) );
1169+
QDomElement workElement = svg.createElement( QStringLiteral( "cc:Work" ) );
1170+
1171+
auto addTextNode = [&workElement, &svg]( const QString & tag, const QString & value )
1172+
{
1173+
QDomElement element = svg.createElement( tag );
1174+
QDomText t = svg.createTextNode( value );
1175+
element.appendChild( t );
1176+
workElement.appendChild( element );
1177+
};
1178+
1179+
addTextNode( QStringLiteral( "dc:format" ), QStringLiteral( "image/svg+xml" ) );
1180+
addTextNode( QStringLiteral( "dc:title" ), metadata.title() );
1181+
addTextNode( QStringLiteral( "dc:date" ), metadata.creationDateTime().toString( Qt::ISODate ) );
1182+
addTextNode( QStringLiteral( "dc:identifier" ), metadata.identifier() );
1183+
addTextNode( QStringLiteral( "dc:description" ), metadata.abstract() );
1184+
1185+
auto addAgentNode = [&workElement, &svg]( const QString & tag, const QString & value )
1186+
{
1187+
QDomElement element = svg.createElement( tag );
1188+
QDomElement agentElement = svg.createElement( QStringLiteral( "cc:Agent" ) );
1189+
QDomElement titleElement = svg.createElement( QStringLiteral( "dc:title" ) );
1190+
QDomText t = svg.createTextNode( value );
1191+
titleElement.appendChild( t );
1192+
agentElement.appendChild( titleElement );
1193+
element.appendChild( agentElement );
1194+
workElement.appendChild( element );
1195+
};
1196+
1197+
addAgentNode( QStringLiteral( "dc:creator" ), metadata.author() );
1198+
addAgentNode( QStringLiteral( "dc:publisher" ), QStringLiteral( "QGIS %1" ).arg( Qgis::QGIS_VERSION ) );
1199+
1200+
// keywords
1201+
{
1202+
QDomElement element = svg.createElement( QStringLiteral( "dc:subject" ) );
1203+
QDomElement bagElement = svg.createElement( QStringLiteral( "rdf:Bag" ) );
1204+
QgsAbstractMetadataBase::KeywordMap keywords = metadata.keywords();
1205+
for ( auto it = keywords.constBegin(); it != keywords.constEnd(); ++it )
1206+
{
1207+
const QStringList words = it.value();
1208+
for ( const QString &keyword : words )
1209+
{
1210+
QDomElement liElement = svg.createElement( QStringLiteral( "rdf:li" ) );
1211+
QDomText t = svg.createTextNode( keyword );
1212+
liElement.appendChild( t );
1213+
bagElement.appendChild( liElement );
1214+
}
1215+
}
1216+
element.appendChild( bagElement );
1217+
workElement.appendChild( element );
1218+
}
1219+
1220+
rdfElement.appendChild( workElement );
1221+
metadataElement.appendChild( rdfElement );
1222+
svg.documentElement().appendChild( metadataElement );
1223+
}
1224+
11251225
std::unique_ptr<double[]> QgsLayoutExporter::computeGeoTransform( const QgsLayoutItemMap *map, const QRectF &region, double dpi ) const
11261226
{
11271227
if ( !map )

‎src/core/layout/qgslayoutexporter.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,14 @@ class CORE_EXPORT QgsLayoutExporter
384384
*/
385385
bool exportAsLayers = false;
386386

387+
/**
388+
* Indicates whether SVG export should include RDF metadata generated
389+
* from the layout's project's metadata.
390+
*
391+
* \since QGIS 3.2
392+
*/
393+
bool exportMetadata = true;
394+
387395
/**
388396
* Layout context flags, which control how the export will be created.
389397
*/
@@ -519,7 +527,9 @@ class CORE_EXPORT QgsLayoutExporter
519527

520528
ExportResult renderToLayeredSvg( const SvgExportSettings &settings, double width, double height, int page, QRectF bounds,
521529
const QString &filename, int svgLayerId, const QString &layerName,
522-
QDomDocument &svg, QDomNode &svgDocRoot ) const;
530+
QDomDocument &svg, QDomNode &svgDocRoot, bool includeMetadata ) const;
531+
532+
void appendMetadataToSvg( QDomDocument &svg ) const;
523533

524534
friend class TestQgsLayout;
525535

‎src/ui/layout/qgssvgexportoptions.ui

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<x>0</x>
88
<y>0</y>
99
<width>489</width>
10-
<height>319</height>
10+
<height>351</height>
1111
</rect>
1212
</property>
1313
<property name="windowTitle">
@@ -23,7 +23,7 @@
2323
<item>
2424
<widget class="QCheckBox" name="chkMapLayersAsGroup">
2525
<property name="text">
26-
<string>Export map layers as svg groups (may affect label placement)</string>
26+
<string>Export map layers as SVG groups (may affect label placement)</string>
2727
</property>
2828
<property name="checked">
2929
<bool>false</bool>
@@ -56,6 +56,19 @@
5656
</property>
5757
</widget>
5858
</item>
59+
<item>
60+
<widget class="QCheckBox" name="mIncludeMetadataCheckbox">
61+
<property name="toolTip">
62+
<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>
63+
</property>
64+
<property name="text">
65+
<string>Export RDF metadata</string>
66+
</property>
67+
<property name="checked">
68+
<bool>true</bool>
69+
</property>
70+
</widget>
71+
</item>
5972
</layout>
6073
</widget>
6174
</item>
@@ -205,8 +218,9 @@
205218
<tabstops>
206219
<tabstop>chkMapLayersAsGroup</tabstop>
207220
<tabstop>chkTextAsOutline</tabstop>
208-
<tabstop>mClipToContentGroupBox</tabstop>
209221
<tabstop>mForceVectorCheckBox</tabstop>
222+
<tabstop>mIncludeMetadataCheckbox</tabstop>
223+
<tabstop>mClipToContentGroupBox</tabstop>
210224
<tabstop>mTopMarginSpinBox</tabstop>
211225
<tabstop>mLeftMarginSpinBox</tabstop>
212226
<tabstop>mRightMarginSpinBox</tabstop>

‎tests/src/python/test_qgslayoutexporter.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import shutil
1919
import os
2020
import subprocess
21+
from xml.dom import minidom
2122

2223
from qgis.core import (QgsMultiRenderChecker,
2324
QgsLayoutExporter,
@@ -39,7 +40,7 @@
3940
QgsPrintLayout,
4041
QgsSingleSymbolRenderer,
4142
QgsReport)
42-
from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt
43+
from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt, QDateTime, QDate, QTime
4344
from qgis.PyQt.QtGui import QImage, QPainter
4445
from qgis.PyQt.QtPrintSupport import QPrinter
4546
from qgis.PyQt.QtSvg import QSvgRenderer, QSvgGenerator
@@ -418,6 +419,14 @@ def testExportToPdf(self):
418419
self.assertTrue(self.checkImage('exporttopdfdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
419420

420421
def testExportToSvg(self):
422+
md = QgsProject.instance().metadata()
423+
md.setTitle('proj title')
424+
md.setAuthor('proj author')
425+
md.setCreationDateTime(QDateTime(QDate(2011, 5, 3), QTime(9, 4, 5)))
426+
md.setIdentifier('proj identifier')
427+
md.setAbstract('proj abstract')
428+
md.setKeywords({'kw': ['kw1', 'kw2']})
429+
QgsProject.instance().setMetadata(md)
421430
l = QgsLayout(QgsProject.instance())
422431
l.initializeDefaults()
423432

@@ -453,13 +462,30 @@ def testExportToSvg(self):
453462
settings = QgsLayoutExporter.SvgExportSettings()
454463
settings.dpi = 80
455464
settings.forceVectorOutput = False
465+
settings.exportMetadata = True
456466

457467
svg_file_path = os.path.join(self.basetestpath, 'test_exporttosvgdpi.svg')
458468
svg_file_path_2 = os.path.join(self.basetestpath, 'test_exporttosvgdpi_2.svg')
459469
self.assertEqual(exporter.exportToSvg(svg_file_path, settings), QgsLayoutExporter.Success)
460470
self.assertTrue(os.path.exists(svg_file_path))
461471
self.assertTrue(os.path.exists(svg_file_path_2))
462472

473+
# metadata
474+
def checkMetadata(f, expected):
475+
# ideally we'd check the path too - but that's very complex given that
476+
# the output from Qt svg generator isn't valid XML, and no Python standard library
477+
# xml parser handles invalid xml...
478+
self.assertEqual('proj title' in open(f).read(), expected)
479+
self.assertEqual('proj author' in open(f).read(), expected)
480+
self.assertEqual('proj identifier' in open(f).read(), expected)
481+
self.assertEqual('2011-05-03' in open(f).read(), expected)
482+
self.assertEqual('proj abstract' in open(f).read(), expected)
483+
self.assertEqual('kw1' in open(f).read(), expected)
484+
self.assertEqual('kw2' in open(f).read(), expected)
485+
486+
for f in [svg_file_path, svg_file_path_2]:
487+
checkMetadata(f, True)
488+
463489
rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttosvgdpi.png')
464490
svgToPng(svg_file_path, rendered_page_1, width=936)
465491
rendered_page_2 = os.path.join(self.basetestpath, 'test_exporttosvgdpi2.png')
@@ -468,8 +494,15 @@ def testExportToSvg(self):
468494
self.assertTrue(self.checkImage('exporttosvgdpi_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
469495
self.assertTrue(self.checkImage('exporttosvgdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
470496

497+
# no metadata
498+
settings.exportMetadata = False
499+
self.assertEqual(exporter.exportToSvg(svg_file_path, settings), QgsLayoutExporter.Success)
500+
for f in [svg_file_path, svg_file_path_2]:
501+
checkMetadata(f, False)
502+
471503
# layered
472504
settings.exportAsLayers = True
505+
settings.exportMetadata = True
473506

474507
svg_file_path = os.path.join(self.basetestpath, 'test_exporttosvglayered.svg')
475508
svg_file_path_2 = os.path.join(self.basetestpath, 'test_exporttosvglayered_2.svg')
@@ -485,6 +518,16 @@ def testExportToSvg(self):
485518
self.assertTrue(self.checkImage('exporttosvglayered_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
486519
self.assertTrue(self.checkImage('exporttosvglayered_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
487520

521+
for f in [svg_file_path, svg_file_path_2]:
522+
checkMetadata(f, True)
523+
524+
# layered no metadata
525+
settings.exportAsLayers = True
526+
settings.exportMetadata = False
527+
self.assertEqual(exporter.exportToSvg(svg_file_path, settings), QgsLayoutExporter.Success)
528+
for f in [svg_file_path, svg_file_path_2]:
529+
checkMetadata(f, False)
530+
488531
def testPrint(self):
489532
l = QgsLayout(QgsProject.instance())
490533
l.initializeDefaults()

0 commit comments

Comments
 (0)
Please sign in to comment.