Skip to content

Commit 2007792

Browse files
committed
Restore layered svg export option
1 parent d06e127 commit 2007792

File tree

5 files changed

+227
-20
lines changed

5 files changed

+227
-20
lines changed

python/core/layout/qgslayoutexporter.sip

Lines changed: 8 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
@@ -272,6 +273,13 @@ containing items are exported.
272273
%Docstring
273274
Crop to content margins, in layout units. These margins will be added
274275
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.
275283
%End
276284

277285
QgsLayoutContext::Flags flags;

src/app/layout/qgslayoutdesignerdialog.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,6 +1542,8 @@ void QgsLayoutDesignerDialog::exportToRaster()
15421542
break;
15431543

15441544
case QgsLayoutExporter::PrintError:
1545+
case QgsLayoutExporter::SvgLayerError:
1546+
// no meaning for raster exports, will not be encountered
15451547
break;
15461548

15471549
case QgsLayoutExporter::FileError:
@@ -1668,6 +1670,10 @@ void QgsLayoutDesignerDialog::exportToPdf()
16681670
"Please try a lower resolution or a smaller paper size." ),
16691671
QMessageBox::Ok, QMessageBox::Ok );
16701672
break;
1673+
1674+
case QgsLayoutExporter::SvgLayerError:
1675+
// no meaning for PDF exports, will not be encountered
1676+
break;
16711677
}
16721678

16731679
mView->setPaintingEnabled( true );
@@ -1775,6 +1781,7 @@ void QgsLayoutDesignerDialog::exportToSvg()
17751781
svgSettings.cropToContents = clipToContent;
17761782
svgSettings.cropMargins = QgsMargins( marginLeft, marginTop, marginRight, marginBottom );
17771783
svgSettings.forceVectorOutput = options.mForceVectorCheckBox->isChecked();
1784+
svgSettings.exportAsLayers = groupLayers;
17781785

17791786
// force a refresh, to e.g. update data defined properties, tables, etc
17801787
mLayout->refresh();
@@ -1798,6 +1805,13 @@ void QgsLayoutDesignerDialog::exportToSvg()
17981805
QMessageBox::Ok );
17991806
break;
18001807

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+
18011815
case QgsLayoutExporter::PrintError:
18021816
QMessageBox::warning( this, tr( "Export to SVG" ),
18031817
tr( "Could not create print device." ),

src/core/layout/qgslayoutexporter.cpp

Lines changed: 175 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,39 @@ class LayoutGuideHider
7878
QHash< QgsLayoutGuide *, bool > mPrevVisibility;
7979
};
8080

81+
class LayoutItemHider
82+
{
83+
public:
84+
explicit LayoutItemHider( const QList<QGraphicsItem *> &items )
85+
{
86+
for ( QGraphicsItem *item : items )
87+
{
88+
mPrevVisibility[item] = item->isVisible();
89+
item->hide();
90+
}
91+
}
92+
93+
void hideAll()
94+
{
95+
for ( auto it = mPrevVisibility.constBegin(); it != mPrevVisibility.constEnd(); ++it )
96+
{
97+
it.key()->hide();
98+
}
99+
}
100+
101+
~LayoutItemHider()
102+
{
103+
for ( auto it = mPrevVisibility.constBegin(); it != mPrevVisibility.constEnd(); ++it )
104+
{
105+
it.key()->setVisible( it.value() );
106+
}
107+
}
108+
109+
private:
110+
111+
QHash<QGraphicsItem *, bool> mPrevVisibility;
112+
};
113+
81114
///@endcond PRIVATE
82115

83116
QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout )
@@ -240,19 +273,22 @@ class LayoutContextSettingsRestorer
240273
: mLayout( layout )
241274
, mPreviousDpi( layout->context().dpi() )
242275
, mPreviousFlags( layout->context().flags() )
276+
, mPreviousExportLayer( layout->context().currentExportLayer() )
243277
{
244278
}
245279

