Skip to content

Commit ef1bdd3

Browse files
authoredDec 19, 2017
Merge pull request #5909 from nyalldawson/layout_next
Layout SVG exports
2 parents 41baaad + 4054a2b commit ef1bdd3

22 files changed

+761
-72
lines changed
 

‎python/core/layout/qgslayoutexporter.sip

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ Returns the rendered image, or a null QImage if the image does not fit into avai
123123
MemoryError,
124124
FileError,
125125
PrintError,
126+
SvgLayerError,
126127
};
127128

128129
struct ImageExportSettings
@@ -236,6 +237,62 @@ Layout context flags, which control how the export will be created.
236237
%Docstring
237238
Exports the layout as a PDF to the a ``filePath``, using the specified export ``settings``.
238239

240+
Returns a result code indicating whether the export was successful or an
241+
error was encountered.
242+
%End
243+
244+
245+
struct SvgExportSettings
246+
{
247+
SvgExportSettings();
248+
%Docstring
249+
Constructor for SvgExportSettings
250+
%End
251+
252+
double dpi;
253+
%Docstring
254+
Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.
255+
%End
256+
257+
bool forceVectorOutput;
258+
%Docstring
259+
Set to true to force vector object exports, even when the resultant appearance will differ
260+
from the layout. If false, some items may be rasterized in order to maintain their
261+
correct appearance in the output.
262+
263+
This option is mutually exclusive with rasterizeWholeImage.
264+
%End
265+
266+
bool cropToContents;
267+
%Docstring
268+
Set to true if image should be cropped so only parts of the layout
269+
containing items are exported.
270+
%End
271+
272+
QgsMargins cropMargins;
273+
%Docstring
274+
Crop to content margins, in layout units. These margins will be added
275+
to the bounds of the exported layout if cropToContents is true.
276+
%End
277+
278+
bool exportAsLayers;
279+
%Docstring
280+
Set to true to export as a layered SVG file.
281+
Note that this option is considered experimental, and the generated
282+
SVG may differ from the expected appearance of the layout.
283+
%End
284+
285+
QgsLayoutContext::Flags flags;
286+
%Docstring
287+
Layout context flags, which control how the export will be created.
288+
%End
289+
290+
};
291+
292+
ExportResult exportToSvg( const QString &filePath, const QgsLayoutExporter::SvgExportSettings &settings );
293+
%Docstring
294+
Exports the layout as an SVG to the a ``filePath``, using the specified export ``settings``.
295+
239296
Returns a result code indicating whether the export was successful or an
240297
error was encountered.
241298
%End

‎python/core/layout/qgslayoutitempage.sip

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,9 @@ will be set to true if string could be successfully interpreted as a
9191
page orientation.
9292
%End
9393

94-
virtual void attemptResize( const QgsLayoutSize &size, bool includesFrame = false );
94+
virtual QRectF boundingRect() const;
9595

96+
virtual void attemptResize( const QgsLayoutSize &size, bool includesFrame = false );
9697

9798
virtual QgsAbstractLayoutUndoCommand *createCommand( const QString &text, int id, QUndoCommand *parent = 0 ) /Factory/;
9899

‎python/core/qgsfileutils.sip

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ will be returned unchanged.
6565
.. seealso:: :py:func:`extensionsFromFilter()`
6666

6767
.. seealso:: :py:func:`ensureFileNameHasExtension()`
68+
%End
69+
70+
static QString stringToSafeFilename( const QString &string );
71+
%Docstring
72+
Converts a ``string`` to a safe filename, replacing characters which are not safe
73+
for filenames with an '_' character.
74+
75+
This method should be called with file names only, not complete paths.
6876
%End
6977
};
7078

‎src/app/layout/qgslayoutdesignerdialog.cpp

Lines changed: 193 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "qgslayoutitemregistry.h"
2020
#include "qgssettings.h"
2121
#include "qgisapp.h"
22+
#include "qgsfileutils.h"
2223
#include "qgslogger.h"
2324
#include "qgslayout.h"
2425
#include "qgslayoutappmenuprovider.h"
@@ -53,6 +54,7 @@
5354
#include "qgsbusyindicatordialog.h"
5455
#include "qgslayoutundostack.h"
5556
#include "qgslayoutpagecollection.h"
57+
#include "ui_qgssvgexportoptions.h"
5658
#include <QShortcut>
5759
#include <QComboBox>
5860
#include <QLineEdit>
@@ -182,6 +184,7 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla
182184

