Skip to content

Commit 613b158

Browse files
committed
Start restoring SVG export
1 parent cbc8570 commit 613b158

File tree

6 files changed

+386
-1
lines changed

6 files changed

+386
-1
lines changed

python/core/layout/qgslayoutexporter.sip

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,55 @@ Layout context flags, which control how the export will be created.
236236
%Docstring
237237
Exports the layout as a PDF to the a ``filePath``, using the specified export ``settings``.
238238

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

src/app/layout/qgslayoutdesignerdialog.cpp

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
#include "qgsbusyindicatordialog.h"
5454
#include "qgslayoutundostack.h"
5555
#include "qgslayoutpagecollection.h"
56+
#include "ui_qgssvgexportoptions.h"
5657
#include <QShortcut>
5758
#include <QComboBox>
5859
#include <QLineEdit>
@@ -182,6 +183,7 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla
182183

183184
connect( mActionExportAsImage, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportToRaster );
184185
connect( mActionExportAsPDF, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportToPdf );
186+
connect( mActionExportAsSVG, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportToSvg );
185187

186188
connect( mActionShowGrid, &QAction::triggered, this, &QgsLayoutDesignerDialog::showGrid );
187189
connect( mActionSnapGrid, &QAction::triggered, this, &QgsLayoutDesignerDialog::snapToGrid );
@@ -1671,6 +1673,152 @@ void QgsLayoutDesignerDialog::exportToPdf()
16711673
QApplication::restoreOverrideCursor();
16721674
}
16731675

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

1967+
void QgsLayoutDesignerDialog::showSvgExportWarning()
1968+
{
1969+
QgsSettings settings;
1970+
1971+
bool displaySVGWarning = settings.value( QStringLiteral( "/UI/displaySVGWarning" ), true ).toBool();
1972+
1973+
if ( displaySVGWarning )
1974+
{
1975+
QgsMessageViewer m( this );
1976+
m.setWindowTitle( tr( "Export as SVG" ) );
1977+
m.setCheckBoxText( tr( "Don't show this message again" ) );
1978+
m.setCheckBoxState( Qt::Unchecked );
1979+
m.setCheckBoxVisible( true );
1980+
m.setCheckBoxQgsSettingsLabel( QStringLiteral( "/UI/displaySVGWarning" ) );
1981+
m.setMessageAsHtml( tr( "<p>The SVG export function in QGIS has several "
1982+
"problems due to bugs and deficiencies in the " )
1983+
+ tr( "underlying Qt SVG library. In particular, there are problems "
1984+
"with layers not being clipped to the map "
1985+
"bounding box.</p>" )
1986+
+ tr( "If you require a vector-based output file from "
1987+
"QGIS it is suggested that you try exporting "
1988+
"to PDF if the SVG output is not "
1989+
"satisfactory."
1990+
"</p>" ) );
1991+
m.exec();
1992+
}
1993+
}
1994+
18191995
bool QgsLayoutDesignerDialog::requiresRasterization() const
18201996
{
18211997
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

src/core/layout/qgslayoutexporter.cpp

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "qgslayoutguidecollection.h"
2424
#include <QImageWriter>
2525
#include <QSize>
26+
#include <QSvgGenerator>
2627

2728
#include "gdal.h"
2829
#include "cpl_conv.h"
@@ -401,6 +402,103 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f
401402
return result;
402403
}
403404

405+
QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &filePath, const QgsLayoutExporter::SvgExportSettings &s )
406+
{
407+
if ( !mLayout )
408+
return PrintError;
409+
410+
SvgExportSettings settings = s;
411+
if ( settings.dpi <= 0 )
412+
settings.dpi = mLayout->context().dpi();
413+
414+
mErrorFileName.clear();
415+
416+
LayoutContextPreviewSettingRestorer restorer( mLayout );
417+
( void )restorer;
418+
LayoutContextSettingsRestorer contextRestorer( mLayout );
419+
( void )contextRestorer;
420+
mLayout->context().setDpi( settings.dpi );
421+
422+
mLayout->context().setFlag( QgsLayoutContext::FlagForceVectorOutput, settings.forceVectorOutput );
423+
424+
QFileInfo fi( filePath );
425+
PageExportDetails pageDetails;
426+
pageDetails.directory = fi.path();
427+
pageDetails.baseName = fi.baseName();
428+
pageDetails.extension = fi.completeSuffix();
429+
430+
double inchesToLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutInches ) );
431+
432+
for ( int i = 0; i < mLayout->pageCollection()->pageCount(); ++i )
433+
{
434+
if ( !mLayout->pageCollection()->shouldExportPage( i ) )
435+
{
436+
continue;
437+
}
438+
439+
pageDetails.page = i;
440+
QString fileName = generateFileName( pageDetails );
441+
442+
QSvgGenerator generator;
443+
generator.setTitle( mLayout->project()->title() );
444+
generator.setFileName( fileName );
445+
446+
QRectF bounds;
447+
if ( settings.cropToContents )
448+
{
449+
if ( mLayout->pageCollection()->pageCount() == 1 )
450+
{
451+
// single page, so include everything
452+
bounds = mLayout->layoutBounds( true );
453+
}
454+
else
455+
{
456+
// multi page, so just clip to items on current page
457+
bounds = mLayout->pageItemBounds( i, true );
458+
}
459+
bounds = bounds.adjusted( -settings.cropMargins.left(),
460+
-settings.cropMargins.top(),
461+
settings.cropMargins.right(),
462+
settings.cropMargins.bottom() );
463+
}
464+
else
465+
{
466+
QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( i );
467+
bounds = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() );
468+
}
469+
470+
//width in pixel
471+
int width = ( int )( bounds.width() * settings.dpi / inchesToLayoutUnits );
472+
//height in pixel
473+
int height = ( int )( bounds.height() * settings.dpi / inchesToLayoutUnits );
474+
if ( width == 0 || height == 0 )
475+
{
476+
//invalid size, skip this page
477+
continue;
478+
}
479+
generator.setSize( QSize( width, height ) );
480+
generator.setViewBox( QRect( 0, 0, width, height ) );
481+
generator.setResolution( settings.dpi );
482+
483+
QPainter p;
484+
bool createOk = p.begin( &generator );
485+
if ( !createOk )
486+
{
487+
mErrorFileName = fileName;
488+
return FileError;
489+
}
490+
491+
if ( settings.cropToContents )
492+
renderRegion( &p, bounds );
493+
else
494+
renderPage( &p, i );
495+
496+
p.end();
497+
}
498+
499+
return Success;
500+
}
501+
404502
void QgsLayoutExporter::preparePrintAsPdf( QPrinter &printer, const QString &filePath )
405503
{
406504
printer.setOutputFileName( filePath );

0 commit comments

Comments
 (0)