diff --git a/python/PyQt6/core/auto_additions/qgslayoutexporter.py b/python/PyQt6/core/auto_additions/qgslayoutexporter.py index 68b79e5c9c80..852828607245 100644 --- a/python/PyQt6/core/auto_additions/qgslayoutexporter.py +++ b/python/PyQt6/core/auto_additions/qgslayoutexporter.py @@ -13,7 +13,7 @@ except NameError: pass try: - QgsLayoutExporter.ImageExportSettings.__attribute_docs__ = {'dpi': 'Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.', 'imageSize': "Manual size in pixels for output image. If imageSize is not\nset then it will be automatically calculated based on the\noutput dpi and layout size.\n\nIf cropToContents is ``True`` then imageSize has no effect.\n\nBe careful when specifying manual sizes if pages in the layout\nhave differing sizes! It's likely not going to give a reasonable\noutput in this case, and the automatic dpi-based image size should be\nused instead.", 'cropToContents': 'Set to ``True`` if image should be cropped so only parts of the layout\ncontaining items are exported.', 'cropMargins': 'Crop to content margins, in pixels. These margins will be added\nto the bounds of the exported layout if cropToContents is ``True``.', 'pages': 'List of specific pages to export, or an empty list to\nexport all pages.\n\nPage numbers are 0 index based, so the first page in the\nlayout corresponds to page 0.', 'generateWorldFile': 'Set to ``True`` to generate an external world file alongside\nexported images.', 'exportMetadata': "Indicates whether image 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.', '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.ImageExportSettings.__attribute_docs__ = {'dpi': 'Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.', 'imageSize': "Manual size in pixels for output image. If imageSize is not\nset then it will be automatically calculated based on the\noutput dpi and layout size.\n\nIf cropToContents is ``True`` then imageSize has no effect.\n\nBe careful when specifying manual sizes if pages in the layout\nhave differing sizes! It's likely not going to give a reasonable\noutput in this case, and the automatic dpi-based image size should be\nused instead.", 'cropToContents': 'Set to ``True`` if image should be cropped so only parts of the layout\ncontaining items are exported.', 'cropMargins': 'Crop to content margins, in pixels. These margins will be added\nto the bounds of the exported layout if cropToContents is ``True``.', 'pages': 'List of specific pages to export, or an empty list to\nexport all pages.\n\nPage numbers are 0 index based, so the first page in the\nlayout corresponds to page 0.', 'generateWorldFile': 'Set to ``True`` to generate an external world file alongside\nexported images.', 'exportMetadata': "Indicates whether image 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.', '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', 'quality': 'Image quality, typically used for JPEG compression (whose quality ranges from 1 to 100)\nif quality is set to -1, the default quality will be used.\n\n.. versionadded:: 3.42'} QgsLayoutExporter.ImageExportSettings.__doc__ = """Contains settings relating to exporting layouts to raster images""" QgsLayoutExporter.ImageExportSettings.__group__ = ['layout'] except NameError: diff --git a/python/PyQt6/core/auto_generated/layout/qgslayoutexporter.sip.in b/python/PyQt6/core/auto_generated/layout/qgslayoutexporter.sip.in index 754e836273c1..62ec0cdff30d 100644 --- a/python/PyQt6/core/auto_generated/layout/qgslayoutexporter.sip.in +++ b/python/PyQt6/core/auto_generated/layout/qgslayoutexporter.sip.in @@ -24,6 +24,7 @@ Handles rendering and exports of layouts to various formats. + struct PageExportDetails { QString directory; @@ -142,6 +143,9 @@ Returns the rendered image, or a null QImage if the image does not fit into avai QVector predefinedMapScales; + + int quality; + }; ExportResult exportToImage( const QString &filePath, const QgsLayoutExporter::ImageExportSettings &settings ); diff --git a/python/core/auto_additions/qgslayoutexporter.py b/python/core/auto_additions/qgslayoutexporter.py index e6187b50435f..4644872770eb 100644 --- a/python/core/auto_additions/qgslayoutexporter.py +++ b/python/core/auto_additions/qgslayoutexporter.py @@ -6,7 +6,7 @@ except NameError: pass try: - QgsLayoutExporter.ImageExportSettings.__attribute_docs__ = {'dpi': 'Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.', 'imageSize': "Manual size in pixels for output image. If imageSize is not\nset then it will be automatically calculated based on the\noutput dpi and layout size.\n\nIf cropToContents is ``True`` then imageSize has no effect.\n\nBe careful when specifying manual sizes if pages in the layout\nhave differing sizes! It's likely not going to give a reasonable\noutput in this case, and the automatic dpi-based image size should be\nused instead.", 'cropToContents': 'Set to ``True`` if image should be cropped so only parts of the layout\ncontaining items are exported.', 'cropMargins': 'Crop to content margins, in pixels. These margins will be added\nto the bounds of the exported layout if cropToContents is ``True``.', 'pages': 'List of specific pages to export, or an empty list to\nexport all pages.\n\nPage numbers are 0 index based, so the first page in the\nlayout corresponds to page 0.', 'generateWorldFile': 'Set to ``True`` to generate an external world file alongside\nexported images.', 'exportMetadata': "Indicates whether image 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.', '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.ImageExportSettings.__attribute_docs__ = {'dpi': 'Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.', 'imageSize': "Manual size in pixels for output image. If imageSize is not\nset then it will be automatically calculated based on the\noutput dpi and layout size.\n\nIf cropToContents is ``True`` then imageSize has no effect.\n\nBe careful when specifying manual sizes if pages in the layout\nhave differing sizes! It's likely not going to give a reasonable\noutput in this case, and the automatic dpi-based image size should be\nused instead.", 'cropToContents': 'Set to ``True`` if image should be cropped so only parts of the layout\ncontaining items are exported.', 'cropMargins': 'Crop to content margins, in pixels. These margins will be added\nto the bounds of the exported layout if cropToContents is ``True``.', 'pages': 'List of specific pages to export, or an empty list to\nexport all pages.\n\nPage numbers are 0 index based, so the first page in the\nlayout corresponds to page 0.', 'generateWorldFile': 'Set to ``True`` to generate an external world file alongside\nexported images.', 'exportMetadata': "Indicates whether image 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.', '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', 'quality': 'Image quality, typically used for JPEG compression (whose quality ranges from 1 to 100)\nif quality is set to -1, the default quality will be used.\n\n.. versionadded:: 3.42'} QgsLayoutExporter.ImageExportSettings.__doc__ = """Contains settings relating to exporting layouts to raster images""" QgsLayoutExporter.ImageExportSettings.__group__ = ['layout'] except NameError: diff --git a/python/core/auto_generated/layout/qgslayoutexporter.sip.in b/python/core/auto_generated/layout/qgslayoutexporter.sip.in index 71e3605c52b1..f359aef2279f 100644 --- a/python/core/auto_generated/layout/qgslayoutexporter.sip.in +++ b/python/core/auto_generated/layout/qgslayoutexporter.sip.in @@ -24,6 +24,7 @@ Handles rendering and exports of layouts to various formats. + struct PageExportDetails { QString directory; @@ -142,6 +143,9 @@ Returns the rendered image, or a null QImage if the image does not fit into avai QVector predefinedMapScales; + + int quality; + }; ExportResult exportToImage( const QString &filePath, const QgsLayoutExporter::ImageExportSettings &settings ); diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 7926ddbf621b..f1b38152d13a 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -2296,7 +2296,7 @@ void QgsLayoutDesignerDialog::exportToRaster() QgsLayoutExporter::ImageExportSettings settings; QSize imageSize; - if ( !getRasterExportSettings( settings, imageSize ) ) + if ( !getRasterExportSettings( settings, imageSize, fileNExt.second ) ) return; mView->setPaintingEnabled( false ); @@ -2999,7 +2999,7 @@ void QgsLayoutDesignerDialog::exportAtlasToRaster() QgsLayoutExporter::ImageExportSettings settings; QSize imageSize; - if ( !getRasterExportSettings( settings, imageSize ) ) + if ( !getRasterExportSettings( settings, imageSize, format ) ) return; mView->setPaintingEnabled( false ); @@ -3532,7 +3532,7 @@ void QgsLayoutDesignerDialog::exportReportToRaster() QgsLayoutExporter::ImageExportSettings settings; QSize imageSize; - if ( !getRasterExportSettings( settings, imageSize ) ) + if ( !getRasterExportSettings( settings, imageSize, fileNExt.second ) ) return; mView->setPaintingEnabled( false ); @@ -4336,7 +4336,7 @@ bool QgsLayoutDesignerDialog::showFileSizeWarning() return true; } -bool QgsLayoutDesignerDialog::getRasterExportSettings( QgsLayoutExporter::ImageExportSettings &settings, QSize &imageSize ) +bool QgsLayoutDesignerDialog::getRasterExportSettings( QgsLayoutExporter::ImageExportSettings &settings, QSize &imageSize, const QString &fileExtension ) { QSizeF maxPageSize; bool hasUniformPageSizes = false; @@ -4366,7 +4366,7 @@ bool QgsLayoutDesignerDialog::getRasterExportSettings( QgsLayoutExporter::ImageE antialias = mLayout->customProperty( QStringLiteral( "imageAntialias" ), true ).toBool(); } - QgsLayoutImageExportOptionsDialog imageDlg( this ); + QgsLayoutImageExportOptionsDialog imageDlg( this, fileExtension ); imageDlg.setImageSize( maxPageSize ); imageDlg.setResolution( dpi ); imageDlg.setCropToContents( cropToContents ); @@ -4375,6 +4375,7 @@ bool QgsLayoutDesignerDialog::getRasterExportSettings( QgsLayoutExporter::ImageE imageDlg.setGenerateWorldFile( mLayout->customProperty( QStringLiteral( "exportWorldFile" ), false ).toBool() ); imageDlg.setAntialiasing( antialias ); imageDlg.setOpenAfterExporting( QgsLayoutExporter::settingOpenAfterExportingImage->value() ); + imageDlg.setQuality( QgsLayoutExporter::settingImageQuality->value() ); if ( !imageDlg.exec() ) return false; @@ -4409,6 +4410,12 @@ bool QgsLayoutDesignerDialog::getRasterExportSettings( QgsLayoutExporter::ImageE else settings.flags &= ~QgsLayoutRenderContext::FlagAntialiasing; + settings.quality = imageDlg.quality(); + if ( settings.quality != -1 ) + { + QgsLayoutExporter::settingImageQuality->setValue( imageDlg.quality() ); + } + return true; } diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index 9868e73f37f9..7c6ee723c445 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -564,7 +564,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, public Ui::QgsLayoutDesignerB void showForceVectorWarning(); bool showFileSizeWarning(); - bool getRasterExportSettings( QgsLayoutExporter::ImageExportSettings &settings, QSize &imageSize ); + bool getRasterExportSettings( QgsLayoutExporter::ImageExportSettings &settings, QSize &imageSize, const QString &fileExtension ); bool getSvgExportSettings( QgsLayoutExporter::SvgExportSettings &settings ); bool getPdfExportSettings( QgsLayoutExporter::PdfExportSettings &settings, bool allowGeospatialPdfExport = true, const QString &geospatialPdfReason = QString() ); diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 6ae555dbb0a9..f364465510ac 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -155,6 +155,7 @@ class LayoutItemHider const QgsSettingsEntryBool *QgsLayoutExporter::settingOpenAfterExportingImage = new QgsSettingsEntryBool( QStringLiteral( "open-after-exporting-image" ), QgsSettingsTree::sTreeLayout, false, QObject::tr( "Whether to open the exported image file with the default viewer after exporting a print layout" ) ); const QgsSettingsEntryBool *QgsLayoutExporter::settingOpenAfterExportingPdf = new QgsSettingsEntryBool( QStringLiteral( "open-after-exporting-pdf" ), QgsSettingsTree::sTreeLayout, false, QObject::tr( "Whether to open the exported PDF file with the default viewer after exporting a print layout" ) ); const QgsSettingsEntryBool *QgsLayoutExporter::settingOpenAfterExportingSvg = new QgsSettingsEntryBool( QStringLiteral( "open-after-exporting-svg" ), QgsSettingsTree::sTreeLayout, false, QObject::tr( "Whether to open the exported SVG file with the default viewer after exporting a print layout" ) ); +const QgsSettingsEntryInteger *QgsLayoutExporter::settingImageQuality = new QgsSettingsEntryInteger( QStringLiteral( "image-quality" ), QgsSettingsTree::sTreeLayout, 90, QObject::tr( "Image quality for lossy formats (e.g. JPEG)" ) ); QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout ) : mLayout( layout ) @@ -456,7 +457,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString return MemoryError; } - if ( !saveImage( image, outputFilePath, pageDetails.extension, settings.exportMetadata ? mLayout->project() : nullptr ) ) + if ( !saveImage( image, outputFilePath, pageDetails.extension, settings.exportMetadata ? mLayout->project() : nullptr, settings.quality ) ) { mErrorFileName = outputFilePath; return FileError; @@ -2216,13 +2217,17 @@ void QgsLayoutExporter::captureLabelingResults() } } -bool QgsLayoutExporter::saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat, QgsProject *projectForMetadata ) +bool QgsLayoutExporter::saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat, QgsProject *projectForMetadata, int quality ) { QImageWriter w( imageFilename, imageFormat.toLocal8Bit().constData() ); if ( imageFormat.compare( QLatin1String( "tiff" ), Qt::CaseInsensitive ) == 0 || imageFormat.compare( QLatin1String( "tif" ), Qt::CaseInsensitive ) == 0 ) { w.setCompression( 1 ); //use LZW compression } + + // Set the quality for i.e. JPEG images. -1 means default quality. + w.setQuality( quality ); + if ( projectForMetadata ) { w.setText( QStringLiteral( "Author" ), projectForMetadata->metadata().author() ); diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index e15f6b6692c1..2557c990ba43 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -41,6 +41,7 @@ class QgsAbstractLayoutIterator; class QgsFeedback; class QgsLabelingResults; class QgsSettingsEntryBool; +class QgsSettingsEntryInteger; /** * \ingroup core @@ -61,6 +62,9 @@ class CORE_EXPORT QgsLayoutExporter //! Settings entry - Whether to automatically open svgs after exporting them \since QGIS 3.34 static const QgsSettingsEntryBool *settingOpenAfterExportingSvg SIP_SKIP; + //! Settings entry - Image quality for lossy formats \since QGIS 3.42 + static const QgsSettingsEntryInteger *settingImageQuality SIP_SKIP; + //! Contains details of a page being exported by the class struct PageExportDetails { @@ -231,6 +235,14 @@ class CORE_EXPORT QgsLayoutExporter */ QVector predefinedMapScales; + + /** + * Image quality, typically used for JPEG compression (whose quality ranges from 1 to 100) + * if quality is set to -1, the default quality will be used. + * \since QGIS 3.42 + */ + int quality = -1; + }; /** @@ -719,7 +731,7 @@ class CORE_EXPORT QgsLayoutExporter /** * Saves an image to a file, possibly using format specific options (e.g. LZW compression for tiff) */ - static bool saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat, QgsProject *projectForMetadata ); + static bool saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat, QgsProject *projectForMetadata, int quality = -1 ); /** * Computes a GDAL style geotransform for georeferencing a layout. diff --git a/src/gui/layout/qgslayoutimageexportoptionsdialog.cpp b/src/gui/layout/qgslayoutimageexportoptionsdialog.cpp index d3ea75555722..6b5e0251aa6a 100644 --- a/src/gui/layout/qgslayoutimageexportoptionsdialog.cpp +++ b/src/gui/layout/qgslayoutimageexportoptionsdialog.cpp @@ -23,9 +23,12 @@ #include #include +#include +#include -QgsLayoutImageExportOptionsDialog::QgsLayoutImageExportOptionsDialog( QWidget *parent, Qt::WindowFlags flags ) +QgsLayoutImageExportOptionsDialog::QgsLayoutImageExportOptionsDialog( QWidget *parent, const QString &fileExtension, Qt::WindowFlags flags ) : QDialog( parent, flags ) + , mFileExtension( fileExtension ) { setupUi( this ); connect( mWidthSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsLayoutImageExportOptionsDialog::mWidthSpinBox_valueChanged ); @@ -34,6 +37,16 @@ QgsLayoutImageExportOptionsDialog::QgsLayoutImageExportOptionsDialog( QWidget *p connect( mClipToContentGroupBox, &QGroupBox::toggled, this, &QgsLayoutImageExportOptionsDialog::clipToContentsToggled ); connect( mHelpButtonBox, &QDialogButtonBox::helpRequested, this, &QgsLayoutImageExportOptionsDialog::showHelp ); + + const bool showQuality = shouldShowQuality(); + mQualitySpinBox->setVisible( showQuality ); + mQualitySlider->setVisible( showQuality ); + mQualityLabel->setVisible( showQuality ); + mQualityLabel->setText( tr( "%1 quality", "Image format" ).arg( mFileExtension.toUpper() ) ); + + connect( mQualitySpinBox, qOverload< int >( &QSpinBox::valueChanged ), mQualitySlider, &QSlider::setValue ); + connect( mQualitySlider, &QSlider::valueChanged, mQualitySpinBox, &QSpinBox::setValue ); + QgsGui::enableAutoGeometryRestore( this ); } @@ -142,6 +155,20 @@ void QgsLayoutImageExportOptionsDialog::setOpenAfterExporting( bool enabled ) mOpenAfterExportingCheckBox->setChecked( enabled ); } +int QgsLayoutImageExportOptionsDialog::quality() const +{ + if ( !shouldShowQuality() ) + { + return -1; + } + return mQualitySpinBox->value(); +} + +void QgsLayoutImageExportOptionsDialog::setQuality( int quality ) +{ + mQualitySpinBox->setValue( quality ); +} + void QgsLayoutImageExportOptionsDialog::mWidthSpinBox_valueChanged( int value ) { mHeightSpinBox->blockSignals( true ); @@ -201,3 +228,16 @@ void QgsLayoutImageExportOptionsDialog::showHelp() { QgsHelp::openHelp( QStringLiteral( "print_composer/create_output.html" ) ); } + +bool QgsLayoutImageExportOptionsDialog::shouldShowQuality() const +{ + const QStringList validExtensions = { "jpeg", "jpg" }; + for ( const QString &ext : validExtensions ) + { + if ( mFileExtension.toLower() == ext ) + { + return true; + } + } + return false; +} diff --git a/src/gui/layout/qgslayoutimageexportoptionsdialog.h b/src/gui/layout/qgslayoutimageexportoptionsdialog.h index 0c2ff746f60e..f1af69a3879a 100644 --- a/src/gui/layout/qgslayoutimageexportoptionsdialog.h +++ b/src/gui/layout/qgslayoutimageexportoptionsdialog.h @@ -41,9 +41,10 @@ class GUI_EXPORT QgsLayoutImageExportOptionsDialog: public QDialog, private Ui:: /** * Constructor for QgsLayoutImageExportOptionsDialog * \param parent parent widget + * \param fileExtension output image file extension * \param flags window flags */ - QgsLayoutImageExportOptionsDialog( QWidget *parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags() ); + QgsLayoutImageExportOptionsDialog( QWidget *parent = nullptr, const QString &fileExtension = QString(), Qt::WindowFlags flags = Qt::WindowFlags() ); /** * Sets the initial resolution displayed in the dialog. @@ -138,6 +139,12 @@ class GUI_EXPORT QgsLayoutImageExportOptionsDialog: public QDialog, private Ui:: //! Returns whether the pdf should be opened after exporting it bool openAfterExporting() const; + + //! Sets the image quality (for JPEG) + void setQuality( int quality ); + //! Returns the image quality + int quality() const; + private slots: void mWidthSpinBox_valueChanged( int value ); @@ -148,7 +155,9 @@ class GUI_EXPORT QgsLayoutImageExportOptionsDialog: public QDialog, private Ui:: private: + bool shouldShowQuality() const; QSizeF mImageSize; + QString mFileExtension; }; diff --git a/src/ui/layout/qgslayoutimageexportoptions.ui b/src/ui/layout/qgslayoutimageexportoptions.ui index c5605c615156..6e230a3b8b9f 100644 --- a/src/ui/layout/qgslayoutimageexportoptions.ui +++ b/src/ui/layout/qgslayoutimageexportoptions.ui @@ -20,20 +20,33 @@ Export Options - - - - Page height + + + + Qt::Horizontal - + + + 40 + 20 + + + - - + + - If unchecked, the generated images will not be antialiased + If checked, a separate world file which georeferences exported images will be created - Enable antialiasing + Generate world file + + + + + + + Page width @@ -54,33 +67,27 @@ 99999999 - + false - - - - If checked, a separate world file which georeferences exported images will be created - + + - Generate world file + Export resolution - - - - Qt::Horizontal + + + + If unchecked, the generated images will not be antialiased - - - 40 - 20 - + + Enable antialiasing - + @@ -93,15 +100,15 @@ 3000 - + false - - + + - Page width + Page height @@ -122,15 +129,47 @@ 99999999 - + false - - + + - Export resolution + Quality + + + + + + + 1 + + + 100 + + + 90 + + + Qt::Horizontal + + + + + + + % + + + 1 + + + 100 + + + 90 @@ -330,6 +369,8 @@ mResolutionSpinBox mWidthSpinBox mHeightSpinBox + mQualitySlider + mQualitySpinBox mAntialiasingCheckBox mGenerateWorldFile mClipToContentGroupBox @@ -337,6 +378,7 @@ mLeftMarginSpinBox mRightMarginSpinBox mBottomMarginSpinBox + mOpenAfterExportingCheckBox