Merge pull request #5909 from nyalldawson/layout_next

Layout SVG exports
This commit is contained in:
Nyall Dawson 2017-12-19 15:29:50 +11:00 committed by GitHub
commit ef1bdd30f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 761 additions and 72 deletions

View File

@ -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

View File

@ -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/;

View File

@ -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
};

View File

@ -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;

View File

@ -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;

View File

@ -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 );
}

View File

@ -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

View File

@ -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()

View File

@ -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 &region, double dpi ) const
{
if ( !map )

View File

@ -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;
};

View File

@ -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 );

View File

@ -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;
}
}

View File

@ -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 );

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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()

View File

@ -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()