183185
connect( mActionExportAsImage, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportToRaster );
184186
connect( mActionExportAsPDF, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportToPdf );
187+
connect( mActionExportAsSVG, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportToSvg );
185188

186189
connect( mActionShowGrid, &QAction::triggered, this, &QgsLayoutDesignerDialog::showGrid );
187190
connect( mActionSnapGrid, &QAction::triggered, this, &QgsLayoutDesignerDialog::snapToGrid );
@@ -1474,7 +1477,7 @@ void QgsLayoutDesignerDialog::exportToRaster()
14741477
QgsAtlasComposition *atlasMap = &mComposition->atlasComposition();
14751478
#endif
14761479

1477-
QString outputFileName;
1480+
QString outputFileName = QgsFileUtils::stringToSafeFilename( mLayout->name() );
14781481
#if 0 //TODO
14791482
if ( atlasMap->enabled() && mComposition->atlasMode() == QgsComposition::PreviewAtlas )
14801483
{
@@ -1539,6 +1542,8 @@ void QgsLayoutDesignerDialog::exportToRaster()
15391542
break;
15401543

15411544
case QgsLayoutExporter::PrintError:
1545+
case QgsLayoutExporter::SvgLayerError:
1546+
// no meaning for raster exports, will not be encountered
15421547
break;
15431548

15441549
case QgsLayoutExporter::FileError:
@@ -1594,7 +1599,7 @@ void QgsLayoutDesignerDialog::exportToPdf()
15941599
else
15951600
{
15961601
#endif
1597-
outputFileName = file.path();
1602+
outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mLayout->name() ) + QStringLiteral( ".pdf" );
15981603
#if 0 //TODO
15991604
}
16001605
#endif
@@ -1665,12 +1670,170 @@ void QgsLayoutDesignerDialog::exportToPdf()
16651670
"Please try a lower resolution or a smaller paper size." ),
16661671
QMessageBox::Ok, QMessageBox::Ok );
16671672
break;
1673+
1674+
case QgsLayoutExporter::SvgLayerError:
1675+
// no meaning for PDF exports, will not be encountered
1676+
break;
16681677
}
16691678

16701679
mView->setPaintingEnabled( true );
16711680
QApplication::restoreOverrideCursor();
16721681
}
16731682

