diff --git a/python/PyQt6/core/auto_additions/qgslayoutexporter.py b/python/PyQt6/core/auto_additions/qgslayoutexporter.py index 975a06129e5e..2b35b7119489 100644 --- a/python/PyQt6/core/auto_additions/qgslayoutexporter.py +++ b/python/PyQt6/core/auto_additions/qgslayoutexporter.py @@ -21,8 +21,8 @@ except (NameError, AttributeError): pass try: - QgsLayoutExporter.PdfExportSettings.__attribute_docs__ = {'dpi': 'Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.', 'rasterizeWholeImage': 'Set to ``True`` to force whole layout to be rasterized while exporting.\n\nThis option is mutually exclusive with forceVectorOutput.', 'forceVectorOutput': 'Set to ``True`` to force vector object exports, even when the resultant appearance will differ\nfrom the layout. If ``False``, some items may be rasterized in order to maintain their\ncorrect appearance in the output.\n\nThis option is mutually exclusive with rasterizeWholeImage.', 'appendGeoreference': 'Indicates whether PDF export should append georeference data\n\n.. versionadded:: 3.10', 'exportMetadata': "Indicates whether PDF export should include metadata generated\nfrom the layout's project's metadata.\n\n.. versionadded:: 3.2", 'flags': 'Layout context flags, which control how the export will be created.', 'textRenderFormat': 'Text rendering format, which controls how text should be rendered in the export (e.g.\nas paths or real text objects).\n\n.. versionadded:: 3.4.3', 'simplifyGeometries': 'Indicates whether vector geometries should be simplified to avoid redundant extraneous detail,\nsuch as vertices which are not visible at the specified dpi of the output.\n\n.. versionadded:: 3.10', 'writeGeoPdf': '``True`` if geospatial PDF files should be created, instead of normal PDF files.\n\nWhilst geospatial PDF files can include some desirable properties like the ability to interactively\nquery map features, they also can result in lower-quality output files, or forced rasterization\nof layers.\n\n.. note::\n\n Requires builds based on GDAL 3.0 or greater.\n\n.. versionadded:: 3.10', 'exportLayersAsSeperateFiles': '``True`` if individual layers from the layout should be rendered to separate PDF files.\n\nThis option allows for separation of logic layout layers to individual PDF files. For instance,\nif this option is ``True``, then a separate PDF file will be created per layer per map item in the\nlayout. Additionally, separate PDF files may be created for other complex layout items, resulting\nin a set of PDF files which contain logical atomic components of the layout.\n\nThis option is designed to allow the PDF files to be composited back together in an external\napplication (e.g. Adobe Illustrator) as a non-QGIS, post-production step.\n\n.. versionadded:: 3.14', 'useIso32000ExtensionFormatGeoreferencing': '``True`` if ISO3200 extension format georeferencing should be used.\n\nThis is a recommended setting which results in Geospatial PDF files compatible\nwith the built-in Acrobat geospatial tools.\n\nIf PdfExportSettings.writeGeoPdf is ``False`` than this option has no effect.', 'useOgcBestPracticeFormatGeoreferencing': '``True`` if OGC "best practice" format georeferencing should be used.\n\n.. warning::\n\n This results in geospatial PDF files compatible with a unnamed suite of tools starting with Terra and ending with Go, but\n can break compatibility with the built-in Acrobat geospatial tools (yes, Geospatial PDF\n format is a mess!).\n\nIf PdfExportSettings.writeGeoPdf is ``False`` than this option has no effect.\n\n.. deprecated:: 3.42\n\n This parameter has no longer any effect. Only ISO 32000 georeferencing is handled.', 'includeGeoPdfFeatures': '``True`` if feature vector information (such as attributes) should be exported during Geospatial PDF exports.\n\nIf PdfExportSettings.writeGeoPdf is ``False`` than this option has no effect.', 'exportThemes': "Optional list of map themes to export as Geospatial PDF layer groups.\n\nIf set, map item's which are not assigned a specific map theme will iterate through all listed\nthemes and a Geospatial PDF layer group will be created for each.\n\nIf PdfExportSettings.writeGeoPdf is ``False`` than this option has no effect.", 'predefinedMapScales': 'A list of predefined scales to use with the layout. This is used\nfor maps which are set to the predefined atlas scaling mode.\n\n.. versionadded:: 3.10'} - QgsLayoutExporter.PdfExportSettings.__annotations__ = {'dpi': float, 'rasterizeWholeImage': bool, 'forceVectorOutput': bool, 'appendGeoreference': bool, 'exportMetadata': bool, 'flags': 'Qgis.LayoutRenderFlags', 'textRenderFormat': 'Qgis.TextRenderFormat', 'simplifyGeometries': bool, 'writeGeoPdf': bool, 'exportLayersAsSeperateFiles': bool, 'useIso32000ExtensionFormatGeoreferencing': bool, 'useOgcBestPracticeFormatGeoreferencing': bool, 'includeGeoPdfFeatures': bool, 'exportThemes': 'List[str]', 'predefinedMapScales': 'List[float]'} + QgsLayoutExporter.PdfExportSettings.__attribute_docs__ = {'dpi': 'Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.', 'rasterizeWholeImage': 'Set to ``True`` to force whole layout to be rasterized while exporting.\n\nThis option is mutually exclusive with forceVectorOutput.', 'forceVectorOutput': 'Set to ``True`` to force vector object exports, even when the resultant appearance will differ\nfrom the layout. If ``False``, some items may be rasterized in order to maintain their\ncorrect appearance in the output.\n\nThis option is mutually exclusive with rasterizeWholeImage.', 'appendGeoreference': 'Indicates whether PDF export should append georeference data\n\n.. versionadded:: 3.10', 'exportMetadata': "Indicates whether PDF export should include metadata generated\nfrom the layout's project's metadata.\n\n.. versionadded:: 3.2", 'flags': 'Layout context flags, which control how the export will be created.', 'textRenderFormat': 'Text rendering format, which controls how text should be rendered in the export (e.g.\nas paths or real text objects).\n\n.. versionadded:: 3.4.3', 'simplifyGeometries': 'Indicates whether vector geometries should be simplified to avoid redundant extraneous detail,\nsuch as vertices which are not visible at the specified dpi of the output.\n\n.. versionadded:: 3.10', 'writeGeoPdf': '``True`` if geospatial PDF files should be created, instead of normal PDF files.\n\nWhilst geospatial PDF files can include some desirable properties like the ability to interactively\nquery map features, they also can result in lower-quality output files, or forced rasterization\nof layers.\n\n.. note::\n\n Requires builds based on GDAL 3.0 or greater.\n\n.. versionadded:: 3.10', 'exportLayersAsSeperateFiles': '``True`` if individual layers from the layout should be rendered to separate PDF files.\n\nThis option allows for separation of logic layout layers to individual PDF files. For instance,\nif this option is ``True``, then a separate PDF file will be created per layer per map item in the\nlayout. Additionally, separate PDF files may be created for other complex layout items, resulting\nin a set of PDF files which contain logical atomic components of the layout.\n\nThis option is designed to allow the PDF files to be composited back together in an external\napplication (e.g. Adobe Illustrator) as a non-QGIS, post-production step.\n\n.. versionadded:: 3.14', 'useIso32000ExtensionFormatGeoreferencing': '``True`` if ISO3200 extension format georeferencing should be used.\n\nThis is a recommended setting which results in Geospatial PDF files compatible\nwith the built-in Acrobat geospatial tools.\n\nIf PdfExportSettings.writeGeoPdf is ``False`` then this option has no effect.', 'useOgcBestPracticeFormatGeoreferencing': '``True`` if OGC "best practice" format georeferencing should be used.\n\n.. warning::\n\n This results in geospatial PDF files compatible with a unnamed suite of tools starting with Terra and ending with Go, but\n can break compatibility with the built-in Acrobat geospatial tools (yes, Geospatial PDF\n format is a mess!).\n\nIf PdfExportSettings.writeGeoPdf is ``False`` then this option has no effect.\n\n.. deprecated:: 3.42\n\n This parameter has no longer any effect. Only ISO 32000 georeferencing is handled.', 'includeGeoPdfFeatures': '``True`` if feature vector information (such as attributes) should be exported during Geospatial PDF exports.\n\nIf PdfExportSettings.writeGeoPdf is ``False`` then this option has no effect.', 'exportThemes': "Optional list of map themes to export as Geospatial PDF layer groups.\n\nIf set, map item's which are not assigned a specific map theme will iterate through all listed\nthemes and a Geospatial PDF layer group will be created for each.\n\nIf PdfExportSettings.writeGeoPdf is ``False`` or PdfExportSettings.useQGISLayerTreeProperties is ``True``\nthen this option has no effect.", 'predefinedMapScales': 'A list of predefined scales to use with the layout. This is used\nfor maps which are set to the predefined atlas scaling mode.\n\n.. versionadded:: 3.10', 'useQgisLayerTreeProperties': 'If set to ``True``, the layer tree from the QGIS project should be used when creating a Geospatial PDF.\nIn that case, layer/group names, order, and visibility from the QGIS project will be reflected in the output PDF.\n\nWhen this option is active, the PdfExportSettings.exportThemes option has no effect.\n\nIf PdfExportSettings.writeGeoPdf is ``False`` then this option has no effect.\n\n.. versionadded:: 4.0'} + QgsLayoutExporter.PdfExportSettings.__annotations__ = {'dpi': float, 'rasterizeWholeImage': bool, 'forceVectorOutput': bool, 'appendGeoreference': bool, 'exportMetadata': bool, 'flags': 'Qgis.LayoutRenderFlags', 'textRenderFormat': 'Qgis.TextRenderFormat', 'simplifyGeometries': bool, 'writeGeoPdf': bool, 'exportLayersAsSeperateFiles': bool, 'useIso32000ExtensionFormatGeoreferencing': bool, 'useOgcBestPracticeFormatGeoreferencing': bool, 'includeGeoPdfFeatures': bool, 'exportThemes': 'List[str]', 'predefinedMapScales': 'List[float]', 'useQgisLayerTreeProperties': bool} QgsLayoutExporter.PdfExportSettings.__doc__ = """Contains settings relating to exporting layouts to PDF""" QgsLayoutExporter.PdfExportSettings.__group__ = ['layout'] except (NameError, AttributeError): diff --git a/python/PyQt6/core/auto_generated/layout/qgslayoutexporter.sip.in b/python/PyQt6/core/auto_generated/layout/qgslayoutexporter.sip.in index 4643e29cf24f..2a9b4d5d5938 100644 --- a/python/PyQt6/core/auto_generated/layout/qgslayoutexporter.sip.in +++ b/python/PyQt6/core/auto_generated/layout/qgslayoutexporter.sip.in @@ -219,6 +219,8 @@ set to the error description. QStringList exportThemes; QVector predefinedMapScales; + + bool useQgisLayerTreeProperties; }; ExportResult exportToPdf( const QString &filePath, const QgsLayoutExporter::PdfExportSettings &settings ); diff --git a/python/core/auto_additions/qgslayoutexporter.py b/python/core/auto_additions/qgslayoutexporter.py index 7ccb20200aca..a9444546a5bc 100644 --- a/python/core/auto_additions/qgslayoutexporter.py +++ b/python/core/auto_additions/qgslayoutexporter.py @@ -14,8 +14,8 @@ except (NameError, AttributeError): pass try: - QgsLayoutExporter.PdfExportSettings.__attribute_docs__ = {'dpi': 'Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.', 'rasterizeWholeImage': 'Set to ``True`` to force whole layout to be rasterized while exporting.\n\nThis option is mutually exclusive with forceVectorOutput.', 'forceVectorOutput': 'Set to ``True`` to force vector object exports, even when the resultant appearance will differ\nfrom the layout. If ``False``, some items may be rasterized in order to maintain their\ncorrect appearance in the output.\n\nThis option is mutually exclusive with rasterizeWholeImage.', 'appendGeoreference': 'Indicates whether PDF export should append georeference data\n\n.. versionadded:: 3.10', 'exportMetadata': "Indicates whether PDF export should include metadata generated\nfrom the layout's project's metadata.\n\n.. versionadded:: 3.2", 'flags': 'Layout context flags, which control how the export will be created.', 'textRenderFormat': 'Text rendering format, which controls how text should be rendered in the export (e.g.\nas paths or real text objects).\n\n.. versionadded:: 3.4.3', 'simplifyGeometries': 'Indicates whether vector geometries should be simplified to avoid redundant extraneous detail,\nsuch as vertices which are not visible at the specified dpi of the output.\n\n.. versionadded:: 3.10', 'writeGeoPdf': '``True`` if geospatial PDF files should be created, instead of normal PDF files.\n\nWhilst geospatial PDF files can include some desirable properties like the ability to interactively\nquery map features, they also can result in lower-quality output files, or forced rasterization\nof layers.\n\n.. note::\n\n Requires builds based on GDAL 3.0 or greater.\n\n.. versionadded:: 3.10', 'exportLayersAsSeperateFiles': '``True`` if individual layers from the layout should be rendered to separate PDF files.\n\nThis option allows for separation of logic layout layers to individual PDF files. For instance,\nif this option is ``True``, then a separate PDF file will be created per layer per map item in the\nlayout. Additionally, separate PDF files may be created for other complex layout items, resulting\nin a set of PDF files which contain logical atomic components of the layout.\n\nThis option is designed to allow the PDF files to be composited back together in an external\napplication (e.g. Adobe Illustrator) as a non-QGIS, post-production step.\n\n.. versionadded:: 3.14', 'useIso32000ExtensionFormatGeoreferencing': '``True`` if ISO3200 extension format georeferencing should be used.\n\nThis is a recommended setting which results in Geospatial PDF files compatible\nwith the built-in Acrobat geospatial tools.\n\nIf PdfExportSettings.writeGeoPdf is ``False`` than this option has no effect.', 'useOgcBestPracticeFormatGeoreferencing': '``True`` if OGC "best practice" format georeferencing should be used.\n\n.. warning::\n\n This results in geospatial PDF files compatible with a unnamed suite of tools starting with Terra and ending with Go, but\n can break compatibility with the built-in Acrobat geospatial tools (yes, Geospatial PDF\n format is a mess!).\n\nIf PdfExportSettings.writeGeoPdf is ``False`` than this option has no effect.\n\n.. deprecated:: 3.42\n\n This parameter has no longer any effect. Only ISO 32000 georeferencing is handled.', 'includeGeoPdfFeatures': '``True`` if feature vector information (such as attributes) should be exported during Geospatial PDF exports.\n\nIf PdfExportSettings.writeGeoPdf is ``False`` than this option has no effect.', 'exportThemes': "Optional list of map themes to export as Geospatial PDF layer groups.\n\nIf set, map item's which are not assigned a specific map theme will iterate through all listed\nthemes and a Geospatial PDF layer group will be created for each.\n\nIf PdfExportSettings.writeGeoPdf is ``False`` than this option has no effect.", 'predefinedMapScales': 'A list of predefined scales to use with the layout. This is used\nfor maps which are set to the predefined atlas scaling mode.\n\n.. versionadded:: 3.10'} - QgsLayoutExporter.PdfExportSettings.__annotations__ = {'dpi': float, 'rasterizeWholeImage': bool, 'forceVectorOutput': bool, 'appendGeoreference': bool, 'exportMetadata': bool, 'flags': 'Qgis.LayoutRenderFlags', 'textRenderFormat': 'Qgis.TextRenderFormat', 'simplifyGeometries': bool, 'writeGeoPdf': bool, 'exportLayersAsSeperateFiles': bool, 'useIso32000ExtensionFormatGeoreferencing': bool, 'useOgcBestPracticeFormatGeoreferencing': bool, 'includeGeoPdfFeatures': bool, 'exportThemes': 'List[str]', 'predefinedMapScales': 'List[float]'} + QgsLayoutExporter.PdfExportSettings.__attribute_docs__ = {'dpi': 'Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.', 'rasterizeWholeImage': 'Set to ``True`` to force whole layout to be rasterized while exporting.\n\nThis option is mutually exclusive with forceVectorOutput.', 'forceVectorOutput': 'Set to ``True`` to force vector object exports, even when the resultant appearance will differ\nfrom the layout. If ``False``, some items may be rasterized in order to maintain their\ncorrect appearance in the output.\n\nThis option is mutually exclusive with rasterizeWholeImage.', 'appendGeoreference': 'Indicates whether PDF export should append georeference data\n\n.. versionadded:: 3.10', 'exportMetadata': "Indicates whether PDF export should include metadata generated\nfrom the layout's project's metadata.\n\n.. versionadded:: 3.2", 'flags': 'Layout context flags, which control how the export will be created.', 'textRenderFormat': 'Text rendering format, which controls how text should be rendered in the export (e.g.\nas paths or real text objects).\n\n.. versionadded:: 3.4.3', 'simplifyGeometries': 'Indicates whether vector geometries should be simplified to avoid redundant extraneous detail,\nsuch as vertices which are not visible at the specified dpi of the output.\n\n.. versionadded:: 3.10', 'writeGeoPdf': '``True`` if geospatial PDF files should be created, instead of normal PDF files.\n\nWhilst geospatial PDF files can include some desirable properties like the ability to interactively\nquery map features, they also can result in lower-quality output files, or forced rasterization\nof layers.\n\n.. note::\n\n Requires builds based on GDAL 3.0 or greater.\n\n.. versionadded:: 3.10', 'exportLayersAsSeperateFiles': '``True`` if individual layers from the layout should be rendered to separate PDF files.\n\nThis option allows for separation of logic layout layers to individual PDF files. For instance,\nif this option is ``True``, then a separate PDF file will be created per layer per map item in the\nlayout. Additionally, separate PDF files may be created for other complex layout items, resulting\nin a set of PDF files which contain logical atomic components of the layout.\n\nThis option is designed to allow the PDF files to be composited back together in an external\napplication (e.g. Adobe Illustrator) as a non-QGIS, post-production step.\n\n.. versionadded:: 3.14', 'useIso32000ExtensionFormatGeoreferencing': '``True`` if ISO3200 extension format georeferencing should be used.\n\nThis is a recommended setting which results in Geospatial PDF files compatible\nwith the built-in Acrobat geospatial tools.\n\nIf PdfExportSettings.writeGeoPdf is ``False`` then this option has no effect.', 'useOgcBestPracticeFormatGeoreferencing': '``True`` if OGC "best practice" format georeferencing should be used.\n\n.. warning::\n\n This results in geospatial PDF files compatible with a unnamed suite of tools starting with Terra and ending with Go, but\n can break compatibility with the built-in Acrobat geospatial tools (yes, Geospatial PDF\n format is a mess!).\n\nIf PdfExportSettings.writeGeoPdf is ``False`` then this option has no effect.\n\n.. deprecated:: 3.42\n\n This parameter has no longer any effect. Only ISO 32000 georeferencing is handled.', 'includeGeoPdfFeatures': '``True`` if feature vector information (such as attributes) should be exported during Geospatial PDF exports.\n\nIf PdfExportSettings.writeGeoPdf is ``False`` then this option has no effect.', 'exportThemes': "Optional list of map themes to export as Geospatial PDF layer groups.\n\nIf set, map item's which are not assigned a specific map theme will iterate through all listed\nthemes and a Geospatial PDF layer group will be created for each.\n\nIf PdfExportSettings.writeGeoPdf is ``False`` or PdfExportSettings.useQGISLayerTreeProperties is ``True``\nthen this option has no effect.", 'predefinedMapScales': 'A list of predefined scales to use with the layout. This is used\nfor maps which are set to the predefined atlas scaling mode.\n\n.. versionadded:: 3.10', 'useQgisLayerTreeProperties': 'If set to ``True``, the layer tree from the QGIS project should be used when creating a Geospatial PDF.\nIn that case, layer/group names, order, and visibility from the QGIS project will be reflected in the output PDF.\n\nWhen this option is active, the PdfExportSettings.exportThemes option has no effect.\n\nIf PdfExportSettings.writeGeoPdf is ``False`` then this option has no effect.\n\n.. versionadded:: 4.0'} + QgsLayoutExporter.PdfExportSettings.__annotations__ = {'dpi': float, 'rasterizeWholeImage': bool, 'forceVectorOutput': bool, 'appendGeoreference': bool, 'exportMetadata': bool, 'flags': 'Qgis.LayoutRenderFlags', 'textRenderFormat': 'Qgis.TextRenderFormat', 'simplifyGeometries': bool, 'writeGeoPdf': bool, 'exportLayersAsSeperateFiles': bool, 'useIso32000ExtensionFormatGeoreferencing': bool, 'useOgcBestPracticeFormatGeoreferencing': bool, 'includeGeoPdfFeatures': bool, 'exportThemes': 'List[str]', 'predefinedMapScales': 'List[float]', 'useQgisLayerTreeProperties': bool} QgsLayoutExporter.PdfExportSettings.__doc__ = """Contains settings relating to exporting layouts to PDF""" QgsLayoutExporter.PdfExportSettings.__group__ = ['layout'] except (NameError, AttributeError): diff --git a/python/core/auto_generated/layout/qgslayoutexporter.sip.in b/python/core/auto_generated/layout/qgslayoutexporter.sip.in index 237c4f51ed70..c1835dfbb04a 100644 --- a/python/core/auto_generated/layout/qgslayoutexporter.sip.in +++ b/python/core/auto_generated/layout/qgslayoutexporter.sip.in @@ -219,6 +219,8 @@ set to the error description. QStringList exportThemes; QVector predefinedMapScales; + + bool useQgisLayerTreeProperties; }; ExportResult exportToPdf( const QString &filePath, const QgsLayoutExporter::PdfExportSettings &settings ); diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 55e4a821cae1..4b295a544a16 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -4365,6 +4365,7 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport bool disableRasterTiles = false; bool simplify = true; bool geospatialPdf = false; + bool useQgisLayerTreeConfig = false; bool losslessImages = false; QStringList exportThemes; QStringList geospatialPdfLayerOrder; @@ -4378,6 +4379,7 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport disableRasterTiles = mLayout->customProperty( u"pdfDisableRasterTiles"_s, 0 ).toBool(); simplify = mLayout->customProperty( u"pdfSimplify"_s, 1 ).toBool(); geospatialPdf = mLayout->customProperty( u"pdfCreateGeoPdf"_s, 0 ).toBool(); + useQgisLayerTreeConfig = mLayout->customProperty( u"pdfUseQgisConfig"_s, 0 ).toBool(); const QString themes = mLayout->customProperty( u"pdfExportThemes"_s ).toString(); if ( !themes.isEmpty() ) exportThemes = themes.split( u"~~~"_s ); @@ -4399,6 +4401,8 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport if ( mLayout ) mLayout->layoutItems( maps ); + bool mapWithNoPresets = false; + for ( const QgsLayoutItemMap *map : maps ) { if ( !map->crs().isValid() ) @@ -4417,6 +4421,9 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport break; } #endif + + if ( !map->followVisibilityPreset() && !map->keepLayerSet() ) + mapWithNoPresets = true; } QgsLayoutPdfExportOptionsDialog dialog( this, allowGeospatialPdfExport, dialogGeospatialPdfReason, geospatialPdfLayerOrder ); @@ -4433,6 +4440,13 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport dialog.setLosslessImageExport( losslessImages ); dialog.setOpenAfterExporting( QgsLayoutExporter::settingOpenAfterExportingPdf->value() ); + // The Follow QGIS layer tree configuration radio button is only enabled + // if there is at least one map that does not follow map themes or locked layers. + if ( !mapWithNoPresets ) + dialog.disableUseQgisLayerTreeConfig(); + else + dialog.setUseQgisLayerTreeConfig( useQgisLayerTreeConfig ); + if ( dialog.exec() != QDialog::Accepted ) return false; @@ -4443,6 +4457,7 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport simplify = dialog.geometriesSimplified(); Qgis::TextRenderFormat textRenderFormat = dialog.textRenderFormat(); geospatialPdf = dialog.exportGeospatialPdf(); + useQgisLayerTreeConfig = dialog.useQgisLayerTreeConfig(); exportThemes = dialog.exportThemes(); geospatialPdfLayerOrder = dialog.geospatialPdfLayerOrder(); losslessImages = dialog.losslessImageExport(); @@ -4458,6 +4473,7 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport mLayout->setCustomProperty( u"pdfTextFormat"_s, static_cast( textRenderFormat ) ); mLayout->setCustomProperty( u"pdfSimplify"_s, simplify ? 1 : 0 ); mLayout->setCustomProperty( u"pdfCreateGeoPdf"_s, geospatialPdf ? 1 : 0 ); + mLayout->setCustomProperty( u"pdfUseQgisConfig"_s, useQgisLayerTreeConfig ? 1 : 0 ); mLayout->setCustomProperty( u"pdfExportThemes"_s, exportThemes.join( "~~~"_L1 ) ); mLayout->setCustomProperty( u"pdfLayerOrder"_s, geospatialPdfLayerOrder.join( "~~~"_L1 ) ); mLayout->setCustomProperty( u"pdfGroupOrder"_s, dialog.geospatialPdfGroupOrder() ); @@ -4470,6 +4486,7 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport settings.textRenderFormat = textRenderFormat; settings.simplifyGeometries = simplify; settings.writeGeoPdf = geospatialPdf; + settings.useQgisLayerTreeProperties = useQgisLayerTreeConfig; settings.useIso32000ExtensionFormatGeoreferencing = true; settings.exportThemes = exportThemes; settings.predefinedMapScales = QgsLayoutUtils::predefinedScales( mLayout ); diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 54b0d951d7e5..71206c02425e 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -599,7 +599,11 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f mLayout->renderContext().setFlag( Qgis::LayoutRenderFlag::SynchronousLegendGraphics, true ); mLayout->renderContext().setTextRenderFormat( settings.textRenderFormat ); - mLayout->renderContext().setExportThemes( settings.exportThemes ); + + if ( settings.writeGeoPdf && !settings.useQgisLayerTreeProperties ) + { + mLayout->renderContext().setExportThemes( settings.exportThemes ); + } ExportResult result = Success; if ( settings.writeGeoPdf || settings.exportLayersAsSeperateFiles ) //#spellok @@ -613,6 +617,19 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f const QList items = mLayout->items( Qt::AscendingOrder ); + if ( settings.writeGeoPdf && settings.useQgisLayerTreeProperties ) + { + bool res = geospatialPdfExporter->setMapItemLayersBeforeRendering(); + // If no map was found to set project layers, it means that all of them + // have map theme presets or have locked layers, which is not supported + // when exporting a Geospatial PDF following QGIS layer tree properties. + if ( !res ) + { + mErrorMessage = u"The Geospatial PDF cannot be exported following QGIS project configuration: At least one map layout item must not follow map themes nor locked layers."_s; + return PrintError; + } + } + QList< QgsLayoutGeospatialPdfExporter::ComponentLayerDetail > pdfComponents; const QDir baseDir = settings.exportLayersAsSeperateFiles ? QFileInfo( filePath ).dir() : QDir(); //#spellok @@ -656,6 +673,12 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f return item->customProperty( u"pdfExportGroup"_s ).toString(); }; result = handleLayeredExport( items, exportFunc, getExportGroupNameFunc ); + + if ( settings.writeGeoPdf && settings.useQgisLayerTreeProperties ) + { + // Restore map item layers right after the layer rendering + geospatialPdfExporter->restoreMapItemLayersAfterRendering(); + } if ( result != Success ) return result; @@ -667,7 +690,6 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f QgsLayoutSize pageSize = mLayout->pageCollection()->page( 0 )->sizeWithUnits(); QgsLayoutSize pageSizeMM = mLayout->renderContext().measurementConverter().convert( pageSize, Qgis::LayoutUnit::Millimeters ); details.pageSizeMm = pageSizeMM.toQSizeF(); - details.mutuallyExclusiveGroups = mutuallyExclusiveGroups; if ( settings.exportMetadata ) { @@ -681,12 +703,6 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f details.keywords = mLayout->project()->metadata().keywords(); } - const QList< QgsMapLayer * > layers = mLayout->project()->mapLayers().values(); - for ( const QgsMapLayer *layer : layers ) - { - details.layerIdToPdfLayerTreeNameMap.insert( layer->id(), layer->name() ); - } - if ( settings.appendGeoreference ) { // setup georeferencing @@ -727,12 +743,23 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f } } - details.customLayerTreeGroups = geospatialPdfExporter->customLayerTreeGroups(); - details.initialLayerVisibility = geospatialPdfExporter->initialLayerVisibility(); - details.layerOrder = geospatialPdfExporter->layerOrder(); - details.layerTreeGroupOrder = geospatialPdfExporter->layerTreeGroupOrder(); + if ( !settings.useQgisLayerTreeProperties ) + { + details.customLayerTreeGroups = geospatialPdfExporter->customLayerTreeGroups(); + details.initialLayerVisibility = geospatialPdfExporter->initialLayerVisibility(); + details.layerOrder = geospatialPdfExporter->layerOrder(); + details.layerTreeGroupOrder = geospatialPdfExporter->layerTreeGroupOrder(); + details.mutuallyExclusiveGroups = mutuallyExclusiveGroups; + + const QList< QgsMapLayer * > layers = mLayout->project()->mapLayers().values(); + for ( const QgsMapLayer *layer : layers ) + { + details.layerIdToPdfLayerTreeNameMap.insert( layer->id(), layer->name() ); + } + } details.includeFeatures = settings.includeGeoPdfFeatures; details.useIso32000ExtensionFormatGeoreferencing = settings.useIso32000ExtensionFormatGeoreferencing; + details.useQgisLayerTreeProperties = settings.useQgisLayerTreeProperties; if ( !geospatialPdfExporter->finalize( pdfComponents, filePath, details ) ) { diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index 9fd57bb2367b..ae4ef2c4b2af 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -369,7 +369,7 @@ class CORE_EXPORT QgsLayoutExporter * This is a recommended setting which results in Geospatial PDF files compatible * with the built-in Acrobat geospatial tools. * - * If PdfExportSettings::writeGeoPdf is FALSE than this option has no effect. + * If PdfExportSettings::writeGeoPdf is FALSE then this option has no effect. */ bool useIso32000ExtensionFormatGeoreferencing = true; @@ -380,7 +380,7 @@ class CORE_EXPORT QgsLayoutExporter * can break compatibility with the built-in Acrobat geospatial tools (yes, Geospatial PDF * format is a mess!). * - * If PdfExportSettings::writeGeoPdf is FALSE than this option has no effect. + * If PdfExportSettings::writeGeoPdf is FALSE then this option has no effect. * * \deprecated QGIS 3.42. This parameter has no longer any effect. Only ISO 32000 georeferencing is handled. */ @@ -389,7 +389,7 @@ class CORE_EXPORT QgsLayoutExporter /** * TRUE if feature vector information (such as attributes) should be exported during Geospatial PDF exports. * - * If PdfExportSettings::writeGeoPdf is FALSE than this option has no effect. + * If PdfExportSettings::writeGeoPdf is FALSE then this option has no effect. */ bool includeGeoPdfFeatures = true; @@ -399,7 +399,8 @@ class CORE_EXPORT QgsLayoutExporter * If set, map item's which are not assigned a specific map theme will iterate through all listed * themes and a Geospatial PDF layer group will be created for each. * - * If PdfExportSettings::writeGeoPdf is FALSE than this option has no effect. + * If PdfExportSettings::writeGeoPdf is FALSE or PdfExportSettings::useQGISLayerTreeProperties is TRUE + * then this option has no effect. */ QStringList exportThemes; @@ -409,6 +410,18 @@ class CORE_EXPORT QgsLayoutExporter * \since QGIS 3.10 */ QVector predefinedMapScales; + + /** + * If set to TRUE, the layer tree from the QGIS project should be used when creating a Geospatial PDF. + * In that case, layer/group names, order, and visibility from the QGIS project will be reflected in the output PDF. + * + * When this option is active, the PdfExportSettings::exportThemes option has no effect. + * + * If PdfExportSettings::writeGeoPdf is FALSE then this option has no effect. + * + * \since QGIS 4.0 + */ + bool useQgisLayerTreeProperties = false; }; /** diff --git a/src/core/layout/qgslayoutgeopdfexporter.cpp b/src/core/layout/qgslayoutgeopdfexporter.cpp index 5665ab7a6883..58e6efe26b1b 100644 --- a/src/core/layout/qgslayoutgeopdfexporter.cpp +++ b/src/core/layout/qgslayoutgeopdfexporter.cpp @@ -192,3 +192,42 @@ QgsAbstractGeospatialPdfExporter::VectorComponentDetail QgsLayoutGeospatialPdfEx return detail; } +QgsLayerTree *QgsLayoutGeospatialPdfExporter::qgisLayerTree() const +{ + return mLayout->project()->layerTreeRoot(); +} + +bool QgsLayoutGeospatialPdfExporter::setMapItemLayersBeforeRendering() +{ + bool res = false; + + // Set project layers (including invisible ones) to all maps that + // don't follow themes nor locked layers. We'll restore their + // previous layer sets right after the rendering. + QList< QgsLayoutItemMap * > maps; + mLayout->layoutItems( maps ); + for ( QgsLayoutItemMap *map : std::as_const( maps ) ) + { + if ( !map->followVisibilityPreset() && !map->keepLayerSet() ) + { + // Store previous list of layers to restore it after the rendering + mTemporaryLayersToRender.insert( map->uuid(), map->layers() ); + map->setLayers( mLayout->project()->layerTreeRoot()->layerOrder() ); + res = true; + } + } + return res; +} + +void QgsLayoutGeospatialPdfExporter::restoreMapItemLayersAfterRendering() +{ + for ( auto it = mTemporaryLayersToRender.constBegin(); it != mTemporaryLayersToRender.constEnd(); it++ ) + { + QgsLayoutItem *item = mLayout->itemByUuid( it.key() ); + if ( item && item->type() == QgsLayoutItemRegistry::ItemType::LayoutMap ) + { + static_cast< QgsLayoutItemMap * >( item )->setLayers( it.value() ); + } + } + mTemporaryLayersToRender.clear(); +} diff --git a/src/core/layout/qgslayoutgeopdfexporter.h b/src/core/layout/qgslayoutgeopdfexporter.h index 2ebecd8ba98a..4df4a059b490 100644 --- a/src/core/layout/qgslayoutgeopdfexporter.h +++ b/src/core/layout/qgslayoutgeopdfexporter.h @@ -92,10 +92,34 @@ class CORE_EXPORT QgsLayoutGeospatialPdfExporter : public QgsAbstractGeospatialP */ QStringList layerTreeGroupOrder() const { return mLayerTreeGroupOrder; } + /** + * Sets QGIS project layers (including invisible ones) to layout item maps before rendering. + * Only map items that do not follow map themes nor locked layers are altered. + * + * Used by Geospatial PDF exports that should follow QGIS layer tree properties. + * + * \return whether at least one map was found not following map themes nor locked layers. + * + * \see restoreMapItemLayersAfterRendering() + * \since QGIS 4.0 + */ + bool setMapItemLayersBeforeRendering(); + + /** + * Restores map item layers after a rendering operation for Geospatial PDFs that follow QGIS layer tree properties. + * + * \see setMapItemLayersBeforeRendering() + * + * \since QGIS 4.0 + */ + void restoreMapItemLayersAfterRendering(); + private: VectorComponentDetail componentDetailForLayerId( const QString &layerId ) override; + QgsLayerTree *qgisLayerTree() const override; + QgsLayout *mLayout = nullptr; QHash< QgsLayoutItemMap *, QgsGeospatialPdfRenderedFeatureHandler * > mMapHandlers; @@ -104,6 +128,12 @@ class CORE_EXPORT QgsLayoutGeospatialPdfExporter : public QgsAbstractGeospatialP QStringList mLayerOrder; QStringList mLayerTreeGroupOrder; + /** + * To restore map item layers after a rendering operation for + * Geospatial PDF exports that follow QGIS layer tree properties + */ + QMap< QString, QList< QgsMapLayer * > > mTemporaryLayersToRender; + friend class TestQgsLayoutGeospatialPdfExport; }; diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index 9bcbdf99800a..4144ac283e3e 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -393,6 +393,9 @@ void QgsLayoutItemMap::setLayers( const QList &layers ) else { std::unique_ptr groupLayerClone { groupLayer->clone() }; + // It is important to preserve the original groupLayer id. + // E.g., to identify rendered files (by id) in Geospatial PDF exports. + groupLayerClone->setId( groupLayer->id() ); mGroupLayers[ groupLayer->id() ] = std::move( groupLayerClone ); *it = mGroupLayers[ groupLayer->id() ].get(); } diff --git a/src/core/maprenderer/qgsmaprenderertask.cpp b/src/core/maprenderer/qgsmaprenderertask.cpp index 40c8a7fd15ec..d6b99e013fe3 100644 --- a/src/core/maprenderer/qgsmaprenderertask.cpp +++ b/src/core/maprenderer/qgsmaprenderertask.cpp @@ -71,6 +71,8 @@ class QgsMapRendererTaskGeospatialPdfExporter : public QgsAbstractGeospatialPdfE return mLayerDetails.value( layerId ); } + QgsLayerTree *qgisLayerTree() const override { return nullptr; } + QMap< QString, VectorComponentDetail > mLayerDetails; }; diff --git a/src/core/qgsabstractgeopdfexporter.cpp b/src/core/qgsabstractgeopdfexporter.cpp index b5d076f6ed1d..0e9a2f54b0e8 100644 --- a/src/core/qgsabstractgeopdfexporter.cpp +++ b/src/core/qgsabstractgeopdfexporter.cpp @@ -24,6 +24,7 @@ #include "qgsgeometry.h" #include "qgslogger.h" #include "qgsvectorfilewriter.h" +#include "qgsvectorlayer.h" #include #include @@ -234,75 +235,46 @@ bool QgsAbstractGeospatialPdfExporter::saveTemporaryLayers() return true; } -///@cond PRIVATE -struct TreeNode +QString QgsAbstractGeospatialPdfExporter::createCompositionXml( const QList &components, const ExportDetails &details ) const { - QString id; - bool initiallyVisible = false; - QString name; - QString mutuallyExclusiveGroupId; - QString mapLayerId; - std::vector< std::unique_ptr< TreeNode > > children; - TreeNode *parent = nullptr; - - void addChild( std::unique_ptr< TreeNode > child ) - { - child->parent = this; - children.emplace_back( std::move( child ) ); - } + QDomDocument doc; + QDomElement compositionElem = doc.createElement( u"PDFComposition"_s ); - QDomElement toElement( QDomDocument &doc ) const - { - QDomElement layerElement = doc.createElement( u"Layer"_s ); - layerElement.setAttribute( u"id"_s, id ); - layerElement.setAttribute( u"name"_s, name ); - layerElement.setAttribute( u"initiallyVisible"_s, initiallyVisible ? u"true"_s : u"false"_s ); - if ( !mutuallyExclusiveGroupId.isEmpty() ) - layerElement.setAttribute( u"mutuallyExclusiveGroupId"_s, mutuallyExclusiveGroupId ); + // metadata + createMetadataXmlSection( compositionElem, doc, details ); - for ( const auto &child : children ) - { - layerElement.appendChild( child->toElement( doc ) ); - } + // pages + QDomElement page = doc.createElement( u"Page"_s ); + const double pageWidthPdfUnits = std::ceil( details.pageSizeMm.width() / 25.4 * 72 ); + const double pageHeightPdfUnits = std::ceil( details.pageSizeMm.height() / 25.4 * 72 ); + createPageDimensionXmlSection( page, doc, pageWidthPdfUnits, pageHeightPdfUnits ); - return layerElement; - } + // georeferencing + createGeoreferencingXmlSection( page, doc, details, pageWidthPdfUnits, pageHeightPdfUnits ); - QDomElement createIfLayerOnElement( QDomDocument &doc, QDomElement &contentElement ) const + // layer tree and content + QgsLayerTree *layerTree = qgisLayerTree(); + if ( details.useQgisLayerTreeProperties && layerTree ) { - QDomElement element = doc.createElement( u"IfLayerOn"_s ); - element.setAttribute( u"layerId"_s, id ); - contentElement.appendChild( element ); - return element; + createLayerTreeAndContentXmlSectionsFromLayerTree( layerTree, compositionElem, page, doc, components, details ); } - - QDomElement createNestedIfLayerOnElements( QDomDocument &doc, QDomElement &contentElement ) const + else { - TreeNode *currentParent = parent; - QDomElement finalElement = doc.createElement( u"IfLayerOn"_s ); - finalElement.setAttribute( u"layerId"_s, id ); - - QDomElement currentElement = finalElement; - while ( currentParent ) - { - QDomElement ifGroupOn = doc.createElement( u"IfLayerOn"_s ); - ifGroupOn.setAttribute( u"layerId"_s, currentParent->id ); - ifGroupOn.appendChild( currentElement ); - currentElement = ifGroupOn; - currentParent = currentParent->parent; - } - contentElement.appendChild( currentElement ); - return finalElement; + createLayerTreeAndContentXmlSections( compositionElem, page, doc, components, details ); } -}; -///@endcond -QString QgsAbstractGeospatialPdfExporter::createCompositionXml( const QList &components, const ExportDetails &details ) -{ - QDomDocument doc; + compositionElem.appendChild( page ); + doc.appendChild( compositionElem ); - QDomElement compositionElem = doc.createElement( u"PDFComposition"_s ); + QString composition; + QTextStream stream( &composition ); + doc.save( stream, -1 ); + + return composition; +} +void QgsAbstractGeospatialPdfExporter::createMetadataXmlSection( QDomElement &compositionElem, QDomDocument &doc, const ExportDetails &details ) const +{ // metadata tags QDomElement metadata = doc.createElement( u"Metadata"_s ); if ( !details.author.isEmpty() ) @@ -367,7 +339,100 @@ QString QgsAbstractGeospatialPdfExporter::createCompositionXml( const QList &components, const ExportDetails &details ) const +{ QSet< QString > createdLayerIds; std::vector< std::unique_ptr< TreeNode > > rootGroups; std::vector< std::unique_ptr< TreeNode > > rootLayers; @@ -404,6 +469,7 @@ QString QgsAbstractGeospatialPdfExporter::createCompositionXml( const QList groupNameToTreeNode; QMap< QString, TreeNode * > layerIdToTreeNode; @@ -511,95 +577,103 @@ QString QgsAbstractGeospatialPdfExporter::createCompositionXml( const QList &a, const std::unique_ptr< TreeNode > &b ) -> bool { - QDomElement georeferencing = doc.createElement( u"Georeferencing"_s ); - georeferencing.setAttribute( u"id"_s, u"georeferenced_%1"_s.arg( i++ ) ); - georeferencing.setAttribute( u"ISO32000ExtensionFormat"_s, details.useIso32000ExtensionFormatGeoreferencing ? u"true"_s : u"false"_s ); + return layerTreeGroupOrder.indexOf( a->name ) < layerTreeGroupOrder.indexOf( b->name ); + } ); - if ( section.crs.isValid() ) + bool haveFoundMutuallyExclusiveGroup = false; + for ( const auto &node : std::as_const( rootGroups ) ) + { + if ( !node->mutuallyExclusiveGroupId.isEmpty() ) { - QDomElement srs = doc.createElement( u"SRS"_s ); - // not currently used by GDAL or the PDF spec, but exposed in the GDAL XML schema. Maybe something we'll need to consider down the track... - // srs.setAttribute( u"dataAxisToSRSAxisMapping"_s, u"2,1"_s ); - if ( !section.crs.authid().isEmpty() && !section.crs.authid().startsWith( u"user"_s, Qt::CaseInsensitive ) ) - { - srs.appendChild( doc.createTextNode( section.crs.authid() ) ); - } - else - { - srs.appendChild( doc.createTextNode( section.crs.toWkt( Qgis::CrsWktVariant::PreferredGdal ) ) ); - } - georeferencing.appendChild( srs ); + // only the first object in a mutually exclusive group is initially visible + node->initiallyVisible = !haveFoundMutuallyExclusiveGroup; + haveFoundMutuallyExclusiveGroup = true; } + layerTree.appendChild( node->toElement( doc ) ); + } - if ( !section.pageBoundsPolygon.isEmpty() ) - { - /* - Define a polygon / neatline in PDF units into which the - Measure tool will display coordinates. - If not specified, BoundingBox will be used instead. - If none of BoundingBox and BoundingPolygon are specified, - the whole PDF page will be assumed to be georeferenced. - */ - QDomElement boundingPolygon = doc.createElement( u"BoundingPolygon"_s ); + // filter out groups which don't have any content + layerTreeGroupOrder.erase( std::remove_if( layerTreeGroupOrder.begin(), layerTreeGroupOrder.end(), [&details]( const QString & group ) + { + return details.customLayerTreeGroups.key( group ).isEmpty(); + } ), layerTreeGroupOrder.end() ); - // transform to PDF coordinate space - QTransform t = QTransform::fromTranslate( 0, pageHeightPdfUnits ).scale( pageWidthPdfUnits / details.pageSizeMm.width(), - -pageHeightPdfUnits / details.pageSizeMm.height() ); - QgsPolygon p = section.pageBoundsPolygon; - p.transform( t ); - boundingPolygon.appendChild( doc.createTextNode( p.asWkt() ) ); + // then top-level layers + std::sort( rootLayers.begin(), rootLayers.end(), [&details]( const std::unique_ptr< TreeNode > &a, const std::unique_ptr< TreeNode > &b ) -> bool + { + const int indexA = details.layerOrder.indexOf( a->mapLayerId ); + const int indexB = details.layerOrder.indexOf( b->mapLayerId ); - georeferencing.appendChild( boundingPolygon ); - } - else - { - /* Define the viewport where georeferenced coordinates are available. - If not specified, the extent of BoundingPolygon will be used instead. - If none of BoundingBox and BoundingPolygon are specified, - the whole PDF page will be assumed to be georeferenced. - */ - QDomElement boundingBox = doc.createElement( u"BoundingBox"_s ); - boundingBox.setAttribute( u"x1"_s, qgsDoubleToString( section.pageBoundsMm.xMinimum() / 25.4 * 72 ) ); - boundingBox.setAttribute( u"y1"_s, qgsDoubleToString( section.pageBoundsMm.yMinimum() / 25.4 * 72 ) ); - boundingBox.setAttribute( u"x2"_s, qgsDoubleToString( section.pageBoundsMm.xMaximum() / 25.4 * 72 ) ); - boundingBox.setAttribute( u"y2"_s, qgsDoubleToString( section.pageBoundsMm.yMaximum() / 25.4 * 72 ) ); - georeferencing.appendChild( boundingBox ); - } + if ( indexA >= 0 && indexB >= 0 ) + return indexA < indexB; + else if ( indexA >= 0 ) + return true; + else if ( indexB >= 0 ) + return false; - for ( const ControlPoint &point : section.controlPoints ) + return a->name.localeAwareCompare( b->name ) < 0; + } ); + + for ( const auto &node : std::as_const( rootLayers ) ) + { + layerTree.appendChild( node->toElement( doc ) ); + } + + compositionElem.appendChild( layerTree ); +} + +void QgsAbstractGeospatialPdfExporter::createLayerTreeAndContentXmlSectionsFromLayerTree( const QgsLayerTree *layerTree, QDomElement &compositionElem, QDomElement &pageElem, QDomDocument &doc, const QList &components, const ExportDetails &details ) const +{ + QMap< QString, TreeNode * > groupNameToTreeNode; + QMap< QString, TreeNode * > layerIdToTreeNode; + + // Add tree structure from QGIS layer tree to the intermediate TreeNode struct + std::unique_ptr< TreeNode > rootPdfNode = createPdfTreeNodes( groupNameToTreeNode, layerIdToTreeNode, layerTree ); + rootPdfNode->isRootNode = true; // To skip the layer tree root from the PDF layer tree + + // Add missing groups from other components + for ( const ComponentLayerDetail &component : components ) + { + if ( !component.group.isEmpty() && !groupNameToTreeNode.contains( component.group ) ) { - QDomElement cp1 = doc.createElement( u"ControlPoint"_s ); - cp1.setAttribute( u"x"_s, qgsDoubleToString( point.pagePoint.x() / 25.4 * 72 ) ); - cp1.setAttribute( u"y"_s, qgsDoubleToString( ( details.pageSizeMm.height() - point.pagePoint.y() ) / 25.4 * 72 ) ); - cp1.setAttribute( u"GeoX"_s, qgsDoubleToString( point.geoPoint.x() ) ); - cp1.setAttribute( u"GeoY"_s, qgsDoubleToString( point.geoPoint.y() ) ); - georeferencing.appendChild( cp1 ); + auto pdfTreeGroup = std::make_unique< TreeNode >(); + const QString id = QUuid::createUuid().toString(); + pdfTreeGroup->id = id; + pdfTreeGroup->name = component.group; + pdfTreeGroup->initiallyVisible = true; + groupNameToTreeNode[ pdfTreeGroup->name ] = pdfTreeGroup.get(); + rootPdfNode->addChild( std::move( pdfTreeGroup ) ); } - - page.appendChild( georeferencing ); } + QDomElement contentElem = doc.createElement( u"Content"_s ); + createContentXmlSection( contentElem, doc, groupNameToTreeNode, layerIdToTreeNode, components, details ); + + pageElem.appendChild( contentElem ); + + // PDF Layer Tree + QDomElement layerTreeElem = doc.createElement( u"LayerTree"_s ); + rootPdfNode->toChildrenElements( doc, layerTreeElem ); + compositionElem.appendChild( layerTreeElem ); +} + +void QgsAbstractGeospatialPdfExporter::createContentXmlSection( QDomElement &contentElem, QDomDocument &doc, const QMap< QString, TreeNode * > &groupNameToTreeNode, const QMap< QString, TreeNode * > &layerIdToTreeNode, const QList &components, const ExportDetails &details ) const +{ auto createPdfDatasetElement = [&doc]( const ComponentLayerDetail & component ) -> QDomElement { QDomElement pdfDataset = doc.createElement( u"PDF"_s ); @@ -615,25 +689,24 @@ QString QgsAbstractGeospatialPdfExporter::createCompositionXml( const QListcreateNestedIfLayerOnElements( doc, content ); + QDomElement ifLayerOnElement = treeNode->createNestedIfLayerOnElements( doc, contentElem ); ifLayerOnElement.appendChild( createPdfDatasetElement( component ) ); } } else if ( TreeNode *groupNode = groupNameToTreeNode.value( component.group ) ) { - QDomElement ifGroupOn = groupNode->createIfLayerOnElement( doc, content ); + QDomElement ifGroupOn = groupNode->createIfLayerOnElement( doc, contentElem ); ifGroupOn.appendChild( createPdfDatasetElement( component ) ); } } @@ -645,7 +718,7 @@ QString QgsAbstractGeospatialPdfExporter::createCompositionXml( const QListcreateNestedIfLayerOnElements( doc, content ); + QDomElement ifLayerOnElement = treeNode->createNestedIfLayerOnElements( doc, contentElem ); QDomElement vectorDataset = doc.createElement( u"Vector"_s ); vectorDataset.setAttribute( u"dataset"_s, component.sourceVectorPath ); @@ -660,71 +733,92 @@ QString QgsAbstractGeospatialPdfExporter::createCompositionXml( const QList QgsAbstractGeospatialPdfExporter::createPdfTreeNodes( QMap< QString, TreeNode * > &groupNameToTreeNode, QMap< QString, TreeNode * > &layerIdToTreeNode, const QgsLayerTreeGroup *layerTreeGroup ) const +{ + auto pdfTreeNodes = std::make_unique< TreeNode >(); + const QString id = QUuid::createUuid().toString(); + pdfTreeNodes->id = id; + pdfTreeNodes->name = layerTreeGroup->name(); + pdfTreeNodes->initiallyVisible = layerTreeGroup->itemVisibilityChecked(); - // groups are added first + const QList groupChildren = layerTreeGroup->children(); - // sort root groups in desired order - std::sort( rootGroups.begin(), rootGroups.end(), [&layerTreeGroupOrder]( const std::unique_ptr< TreeNode > &a, const std::unique_ptr< TreeNode > &b ) -> bool - { - return layerTreeGroupOrder.indexOf( a->name ) < layerTreeGroupOrder.indexOf( b->name ); - } ); - - bool haveFoundMutuallyExclusiveGroup = false; - for ( const auto &node : std::as_const( rootGroups ) ) + for ( QgsLayerTreeNode *qgisNode : groupChildren ) { - if ( !node->mutuallyExclusiveGroupId.isEmpty() ) + switch ( qgisNode->nodeType() ) { - // only the first object in a mutually exclusive group is initially visible - node->initiallyVisible = !haveFoundMutuallyExclusiveGroup; - haveFoundMutuallyExclusiveGroup = true; - } - layerTree.appendChild( node->toElement( doc ) ); - } - - // filter out groups which don't have any content - layerTreeGroupOrder.erase( std::remove_if( layerTreeGroupOrder.begin(), layerTreeGroupOrder.end(), [&details]( const QString & group ) - { - return details.customLayerTreeGroups.key( group ).isEmpty(); - } ), layerTreeGroupOrder.end() ); + case QgsLayerTreeNode::NodeLayer: + { + QgsLayerTreeLayer *layerTreeLayer = qobject_cast( qgisNode ); + // Skip invalid layers, tables and unknown geometry types, since they won't appear in the PDF + if ( !layerTreeLayer->layer()->isValid() ) + break; - // then top-level layers - std::sort( rootLayers.begin(), rootLayers.end(), [&details]( const std::unique_ptr< TreeNode > &a, const std::unique_ptr< TreeNode > &b ) -> bool - { - const int indexA = details.layerOrder.indexOf( a->mapLayerId ); - const int indexB = details.layerOrder.indexOf( b->mapLayerId ); + if ( layerTreeLayer->layer()->type() == Qgis::LayerType::Vector ) + { + QgsVectorLayer *vectorLayer = qobject_cast< QgsVectorLayer * >( layerTreeLayer->layer() ); + if ( vectorLayer->geometryType() == Qgis::GeometryType::Unknown || vectorLayer->geometryType() == Qgis::GeometryType::Null ) + break; + } - if ( indexA >= 0 && indexB >= 0 ) - return indexA < indexB; - else if ( indexA >= 0 ) - return true; - else if ( indexB >= 0 ) - return false; + auto pdfLayerNode = std::make_unique< TreeNode >(); + pdfLayerNode->id = layerTreeLayer->layerId(); + pdfLayerNode->name = layerTreeLayer->name(); + pdfLayerNode->initiallyVisible = layerTreeLayer->itemVisibilityChecked(); + pdfLayerNode->mapLayerId = layerTreeLayer->layerId(); + layerIdToTreeNode.insert( pdfLayerNode->id, pdfLayerNode.get() ); + pdfTreeNodes->addChild( std::move( pdfLayerNode ) ); + break; + } - return a->name.localeAwareCompare( b->name ) < 0; - } ); + case QgsLayerTreeNode::NodeGroup: + { + QgsLayerTreeGroup *childLayerTreeGroup = qobject_cast( qgisNode ); - for ( const auto &node : std::as_const( rootLayers ) ) - { - layerTree.appendChild( node->toElement( doc ) ); - } + // GroupLayers support + if ( QgsGroupLayer *groupLayer = childLayerTreeGroup->groupLayer() ) + { + // We deal with it as another map layer + auto pdfLayerNode = std::make_unique< TreeNode >(); + pdfLayerNode->id = groupLayer->id(); + pdfLayerNode->name = childLayerTreeGroup->name(); + pdfLayerNode->initiallyVisible = childLayerTreeGroup->itemVisibilityChecked(); + pdfLayerNode->mapLayerId = groupLayer->id(); + layerIdToTreeNode.insert( pdfLayerNode->id, pdfLayerNode.get() ); + pdfTreeNodes->addChild( std::move( pdfLayerNode ) ); + break; + } - compositionElem.appendChild( layerTree ); - compositionElem.appendChild( page ); + // Skip empty groups + if ( !childLayerTreeGroup->children().empty() ) + { + std::unique_ptr< TreeNode > pdfGroupNode = createPdfTreeNodes( groupNameToTreeNode, layerIdToTreeNode, childLayerTreeGroup ); + + // A group that is not empty in the QGIS layer tree, may be emptied + // if it only contais invalid and/or geometryless layers. Skip it! + if ( !pdfGroupNode->children.empty() ) + { + pdfTreeNodes->addChild( std::move( pdfGroupNode ) ); + } + } + break; + } - doc.appendChild( compositionElem ); + case QgsLayerTreeNode::NodeCustom: + break; + } + } - QString composition; - QTextStream stream( &composition ); - doc.save( stream, -1 ); + // Now we know if our group is not empty. Add it to the groupNameToTreeNode then. + if ( !pdfTreeNodes->children.empty() ) + { + groupNameToTreeNode[ pdfTreeNodes->name ] = pdfTreeNodes.get(); + } - return composition; + return pdfTreeNodes; } QString QgsAbstractGeospatialPdfExporter::compositionModeToString( QPainter::CompositionMode mode ) diff --git a/src/core/qgsabstractgeopdfexporter.h b/src/core/qgsabstractgeopdfexporter.h index f54ca8d99041..37faa525c4a7 100644 --- a/src/core/qgsabstractgeopdfexporter.h +++ b/src/core/qgsabstractgeopdfexporter.h @@ -20,6 +20,7 @@ #include "qgsabstractmetadatabase.h" #include "qgscoordinatereferencesystem.h" #include "qgsfeature.h" +#include "qgslayertree.h" #include "qgspolygon.h" #include @@ -33,6 +34,80 @@ class QgsGeospatialPdfRenderedFeatureHandler; + +///@cond PRIVATE +struct TreeNode +{ + QString id; + bool initiallyVisible = false; + QString name; + QString mutuallyExclusiveGroupId; + QString mapLayerId; + std::vector< std::unique_ptr< TreeNode > > children; + TreeNode *parent = nullptr; + bool isRootNode = false; + + void addChild( std::unique_ptr< TreeNode > child ) + { + child->parent = this; + children.emplace_back( std::move( child ) ); + } + + QDomElement toElement( QDomDocument &doc ) const + { + QDomElement layerElement = doc.createElement( u"Layer"_s ); + layerElement.setAttribute( u"id"_s, id ); + layerElement.setAttribute( u"name"_s, name ); + layerElement.setAttribute( u"initiallyVisible"_s, initiallyVisible ? u"true"_s : u"false"_s ); + if ( !mutuallyExclusiveGroupId.isEmpty() ) + layerElement.setAttribute( u"mutuallyExclusiveGroupId"_s, mutuallyExclusiveGroupId ); + + for ( const auto &child : children ) + { + layerElement.appendChild( child->toElement( doc ) ); + } + + return layerElement; + } + + void toChildrenElements( QDomDocument &doc, QDomElement &layerTreeElem ) const + { + for ( const auto &child : children ) + { + layerTreeElem.appendChild( child->toElement( doc ) ); + } + } + + QDomElement createIfLayerOnElement( QDomDocument &doc, QDomElement &contentElement ) const + { + QDomElement element = doc.createElement( u"IfLayerOn"_s ); + element.setAttribute( u"layerId"_s, id ); + contentElement.appendChild( element ); + return element; + } + + QDomElement createNestedIfLayerOnElements( QDomDocument &doc, QDomElement &contentElement ) const + { + TreeNode *currentParent = parent; + QDomElement finalElement = doc.createElement( u"IfLayerOn"_s ); + finalElement.setAttribute( u"layerId"_s, id ); + + QDomElement currentElement = finalElement; + while ( currentParent && !currentParent->isRootNode ) + { + QDomElement ifGroupOn = doc.createElement( u"IfLayerOn"_s ); + ifGroupOn.setAttribute( u"layerId"_s, currentParent->id ); + ifGroupOn.appendChild( currentElement ); + currentElement = ifGroupOn; + currentParent = currentParent->parent; + } + contentElement.appendChild( currentElement ); + return finalElement; + } +}; +///@endcond + + /** * \class QgsAbstractGeospatialPdfExporter * \ingroup core @@ -256,6 +331,8 @@ class CORE_EXPORT QgsAbstractGeospatialPdfExporter * Layers which are not included in this group will always have their own individual layer tree entry * created for them automatically. * + * If ExportDetails::useQGISLayerTreeProperties is TRUE then this option has no effect. + * * \see layerTreeGroupOrder */ QMap< QString, QString > customLayerTreeGroups; @@ -263,6 +340,8 @@ class CORE_EXPORT QgsAbstractGeospatialPdfExporter /** * Optional map of map layer ID to custom layer tree name to show in the created PDF file. * + * If ExportDetails::useQGISLayerTreeProperties is TRUE then this option has no effect. + * * \since QGIS 3.14 */ QMap< QString, QString > layerIdToPdfLayerTreeNameMap; @@ -271,6 +350,8 @@ class CORE_EXPORT QgsAbstractGeospatialPdfExporter * Optional map of map layer ID to initial visibility state. If a layer ID is not present in this, * it will default to being initially visible when opening the PDF. * + * If ExportDetails::useQGISLayerTreeProperties is TRUE then this option has no effect. + * * \since QGIS 3.14 */ QMap< QString, bool > initialLayerVisibility; @@ -280,6 +361,8 @@ class CORE_EXPORT QgsAbstractGeospatialPdfExporter * * Layers appearing earlier in the list will show earlier in the Geospatial PDF layer tree list. * + * If ExportDetails::useQGISLayerTreeProperties is TRUE then this option has no effect. + * * \see layerTreeGroupOrder * * \since QGIS 3.14 @@ -291,6 +374,8 @@ class CORE_EXPORT QgsAbstractGeospatialPdfExporter * * Groups appearing earlier in the list will show earlier in the Geospatial PDF layer tree list. * + * If ExportDetails::useQGISLayerTreeProperties is TRUE then this option has no effect. + * * \see layerOrder * \see customLayerTreeGroups * @@ -301,10 +386,31 @@ class CORE_EXPORT QgsAbstractGeospatialPdfExporter /** * Contains a list of group names which should be considered as mutually exclusive. * + * If ExportDetails::useQGISLayerTreeProperties is TRUE then this option has no effect. + * * \since QGIS 3.40 */ QSet< QString > mutuallyExclusiveGroups; + /** + * If set to TRUE, the layer tree from the QGIS project should be used when creating a Geospatial PDF. + * In that case, layer/group names, order, and visibility from the QGIS project will be reflected in the output PDF. + * + * When active, other settings like layerOrder, customLayerTreeGroups, + * layerTreeGroupOrder, initialLayerVisibility, mutuallyExclusiveGroups, and + * layerIdToPdfLayerTreeNameMap have no effect. + * + * \see layerOrder + * \see customLayerTreeGroups + * \see layerTreeGroupOrder + * \see initialLayerVisibility + * \see layerIdToPdfLayerTreeNameMap + * \see mutuallyExclusiveGroups + * + * \since QGIS 4.0 + */ + bool useQgisLayerTreeProperties = false; + }; /** @@ -377,6 +483,11 @@ class CORE_EXPORT QgsAbstractGeospatialPdfExporter */ virtual VectorComponentDetail componentDetailForLayerId( const QString &layerId ) = 0; + /** + * Returns the QGIS layer tree so that a composition XML can be created based on its properties. + */ + virtual QgsLayerTree *qgisLayerTree() const = 0; + QList< VectorComponentDetail > mVectorComponents; QString mErrorMessage; @@ -385,7 +496,19 @@ class CORE_EXPORT QgsAbstractGeospatialPdfExporter bool saveTemporaryLayers(); - QString createCompositionXml( const QList< QgsAbstractGeospatialPdfExporter::ComponentLayerDetail > &components, const ExportDetails &details ); + QString createCompositionXml( const QList< QgsAbstractGeospatialPdfExporter::ComponentLayerDetail > &components, const ExportDetails &details ) const; + void createMetadataXmlSection( QDomElement &compositionElem, QDomDocument &doc, const ExportDetails &details ) const; + void createPageDimensionXmlSection( QDomElement &pageElem, QDomDocument &doc, const double pageWidthPdfUnits, const double pageHeightPdfUnits ) const; + void createGeoreferencingXmlSection( QDomElement &pageElem, QDomDocument &doc, const ExportDetails &details, const double pageWidthPdfUnits, const double pageHeightPdfUnits ) const; + void createContentXmlSection( QDomElement &contentElem, QDomDocument &doc, const QMap< QString, TreeNode * > &groupNameToTreeNode, const QMap< QString, TreeNode * > &layerIdToTreeNode, const QList &components, const ExportDetails &details ) const; + + void createLayerTreeAndContentXmlSections( QDomElement &compositionElem, QDomElement &pageElem, QDomDocument &doc, const QList &components, const ExportDetails &details ) const; + void createLayerTreeAndContentXmlSectionsFromLayerTree( const QgsLayerTree *layerTree, QDomElement &compositionElem, QDomElement &pageElem, QDomDocument &doc, const QList &components, const ExportDetails &details ) const; + + /** + * Creates a TreeNode structure from a given layer tree group recursively. + */ + std::unique_ptr< TreeNode > createPdfTreeNodes( QMap< QString, TreeNode * > &groupNameToTreeNode, QMap< QString, TreeNode * > &layerIdToTreeNode, const QgsLayerTreeGroup *layerTreeGroup ) const; /** * Returns the GDAL string representation of the specified QPainter composition \a mode. diff --git a/src/gui/layout/qgslayoutpdfexportoptionsdialog.cpp b/src/gui/layout/qgslayoutpdfexportoptionsdialog.cpp index 4b2eeb8e43f1..e3ad1c9f6490 100644 --- a/src/gui/layout/qgslayoutpdfexportoptionsdialog.cpp +++ b/src/gui/layout/qgslayoutpdfexportoptionsdialog.cpp @@ -61,6 +61,10 @@ QgsLayoutPdfExportOptionsDialog::QgsLayoutPdfExportOptionsDialog( QWidget *paren mGeospatialPDFOptionsStackedWidget->setCurrentIndex( 1 ); } + mGeospatialPDFCustomConfigRadioButton->setChecked( true ); + mGeospatialPDFCustomConfigFrame->setEnabled( true ); + connect( mGeospatialPDFQgisConfigRadioButton, &QRadioButton::toggled, this, &QgsLayoutPdfExportOptionsDialog::toggleQgisConfig ); + mComboImageCompression->addItem( tr( "Lossy (JPEG)" ), false ); mComboImageCompression->addItem( tr( "Lossless" ), true ); @@ -200,6 +204,30 @@ bool QgsLayoutPdfExportOptionsDialog::exportGeospatialPdf() const return mGeospatialPDFGroupBox->isChecked(); } +void QgsLayoutPdfExportOptionsDialog::setUseQgisLayerTreeConfig( bool enabled ) +{ + if ( !mGeospatialPdfAvailable ) + return; + + mGeospatialPDFQgisConfigRadioButton->setChecked( enabled ); + mGeospatialPDFCustomConfigFrame->setEnabled( !enabled ); +} + +bool QgsLayoutPdfExportOptionsDialog::useQgisLayerTreeConfig() const +{ + if ( !mGeospatialPdfAvailable ) + return false; + + return mGeospatialPDFQgisConfigRadioButton->isChecked(); +} + +void QgsLayoutPdfExportOptionsDialog::disableUseQgisLayerTreeConfig() +{ + setUseQgisLayerTreeConfig( false ); + mGeospatialPDFQgisConfigRadioButton->setEnabled( false ); + mGeospatialPDFQgisConfigRadioButton->setToolTip( u"Unavailable: All map items in the layout are currently following either map themes or locked layers, which is not compatible with the QGIS layer tree configuration."_s ); +} + void QgsLayoutPdfExportOptionsDialog::setExportThemes( const QStringList &themes ) { if ( !mGeospatialPdfAvailable ) @@ -303,3 +331,8 @@ void QgsLayoutPdfExportOptionsDialog::showContextMenuForGeospatialPdfStructure( mGeospatialPdfStructureTreeMenu->exec( mGeospatialPdfStructureTree->mapToGlobal( point ) ); } } + +void QgsLayoutPdfExportOptionsDialog::toggleQgisConfig() +{ + mGeospatialPDFCustomConfigFrame->setEnabled( !mGeospatialPDFQgisConfigRadioButton->isChecked() ); +} diff --git a/src/gui/layout/qgslayoutpdfexportoptionsdialog.h b/src/gui/layout/qgslayoutpdfexportoptionsdialog.h index 9f8ebb1020cc..a55c9bd9ab74 100644 --- a/src/gui/layout/qgslayoutpdfexportoptionsdialog.h +++ b/src/gui/layout/qgslayoutpdfexportoptionsdialog.h @@ -91,6 +91,22 @@ class GUI_EXPORT QgsLayoutPdfExportOptionsDialog : public QDialog, private Ui::Q //! Returns whether Geospatial PDF export is enabled bool exportGeospatialPdf() const; + /** + * Sets whether to use QGIS layer tree config to export a Geospatial PDF. + * + * If disabled, a custom configuration will be used. + */ + void setUseQgisLayerTreeConfig( bool enabled ); + //! Returns whether to use QGIS layer tree config to export a Geospatial PDF + bool useQgisLayerTreeConfig() const; + /** + * Disables the option to follow QGIS layer tree configuration from the GUI. + * + * That option should be disabled if all map items in the layout follow + * either map themes or locked layers. + */ + void disableUseQgisLayerTreeConfig(); + //! Sets the list of export themes void setExportThemes( const QStringList &themes ); //! Returns the list of export themes @@ -109,6 +125,7 @@ class GUI_EXPORT QgsLayoutPdfExportOptionsDialog : public QDialog, private Ui::Q private slots: + void toggleQgisConfig(); void showHelp(); void showContextMenuForGeospatialPdfStructure( QPoint point, const QModelIndex &index ); diff --git a/src/ui/layout/qgspdfexportoptions.ui b/src/ui/layout/qgspdfexportoptions.ui index a88d53cec120..f9b2f5b99845 100644 --- a/src/ui/layout/qgspdfexportoptions.ui +++ b/src/ui/layout/qgspdfexportoptions.ui @@ -17,7 +17,7 @@ - QFrame::NoFrame + QFrame::Shape::NoFrame true @@ -28,7 +28,7 @@ 0 0 533 - 944 + 1002 @@ -162,61 +162,114 @@ - + - 0 + 9 - 0 + 9 - 0 + 9 - 0 + 9 - - - - Include multiple map themes + + + + Set a custom configuration of layer and group names, order, etc. - - true + + Custom configuration - false + true - - - - - - - - - Layer Structure + + + + Prefer QGIS Layer Tree properties for layer and group names, order, and visibility. - - false + + Follow QGIS layer tree configuration - - - - - Uncheck layers to avoid exporting vector feature information for those layers, and optionally set the group name to allow multiple layers to be joined into a single logical PDF group. Layers can be dragged and dropped to rearrange their order in the generated Geospatial PDF table of contents. + + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Raised + + + + 20 + + + 0 + + + 0 + + + 0 + + + + + Include multiple map themes - + true + + false + + + + 9 + + + + + - - - - true + + + + Layer Structure + + + false + + + 9 + + + + + Uncheck layers to avoid exporting vector feature information for those layers, and optionally set the group name to allow multiple layers to be joined into a single logical PDF group. Layers can be dragged and dropped to rearrange their order in the generated Geospatial PDF table of contents. + + + true + + + + + + + true + + + + @@ -264,10 +317,10 @@ - Qt::Vertical + Qt::Orientation::Vertical - QSizePolicy::Expanding + QSizePolicy::Policy::Expanding @@ -286,17 +339,17 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Help + QDialogButtonBox::StandardButton::Help - Qt::Horizontal + Qt::Orientation::Horizontal @@ -316,10 +369,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Cancel|QDialogButtonBox::Save + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save @@ -349,11 +402,14 @@ mTextRenderFormatComboBox mComboImageCompression mGeospatialPDFGroupBox + mGeospatialPDFQgisConfigRadioButton + mGeospatialPDFCustomConfigRadioButton mIncludeMapThemesCheck mThemesList mGeospatialPdfStructureTree - mSimplifyGeometriesCheckbox mDisableRasterTilingCheckBox + mSimplifyGeometriesCheckbox + mOpenAfterExportingCheckBox diff --git a/tests/src/core/testqgsgeopdfexport.cpp b/tests/src/core/testqgsgeopdfexport.cpp index 494de8009292..2169b879af01 100644 --- a/tests/src/core/testqgsgeopdfexport.cpp +++ b/tests/src/core/testqgsgeopdfexport.cpp @@ -24,6 +24,9 @@ class TestGeospatialPdfExporter : public QgsAbstractGeospatialPdfExporter { + public: + void setQGISLayerTree( QgsLayerTree *layerTree ) { mLayerTree = layerTree; } + private: VectorComponentDetail componentDetailForLayerId( const QString &layerId ) override { @@ -34,6 +37,10 @@ class TestGeospatialPdfExporter : public QgsAbstractGeospatialPdfExporter detail.displayAttribute = u"attr %1"_s.arg( layerId ); return detail; } + virtual QgsLayerTree *qgisLayerTree() const override { return mLayerTree; }; + + // For testing purposes + QgsLayerTree *mLayerTree = nullptr; }; class TestQgsGeospatialPdfExport : public QgsTest @@ -59,6 +66,10 @@ class TestQgsGeospatialPdfExport : public QgsTest void compositionMode(); void testMutuallyExclusiveGroupsLayers(); void testMutuallyExclusiveGroupsCustom(); + void testCreatePdfTreeNodes(); + void testCreatePdfTreeNodesWithGroupLayer(); + void testUseQgisLayerTree(); + void testUseQgisLayerTreeInvisibleNodes(); }; void TestQgsGeospatialPdfExport::initTestCase() @@ -979,6 +990,459 @@ void TestQgsGeospatialPdfExport::compositionMode() QCOMPARE( QgsAbstractGeospatialPdfExporter::compositionModeToString( QPainter::CompositionMode_Plus ), u"Normal"_s ); } +void TestQgsGeospatialPdfExport::testCreatePdfTreeNodes() +{ + TestGeospatialPdfExporter geospatialPdfExporter; + QgsProject p; + QVERIFY( p.read( TEST_DATA_DIR + u"/geospatial_pdf_projects/test_nested_groups_no_graphics.qgz"_s ) ); + + QMap< QString, TreeNode * > groupNameToTreeNode; + QMap< QString, TreeNode * > layerIdToTreeNode; + + QgsLayerTree *qgisLayerTree = p.layerTreeRoot(); + + std::unique_ptr< TreeNode > rootTreeNode = geospatialPdfExporter.createPdfTreeNodes( groupNameToTreeNode, layerIdToTreeNode, qgisLayerTree ); + rootTreeNode->isRootNode = true; + + // Check the rootTreeNode structure matches the one from the project's layer tree + + // First check the root tree node + QVERIFY( rootTreeNode->isRootNode ); + QVERIFY( !rootTreeNode->id.isEmpty() ); + QVERIFY( rootTreeNode->name.isEmpty() ); + QVERIFY( rootTreeNode->mapLayerId.isEmpty() ); + QVERIFY( !rootTreeNode->parent ); + + // Then check children + QCOMPARE( rootTreeNode->children.size(), 3L ); + TreeNode *firstChild = rootTreeNode->children.at( 0 ).get(); + QVERIFY( firstChild->parent ); + QCOMPARE( firstChild->parent->id, rootTreeNode->id ); + QVERIFY( firstChild->children.empty() ); + QCOMPARE( firstChild->name, qgisLayerTree->children().at( 0 )->name() ); // points + + TreeNode *secondChild = rootTreeNode->children.at( 1 ).get(); + QVERIFY( secondChild->parent ); + QCOMPARE( secondChild->parent->id, rootTreeNode->id ); + QCOMPARE( secondChild->children.size(), 3L ); + QCOMPARE( secondChild->name, qgisLayerTree->children().at( 1 )->name() ); // group1 + + TreeNode *firstSubChild = secondChild->children.at( 0 ).get(); + QVERIFY( firstSubChild->parent ); + QCOMPARE( firstSubChild->parent->id, secondChild->id ); + QVERIFY( firstSubChild->children.empty() ); + QCOMPARE( firstSubChild->name, qgisLayerTree->children().at( 1 )->children().at( 0 )->name() ); // multipoint + + TreeNode *secondSubChild = secondChild->children.at( 1 ).get(); + QVERIFY( secondSubChild->parent ); + QCOMPARE( secondSubChild->parent->id, secondChild->id ); + QCOMPARE( secondSubChild->children.size(), 1L ); + QCOMPARE( secondSubChild->name, qgisLayerTree->children().at( 1 )->children().at( 1 )->name() ); // sub-group1 + + TreeNode *firstSubSubChild = secondSubChild->children.at( 0 ).get(); + QVERIFY( firstSubSubChild->parent ); + QCOMPARE( firstSubSubChild->parent->id, secondSubChild->id ); + QVERIFY( firstSubSubChild->children.empty() ); + QCOMPARE( firstSubSubChild->name, qgisLayerTree->children().at( 1 )->children().at( 1 )->children().at( 0 )->name() ); // lines + + TreeNode *thirdSubChild = secondChild->children.at( 2 ).get(); + QVERIFY( thirdSubChild->parent ); + QCOMPARE( thirdSubChild->parent->id, secondChild->id ); + QVERIFY( thirdSubChild->children.empty() ); + QCOMPARE( thirdSubChild->name, qgisLayerTree->children().at( 1 )->children().at( 2 )->name() ); // polys + + + TreeNode *thirdChild = rootTreeNode->children.at( 2 ).get(); + QVERIFY( thirdChild->parent ); + QCOMPARE( thirdChild->parent->id, rootTreeNode->id ); + QVERIFY( thirdChild->children.empty() ); + QCOMPARE( thirdChild->name, qgisLayerTree->children().at( 2 )->name() ); // raster_layer + + // Check expected behavior from TreeNode methods + + // Check the PDF tree layer (note that parent root node is not included in the output) + QDomDocument doc; + QDomElement layerTreeElem = doc.createElement( u"LayerTree"_s ); + rootTreeNode->toChildrenElements( doc, layerTreeElem ); + QDomNodeList layerTreeList = layerTreeElem.childNodes(); + QCOMPARE( layerTreeList.count(), 3 ); + + QgsMapLayer *layer0 = qobject_cast< QgsLayerTreeLayer * >( qgisLayerTree->children().at( 0 ) )->layer(); + QgsLayerTreeGroup *group1 = qobject_cast< QgsLayerTreeGroup * >( qgisLayerTree->children().at( 1 ) ); + QgsMapLayer *layer2 = qobject_cast< QgsLayerTreeLayer * >( qgisLayerTree->children().at( 2 ) )->layer(); + + QCOMPARE( layerTreeList.at( 0 ).toElement().attribute( u"id"_s ), layer0->id() ); + QCOMPARE( layerTreeList.at( 0 ).toElement().attribute( u"name"_s ), layer0->name() ); + QCOMPARE( layerTreeList.at( 0 ).toElement().attribute( u"initiallyVisible"_s ), u"true"_s ); + + QCOMPARE( layerTreeList.at( 1 ).toElement().attribute( u"id"_s ), secondChild->id ); + QCOMPARE( layerTreeList.at( 1 ).toElement().attribute( u"name"_s ), group1->name() ); + QCOMPARE( layerTreeList.at( 1 ).toElement().attribute( u"initiallyVisible"_s ), u"true"_s ); + + QDomNodeList group1LayerTreeList = layerTreeList.at( 1 ).toElement().childNodes(); + QCOMPARE( group1LayerTreeList.count(), 3L ); + + QgsMapLayer *layer1_0 = qobject_cast< QgsLayerTreeLayer * >( qgisLayerTree->children().at( 1 )->children().at( 0 ) )->layer(); + QgsLayerTreeGroup *group1_1 = qobject_cast< QgsLayerTreeGroup * >( qgisLayerTree->children().at( 1 )->children().at( 1 ) ); + QgsMapLayer *layer1_2 = qobject_cast< QgsLayerTreeLayer * >( qgisLayerTree->children().at( 1 )->children().at( 2 ) )->layer(); + + QCOMPARE( group1LayerTreeList.at( 0 ).toElement().attribute( u"id"_s ), layer1_0->id() ); + QCOMPARE( group1LayerTreeList.at( 0 ).toElement().attribute( u"name"_s ), layer1_0->name() ); + QCOMPARE( group1LayerTreeList.at( 0 ).toElement().attribute( u"initiallyVisible"_s ), u"true"_s ); + + QCOMPARE( group1LayerTreeList.at( 1 ).toElement().attribute( u"id"_s ), secondSubChild->id ); + QCOMPARE( group1LayerTreeList.at( 1 ).toElement().attribute( u"name"_s ), group1_1->name() ); + QCOMPARE( group1LayerTreeList.at( 1 ).toElement().attribute( u"initiallyVisible"_s ), u"true"_s ); + + QgsMapLayer *layer1_1_0 = qobject_cast< QgsLayerTreeLayer * >( qgisLayerTree->children().at( 1 )->children().at( 1 )->children().at( 0 ) )->layer(); + + QDomNodeList group1_1LayerTreeList = group1LayerTreeList.at( 1 ).toElement().childNodes(); + QCOMPARE( group1_1LayerTreeList.count(), 1L ); + + QCOMPARE( group1_1LayerTreeList.at( 0 ).toElement().attribute( u"id"_s ), layer1_1_0->id() ); + QCOMPARE( group1_1LayerTreeList.at( 0 ).toElement().attribute( u"name"_s ), layer1_1_0->name() ); + QCOMPARE( group1_1LayerTreeList.at( 0 ).toElement().attribute( u"initiallyVisible"_s ), u"true"_s ); + + QCOMPARE( group1LayerTreeList.at( 2 ).toElement().attribute( u"id"_s ), layer1_2->id() ); + QCOMPARE( group1LayerTreeList.at( 2 ).toElement().attribute( u"name"_s ), layer1_2->name() ); + QCOMPARE( group1LayerTreeList.at( 2 ).toElement().attribute( u"initiallyVisible"_s ), u"true"_s ); + + QCOMPARE( layerTreeList.at( 2 ).toElement().attribute( u"id"_s ), layer2->id() ); + QCOMPARE( layerTreeList.at( 2 ).toElement().attribute( u"name"_s ), layer2->name() ); + QCOMPARE( layerTreeList.at( 2 ).toElement().attribute( u"initiallyVisible"_s ), u"true"_s ); + + // Check that createNestedIfLayerOnElements does not include the parent root node + QDomElement contentElem = doc.createElement( u"Content"_s ); + firstChild->createNestedIfLayerOnElements( doc, contentElem ); + QCOMPARE( contentElem.elementsByTagName( u"IfLayerOn"_s ).count(), 1L ); + QDomElement firstChildElem = contentElem.firstChild().toElement(); + QCOMPARE( firstChildElem.attribute( u"layerId"_s ), firstChild->mapLayerId ); // layer has no (root) parent + QCOMPARE( firstChildElem.elementsByTagName( u"IfLayerOn"_s ).count(), 0L ); + + QDomElement contentElem2 = doc.createElement( u"Content"_s ); + firstSubChild->createNestedIfLayerOnElements( doc, contentElem2 ); + QCOMPARE( contentElem2.childNodes().count(), 1L ); + QDomElement firstChildElem2 = contentElem2.childNodes().at( 0 ).toElement(); + QCOMPARE( firstChildElem2.attribute( u"layerId"_s ), secondChild->id ); // parent layer tree group id (i.e., no 'root' parent) + QCOMPARE( firstChildElem2.childNodes().count(), 1L ); + QDomElement firstSubChildElem = firstChildElem2.childNodes().at( 0 ).toElement(); + QCOMPARE( firstSubChildElem.attribute( u"layerId"_s ), firstSubChild->mapLayerId ); + QCOMPARE( firstSubChildElem.childNodes().count(), 0L ); +} + +void TestQgsGeospatialPdfExport::testCreatePdfTreeNodesWithGroupLayer() +{ + TestGeospatialPdfExporter geospatialPdfExporter; + QgsProject p; + QVERIFY( p.read( TEST_DATA_DIR + u"/geospatial_pdf_projects/test_non_nested_group_no_graphics_with_grouplayer.qgz"_s ) ); + + QMap< QString, TreeNode * > groupNameToTreeNode; + QMap< QString, TreeNode * > layerIdToTreeNode; + + QgsLayerTree *qgisLayerTree = p.layerTreeRoot(); + + std::unique_ptr< TreeNode > rootTreeNode = geospatialPdfExporter.createPdfTreeNodes( groupNameToTreeNode, layerIdToTreeNode, qgisLayerTree ); + rootTreeNode->isRootNode = true; + + // Check the rootTreeNode structure matches the one from the project's layer tree + + // First check the root tree node + QVERIFY( rootTreeNode->isRootNode ); + QVERIFY( !rootTreeNode->id.isEmpty() ); + QVERIFY( rootTreeNode->name.isEmpty() ); + QVERIFY( rootTreeNode->mapLayerId.isEmpty() ); + QVERIFY( !rootTreeNode->parent ); + + // Then check children + QCOMPARE( rootTreeNode->children.size(), 4L ); + TreeNode *firstChild = rootTreeNode->children.at( 0 ).get(); + QVERIFY( firstChild->parent ); + QCOMPARE( firstChild->parent->id, rootTreeNode->id ); + QVERIFY( firstChild->children.empty() ); + QCOMPARE( firstChild->name, qgisLayerTree->children().at( 0 )->name() ); // points + + TreeNode *secondChild = rootTreeNode->children.at( 1 ).get(); + QVERIFY( secondChild->parent ); + QCOMPARE( secondChild->parent->id, rootTreeNode->id ); + QCOMPARE( secondChild->children.size(), 0L ); + QCOMPARE( secondChild->name, qgisLayerTree->children().at( 1 )->name() ); // group1 + + TreeNode *thirdChild = rootTreeNode->children.at( 2 ).get(); + QVERIFY( thirdChild->parent ); + QCOMPARE( thirdChild->parent->id, rootTreeNode->id ); + QVERIFY( thirdChild->children.empty() ); + QCOMPARE( thirdChild->name, qgisLayerTree->children().at( 2 )->name() ); // polys + + TreeNode *fourthChild = rootTreeNode->children.at( 3 ).get(); + QVERIFY( fourthChild->parent ); + QCOMPARE( fourthChild->parent->id, rootTreeNode->id ); + QVERIFY( fourthChild->children.empty() ); + QCOMPARE( fourthChild->name, qgisLayerTree->children().at( 3 )->name() ); // raster_layer + + // Check expected behavior from TreeNode methods + + // Check the PDF tree layer (note that parent root node is not included in the output) + QDomDocument doc; + QDomElement layerTreeElem = doc.createElement( u"LayerTree"_s ); + rootTreeNode->toChildrenElements( doc, layerTreeElem ); + QDomNodeList layerTreeList = layerTreeElem.childNodes(); + QCOMPARE( layerTreeList.count(), 4 ); + + QgsMapLayer *layer0 = qobject_cast< QgsLayerTreeLayer * >( qgisLayerTree->children().at( 0 ) )->layer(); + QgsLayerTreeGroup *group1 = qobject_cast< QgsLayerTreeGroup * >( qgisLayerTree->children().at( 1 ) ); + QgsMapLayer *layer2 = qobject_cast< QgsLayerTreeLayer * >( qgisLayerTree->children().at( 2 ) )->layer(); + QgsMapLayer *layer3 = qobject_cast< QgsLayerTreeLayer * >( qgisLayerTree->children().at( 3 ) )->layer(); + + QCOMPARE( layerTreeList.at( 0 ).toElement().attribute( u"id"_s ), layer0->id() ); + QCOMPARE( layerTreeList.at( 0 ).toElement().attribute( u"name"_s ), layer0->name() ); + QCOMPARE( layerTreeList.at( 0 ).toElement().attribute( u"initiallyVisible"_s ), u"true"_s ); + + QCOMPARE( layerTreeList.at( 1 ).toElement().attribute( u"id"_s ), secondChild->id ); + QCOMPARE( layerTreeList.at( 1 ).toElement().attribute( u"name"_s ), group1->name() ); + QCOMPARE( layerTreeList.at( 1 ).toElement().attribute( u"initiallyVisible"_s ), u"true"_s ); + QCOMPARE( layerTreeList.at( 1 ).toElement().childNodes().count(), 0 ); + + QCOMPARE( layerTreeList.at( 2 ).toElement().attribute( u"id"_s ), thirdChild->id ); + QCOMPARE( layerTreeList.at( 2 ).toElement().attribute( u"name"_s ), layer2->name() ); + QCOMPARE( layerTreeList.at( 2 ).toElement().attribute( u"initiallyVisible"_s ), u"true"_s ); + + QCOMPARE( layerTreeList.at( 3 ).toElement().attribute( u"id"_s ), fourthChild->id ); + QCOMPARE( layerTreeList.at( 3 ).toElement().attribute( u"name"_s ), layer3->name() ); + QCOMPARE( layerTreeList.at( 3 ).toElement().attribute( u"initiallyVisible"_s ), u"true"_s ); + + // Check that createNestedIfLayerOnElements does not include the parent root node + QDomElement contentElem = doc.createElement( u"Content"_s ); + firstChild->createNestedIfLayerOnElements( doc, contentElem ); + QCOMPARE( contentElem.elementsByTagName( u"IfLayerOn"_s ).count(), 1L ); + QDomElement firstChildElem = contentElem.firstChild().toElement(); + QCOMPARE( firstChildElem.attribute( u"layerId"_s ), firstChild->mapLayerId ); // layer has no (root) parent + QCOMPARE( firstChildElem.elementsByTagName( u"IfLayerOn"_s ).count(), 0L ); + + // Check that createNestedIfLayerOnElements does not include the parent root node + QDomElement contentElem2 = doc.createElement( u"Content"_s ); + secondChild->createNestedIfLayerOnElements( doc, contentElem2 ); + QCOMPARE( contentElem2.elementsByTagName( u"IfLayerOn"_s ).count(), 1L ); + QDomElement secondChildElem = contentElem2.firstChild().toElement(); + QCOMPARE( secondChildElem.attribute( u"layerId"_s ), secondChild->mapLayerId ); // layer has no (root) parent + QCOMPARE( secondChildElem.elementsByTagName( u"IfLayerOn"_s ).count(), 0L ); +} + +void TestQgsGeospatialPdfExport::testUseQgisLayerTree() +{ + TestGeospatialPdfExporter geospatialPdfExporter; + + // Layer tree structure: + // + points + // + group1 + // + multipoint + // + sub-group1 + // + lines + // + polys + // + raster_layer + QgsProject p; + QVERIFY( p.read( TEST_DATA_DIR + u"/geospatial_pdf_projects/test_nested_groups_no_graphics.qgz"_s ) ); + QgsLayerTree *qgisLayerTree = p.layerTreeRoot(); + const QList< QgsMapLayer * > qgisLayers = qgisLayerTree->layerOrder(); + QgsMapLayer *layerPoints = qgisLayers.at( 0 ); + QgsMapLayer *layerMultipoint = qgisLayers.at( 1 ); + QgsMapLayer *layerLines = qgisLayers.at( 2 ); + QgsMapLayer *layerPolys = qgisLayers.at( 3 ); + QgsMapLayer *layerRaster = qgisLayers.at( 4 ); + + geospatialPdfExporter.setQGISLayerTree( qgisLayerTree ); + + QList renderedLayers; + + for ( const auto &qgisLayer : qgisLayers ) + { + QgsAbstractGeospatialPdfExporter::ComponentLayerDetail detail; + detail.mapLayerId = qgisLayer->id(); + detail.name = qgisLayer->name(); + detail.opacity = 0.7; + detail.compositionMode = QPainter::CompositionMode_Screen; + detail.sourcePdfPath = u"%1.pdf"_s.arg( qgisLayer->name() ); + + renderedLayers << detail; + } + + QgsAbstractGeospatialPdfExporter::ExportDetails details; + details.useQgisLayerTreeProperties = true; + details.includeFeatures = false; + + // Check the composition XML + QString composition = geospatialPdfExporter.createCompositionXml( renderedLayers, details ); + QgsDebugMsgLevel( composition, 1 ); + QDomDocument doc; + doc.setContent( composition ); + QVERIFY( true ); + + // Check the PDF layer tree + QDomNodeList layerTreeList = doc.elementsByTagName( u"LayerTree"_s ).at( 0 ).toElement().childNodes(); + QCOMPARE( layerTreeList.count(), 3 ); + QCOMPARE( layerTreeList.at( 0 ).toElement().attribute( u"name"_s ), layerPoints->name() ); + QCOMPARE( layerTreeList.at( 0 ).toElement().attribute( u"id"_s ), layerPoints->id() ); + QCOMPARE( layerTreeList.at( 1 ).toElement().attribute( u"name"_s ), u"group1"_s ); + QString group1Id = layerTreeList.at( 1 ).toElement().attribute( u"id"_s ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 0 ).toElement().attribute( u"name"_s ), u"multipoint"_s ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 0 ).toElement().attribute( u"id"_s ), layerMultipoint->id() ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 1 ).toElement().attribute( u"name"_s ), u"sub-group1"_s ); + QString subGroup1Id = layerTreeList.at( 1 ).childNodes().at( 1 ).toElement().attribute( u"id"_s ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 1 ).childNodes().at( 0 ).toElement().attribute( u"name"_s ), layerLines->name() ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 1 ).childNodes().at( 0 ).toElement().attribute( u"id"_s ), layerLines->id() ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 2 ).toElement().attribute( u"name"_s ), layerPolys->name() ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 2 ).toElement().attribute( u"id"_s ), layerPolys->id() ); + QCOMPARE( layerTreeList.at( 2 ).toElement().attribute( u"name"_s ), layerRaster->name() ); + QCOMPARE( layerTreeList.at( 2 ).toElement().attribute( u"id"_s ), layerRaster->id() ); + + // Check the content section + QDomNodeList contentList = doc.documentElement().firstChildElement( u"Page"_s ).firstChildElement( u"Content"_s ).childNodes(); + QCOMPARE( contentList.count(), 5 ); + + QCOMPARE( contentList.at( 0 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 0 ).toElement().attribute( u"layerId"_s ), layerPoints->id() ); + QCOMPARE( contentList.at( 0 ).toElement().elementsByTagName( u"PDF"_s ).count(), 1 ); + QCOMPARE( contentList.at( 0 ).toElement().elementsByTagName( u"PDF"_s ).at( 0 ).toElement().attribute( u"dataset"_s ), u"%1.pdf"_s.arg( layerPoints->name() ) ); + + QCOMPARE( contentList.at( 1 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 1 ).toElement().attribute( u"layerId"_s ), group1Id ); + QCOMPARE( contentList.at( 1 ).childNodes().at( 0 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 1 ).childNodes().at( 0 ).toElement().attribute( u"layerId"_s ), layerMultipoint->id() ); + QCOMPARE( contentList.at( 1 ).childNodes().at( 0 ).toElement().elementsByTagName( u"PDF"_s ).count(), 1 ); + QCOMPARE( contentList.at( 1 ).childNodes().at( 0 ).toElement().elementsByTagName( u"PDF"_s ).at( 0 ).toElement().attribute( u"dataset"_s ), u"%1.pdf"_s.arg( layerMultipoint->name() ) ); + + QCOMPARE( contentList.at( 2 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 2 ).toElement().attribute( u"layerId"_s ), group1Id ); + QCOMPARE( contentList.at( 2 ).toElement().childNodes().at( 0 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 2 ).toElement().childNodes().at( 0 ).toElement().attribute( u"layerId"_s ), subGroup1Id ); + QCOMPARE( contentList.at( 2 ).toElement().childNodes().at( 0 ).childNodes().at( 0 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 2 ).toElement().childNodes().at( 0 ).childNodes().at( 0 ).toElement().attribute( u"layerId"_s ), layerLines->id() ); + QCOMPARE( contentList.at( 2 ).toElement().childNodes().at( 0 ).childNodes().at( 0 ).toElement().elementsByTagName( u"PDF"_s ).count(), 1 ); + QCOMPARE( contentList.at( 2 ).toElement().childNodes().at( 0 ).childNodes().at( 0 ).toElement().elementsByTagName( u"PDF"_s ).at( 0 ).toElement().attribute( u"dataset"_s ), u"%1.pdf"_s.arg( layerLines->name() ) ); + + QCOMPARE( contentList.at( 3 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 3 ).toElement().attribute( u"layerId"_s ), group1Id ); + QCOMPARE( contentList.at( 3 ).childNodes().at( 0 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 3 ).childNodes().at( 0 ).toElement().attribute( u"layerId"_s ), layerPolys->id() ); + QCOMPARE( contentList.at( 3 ).childNodes().at( 0 ).toElement().elementsByTagName( u"PDF"_s ).count(), 1 ); + QCOMPARE( contentList.at( 3 ).childNodes().at( 0 ).toElement().elementsByTagName( u"PDF"_s ).at( 0 ).toElement().attribute( u"dataset"_s ), u"%1.pdf"_s.arg( layerPolys->name() ) ); + + QCOMPARE( contentList.at( 4 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 4 ).toElement().attribute( u"layerId"_s ), layerRaster->id() ); + QCOMPARE( contentList.at( 4 ).toElement().elementsByTagName( u"PDF"_s ).count(), 1 ); + QCOMPARE( contentList.at( 4 ).toElement().elementsByTagName( u"PDF"_s ).at( 0 ).toElement().attribute( u"dataset"_s ), u"%1.pdf"_s.arg( layerRaster->name() ) ); +} + +void TestQgsGeospatialPdfExport::testUseQgisLayerTreeInvisibleNodes() +{ + TestGeospatialPdfExporter geospatialPdfExporter; + + // Layer tree structure: + // + points [invisible] + // + group1 + // + multipoint + // + sub-group1 [invisible] + // + lines + // + polys [invisible] + // + raster_layer + QgsProject p; + QVERIFY( p.read( TEST_DATA_DIR + u"/geospatial_pdf_projects/test_nested_groups_no_graphics_invisible_layers.qgz"_s ) ); + QgsLayerTree *qgisLayerTree = p.layerTreeRoot(); + const QList< QgsMapLayer * > qgisLayers = qgisLayerTree->layerOrder(); + QgsMapLayer *layerPoints = qgisLayers.at( 0 ); + QgsMapLayer *layerMultipoint = qgisLayers.at( 1 ); + QgsMapLayer *layerLines = qgisLayers.at( 2 ); + QgsMapLayer *layerPolys = qgisLayers.at( 3 ); + QgsMapLayer *layerRaster = qgisLayers.at( 4 ); + + geospatialPdfExporter.setQGISLayerTree( qgisLayerTree ); + + QList renderedLayers; + + for ( const auto &qgisLayer : qgisLayers ) + { + QgsAbstractGeospatialPdfExporter::ComponentLayerDetail detail; + detail.mapLayerId = qgisLayer->id(); + detail.name = qgisLayer->name(); + detail.opacity = 0.7; + detail.compositionMode = QPainter::CompositionMode_Screen; + detail.sourcePdfPath = u"%1.pdf"_s.arg( qgisLayer->name() ); + + renderedLayers << detail; + } + + QgsAbstractGeospatialPdfExporter::ExportDetails details; + details.useQgisLayerTreeProperties = true; + details.includeFeatures = false; + + // Check the composition XML + QString composition = geospatialPdfExporter.createCompositionXml( renderedLayers, details ); + QgsDebugMsgLevel( composition, 1 ); + QDomDocument doc; + doc.setContent( composition ); + QVERIFY( true ); + + // Check the PDF layer tree + QDomNodeList layerTreeList = doc.elementsByTagName( u"LayerTree"_s ).at( 0 ).toElement().childNodes(); + QCOMPARE( layerTreeList.count(), 3 ); + QCOMPARE( layerTreeList.at( 0 ).toElement().attribute( u"name"_s ), layerPoints->name() ); + QCOMPARE( layerTreeList.at( 0 ).toElement().attribute( u"id"_s ), layerPoints->id() ); + QCOMPARE( layerTreeList.at( 0 ).toElement().attribute( u"initiallyVisible"_s ), "false" ); + QCOMPARE( layerTreeList.at( 1 ).toElement().attribute( u"name"_s ), u"group1"_s ); + QCOMPARE( layerTreeList.at( 1 ).toElement().attribute( u"initiallyVisible"_s ), "true" ); + QString group1Id = layerTreeList.at( 1 ).toElement().attribute( u"id"_s ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 0 ).toElement().attribute( u"name"_s ), u"multipoint"_s ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 0 ).toElement().attribute( u"id"_s ), layerMultipoint->id() ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 0 ).toElement().attribute( u"initiallyVisible"_s ), "true" ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 1 ).toElement().attribute( u"name"_s ), u"sub-group1"_s ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 1 ).toElement().attribute( u"initiallyVisible"_s ), "false" ); + QString subGroup1Id = layerTreeList.at( 1 ).childNodes().at( 1 ).toElement().attribute( u"id"_s ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 1 ).childNodes().at( 0 ).toElement().attribute( u"name"_s ), layerLines->name() ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 1 ).childNodes().at( 0 ).toElement().attribute( u"id"_s ), layerLines->id() ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 1 ).childNodes().at( 0 ).toElement().attribute( u"initiallyVisible"_s ), "true" ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 2 ).toElement().attribute( u"name"_s ), layerPolys->name() ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 2 ).toElement().attribute( u"id"_s ), layerPolys->id() ); + QCOMPARE( layerTreeList.at( 1 ).childNodes().at( 2 ).toElement().attribute( u"initiallyVisible"_s ), "false" ); + QCOMPARE( layerTreeList.at( 2 ).toElement().attribute( u"name"_s ), layerRaster->name() ); + QCOMPARE( layerTreeList.at( 2 ).toElement().attribute( u"id"_s ), layerRaster->id() ); + QCOMPARE( layerTreeList.at( 2 ).toElement().attribute( u"initiallyVisible"_s ), "true" ); + + // Check the content section + QDomNodeList contentList = doc.documentElement().firstChildElement( u"Page"_s ).firstChildElement( u"Content"_s ).childNodes(); + QCOMPARE( contentList.count(), 5 ); + + QCOMPARE( contentList.at( 0 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 0 ).toElement().attribute( u"layerId"_s ), layerPoints->id() ); + QCOMPARE( contentList.at( 0 ).toElement().elementsByTagName( u"PDF"_s ).count(), 1 ); + QCOMPARE( contentList.at( 0 ).toElement().elementsByTagName( u"PDF"_s ).at( 0 ).toElement().attribute( u"dataset"_s ), u"%1.pdf"_s.arg( layerPoints->name() ) ); + + QCOMPARE( contentList.at( 1 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 1 ).toElement().attribute( u"layerId"_s ), group1Id ); + QCOMPARE( contentList.at( 1 ).childNodes().at( 0 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 1 ).childNodes().at( 0 ).toElement().attribute( u"layerId"_s ), layerMultipoint->id() ); + QCOMPARE( contentList.at( 1 ).childNodes().at( 0 ).toElement().elementsByTagName( u"PDF"_s ).count(), 1 ); + QCOMPARE( contentList.at( 1 ).childNodes().at( 0 ).toElement().elementsByTagName( u"PDF"_s ).at( 0 ).toElement().attribute( u"dataset"_s ), u"%1.pdf"_s.arg( layerMultipoint->name() ) ); + + QCOMPARE( contentList.at( 2 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 2 ).toElement().attribute( u"layerId"_s ), group1Id ); + QCOMPARE( contentList.at( 2 ).toElement().childNodes().at( 0 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 2 ).toElement().childNodes().at( 0 ).toElement().attribute( u"layerId"_s ), subGroup1Id ); + QCOMPARE( contentList.at( 2 ).toElement().childNodes().at( 0 ).childNodes().at( 0 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 2 ).toElement().childNodes().at( 0 ).childNodes().at( 0 ).toElement().attribute( u"layerId"_s ), layerLines->id() ); + QCOMPARE( contentList.at( 2 ).toElement().childNodes().at( 0 ).childNodes().at( 0 ).toElement().elementsByTagName( u"PDF"_s ).count(), 1 ); + QCOMPARE( contentList.at( 2 ).toElement().childNodes().at( 0 ).childNodes().at( 0 ).toElement().elementsByTagName( u"PDF"_s ).at( 0 ).toElement().attribute( u"dataset"_s ), u"%1.pdf"_s.arg( layerLines->name() ) ); + + QCOMPARE( contentList.at( 3 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 3 ).toElement().attribute( u"layerId"_s ), group1Id ); + QCOMPARE( contentList.at( 3 ).childNodes().at( 0 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 3 ).childNodes().at( 0 ).toElement().attribute( u"layerId"_s ), layerPolys->id() ); + QCOMPARE( contentList.at( 3 ).childNodes().at( 0 ).toElement().elementsByTagName( u"PDF"_s ).count(), 1 ); + QCOMPARE( contentList.at( 3 ).childNodes().at( 0 ).toElement().elementsByTagName( u"PDF"_s ).at( 0 ).toElement().attribute( u"dataset"_s ), u"%1.pdf"_s.arg( layerPolys->name() ) ); + + QCOMPARE( contentList.at( 4 ).toElement().tagName(), u"IfLayerOn"_s ); + QCOMPARE( contentList.at( 4 ).toElement().attribute( u"layerId"_s ), layerRaster->id() ); + QCOMPARE( contentList.at( 4 ).toElement().elementsByTagName( u"PDF"_s ).count(), 1 ); + QCOMPARE( contentList.at( 4 ).toElement().elementsByTagName( u"PDF"_s ).at( 0 ).toElement().attribute( u"dataset"_s ), u"%1.pdf"_s.arg( layerRaster->name() ) ); +} QGSTEST_MAIN( TestQgsGeospatialPdfExport ) #include "testqgsgeopdfexport.moc" diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py index ecba683ef2d5..912ad1569d8f 100644 --- a/tests/src/python/test_qgslayoutexporter.py +++ b/tests/src/python/test_qgslayoutexporter.py @@ -19,7 +19,7 @@ from typing import Optional -from osgeo import gdal +from osgeo import gdal, ogr from qgis.PyQt.QtCore import ( QDate, QDateTime, @@ -1940,5 +1940,223 @@ def testHitTestLegendUpdate(self): self.assertEqual(legend_node.maximum(), 165) + def testGeospatialPdfQgisLayerTree(self): + def exportLayout(project_name, pdf_name, settings): + p = QgsProject() + p.read(os.path.join(TEST_DATA_DIR, "geospatial_pdf_projects", project_name)) + l = p.layoutManager().layoutByName("layout") + exporter = QgsLayoutExporter(l) + + pdf_file_path = os.path.join(self.basetestpath, pdf_name) + self.assertEqual( + exporter.exportToPdf(pdf_file_path, settings), + QgsLayoutExporter.ExportResult.Success, + ) + self.assertTrue(os.path.exists(pdf_file_path)) + + ds = gdal.Open(pdf_file_path) + op = gdal.InfoOptions(options="-mdd LAYERS -json") + i = gdal.Info(ds, options=op) + return ds, i + + def check_no_feature_info(ds): + defn = ds.GetLayerByName("points").GetLayerDefn() + self.assertEqual(defn.GetFieldCount(), 0) + + def check_feature_info(ds): + defn = ds.GetLayerByName("points").GetLayerDefn() + self.assertEqual( + ogr.GeometryTypeToName(ds.GetLayerByName("points").GetGeomType()), + "Point", + ) + self.assertEqual(defn.GetFieldCount(), 6) + self.assertEqual(ds.GetLayerByName("points").GetFeatureCount(), 17) + self.assertEqual( + ds.GetLayerByName("points").GetFeature(0).GetField("Class"), "Jet" + ) + + # Check export with no features + settings = QgsLayoutExporter.PdfExportSettings() + settings.writeGeoPdf = True + settings.includeGeoPdfFeatures = False + settings.useQgisLayerTreeProperties = True + + ds, i = exportLayout( + "test_nested_groups_no_graphics.qgz", + "test_geospatial_pdf_qgis_layer_tree_no_features.pdf", + settings, + ) + + expected_metadata_layers = { + "LAYER_00_NAME": "points", + "LAYER_01_NAME": "group1", + "LAYER_02_NAME": "group1.multipoint", + "LAYER_03_NAME": "group1.sub-group1", + "LAYER_04_NAME": "group1.sub-group1.lines", + "LAYER_05_NAME": "group1.polys", + "LAYER_06_NAME": "raster_layer", + } + self.assertEqual(i["metadata"]["LAYERS"], expected_metadata_layers) + check_no_feature_info(ds) + + # Check export with features + settings = QgsLayoutExporter.PdfExportSettings() + settings.writeGeoPdf = True + settings.includeGeoPdfFeatures = True + settings.useQgisLayerTreeProperties = True + + ds, i = exportLayout( + "test_nested_groups_no_graphics.qgz", + "test_geospatial_pdf_qgis_layer_tree.pdf", + settings, + ) + self.assertEqual(i["metadata"]["LAYERS"], expected_metadata_layers) + check_feature_info(ds) + + # Check export with no features (grouped graphics) + settings = QgsLayoutExporter.PdfExportSettings() + settings.writeGeoPdf = True + settings.includeGeoPdfFeatures = False + settings.useQgisLayerTreeProperties = True + + ds, i = exportLayout( + "test_nested_groups_with_grouped_graphics.qgz", + "test_geospatial_pdf_qgis_layer_tree_no_features_grouped_graphics.pdf", + settings, + ) + + expected_metadata_layers = { + "LAYER_00_NAME": "points", + "LAYER_01_NAME": "group1", + "LAYER_02_NAME": "group1.multipoint", + "LAYER_03_NAME": "group1.sub-group1", + "LAYER_04_NAME": "group1.sub-group1.lines", + "LAYER_05_NAME": "group1.polys", + "LAYER_06_NAME": "raster_layer", + "LAYER_07_NAME": "Map_info", + } + self.assertEqual(i["metadata"]["LAYERS"], expected_metadata_layers) + check_no_feature_info(ds) + + # Check export with features (grouped graphics) + settings = QgsLayoutExporter.PdfExportSettings() + settings.writeGeoPdf = True + settings.includeGeoPdfFeatures = True + settings.useQgisLayerTreeProperties = True + + ds, i = exportLayout( + "test_nested_groups_with_grouped_graphics.qgz", + "test_geospatial_pdf_qgis_layer_tree_grouped_graphics.pdf", + settings, + ) + self.assertEqual(i["metadata"]["LAYERS"], expected_metadata_layers) + check_feature_info(ds) + + # Check export with no features (invisible nodes) + settings = QgsLayoutExporter.PdfExportSettings() + settings.writeGeoPdf = True + settings.includeGeoPdfFeatures = False + settings.useQgisLayerTreeProperties = True + + ds, i = exportLayout( + "test_nested_groups_no_graphics_invisible_layers.qgz", + "test_geospatial_pdf_qgis_layer_tree_no_features_invisible_layers.pdf", + settings, + ) + + expected_metadata_layers = { + "LAYER_00_NAME": "points", + "LAYER_01_NAME": "group1", + "LAYER_02_NAME": "group1.multipoint", + "LAYER_03_NAME": "group1.sub-group1", + "LAYER_04_NAME": "group1.sub-group1.lines", + "LAYER_05_NAME": "group1.polys", + "LAYER_06_NAME": "raster_layer", + } + self.assertEqual(i["metadata"]["LAYERS"], expected_metadata_layers) + check_no_feature_info(ds) + + # Check export with features (invisible nodes) + settings = QgsLayoutExporter.PdfExportSettings() + settings.writeGeoPdf = True + settings.includeGeoPdfFeatures = True + settings.useQgisLayerTreeProperties = True + + ds, i = exportLayout( + "test_nested_groups_no_graphics_invisible_layers.qgz", + "test_geospatial_pdf_qgis_layer_tree_invisible_layers.pdf", + settings, + ) + self.assertEqual(i["metadata"]["LAYERS"], expected_metadata_layers) + check_feature_info(ds) + + # Check export with no features (with grouplayer) + settings = QgsLayoutExporter.PdfExportSettings() + settings.writeGeoPdf = True + settings.includeGeoPdfFeatures = False + settings.useQgisLayerTreeProperties = True + + ds, i = exportLayout( + "test_non_nested_group_no_graphics_with_grouplayer.qgz", + "test_geospatial_pdf_qgis_layer_tree_no_features_group_layer.pdf", + settings, + ) + + expected_metadata_layers = { + "LAYER_00_NAME": "points", + "LAYER_01_NAME": "group1", + "LAYER_02_NAME": "polys", + "LAYER_03_NAME": "raster_layer", + } + self.assertEqual(i["metadata"]["LAYERS"], expected_metadata_layers) + check_no_feature_info(ds) + + # Check export with features (with grouplayer) + settings = QgsLayoutExporter.PdfExportSettings() + settings.writeGeoPdf = True + settings.includeGeoPdfFeatures = True + settings.useQgisLayerTreeProperties = True + + ds, i = exportLayout( + "test_non_nested_group_no_graphics_with_grouplayer.qgz", + "test_geospatial_pdf_qgis_layer_tree_group_layer.pdf", + settings, + ) + self.assertEqual(i["metadata"]["LAYERS"], expected_metadata_layers) + check_feature_info(ds) + + def testErrorGeospatialPdfQgisLayerTreeAndThemes(self): + p = QgsProject() + p.read( + os.path.join( + TEST_DATA_DIR, + "geospatial_pdf_projects", + "test_nested_groups_no_graphics.qgz", + ) + ) + l = p.layoutManager().layoutByName("layout") + + # Make the map item follow a locked set, which is + # incompatible with the Follo QGIS layer tree option + map = l.referenceMap() + map.setKeepLayerSet(True) + + exporter = QgsLayoutExporter(l) + settings = QgsLayoutExporter.PdfExportSettings() + settings.writeGeoPdf = True + settings.useQgisLayerTreeProperties = True + + # Check that we get an error and an error message + pdf_file_path = os.path.join( + self.basetestpath, + "test_error_export_geospatial_pdf_qgis_layer_tree_and_themes.pdf", + ) + self.assertEqual( + exporter.exportToPdf(pdf_file_path, settings), + QgsLayoutExporter.ExportResult.PrintError, + ) + self.assertTrue(len(exporter.errorMessage()) > 0) + + if __name__ == "__main__": unittest.main() diff --git a/tests/testdata/geospatial_pdf_projects/test_nested_groups_no_graphics.qgz b/tests/testdata/geospatial_pdf_projects/test_nested_groups_no_graphics.qgz new file mode 100644 index 000000000000..891e9f202a76 Binary files /dev/null and b/tests/testdata/geospatial_pdf_projects/test_nested_groups_no_graphics.qgz differ diff --git a/tests/testdata/geospatial_pdf_projects/test_nested_groups_no_graphics_invisible_layers.qgz b/tests/testdata/geospatial_pdf_projects/test_nested_groups_no_graphics_invisible_layers.qgz new file mode 100644 index 000000000000..9535371e51e9 Binary files /dev/null and b/tests/testdata/geospatial_pdf_projects/test_nested_groups_no_graphics_invisible_layers.qgz differ diff --git a/tests/testdata/geospatial_pdf_projects/test_nested_groups_with_grouped_graphics.qgz b/tests/testdata/geospatial_pdf_projects/test_nested_groups_with_grouped_graphics.qgz new file mode 100644 index 000000000000..9632ba08a919 Binary files /dev/null and b/tests/testdata/geospatial_pdf_projects/test_nested_groups_with_grouped_graphics.qgz differ diff --git a/tests/testdata/geospatial_pdf_projects/test_non_nested_group_no_graphics_with_grouplayer.qgz b/tests/testdata/geospatial_pdf_projects/test_non_nested_group_no_graphics_with_grouplayer.qgz new file mode 100644 index 000000000000..87496c52e55e Binary files /dev/null and b/tests/testdata/geospatial_pdf_projects/test_non_nested_group_no_graphics_with_grouplayer.qgz differ