Skip to content

Commit aa7986f

Browse files
committedDec 17, 2017
Implement methods for exporting layouts as raster, add tests
1 parent a56c937 commit aa7986f

File tree

16 files changed

+743
-6
lines changed

16 files changed

+743
-6
lines changed
 

‎python/core/layout/qgslayoutexporter.sip

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class QgsLayoutExporter
2626
Constructor for QgsLayoutExporter, for the specified ``layout``.
2727
%End
2828

29-
void renderPage( QPainter *painter, int page );
29+
void renderPage( QPainter *painter, int page ) const;
3030
%Docstring
3131
Renders a full page to a destination ``painter``.
3232

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

39-
void renderRegion( QPainter *painter, const QRectF &region );
39+
QImage renderPageToImage( int page, QSize imageSize = QSize(), double dpi = 0 ) const;
40+
%Docstring
41+
Renders a full page to an image.
42+
43+
The ``page`` argument specifies the page number to render. Page numbers
44+
are 0 based, such that the first page in a layout is page 0.
45+
46+
The optional ``imageSize`` parameter can specify the target image size, in pixels.
47+
It is the caller's responsibility to ensure that the ratio of the target image size
48+
matches the ratio of the corresponding layout page size.
49+
50+
The ``dpi`` parameter is an optional dpi override. Set to 0 to use the default layout print
51+
resolution. This parameter has no effect if ``imageSize`` is specified.
52+
53+
Returns the rendered image, or a null QImage if the image does not fit into available memory.
54+
55+
.. seealso:: :py:func:`renderPage()`
56+
.. seealso:: :py:func:`renderRegionToImage()`
57+
:rtype: QImage
58+
%End
59+
60+
void renderRegion( QPainter *painter, const QRectF &region ) const;
4061
%Docstring
4162
Renders a ``region`` from the layout to a ``painter``. This method can be used
4263
to render sections of pages rather than full pages.
4364

4465
.. seealso:: :py:func:`renderPage()`
66+
.. seealso:: :py:func:`renderRegionToImage()`
67+
%End
68+
69+
QImage renderRegionToImage( const QRectF &region, QSize imageSize = QSize(), double dpi = 0 ) const;
70+
%Docstring
71+
Renders a ``region`` of the layout to an image. This method can be used to render
72+
sections of pages rather than full pages.
73+
74+
The optional ``imageSize`` parameter can specify the target image size, in pixels.
75+
It is the caller's responsibility to ensure that the ratio of the target image size
76+
matches the ratio of the specified region of the layout.
77+
78+
The ``dpi`` parameter is an optional dpi override. Set to 0 to use the default layout print
79+
resolution. This parameter has no effect if ``imageSize`` is specified.
80+
81+
Returns the rendered image, or a null QImage if the image does not fit into available memory.
82+
83+
.. seealso:: :py:func:`renderRegion()`
84+
.. seealso:: :py:func:`renderPageToImage()`
85+
:rtype: QImage
86+
%End
87+
88+
89+
enum ExportResult
90+
{
91+
Success,
92+
MemoryError,
93+
FileError,
94+
};
95+
96+
struct ImageExportSettings
97+
{
98+
double dpi;
99+
%Docstring
100+
Resolution to export layout at
101+
%End
102+
103+
QSize imageSize;
104+
%Docstring
105+
Manual size in pixels for output image. If imageSize is not
106+
set then it will be automatically calculated based on the
107+
output dpi and layout size.
108+
109+
If cropToContents is true then imageSize has no effect.
110+
111+
Be careful when specifying manual sizes if pages in the layout
112+
have differing sizes! It's likely not going to give a reasonable
113+
output in this case, and the automatic dpi-based image size should be
114+
used instead.
115+
%End
116+
117+
bool cropToContents;
118+
%Docstring
119+
Set to true if image should be cropped so only parts of the layout
120+
containing items are exported.
121+
%End
122+
123+
QgsMargins cropMargins;
124+
%Docstring
125+
Crop to content margins, in pixels. These margins will be added
126+
to the bounds of the exported layout if cropToContents is true.
127+
%End
128+
129+
QList< int > pages;
130+
%Docstring
131+
List of specific pages to export, or an empty list to
132+
export all pages.
133+
134+
Page numbers are 0 index based, so the first page in the
135+
layout corresponds to page 0.
136+
%End
137+
138+
bool generateWorldFile;
139+
%Docstring
140+
Set to true to generate an external world file alonside
141+
exported images.
142+
%End
143+
144+
};
145+
146+
ExportResult exportToImage( const QString &filePath, const ImageExportSettings &settings );
147+
%Docstring
148+
Exports the layout to the a ``filePath``, using the specified export ``settings``.
149+
150+
If the layout is a multi-page layout, then filenames for each page will automatically
151+
be generated by appending "_1", "_2", etc to the image file's base name.
152+
153+
Returns a result code indicating whether the export was successful or an
154+
error was encountered.
155+
:rtype: ExportResult
45156
%End
46157