246280
~LayoutContextSettingsRestorer()
247281
{
248282
mLayout->context().setDpi( mPreviousDpi );
249283
mLayout->context().setFlags( mPreviousFlags );
284+
mLayout->context().setCurrentExportLayer( mPreviousExportLayer );
250285
}
251286

252287
private:
253288
QgsLayout *mLayout = nullptr;
254289
double mPreviousDpi = 0;
255290
QgsLayoutContext::Flags mPreviousFlags = 0;
291+
int mPreviousExportLayer = 0;
256292
};
257293
///@endcond PRIVATE
258294

@@ -439,10 +475,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
439475
pageDetails.page = i;
440476
QString fileName = generateFileName( pageDetails );
441477

442-
QSvgGenerator generator;
443-
generator.setTitle( mLayout->project()->title() );
444-
generator.setFileName( fileName );
445-
478+
QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( i );
446479
QRectF bounds;
447480
if ( settings.cropToContents )
448481
{
@@ -463,7 +496,6 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
463496
}
464497
else
465498
{
466-
QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( i );
467499
bounds = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() );
468500
}
469501

@@ -476,24 +508,98 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
476508
//invalid size, skip this page
477509
continue;
478510
}
479-
generator.setSize( QSize( width, height ) );
480-
generator.setViewBox( QRect( 0, 0, width, height ) );
481-
generator.setResolution( settings.dpi );
482511

483-
QPainter p;
484-
bool createOk = p.begin( &generator );
485-
if ( !createOk )
512+
if ( settings.exportAsLayers )
486513
{
487-
mErrorFileName = fileName;
488-
return FileError;
489-
}
514+
const QRectF paperRect = QRectF( pageItem->pos().x(),
515+
pageItem->pos().y(),
516+
pageItem->rect().width(),
517+
pageItem->rect().height() );
518+
QDomDocument svg;
519+
QDomNode svgDocRoot;
520+
const QList<QGraphicsItem *> items = mLayout->items( paperRect,
521+
Qt::IntersectsItemBoundingRect,
522+
Qt::AscendingOrder );
523+
524+
LayoutItemHider itemHider( items );
525+
( void )itemHider;
526+
527+
int layoutItemLayerIdx = 0;
528+
auto it = items.constBegin();
529+
for ( unsigned svgLayerId = 1; it != items.constEnd(); ++svgLayerId )
530+
{
531+
itemHider.hideAll();
532+
QgsLayoutItem *layoutItem = dynamic_cast<QgsLayoutItem *>( *it );
533+
QString layerName = QObject::tr( "Layer %1" ).arg( svgLayerId );
534+
if ( layoutItem && layoutItem->numberExportLayers() > 0 )
535+
{
536+
layoutItem->show();
537+
mLayout->context().setCurrentExportLayer( layoutItemLayerIdx );
538+
++layoutItemLayerIdx;
539+
}
540+
else
541+
{
542+
// show all items until the next item that renders on a separate layer
543+
for ( ; it != items.constEnd(); ++it )
544+
{
545+
layoutItem = dynamic_cast<QgsLayoutItem *>( *it );
546+
if ( layoutItem && layoutItem->numberExportLayers() > 0 )
547+
{
548+
break;
549+
}
550+
else
551+
{
552+
( *it )->show();
553+
}
554+
}
555+
}
556+
557+
ExportResult result = renderToLayeredSvg( settings, width, height, i, bounds, fileName, svgLayerId, layerName, svg, svgDocRoot );
558+
if ( result != Success )
559+
return result;
560+
561+
if ( layoutItem && layoutItem->numberExportLayers() > 0 && layoutItem->numberExportLayers() == layoutItemLayerIdx ) // restore and pass to next item
562+
{
563+
mLayout->context().setCurrentExportLayer( -1 );
564+
layoutItemLayerIdx = 0;
565+
++it;
566+
}
567+
}
490568

