Skip to content

Commit

Permalink
Restore layered svg export option
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed Dec 19, 2017
1 parent d06e127 commit 2007792
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 20 deletions.
8 changes: 8 additions & 0 deletions python/core/layout/qgslayoutexporter.sip
Expand Up @@ -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
Expand Down Expand Up @@ -272,6 +273,13 @@ containing items are exported.
%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;
Expand Down
14 changes: 14 additions & 0 deletions src/app/layout/qgslayoutdesignerdialog.cpp
Expand Up @@ -1542,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:
Expand Down Expand Up @@ -1668,6 +1670,10 @@ 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 );
Expand Down Expand Up @@ -1775,6 +1781,7 @@ void QgsLayoutDesignerDialog::exportToSvg()
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();
Expand All @@ -1798,6 +1805,13 @@ void QgsLayoutDesignerDialog::exportToSvg()
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." ),
Expand Down
193 changes: 175 additions & 18 deletions src/core/layout/qgslayoutexporter.cpp
Expand Up @@ -78,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 )
Expand Down Expand Up @@ -240,19 +273,22 @@ class LayoutContextSettingsRestorer
: mLayout( layout )
, mPreviousDpi( layout->context().dpi() )
, mPreviousFlags( layout->context().flags() )
, mPreviousExportLayer( layout->context().currentExportLayer() )
{
}

~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

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

QSvgGenerator generator;
generator.setTitle( mLayout->project()->title() );
generator.setFileName( fileName );

QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( i );
QRectF bounds;
if ( settings.cropToContents )
{
Expand All @@ -463,7 +496,6 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
}
else
{
QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( i );
bounds = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() );
}

Expand All @@ -476,24 +508,98 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f
//invalid size, skip this page
continue;
}
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 )
if ( settings.exportAsLayers )
{
mErrorFileName = fileName;
return FileError;
}
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;
}
}

if ( settings.cropToContents )
renderRegion( &p, bounds );
QFile out( fileName );
bool openOk = out.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate );
if ( !openOk )
{
mErrorFileName = fileName;
return FileError;
}

out.write( svg.toByteArray() );
}
else
renderPage( &p, i );
{
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();
p.end();
}
}

return Success;
Expand Down Expand Up @@ -622,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 )
Expand Down
14 changes: 13 additions & 1 deletion src/core/layout/qgslayoutexporter.h
Expand Up @@ -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
Expand Down Expand Up @@ -280,6 +281,13 @@ class CORE_EXPORT QgsLayoutExporter
*/
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.
*/
Expand Down Expand Up @@ -347,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;

Expand Down Expand Up @@ -398,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;

};
Expand Down
18 changes: 17 additions & 1 deletion tests/src/python/test_qgslayoutexporter.py
Expand Up @@ -453,14 +453,30 @@ def testExportToSvg(self):
self.assertTrue(os.path.exists(svg_file_path_2))

rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttosvgdpi.png')
dpi = 80
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()
Expand Down

0 comments on commit 2007792

Please sign in to comment.