mirror of
https://github.com/qgis/QGIS.git
synced 2025-02-25 00:58:06 -05:00
[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
This commit is contained in:
parent
1d4ff69553
commit
2eacc4c4b4
@ -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,22 +1328,27 @@ 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
|
||||
@ -1349,12 +1356,48 @@ bool QgsLayoutExporter::georeferenceOutput( const QString &file, QgsLayoutItemMa
|
||||
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 shutil
|
||||
import os
|
||||
import subprocess
|
||||
from xml.dom import minidom
|
||||
from osgeo import gdal
|
||||
|
||||
from qgis.core import (QgsMultiRenderChecker,
|
||||
QgsLayoutExporter,
|
||||
@ -40,7 +41,7 @@ from qgis.core import (QgsMultiRenderChecker,
|
||||
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 @@ class TestQgsLayoutExporter(unittest.TestCase):
|
||||
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 @@ class TestQgsLayoutExporter(unittest.TestCase):
|
||||
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 @@ class TestQgsLayoutExporter(unittest.TestCase):
|
||||
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 @@ class TestQgsLayoutExporter(unittest.TestCase):
|
||||
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 @@ class TestQgsLayoutExporter(unittest.TestCase):
|
||||
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']})
|
||||
|
Loading…
x
Reference in New Issue
Block a user