491-
if ( settings.cropToContents )
492-
renderRegion( &p, bounds );
569+
QFile out( fileName );
570+
bool openOk = out.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate );
571+
if ( !openOk )
572+
{
573+
mErrorFileName = fileName;
574+
return FileError;
575+
}
576+
577+
out.write( svg.toByteArray() );
578+
}
493579
else
494-
renderPage( &p, i );
580+
{
581+
QSvgGenerator generator;
582+
generator.setTitle( mLayout->project()->title() );
583+
generator.setFileName( fileName );
584+
generator.setSize( QSize( width, height ) );
585+
generator.setViewBox( QRect( 0, 0, width, height ) );
586+
generator.setResolution( settings.dpi );
587+
588+
QPainter p;
589+
bool createOk = p.begin( &generator );
590+
if ( !createOk )
591+
{
592+
mErrorFileName = fileName;
593+
return FileError;
594+
}
595+
596+
if ( settings.cropToContents )
597+
renderRegion( &p, bounds );
598+
else
599+
renderPage( &p, i );
495600

496-
p.end();
601+
p.end();
602+
}
497603
}
498604

499605
return Success;
@@ -622,6 +728,57 @@ void QgsLayoutExporter::updatePrinterPageSize( QPrinter &printer, int page )
622728
printer.setPaperSize( pageSizeMM.toQSizeF(), QPrinter::Millimeter );
623729
}
624730

731+
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
732+
{
733+
QBuffer svgBuffer;
734+
{
735+
QSvgGenerator generator;
736+
generator.setTitle( mLayout->name() );
737+
generator.setOutputDevice( &svgBuffer );
738+
generator.setSize( QSize( width, height ) );
739+
generator.setViewBox( QRect( 0, 0, width, height ) );
740+
generator.setResolution( settings.dpi ); //because the rendering is done in mm, convert the dpi
741+
742+
QPainter svgPainter( &generator );
743+
if ( settings.cropToContents )
744+
renderRegion( &svgPainter, bounds );
745+
else
746+
renderPage( &svgPainter, page );
747+
}
748+
749+
// post-process svg output to create groups in a single svg file
750+
// we create inkscape layers since it's nice and clean and free
751+
// and fully svg compatible
752+
{
753+
svgBuffer.close();
754+
svgBuffer.open( QIODevice::ReadOnly );
755+
QDomDocument doc;
756+
QString errorMsg;
757+
int errorLine;
758+
if ( ! doc.setContent( &svgBuffer, false, &errorMsg, &errorLine ) )
759+
{
760+
mErrorFileName = filename;
761+
return SvgLayerError;
762+
}
763+
if ( 1 == svgLayerId )
764+
{
765+
svg = QDomDocument( doc.doctype() );
766+
svg.appendChild( svg.importNode( doc.firstChild(), false ) );
767+
svgDocRoot = svg.importNode( doc.elementsByTagName( QStringLiteral( "svg" ) ).at( 0 ), false );
768+
svgDocRoot.toElement().setAttribute( QStringLiteral( "xmlns:inkscape" ), QStringLiteral( "http://www.inkscape.org/namespaces/inkscape" ) );
769+
svg.appendChild( svgDocRoot );
770+
}
771+
QDomNode mainGroup = svg.importNode( doc.elementsByTagName( QStringLiteral( "g" ) ).at( 0 ), true );
772+
mainGroup.toElement().setAttribute( QStringLiteral( "id" ), layerName );
773+
mainGroup.toElement().setAttribute( QStringLiteral( "inkscape:label" ), layerName );
774+
mainGroup.toElement().setAttribute( QStringLiteral( "inkscape:groupmode" ), QStringLiteral( "layer" ) );
775+
QDomNode defs = svg.importNode( doc.elementsByTagName( QStringLiteral( "defs" ) ).at( 0 ), true );
776+
svgDocRoot.appendChild( defs );
777+
svgDocRoot.appendChild( mainGroup );
778+
}
779+
return Success;
780+
}
781+
625782
std::unique_ptr<double[]> QgsLayoutExporter::computeGeoTransform( const QgsLayoutItemMap *map, const QRectF &region, double dpi ) const
626783
{
627784
if ( !map )

src/core/layout/qgslayoutexporter.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ class CORE_EXPORT QgsLayoutExporter
132132
MemoryError, //!< Unable to allocate memory required to export
133133
FileError, //!< Could not write to destination file, likely due to a lock held by another application
134134
PrintError, //!< Could not start printing to destination device
135+
SvgLayerError, //!< Could not create layered SVG file
135136
};
136137

