Skip to content

Commit a600b51

Browse files
committed
[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

+2
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

+6
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

+123-23
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

+11-1
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

+17-3
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>

0 commit comments

Comments
 (0)