47158
};

‎src/core/layout/qgslayoutexporter.cpp

Lines changed: 226 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@
1818
#include "qgslayout.h"
1919
#include "qgslayoutitemmap.h"
2020
#include "qgslayoutpagecollection.h"
21+
#include <QImageWriter>
22+
#include <QSize>
2123

2224
QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout )
2325
: mLayout( layout )
2426
{
2527

2628
}
2729

28-
void QgsLayoutExporter::renderPage( QPainter *painter, int page )
30+
void QgsLayoutExporter::renderPage( QPainter *painter, int page ) const
2931
{
3032
if ( !mLayout )
3133
return;
@@ -45,7 +47,27 @@ void QgsLayoutExporter::renderPage( QPainter *painter, int page )
4547
renderRegion( painter, paperRect );
4648
}
4749

48-
void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF &region )
50+
QImage QgsLayoutExporter::renderPageToImage( int page, QSize imageSize, double dpi ) const
51+
{
52+
if ( !mLayout )
53+
return QImage();
54+
55+
if ( mLayout->pageCollection()->pageCount() <= page || page < 0 )
56+
{
57+
return QImage();
58+
}
59+
60+
QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( page );
61+
if ( !pageItem )
62+
{
63+
return QImage();
64+
}
65+
66+
QRectF paperRect = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() );
67+
return renderRegionToImage( paperRect, imageSize, dpi );
68+
}
69+
70+
void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF &region ) const
4971
{
5072
QPaintDevice *paintDevice = painter->device();
5173
if ( !paintDevice || !mLayout )
@@ -68,3 +90,205 @@ void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF &region )
6890
mLayout->context().mIsPreviewRender = true;
6991
}
7092