137138
//! Contains settings relating to exporting layouts to raster images
@@ -280,6 +281,13 @@ class CORE_EXPORT QgsLayoutExporter
280281
*/
281282
QgsMargins cropMargins;
282283

284+
/**
285+
* Set to true to export as a layered SVG file.
286+
* Note that this option is considered experimental, and the generated
287+
* SVG may differ from the expected appearance of the layout.
288+
*/
289+
bool exportAsLayers = false;
290+
283291
/**
284292
* Layout context flags, which control how the export will be created.
285293
*/
@@ -347,7 +355,7 @@ class CORE_EXPORT QgsLayoutExporter
347355

348356
QPointer< QgsLayout > mLayout;
349357

350-
QString mErrorFileName;
358+
mutable QString mErrorFileName;
351359

352360
QImage createImage( const ImageExportSettings &settings, int page, QRectF &bounds, bool &skipPage ) const;
353361

@@ -398,6 +406,10 @@ class CORE_EXPORT QgsLayoutExporter
398406

399407
void updatePrinterPageSize( QPrinter &printer, int page );
400408

409+
ExportResult renderToLayeredSvg( const SvgExportSettings &settings, double width, double height, int page, QRectF bounds,
410+
const QString &filename, int svgLayerId, const QString &layerName,
411+
QDomDocument &svg, QDomNode &svgDocRoot ) const;
412+
401413
friend class TestQgsLayout;
402414

403415
};

tests/src/python/test_qgslayoutexporter.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,14 +453,30 @@ def testExportToSvg(self):
453453
self.assertTrue(os.path.exists(svg_file_path_2))
454454

455455
rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttosvgdpi.png')
456-
dpi = 80
457456
svgToPng(svg_file_path, rendered_page_1, width=936)
458457
rendered_page_2 = os.path.join(self.basetestpath, 'test_exporttosvgdpi2.png')
459458
svgToPng(svg_file_path_2, rendered_page_2, width=467)
460459

461460
self.assertTrue(self.checkImage('exporttosvgdpi_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
462461
self.assertTrue(self.checkImage('exporttosvgdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
463462

463+
# layered
464+
settings.exportAsLayers = True
465+
466+
svg_file_path = os.path.join(self.basetestpath, 'test_exporttosvglayered.svg')
467+
svg_file_path_2 = os.path.join(self.basetestpath, 'test_exporttosvglayered_2.svg')
468+
self.assertEqual(exporter.exportToSvg(svg_file_path, settings), QgsLayoutExporter.Success)
469+
self.assertTrue(os.path.exists(svg_file_path))
470+
self.assertTrue(os.path.exists(svg_file_path_2))
471+
472+
rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttosvglayered.png')
473+
svgToPng(svg_file_path, rendered_page_1, width=936)
474+
rendered_page_2 = os.path.join(self.basetestpath, 'test_exporttosvglayered2.png')
475+
svgToPng(svg_file_path_2, rendered_page_2, width=467)
476+
477+
self.assertTrue(self.checkImage('exporttosvglayered_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
478+
self.assertTrue(self.checkImage('exporttosvglayered_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
479+
464480
def testExportWorldFile(self):
465481
l = QgsLayout(QgsProject.instance())
466482
l.initializeDefaults()

0 commit comments

Comments
 (0)