Skip to content
Permalink
Browse files

[FEATURE][layouts] Export project metadata in PDF/image exports

Includes project metadata in PDF exports, and supported image
based formats.

Image based metadata support depends on the format and the
Qt library's handling of each particular format (e.g. PNG
outputs are well supported).

Developed for Arpa Piemonte (Dipartimento Tematico Geologia e Dissesto)
within ERIKUS project
  • Loading branch information
nyalldawson committed Mar 20, 2018
1 parent 1d4ff69 commit 2eacc4c4b41d1740f45349f6fcbab5f34f261588
@@ -135,6 +135,9 @@ Constructor for ImageExportSettings

bool generateWorldFile;

bool exportMetadata;


QgsLayoutRenderContext::Flags flags;

};
@@ -180,6 +183,8 @@ Constructor for PdfExportSettings

bool forceVectorOutput;

bool exportMetadata;

QgsLayoutRenderContext::Flags flags;

};
@@ -363,15 +363,16 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString
return MemoryError;
}

if ( !saveImage( image, outputFilePath, pageDetails.extension ) )
if ( !saveImage( image, outputFilePath, pageDetails.extension, settings.exportMetadata ? mLayout->project() : nullptr ) )
{
mErrorFileName = outputFilePath;
return FileError;
}

if ( page == worldFilePageNo )
const bool shouldGeoreference = ( page == worldFilePageNo );
if ( shouldGeoreference )
{
georeferenceOutput( outputFilePath, nullptr, bounds, settings.dpi );
georeferenceOutputPrivate( outputFilePath, nullptr, bounds, settings.dpi, shouldGeoreference );

if ( settings.generateWorldFile )
{
@@ -481,9 +482,10 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f
ExportResult result = printPrivate( printer, p, false, settings.dpi, settings.rasterizeWholeImage );
p.end();

if ( mLayout->pageCollection()->pageCount() == 1 )
const bool shouldGeoreference = mLayout->pageCollection()->pageCount() == 1;
if ( shouldGeoreference || settings.exportMetadata )
{
georeferenceOutput( filePath, nullptr, QRectF(), settings.dpi );
georeferenceOutputPrivate( filePath, nullptr, QRectF(), settings.dpi, shouldGeoreference, settings.exportMetadata );
}
return result;
}
@@ -1326,35 +1328,76 @@ void QgsLayoutExporter::writeWorldFile( const QString &worldFileName, double a,
}

bool QgsLayoutExporter::georeferenceOutput( const QString &file, QgsLayoutItemMap *map, const QRectF &exportRegion, double dpi ) const
{
return georeferenceOutputPrivate( file, map, exportRegion, dpi, false );
}

bool QgsLayoutExporter::georeferenceOutputPrivate( const QString &file, QgsLayoutItemMap *map, const QRectF &exportRegion, double dpi, bool includeGeoreference, bool includeMetadata ) const
{
if ( !mLayout )
return false;

if ( !map )
if ( !map && includeGeoreference )
map = mLayout->referenceMap();

if ( !map )
return false; // no reference map
std::unique_ptr<double[]> t;

if ( dpi < 0 )
dpi = mLayout->renderContext().dpi();
if ( map && includeGeoreference )
{
if ( dpi < 0 )
dpi = mLayout->renderContext().dpi();

std::unique_ptr<double[]> t = computeGeoTransform( map, exportRegion, dpi );
if ( !t )
return false;
t = computeGeoTransform( map, exportRegion, dpi );
}

// important - we need to manually specify the DPI in advance, as GDAL will otherwise
// assume a DPI of 150
CPLSetConfigOption( "GDAL_PDF_DPI", QString::number( dpi ).toLocal8Bit().constData() );
gdal::dataset_unique_ptr outputDS( GDALOpen( file.toLocal8Bit().constData(), GA_Update ) );
if ( outputDS )
{
GDALSetGeoTransform( outputDS.get(), t.get() );
#if 0
//TODO - metadata can be set here, e.g.:
GDALSetMetadataItem( outputDS, "AUTHOR", "me", nullptr );
#endif
GDALSetProjection( outputDS.get(), map->crs().toWkt().toLocal8Bit().constData() );
if ( t )
GDALSetGeoTransform( outputDS.get(), t.get() );

if ( includeMetadata )
{
QString creationDateString;
const QDateTime creationDateTime = mLayout->project()->metadata().creationDateTime();
if ( creationDateTime.isValid() )
{
creationDateString = QStringLiteral( "D:%1" ).arg( mLayout->project()->metadata().creationDateTime().toString( QStringLiteral( "yyyyMMddHHmmss" ) ) );
if ( creationDateTime.timeZone().isValid() )
{
int offsetFromUtc = creationDateTime.timeZone().offsetFromUtc( creationDateTime );
creationDateString += ( offsetFromUtc >= 0 ) ? '+' : '-';
offsetFromUtc = std::abs( offsetFromUtc );
int offsetHours = offsetFromUtc / 3600;
int offsetMins = ( offsetFromUtc % 3600 ) / 60;
creationDateString += QStringLiteral( "%1'%2'" ).arg( offsetHours ).arg( offsetMins );
}
}
GDALSetMetadataItem( outputDS.get(), "CREATION_DATE", creationDateString.toLocal8Bit().constData(), nullptr );

GDALSetMetadataItem( outputDS.get(), "AUTHOR", mLayout->project()->metadata().author().toLocal8Bit().constData(), nullptr );
const QString creator = QStringLiteral( "QGIS %1" ).arg( Qgis::QGIS_VERSION );
GDALSetMetadataItem( outputDS.get(), "CREATOR", creator.toLocal8Bit().constData(), nullptr );
GDALSetMetadataItem( outputDS.get(), "PRODUCER", creator.toLocal8Bit().constData(), nullptr );
GDALSetMetadataItem( outputDS.get(), "SUBJECT", mLayout->project()->metadata().abstract().toLocal8Bit().constData(), nullptr );
GDALSetMetadataItem( outputDS.get(), "TITLE", mLayout->project()->metadata().title().toLocal8Bit().constData(), nullptr );

const QgsAbstractMetadataBase::KeywordMap keywords = mLayout->project()->metadata().keywords();
QStringList allKeywords;
for ( auto it = keywords.constBegin(); it != keywords.constEnd(); ++it )
{
allKeywords.append( it.value() );
}
allKeywords = allKeywords.toSet().toList();
const QString keywordString = allKeywords.join( ',' );
GDALSetMetadataItem( outputDS.get(), "KEYWORDS", keywordString.toLocal8Bit().constData(), nullptr );
}

if ( t )
GDALSetProjection( outputDS.get(), map->crs().toWkt().toLocal8Bit().constData() );
}
CPLSetConfigOption( "GDAL_PDF_DPI", nullptr );

@@ -1504,13 +1547,33 @@ QString QgsLayoutExporter::generateFileName( const PageExportDetails &details )
}
}