93+
QImage QgsLayoutExporter::renderRegionToImage( const QRectF &region, QSize imageSize, double dpi ) const
94+
{
95+
double resolution = mLayout->context().dpi();
96+
double oneInchInLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutInches ) );
97+
if ( imageSize.isValid() )
98+
{
99+
//output size in pixels specified, calculate resolution using average of
100+
//derived x/y dpi
101+
resolution = ( imageSize.width() / region.width()
102+
+ imageSize.height() / region.height() ) / 2.0 * oneInchInLayoutUnits;
103+
}
104+
else if ( dpi > 0 )
105+
{
106+
//dpi overridden by function parameters
107+
resolution = dpi;
108+
}
109+
110+
int width = imageSize.isValid() ? imageSize.width()
111+
: static_cast< int >( resolution * region.width() / oneInchInLayoutUnits );
112+
int height = imageSize.isValid() ? imageSize.height()
113+
: static_cast< int >( resolution * region.height() / oneInchInLayoutUnits );
114+
115+
QImage image( QSize( width, height ), QImage::Format_ARGB32 );
116+
if ( !image.isNull() )
117+
{
118+
image.setDotsPerMeterX( resolution / 25.4 * 1000 );
119+
image.setDotsPerMeterY( resolution / 25.4 * 1000 );
120+
image.fill( Qt::transparent );
121+
QPainter imagePainter( &image );
122+
renderRegion( &imagePainter, region );
123+
if ( !imagePainter.isActive() )
124+
return QImage();
125+
}
126+
127+
return image;
128+
}
129+
130+
///@cond PRIVATE
131+
class LayoutDpiRestorer
132+
{
133+
public:
134+
135+
LayoutDpiRestorer( QgsLayout *layout )
136+
: mLayout( layout )
137+
, mPreviousSetting( layout->context().dpi() )
138+
{
139+
}
140+
141+
~LayoutDpiRestorer()
142+
{
143+
mLayout->context().setDpi( mPreviousSetting );
144+
}
145+
146+
private:
147+
QgsLayout *mLayout = nullptr;
148+
double mPreviousSetting = 0;
149+
};
150+
///@endcond PRIVATE
151+
QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString &filePath, const QgsLayoutExporter::ImageExportSettings &settings )
152+
{
153+
int worldFilePageNo = -1;
154+
if ( mLayout->referenceMap() )
155+
{
156+
worldFilePageNo = mLayout->referenceMap()->page();
157+
}
158+
159+
QFileInfo fi( filePath );
160+
QString path = fi.path();
161+
QString baseName = fi.baseName();
162+
QString extension = fi.completeSuffix();
163+
164+
LayoutDpiRestorer dpiRestorer( mLayout );
165+
( void )dpiRestorer;
166+
mLayout->context().setDpi( settings.dpi );
167+
168+
QList< int > pages;
169+
if ( settings.pages.empty() )
170+
{
171+
for ( int page = 0; page < mLayout->pageCollection()->pageCount(); ++page )
172+
pages << page;
173+
}
174+
else
175+
{
176+
for ( int page : settings.pages )
177+
{
178+
if ( page >= 0 && page < mLayout->pageCollection()->pageCount() )
179+
pages << page;
180+
}
181+
}
182+
183+
for ( int page : qgis::as_const( pages ) )
184+
{
185+
if ( !mLayout->pageCollection()->shouldExportPage( page ) )
186+
{
187+
continue;
188+
}
189+
190+
bool skip = false;
191+
QRectF bounds;
192+
QImage image = createImage( settings, page, bounds, skip );
193+
194+
if ( skip )
195+
continue; // should skip this page, e.g. null size
196+
197+
if ( image.isNull() )
198+
{
199+
return MemoryError;
200+
}
201+
202+
QString outputFilePath = generateFileName( path, baseName, extension, page );
203+
if ( !saveImage( image, outputFilePath, extension ) )
204+
{
205+
return FileError;
206+
}
207+
208+
#if 0 //TODO
209+
if ( page == worldFilePageNo )
210+
{
211+
mLayout->georeferenceOutput( outputFilePath, nullptr, bounds, imageDlg.resolution() );
212+
213+
if ( settings.generateWorldFile )
214+
{
215+
// should generate world file for this page
216+
double a, b, c, d, e, f;
217+
if ( bounds.isValid() )
218+
mLayout->computeWorldFileParameters( bounds, a, b, c, d, e, f );
219+
else
220+
mLayout->computeWorldFileParameters( a, b, c, d, e, f );
221+
222+
QFileInfo fi( outputFilePath );
223+
// build the world file name
224+
QString outputSuffix = fi.suffix();
225+
QString worldFileName = fi.absolutePath() + '/' + fi.baseName() + '.'
226+
+ outputSuffix.at( 0 ) + outputSuffix.at( fi.suffix().size() - 1 ) + 'w';
227+
228+
writeWorldFile( worldFileName, a, b, c, d, e, f );
229+
}
230+
}
231+
#endif
232+
}
233+
return Success;
234+
}
235+
236+
QImage QgsLayoutExporter::createImage( const QgsLayoutExporter::ImageExportSettings &settings, int page, QRectF &bounds, bool &skipPage ) const
237+
{
238+
bounds = QRectF();
239+
skipPage = false;
240+
241+
if ( settings.cropToContents )
242+
{
243+
if ( mLayout->pageCollection()->pageCount() == 1 )
244+
{
245+
// single page, so include everything
246+
bounds = mLayout->layoutBounds( true );
247+
}
248+
else
249+
{
250+
// multi page, so just clip to items on current page
251+
bounds = mLayout->pageItemBounds( page, true );
252+
}
253+
if ( bounds.width() <= 0 || bounds.height() <= 0 )
254+
{
255+
//invalid size, skip page
256+
skipPage = true;
257+
return QImage();
258+
}
259+
260+
double pixelToLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutPixels ) );
261+
bounds = bounds.adjusted( -settings.cropMargins.left() * pixelToLayoutUnits,
262+
-settings.cropMargins.top() * pixelToLayoutUnits,
263+
settings.cropMargins.right() * pixelToLayoutUnits,
264+
settings.cropMargins.bottom() * pixelToLayoutUnits );
265+
return renderRegionToImage( bounds, QSize(), settings.dpi );
266+
}
267+
else
268+
{
269+
return renderPageToImage( page, settings.imageSize, settings.dpi );
270+
}
271+
}
272+
273+
QString QgsLayoutExporter::generateFileName( const QString &path, const QString &baseName, const QString &suffix, int page ) const
274+
{
275+
if ( page == 0 )
276+
{
277+
return path + '/' + baseName + '.' + suffix;
278+
}
279+
else
280+
{
281+
return path + '/' + baseName + '_' + QString::number( page + 1 ) + '.' + suffix;
282+
}
283+
}
284+
285+
bool QgsLayoutExporter::saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat )
286+
{
287+
QImageWriter w( imageFilename, imageFormat.toLocal8Bit().constData() );
288+
if ( imageFormat.compare( QLatin1String( "tiff" ), Qt::CaseInsensitive ) == 0 || imageFormat.compare( QLatin1String( "tif" ), Qt::CaseInsensitive ) == 0 )
289+
{
290+
w.setCompression( 1 ); //use LZW compression
291+
}
292+
return w.write( image );
293+
}
294+