1683+
void QgsLayoutDesignerDialog::exportToSvg()
1684+
{
1685+
if ( containsWmsLayers() )
1686+
{
1687+
showWmsPrintingWarning();
1688+
}
1689+
1690+
showSvgExportWarning();
1691+
1692+
QgsSettings settings;
1693+
QString lastUsedFile = settings.value( QStringLiteral( "UI/lastSaveAsSvgFile" ), QStringLiteral( "qgis.svg" ) ).toString();
1694+
QFileInfo file( lastUsedFile );
1695+
QString outputFileName = QgsFileUtils::stringToSafeFilename( mLayout->name() );
1696+
1697+
#if 0// TODO
1698+
if ( hasAnAtlas && !atlasOnASingleFile &&
1699+
( mode == QgsComposer::Atlas || mComposition->atlasMode() == QgsComposition::PreviewAtlas ) )
1700+
{
1701+
outputFileName = QDir( file.path() ).filePath( atlasMap->currentFilename() ) + ".pdf";
1702+
}
1703+
else
1704+
{
1705+
#endif
1706+
outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mLayout->name() ) + QStringLiteral( ".svg" );
1707+
#if 0 //TODO
1708+
}
1709+
#endif
1710+
1711+
#ifdef Q_OS_MAC
1712+
QgisApp::instance()->activateWindow();
1713+
this->raise();
1714+
#endif
1715+
outputFileName = QFileDialog::getSaveFileName(
1716+
this,
1717+
tr( "Export to SVG" ),
1718+
outputFileName,
1719+
tr( "SVG Format" ) + " (*.svg *.SVG)" );
1720+
this->activateWindow();
1721+
if ( outputFileName.isEmpty() )
1722+
{
1723+
return;
1724+
}
1725+
1726+
if ( !outputFileName.endsWith( QLatin1String( ".svg" ), Qt::CaseInsensitive ) )
1727+
{
1728+
outputFileName += QLatin1String( ".svg" );
1729+
}
1730+
1731+
settings.setValue( QStringLiteral( "UI/lastSaveAsSvgFile" ), outputFileName );
1732+
1733+
bool groupLayers = false;
1734+
bool prevSettingLabelsAsOutlines = mLayout->project()->readBoolEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), true );
1735+
bool clipToContent = false;
1736+
double marginTop = 0.0;
1737+
double marginRight = 0.0;
1738+
double marginBottom = 0.0;
1739+
double marginLeft = 0.0;
1740+
bool previousForceVector = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool();
1741+
1742+
// open options dialog
1743+
QDialog dialog;
1744+
Ui::QgsSvgExportOptionsDialog options;
1745+
options.setupUi( &dialog );
1746+
options.chkTextAsOutline->setChecked( prevSettingLabelsAsOutlines );
1747+
options.chkMapLayersAsGroup->setChecked( mLayout->customProperty( QStringLiteral( "svgGroupLayers" ), false ).toBool() );
1748+
options.mClipToContentGroupBox->setChecked( mLayout->customProperty( QStringLiteral( "svgCropToContents" ), false ).toBool() );
1749+
options.mForceVectorCheckBox->setChecked( previousForceVector );
1750+
options.mTopMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginTop" ), 0 ).toInt() );
1751+
options.mRightMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginRight" ), 0 ).toInt() );
1752+
options.mBottomMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginBottom" ), 0 ).toInt() );
1753+
options.mLeftMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginLeft" ), 0 ).toInt() );
1754+
1755+
if ( dialog.exec() != QDialog::Accepted )
1756+
return;
1757+
1758+
groupLayers = options.chkMapLayersAsGroup->isChecked();
1759+
clipToContent = options.mClipToContentGroupBox->isChecked();
1760+
marginTop = options.mTopMarginSpinBox->value();
1761+
marginRight = options.mRightMarginSpinBox->value();
1762+
marginBottom = options.mBottomMarginSpinBox->value();
1763+
marginLeft = options.mLeftMarginSpinBox->value();
1764+
1765+
//save dialog settings
1766+
mLayout->setCustomProperty( QStringLiteral( "svgGroupLayers" ), groupLayers );
1767+
mLayout->setCustomProperty( QStringLiteral( "svgCropToContents" ), clipToContent );
1768+
mLayout->setCustomProperty( QStringLiteral( "svgCropMarginTop" ), marginTop );
1769+
mLayout->setCustomProperty( QStringLiteral( "svgCropMarginRight" ), marginRight );
1770+
mLayout->setCustomProperty( QStringLiteral( "svgCropMarginBottom" ), marginBottom );
1771+
mLayout->setCustomProperty( QStringLiteral( "svgCropMarginLeft" ), marginLeft );
1772+
1773+
//temporarily override label draw outlines setting
1774+
mLayout->project()->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), options.chkTextAsOutline->isChecked() );
1775+
1776+
mView->setPaintingEnabled( false );
1777+
QApplication::setOverrideCursor( Qt::BusyCursor );
1778+
1779+
QgsLayoutExporter::SvgExportSettings svgSettings;
1780+
svgSettings.forceVectorOutput = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool();
1781+
svgSettings.cropToContents = clipToContent;
1782+
svgSettings.cropMargins = QgsMargins( marginLeft, marginTop, marginRight, marginBottom );
1783+
svgSettings.forceVectorOutput = options.mForceVectorCheckBox->isChecked();
1784+
svgSettings.exportAsLayers = groupLayers;
1785+
1786+
// force a refresh, to e.g. update data defined properties, tables, etc
1787+
mLayout->refresh();
1788+
1789+
QFileInfo fi( outputFileName );
1790+
QgsLayoutExporter exporter( mLayout );
1791+
switch ( exporter.exportToSvg( outputFileName, svgSettings ) )
1792+
{
1793+
case QgsLayoutExporter::Success:
1794+
{
1795+
mMessageBar->pushMessage( tr( "Export layout" ),
1796+
tr( "Successfully exported layout to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( fi.path() ).toString(), outputFileName ),
1797+
QgsMessageBar::INFO, 0 );
1798+
break;
1799+
}
1800+
1801+
case QgsLayoutExporter::FileError:
1802+
QMessageBox::warning( this, tr( "Export to SVG" ),
1803+
tr( "Cannot write to %1.\n\nThis file may be open in another application." ).arg( outputFileName ),
1804+
QMessageBox::Ok,
1805+
QMessageBox::Ok );
1806+
break;
1807+
1808+
case QgsLayoutExporter::SvgLayerError:
1809+
QMessageBox::warning( this, tr( "Export to SVG" ),
1810+
tr( "Cannot create layered SVG file %1." ).arg( outputFileName ),
1811+
QMessageBox::Ok,
1812+
QMessageBox::Ok );
1813+
break;
1814+
1815+
case QgsLayoutExporter::PrintError:
1816+
QMessageBox::warning( this, tr( "Export to SVG" ),
1817+
tr( "Could not create print device." ),
1818+
QMessageBox::Ok,
1819+
QMessageBox::Ok );
1820+
break;
1821+
1822+
1823+
case QgsLayoutExporter::MemoryError:
1824+
QMessageBox::warning( this, tr( "Memory Allocation Error" ),
1825+
tr( "Exporting the SVG "
1826+
"resulted in a memory overflow.\n\n"
1827+
"Please try a lower resolution or a smaller paper size." ),
1828+
QMessageBox::Ok, QMessageBox::Ok );
1829+
break;
1830+
}
1831+
1832+
mView->setPaintingEnabled( true );
1833+
mLayout->project()->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), prevSettingLabelsAsOutlines );
1834+
QApplication::restoreOverrideCursor();
1835+
}
1836+
16741837
void QgsLayoutDesignerDialog::paste()
16751838
{
16761839
QPointF pt = mView->mapFromGlobal( QCursor::pos() );
@@ -1816,6 +1979,34 @@ void QgsLayoutDesignerDialog::showWmsPrintingWarning()
18161979
}
18171980
}
18181981

