From 29a1ea840616a9a3aeb2070e86696b3c74158adb Mon Sep 17 00:00:00 2001 From: Julien Cabieces Date: Thu, 16 Nov 2023 10:33:51 +0100 Subject: [PATCH 1/8] feat(CMYK): Generate a valid PDF/X-4 file When a colorspace has been defined, we generate a valid PDF/X-4 file. --- src/core/layout/qgslayoutexporter.cpp | 108 ++++++++++++++++++++- src/core/layout/qgslayoutexporter.h | 6 ++ tests/src/python/test_qgslayoutexporter.py | 56 +++++++++++ 3 files changed, 165 insertions(+), 5 deletions(-) diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 50335b01dbe3..840bd1cb13ff 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -26,6 +26,7 @@ #include "qgslayoutgeopdfexporter.h" #include "qgslinestring.h" #include "qgsmessagelog.h" +#include "qgsprojectstylesettings.h" #include "qgslabelingresults.h" #include "qgssettingsentryimpl.h" #include "qgssettingstree.h" @@ -36,6 +37,10 @@ #include #include #include +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) +#include +#include +#endif #include "gdal.h" #include "cpl_conv.h" @@ -650,8 +655,8 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f { // copy layout metadata to GeoPDF export settings details.author = mLayout->project()->metadata().author(); - details.producer = QStringLiteral( "QGIS %1" ).arg( Qgis::version() ); - details.creator = QStringLiteral( "QGIS %1" ).arg( Qgis::version() ); + details.producer = getCreator(); + details.creator = getCreator(); details.creationDateTime = mLayout->project()->metadata().creationDateTime(); details.subject = mLayout->project()->metadata().abstract(); details.title = mLayout->project()->metadata().title(); @@ -1275,6 +1280,50 @@ void QgsLayoutExporter::preparePrintAsPdf( QgsLayout *layout, QPagedPaintDevice updatePrinterPageSize( layout, device, firstPageToBeExported( layout ) ); + // force a non empty title to avoid invalid (according to specification) PDF/X-4 + const QString title = layout->project()->metadata().title().isEmpty() ? + fi.baseName() : layout->project()->metadata().title(); + + QPdfWriter *pdfWriter = static_cast( device ); + pdfWriter->setTitle( title ); + + QPagedPaintDevice::PdfVersion pdfVersion = QPagedPaintDevice::PdfVersion_1_4; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + + const QgsProjectStyleSettings *styleSettings = layout->project() ? layout->project()->styleSettings() : nullptr; + if ( styleSettings ) + { + // We don't want to let AUTO color model because we could end up writing RGB colors with a CMYK + // output intent color model and vice versa, so we force color conversion + switch ( styleSettings->colorModel() ) + { + case Qgis::ColorModel::Cmyk: + pdfWriter->setColorModel( QPdfWriter::ColorModel::CMYK ); + break; + + case Qgis::ColorModel::Rgb: + pdfWriter->setColorModel( QPdfWriter::ColorModel::RGB ); + break; + } + + const QColorSpace colorSpace = styleSettings->colorSpace(); + if ( colorSpace.isValid() ) + { + QPdfOutputIntent outputIntent; + outputIntent.setOutputProfile( colorSpace ); + pdfWriter->setOutputIntent( outputIntent ); + + // PDF/X-4 standard allows PDF to be printing ready and is only possible if a color space has been set + pdfVersion = QPagedPaintDevice::PdfVersion_X4; + } + } + +#endif + + pdfWriter->setPdfVersion( pdfVersion ); + setXmpMetadata( pdfWriter, layout ); + // TODO: add option for this in layout // May not work on Windows or non-X11 Linux. Works fine on Mac using QPrinter::NativeFormat //printer.setFontEmbeddingEnabled( true ); @@ -1529,7 +1578,7 @@ void QgsLayoutExporter::appendMetadataToSvg( QDomDocument &svg ) const }; addAgentNode( QStringLiteral( "dc:creator" ), metadata.author() ); - addAgentNode( QStringLiteral( "dc:publisher" ), QStringLiteral( "QGIS %1" ).arg( Qgis::version() ) ); + addAgentNode( QStringLiteral( "dc:publisher" ), getCreator() ); // keywords { @@ -1714,7 +1763,7 @@ bool QgsLayoutExporter::georeferenceOutputPrivate( const QString &file, QgsLayou GDALSetMetadataItem( outputDS.get(), "CREATION_DATE", creationDateString.toUtf8().constData(), nullptr ); GDALSetMetadataItem( outputDS.get(), "AUTHOR", mLayout->project()->metadata().author().toUtf8().constData(), nullptr ); - const QString creator = QStringLiteral( "QGIS %1" ).arg( Qgis::version() ); + const QString creator = getCreator(); GDALSetMetadataItem( outputDS.get(), "CREATOR", creator.toUtf8().constData(), nullptr ); GDALSetMetadataItem( outputDS.get(), "PRODUCER", creator.toUtf8().constData(), nullptr ); GDALSetMetadataItem( outputDS.get(), "SUBJECT", mLayout->project()->metadata().abstract().toUtf8().constData(), nullptr ); @@ -2172,7 +2221,7 @@ bool QgsLayoutExporter::saveImage( const QImage &image, const QString &imageFile if ( projectForMetadata ) { w.setText( QStringLiteral( "Author" ), projectForMetadata->metadata().author() ); - const QString creator = QStringLiteral( "QGIS %1" ).arg( Qgis::version() ); + const QString creator = getCreator(); w.setText( QStringLiteral( "Creator" ), creator ); w.setText( QStringLiteral( "Producer" ), creator ); w.setText( QStringLiteral( "Subject" ), projectForMetadata->metadata().abstract() ); @@ -2190,3 +2239,52 @@ bool QgsLayoutExporter::saveImage( const QImage &image, const QString &imageFile } return w.write( image ); } + +QString QgsLayoutExporter::getCreator() +{ + return QStringLiteral( "QGIS %1" ).arg( Qgis::version() ); +} + +void QgsLayoutExporter::setXmpMetadata( QPdfWriter *pdfWriter, QgsLayout *layout ) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + QUuid uuid = pdfWriter->documentId(); +#else + QUuid uuid = QUuid::createUuid(); +#endif + + // XMP metadata date format differs from PDF dictionary one + const QDateTime creationDateTime = layout->project()->metadata().creationDateTime(); + const QByteArray creationDateMetadata = creationDateTime.toOffsetFromUtc( creationDateTime.offsetFromUtc() ).toString( Qt::ISODate ).toUtf8(); + + const QByteArray author = layout->project()->metadata().author().toUtf8(); + const QByteArray creator = getCreator().toUtf8(); + const QByteArray xmpMetadata = + "\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " " + pdfWriter->title().toUtf8() + "\n" + " \n" + " \n" + " \n" + " \n" + " " + author + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + // see qpdf.cpp QPdfEnginePrivate::writeXmpDocumentMetaData + + ( pdfWriter->pdfVersion() == QPagedPaintDevice::PdfVersion_X4 ? " \n" : "" ) + +#endif + " \n" + "\n" + "\n"; + + pdfWriter->setDocumentXmpMetadata( xmpMetadata ); +} diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index c5fb3018f749..842b3e06b350 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -777,6 +777,12 @@ class CORE_EXPORT QgsLayoutExporter const std::function &getItemExportGroupFunc ); + // Returns PDF creator (used also as producer) + static QString getCreator(); + + // Set PDF XMP metadata on pdfWriter for given layout + static void setXmpMetadata( QPdfWriter *pdfWriter, QgsLayout *layout ); + static QgsVectorSimplifyMethod createExportSimplifyMethod(); static QgsMaskRenderSettings createExportMaskSettings(); friend class TestQgsLayout; diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py index 04114680aaf6..dcbcb5bbfef8 100644 --- a/tests/src/python/test_qgslayoutexporter.py +++ b/tests/src/python/test_qgslayoutexporter.py @@ -12,6 +12,10 @@ import os import subprocess import tempfile +import xml.etree.ElementTree as etree +from uuid import UUID +from io import StringIO + from typing import Optional from osgeo import gdal @@ -29,6 +33,7 @@ from qgis.PyQt.QtPrintSupport import QPrinter from qgis.PyQt.QtSvg import QSvgRenderer from qgis.core import ( + Qgis, QgsCoordinateReferenceSystem, QgsFeature, QgsFillSymbol, @@ -523,6 +528,57 @@ def testExportToPdf(self): self.assertEqual(metadata['KEYWORDS'], 'KWx: kw3,kw4;kw: kw1,kw2') self.assertEqual(metadata['SUBJECT'], 'proj abstract') self.assertEqual(metadata['TITLE'], 'proj title') + qgisId = f"QGIS {Qgis.version()}" + self.assertEqual(metadata['CREATOR'], qgisId) + + # check XMP metadata + xmpMetadata = d.GetMetadata("xml:XMP") + self.assertEqual(len(xmpMetadata), 1) + xmp = xmpMetadata[0] + self.assertTrue(xmp) + xmpDoc = etree.fromstring(xmp) + namespaces = dict([node for _, node in etree.iterparse(StringIO(xmp), events=['start-ns'])]) + + title = xmpDoc.findall("rdf:RDF/rdf:Description/dc:title/rdf:Alt/rdf:li", namespaces) + self.assertEqual(len(title), 1) + self.assertEqual(title[0].text, 'proj title') + + creator = xmpDoc.findall("rdf:RDF/rdf:Description/dc:creator/rdf:Seq/rdf:li", namespaces) + self.assertEqual(len(creator), 1) + self.assertEqual(creator[0].text, 'proj author') + + producer = xmpDoc.findall("rdf:RDF/rdf:Description[@pdf:Producer]", namespaces) + self.assertEqual(len(producer), 1) + self.assertEqual(producer[0].attrib["{" + namespaces["pdf"] + "}" + "Producer"], qgisId) + + producer2 = xmpDoc.findall("rdf:RDF/rdf:Description[@xmp:CreatorTool]", namespaces) + self.assertEqual(len(producer2), 1) + self.assertEqual(producer2[0].attrib["{" + namespaces["xmp"] + "}" + "CreatorTool"], qgisId) + + creationDateTags = xmpDoc.findall("rdf:RDF/rdf:Description[@xmp:CreateDate]", namespaces) + self.assertEqual(len(creationDateTags), 1) + creationDate = creationDateTags[0].attrib["{" + namespaces["xmp"] + "}" + "CreateDate"] + self.assertEqual(creationDate, "2011-05-03T09:04:05+10:00") + + metadataDateTags = xmpDoc.findall("rdf:RDF/rdf:Description[@xmp:MetadataDate]", namespaces) + self.assertEqual(len(metadataDateTags), 1) + metadataDate = metadataDateTags[0].attrib["{" + namespaces["xmp"] + "}" + "MetadataDate"] + self.assertEqual(metadataDate, "2011-05-03T09:04:05+10:00") + + modifyDateTags = xmpDoc.findall("rdf:RDF/rdf:Description[@xmp:ModifyDate]", namespaces) + self.assertEqual(len(modifyDateTags), 1) + modifyDate = modifyDateTags[0].attrib["{" + namespaces["xmp"] + "}" + "ModifyDate"] + self.assertEqual(modifyDate, "2011-05-03T09:04:05+10:00") + + docIdTags = xmpDoc.findall("rdf:RDF/rdf:Description[@xmpMM:DocumentID]", namespaces) + self.assertEqual(len(docIdTags), 1) + docId = docIdTags[0].attrib["{" + namespaces["xmpMM"] + "}" + "DocumentID"] + uuidValid = True + try: + test = UUID(docId) + except ValueError: + uuidValid = False + self.assertTrue(uuidValid) def testExportToPdfGeoreference(self): md = QgsProject.instance().metadata() From b8a1577cf1725a34b3aa40721cf582dd2fc73a6c Mon Sep 17 00:00:00 2001 From: Julien Cabieces Date: Thu, 29 Aug 2024 10:56:51 +0200 Subject: [PATCH 2/8] fix(PdfExport): Rewrite XMP metadata writing to escape XML chars --- src/core/layout/qgslayoutexporter.cpp | 158 +++++++++++++++++---- tests/src/python/test_qgslayoutexporter.py | 8 +- 2 files changed, 134 insertions(+), 32 deletions(-) diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 840bd1cb13ff..534a96f7298e 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -41,6 +41,7 @@ #include #include #endif +#include #include "gdal.h" #include "cpl_conv.h" @@ -2248,43 +2249,142 @@ QString QgsLayoutExporter::getCreator() void QgsLayoutExporter::setXmpMetadata( QPdfWriter *pdfWriter, QgsLayout *layout ) { #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) - QUuid uuid = pdfWriter->documentId(); + QUuid documentId = pdfWriter->documentId(); #else - QUuid uuid = QUuid::createUuid(); + QUuid documentId = QUuid::createUuid(); #endif // XMP metadata date format differs from PDF dictionary one const QDateTime creationDateTime = layout->project()->metadata().creationDateTime(); - const QByteArray creationDateMetadata = creationDateTime.toOffsetFromUtc( creationDateTime.offsetFromUtc() ).toString( Qt::ISODate ).toUtf8(); - - const QByteArray author = layout->project()->metadata().author().toUtf8(); + const QByteArray metaDataDate = creationDateTime.toOffsetFromUtc( creationDateTime.offsetFromUtc() ).toString( Qt::ISODate ).toUtf8(); + const QByteArray title = pdfWriter->title().toUtf8(); const QByteArray creator = getCreator().toUtf8(); - const QByteArray xmpMetadata = - "\n" - "\n" - " \n" - " \n" - " \n" - " \n" - " " + pdfWriter->title().toUtf8() + "\n" - " \n" - " \n" - " \n" - " \n" - " " + author + "\n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" + const QByteArray producer = creator; + const QByteArray author = layout->project()->metadata().author().toUtf8(); + + // heavily inspired from qpdf.cpp QPdfEnginePrivate::writeXmpDocumentMetaData + + const QLatin1String xmlNS( "http://www.w3.org/XML/1998/namespace" ); + const QLatin1String adobeNS( "adobe:ns:meta/" ); + const QLatin1String rdfNS( "http://www.w3.org/1999/02/22-rdf-syntax-ns#" ); + const QLatin1String dcNS( "http://purl.org/dc/elements/1.1/" ); + const QLatin1String xmpNS( "http://ns.adobe.com/xap/1.0/" ); + const QLatin1String xmpMMNS( "http://ns.adobe.com/xap/1.0/mm/" ); + const QLatin1String pdfNS( "http://ns.adobe.com/pdf/1.3/" ); + const QLatin1String pdfaidNS( "http://www.aiim.org/pdfa/ns/id/" ); + const QLatin1String pdfxidNS( "http://www.npes.org/pdfx/ns/id/" ); + + QByteArray xmpMetadata; + QBuffer output( &xmpMetadata ); + output.open( QIODevice::WriteOnly ); + output.write( "" ); + + QXmlStreamWriter w( &output ); + w.setAutoFormatting( true ); + w.writeNamespace( adobeNS, "x" ); + w.writeNamespace( rdfNS, "rdf" ); + w.writeNamespace( dcNS, "dc" ); + w.writeNamespace( xmpNS, "xmp" ); + w.writeNamespace( xmpMMNS, "xmpMM" ); + w.writeNamespace( pdfNS, "pdf" ); + w.writeNamespace( pdfaidNS, "pdfaid" ); + w.writeNamespace( pdfxidNS, "pdfxid" ); + + w.writeStartElement( adobeNS, "xmpmeta" ); + w.writeStartElement( rdfNS, "RDF" ); + + // DC + w.writeStartElement( rdfNS, "Description" ); + w.writeAttribute( rdfNS, "about", "" ); + w.writeStartElement( dcNS, "title" ); + w.writeStartElement( rdfNS, "Alt" ); + w.writeStartElement( rdfNS, "li" ); + w.writeAttribute( xmlNS, "lang", "x-default" ); + w.writeCharacters( title ); + w.writeEndElement(); + w.writeEndElement(); + w.writeEndElement(); + + w.writeStartElement( dcNS, "creator" ); + w.writeStartElement( rdfNS, "Seq" ); + w.writeStartElement( rdfNS, "li" ); + w.writeCharacters( author ); + w.writeEndElement(); + w.writeEndElement(); + w.writeEndElement(); + + w.writeEndElement(); + + // PDF + w.writeStartElement( rdfNS, "Description" ); + w.writeAttribute( rdfNS, "about", "" ); + w.writeAttribute( pdfNS, "Producer", producer ); + w.writeAttribute( pdfNS, "Trapped", "False" ); + w.writeEndElement(); + + // XMP + w.writeStartElement( rdfNS, "Description" ); + w.writeAttribute( rdfNS, "about", "" ); + w.writeAttribute( xmpNS, "CreatorTool", creator ); + w.writeAttribute( xmpNS, "CreateDate", metaDataDate ); + w.writeAttribute( xmpNS, "ModifyDate", metaDataDate ); + w.writeAttribute( xmpNS, "MetadataDate", metaDataDate ); + w.writeEndElement(); + + // XMPMM + w.writeStartElement( rdfNS, "Description" ); + w.writeAttribute( rdfNS, "about", "" ); + w.writeAttribute( xmpMMNS, "DocumentID", "uuid:" + documentId.toString( QUuid::WithoutBraces ) ); + w.writeAttribute( xmpMMNS, "VersionID", "1" ); + w.writeAttribute( xmpMMNS, "RenditionClass", "default" ); + w.writeEndElement(); + #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) - // see qpdf.cpp QPdfEnginePrivate::writeXmpDocumentMetaData - + ( pdfWriter->pdfVersion() == QPagedPaintDevice::PdfVersion_X4 ? " \n" : "" ) + + + // Version-specific + switch ( pdfWriter->pdfVersion() ) + { + case QPagedPaintDevice::PdfVersion_1_4: + case QPagedPaintDevice::PdfVersion_A1b: // A1b and 1.6 are not used by QGIS + case QPagedPaintDevice::PdfVersion_1_6: + break; + case QPagedPaintDevice::PdfVersion_X4: + w.writeStartElement( rdfNS, "Description" ); + w.writeAttribute( rdfNS, "about", "" ); + w.writeAttribute( pdfxidNS, "GTS_PDFXVersion", "PDF/X-4" ); + w.writeEndElement(); + break; + } + #endif - " \n" - "\n" - "\n"; + + w.writeEndElement(); // + w.writeEndElement(); // + + w.writeEndDocument(); + output.write( "" ); + + + + + + + + + + + + + + + + + + + + + + pdfWriter->setDocumentXmpMetadata( xmpMetadata ); } diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py index dcbcb5bbfef8..1aed79bdc276 100644 --- a/tests/src/python/test_qgslayoutexporter.py +++ b/tests/src/python/test_qgslayoutexporter.py @@ -450,7 +450,9 @@ def testExportToImage(self): def testExportToPdf(self): md = QgsProject.instance().metadata() - md.setTitle('proj title') + + projTitle = 'proj title /<é' + md.setTitle(projTitle) md.setAuthor('proj author') md.setCreationDateTime(QDateTime(QDate(2011, 5, 3), QTime(9, 4, 5), QTimeZone(36000))) md.setIdentifier('proj identifier') @@ -527,7 +529,7 @@ def testExportToPdf(self): self.assertEqual(metadata['CREATION_DATE'], "D:20110503090405+10'0'") self.assertEqual(metadata['KEYWORDS'], 'KWx: kw3,kw4;kw: kw1,kw2') self.assertEqual(metadata['SUBJECT'], 'proj abstract') - self.assertEqual(metadata['TITLE'], 'proj title') + self.assertEqual(metadata['TITLE'], projTitle) qgisId = f"QGIS {Qgis.version()}" self.assertEqual(metadata['CREATOR'], qgisId) @@ -541,7 +543,7 @@ def testExportToPdf(self): title = xmpDoc.findall("rdf:RDF/rdf:Description/dc:title/rdf:Alt/rdf:li", namespaces) self.assertEqual(len(title), 1) - self.assertEqual(title[0].text, 'proj title') + self.assertEqual(title[0].text, projTitle) creator = xmpDoc.findall("rdf:RDF/rdf:Description/dc:creator/rdf:Seq/rdf:li", namespaces) self.assertEqual(len(creator), 1) From b6b7a6bf642afc9cd21ff61d0e6ab45517269496 Mon Sep 17 00:00:00 2001 From: Julien Cabieces Date: Thu, 29 Aug 2024 14:44:09 +0200 Subject: [PATCH 3/8] style(ExportPdf): make spellcheck happy --- src/core/layout/qgslayoutexporter.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 534a96f7298e..aa9cde528a34 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -2281,14 +2281,14 @@ void QgsLayoutExporter::setXmpMetadata( QPdfWriter *pdfWriter, QgsLayout *layout QXmlStreamWriter w( &output ); w.setAutoFormatting( true ); - w.writeNamespace( adobeNS, "x" ); - w.writeNamespace( rdfNS, "rdf" ); - w.writeNamespace( dcNS, "dc" ); - w.writeNamespace( xmpNS, "xmp" ); - w.writeNamespace( xmpMMNS, "xmpMM" ); - w.writeNamespace( pdfNS, "pdf" ); - w.writeNamespace( pdfaidNS, "pdfaid" ); - w.writeNamespace( pdfxidNS, "pdfxid" ); + w.writeNamespace( adobeNS, "x" ); //#spellok + w.writeNamespace( rdfNS, "rdf" ); //#spellok + w.writeNamespace( dcNS, "dc" ); //#spellok + w.writeNamespace( xmpNS, "xmp" ); //#spellok + w.writeNamespace( xmpMMNS, "xmpMM" ); //#spellok + w.writeNamespace( pdfNS, "pdf" ); //#spellok + w.writeNamespace( pdfaidNS, "pdfaid" ); //#spellok + w.writeNamespace( pdfxidNS, "pdfxid" ); //#spellok w.writeStartElement( adobeNS, "xmpmeta" ); w.writeStartElement( rdfNS, "RDF" ); From e1de5dccd26c1304255e7f0c172eff72a5218004 Mon Sep 17 00:00:00 2001 From: Julien Cabieces Date: Thu, 29 Aug 2024 15:56:14 +0200 Subject: [PATCH 4/8] fix(ExportPdf): write correctly utf-8 chars in XML --- src/core/layout/qgslayoutexporter.cpp | 32 ++++------------------ tests/src/python/test_qgslayoutexporter.py | 9 +++--- 2 files changed, 10 insertions(+), 31 deletions(-) diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index aa9cde528a34..1c53cb8b807e 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -2256,11 +2256,11 @@ void QgsLayoutExporter::setXmpMetadata( QPdfWriter *pdfWriter, QgsLayout *layout // XMP metadata date format differs from PDF dictionary one const QDateTime creationDateTime = layout->project()->metadata().creationDateTime(); - const QByteArray metaDataDate = creationDateTime.toOffsetFromUtc( creationDateTime.offsetFromUtc() ).toString( Qt::ISODate ).toUtf8(); - const QByteArray title = pdfWriter->title().toUtf8(); - const QByteArray creator = getCreator().toUtf8(); - const QByteArray producer = creator; - const QByteArray author = layout->project()->metadata().author().toUtf8(); + const QString metaDataDate = creationDateTime.toOffsetFromUtc( creationDateTime.offsetFromUtc() ).toString( Qt::ISODate ); + const QString title = pdfWriter->title(); + const QString creator = getCreator(); + const QString producer = creator; + const QString author = layout->project()->metadata().author(); // heavily inspired from qpdf.cpp QPdfEnginePrivate::writeXmpDocumentMetaData @@ -2364,27 +2364,5 @@ void QgsLayoutExporter::setXmpMetadata( QPdfWriter *pdfWriter, QgsLayout *layout w.writeEndDocument(); output.write( "" ); - - - - - - - - - - - - - - - - - - - - - - pdfWriter->setDocumentXmpMetadata( xmpMetadata ); } diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py index 1aed79bdc276..a316bba7db12 100644 --- a/tests/src/python/test_qgslayoutexporter.py +++ b/tests/src/python/test_qgslayoutexporter.py @@ -451,9 +451,10 @@ def testExportToImage(self): def testExportToPdf(self): md = QgsProject.instance().metadata() - projTitle = 'proj title /<é' + projTitle = 'proj titlea /<é' + projAuthor = 'proj author /<é' md.setTitle(projTitle) - md.setAuthor('proj author') + md.setAuthor(projAuthor) md.setCreationDateTime(QDateTime(QDate(2011, 5, 3), QTime(9, 4, 5), QTimeZone(36000))) md.setIdentifier('proj identifier') md.setAbstract('proj abstract') @@ -525,7 +526,7 @@ def testExportToPdf(self): d = gdal.Open(pdf_file_path) metadata = d.GetMetadata() - self.assertEqual(metadata['AUTHOR'], 'proj author') + self.assertEqual(metadata['AUTHOR'], projAuthor) self.assertEqual(metadata['CREATION_DATE'], "D:20110503090405+10'0'") self.assertEqual(metadata['KEYWORDS'], 'KWx: kw3,kw4;kw: kw1,kw2') self.assertEqual(metadata['SUBJECT'], 'proj abstract') @@ -547,7 +548,7 @@ def testExportToPdf(self): creator = xmpDoc.findall("rdf:RDF/rdf:Description/dc:creator/rdf:Seq/rdf:li", namespaces) self.assertEqual(len(creator), 1) - self.assertEqual(creator[0].text, 'proj author') + self.assertEqual(creator[0].text, projAuthor) producer = xmpDoc.findall("rdf:RDF/rdf:Description[@pdf:Producer]", namespaces) self.assertEqual(len(producer), 1) From 375e940752f380bfcfb1109bc250dee0a79dcc5d Mon Sep 17 00:00:00 2001 From: Julien Cabieces Date: Tue, 3 Sep 2024 09:12:41 +0200 Subject: [PATCH 5/8] fix(PdfExport): check for null project Co-authored-by: Nyall Dawson --- src/core/layout/qgslayoutexporter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 1c53cb8b807e..e579dc9c738a 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -1282,7 +1282,7 @@ void QgsLayoutExporter::preparePrintAsPdf( QgsLayout *layout, QPagedPaintDevice updatePrinterPageSize( layout, device, firstPageToBeExported( layout ) ); // force a non empty title to avoid invalid (according to specification) PDF/X-4 - const QString title = layout->project()->metadata().title().isEmpty() ? + const QString title = !layout->project() || layout->project()->metadata().title().isEmpty() ? fi.baseName() : layout->project()->metadata().title(); QPdfWriter *pdfWriter = static_cast( device ); From 5b7e41c60c84190bbad989c36194e0426fc1115a Mon Sep 17 00:00:00 2001 From: Julien Cabieces Date: Tue, 3 Sep 2024 09:17:26 +0200 Subject: [PATCH 6/8] style(PdfExport): one lining styleSetting condition Co-authored-by: Nyall Dawson --- src/core/layout/qgslayoutexporter.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index e579dc9c738a..2decbd280a33 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -1292,8 +1292,7 @@ void QgsLayoutExporter::preparePrintAsPdf( QgsLayout *layout, QPagedPaintDevice #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) - const QgsProjectStyleSettings *styleSettings = layout->project() ? layout->project()->styleSettings() : nullptr; - if ( styleSettings ) + if ( const QgsProjectStyleSettings *styleSettings = ( layout->project() ? layout->project()->styleSettings() : nullptr ) ) { // We don't want to let AUTO color model because we could end up writing RGB colors with a CMYK // output intent color model and vice versa, so we force color conversion From c2297fcc5b267d1ba4ac42aecabafcc9c6e94ac9 Mon Sep 17 00:00:00 2001 From: Julien Cabieces Date: Tue, 3 Sep 2024 09:24:32 +0200 Subject: [PATCH 7/8] style(PdfExport): Avoid ugly static cast --- src/core/layout/qgslayoutexporter.cpp | 17 ++++++++--------- src/core/layout/qgslayoutexporter.h | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 2decbd280a33..5a4694546403 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -1270,7 +1270,7 @@ QMap QgsLayoutExporter::takeLabelingResults() return res; } -void QgsLayoutExporter::preparePrintAsPdf( QgsLayout *layout, QPagedPaintDevice *device, const QString &filePath ) +void QgsLayoutExporter::preparePrintAsPdf( QgsLayout *layout, QPdfWriter *device, const QString &filePath ) { QFileInfo fi( filePath ); QDir dir; @@ -1285,8 +1285,7 @@ void QgsLayoutExporter::preparePrintAsPdf( QgsLayout *layout, QPagedPaintDevice const QString title = !layout->project() || layout->project()->metadata().title().isEmpty() ? fi.baseName() : layout->project()->metadata().title(); - QPdfWriter *pdfWriter = static_cast( device ); - pdfWriter->setTitle( title ); + device->setTitle( title ); QPagedPaintDevice::PdfVersion pdfVersion = QPagedPaintDevice::PdfVersion_1_4; @@ -1299,11 +1298,11 @@ void QgsLayoutExporter::preparePrintAsPdf( QgsLayout *layout, QPagedPaintDevice switch ( styleSettings->colorModel() ) { case Qgis::ColorModel::Cmyk: - pdfWriter->setColorModel( QPdfWriter::ColorModel::CMYK ); + device->setColorModel( QPdfWriter::ColorModel::CMYK ); break; case Qgis::ColorModel::Rgb: - pdfWriter->setColorModel( QPdfWriter::ColorModel::RGB ); + device->setColorModel( QPdfWriter::ColorModel::RGB ); break; } @@ -1312,7 +1311,7 @@ void QgsLayoutExporter::preparePrintAsPdf( QgsLayout *layout, QPagedPaintDevice { QPdfOutputIntent outputIntent; outputIntent.setOutputProfile( colorSpace ); - pdfWriter->setOutputIntent( outputIntent ); + device->setOutputIntent( outputIntent ); // PDF/X-4 standard allows PDF to be printing ready and is only possible if a color space has been set pdfVersion = QPagedPaintDevice::PdfVersion_X4; @@ -1321,8 +1320,8 @@ void QgsLayoutExporter::preparePrintAsPdf( QgsLayout *layout, QPagedPaintDevice #endif - pdfWriter->setPdfVersion( pdfVersion ); - setXmpMetadata( pdfWriter, layout ); + device->setPdfVersion( pdfVersion ); + setXmpMetadata( device, layout ); // TODO: add option for this in layout // May not work on Windows or non-X11 Linux. Works fine on Mac using QPrinter::NativeFormat @@ -1331,7 +1330,7 @@ void QgsLayoutExporter::preparePrintAsPdf( QgsLayout *layout, QPagedPaintDevice #if defined(HAS_KDE_QT5_PDF_TRANSFORM_FIX) || QT_VERSION >= QT_VERSION_CHECK(6, 3, 0) // paint engine hack not required, fixed upstream #else - QgsPaintEngineHack::fixEngineFlags( device->paintEngine() ); + QgsPaintEngineHack::fixEngineFlags( static_cast( device )->paintEngine() ); #endif } diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index 842b3e06b350..834a6a18e956 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -742,7 +742,7 @@ class CORE_EXPORT QgsLayoutExporter /** * Prepare a \a device for printing a layout as a PDF, to the destination \a filePath. */ - static void preparePrintAsPdf( QgsLayout *layout, QPagedPaintDevice *device, const QString &filePath ); + static void preparePrintAsPdf( QgsLayout *layout, QPdfWriter *device, const QString &filePath ); static void preparePrint( QgsLayout *layout, QPagedPaintDevice *device, bool setFirstPageSize = false ); From 7e09c55c38d28759d74a3a689ba2d815168d5615 Mon Sep 17 00:00:00 2001 From: Julien Cabieces Date: Tue, 3 Sep 2024 09:24:53 +0200 Subject: [PATCH 8/8] fix(PdfExport): Check for null project --- src/core/layout/qgslayoutexporter.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 5a4694546403..05b3b64cf1f2 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -2253,12 +2253,12 @@ void QgsLayoutExporter::setXmpMetadata( QPdfWriter *pdfWriter, QgsLayout *layout #endif // XMP metadata date format differs from PDF dictionary one - const QDateTime creationDateTime = layout->project()->metadata().creationDateTime(); - const QString metaDataDate = creationDateTime.toOffsetFromUtc( creationDateTime.offsetFromUtc() ).toString( Qt::ISODate ); + const QDateTime creationDateTime = layout->project() ? layout->project()->metadata().creationDateTime() : QDateTime(); + const QString metaDataDate = creationDateTime.isValid() ? creationDateTime.toOffsetFromUtc( creationDateTime.offsetFromUtc() ).toString( Qt::ISODate ) : QString(); const QString title = pdfWriter->title(); const QString creator = getCreator(); const QString producer = creator; - const QString author = layout->project()->metadata().author(); + const QString author = layout->project() ? layout->project()->metadata().author() : QString(); // heavily inspired from qpdf.cpp QPdfEnginePrivate::writeXmpDocumentMetaData