‎src/core/layout/qgslayoutexporter.h

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
#define QGSLAYOUTEXPORTER_H
1818

1919
#include "qgis_core.h"
20+
#include "qgsmargins.h"
2021
#include <QPointer>
22+
#include <QSize>
2123

2224
class QgsLayout;
2325
class QPainter;
@@ -46,19 +48,137 @@ class CORE_EXPORT QgsLayoutExporter
4648
*
4749
* \see renderRect()
4850
*/
49-
void renderPage( QPainter *painter, int page );
51+
void renderPage( QPainter *painter, int page ) const;
52+
53+
/**
54+
* Renders a full page to an image.
55+
*
56+
* The \a page argument specifies the page number to render. Page numbers
57+
* are 0 based, such that the first page in a layout is page 0.
58+
*
59+
* The optional \a imageSize parameter can specify the target image size, in pixels.
60+
* It is the caller's responsibility to ensure that the ratio of the target image size
61+
* matches the ratio of the corresponding layout page size.
62+
*
63+
* The \a dpi parameter is an optional dpi override. Set to 0 to use the default layout print
64+
* resolution. This parameter has no effect if \a imageSize is specified.
65+
*
66+
* Returns the rendered image, or a null QImage if the image does not fit into available memory.
67+
*
68+
* \see renderPage()
69+
* \see renderRegionToImage()
70+
*/
71+
QImage renderPageToImage( int page, QSize imageSize = QSize(), double dpi = 0 ) const;
5072

