mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-16 00:03:12 -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:
parent
18408fa2c1
commit
a600b51bad
@ -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 )
|
||||
@ -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 ®ion, 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>
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user