bool QgsLayoutExporter::saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat )
bool QgsLayoutExporter::saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat, QgsProject *projectForMetadata )
{
QImageWriter w( imageFilename, imageFormat.toLocal8Bit().constData() );
if ( imageFormat.compare( QLatin1String( "tiff" ), Qt::CaseInsensitive ) == 0 || imageFormat.compare( QLatin1String( "tif" ), Qt::CaseInsensitive ) == 0 )
{
w.setCompression( 1 ); //use LZW compression
}
if ( projectForMetadata )
{
w.setText( "Author", projectForMetadata->metadata().author() );
const QString creator = QStringLiteral( "QGIS %1" ).arg( Qgis::QGIS_VERSION );
w.setText( "Creator", creator );
w.setText( "Producer", creator );
w.setText( "Subject", projectForMetadata->metadata().abstract() );
w.setText( "Created", projectForMetadata->metadata().creationDateTime().toString( Qt::ISODate ) );
w.setText( "Title", projectForMetadata->metadata().title() );

const QgsAbstractMetadataBase::KeywordMap keywords = projectForMetadata->metadata().keywords();
QStringList allKeywords;
for ( auto it = keywords.constBegin(); it != keywords.constEnd(); ++it )
{
allKeywords.append( it.value() );
}
allKeywords = allKeywords.toSet().toList();
const QString keywordString = allKeywords.join( ',' );
w.setText( "Keywords", keywordString );
}
return w.write( image );
}

@@ -191,6 +191,15 @@ class CORE_EXPORT QgsLayoutExporter
*/
bool generateWorldFile = false;

/**
* Indicates whether image export should include 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.
*/
@@ -253,6 +262,14 @@ class CORE_EXPORT QgsLayoutExporter
*/
bool forceVectorOutput = false;

/**
* Indicates whether PDF export should include 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.
*/
@@ -481,7 +498,7 @@ class CORE_EXPORT QgsLayoutExporter
/**
* Saves an image to a file, possibly using format specific options (e.g. LZW compression for tiff)
*/
static bool saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat );
static bool saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat, QgsProject *projectForMetadata );