5173
/**
5274
* Renders a \a region from the layout to a \a painter. This method can be used
5375
* to render sections of pages rather than full pages.
5476
*
5577
* \see renderPage()
78+
* \see renderRegionToImage()
5679
*/
57-
void renderRegion( QPainter *painter, const QRectF &region );
80+
void renderRegion( QPainter *painter, const QRectF &region ) const;
81+
82+
/**
83+
* Renders a \a region of the layout to an image. This method can be used to render
84+
* sections of pages rather than full pages.
85+
*
86+
* The optional \a imageSize parameter can specify the target image size, in pixels.
87+
* It is the caller's responsibility to ensure that the ratio of the target image size
88+
* matches the ratio of the specified region of the layout.
89+
*
90+
* The \a dpi parameter is an optional dpi override. Set to 0 to use the default layout print
91+
* resolution. This parameter has no effect if \a imageSize is specified.
92+
*
93+
* Returns the rendered image, or a null QImage if the image does not fit into available memory.
94+
*
95+
* \see renderRegion()
96+
* \see renderPageToImage()
97+
*/
98+
QImage renderRegionToImage( const QRectF &region, QSize imageSize = QSize(), double dpi = 0 ) const;
99+
100+
101+
//! Result codes for exporting layouts
102+
enum ExportResult
103+
{
104+
Success, //!< Export was successful
105+
MemoryError, //!< Unable to allocate memory required to export
106+
FileError, //!< Could not write to destination file, likely due to a lock held by another application
107+
};
108+
109+
//! Contains settings relating to exporting layouts to raster images
110+
struct ImageExportSettings
111+
{
112+
//! Resolution to export layout at
113+
double dpi;
114+
115+
/**
116+
* Manual size in pixels for output image. If imageSize is not
117+
* set then it will be automatically calculated based on the
118+
* output dpi and layout size.
119+
*
120+
* If cropToContents is true then imageSize has no effect.
121+
*
122+
* Be careful when specifying manual sizes if pages in the layout
123+
* have differing sizes! It's likely not going to give a reasonable
124+
* output in this case, and the automatic dpi-based image size should be
125+
* used instead.
126+
*/
127+
QSize imageSize;
128+
129+
/**
130+
* Set to true if image should be cropped so only parts of the layout
131+
* containing items are exported.
132+
*/
133+
bool cropToContents = false;
134+
135+
/**
136+
* Crop to content margins, in pixels. These margins will be added
137+
* to the bounds of the exported layout if cropToContents is true.
138+
*/
139+
QgsMargins cropMargins;
140+
141+
/**
142+
* List of specific pages to export, or an empty list to
143+
* export all pages.
144+
*
145+
* Page numbers are 0 index based, so the first page in the
146+
* layout corresponds to page 0.
147+
*/
148+
QList< int > pages;
149+
150+
/**
151+
* Set to true to generate an external world file alonside
152+
* exported images.
153+
*/
154+
bool generateWorldFile = false;
155+
156+
};
157+
158+
/**
159+
* Exports the layout to the a \a filePath, using the specified export \a settings.
160+
*
161+
* If the layout is a multi-page layout, then filenames for each page will automatically
162+
* be generated by appending "_1", "_2", etc to the image file's base name.
163+
*
164+
* Returns a result code indicating whether the export was successful or an
165+
* error was encountered.
166+
*/
167+
ExportResult exportToImage( const QString &filePath, const ImageExportSettings &settings );
58168

59169
private:
60170

61171
QPointer< QgsLayout > mLayout;
172+
173+
QImage createImage( const ImageExportSettings &settings, int page, QRectF &bounds, bool &skipPage ) const;
174+
175+
QString generateFileName( const QString &path, const QString &baseName, const QString &suffix, int page ) const;
176+
177+
/**
178+
* Saves an image to a file, possibly using format specific options (e.g. LZW compression for tiff)
179+
*/
180+
static bool saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat );
181+
62182
};
63183

64184
#endif //QGSLAYOUTEXPORTER_H

