Skip to content

Commit

Permalink
Implement methods for exporting layouts as raster, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed Dec 17, 2017
1 parent a56c937 commit aa7986f
Show file tree
Hide file tree
Showing 16 changed files with 743 additions and 6 deletions.
115 changes: 113 additions & 2 deletions python/core/layout/qgslayoutexporter.sip
Expand Up @@ -26,7 +26,7 @@ class QgsLayoutExporter
Constructor for QgsLayoutExporter, for the specified ``layout``.
%End

void renderPage( QPainter *painter, int page );
void renderPage( QPainter *painter, int page ) const;
%Docstring
Renders a full page to a destination ``painter``.

Expand All @@ -36,12 +36,123 @@ are 0 based, such that the first page in a layout is page 0.
.. seealso:: :py:func:`renderRect()`
%End

void renderRegion( QPainter *painter, const QRectF &region );
QImage renderPageToImage( int page, QSize imageSize = QSize(), double dpi = 0 ) const;
%Docstring
Renders a full page to an image.

The ``page`` argument specifies the page number to render. Page numbers
are 0 based, such that the first page in a layout is page 0.

The optional ``imageSize`` parameter can specify the target image size, in pixels.
It is the caller's responsibility to ensure that the ratio of the target image size
matches the ratio of the corresponding layout page size.

The ``dpi`` parameter is an optional dpi override. Set to 0 to use the default layout print
resolution. This parameter has no effect if ``imageSize`` is specified.

Returns the rendered image, or a null QImage if the image does not fit into available memory.

.. seealso:: :py:func:`renderPage()`
.. seealso:: :py:func:`renderRegionToImage()`
:rtype: QImage
%End

void renderRegion( QPainter *painter, const QRectF &region ) const;
%Docstring
Renders a ``region`` from the layout to a ``painter``. This method can be used
to render sections of pages rather than full pages.

.. seealso:: :py:func:`renderPage()`
.. seealso:: :py:func:`renderRegionToImage()`
%End

QImage renderRegionToImage( const QRectF &region, QSize imageSize = QSize(), double dpi = 0 ) const;
%Docstring
Renders a ``region`` of the layout to an image. This method can be used to render
sections of pages rather than full pages.

The optional ``imageSize`` parameter can specify the target image size, in pixels.
It is the caller's responsibility to ensure that the ratio of the target image size
matches the ratio of the specified region of the layout.

The ``dpi`` parameter is an optional dpi override. Set to 0 to use the default layout print
resolution. This parameter has no effect if ``imageSize`` is specified.

Returns the rendered image, or a null QImage if the image does not fit into available memory.

.. seealso:: :py:func:`renderRegion()`
.. seealso:: :py:func:`renderPageToImage()`
:rtype: QImage
%End


enum ExportResult
{
Success,
MemoryError,
FileError,
};

struct ImageExportSettings
{
double dpi;
%Docstring
Resolution to export layout at
%End

QSize imageSize;
%Docstring
Manual size in pixels for output image. If imageSize is not
set then it will be automatically calculated based on the
output dpi and layout size.

If cropToContents is true then imageSize has no effect.

Be careful when specifying manual sizes if pages in the layout
have differing sizes! It's likely not going to give a reasonable
output in this case, and the automatic dpi-based image size should be
used instead.
%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 pixels. These margins will be added
to the bounds of the exported layout if cropToContents is true.
%End

QList< int > pages;
%Docstring
List of specific pages to export, or an empty list to
export all pages.

Page numbers are 0 index based, so the first page in the
layout corresponds to page 0.
%End

bool generateWorldFile;
%Docstring
Set to true to generate an external world file alonside
exported images.
%End

};

ExportResult exportToImage( const QString &filePath, const ImageExportSettings &settings );
%Docstring
Exports the layout to the a ``filePath``, using the specified export ``settings``.

If the layout is a multi-page layout, then filenames for each page will automatically
be generated by appending "_1", "_2", etc to the image file's base name.

Returns a result code indicating whether the export was successful or an
error was encountered.
:rtype: ExportResult
%End

};
Expand Down
228 changes: 226 additions & 2 deletions src/core/layout/qgslayoutexporter.cpp
Expand Up @@ -18,14 +18,16 @@
#include "qgslayout.h"
#include "qgslayoutitemmap.h"
#include "qgslayoutpagecollection.h"
#include <QImageWriter>
#include <QSize>

QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout )
: mLayout( layout )
{

}

void QgsLayoutExporter::renderPage( QPainter *painter, int page )
void QgsLayoutExporter::renderPage( QPainter *painter, int page ) const
{
if ( !mLayout )
return;
Expand All @@ -45,7 +47,27 @@ void QgsLayoutExporter::renderPage( QPainter *painter, int page )
renderRegion( painter, paperRect );
}

void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF &region )
QImage QgsLayoutExporter::renderPageToImage( int page, QSize imageSize, double dpi ) const
{
if ( !mLayout )
return QImage();

if ( mLayout->pageCollection()->pageCount() <= page || page < 0 )
{
return QImage();
}

QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( page );
if ( !pageItem )
{
return QImage();
}

QRectF paperRect = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() );
return renderRegionToImage( paperRect, imageSize, dpi );
}

void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF &region ) const
{
QPaintDevice *paintDevice = painter->device();
if ( !paintDevice || !mLayout )
Expand All @@ -68,3 +90,205 @@ void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF &region )
mLayout->context().mIsPreviewRender = true;
}