/**
* Computes a GDAL style geotransform for georeferencing a layout.
@@ -531,6 +548,9 @@ class CORE_EXPORT QgsLayoutExporter

void appendMetadataToSvg( QDomDocument &svg ) const;

bool georeferenceOutputPrivate( const QString &file, QgsLayoutItemMap *referenceMap = nullptr,
const QRectF &exportRegion = QRectF(), double dpi = -1, bool includeGeoreference = true, bool includeMetadata = false ) const;

friend class TestQgsLayout;

};
@@ -207,7 +207,7 @@
<item row="2" column="1">
<widget class="QDateTimeEdit" name="mCreationDateTimeEdit">
<property name="displayFormat">
<string>yyyy-mm-dd HH:mm</string>
<string>yyyy-MM-dd HH:mm:ss</string>
</property>
<property name="calendarPopup">
<bool>true</bool>
@@ -19,6 +19,7 @@
import os
import subprocess
from xml.dom import minidom
from osgeo import gdal

from qgis.core import (QgsMultiRenderChecker,
QgsLayoutExporter,
@@ -40,7 +41,7 @@
QgsPrintLayout,
QgsSingleSymbolRenderer,
QgsReport)
from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt, QDateTime, QDate, QTime
from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt, QDateTime, QDate, QTime, QTimeZone
from qgis.PyQt.QtGui import QImage, QPainter
from qgis.PyQt.QtPrintSupport import QPrinter
from qgis.PyQt.QtSvg import QSvgRenderer, QSvgGenerator
@@ -294,6 +295,14 @@ def testRenderRegionToImage(self):
self.assertTrue(self.checkImage('rendertoimageregionoverridedpi', 'rendertoimageregionoverridedpi', rendered_file_path))

def testExportToImage(self):
md = QgsProject.instance().metadata()
md.setTitle('proj title')
md.setAuthor('proj author')
md.setCreationDateTime(QDateTime(QDate(2011, 5, 3), QTime(9, 4, 5), QTimeZone(36000)))
md.setIdentifier('proj identifier')
md.setAbstract('proj abstract')
md.setKeywords({'kw': ['kw1', 'kw2']})
QgsProject.instance().setMetadata(md)
l = QgsLayout(QgsProject.instance())
l.initializeDefaults()

@@ -336,6 +345,15 @@ def testExportToImage(self):
page2_path = os.path.join(self.basetestpath, 'test_exporttoimagedpi_2.png')
self.assertTrue(self.checkImage('exporttoimagedpi_page2', 'exporttoimagedpi_page2', page2_path))

for f in (rendered_file_path, page2_path):
d = gdal.Open(f)
metadata = d.GetMetadata()
self.assertEqual(metadata['Author'], 'proj author')
self.assertEqual(metadata['Created'], '2011-05-03T09:04:05+10:00')
self.assertIn(metadata['Keywords'], ('kw1,kw2', 'kw2,kw1'))
self.assertEqual(metadata['Subject'], 'proj abstract')
self.assertEqual(metadata['Title'], 'proj title')

# crop to contents
settings.cropToContents = True
settings.cropMargins = QgsMargins(10, 20, 30, 40)
@@ -368,6 +386,15 @@ def testExportToImage(self):
self.assertTrue(self.checkImage('exporttoimagesize_page2', 'exporttoimagesize_page2', page2_path))

def testExportToPdf(self):
md = QgsProject.instance().metadata()
md.setTitle('proj title')
md.setAuthor('proj author')
md.setCreationDateTime(QDateTime(QDate(2011, 5, 3), QTime(9, 4, 5), QTimeZone(36000)))
md.setIdentifier('proj identifier')
md.setAbstract('proj abstract')
md.setKeywords({'kw': ['kw1', 'kw2']})
QgsProject.instance().setMetadata(md)

l = QgsLayout(QgsProject.instance())
l.initializeDefaults()

@@ -404,6 +431,7 @@ def testExportToPdf(self):
settings.dpi = 80
settings.rasterizeWholeImage = False
settings.forceVectorOutput = False
settings.exportMetadata = True

pdf_file_path = os.path.join(self.basetestpath, 'test_exporttopdfdpi.pdf')
self.assertEqual(exporter.exportToPdf(pdf_file_path, settings), QgsLayoutExporter.Success)
@@ -418,11 +446,19 @@ def testExportToPdf(self):
self.assertTrue(self.checkImage('exporttopdfdpi_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
self.assertTrue(self.checkImage('exporttopdfdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))

d = gdal.Open(pdf_file_path)
metadata = d.GetMetadata()
self.assertEqual(metadata['AUTHOR'], 'proj author')
self.assertEqual(metadata['CREATION_DATE'], "D:20110503090405+10'0'")
self.assertIn(metadata['KEYWORDS'], ('kw1,kw2', 'kw2,kw1'))
self.assertEqual(metadata['SUBJECT'], 'proj abstract')
self.assertEqual(metadata['TITLE'], 'proj title')

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.setCreationDateTime(QDateTime(QDate(2011, 5, 3), QTime(9, 4, 5), QTimeZone(36000)))
md.setIdentifier('proj identifier')
md.setAbstract('proj abstract')
md.setKeywords({'kw': ['kw1', 'kw2']})

0 comments on commit 2eacc4c

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