‎tests/src/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ ADD_PYTHON_TEST(PyQgsLayerTreeMapCanvasBridge test_qgslayertreemapcanvasbridge.p
8383
ADD_PYTHON_TEST(PyQgsLayerTree test_qgslayertree.py)
8484
ADD_PYTHON_TEST(PyQgsLayout test_qgslayout.py)
8585
ADD_PYTHON_TEST(PyQgsLayoutAlign test_qgslayoutaligner.py)
86+
ADD_PYTHON_TEST(PyQgsLayoutExporter test_qgslayoutexporter.py)
8687
ADD_PYTHON_TEST(PyQgsLayoutManager test_qgslayoutmanager.py)
8788
ADD_PYTHON_TEST(PyQgsLayoutPageCollection test_qgslayoutpagecollection.py)
8889
ADD_PYTHON_TEST(PyQgsLayoutView test_qgslayoutview.py)
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
# -*- coding: utf-8 -*-
2+
"""QGIS Unit tests for QgsLayoutExporter
3+
4+
.. note:: This program is free software; you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation; either version 2 of the License, or
7+
(at your option) any later version.
8+
"""
9+
__author__ = 'Nyall Dawson'
10+
__date__ = '11/12/2017'
11+
__copyright__ = 'Copyright 2017, The QGIS Project'
12+
# This will get replaced with a git SHA1 when you do a git archive
13+
__revision__ = '$Format:%H$'
14+
15+
import qgis # NOQA
16+
import sip
17+
import tempfile
18+
import shutil
19+
import os
20+
21+
from qgis.core import (QgsMultiRenderChecker,
22+
QgsLayoutExporter,
23+
QgsLayout,
24+
QgsProject,
25+
QgsMargins,
26+
QgsLayoutItemShape,
27+
QgsLayoutItemPage,
28+
QgsLayoutPoint,
29+
QgsSimpleFillSymbolLayer,
30+
QgsFillSymbol)
31+
from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt
32+
from qgis.PyQt.QtGui import QImage, QPainter
33+
34+
from qgis.testing import start_app, unittest
35+
36+
start_app()
37+
38+
39+
class TestQgsLayoutExporter(unittest.TestCase):
40+
41+
@classmethod
42+
def setUpClass(cls):
43+
"""Run before all tests"""
44+
cls.basetestpath = tempfile.mkdtemp()
45+
cls.dots_per_meter = 96 / 25.4 * 1000
46+
47+
def setUp(self):
48+
self.report = "<h1>Python QgsLayoutExporter Tests</h1>\n"
49+
50+
def tearDown(self):
51+
report_file_path = "%s/qgistest.html" % QDir.tempPath()
52+
with open(report_file_path, 'a') as report_file:
53+
report_file.write(self.report)
54+
55+
def checkImage(self, name, reference_image, rendered_image):
56+
checker = QgsMultiRenderChecker()
57+
checker.setControlPathPrefix("layout_exporter")
58+
checker.setControlName("expected_layoutexporter_" + reference_image)
59+
checker.setRenderedImage(rendered_image)
60+
checker.setColorTolerance(2)
61+
result = checker.runTest(name, 20)
62+
self.report += checker.report()
63+
print((self.report))
64+
return result
65+
66+
def testRenderPage(self):
67+
l = QgsLayout(QgsProject.instance())
68+
l.initializeDefaults()
69+
70+
# add some items
71+
item1 = QgsLayoutItemShape(l)
72+
item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
73+
fill = QgsSimpleFillSymbolLayer()
74+
fill_symbol = QgsFillSymbol()
75+
fill_symbol.changeSymbolLayer(0, fill)
76+
fill.setColor(Qt.green)
77+
fill.setStrokeStyle(Qt.NoPen)
78+
item1.setSymbol(fill_symbol)
79+
l.addItem(item1)
80+
81+
# get width/height, create image and render the composition to it
82+
size = QSize(1122, 794)
83+
output_image = QImage(size, QImage.Format_RGB32)
84+
85+
output_image.setDotsPerMeterX(self.dots_per_meter)
86+
output_image.setDotsPerMeterY(self.dots_per_meter)
87+
QgsMultiRenderChecker.drawBackground(output_image)
88+
painter = QPainter(output_image)
89+
exporter = QgsLayoutExporter(l)
90+
91+
# valid page
92+
exporter.renderPage(painter, 0)
93+
painter.end()
94+
95+
rendered_file_path = os.path.join(self.basetestpath, 'test_renderpage.png')
96+
output_image.save(rendered_file_path, "PNG")
97+
self.assertTrue(self.checkImage('renderpage', 'renderpage', rendered_file_path))
98+
99+
def testRenderPageToImage(self):
100+
l = QgsLayout(QgsProject.instance())
101+
l.initializeDefaults()
102+
103+
# add some items
104+
item1 = QgsLayoutItemShape(l)
105+
item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
106+
fill = QgsSimpleFillSymbolLayer()
107+
fill_symbol = QgsFillSymbol()
108+
fill_symbol.changeSymbolLayer(0, fill)
109+
fill.setColor(Qt.green)
110+
fill.setStrokeStyle(Qt.NoPen)
111+
item1.setSymbol(fill_symbol)
112+
l.addItem(item1)
113+
114+
exporter = QgsLayoutExporter(l)
115+
size = QSize(1122, 794)
116+
117+
# bad page numbers
118+
image = exporter.renderPageToImage(-1, size)
119+
self.assertTrue(image.isNull())
120+
image = exporter.renderPageToImage(1, size)
121+
self.assertTrue(image.isNull())
122+
123+
# good page
124+
image = exporter.renderPageToImage(0, size)
125+
self.assertFalse(image.isNull())
126+
127+
rendered_file_path = os.path.join(self.basetestpath, 'test_rendertoimagepage.png')
128+
image.save(rendered_file_path, "PNG")
129+
self.assertTrue(self.checkImage('rendertoimagepage', 'rendertoimagepage', rendered_file_path))
130+
131+
def testRenderRegion(self):
132+
l = QgsLayout(QgsProject.instance())
133+
l.initializeDefaults()
134+
135+
# add some items
136+
item1 = QgsLayoutItemShape(l)
137+
item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
138+
fill = QgsSimpleFillSymbolLayer()
139+
fill_symbol = QgsFillSymbol()
140+
fill_symbol.changeSymbolLayer(0, fill)
141+
fill.setColor(Qt.green)
142+
fill.setStrokeStyle(Qt.NoPen)
143+
item1.setSymbol(fill_symbol)
144+
l.addItem(item1)
145+
146+
# get width/height, create image and render the composition to it
147+
size = QSize(560, 509)
148+
output_image = QImage(size, QImage.Format_RGB32)
149+
150+
output_image.setDotsPerMeterX(self.dots_per_meter)
151+
output_image.setDotsPerMeterY(self.dots_per_meter)
152+
QgsMultiRenderChecker.drawBackground(output_image)
153+
painter = QPainter(output_image)
154+
exporter = QgsLayoutExporter(l)
155+
156+
exporter.renderRegion(painter, QRectF(5, 10, 110, 100))
157+
painter.end()
158+
159+
rendered_file_path = os.path.join(self.basetestpath, 'test_renderregion.png')
160+
output_image.save(rendered_file_path, "PNG")
161+
self.assertTrue(self.checkImage('renderregion', 'renderregion', rendered_file_path))
162+
163+
def testRenderRegionToImage(self):
164+
l = QgsLayout(QgsProject.instance())
165+
l.initializeDefaults()
166+
167+
# add some items
168+
item1 = QgsLayoutItemShape(l)
169+
item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
170+
fill = QgsSimpleFillSymbolLayer()
171+
fill_symbol = QgsFillSymbol()
172+
fill_symbol.changeSymbolLayer(0, fill)
173+
fill.setColor(Qt.green)
174+
fill.setStrokeStyle(Qt.NoPen)
175+
item1.setSymbol(fill_symbol)
176+
l.addItem(item1)
177+
178+
exporter = QgsLayoutExporter(l)
179+
size = QSize(560, 509)
180+
181+
image = exporter.renderRegionToImage(QRectF(5, 10, 110, 100), size)
182+
self.assertFalse(image.isNull())
183+
184+
rendered_file_path = os.path.join(self.basetestpath, 'test_rendertoimageregionsize.png')
185+
image.save(rendered_file_path, "PNG")
186+
self.assertTrue(self.checkImage('rendertoimageregionsize', 'rendertoimageregionsize', rendered_file_path))
187+
188+
# using layout dpi
189+
l.context().setDpi(40)
190+
image = exporter.renderRegionToImage(QRectF(5, 10, 110, 100))
191+
self.assertFalse(image.isNull())
192+
193+
rendered_file_path = os.path.join(self.basetestpath, 'test_rendertoimageregiondpi.png')
194+
image.save(rendered_file_path, "PNG")
195+
self.assertTrue(self.checkImage('rendertoimageregiondpi', 'rendertoimageregiondpi', rendered_file_path))
196+
197+
# overridding dpi
198+
image = exporter.renderRegionToImage(QRectF(5, 10, 110, 100), QSize(), 80)
199+
self.assertFalse(image.isNull())
200+
201+
rendered_file_path = os.path.join(self.basetestpath, 'test_rendertoimageregionoverridedpi.png')
202+
image.save(rendered_file_path, "PNG")
203+
self.assertTrue(self.checkImage('rendertoimageregionoverridedpi', 'rendertoimageregionoverridedpi', rendered_file_path))
204+
205+
def testExportToImage(self):
206+
l = QgsLayout(QgsProject.instance())
207+
l.initializeDefaults()
208+
209+
# add a second page
210+
page2 = QgsLayoutItemPage(l)
211+
page2.setPageSize('A5')
212+
l.pageCollection().addPage(page2)
213+
214+
# add some items
215+
item1 = QgsLayoutItemShape(l)
216+
item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
217+
fill = QgsSimpleFillSymbolLayer()
218+
fill_symbol = QgsFillSymbol()
219+
fill_symbol.changeSymbolLayer(0, fill)
220+
fill.setColor(Qt.green)
221+
fill.setStrokeStyle(Qt.NoPen)
222+
item1.setSymbol(fill_symbol)
223+
l.addItem(item1)
224+
225+
item2 = QgsLayoutItemShape(l)
226+
item2.attemptSetSceneRect(QRectF(10, 20, 100, 150))
227+
item2.attemptMove(QgsLayoutPoint(10, 20), page=1)
228+
fill = QgsSimpleFillSymbolLayer()
229+
fill_symbol = QgsFillSymbol()
230+
fill_symbol.changeSymbolLayer(0, fill)
231+
fill.setColor(Qt.cyan)
232+
fill.setStrokeStyle(Qt.NoPen)
233+
item2.setSymbol(fill_symbol)
234+
l.addItem(item2)
235+
236+
exporter = QgsLayoutExporter(l)
237+
# setup settings
238+
settings = QgsLayoutExporter.ImageExportSettings()
239+
settings.dpi = 80
240+
241+
rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagedpi.png')
242+
self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
243+
244+
self.assertTrue(self.checkImage('exporttoimagedpi_page1', 'exporttoimagedpi_page1', rendered_file_path))
245+
page2_path = os.path.join(self.basetestpath, 'test_exporttoimagedpi_2.png')
246+
self.assertTrue(self.checkImage('exporttoimagedpi_page2', 'exporttoimagedpi_page2', page2_path))
247+
248+
# crop to contents
249+
settings.cropToContents = True
250+
settings.cropMargins = QgsMargins(10, 20, 30, 40)
251+
252+
rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagecropped.png')
253+
self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
254+
255+
self.assertTrue(self.checkImage('exporttoimagecropped_page1', 'exporttoimagecropped_page1', rendered_file_path))
256+
page2_path = os.path.join(self.basetestpath, 'test_exporttoimagecropped_2.png')
257+
self.assertTrue(self.checkImage('exporttoimagecropped_page2', 'exporttoimagecropped_page2', page2_path))
258+
259+
# specific pages
260+
settings.cropToContents = False
261+
settings.pages = [1]
262+
263+
rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagepages.png')
264+
self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
265+
266+
self.assertFalse(os.path.exists(rendered_file_path))
267+
page2_path = os.path.join(self.basetestpath, 'test_exporttoimagepages_2.png')
268+
self.assertTrue(self.checkImage('exporttoimagedpi_page2', 'exporttoimagedpi_page2', page2_path))
269+
270+
# image size
271+
settings.imageSize = QSize(600, 851)
272+
273+
rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagesize.png')
274+
self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
275+
self.assertFalse(os.path.exists(rendered_file_path))
276+
page2_path = os.path.join(self.basetestpath, 'test_exporttoimagesize_2.png')
277+
self.assertTrue(self.checkImage('exporttoimagesize_page2', 'exporttoimagesize_page2', page2_path))
278+
279+
280+
if __name__ == '__main__':
281+
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.