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
5it under the terms of the GNU General Public License as published by
6the 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
13import qgis  # NOQA
14from qgis.PyQt import sip
15import tempfile
16import shutil
17import os
18import subprocess
19from xml.dom import minidom
20from osgeo import gdal
21
22from qgis.core import (QgsMultiRenderChecker,
23                       QgsLayoutExporter,
24                       QgsLayout,
25                       QgsProject,
26                       QgsMargins,
27                       QgsLayoutItemShape,
28                       QgsLayoutItemLabel,
29                       QgsLayoutGuide,
30                       QgsRectangle,
31                       QgsLayoutItemPage,
32                       QgsLayoutItemMap,
33                       QgsLayoutItemScaleBar,
34                       QgsLayoutPoint,
35                       QgsLayoutMeasurement,
36                       QgsUnitTypes,
37                       QgsSimpleFillSymbolLayer,
38                       QgsFillSymbol,
39                       QgsVectorLayer,
40                       QgsCoordinateReferenceSystem,
41                       QgsPrintLayout,
42                       QgsSingleSymbolRenderer,
43                       QgsRenderContext,
44                       QgsReport,
45                       QgsPalLayerSettings,
46                       QgsFeature,
47                       QgsGeometry,
48                       QgsPointXY,
49                       QgsVectorLayerSimpleLabeling)
50from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt, QDateTime, QDate, QTime, QTimeZone
51from qgis.PyQt.QtGui import QImage, QPainter
52from qgis.PyQt.QtPrintSupport import QPrinter
53from qgis.PyQt.QtSvg import QSvgRenderer, QSvgGenerator
54
55from qgis.testing import start_app, unittest
56
57from utilities import getExecutablePath, unitTestDataPath
58
59TEST_DATA_DIR = unitTestDataPath()
60
61# PDF-to-image utility
62# look for Poppler w/ Cairo, then muPDF
63# * Poppler w/ Cairo renders correctly
64# * Poppler w/o Cairo does not always correctly render vectors in PDF to image
65# * muPDF renders correctly, but slightly shifts colors
66for util in [
67    'pdftocairo',
68    # 'mudraw',
69]:
70    PDFUTIL = getExecutablePath(util)
71    if PDFUTIL:
72        break
73
74# noinspection PyUnboundLocalVariable
75if not PDFUTIL:
76    raise Exception('PDF-to-image utility not found on PATH: '
77                    'install Poppler (with Cairo)')
78
79
80def pdfToPng(pdf_file_path, rendered_file_path, page, dpi=96):
81    if PDFUTIL.strip().endswith('pdftocairo'):
82        filebase = os.path.join(
83            os.path.dirname(rendered_file_path),
84            os.path.splitext(os.path.basename(rendered_file_path))[0]
85        )
86        call = [
87            PDFUTIL, '-png', '-singlefile', '-r', str(dpi),
88            '-x', '0', '-y', '0', '-f', str(page), '-l', str(page),
89            pdf_file_path, filebase
90        ]
91    elif PDFUTIL.strip().endswith('mudraw'):
92        call = [
93            PDFUTIL, '-c', 'rgba',
94            '-r', str(dpi), '-f', str(page), '-l', str(page),
95            # '-b', '8',
96            '-o', rendered_file_path, pdf_file_path
97        ]
98    else:
99        return False, ''
100
101    print("exportToPdf call: {0}".format(' '.join(call)))
102    try:
103        subprocess.check_call(call)
104    except subprocess.CalledProcessError as e:
105        assert False, ("exportToPdf failed!\n"
106                       "cmd: {0}\n"
107                       "returncode: {1}\n"
108                       "message: {2}".format(e.cmd, e.returncode, e.message))
109
110
111def svgToPng(svg_file_path, rendered_file_path, width):
112    svgr = QSvgRenderer(svg_file_path)
113
114    height = int(width / svgr.viewBoxF().width() * svgr.viewBoxF().height())
115
116    image = QImage(width, height, QImage.Format_ARGB32)
117    image.fill(Qt.transparent)
118
119    p = QPainter(image)
120    p.setRenderHint(QPainter.Antialiasing, False)
121    svgr.render(p)
122    p.end()
123
124    res = image.save(rendered_file_path, 'png')
125    if not res:
126        os.unlink(rendered_file_path)
127
128
129start_app()
130
131
132class TestQgsLayoutExporter(unittest.TestCase):
133
134    @classmethod
135    def setUpClass(cls):
136        """Run before all tests"""
137        cls.basetestpath = tempfile.mkdtemp()
138        cls.dots_per_meter = int(96 / 25.4 * 1000)
139
140    def setUp(self):
141        self.report = "<h1>Python QgsLayoutExporter Tests</h1>\n"
142
143    def tearDown(self):
144        report_file_path = "%s/qgistest.html" % QDir.tempPath()
145        with open(report_file_path, 'a') as report_file:
146            report_file.write(self.report)
147
148    def checkImage(self, name, reference_image, rendered_image, size_tolerance=0):
149        checker = QgsMultiRenderChecker()
150        checker.setControlPathPrefix("layout_exporter")
151        checker.setControlName("expected_layoutexporter_" + reference_image)
152        checker.setRenderedImage(rendered_image)
153        checker.setColorTolerance(2)
154        checker.setSizeTolerance(size_tolerance, size_tolerance)
155        result = checker.runTest(name, 20)
156        self.report += checker.report()
157        print((self.report))
158        return result
159
160    def testRenderPage(self):
161        l = QgsLayout(QgsProject.instance())
162        l.initializeDefaults()
163
164        # add some items
165        item1 = QgsLayoutItemShape(l)
166        item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
167        fill = QgsSimpleFillSymbolLayer()
168        fill_symbol = QgsFillSymbol()
169        fill_symbol.changeSymbolLayer(0, fill)
170        fill.setColor(Qt.green)
171        fill.setStrokeStyle(Qt.NoPen)
172        item1.setSymbol(fill_symbol)
173        l.addItem(item1)
174
175        # get width/height, create image and render the composition to it
176        size = QSize(1122, 794)
177        output_image = QImage(size, QImage.Format_RGB32)
178
179        output_image.setDotsPerMeterX(self.dots_per_meter)
180        output_image.setDotsPerMeterY(self.dots_per_meter)
181        QgsMultiRenderChecker.drawBackground(output_image)
182        painter = QPainter(output_image)
183        exporter = QgsLayoutExporter(l)
184
185        # valid page
186        exporter.renderPage(painter, 0)
187        painter.end()
188
189        rendered_file_path = os.path.join(self.basetestpath, 'test_renderpage.png')
190        output_image.save(rendered_file_path, "PNG")
191        self.assertTrue(self.checkImage('renderpage', 'renderpage', rendered_file_path))
192
193    def testRenderPageToImage(self):
194        l = QgsLayout(QgsProject.instance())
195        l.initializeDefaults()
196
197        # add some items
198        item1 = QgsLayoutItemShape(l)
199        item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
200        fill = QgsSimpleFillSymbolLayer()
201        fill_symbol = QgsFillSymbol()
202        fill_symbol.changeSymbolLayer(0, fill)
203        fill.setColor(Qt.green)
204        fill.setStrokeStyle(Qt.NoPen)
205        item1.setSymbol(fill_symbol)
206        l.addItem(item1)
207
208        exporter = QgsLayoutExporter(l)
209        size = QSize(1122, 794)
210
211        # bad page numbers
212        image = exporter.renderPageToImage(-1, size)
213        self.assertTrue(image.isNull())
214        image = exporter.renderPageToImage(1, size)
215        self.assertTrue(image.isNull())
216
217        # good page
218        image = exporter.renderPageToImage(0, size)
219        self.assertFalse(image.isNull())
220
221        rendered_file_path = os.path.join(self.basetestpath, 'test_rendertoimagepage.png')
222        image.save(rendered_file_path, "PNG")
223        self.assertTrue(self.checkImage('rendertoimagepage', 'rendertoimagepage', rendered_file_path))
224
225    def testRenderRegion(self):
226        l = QgsLayout(QgsProject.instance())
227        l.initializeDefaults()
228
229        # add a guide, to ensure it is not included in export
230        g1 = QgsLayoutGuide(Qt.Horizontal, QgsLayoutMeasurement(15, QgsUnitTypes.LayoutMillimeters), l.pageCollection().page(0))
231        l.guides().addGuide(g1)
232
233        # add some items
234        item1 = QgsLayoutItemShape(l)
235        item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
236        fill = QgsSimpleFillSymbolLayer()
237        fill_symbol = QgsFillSymbol()
238        fill_symbol.changeSymbolLayer(0, fill)
239        fill.setColor(Qt.green)
240        fill.setStrokeStyle(Qt.NoPen)
241        item1.setSymbol(fill_symbol)
242        l.addItem(item1)
243
244        # get width/height, create image and render the composition to it
245        size = QSize(560, 509)
246        output_image = QImage(size, QImage.Format_RGB32)
247
248        output_image.setDotsPerMeterX(self.dots_per_meter)
249        output_image.setDotsPerMeterY(self.dots_per_meter)
250        QgsMultiRenderChecker.drawBackground(output_image)
251        painter = QPainter(output_image)
252        exporter = QgsLayoutExporter(l)
253
254        exporter.renderRegion(painter, QRectF(5, 10, 110, 100))
255        painter.end()
256
257        rendered_file_path = os.path.join(self.basetestpath, 'test_renderregion.png')
258        output_image.save(rendered_file_path, "PNG")
259        self.assertTrue(self.checkImage('renderregion', 'renderregion', rendered_file_path))
260
261    def testRenderRegionToImage(self):
262        l = QgsLayout(QgsProject.instance())
263        l.initializeDefaults()
264
265        # add some items
266        item1 = QgsLayoutItemShape(l)
267        item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
268        fill = QgsSimpleFillSymbolLayer()
269        fill_symbol = QgsFillSymbol()
270        fill_symbol.changeSymbolLayer(0, fill)
271        fill.setColor(Qt.green)
272        fill.setStrokeStyle(Qt.NoPen)
273        item1.setSymbol(fill_symbol)
274        l.addItem(item1)
275
276        exporter = QgsLayoutExporter(l)
277        size = QSize(560, 509)
278
279        image = exporter.renderRegionToImage(QRectF(5, 10, 110, 100), size)
280        self.assertFalse(image.isNull())
281
282        rendered_file_path = os.path.join(self.basetestpath, 'test_rendertoimageregionsize.png')
283        image.save(rendered_file_path, "PNG")
284        self.assertTrue(self.checkImage('rendertoimageregionsize', 'rendertoimageregionsize', rendered_file_path))
285
286        # using layout dpi
287        l.renderContext().setDpi(40)
288        image = exporter.renderRegionToImage(QRectF(5, 10, 110, 100))
289        self.assertFalse(image.isNull())
290
291        rendered_file_path = os.path.join(self.basetestpath, 'test_rendertoimageregiondpi.png')
292        image.save(rendered_file_path, "PNG")
293        self.assertTrue(self.checkImage('rendertoimageregiondpi', 'rendertoimageregiondpi', rendered_file_path))
294
295        # overriding dpi
296        image = exporter.renderRegionToImage(QRectF(5, 10, 110, 100), QSize(), 80)
297        self.assertFalse(image.isNull())
298
299        rendered_file_path = os.path.join(self.basetestpath, 'test_rendertoimageregionoverridedpi.png')
300        image.save(rendered_file_path, "PNG")
301        self.assertTrue(self.checkImage('rendertoimageregionoverridedpi', 'rendertoimageregionoverridedpi', rendered_file_path))
302
303    def testExportToImage(self):
304        md = QgsProject.instance().metadata()
305        md.setTitle('proj title')
306        md.setAuthor('proj author')
307        md.setCreationDateTime(QDateTime(QDate(2011, 5, 3), QTime(9, 4, 5), QTimeZone(36000)))
308        md.setIdentifier('proj identifier')
309        md.setAbstract('proj abstract')
310        md.setKeywords({'kw': ['kw1', 'kw2'], 'KWx': ['kw3', 'kw4']})
311        QgsProject.instance().setMetadata(md)
312        l = QgsLayout(QgsProject.instance())
313        l.initializeDefaults()
314
315        # add a second page
316        page2 = QgsLayoutItemPage(l)
317        page2.setPageSize('A5')
318        l.pageCollection().addPage(page2)
319
320        # add some items
321        item1 = QgsLayoutItemShape(l)
322        item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
323        fill = QgsSimpleFillSymbolLayer()
324        fill_symbol = QgsFillSymbol()
325        fill_symbol.changeSymbolLayer(0, fill)
326        fill.setColor(Qt.green)
327        fill.setStrokeStyle(Qt.NoPen)
328        item1.setSymbol(fill_symbol)
329        l.addItem(item1)
330
331        item2 = QgsLayoutItemShape(l)
332        item2.attemptSetSceneRect(QRectF(10, 20, 100, 150))
333        item2.attemptMove(QgsLayoutPoint(10, 20), page=1)
334        fill = QgsSimpleFillSymbolLayer()
335        fill_symbol = QgsFillSymbol()
336        fill_symbol.changeSymbolLayer(0, fill)
337        fill.setColor(Qt.cyan)
338        fill.setStrokeStyle(Qt.NoPen)
339        item2.setSymbol(fill_symbol)
340        l.addItem(item2)
341
342        exporter = QgsLayoutExporter(l)
343        # setup settings
344        settings = QgsLayoutExporter.ImageExportSettings()
345        settings.dpi = 80
346
347        rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagedpi.png')
348        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
349
350        self.assertTrue(self.checkImage('exporttoimagedpi_page1', 'exporttoimagedpi_page1', rendered_file_path))
351        page2_path = os.path.join(self.basetestpath, 'test_exporttoimagedpi_2.png')
352        self.assertTrue(self.checkImage('exporttoimagedpi_page2', 'exporttoimagedpi_page2', page2_path))
353
354        for f in (rendered_file_path, page2_path):
355            d = gdal.Open(f)
356            metadata = d.GetMetadata()
357            self.assertEqual(metadata['Author'], 'proj author')
358            self.assertEqual(metadata['Created'], '2011-05-03T09:04:05+10:00')
359            self.assertEqual(metadata['Keywords'], 'KWx: kw3,kw4;kw: kw1,kw2')
360            self.assertEqual(metadata['Subject'], 'proj abstract')
361            self.assertEqual(metadata['Title'], 'proj title')
362
363        # crop to contents
364        settings.cropToContents = True
365        settings.cropMargins = QgsMargins(10, 20, 30, 40)
366
367        rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagecropped.png')
368        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
369
370        self.assertTrue(self.checkImage('exporttoimagecropped_page1', 'exporttoimagecropped_page1', rendered_file_path))
371        page2_path = os.path.join(self.basetestpath, 'test_exporttoimagecropped_2.png')
372        self.assertTrue(self.checkImage('exporttoimagecropped_page2', 'exporttoimagecropped_page2', page2_path))
373
374        # specific pages
375        settings.cropToContents = False
376        settings.pages = [1]
377
378        rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagepages.png')
379        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
380
381        self.assertFalse(os.path.exists(rendered_file_path))
382        page2_path = os.path.join(self.basetestpath, 'test_exporttoimagepages_2.png')
383        self.assertTrue(self.checkImage('exporttoimagedpi_page2', 'exporttoimagedpi_page2', page2_path))
384
385        # image size
386        settings.imageSize = QSize(600, 851)
387        rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagesize.png')
388        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
389        self.assertFalse(os.path.exists(rendered_file_path))
390        page2_path = os.path.join(self.basetestpath, 'test_exporttoimagesize_2.png')
391        self.assertTrue(self.checkImage('exporttoimagesize_page2', 'exporttoimagesize_page2', page2_path))
392
393        # image size with incorrect aspect ratio
394        # this can happen as a result of data defined page sizes
395        settings.imageSize = QSize(851, 600)
396        rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagesizebadaspect.png')
397        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
398
399        page2_path = os.path.join(self.basetestpath, 'test_exporttoimagesizebadaspect_2.png')
400        im = QImage(page2_path)
401        self.assertTrue(self.checkImage('exporttoimagesize_badaspect', 'exporttoimagedpi_page2', page2_path), '{}x{}'.format(im.width(), im.height()))
402
403    def testExportToPdf(self):
404        md = QgsProject.instance().metadata()
405        md.setTitle('proj title')
406        md.setAuthor('proj author')
407        md.setCreationDateTime(QDateTime(QDate(2011, 5, 3), QTime(9, 4, 5), QTimeZone(36000)))
408        md.setIdentifier('proj identifier')
409        md.setAbstract('proj abstract')
410        md.setKeywords({'kw': ['kw1', 'kw2'], 'KWx': ['kw3', 'kw4']})
411        QgsProject.instance().setMetadata(md)
412
413        l = QgsLayout(QgsProject.instance())
414        l.initializeDefaults()
415
416        # add a second page
417        page2 = QgsLayoutItemPage(l)
418        page2.setPageSize('A5')
419        l.pageCollection().addPage(page2)
420
421        # add some items
422        item1 = QgsLayoutItemShape(l)
423        item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
424        fill = QgsSimpleFillSymbolLayer()
425        fill_symbol = QgsFillSymbol()
426        fill_symbol.changeSymbolLayer(0, fill)
427        fill.setColor(Qt.green)
428        fill.setStrokeStyle(Qt.NoPen)
429        item1.setSymbol(fill_symbol)
430        l.addItem(item1)
431
432        item2 = QgsLayoutItemShape(l)
433        item2.attemptSetSceneRect(QRectF(10, 20, 100, 150))
434        item2.attemptMove(QgsLayoutPoint(10, 20), page=1)
435        fill = QgsSimpleFillSymbolLayer()
436        fill_symbol = QgsFillSymbol()
437        fill_symbol.changeSymbolLayer(0, fill)
438        fill.setColor(Qt.cyan)
439        fill.setStrokeStyle(Qt.NoPen)
440        item2.setSymbol(fill_symbol)
441        l.addItem(item2)
442
443        exporter = QgsLayoutExporter(l)
444        # setup settings
445        settings = QgsLayoutExporter.PdfExportSettings()
446        settings.dpi = 80
447        settings.rasterizeWholeImage = False
448        settings.forceVectorOutput = False
449        settings.exportMetadata = True
450
451        pdf_file_path = os.path.join(self.basetestpath, 'test_exporttopdfdpi.pdf')
452        self.assertEqual(exporter.exportToPdf(pdf_file_path, settings), QgsLayoutExporter.Success)
453        self.assertTrue(os.path.exists(pdf_file_path))
454
455        rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttopdfdpi.png')
456        dpi = 80
457        pdfToPng(pdf_file_path, rendered_page_1, dpi=dpi, page=1)
458        rendered_page_2 = os.path.join(self.basetestpath, 'test_exporttopdfdpi2.png')
459        pdfToPng(pdf_file_path, rendered_page_2, dpi=dpi, page=2)
460
461        self.assertTrue(self.checkImage('exporttopdfdpi_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
462        self.assertTrue(self.checkImage('exporttopdfdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
463
464        d = gdal.Open(pdf_file_path)
465        metadata = d.GetMetadata()
466        self.assertEqual(metadata['AUTHOR'], 'proj author')
467        self.assertEqual(metadata['CREATION_DATE'], "D:20110503090405+10'0'")
468        self.assertEqual(metadata['KEYWORDS'], 'KWx: kw3,kw4;kw: kw1,kw2')
469        self.assertEqual(metadata['SUBJECT'], 'proj abstract')
470        self.assertEqual(metadata['TITLE'], 'proj title')
471
472    def testExportToPdfGeoreference(self):
473        md = QgsProject.instance().metadata()
474        md.setTitle('proj title')
475        md.setAuthor('proj author')
476        md.setCreationDateTime(QDateTime(QDate(2011, 5, 3), QTime(9, 4, 5), QTimeZone(36000)))
477        md.setIdentifier('proj identifier')
478        md.setAbstract('proj abstract')
479        md.setKeywords({'kw': ['kw1', 'kw2'], 'KWx': ['kw3', 'kw4']})
480        QgsProject.instance().setMetadata(md)
481
482        l = QgsLayout(QgsProject.instance())
483        l.initializeDefaults()
484
485        # add some items
486        map = QgsLayoutItemMap(l)
487        map.attemptSetSceneRect(QRectF(30, 60, 200, 100))
488        extent = QgsRectangle(333218, 1167809, 348781, 1180875)
489        map.setCrs(QgsCoordinateReferenceSystem('EPSG:3148'))
490        map.setExtent(extent)
491        l.addLayoutItem(map)
492
493        exporter = QgsLayoutExporter(l)
494        # setup settings
495        settings = QgsLayoutExporter.PdfExportSettings()
496        settings.dpi = 96
497        settings.rasterizeWholeImage = False
498        settings.forceVectorOutput = False
499        settings.appendGeoreference = True
500        settings.exportMetadata = False
501
502        pdf_file_path = os.path.join(self.basetestpath, 'test_exporttopdf_georeference.pdf')
503        self.assertEqual(exporter.exportToPdf(pdf_file_path, settings), QgsLayoutExporter.Success)
504        self.assertTrue(os.path.exists(pdf_file_path))
505
506        d = gdal.Open(pdf_file_path)
507
508        # check if georeferencing was successful
509        geoTransform = d.GetGeoTransform()
510        self.assertAlmostEqual(geoTransform[0], 330883.5499999996, 4)
511        self.assertAlmostEqual(geoTransform[1], 13.184029109934016, 4)
512        self.assertAlmostEqual(geoTransform[2], 0.0, 4)
513        self.assertAlmostEqual(geoTransform[3], 1185550.768915511, 4)
514        self.assertAlmostEqual(geoTransform[4], 0.0, 4)
515        self.assertAlmostEqual(geoTransform[5], -13.183886222186642, 4)
516
517        # check that the metadata has _not_ been added to the exported PDF
518        metadata = d.GetMetadata()
519        self.assertFalse('AUTHOR' in metadata)
520
521        exporter = QgsLayoutExporter(l)
522        # setup settings
523        settings = QgsLayoutExporter.PdfExportSettings()
524        settings.dpi = 96
525        settings.rasterizeWholeImage = False
526        settings.forceVectorOutput = False
527        settings.appendGeoreference = False
528        settings.exportMetadata = False
529
530        pdf_file_path = os.path.join(self.basetestpath, 'test_exporttopdf_nogeoreference.pdf')
531        self.assertEqual(exporter.exportToPdf(pdf_file_path, settings), QgsLayoutExporter.Success)
532        self.assertTrue(os.path.exists(pdf_file_path))
533
534        d = gdal.Open(pdf_file_path)
535        # check that georeference information has _not_ been added to the exported PDF
536        self.assertEqual(d.GetGeoTransform(), (0.0, 1.0, 0.0, 0.0, 0.0, 1.0))
537
538    def testExportToPdfSkipFirstPage(self):
539        l = QgsLayout(QgsProject.instance())
540        l.initializeDefaults()
541
542        # page 1 is excluded from export
543        page1 = l.pageCollection().page(0)
544        page1.setExcludeFromExports(True)
545
546        # add a second page
547        page2 = QgsLayoutItemPage(l)
548        page2.setPageSize('A5')
549        l.pageCollection().addPage(page2)
550
551        item2 = QgsLayoutItemShape(l)
552        item2.attemptSetSceneRect(QRectF(10, 20, 100, 150))
553        item2.attemptMove(QgsLayoutPoint(10, 20), page=1)
554        fill = QgsSimpleFillSymbolLayer()
555        fill_symbol = QgsFillSymbol()
556        fill_symbol.changeSymbolLayer(0, fill)
557        fill.setColor(Qt.cyan)
558        fill.setStrokeStyle(Qt.NoPen)
559        item2.setSymbol(fill_symbol)
560        l.addItem(item2)
561
562        exporter = QgsLayoutExporter(l)
563        # setup settings
564        settings = QgsLayoutExporter.PdfExportSettings()
565        settings.dpi = 80
566        settings.rasterizeWholeImage = False
567        settings.forceVectorOutput = False
568        settings.exportMetadata = True
569
570        pdf_file_path = os.path.join(self.basetestpath, 'test_exporttopdfdpi_skip_first.pdf')
571        self.assertEqual(exporter.exportToPdf(pdf_file_path, settings), QgsLayoutExporter.Success)
572        self.assertTrue(os.path.exists(pdf_file_path))
573
574        rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttopdfdpi_skip_first.png')
575        dpi = 80
576        pdfToPng(pdf_file_path, rendered_page_1, dpi=dpi, page=1)
577
578        self.assertTrue(self.checkImage('test_exporttopdfdpi_skip_first', 'exporttopdfdpi_page2', rendered_page_1, size_tolerance=1))
579
580    def testExportToSvg(self):
581        md = QgsProject.instance().metadata()
582        md.setTitle('proj title')
583        md.setAuthor('proj author')
584        md.setCreationDateTime(QDateTime(QDate(2011, 5, 3), QTime(9, 4, 5), QTimeZone(36000)))
585        md.setIdentifier('proj identifier')
586        md.setAbstract('proj abstract')
587        md.setKeywords({'kw': ['kw1', 'kw2']})
588        QgsProject.instance().setMetadata(md)
589        l = QgsLayout(QgsProject.instance())
590        l.initializeDefaults()
591
592        # add a second page
593        page2 = QgsLayoutItemPage(l)
594        page2.setPageSize('A5')
595        l.pageCollection().addPage(page2)
596
597        # add some items
598        item1 = QgsLayoutItemShape(l)
599        item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
600        fill = QgsSimpleFillSymbolLayer()
601        fill_symbol = QgsFillSymbol()
602        fill_symbol.changeSymbolLayer(0, fill)
603        fill.setColor(Qt.green)
604        fill.setStrokeStyle(Qt.NoPen)
605        item1.setSymbol(fill_symbol)
606        l.addItem(item1)
607
608        item2 = QgsLayoutItemShape(l)
609        item2.attemptSetSceneRect(QRectF(10, 20, 100, 150))
610        item2.attemptMove(QgsLayoutPoint(10, 20), page=1)
611        fill = QgsSimpleFillSymbolLayer()
612        fill_symbol = QgsFillSymbol()
613        fill_symbol.changeSymbolLayer(0, fill)
614        fill.setColor(Qt.cyan)
615        fill.setStrokeStyle(Qt.NoPen)
616        item2.setSymbol(fill_symbol)
617        l.addItem(item2)
618
619        exporter = QgsLayoutExporter(l)
620        # setup settings
621        settings = QgsLayoutExporter.SvgExportSettings()
622        settings.dpi = 80
623        settings.forceVectorOutput = False
624        settings.exportMetadata = True
625
626        svg_file_path = os.path.join(self.basetestpath, 'test_exporttosvgdpi.svg')
627        svg_file_path_2 = os.path.join(self.basetestpath, 'test_exporttosvgdpi_2.svg')
628        self.assertEqual(exporter.exportToSvg(svg_file_path, settings), QgsLayoutExporter.Success)
629        self.assertTrue(os.path.exists(svg_file_path))
630        self.assertTrue(os.path.exists(svg_file_path_2))
631
632        # metadata
633        def checkMetadata(f, expected):
634            # ideally we'd check the path too - but that's very complex given that
635            # the output from Qt svg generator isn't valid XML, and no Python standard library
636            # xml parser handles invalid xml...
637            self.assertEqual('proj title' in open(f).read(), expected)
638            self.assertEqual('proj author' in open(f).read(), expected)
639            self.assertEqual('proj identifier' in open(f).read(), expected)
640            self.assertEqual('2011-05-03' in open(f).read(), expected)
641            self.assertEqual('proj abstract' in open(f).read(), expected)
642            self.assertEqual('kw1' in open(f).read(), expected)
643            self.assertEqual('kw2' in open(f).read(), expected)
644            self.assertEqual('xmlns:cc="http://creativecommons.org/ns#"' in open(f).read(), expected)
645
646        for f in [svg_file_path, svg_file_path_2]:
647            checkMetadata(f, True)
648
649        rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttosvgdpi.png')
650        svgToPng(svg_file_path, rendered_page_1, width=936)
651        rendered_page_2 = os.path.join(self.basetestpath, 'test_exporttosvgdpi2.png')
652        svgToPng(svg_file_path_2, rendered_page_2, width=467)
653
654        self.assertTrue(self.checkImage('exporttosvgdpi_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
655        self.assertTrue(self.checkImage('exporttosvgdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
656
657        # no metadata
658        settings.exportMetadata = False
659        self.assertEqual(exporter.exportToSvg(svg_file_path, settings), QgsLayoutExporter.Success)
660        for f in [svg_file_path, svg_file_path_2]:
661            checkMetadata(f, False)
662
663        # layered
664        settings.exportAsLayers = True
665        settings.exportMetadata = True
666
667        svg_file_path = os.path.join(self.basetestpath, 'test_exporttosvglayered.svg')
668        svg_file_path_2 = os.path.join(self.basetestpath, 'test_exporttosvglayered_2.svg')
669        self.assertEqual(exporter.exportToSvg(svg_file_path, settings), QgsLayoutExporter.Success)
670        self.assertTrue(os.path.exists(svg_file_path))
671        self.assertTrue(os.path.exists(svg_file_path_2))
672
673        rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttosvglayered.png')
674        svgToPng(svg_file_path, rendered_page_1, width=936)
675        rendered_page_2 = os.path.join(self.basetestpath, 'test_exporttosvglayered2.png')
676        svgToPng(svg_file_path_2, rendered_page_2, width=467)
677
678        self.assertTrue(self.checkImage('exporttosvglayered_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
679        self.assertTrue(self.checkImage('exporttosvglayered_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
680
681        for f in [svg_file_path, svg_file_path_2]:
682            checkMetadata(f, True)
683
684        # layered no metadata
685        settings.exportAsLayers = True
686        settings.exportMetadata = False
687        self.assertEqual(exporter.exportToSvg(svg_file_path, settings), QgsLayoutExporter.Success)
688        for f in [svg_file_path, svg_file_path_2]:
689            checkMetadata(f, False)
690
691    def testExportToSvgTextRenderFormat(self):
692        l = QgsLayout(QgsProject.instance())
693        l.initializeDefaults()
694
695        # add a map and scalebar
696        mapitem = QgsLayoutItemMap(l)
697        mapitem.attemptSetSceneRect(QRectF(110, 120, 200, 250))
698        mapitem.zoomToExtent(QgsRectangle(1, 1, 10, 10))
699        mapitem.setScale(666)  # unlikely to appear in the SVG by accident... unless... oh no! RUN!
700        l.addItem(mapitem)
701
702        item1 = QgsLayoutItemScaleBar(l)
703        item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
704        item1.setLinkedMap(mapitem)
705        item1.setStyle('Numeric')
706        l.addItem(item1)
707
708        exporter = QgsLayoutExporter(l)
709        # setup settings
710        settings = QgsLayoutExporter.SvgExportSettings()
711        settings.dpi = 80
712        settings.forceVectorOutput = False
713        settings.exportMetadata = True
714        settings.textRenderFormat = QgsRenderContext.TextFormatAlwaysText
715
716        svg_file_path = os.path.join(self.basetestpath, 'test_exporttosvgtextformattext.svg')
717        self.assertEqual(exporter.exportToSvg(svg_file_path, settings), QgsLayoutExporter.Success)
718        self.assertTrue(os.path.exists(svg_file_path))
719
720        # expect svg to contain a text object with the scale
721        with open(svg_file_path, 'r') as f:
722            lines = ''.join(f.readlines())
723        self.assertIn('<text', lines)
724        self.assertIn('>1:666<', lines)
725
726        # force use of outlines
727        os.unlink(svg_file_path)
728        settings.textRenderFormat = QgsRenderContext.TextFormatAlwaysOutlines
729        self.assertEqual(exporter.exportToSvg(svg_file_path, settings), QgsLayoutExporter.Success)
730        self.assertTrue(os.path.exists(svg_file_path))
731
732        # expect svg NOT to contain a text object with the scale
733        with open(svg_file_path, 'r') as f:
734            lines = ''.join(f.readlines())
735        self.assertNotIn('<text', lines)
736        self.assertNotIn('>1:666<', lines)
737
738    def testPrint(self):
739        l = QgsLayout(QgsProject.instance())
740        l.initializeDefaults()
741
742        # add a second page
743        page2 = QgsLayoutItemPage(l)
744        page2.setPageSize('A5')
745        l.pageCollection().addPage(page2)
746
747        # add some items
748        item1 = QgsLayoutItemShape(l)
749        item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
750        fill = QgsSimpleFillSymbolLayer()
751        fill_symbol = QgsFillSymbol()
752        fill_symbol.changeSymbolLayer(0, fill)
753        fill.setColor(Qt.green)
754        fill.setStrokeStyle(Qt.NoPen)
755        item1.setSymbol(fill_symbol)
756        l.addItem(item1)
757
758        item2 = QgsLayoutItemShape(l)
759        item2.attemptSetSceneRect(QRectF(10, 20, 100, 150))
760        item2.attemptMove(QgsLayoutPoint(10, 20), page=1)
761        fill = QgsSimpleFillSymbolLayer()
762        fill_symbol = QgsFillSymbol()
763        fill_symbol.changeSymbolLayer(0, fill)
764        fill.setColor(Qt.cyan)
765        fill.setStrokeStyle(Qt.NoPen)
766        item2.setSymbol(fill_symbol)
767        l.addItem(item2)
768
769        exporter = QgsLayoutExporter(l)
770        # setup settings
771        settings = QgsLayoutExporter.PrintExportSettings()
772        settings.dpi = 80
773        settings.rasterizeWholeImage = False
774
775        pdf_file_path = os.path.join(self.basetestpath, 'test_printdpi.pdf')
776        # make a qprinter directed to pdf
777        printer = QPrinter()
778        printer.setOutputFileName(pdf_file_path)
779        printer.setOutputFormat(QPrinter.PdfFormat)
780
781        self.assertEqual(exporter.print(printer, settings), QgsLayoutExporter.Success)
782        self.assertTrue(os.path.exists(pdf_file_path))
783
784        rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttopdfdpi.png')
785        dpi = 80
786        pdfToPng(pdf_file_path, rendered_page_1, dpi=dpi, page=1)
787        rendered_page_2 = os.path.join(self.basetestpath, 'test_exporttopdfdpi2.png')
788        pdfToPng(pdf_file_path, rendered_page_2, dpi=dpi, page=2)
789
790        self.assertTrue(self.checkImage('printdpi_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
791        self.assertTrue(self.checkImage('printdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
792
793    def testExportWorldFile(self):
794        l = QgsLayout(QgsProject.instance())
795        l.initializeDefaults()
796
797        # add some items
798        map = QgsLayoutItemMap(l)
799        map.attemptSetSceneRect(QRectF(30, 60, 200, 100))
800        extent = QgsRectangle(2000, 2800, 2500, 2900)
801        map.setExtent(extent)
802        l.addLayoutItem(map)
803
804        exporter = QgsLayoutExporter(l)
805        # setup settings
806        settings = QgsLayoutExporter.ImageExportSettings()
807        settings.dpi = 80
808        settings.generateWorldFile = False
809
810        rendered_file_path = os.path.join(self.basetestpath, 'test_exportwithworldfile.png')
811        world_file_path = os.path.join(self.basetestpath, 'test_exportwithworldfile.pgw')
812        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
813        self.assertTrue(os.path.exists(rendered_file_path))
814        self.assertFalse(os.path.exists(world_file_path))
815
816        # with world file
817        settings.generateWorldFile = True
818        rendered_file_path = os.path.join(self.basetestpath, 'test_exportwithworldfile.png')
819        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
820        self.assertTrue(os.path.exists(rendered_file_path))
821        self.assertTrue(os.path.exists(world_file_path))
822
823        lines = tuple(open(world_file_path, 'r'))
824        values = [float(f) for f in lines]
825        self.assertAlmostEqual(values[0], 0.794117647059, 2)
826        self.assertAlmostEqual(values[1], 0.0, 2)
827        self.assertAlmostEqual(values[2], 0.0, 2)
828        self.assertAlmostEqual(values[3], -0.794251134644, 2)
829        self.assertAlmostEqual(values[4], 1925.000000000000, 2)
830        self.assertAlmostEqual(values[5], 3050.000000000000, 2)
831
832    def testExcludePagesImage(self):
833        l = QgsLayout(QgsProject.instance())
834        l.initializeDefaults()
835        # add a second page
836        page2 = QgsLayoutItemPage(l)
837        page2.setPageSize('A5')
838        l.pageCollection().addPage(page2)
839
840        exporter = QgsLayoutExporter(l)
841        # setup settings
842        settings = QgsLayoutExporter.ImageExportSettings()
843        settings.dpi = 80
844        settings.generateWorldFile = False
845
846        rendered_file_path = os.path.join(self.basetestpath, 'test_exclude_export.png')
847        details = QgsLayoutExporter.PageExportDetails()
848        details.directory = self.basetestpath
849        details.baseName = 'test_exclude_export'
850        details.extension = 'png'
851        details.page = 0
852
853        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
854        self.assertTrue(os.path.exists(exporter.generateFileName(details)))
855        details.page = 1
856        self.assertTrue(os.path.exists(exporter.generateFileName(details)))
857
858        # exclude a page
859        l.pageCollection().page(0).setExcludeFromExports(True)
860        rendered_file_path = os.path.join(self.basetestpath, 'test_exclude_export_excluded.png')
861        details.baseName = 'test_exclude_export_excluded'
862        details.page = 0
863        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
864        self.assertFalse(os.path.exists(exporter.generateFileName(details)))
865        details.page = 1
866        self.assertTrue(os.path.exists(exporter.generateFileName(details)))
867
868        # exclude second page
869        l.pageCollection().page(1).setExcludeFromExports(True)
870        rendered_file_path = os.path.join(self.basetestpath, 'test_exclude_export_excluded_all.png')
871        details.baseName = 'test_exclude_export_excluded_all'
872        details.page = 0
873        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
874        self.assertFalse(os.path.exists(exporter.generateFileName(details)))
875        details.page = 1
876        self.assertFalse(os.path.exists(exporter.generateFileName(details)))
877
878    def testPageFileName(self):
879        l = QgsLayout(QgsProject.instance())
880        exporter = QgsLayoutExporter(l)
881        details = QgsLayoutExporter.PageExportDetails()
882        details.directory = '/tmp/output'
883        details.baseName = 'my_maps'
884        details.extension = 'png'
885        details.page = 0
886        self.assertEqual(exporter.generateFileName(details), '/tmp/output/my_maps.png')
887        details.page = 1
888        self.assertEqual(exporter.generateFileName(details), '/tmp/output/my_maps_2.png')
889        details.page = 2
890        self.assertEqual(exporter.generateFileName(details), '/tmp/output/my_maps_3.png')
891
892    def prepareIteratorLayout(self):
893        layer_path = os.path.join(TEST_DATA_DIR, 'france_parts.shp')
894        layer = QgsVectorLayer(layer_path, 'test', "ogr")
895
896        project = QgsProject()
897        project.addMapLayers([layer])
898        # select epsg:2154
899        crs = QgsCoordinateReferenceSystem('epsg:2154')
900        project.setCrs(crs)
901
902        layout = QgsPrintLayout(project)
903        layout.initializeDefaults()
904
905        # fix the renderer, fill with green
906        props = {"color": "0,127,0", "outline_width": "4", "outline_color": '255,255,255'}
907        fillSymbol = QgsFillSymbol.createSimple(props)
908        renderer = QgsSingleSymbolRenderer(fillSymbol)
909        layer.setRenderer(renderer)
910
911        # the atlas map
912        atlas_map = QgsLayoutItemMap(layout)
913        atlas_map.attemptSetSceneRect(QRectF(20, 20, 130, 130))
914        atlas_map.setFrameEnabled(True)
915        atlas_map.setLayers([layer])
916        layout.addLayoutItem(atlas_map)
917
918        # the atlas
919        atlas = layout.atlas()
920        atlas.setCoverageLayer(layer)
921        atlas.setEnabled(True)
922
923        atlas_map.setExtent(
924            QgsRectangle(332719.06221504929, 6765214.5887386119, 560957.85090677091, 6993453.3774303338))
925
926        atlas_map.setAtlasDriven(True)
927        atlas_map.setAtlasScalingMode(QgsLayoutItemMap.Auto)
928        atlas_map.setAtlasMargin(0.10)
929
930        return project, layout
931
932    def testIteratorToImages(self):
933        project, layout = self.prepareIteratorLayout()
934        atlas = layout.atlas()
935        atlas.setFilenameExpression("'test_exportiteratortoimage_' || \"NAME_1\"")
936
937        # setup settings
938        settings = QgsLayoutExporter.ImageExportSettings()
939        settings.dpi = 80
940
941        result, error = QgsLayoutExporter.exportToImage(atlas, self.basetestpath + '/', 'png', settings)
942        self.assertEqual(result, QgsLayoutExporter.Success, error)
943
944        page1_path = os.path.join(self.basetestpath, 'test_exportiteratortoimage_Basse-Normandie.png')
945        self.assertTrue(self.checkImage('iteratortoimage1', 'iteratortoimage1', page1_path))
946        page2_path = os.path.join(self.basetestpath, 'test_exportiteratortoimage_Bretagne.png')
947        self.assertTrue(self.checkImage('iteratortoimage2', 'iteratortoimage2', page2_path))
948        page3_path = os.path.join(self.basetestpath, 'test_exportiteratortoimage_Centre.png')
949        self.assertTrue(os.path.exists(page3_path))
950        page4_path = os.path.join(self.basetestpath, 'test_exportiteratortoimage_Pays de la Loire.png')
951        self.assertTrue(os.path.exists(page4_path))
952
953    def testIteratorToSvgs(self):
954        project, layout = self.prepareIteratorLayout()
955        atlas = layout.atlas()
956        atlas.setFilenameExpression("'test_exportiteratortosvg_' || \"NAME_1\"")
957
958        # setup settings
959        settings = QgsLayoutExporter.SvgExportSettings()
960        settings.dpi = 80
961        settings.forceVectorOutput = False
962
963        result, error = QgsLayoutExporter.exportToSvg(atlas, self.basetestpath + '/', settings)
964        self.assertEqual(result, QgsLayoutExporter.Success, error)
965
966        page1_path = os.path.join(self.basetestpath, 'test_exportiteratortosvg_Basse-Normandie.svg')
967        rendered_page_1 = os.path.join(self.basetestpath, 'test_exportiteratortosvg_Basse-Normandie.png')
968        svgToPng(page1_path, rendered_page_1, width=935)
969        self.assertTrue(self.checkImage('iteratortosvg1', 'iteratortoimage1', rendered_page_1, size_tolerance=2))
970        page2_path = os.path.join(self.basetestpath, 'test_exportiteratortosvg_Bretagne.svg')
971        rendered_page_2 = os.path.join(self.basetestpath, 'test_exportiteratortosvg_Bretagne.png')
972        svgToPng(page2_path, rendered_page_2, width=935)
973        self.assertTrue(self.checkImage('iteratortosvg2', 'iteratortoimage2', rendered_page_2, size_tolerance=2))
974        page3_path = os.path.join(self.basetestpath, 'test_exportiteratortosvg_Centre.svg')
975        self.assertTrue(os.path.exists(page3_path))
976        page4_path = os.path.join(self.basetestpath, 'test_exportiteratortosvg_Pays de la Loire.svg')
977        self.assertTrue(os.path.exists(page4_path))
978
979    def testIteratorToPdfs(self):
980        project, layout = self.prepareIteratorLayout()
981        atlas = layout.atlas()
982        atlas.setFilenameExpression("'test_exportiteratortopdf_' || \"NAME_1\"")
983
984        # setup settings
985        settings = QgsLayoutExporter.PdfExportSettings()
986        settings.dpi = 80
987        settings.rasterizeWholeImage = False
988        settings.forceVectorOutput = False
989
990        result, error = QgsLayoutExporter.exportToPdfs(atlas, self.basetestpath + '/', settings)
991        self.assertEqual(result, QgsLayoutExporter.Success, error)
992
993        page1_path = os.path.join(self.basetestpath, 'test_exportiteratortopdf_Basse-Normandie.pdf')
994        rendered_page_1 = os.path.join(self.basetestpath, 'test_exportiteratortopdf_Basse-Normandie.png')
995        pdfToPng(page1_path, rendered_page_1, dpi=80, page=1)
996        self.assertTrue(self.checkImage('iteratortopdf1', 'iteratortoimage1', rendered_page_1, size_tolerance=2))
997        page2_path = os.path.join(self.basetestpath, 'test_exportiteratortopdf_Bretagne.pdf')
998        rendered_page_2 = os.path.join(self.basetestpath, 'test_exportiteratortopdf_Bretagne.png')
999        pdfToPng(page2_path, rendered_page_2, dpi=80, page=1)
1000        self.assertTrue(self.checkImage('iteratortopdf2', 'iteratortoimage2', rendered_page_2, size_tolerance=2))
1001        page3_path = os.path.join(self.basetestpath, 'test_exportiteratortopdf_Centre.pdf')
1002        self.assertTrue(os.path.exists(page3_path))
1003        page4_path = os.path.join(self.basetestpath, 'test_exportiteratortopdf_Pays de la Loire.pdf')
1004        self.assertTrue(os.path.exists(page4_path))
1005
1006    def testIteratorToPdf(self):
1007        project, layout = self.prepareIteratorLayout()
1008        atlas = layout.atlas()
1009
1010        # setup settings
1011        settings = QgsLayoutExporter.PdfExportSettings()
1012        settings.dpi = 80
1013        settings.rasterizeWholeImage = False
1014        settings.forceVectorOutput = False
1015
1016        pdf_path = os.path.join(self.basetestpath, 'test_exportiteratortopdf_single.pdf')
1017        result, error = QgsLayoutExporter.exportToPdf(atlas, pdf_path, settings)
1018        self.assertEqual(result, QgsLayoutExporter.Success, error)
1019
1020        rendered_page_1 = os.path.join(self.basetestpath, 'test_exportiteratortopdf_single1.png')
1021        pdfToPng(pdf_path, rendered_page_1, dpi=80, page=1)
1022        self.assertTrue(self.checkImage('iteratortopdfsingle1', 'iteratortoimage1', rendered_page_1, size_tolerance=2))
1023
1024        rendered_page_2 = os.path.join(self.basetestpath, 'test_exportiteratortopdf_single2.png')
1025        pdfToPng(pdf_path, rendered_page_2, dpi=80, page=2)
1026        self.assertTrue(self.checkImage('iteratortopdfsingle2', 'iteratortoimage2', rendered_page_2, size_tolerance=2))
1027
1028        rendered_page_3 = os.path.join(self.basetestpath, 'test_exportiteratortopdf_single3.png')
1029        pdfToPng(pdf_path, rendered_page_3, dpi=80, page=3)
1030        self.assertTrue(os.path.exists(rendered_page_3))
1031        rendered_page_4 = os.path.join(self.basetestpath, 'test_exportiteratortopdf_single4.png')
1032        pdfToPng(pdf_path, rendered_page_4, dpi=80, page=4)
1033        self.assertTrue(os.path.exists(rendered_page_4))
1034
1035    def testPrintIterator(self):
1036        project, layout = self.prepareIteratorLayout()
1037        atlas = layout.atlas()
1038
1039        # setup settings
1040        settings = QgsLayoutExporter.PrintExportSettings()
1041        settings.dpi = 80
1042        settings.rasterizeWholeImage = False
1043
1044        pdf_path = os.path.join(self.basetestpath, 'test_printiterator.pdf')
1045        # make a qprinter directed to pdf
1046        printer = QPrinter()
1047        printer.setOutputFileName(pdf_path)
1048        printer.setOutputFormat(QPrinter.PdfFormat)
1049
1050        result, error = QgsLayoutExporter.print(atlas, printer, settings)
1051        self.assertEqual(result, QgsLayoutExporter.Success, error)
1052
1053        rendered_page_1 = os.path.join(self.basetestpath, 'test_printiterator1.png')
1054        pdfToPng(pdf_path, rendered_page_1, dpi=80, page=1)
1055        self.assertTrue(self.checkImage('printeriterator1', 'iteratortoimage1', rendered_page_1, size_tolerance=2))
1056
1057        rendered_page_2 = os.path.join(self.basetestpath, 'test_printiterator2.png')
1058        pdfToPng(pdf_path, rendered_page_2, dpi=80, page=2)
1059        self.assertTrue(self.checkImage('printiterator2', 'iteratortoimage2', rendered_page_2, size_tolerance=2))
1060
1061        rendered_page_3 = os.path.join(self.basetestpath, 'test_printiterator3.png')
1062        pdfToPng(pdf_path, rendered_page_3, dpi=80, page=3)
1063        self.assertTrue(os.path.exists(rendered_page_3))
1064        rendered_page_4 = os.path.join(self.basetestpath, 'test_printiterator4.png')
1065        pdfToPng(pdf_path, rendered_page_4, dpi=80, page=4)
1066        self.assertTrue(os.path.exists(rendered_page_4))
1067
1068    def testExportReport(self):
1069        p = QgsProject()
1070        r = QgsReport(p)
1071
1072        # add a header
1073        r.setHeaderEnabled(True)
1074        report_header = QgsLayout(p)
1075        report_header.initializeDefaults()
1076        item1 = QgsLayoutItemShape(report_header)
1077        item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
1078        fill = QgsSimpleFillSymbolLayer()
1079        fill_symbol = QgsFillSymbol()
1080        fill_symbol.changeSymbolLayer(0, fill)
1081        fill.setColor(Qt.green)
1082        fill.setStrokeStyle(Qt.NoPen)
1083        item1.setSymbol(fill_symbol)
1084        report_header.addItem(item1)
1085
1086        r.setHeader(report_header)
1087
1088        # add a footer
1089        r.setFooterEnabled(True)
1090        report_footer = QgsLayout(p)
1091        report_footer.initializeDefaults()
1092        item2 = QgsLayoutItemShape(report_footer)
1093        item2.attemptSetSceneRect(QRectF(10, 20, 100, 150))
1094        item2.attemptMove(QgsLayoutPoint(10, 20))
1095        fill = QgsSimpleFillSymbolLayer()
1096        fill_symbol = QgsFillSymbol()
1097        fill_symbol.changeSymbolLayer(0, fill)
1098        fill.setColor(Qt.cyan)
1099        fill.setStrokeStyle(Qt.NoPen)
1100        item2.setSymbol(fill_symbol)
1101        report_footer.addItem(item2)
1102
1103        r.setFooter(report_footer)
1104
1105        # setup settings
1106        settings = QgsLayoutExporter.ImageExportSettings()
1107        settings.dpi = 80
1108
1109        report_path = os.path.join(self.basetestpath, 'test_report')
1110        result, error = QgsLayoutExporter.exportToImage(r, report_path, 'png', settings)
1111        self.assertEqual(result, QgsLayoutExporter.Success, error)
1112
1113        page1_path = os.path.join(self.basetestpath, 'test_report_0001.png')
1114        self.assertTrue(self.checkImage('report_page1', 'report_page1', page1_path))
1115        page2_path = os.path.join(self.basetestpath, 'test_report_0002.png')
1116        self.assertTrue(self.checkImage('report_page2', 'report_page2', page2_path))
1117
1118    def testRequiresRasterization(self):
1119        """
1120        Test QgsLayoutExporter.requiresRasterization
1121        """
1122        l = QgsLayout(QgsProject.instance())
1123        l.initializeDefaults()
1124
1125        # add an item
1126        label = QgsLayoutItemLabel(l)
1127        label.attemptSetSceneRect(QRectF(30, 60, 200, 100))
1128        l.addLayoutItem(label)
1129
1130        self.assertFalse(QgsLayoutExporter.requiresRasterization(l))
1131
1132        # an item with a blend mode will force the whole layout to be rasterized
1133        label.setBlendMode(QPainter.CompositionMode_Overlay)
1134        self.assertTrue(QgsLayoutExporter.requiresRasterization(l))
1135
1136        # but if the item is NOT visible, it won't affect the output in any way..
1137        label.setVisibility(False)
1138        self.assertFalse(QgsLayoutExporter.requiresRasterization(l))
1139
1140    def testContainsAdvancedEffects(self):
1141        """
1142        Test QgsLayoutExporter.containsAdvancedEffects
1143        """
1144        l = QgsLayout(QgsProject.instance())
1145        l.initializeDefaults()
1146
1147        # add an item
1148        label = QgsLayoutItemLabel(l)
1149        label.attemptSetSceneRect(QRectF(30, 60, 200, 100))
1150        l.addLayoutItem(label)
1151
1152        self.assertFalse(QgsLayoutExporter.containsAdvancedEffects(l))
1153
1154        # an item with transparency will force it to be individually rasterized
1155        label.setItemOpacity(0.5)
1156        self.assertTrue(QgsLayoutExporter.containsAdvancedEffects(l))
1157
1158        # but if the item is NOT visible, it won't affect the output in any way..
1159        label.setVisibility(False)
1160        self.assertFalse(QgsLayoutExporter.containsAdvancedEffects(l))
1161
1162    def testLabelingResults(self):
1163        """
1164        Test QgsLayoutExporter.labelingResults()
1165        """
1166        settings = QgsPalLayerSettings()
1167        settings.fieldName = "\"id\""
1168        settings.isExpression = True
1169        settings.placement = QgsPalLayerSettings.OverPoint
1170        settings.priority = 10
1171        settings.displayAll = True
1172
1173        vl = QgsVectorLayer("Point?crs=epsg:4326&field=id:integer", "vl", "memory")
1174        f = QgsFeature()
1175        f.setAttributes([1])
1176        f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(-6.250851540391068, 53.335006994584944)))
1177        self.assertTrue(vl.dataProvider().addFeature(f))
1178        f.setAttributes([8888])
1179        f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(-21.950014487179544, 64.150023619739216)))
1180        self.assertTrue(vl.dataProvider().addFeature(f))
1181        f.setAttributes([33333])
1182        f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(-0.118667702475932, 51.5019405883275)))
1183        self.assertTrue(vl.dataProvider().addFeature(f))
1184        vl.updateExtents()
1185
1186        p = QgsProject()
1187        p.addMapLayer(vl)
1188
1189        vl.setLabeling(QgsVectorLayerSimpleLabeling(settings))
1190        vl.setLabelsEnabled(True)
1191
1192        l = QgsLayout(p)
1193        l.initializeDefaults()
1194
1195        map = QgsLayoutItemMap(l)
1196        map.attemptSetSceneRect(QRectF(20, 20, 200, 100))
1197        map.setFrameEnabled(False)
1198        map.setBackgroundEnabled(False)
1199        map.setCrs(vl.crs())
1200        map.zoomToExtent(vl.extent())
1201        map.setLayers([vl])
1202        l.addLayoutItem(map)
1203
1204        exporter = QgsLayoutExporter(l)
1205        self.assertEqual(exporter.labelingResults(), {})
1206        # setup settings
1207        settings = QgsLayoutExporter.ImageExportSettings()
1208        settings.dpi = 80
1209
1210        rendered_file_path = os.path.join(self.basetestpath, 'test_exportlabelresults.png')
1211        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
1212
1213        results = exporter.labelingResults()
1214        self.assertEqual(len(results), 1)
1215
1216        labels = results[map.uuid()].allLabels()
1217        self.assertEqual(len(labels), 3)
1218        self.assertCountEqual([l.labelText for l in labels], ['1', '33333', '8888'])
1219
1220
1221if __name__ == '__main__':
1222    unittest.main()
1223