1982+
void QgsLayoutDesignerDialog::showSvgExportWarning()
1983+
{
1984+
QgsSettings settings;
1985+
1986+
bool displaySVGWarning = settings.value( QStringLiteral( "/UI/displaySVGWarning" ), true ).toBool();
1987+
1988+
if ( displaySVGWarning )
1989+
{
1990+
QgsMessageViewer m( this );
1991+
m.setWindowTitle( tr( "Export as SVG" ) );
1992+
m.setCheckBoxText( tr( "Don't show this message again" ) );
1993+
m.setCheckBoxState( Qt::Unchecked );
1994+
m.setCheckBoxVisible( true );
1995+
m.setCheckBoxQgsSettingsLabel( QStringLiteral( "/UI/displaySVGWarning" ) );
1996+
m.setMessageAsHtml( tr( "<p>The SVG export function in QGIS has several "
1997+
"problems due to bugs and deficiencies in the " )
1998+
+ tr( "underlying Qt SVG library. In particular, there are problems "
1999+
"with layers not being clipped to the map "
2000+
"bounding box.</p>" )
2001+
+ tr( "If you require a vector-based output file from "
2002+
"QGIS it is suggested that you try exporting "
2003+
"to PDF if the SVG output is not "
2004+
"satisfactory."
2005+
"</p>" ) );
2006+
m.exec();
2007+
}
2008+
}
2009+
18192010
bool QgsLayoutDesignerDialog::requiresRasterization() const
18202011
{
18212012
QList< QgsLayoutItem *> items;

‎src/app/layout/qgslayoutdesignerdialog.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner
284284
void deleteLayout();
285285
void exportToRaster();
286286
void exportToPdf();
287+
void exportToSvg();
287288

288289
private:
289290

@@ -379,6 +380,8 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner
379380
//! Displays a warning because of possible min/max size in WMS
380381
void showWmsPrintingWarning();
381382

383+
void showSvgExportWarning();
384+
382385
//! True if the layout contains advanced effects, such as blend modes
383386
bool requiresRasterization() const;
384387

0 commit comments

Comments
 (0)
Please sign in to comment.