QImage QgsLayoutExporter::renderRegionToImage( const QRectF &region, QSize imageSize, double dpi ) const
{
double resolution = mLayout->context().dpi();
double oneInchInLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutInches ) );
if ( imageSize.isValid() )
{
//output size in pixels specified, calculate resolution using average of
//derived x/y dpi
resolution = ( imageSize.width() / region.width()
+ imageSize.height() / region.height() ) / 2.0 * oneInchInLayoutUnits;
}
else if ( dpi > 0 )
{
//dpi overridden by function parameters
resolution = dpi;
}

int width = imageSize.isValid() ? imageSize.width()
: static_cast< int >( resolution * region.width() / oneInchInLayoutUnits );
int height = imageSize.isValid() ? imageSize.height()
: static_cast< int >( resolution * region.height() / oneInchInLayoutUnits );

QImage image( QSize( width, height ), QImage::Format_ARGB32 );
if ( !image.isNull() )
{
image.setDotsPerMeterX( resolution / 25.4 * 1000 );
image.setDotsPerMeterY( resolution / 25.4 * 1000 );
image.fill( Qt::transparent );
QPainter imagePainter( &image );
renderRegion( &imagePainter, region );
if ( !imagePainter.isActive() )
return QImage();
}

return image;
}

///@cond PRIVATE
class LayoutDpiRestorer
{
public:

LayoutDpiRestorer( QgsLayout *layout )
: mLayout( layout )
, mPreviousSetting( layout->context().dpi() )
{
}

~LayoutDpiRestorer()
{
mLayout->context().setDpi( mPreviousSetting );
}

private:
QgsLayout *mLayout = nullptr;
double mPreviousSetting = 0;
};
///@endcond PRIVATE
QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString &filePath, const QgsLayoutExporter::ImageExportSettings &settings )
{
int worldFilePageNo = -1;
if ( mLayout->referenceMap() )
{
worldFilePageNo = mLayout->referenceMap()->page();
}

QFileInfo fi( filePath );
QString path = fi.path();
QString baseName = fi.baseName();
QString extension = fi.completeSuffix();

LayoutDpiRestorer dpiRestorer( mLayout );
( void )dpiRestorer;
mLayout->context().setDpi( settings.dpi );

QList< int > pages;
if ( settings.pages.empty() )
{
for ( int page = 0; page < mLayout->pageCollection()->pageCount(); ++page )
pages << page;
}
else
{
for ( int page : settings.pages )
{
if ( page >= 0 && page < mLayout->pageCollection()->pageCount() )
pages << page;
}
}

for ( int page : qgis::as_const( pages ) )
{
if ( !mLayout->pageCollection()->shouldExportPage( page ) )
{
continue;
}

bool skip = false;
QRectF bounds;
QImage image = createImage( settings, page, bounds, skip );

if ( skip )
continue; // should skip this page, e.g. null size

if ( image.isNull() )
{
return MemoryError;
}

QString outputFilePath = generateFileName( path, baseName, extension, page );
if ( !saveImage( image, outputFilePath, extension ) )
{
return FileError;
}

#if 0 //TODO
if ( page == worldFilePageNo )
{
mLayout->georeferenceOutput( outputFilePath, nullptr, bounds, imageDlg.resolution() );

if ( settings.generateWorldFile )
{
// should generate world file for this page
double a, b, c, d, e, f;
if ( bounds.isValid() )
mLayout->computeWorldFileParameters( bounds, a, b, c, d, e, f );
else
mLayout->computeWorldFileParameters( a, b, c, d, e, f );

QFileInfo fi( outputFilePath );
// build the world file name
QString outputSuffix = fi.suffix();
QString worldFileName = fi.absolutePath() + '/' + fi.baseName() + '.'
+ outputSuffix.at( 0 ) + outputSuffix.at( fi.suffix().size() - 1 ) + 'w';

writeWorldFile( worldFileName, a, b, c, d, e, f );
}
}
#endif
}
return Success;
}

QImage QgsLayoutExporter::createImage( const QgsLayoutExporter::ImageExportSettings &settings, int page, QRectF &bounds, bool &skipPage ) const
{
bounds = QRectF();
skipPage = false;

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( page, true );
}
if ( bounds.width() <= 0 || bounds.height() <= 0 )
{
//invalid size, skip page
skipPage = true;
return QImage();
}

double pixelToLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutPixels ) );
bounds = bounds.adjusted( -settings.cropMargins.left() * pixelToLayoutUnits,
-settings.cropMargins.top() * pixelToLayoutUnits,
settings.cropMargins.right() * pixelToLayoutUnits,
settings.cropMargins.bottom() * pixelToLayoutUnits );
return renderRegionToImage( bounds, QSize(), settings.dpi );
}
else
{
return renderPageToImage( page, settings.imageSize, settings.dpi );
}
}

QString QgsLayoutExporter::generateFileName( const QString &path, const QString &baseName, const QString &suffix, int page ) const
{
if ( page == 0 )
{
return path + '/' + baseName + '.' + suffix;
}
else
{
return path + '/' + baseName + '_' + QString::number( page + 1 ) + '.' + suffix;
}
}

bool QgsLayoutExporter::saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat )
{
QImageWriter w( imageFilename, imageFormat.toLocal8Bit().constData() );
if ( imageFormat.compare( QLatin1String( "tiff" ), Qt::CaseInsensitive ) == 0 || imageFormat.compare( QLatin1String( "tif" ), Qt::CaseInsensitive ) == 0 )
{
w.setCompression( 1 ); //use LZW compression
}
return w.write( image );
}

0 comments on commit aa7986f

Please sign in to comment.