Skip to content
Permalink
Browse files

[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
  • Loading branch information
nyalldawson committed Mar 20, 2018
1 parent 18408fa commit a600b51badf72e67712cd621a4f6f6df297cd1fb
@@ -276,6 +276,8 @@ Constructor for SvgExportSettings

bool exportAsLayers;

bool exportMetadata;

QgsLayoutRenderContext::Flags flags;

};
@@ -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;
@@ -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 )
@@ -873,27 +876,59 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
}
else
{
QSvgGenerator generator;
generator.setTitle( mLayout->project()->title() );
generator.setFileName( fileName );
generator.setSize( QSize( width, height ) );
generator.setViewBox( QRect( 0, 0, width, height ) );
generator.setResolution( settings.dpi );

QPainter p;
bool createOk = p.begin( &generator );
if ( !createOk )
QBuffer svgBuffer;
{
mErrorFileName = fileName;
return FileError;
QSvgGenerator generator;
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 );

QPainter p;
bool createOk = p.begin( &generator );
if ( !createOk )
{
mErrorFileName = fileName;
return FileError;
}

if ( settings.cropToContents )
renderRegion( &p, bounds );
else
renderPage( &p, i );

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.cropToContents )
renderRegion( &p, bounds );
else
renderPage( &p, i );
if ( settings.exportMetadata )
appendMetadataToSvg( svg );

QFile out( fileName );
bool openOk = out.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate );
if ( !openOk )
{
mErrorFileName = fileName;
return FileError;
}

p.end();
out.write( svg.toByteArray() );
}
}
}

@@ -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 ( const QgsMasterLayoutInterface *l = dynamic_cast< const QgsMasterLayoutInterface * >( mLayout.data() ) )
generator.setTitle( l->name() );
else if ( mLayout->project() )
generator.setTitle( mLayout->project()->title() );
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 )
@@ -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;

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

0 comments on commit a600b51

Please sign in to comment.
You can’t perform that action at this time.