Skip to content

Commit

Permalink
[FEATURE][layouts] Export project metadata in PDF/image exports
Browse files Browse the repository at this point in the history
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 22, 2018
1 parent 1d4ff69 commit 2eacc4c
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 24 deletions.
5 changes: 5 additions & 0 deletions python/core/layout/qgslayoutexporter.sip.in
Expand Up @@ -135,6 +135,9 @@ Constructor for ImageExportSettings

bool generateWorldFile;

bool exportMetadata;


QgsLayoutRenderContext::Flags flags;

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

bool forceVectorOutput;

bool exportMetadata;

QgsLayoutRenderContext::Flags flags;

};
Expand Down
103 changes: 83 additions & 20 deletions src/core/layout/qgslayoutexporter.cpp
Expand Up @@ -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 )
{
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 );

Expand Down Expand Up @@ -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 );
}

22 changes: 21 additions & 1 deletion src/core/layout/qgslayoutexporter.h
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;

};
Expand Down
2 changes: 1 addition & 1 deletion src/ui/qgsmetadatawidget.ui
Expand Up @@ -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>
Expand Down
40 changes: 38 additions & 2 deletions tests/src/python/test_qgslayoutexporter.py
Expand Up @@ -19,6 +19,7 @@
import os
import subprocess
from xml.dom import minidom
from osgeo import gdal

from qgis.core import (QgsMultiRenderChecker,
QgsLayoutExporter,
Expand All @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand All @@ -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']})
Expand Down

0 comments on commit 2eacc4c

Please sign in to comment.