mirror of
https://github.com/qgis/QGIS.git
synced 2025-02-25 00:58:06 -05:00
Merge pull request #5909 from nyalldawson/layout_next
Layout SVG exports
This commit is contained in:
commit
ef1bdd30f1
@ -123,6 +123,7 @@ Returns the rendered image, or a null QImage if the image does not fit into avai
|
||||
MemoryError,
|
||||
FileError,
|
||||
PrintError,
|
||||
SvgLayerError,
|
||||
};
|
||||
|
||||
struct ImageExportSettings
|
||||
@ -236,6 +237,62 @@ Layout context flags, which control how the export will be created.
|
||||
%Docstring
|
||||
Exports the layout as a PDF to the a ``filePath``, using the specified export ``settings``.
|
||||
|
||||
Returns a result code indicating whether the export was successful or an
|
||||
error was encountered.
|
||||
%End
|
||||
|
||||
|
||||
struct SvgExportSettings
|
||||
{
|
||||
SvgExportSettings();
|
||||
%Docstring
|
||||
Constructor for SvgExportSettings
|
||||
%End
|
||||
|
||||
double dpi;
|
||||
%Docstring
|
||||
Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.
|
||||
%End
|
||||
|
||||
bool forceVectorOutput;
|
||||
%Docstring
|
||||
Set to true to force vector object exports, even when the resultant appearance will differ
|
||||
from the layout. If false, some items may be rasterized in order to maintain their
|
||||
correct appearance in the output.
|
||||
|
||||
This option is mutually exclusive with rasterizeWholeImage.
|
||||
%End
|
||||
|
||||
bool cropToContents;
|
||||
%Docstring
|
||||
Set to true if image should be cropped so only parts of the layout
|
||||
containing items are exported.
|
||||
%End
|
||||
|
||||
QgsMargins cropMargins;
|
||||
%Docstring
|
||||
Crop to content margins, in layout units. These margins will be added
|
||||
to the bounds of the exported layout if cropToContents is true.
|
||||
%End
|
||||
|
||||
bool exportAsLayers;
|
||||
%Docstring
|
||||
Set to true to export as a layered SVG file.
|
||||
Note that this option is considered experimental, and the generated
|
||||
SVG may differ from the expected appearance of the layout.
|
||||
%End
|
||||
|
||||
QgsLayoutContext::Flags flags;
|
||||
%Docstring
|
||||
Layout context flags, which control how the export will be created.
|
||||
%End
|
||||
|
||||
};
|
||||
|
||||
ExportResult exportToSvg( const QString &filePath, const QgsLayoutExporter::SvgExportSettings &settings );
|
||||
%Docstring
|
||||
Exports the layout as an SVG to the a ``filePath``, using the specified export ``settings``.
|
||||
|
||||
Returns a result code indicating whether the export was successful or an
|
||||
error was encountered.
|
||||
%End
|
||||
|
@ -91,8 +91,9 @@ will be set to true if string could be successfully interpreted as a
|
||||
page orientation.
|
||||
%End
|
||||
|
||||
virtual void attemptResize( const QgsLayoutSize &size, bool includesFrame = false );
|
||||
virtual QRectF boundingRect() const;
|
||||
|
||||
virtual void attemptResize( const QgsLayoutSize &size, bool includesFrame = false );
|
||||
|
||||
virtual QgsAbstractLayoutUndoCommand *createCommand( const QString &text, int id, QUndoCommand *parent = 0 ) /Factory/;
|
||||
|
||||
|
@ -65,6 +65,14 @@ will be returned unchanged.
|
||||
.. seealso:: :py:func:`extensionsFromFilter()`
|
||||
|
||||
.. seealso:: :py:func:`ensureFileNameHasExtension()`
|
||||
%End
|
||||
|
||||
static QString stringToSafeFilename( const QString &string );
|
||||
%Docstring
|
||||
Converts a ``string`` to a safe filename, replacing characters which are not safe
|
||||
for filenames with an '_' character.
|
||||
|
||||
This method should be called with file names only, not complete paths.
|
||||
%End
|
||||
};
|
||||
|
||||
|
@ -19,6 +19,7 @@
|
||||
#include "qgslayoutitemregistry.h"
|
||||
#include "qgssettings.h"
|
||||
#include "qgisapp.h"
|
||||
#include "qgsfileutils.h"
|
||||
#include "qgslogger.h"
|
||||
#include "qgslayout.h"
|
||||
#include "qgslayoutappmenuprovider.h"
|
||||
@ -53,6 +54,7 @@
|
||||
#include "qgsbusyindicatordialog.h"
|
||||
#include "qgslayoutundostack.h"
|
||||
#include "qgslayoutpagecollection.h"
|
||||
#include "ui_qgssvgexportoptions.h"
|
||||
#include <QShortcut>
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
@ -182,6 +184,7 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla
|
||||
|
||||
connect( mActionExportAsImage, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportToRaster );
|
||||
connect( mActionExportAsPDF, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportToPdf );
|
||||
connect( mActionExportAsSVG, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportToSvg );
|
||||
|
||||
connect( mActionShowGrid, &QAction::triggered, this, &QgsLayoutDesignerDialog::showGrid );
|
||||
connect( mActionSnapGrid, &QAction::triggered, this, &QgsLayoutDesignerDialog::snapToGrid );
|
||||
@ -1474,7 +1477,7 @@ void QgsLayoutDesignerDialog::exportToRaster()
|
||||
QgsAtlasComposition *atlasMap = &mComposition->atlasComposition();
|
||||
#endif
|
||||
|
||||
QString outputFileName;
|
||||
QString outputFileName = QgsFileUtils::stringToSafeFilename( mLayout->name() );
|
||||
#if 0 //TODO
|
||||
if ( atlasMap->enabled() && mComposition->atlasMode() == QgsComposition::PreviewAtlas )
|
||||
{
|
||||
@ -1539,6 +1542,8 @@ void QgsLayoutDesignerDialog::exportToRaster()
|
||||
break;
|
||||
|
||||
case QgsLayoutExporter::PrintError:
|
||||
case QgsLayoutExporter::SvgLayerError:
|
||||
// no meaning for raster exports, will not be encountered
|
||||
break;
|
||||
|
||||
case QgsLayoutExporter::FileError:
|
||||
@ -1594,7 +1599,7 @@ void QgsLayoutDesignerDialog::exportToPdf()
|
||||
else
|
||||
{
|
||||
#endif
|
||||
outputFileName = file.path();
|
||||
outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mLayout->name() ) + QStringLiteral( ".pdf" );
|
||||
#if 0 //TODO
|
||||
}
|
||||
#endif
|
||||
@ -1665,12 +1670,170 @@ void QgsLayoutDesignerDialog::exportToPdf()
|
||||
"Please try a lower resolution or a smaller paper size." ),
|
||||
QMessageBox::Ok, QMessageBox::Ok );
|
||||
break;
|
||||
|
||||
case QgsLayoutExporter::SvgLayerError:
|
||||
// no meaning for PDF exports, will not be encountered
|
||||
break;
|
||||
}
|
||||
|
||||
mView->setPaintingEnabled( true );
|
||||
QApplication::restoreOverrideCursor();
|
||||
}
|
||||
|
||||
void QgsLayoutDesignerDialog::exportToSvg()
|
||||
{
|
||||
if ( containsWmsLayers() )
|
||||
{
|
||||
showWmsPrintingWarning();
|
||||
}
|
||||
|
||||
showSvgExportWarning();
|
||||
|
||||
QgsSettings settings;
|
||||
QString lastUsedFile = settings.value( QStringLiteral( "UI/lastSaveAsSvgFile" ), QStringLiteral( "qgis.svg" ) ).toString();
|
||||
QFileInfo file( lastUsedFile );
|
||||
QString outputFileName = QgsFileUtils::stringToSafeFilename( mLayout->name() );
|
||||
|
||||
#if 0// TODO
|
||||
if ( hasAnAtlas && !atlasOnASingleFile &&
|
||||
( mode == QgsComposer::Atlas || mComposition->atlasMode() == QgsComposition::PreviewAtlas ) )
|
||||
{
|
||||
outputFileName = QDir( file.path() ).filePath( atlasMap->currentFilename() ) + ".pdf";
|
||||
}
|
||||
else
|
||||
{
|
||||
#endif
|
||||
outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mLayout->name() ) + QStringLiteral( ".svg" );
|
||||
#if 0 //TODO
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_MAC
|
||||
QgisApp::instance()->activateWindow();
|
||||
this->raise();
|
||||
#endif
|
||||
outputFileName = QFileDialog::getSaveFileName(
|
||||
this,
|
||||
tr( "Export to SVG" ),
|
||||
outputFileName,
|
||||
tr( "SVG Format" ) + " (*.svg *.SVG)" );
|
||||
this->activateWindow();
|
||||
if ( outputFileName.isEmpty() )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !outputFileName.endsWith( QLatin1String( ".svg" ), Qt::CaseInsensitive ) )
|
||||
{
|
||||
outputFileName += QLatin1String( ".svg" );
|
||||
}
|
||||
|
||||
settings.setValue( QStringLiteral( "UI/lastSaveAsSvgFile" ), outputFileName );
|
||||
|
||||
bool groupLayers = false;
|
||||
bool prevSettingLabelsAsOutlines = mLayout->project()->readBoolEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), true );
|
||||
bool clipToContent = false;
|
||||
double marginTop = 0.0;
|
||||
double marginRight = 0.0;
|
||||
double marginBottom = 0.0;
|
||||
double marginLeft = 0.0;
|
||||
bool previousForceVector = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool();
|
||||
|
||||
// open options dialog
|
||||
QDialog dialog;
|
||||
Ui::QgsSvgExportOptionsDialog options;
|
||||
options.setupUi( &dialog );
|
||||
options.chkTextAsOutline->setChecked( prevSettingLabelsAsOutlines );
|
||||
options.chkMapLayersAsGroup->setChecked( mLayout->customProperty( QStringLiteral( "svgGroupLayers" ), false ).toBool() );
|
||||
options.mClipToContentGroupBox->setChecked( mLayout->customProperty( QStringLiteral( "svgCropToContents" ), false ).toBool() );
|
||||
options.mForceVectorCheckBox->setChecked( previousForceVector );
|
||||
options.mTopMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginTop" ), 0 ).toInt() );
|
||||
options.mRightMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginRight" ), 0 ).toInt() );
|
||||
options.mBottomMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginBottom" ), 0 ).toInt() );
|
||||
options.mLeftMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginLeft" ), 0 ).toInt() );
|
||||
|
||||
if ( dialog.exec() != QDialog::Accepted )
|
||||
return;
|
||||
|
||||
groupLayers = options.chkMapLayersAsGroup->isChecked();
|
||||
clipToContent = options.mClipToContentGroupBox->isChecked();
|
||||
marginTop = options.mTopMarginSpinBox->value();
|
||||
marginRight = options.mRightMarginSpinBox->value();
|
||||
marginBottom = options.mBottomMarginSpinBox->value();
|
||||
marginLeft = options.mLeftMarginSpinBox->value();
|
||||
|
||||
//save dialog settings
|
||||
mLayout->setCustomProperty( QStringLiteral( "svgGroupLayers" ), groupLayers );
|
||||
mLayout->setCustomProperty( QStringLiteral( "svgCropToContents" ), clipToContent );
|
||||
mLayout->setCustomProperty( QStringLiteral( "svgCropMarginTop" ), marginTop );
|
||||
mLayout->setCustomProperty( QStringLiteral( "svgCropMarginRight" ), marginRight );
|
||||
mLayout->setCustomProperty( QStringLiteral( "svgCropMarginBottom" ), marginBottom );
|
||||
mLayout->setCustomProperty( QStringLiteral( "svgCropMarginLeft" ), marginLeft );
|
||||
|
||||
//temporarily override label draw outlines setting
|
||||
mLayout->project()->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), options.chkTextAsOutline->isChecked() );
|
||||
|
||||
mView->setPaintingEnabled( false );
|
||||
QApplication::setOverrideCursor( Qt::BusyCursor );
|
||||
|
||||
QgsLayoutExporter::SvgExportSettings svgSettings;
|
||||
svgSettings.forceVectorOutput = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool();
|
||||
svgSettings.cropToContents = clipToContent;
|
||||
svgSettings.cropMargins = QgsMargins( marginLeft, marginTop, marginRight, marginBottom );
|
||||
svgSettings.forceVectorOutput = options.mForceVectorCheckBox->isChecked();
|
||||
svgSettings.exportAsLayers = groupLayers;
|
||||
|
||||
// force a refresh, to e.g. update data defined properties, tables, etc
|
||||
mLayout->refresh();
|
||||
|
||||
QFileInfo fi( outputFileName );
|
||||
QgsLayoutExporter exporter( mLayout );
|
||||
switch ( exporter.exportToSvg( outputFileName, svgSettings ) )
|
||||
{
|
||||
case QgsLayoutExporter::Success:
|
||||
{
|
||||
mMessageBar->pushMessage( tr( "Export layout" ),
|
||||
tr( "Successfully exported layout to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( fi.path() ).toString(), outputFileName ),
|
||||
QgsMessageBar::INFO, 0 );
|
||||
break;
|
||||
}
|
||||
|
||||
case QgsLayoutExporter::FileError:
|
||||
QMessageBox::warning( this, tr( "Export to SVG" ),
|
||||
tr( "Cannot write to %1.\n\nThis file may be open in another application." ).arg( outputFileName ),
|
||||
QMessageBox::Ok,
|
||||
QMessageBox::Ok );
|
||||
break;
|
||||
|
||||
case QgsLayoutExporter::SvgLayerError:
|
||||
QMessageBox::warning( this, tr( "Export to SVG" ),
|
||||
tr( "Cannot create layered SVG file %1." ).arg( outputFileName ),
|
||||
QMessageBox::Ok,
|
||||
QMessageBox::Ok );
|
||||
break;
|
||||
|
||||
case QgsLayoutExporter::PrintError:
|
||||
QMessageBox::warning( this, tr( "Export to SVG" ),
|
||||
tr( "Could not create print device." ),
|
||||
QMessageBox::Ok,
|
||||
QMessageBox::Ok );
|
||||
break;
|
||||
|
||||
|
||||
case QgsLayoutExporter::MemoryError:
|
||||
QMessageBox::warning( this, tr( "Memory Allocation Error" ),
|
||||
tr( "Exporting the SVG "
|
||||
"resulted in a memory overflow.\n\n"
|
||||
"Please try a lower resolution or a smaller paper size." ),
|
||||
QMessageBox::Ok, QMessageBox::Ok );
|
||||
break;
|
||||
}
|
||||
|
||||
mView->setPaintingEnabled( true );
|
||||
mLayout->project()->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), prevSettingLabelsAsOutlines );
|
||||
QApplication::restoreOverrideCursor();
|
||||
}
|
||||
|
||||
void QgsLayoutDesignerDialog::paste()
|
||||
{
|
||||
QPointF pt = mView->mapFromGlobal( QCursor::pos() );
|
||||
@ -1816,6 +1979,34 @@ void QgsLayoutDesignerDialog::showWmsPrintingWarning()
|
||||
}
|
||||
}
|
||||
|
||||
void QgsLayoutDesignerDialog::showSvgExportWarning()
|
||||
{
|
||||
QgsSettings settings;
|
||||
|
||||
bool displaySVGWarning = settings.value( QStringLiteral( "/UI/displaySVGWarning" ), true ).toBool();
|
||||
|
||||
if ( displaySVGWarning )
|
||||
{
|
||||
QgsMessageViewer m( this );
|
||||
m.setWindowTitle( tr( "Export as SVG" ) );
|
||||
m.setCheckBoxText( tr( "Don't show this message again" ) );
|
||||
m.setCheckBoxState( Qt::Unchecked );
|
||||
m.setCheckBoxVisible( true );
|
||||
m.setCheckBoxQgsSettingsLabel( QStringLiteral( "/UI/displaySVGWarning" ) );
|
||||
m.setMessageAsHtml( tr( "<p>The SVG export function in QGIS has several "
|
||||
"problems due to bugs and deficiencies in the " )
|
||||
+ tr( "underlying Qt SVG library. In particular, there are problems "
|
||||
"with layers not being clipped to the map "
|
||||
"bounding box.</p>" )
|
||||
+ tr( "If you require a vector-based output file from "
|
||||
"QGIS it is suggested that you try exporting "
|
||||
"to PDF if the SVG output is not "
|
||||
"satisfactory."
|
||||
"</p>" ) );
|
||||
m.exec();
|
||||
}
|
||||
}
|
||||
|
||||
bool QgsLayoutDesignerDialog::requiresRasterization() const
|
||||
{
|
||||
QList< QgsLayoutItem *> items;
|
||||
|
@ -284,6 +284,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner
|
||||
void deleteLayout();
|
||||
void exportToRaster();
|
||||
void exportToPdf();
|
||||
void exportToSvg();
|
||||
|
||||
private:
|
||||
|
||||
@ -379,6 +380,8 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner
|
||||
//! Displays a warning because of possible min/max size in WMS
|
||||
void showWmsPrintingWarning();
|
||||
|
||||
void showSvgExportWarning();
|
||||
|
||||
//! True if the layout contains advanced effects, such as blend modes
|
||||
bool requiresRasterization() const;
|
||||
|
||||
|
@ -40,12 +40,11 @@ QgsLayoutGuideWidget::QgsLayoutGuideWidget( QWidget *parent, QgsLayout *layout,
|
||||
mHozGuidesTableView->setEditTriggers( QAbstractItemView::AllEditTriggers );
|
||||
mVertGuidesTableView->setEditTriggers( QAbstractItemView::AllEditTriggers );
|
||||
|
||||
mHozGuidesTableView->setItemDelegateForColumn( 0, new QgsLayoutGuidePositionDelegate( mHozGuidesTableView ) );
|
||||
mHozGuidesTableView->setItemDelegateForColumn( 1, new QgsLayoutGuideUnitDelegate( mHozGuidesTableView ) );
|
||||
|
||||
mHozGuidesTableView->setItemDelegateForColumn( 0, new QgsLayoutGuidePositionDelegate( mHozGuidesTableView, mLayout, mHozProxyModel ) );
|
||||
mHozGuidesTableView->setItemDelegateForColumn( 1, new QgsLayoutGuideUnitDelegate( mHozGuidesTableView, mLayout, mHozProxyModel ) );
|
||||
|
||||
mVertGuidesTableView->setItemDelegateForColumn( 0, new QgsLayoutGuidePositionDelegate( mVertGuidesTableView, mLayout, mVertProxyModel ) );
|
||||
mVertGuidesTableView->setItemDelegateForColumn( 1, new QgsLayoutGuideUnitDelegate( mVertGuidesTableView, mLayout, mVertProxyModel ) );
|
||||
mVertGuidesTableView->setItemDelegateForColumn( 0, new QgsLayoutGuidePositionDelegate( mVertGuidesTableView ) );
|
||||
mVertGuidesTableView->setItemDelegateForColumn( 1, new QgsLayoutGuideUnitDelegate( mVertGuidesTableView ) );
|
||||
|
||||
connect( mAddHozGuideButton, &QPushButton::clicked, this, &QgsLayoutGuideWidget::addHorizontalGuide );
|
||||
connect( mAddVertGuideButton, &QPushButton::clicked, this, &QgsLayoutGuideWidget::addVerticalGuide );
|
||||
@ -141,25 +140,23 @@ void QgsLayoutGuideWidget::applyToAll()
|
||||
}
|
||||
|
||||
|
||||
QgsLayoutGuidePositionDelegate::QgsLayoutGuidePositionDelegate( QObject *parent, QgsLayout *layout, QAbstractItemModel *model )
|
||||
QgsLayoutGuidePositionDelegate::QgsLayoutGuidePositionDelegate( QObject *parent )
|
||||
: QStyledItemDelegate( parent )
|
||||
, mLayout( layout )
|
||||
, mModel( model )
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QWidget *QgsLayoutGuidePositionDelegate::createEditor( QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index ) const
|
||||
QWidget *QgsLayoutGuidePositionDelegate::createEditor( QWidget *parent, const QStyleOptionViewItem &, const QModelIndex & ) const
|
||||
{
|
||||
QgsDoubleSpinBox *spin = new QgsDoubleSpinBox( parent );
|
||||
spin->setMinimum( 0 );
|
||||
spin->setMaximum( 1000000 );
|
||||
spin->setDecimals( 2 );
|
||||
spin->setShowClearButton( false );
|
||||
connect( spin, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, [ = ]( double v )
|
||||
connect( spin, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, [ = ]( double )
|
||||
{
|
||||
// we want to update on every spin change, not just the final
|
||||
setModelData( index, v, QgsLayoutGuideCollection::PositionRole );
|
||||
const_cast< QgsLayoutGuidePositionDelegate * >( this )->emit commitData( spin );
|
||||
} );
|
||||
return spin;
|
||||
}
|
||||
@ -170,26 +167,18 @@ void QgsLayoutGuidePositionDelegate::setModelData( QWidget *editor, QAbstractIte
|
||||
model->setData( index, spin->value(), QgsLayoutGuideCollection::PositionRole );
|
||||
}
|
||||
|
||||
void QgsLayoutGuidePositionDelegate::setModelData( const QModelIndex &index, const QVariant &value, int role ) const
|
||||
{
|
||||
mModel->setData( index, value, role );
|
||||
}
|
||||
|
||||
QgsLayoutGuideUnitDelegate::QgsLayoutGuideUnitDelegate( QObject *parent, QgsLayout *layout, QAbstractItemModel *model )
|
||||
QgsLayoutGuideUnitDelegate::QgsLayoutGuideUnitDelegate( QObject *parent )
|
||||
: QStyledItemDelegate( parent )
|
||||
, mLayout( layout )
|
||||
, mModel( model )
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QWidget *QgsLayoutGuideUnitDelegate::createEditor( QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index ) const
|
||||
QWidget *QgsLayoutGuideUnitDelegate::createEditor( QWidget *parent, const QStyleOptionViewItem &, const QModelIndex & ) const
|
||||
{
|
||||
QgsLayoutUnitsComboBox *unitsCb = new QgsLayoutUnitsComboBox( parent );
|
||||
connect( unitsCb, &QgsLayoutUnitsComboBox::changed, this, [ = ]( int unit )
|
||||
connect( unitsCb, &QgsLayoutUnitsComboBox::changed, this, [ = ]( int )
|
||||
{
|
||||
// we want to update on every unit change, not just the final
|
||||
setModelData( index, unit, QgsLayoutGuideCollection::UnitsRole );
|
||||
const_cast< QgsLayoutGuideUnitDelegate * >( this )->emit commitData( unitsCb );
|
||||
} );
|
||||
return unitsCb;
|
||||
}
|
||||
@ -200,7 +189,3 @@ void QgsLayoutGuideUnitDelegate::setModelData( QWidget *editor, QAbstractItemMod
|
||||
model->setData( index, cb->unit(), QgsLayoutGuideCollection::UnitsRole );
|
||||
}
|
||||
|
||||
void QgsLayoutGuideUnitDelegate::setModelData( const QModelIndex &index, const QVariant &value, int role ) const
|
||||
{
|
||||
mModel->setData( index, value, role );
|
||||
}
|
||||
|
@ -62,18 +62,12 @@ class QgsLayoutGuidePositionDelegate : public QStyledItemDelegate
|
||||
|
||||
public:
|
||||
|
||||
QgsLayoutGuidePositionDelegate( QObject *parent, QgsLayout *layout, QAbstractItemModel *model );
|
||||
QgsLayoutGuidePositionDelegate( QObject *parent );
|
||||
|
||||
protected:
|
||||
QWidget *createEditor( QWidget *parent, const QStyleOptionViewItem & /*option*/, const QModelIndex &index ) const override;
|
||||
void setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const override;
|
||||
|
||||
void setModelData( const QModelIndex &index, const QVariant &value, int role ) const;
|
||||
|
||||
private:
|
||||
|
||||
QgsLayout *mLayout = nullptr;
|
||||
QAbstractItemModel *mModel = nullptr;
|
||||
};
|
||||
|
||||
class QgsLayoutGuideUnitDelegate : public QStyledItemDelegate
|
||||
@ -82,19 +76,12 @@ class QgsLayoutGuideUnitDelegate : public QStyledItemDelegate
|
||||
|
||||
public:
|
||||
|
||||
QgsLayoutGuideUnitDelegate( QObject *parent, QgsLayout *layout, QAbstractItemModel *model );
|
||||
QgsLayoutGuideUnitDelegate( QObject *parent );
|
||||
|
||||
protected:
|
||||
QWidget *createEditor( QWidget *parent, const QStyleOptionViewItem & /*option*/, const QModelIndex &index ) const override;
|
||||
void setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const override;
|
||||
|
||||
void setModelData( const QModelIndex &index, const QVariant &value, int role ) const;
|
||||
|
||||
private:
|
||||
|
||||
QgsLayout *mLayout = nullptr;
|
||||
|
||||
QAbstractItemModel *mModel = nullptr;
|
||||
};
|
||||
|
||||
#endif // QGSLAYOUTGUIDEWIDGET_H
|
||||
|
@ -30,6 +30,8 @@ QgsLayoutLegendLayersDialog::QgsLayoutLegendLayersDialog( QWidget *parent )
|
||||
listMapLayers->setModel( mModel );
|
||||
QModelIndex firstLayer = mModel->index( 0, 0 );
|
||||
listMapLayers->selectionModel()->select( firstLayer, QItemSelectionModel::Select );
|
||||
|
||||
connect( listMapLayers, &QListView::doubleClicked, this, &QgsLayoutLegendLayersDialog::accept );
|
||||
}
|
||||
|
||||
QgsLayoutLegendLayersDialog::~QgsLayoutLegendLayersDialog()
|
||||
|
@ -23,6 +23,7 @@
|
||||
#include "qgslayoutguidecollection.h"
|
||||
#include <QImageWriter>
|
||||
#include <QSize>
|
||||
#include <QSvgGenerator>
|
||||
|
||||
#include "gdal.h"
|
||||
#include "cpl_conv.h"
|
||||
@ -77,6 +78,39 @@ class LayoutGuideHider
|
||||
QHash< QgsLayoutGuide *, bool > mPrevVisibility;
|
||||
};
|
||||
|
||||
class LayoutItemHider
|
||||
{
|
||||
public:
|
||||
explicit LayoutItemHider( const QList<QGraphicsItem *> &items )
|
||||
{
|
||||
for ( QGraphicsItem *item : items )
|
||||
{
|
||||
mPrevVisibility[item] = item->isVisible();
|
||||
item->hide();
|
||||
}
|
||||
}
|
||||
|
||||
void hideAll()
|
||||
{
|
||||
for ( auto it = mPrevVisibility.constBegin(); it != mPrevVisibility.constEnd(); ++it )
|
||||
{
|
||||
it.key()->hide();
|
||||
}
|
||||
}
|
||||
|
||||
~LayoutItemHider()
|
||||
{
|
||||
for ( auto it = mPrevVisibility.constBegin(); it != mPrevVisibility.constEnd(); ++it )
|
||||
{
|
||||
it.key()->setVisible( it.value() );
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
QHash<QGraphicsItem *, bool> mPrevVisibility;
|
||||
};
|
||||
|
||||
///@endcond PRIVATE
|
||||
|
||||
QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout )
|
||||
@ -239,6 +273,7 @@ class LayoutContextSettingsRestorer
|
||||
: mLayout( layout )
|
||||
, mPreviousDpi( layout->context().dpi() )
|
||||
, mPreviousFlags( layout->context().flags() )
|
||||
, mPreviousExportLayer( layout->context().currentExportLayer() )
|
||||
{
|
||||
}
|
||||
|
||||
@ -246,12 +281,14 @@ class LayoutContextSettingsRestorer
|
||||
{
|
||||
mLayout->context().setDpi( mPreviousDpi );
|
||||
mLayout->context().setFlags( mPreviousFlags );
|
||||
mLayout->context().setCurrentExportLayer( mPreviousExportLayer );
|
||||
}
|
||||
|
||||
private:
|
||||
QgsLayout *mLayout = nullptr;
|
||||
double mPreviousDpi = 0;
|
||||
QgsLayoutContext::Flags mPreviousFlags = 0;
|
||||
int mPreviousExportLayer = 0;
|
||||
};
|
||||
///@endcond PRIVATE
|
||||
|
||||
@ -401,6 +438,173 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f
|
||||
return result;
|
||||
}
|
||||
|
||||
QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &filePath, const QgsLayoutExporter::SvgExportSettings &s )
|
||||
{
|
||||
if ( !mLayout )
|
||||
return PrintError;
|
||||
|
||||
SvgExportSettings settings = s;
|
||||
if ( settings.dpi <= 0 )
|
||||
settings.dpi = mLayout->context().dpi();
|
||||
|
||||
mErrorFileName.clear();
|
||||
|
||||
LayoutContextPreviewSettingRestorer restorer( mLayout );
|
||||
( void )restorer;
|
||||
LayoutContextSettingsRestorer contextRestorer( mLayout );
|
||||
( void )contextRestorer;
|
||||
mLayout->context().setDpi( settings.dpi );
|
||||
|
||||
mLayout->context().setFlag( QgsLayoutContext::FlagForceVectorOutput, settings.forceVectorOutput );
|
||||
|
||||
QFileInfo fi( filePath );
|
||||
PageExportDetails pageDetails;
|
||||
pageDetails.directory = fi.path();
|
||||
pageDetails.baseName = fi.baseName();
|
||||
pageDetails.extension = fi.completeSuffix();
|
||||
|
||||
double inchesToLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutInches ) );
|
||||
|
||||
for ( int i = 0; i < mLayout->pageCollection()->pageCount(); ++i )
|
||||
{
|
||||
if ( !mLayout->pageCollection()->shouldExportPage( i ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
pageDetails.page = i;
|
||||
QString fileName = generateFileName( pageDetails );
|
||||
|
||||
QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( i );
|
||||
QRectF bounds;
|
||||
if ( settings.cropToContents )
|
||||
{
|
||||
if ( mLayout->pageCollection()->pageCount() == 1 )
|
||||
{
|
||||
// single page, so include everything
|
||||
bounds = mLayout->layoutBounds( true );
|
||||
}
|
||||
else
|
||||
{
|
||||
// multi page, so just clip to items on current page
|
||||
bounds = mLayout->pageItemBounds( i, true );
|
||||
}
|
||||
bounds = bounds.adjusted( -settings.cropMargins.left(),
|
||||
-settings.cropMargins.top(),
|
||||
settings.cropMargins.right(),
|
||||
settings.cropMargins.bottom() );
|
||||
}
|
||||
else
|
||||
{
|
||||
bounds = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() );
|
||||
}
|
||||
|
||||
//width in pixel
|
||||
int width = ( int )( bounds.width() * settings.dpi / inchesToLayoutUnits );
|
||||
//height in pixel
|
||||
int height = ( int )( bounds.height() * settings.dpi / inchesToLayoutUnits );
|
||||
if ( width == 0 || height == 0 )
|
||||
{
|
||||
//invalid size, skip this page
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( settings.exportAsLayers )
|
||||
{
|
||||
const QRectF paperRect = QRectF( pageItem->pos().x(),
|
||||
pageItem->pos().y(),
|
||||
pageItem->rect().width(),
|
||||
pageItem->rect().height() );
|
||||
QDomDocument svg;
|
||||
QDomNode svgDocRoot;
|
||||
const QList<QGraphicsItem *> items = mLayout->items( paperRect,
|
||||
Qt::IntersectsItemBoundingRect,
|
||||
Qt::AscendingOrder );
|
||||
|
||||
LayoutItemHider itemHider( items );
|
||||
( void )itemHider;
|
||||
|
||||
int layoutItemLayerIdx = 0;
|
||||
auto it = items.constBegin();
|
||||
for ( unsigned svgLayerId = 1; it != items.constEnd(); ++svgLayerId )
|
||||
{
|
||||
itemHider.hideAll();
|
||||
QgsLayoutItem *layoutItem = dynamic_cast<QgsLayoutItem *>( *it );
|
||||
QString layerName = QObject::tr( "Layer %1" ).arg( svgLayerId );
|
||||
if ( layoutItem && layoutItem->numberExportLayers() > 0 )
|
||||
{
|
||||
layoutItem->show();
|
||||
mLayout->context().setCurrentExportLayer( layoutItemLayerIdx );
|
||||
++layoutItemLayerIdx;
|
||||
}
|
||||
else
|
||||
{
|
||||
// show all items until the next item that renders on a separate layer
|
||||
for ( ; it != items.constEnd(); ++it )
|
||||
{
|
||||
layoutItem = dynamic_cast<QgsLayoutItem *>( *it );
|
||||
if ( layoutItem && layoutItem->numberExportLayers() > 0 )
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
( *it )->show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExportResult result = renderToLayeredSvg( settings, width, height, i, bounds, fileName, svgLayerId, layerName, svg, svgDocRoot );
|
||||
if ( result != Success )
|
||||
return result;
|
||||
|
||||
if ( layoutItem && layoutItem->numberExportLayers() > 0 && layoutItem->numberExportLayers() == layoutItemLayerIdx ) // restore and pass to next item
|
||||
{
|
||||
mLayout->context().setCurrentExportLayer( -1 );
|
||||
layoutItemLayerIdx = 0;
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
QFile out( fileName );
|
||||
bool openOk = out.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate );
|
||||
if ( !openOk )
|
||||
{
|
||||
mErrorFileName = fileName;
|
||||
return FileError;
|
||||
}
|
||||
|
||||
out.write( svg.toByteArray() );
|
||||
}
|
||||
else
|
||||
{
|
||||
QSvgGenerator generator;
|
||||
generator.setTitle( mLayout->project()->title() );
|
||||
generator.setFileName( fileName );
|
||||
generator.setSize( QSize( width, height ) );
|
||||
generator.setViewBox( QRect( 0, 0, width, height ) );
|
||||
generator.setResolution( settings.dpi );
|
||||
|
||||
QPainter p;
|
||||
bool createOk = p.begin( &generator );
|
||||
if ( !createOk )
|
||||
{
|
||||
mErrorFileName = fileName;
|
||||
return FileError;
|
||||
}
|
||||
|
||||
if ( settings.cropToContents )
|
||||
renderRegion( &p, bounds );
|
||||
else
|
||||
renderPage( &p, i );
|
||||
|
||||
p.end();
|
||||
}
|
||||
}
|
||||
|
||||
return Success;
|
||||
}
|
||||
|
||||
void QgsLayoutExporter::preparePrintAsPdf( QPrinter &printer, const QString &filePath )
|
||||
{
|
||||
printer.setOutputFileName( filePath );
|
||||
@ -524,6 +728,57 @@ void QgsLayoutExporter::updatePrinterPageSize( QPrinter &printer, int page )
|
||||
printer.setPaperSize( pageSizeMM.toQSizeF(), QPrinter::Millimeter );
|
||||
}
|
||||
|
||||
QgsLayoutExporter::ExportResult QgsLayoutExporter::renderToLayeredSvg( const SvgExportSettings &settings, double width, double height, int page, QRectF bounds, const QString &filename, int svgLayerId, const QString &layerName, QDomDocument &svg, QDomNode &svgDocRoot ) const
|
||||
{
|
||||
QBuffer svgBuffer;
|
||||
{
|
||||
QSvgGenerator generator;
|
||||
generator.setTitle( mLayout->name() );
|
||||
generator.setOutputDevice( &svgBuffer );
|
||||
generator.setSize( QSize( width, height ) );
|
||||
generator.setViewBox( QRect( 0, 0, width, height ) );
|
||||
generator.setResolution( settings.dpi ); //because the rendering is done in mm, convert the dpi
|
||||
|
||||
QPainter svgPainter( &generator );
|
||||
if ( settings.cropToContents )
|
||||
renderRegion( &svgPainter, bounds );
|
||||
else
|
||||
renderPage( &svgPainter, page );
|
||||
}
|
||||
|
||||
// post-process svg output to create groups in a single svg file
|
||||
// we create inkscape layers since it's nice and clean and free
|
||||
// and fully svg compatible
|
||||
{
|
||||
svgBuffer.close();
|
||||
svgBuffer.open( QIODevice::ReadOnly );
|
||||
QDomDocument doc;
|
||||
QString errorMsg;
|
||||
int errorLine;
|
||||
if ( ! doc.setContent( &svgBuffer, false, &errorMsg, &errorLine ) )
|
||||
{
|
||||
mErrorFileName = filename;
|
||||
return SvgLayerError;
|
||||
}
|
||||
if ( 1 == svgLayerId )
|
||||
{
|
||||
svg = QDomDocument( doc.doctype() );
|
||||
svg.appendChild( svg.importNode( doc.firstChild(), false ) );
|
||||
svgDocRoot = svg.importNode( doc.elementsByTagName( QStringLiteral( "svg" ) ).at( 0 ), false );
|
||||
svgDocRoot.toElement().setAttribute( QStringLiteral( "xmlns:inkscape" ), QStringLiteral( "http://www.inkscape.org/namespaces/inkscape" ) );
|
||||
svg.appendChild( svgDocRoot );
|
||||
}
|
||||
QDomNode mainGroup = svg.importNode( doc.elementsByTagName( QStringLiteral( "g" ) ).at( 0 ), true );
|
||||
mainGroup.toElement().setAttribute( QStringLiteral( "id" ), layerName );
|
||||
mainGroup.toElement().setAttribute( QStringLiteral( "inkscape:label" ), layerName );
|
||||
mainGroup.toElement().setAttribute( QStringLiteral( "inkscape:groupmode" ), QStringLiteral( "layer" ) );
|
||||
QDomNode defs = svg.importNode( doc.elementsByTagName( QStringLiteral( "defs" ) ).at( 0 ), true );
|
||||
svgDocRoot.appendChild( defs );
|
||||
svgDocRoot.appendChild( mainGroup );
|
||||
}
|
||||
return Success;
|
||||
}
|
||||
|
||||
std::unique_ptr<double[]> QgsLayoutExporter::computeGeoTransform( const QgsLayoutItemMap *map, const QRectF ®ion, double dpi ) const
|
||||
{
|
||||
if ( !map )
|
||||
|
@ -132,6 +132,7 @@ class CORE_EXPORT QgsLayoutExporter
|
||||
MemoryError, //!< Unable to allocate memory required to export
|
||||
FileError, //!< Could not write to destination file, likely due to a lock held by another application
|
||||
PrintError, //!< Could not start printing to destination device
|
||||
SvgLayerError, //!< Could not create layered SVG file
|
||||
};
|
||||
|
||||
//! Contains settings relating to exporting layouts to raster images
|
||||
@ -247,6 +248,61 @@ class CORE_EXPORT QgsLayoutExporter
|
||||
*/
|
||||
ExportResult exportToPdf( const QString &filePath, const QgsLayoutExporter::PdfExportSettings &settings );
|
||||
|
||||
|
||||
//! Contains settings relating to exporting layouts to SVG
|
||||
struct SvgExportSettings
|
||||
{
|
||||
//! Constructor for SvgExportSettings
|
||||
SvgExportSettings()
|
||||
: flags( QgsLayoutContext::FlagAntialiasing | QgsLayoutContext::FlagUseAdvancedEffects )
|
||||
{}
|
||||
|
||||
//! Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.
|
||||
double dpi = -1;
|
||||
|
||||
/**
|
||||
* Set to true to force vector object exports, even when the resultant appearance will differ
|
||||
* from the layout. If false, some items may be rasterized in order to maintain their
|
||||
* correct appearance in the output.
|
||||
*
|
||||
* This option is mutually exclusive with rasterizeWholeImage.
|
||||
*/
|
||||
bool forceVectorOutput = false;
|
||||
|
||||
/**
|
||||
* Set to true if image should be cropped so only parts of the layout
|
||||
* containing items are exported.
|
||||
*/
|
||||
bool cropToContents = false;
|
||||
|
||||
/**
|
||||
* Crop to content margins, in layout units. These margins will be added
|
||||
* to the bounds of the exported layout if cropToContents is true.
|
||||
*/
|
||||
QgsMargins cropMargins;
|
||||
|
||||
/**
|
||||
* Set to true to export as a layered SVG file.
|
||||
* Note that this option is considered experimental, and the generated
|
||||
* SVG may differ from the expected appearance of the layout.
|
||||
*/
|
||||
bool exportAsLayers = false;
|
||||
|
||||
/**
|
||||
* Layout context flags, which control how the export will be created.
|
||||
*/
|
||||
QgsLayoutContext::Flags flags = 0;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Exports the layout as an SVG to the a \a filePath, using the specified export \a settings.
|
||||
*
|
||||
* Returns a result code indicating whether the export was successful or an
|
||||
* error was encountered.
|
||||
*/
|
||||
ExportResult exportToSvg( const QString &filePath, const QgsLayoutExporter::SvgExportSettings &settings );
|
||||
|
||||
/**
|
||||
* Returns the file name corresponding to the last error encountered during
|
||||
* an export.
|
||||
@ -299,7 +355,7 @@ class CORE_EXPORT QgsLayoutExporter
|
||||
|
||||
QPointer< QgsLayout > mLayout;
|
||||
|
||||
QString mErrorFileName;
|
||||
mutable QString mErrorFileName;
|
||||
|
||||
QImage createImage( const ImageExportSettings &settings, int page, QRectF &bounds, bool &skipPage ) const;
|
||||
|
||||
@ -350,6 +406,10 @@ class CORE_EXPORT QgsLayoutExporter
|
||||
|
||||
void updatePrinterPageSize( QPrinter &printer, int page );
|
||||
|
||||
ExportResult renderToLayeredSvg( const SvgExportSettings &settings, double width, double height, int page, QRectF bounds,
|
||||
const QString &filename, int svgLayerId, const QString &layerName,
|
||||
QDomDocument &svg, QDomNode &svgDocRoot ) const;
|
||||
|
||||
friend class TestQgsLayout;
|
||||
|
||||
};
|
||||
|
@ -140,11 +140,11 @@ void QgsLayoutGuide::setLayoutPosition( double position )
|
||||
switch ( mOrientation )
|
||||
{
|
||||
case Qt::Horizontal:
|
||||
p = mLineItem->mapFromScene( QPointF( 0, position ) ).y();
|
||||
p = mPage->mapFromScene( QPointF( 0, position ) ).y();
|
||||
break;
|
||||
|
||||
case Qt::Vertical:
|
||||
p = mLineItem->mapFromScene( QPointF( position, 0 ) ).x();
|
||||
p = mPage->mapFromScene( QPointF( position, 0 ) ).x();
|
||||
break;
|
||||
}
|
||||
mPosition = mLayout->convertFromLayoutUnits( p, mPosition.units() );
|
||||
@ -299,6 +299,9 @@ bool QgsLayoutGuideCollection::setData( const QModelIndex &index, const QVariant
|
||||
return false;
|
||||
|
||||
QgsLayoutMeasurement m = guide->position();
|
||||
if ( m.length() == newPos )
|
||||
return true;
|
||||
|
||||
m.setLength( newPos );
|
||||
mLayout->undoStack()->beginCommand( mPageCollection, tr( "Move Guide" ), Move + index.row() );
|
||||
whileBlocking( guide )->setPosition( m );
|
||||
|
@ -886,6 +886,7 @@ void QgsLayoutItemMap::paint( QPainter *painter, const QStyleOptionGraphicsItem
|
||||
painter->scale( 1 / dotsPerMM, 1 / dotsPerMM ); // scale painter from mm to dots
|
||||
painter->drawImage( std::round( -tl.x()* dotsPerMM ), std::round( -tl.y() * dotsPerMM ), image );
|
||||
painter->scale( dotsPerMM, dotsPerMM );
|
||||
painter->restore();
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -895,6 +896,7 @@ void QgsLayoutItemMap::paint( QPainter *painter, const QStyleOptionGraphicsItem
|
||||
drawMapBackground( painter );
|
||||
}
|
||||
|
||||
painter->save();
|
||||
painter->setClipRect( thisPaintRect );
|
||||
painter->save();
|
||||
painter->translate( mXOffset, mYOffset );
|
||||
@ -917,14 +919,13 @@ void QgsLayoutItemMap::paint( QPainter *painter, const QStyleOptionGraphicsItem
|
||||
mGridStack->drawItems( painter );
|
||||
}
|
||||
drawAnnotations( painter );
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
if ( shouldDrawPart( Frame ) )
|
||||
{
|
||||
drawMapFrame( painter );
|
||||
}
|
||||
painter->restore();
|
||||
mDrawing = false;
|
||||
}
|
||||
}
|
||||
|
@ -32,10 +32,11 @@ QgsLayoutItemPage::QgsLayoutItemPage( QgsLayout *layout )
|
||||
setFlag( QGraphicsItem::ItemIsMovable, false );
|
||||
setZValue( QgsLayout::ZPage );
|
||||
|
||||
// use a hidden pen to specify the amount the page "bleeds" outside it's scene bounds,
|
||||
// (it's a lot easier than reimplementing boundingRect() just to handle this)
|
||||
QPen shadowPen( QBrush( Qt::transparent ), layout->pageCollection()->pageShadowWidth() * 2 );
|
||||
setPen( shadowPen );
|
||||
connect( this, &QgsLayoutItem::sizePositionChanged, this, [ = ]
|
||||
{
|
||||
mBoundingRect = QRectF();
|
||||
prepareGeometryChange();
|
||||
} );
|
||||
|
||||
QFont font;
|
||||
QFontMetrics fm( font );
|
||||
@ -123,6 +124,17 @@ QgsLayoutItemPage::Orientation QgsLayoutItemPage::decodePageOrientation( const Q
|
||||
return Landscape;
|
||||
}
|
||||
|
||||
QRectF QgsLayoutItemPage::boundingRect() const
|
||||
{
|
||||
if ( mBoundingRect.isNull() )
|
||||
{
|
||||
double shadowWidth = mLayout->pageCollection()->pageShadowWidth();
|
||||
mBoundingRect = rect();
|
||||
mBoundingRect.adjust( 0, 0, shadowWidth, shadowWidth );
|
||||
}
|
||||
return mBoundingRect;
|
||||
}
|
||||
|
||||
void QgsLayoutItemPage::attemptResize( const QgsLayoutSize &size, bool includesFrame )
|
||||
{
|
||||
QgsLayoutItem::attemptResize( size, includesFrame );
|
||||
|
@ -121,8 +121,8 @@ class CORE_EXPORT QgsLayoutItemPage : public QgsLayoutItem
|
||||
*/
|
||||
static QgsLayoutItemPage::Orientation decodePageOrientation( const QString &string, bool *ok SIP_OUT = nullptr );
|
||||
|
||||
QRectF boundingRect() const override;
|
||||
void attemptResize( const QgsLayoutSize &size, bool includesFrame = false ) override;
|
||||
|
||||
QgsAbstractLayoutUndoCommand *createCommand( const QString &text, int id, QUndoCommand *parent = nullptr ) override SIP_FACTORY;
|
||||
|
||||
public slots:
|
||||
@ -140,6 +140,7 @@ class CORE_EXPORT QgsLayoutItemPage : public QgsLayoutItem
|
||||
double mMaximumShadowWidth = -1;
|
||||
|
||||
std::unique_ptr< QgsLayoutItemPageGrid > mGrid;
|
||||
mutable QRectF mBoundingRect;
|
||||
|
||||
friend class TestQgsLayoutPage;
|
||||
};
|
||||
|
@ -69,3 +69,11 @@ QString QgsFileUtils::addExtensionFromFilter( const QString &fileName, const QSt
|
||||
const QStringList extensions = extensionsFromFilter( filter );
|
||||
return ensureFileNameHasExtension( fileName, extensions );
|
||||
}
|
||||
|
||||
QString QgsFileUtils::stringToSafeFilename( const QString &string )
|
||||
{
|
||||
QRegularExpression rx( "[^\\w\\-. ]" );
|
||||
QString s = string;
|
||||
s.replace( rx, QStringLiteral( "_" ) );
|
||||
return s;
|
||||
}
|
||||
|
@ -70,6 +70,14 @@ class CORE_EXPORT QgsFileUtils
|
||||
* \see ensureFileNameHasExtension()
|
||||
*/
|
||||
static QString addExtensionFromFilter( const QString &fileName, const QString &filter );
|
||||
|
||||
/**
|
||||
* Converts a \a string to a safe filename, replacing characters which are not safe
|
||||
* for filenames with an '_' character.
|
||||
*
|
||||
* This method should be called with file names only, not complete paths.
|
||||
*/
|
||||
static QString stringToSafeFilename( const QString &string );
|
||||
};
|
||||
|
||||
#endif // QGSFILEUTILS_H
|
||||
|
@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>489</width>
|
||||
<height>282</height>
|
||||
<height>319</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -46,6 +46,16 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="mForceVectorCheckBox">
|
||||
<property name="toolTip">
|
||||
<string>If checked, the layout will always be kept as vector objects when exported to a compatible format, even if the appearance of the resultant file does not match the layouts settings. If unchecked, some elements in the layout may be rasterized in order to keep their appearance intact.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Always export as vectors</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@ -196,6 +206,7 @@
|
||||
<tabstop>chkMapLayersAsGroup</tabstop>
|
||||
<tabstop>chkTextAsOutline</tabstop>
|
||||
<tabstop>mClipToContentGroupBox</tabstop>
|
||||
<tabstop>mForceVectorCheckBox</tabstop>
|
||||
<tabstop>mTopMarginSpinBox</tabstop>
|
||||
<tabstop>mLeftMarginSpinBox</tabstop>
|
||||
<tabstop>mRightMarginSpinBox</tabstop>
|
||||
|
@ -183,6 +183,9 @@
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="mCheckBoxAutoUpdate">
|
||||
<property name="toolTip">
|
||||
<string>Keeps the legend contents synchronized with the main application legend. Customisation is not possible and must be done in the main application.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Auto update</string>
|
||||
</property>
|
||||
@ -1046,22 +1049,10 @@
|
||||
</widget>
|
||||
<layoutdefault spacing="6" margin="11"/>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>QgsScrollArea</class>
|
||||
<extends>QScrollArea</extends>
|
||||
<header>qgsscrollarea.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>QgsColorButton</class>
|
||||
<extends>QToolButton</extends>
|
||||
<header>qgscolorbutton.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>QgsCollapsibleGroupBoxBasic</class>
|
||||
<extends>QGroupBox</extends>
|
||||
<header>qgscollapsiblegroupbox.h</header>
|
||||
<header location="global">qgscollapsiblegroupbox.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
@ -1070,9 +1061,10 @@
|
||||
<header>qgsdoublespinbox.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>QgsFontButton</class>
|
||||
<extends>QToolButton</extends>
|
||||
<header>qgsfontbutton.h</header>
|
||||
<class>QgsScrollArea</class>
|
||||
<extends>QScrollArea</extends>
|
||||
<header>qgsscrollarea.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>QgsSpinBox</class>
|
||||
@ -1084,6 +1076,17 @@
|
||||
<extends>QComboBox</extends>
|
||||
<header>qgslayoutitemcombobox.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>QgsColorButton</class>
|
||||
<extends>QToolButton</extends>
|
||||
<header>qgscolorbutton.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>QgsFontButton</class>
|
||||
<extends>QToolButton</extends>
|
||||
<header>qgsfontbutton.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>QgsPropertyOverrideButton</class>
|
||||
<extends>QToolButton</extends>
|
||||
|
@ -55,6 +55,12 @@ class TestQgsFileUtils(unittest.TestCase):
|
||||
self.assertEqual(QgsFileUtils.addExtensionFromFilter('test.tif', 'All Files (*.*)'), 'test.tif')
|
||||
self.assertEqual(QgsFileUtils.addExtensionFromFilter('test', 'All Files (*.*)'), 'test')
|
||||
|
||||
def testStringToSafeFilename(self):
|
||||
self.assertEqual(QgsFileUtils.stringToSafeFilename('my FiLe v2.0_new.tif'), 'my FiLe v2.0_new.tif')
|
||||
self.assertEqual(
|
||||
QgsFileUtils.stringToSafeFilename('rendered map_final? rev (12-03-1017)_real@#$&*#%&*$!!@$%^&(*(.tif'),
|
||||
'rendered map_final_ rev _12-03-1017__real____________________.tif')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@ -36,6 +36,7 @@ from qgis.core import (QgsMultiRenderChecker,
|
||||
QgsFillSymbol)
|
||||
from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt
|
||||
from qgis.PyQt.QtGui import QImage, QPainter
|
||||
from qgis.PyQt.QtSvg import QSvgRenderer, QSvgGenerator
|
||||
|
||||
from qgis.testing import start_app, unittest
|
||||
|
||||
@ -91,6 +92,24 @@ def pdfToPng(pdf_file_path, rendered_file_path, page, dpi=96):
|
||||
"message: {2}".format(e.cmd, e.returncode, e.message))
|
||||
|
||||
|
||||
def svgToPng(svg_file_path, rendered_file_path, width):
|
||||
svgr = QSvgRenderer(svg_file_path)
|
||||
|
||||
height = width / svgr.viewBoxF().width() * svgr.viewBoxF().height()
|
||||
|
||||
image = QImage(width, height, QImage.Format_ARGB32)
|
||||
image.fill(Qt.transparent)
|
||||
|
||||
p = QPainter(image)
|
||||
p.setRenderHint(QPainter.Antialiasing, False)
|
||||
svgr.render(p)
|
||||
p.end()
|
||||
|
||||
res = image.save(rendered_file_path, 'png')
|
||||
if not res:
|
||||
os.unlink(rendered_file_path)
|
||||
|
||||
|
||||
start_app()
|
||||
|
||||
|
||||
@ -390,6 +409,74 @@ class TestQgsLayoutExporter(unittest.TestCase):
|
||||
self.assertTrue(self.checkImage('exporttopdfdpi_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
|
||||
self.assertTrue(self.checkImage('exporttopdfdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
|
||||
|
||||
def testExportToSvg(self):
|
||||
l = QgsLayout(QgsProject.instance())
|
||||
l.initializeDefaults()
|
||||
|
||||
# add a second page
|
||||
page2 = QgsLayoutItemPage(l)
|
||||
page2.setPageSize('A5')
|
||||
l.pageCollection().addPage(page2)
|
||||
|
||||
# add some items
|
||||
item1 = QgsLayoutItemShape(l)
|
||||
item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
|
||||
fill = QgsSimpleFillSymbolLayer()
|
||||
fill_symbol = QgsFillSymbol()
|
||||
fill_symbol.changeSymbolLayer(0, fill)
|
||||
fill.setColor(Qt.green)
|
||||
fill.setStrokeStyle(Qt.NoPen)
|
||||
item1.setSymbol(fill_symbol)
|
||||
l.addItem(item1)
|
||||
|
||||
item2 = QgsLayoutItemShape(l)
|
||||
item2.attemptSetSceneRect(QRectF(10, 20, 100, 150))
|
||||
item2.attemptMove(QgsLayoutPoint(10, 20), page=1)
|
||||
fill = QgsSimpleFillSymbolLayer()
|
||||
fill_symbol = QgsFillSymbol()
|
||||
fill_symbol.changeSymbolLayer(0, fill)
|
||||
fill.setColor(Qt.cyan)
|
||||
fill.setStrokeStyle(Qt.NoPen)
|
||||
item2.setSymbol(fill_symbol)
|
||||
l.addItem(item2)
|
||||
|
||||
exporter = QgsLayoutExporter(l)
|
||||
# setup settings
|
||||
settings = QgsLayoutExporter.SvgExportSettings()
|
||||
settings.dpi = 80
|
||||
settings.forceVectorOutput = False
|
||||
|
||||
svg_file_path = os.path.join(self.basetestpath, 'test_exporttosvgdpi.svg')
|
||||
svg_file_path_2 = os.path.join(self.basetestpath, 'test_exporttosvgdpi_2.svg')
|
||||
self.assertEqual(exporter.exportToSvg(svg_file_path, settings), QgsLayoutExporter.Success)
|
||||
self.assertTrue(os.path.exists(svg_file_path))
|
||||
self.assertTrue(os.path.exists(svg_file_path_2))
|
||||
|
||||
rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttosvgdpi.png')
|
||||
svgToPng(svg_file_path, rendered_page_1, width=936)
|
||||
rendered_page_2 = os.path.join(self.basetestpath, 'test_exporttosvgdpi2.png')
|
||||
svgToPng(svg_file_path_2, rendered_page_2, width=467)
|
||||
|
||||
self.assertTrue(self.checkImage('exporttosvgdpi_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
|
||||
self.assertTrue(self.checkImage('exporttosvgdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
|
||||
|
||||
# layered
|
||||
settings.exportAsLayers = True
|
||||
|
||||
svg_file_path = os.path.join(self.basetestpath, 'test_exporttosvglayered.svg')
|
||||
svg_file_path_2 = os.path.join(self.basetestpath, 'test_exporttosvglayered_2.svg')
|
||||
self.assertEqual(exporter.exportToSvg(svg_file_path, settings), QgsLayoutExporter.Success)
|
||||
self.assertTrue(os.path.exists(svg_file_path))
|
||||
self.assertTrue(os.path.exists(svg_file_path_2))
|
||||
|
||||
rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttosvglayered.png')
|
||||
svgToPng(svg_file_path, rendered_page_1, width=936)
|
||||
rendered_page_2 = os.path.join(self.basetestpath, 'test_exporttosvglayered2.png')
|
||||
svgToPng(svg_file_path_2, rendered_page_2, width=467)
|
||||
|
||||
self.assertTrue(self.checkImage('exporttosvglayered_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
|
||||
self.assertTrue(self.checkImage('exporttosvglayered_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
|
||||
|
||||
def testExportWorldFile(self):
|
||||
l = QgsLayout(QgsProject.instance())
|
||||
l.initializeDefaults()
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
Loading…
x
Reference in New Issue
Block a user