class OpenlayersController(QObject):
    """
    Helper class that deals with QWebPage.
    The object lives in GUI thread, its request() slot is asynchronously called
    from worker thread.
    See https://github.com/wonder-sk/qgis-mtr-example-plugin for basic example
    1. Load Web Page with OpenLayers map
    2. Update OL map extend according to QGIS canvas extent
    """

    # signal that reports to the worker thread that the image is ready
    finished = pyqtSignal()

    def __init__(self, parent, context, webPage, layerType):
        QObject.__init__(self, parent)

        debug("OpenlayersController.__init__", 3)
        self.context = context
        self.layerType = layerType

        self.img = QImage()

        self.page = webPage
        self.page.loadFinished.connect(self.pageLoaded)
        # initial size for map
        self.page.setViewportSize(QSize(1, 1))

        self.timerMapReady = QTimer()
        self.timerMapReady.setSingleShot(True)
        self.timerMapReady.setInterval(20)
        self.timerMapReady.timeout.connect(self.checkMapReady)

        self.timer = QTimer()
        self.timer.setInterval(100)
        self.timer.timeout.connect(self.checkMapUpdate)

        self.timerMax = QTimer()
        self.timerMax.setSingleShot(True)
        # TODO: different timeouts for map types
        self.timerMax.setInterval(2500)
        self.timerMax.timeout.connect(self.mapTimeout)

    @pyqtSlot()
    def request(self):
        debug("[GUI THREAD] Processing request", 3)
        self.cancelled = False

        if not self.page.loaded:
            self.init_page()
        else:
            self.setup_map()

    def init_page(self):
        url = self.layerType.html_url()
        debug("page file: %s" % url)
        self.page.mainFrame().load(QUrl(url))
        # wait for page to finish loading
        debug("OpenlayersWorker waiting for page to load", 3)

    def pageLoaded(self):
        debug("[GUI THREAD] pageLoaded", 3)
        if self.cancelled:
            self.emitErrorImage()
            return

        # wait until OpenLayers map is ready
        self.checkMapReady()

    def checkMapReady(self):
        debug("[GUI THREAD] checkMapReady", 3)
        if self.page.mainFrame().evaluateJavaScript("map != undefined"):
            # map ready
            self.page.loaded = True
            self.setup_map()
        else:
            # wait for map
            self.timerMapReady.start()

    def setup_map(self):
        rendererContext = self.context

        # FIXME: self.mapSettings.outputDpi()
        self.outputDpi = rendererContext.painter().device().logicalDpiX()
        debug(" extent: %s" % rendererContext.extent().toString(), 3)
        debug(
            " center: %lf, %lf" % (rendererContext.extent().center().x(),
                                   rendererContext.extent().center().y()), 3)
        debug(
            " size: %d, %d" %
            (rendererContext.painter().viewport().size().width(),
             rendererContext.painter().viewport().size().height()), 3)
        debug(
            " logicalDpiX: %d" %
            rendererContext.painter().device().logicalDpiX(), 3)
        debug(" outputDpi: %lf" % self.outputDpi)
        debug(
            " mapUnitsPerPixel: %f" %
            rendererContext.mapToPixel().mapUnitsPerPixel(), 3)
        # debug(" rasterScaleFactor: %s" % str(rendererContext.
        #                                      rasterScaleFactor()), 3)
        # debug(" outputSize: %d, %d" % (self.iface.mapCanvas().mapRenderer().
        #                                outputSize().width(),
        #                                self.iface.mapCanvas().mapRenderer().
        #                                outputSize().height()), 3)
        # debug(" scale: %lf" % self.iface.mapCanvas().mapRenderer().scale(),
        #                                3)
        #
        # if (self.page.lastExtent == rendererContext.extent()
        #         and self.page.lastViewPortSize == rendererContext.painter().
        #         viewport().size()
        #         and self.page.lastLogicalDpi == rendererContext.painter().
        #         device().logicalDpiX()
        #         and self.page.lastOutputDpi == self.outputDpi
        #         and self.page.lastMapUnitsPerPixel == rendererContext.
        #         mapToPixel().mapUnitsPerPixel()):
        #     self.renderMap()
        #     self.finished.emit()
        #     return

        self.targetSize = rendererContext.painter().viewport().size()
        if rendererContext.painter().device().logicalDpiX() != int(
                self.outputDpi):
            # use screen dpi for printing
            sizeFact = self.outputDpi / 25.4 / rendererContext.mapToPixel(
            ).mapUnitsPerPixel()
            self.targetSize.setWidth(rendererContext.extent().width() *
                                     sizeFact)
            self.targetSize.setHeight(rendererContext.extent().height() *
                                      sizeFact)
        debug(
            " targetSize: %d, %d" %
            (self.targetSize.width(), self.targetSize.height()), 3)

        # find matching OL resolution
        qgisRes = rendererContext.extent().width() / self.targetSize.width()
        olRes = None
        for res in self.page.resolutions():
            if qgisRes >= res:
                olRes = res
                break
        if olRes is None:
            debug("No matching OL resolution found (QGIS resolution: %f)" %
                  qgisRes)
            self.emitErrorImage()
            return

        # adjust OpenLayers viewport to match QGIS extent
        olWidth = rendererContext.extent().width() / olRes
        olHeight = rendererContext.extent().height() / olRes
        debug(
            "    adjust viewport: %f -> %f: %f x %f" %
            (qgisRes, olRes, olWidth, olHeight), 3)
        olSize = QSize(int(olWidth), int(olHeight))
        self.page.setViewportSize(olSize)
        self.page.mainFrame().evaluateJavaScript("map.updateSize();")
        self.img = QImage(olSize, QImage.Format_ARGB32_Premultiplied)

        self.page.extent = rendererContext.extent()
        debug(
            "map.zoomToExtent (%f, %f, %f, %f)" %
            (self.page.extent.xMinimum(), self.page.extent.yMinimum(),
             self.page.extent.xMaximum(), self.page.extent.yMaximum()), 3)
        self.page.mainFrame().evaluateJavaScript(
            "map.zoomToExtent(new OpenLayers.Bounds(%f, %f, %f, %f), true);" %
            (self.page.extent.xMinimum(), self.page.extent.yMinimum(),
             self.page.extent.xMaximum(), self.page.extent.yMaximum()))
        olextent = self.page.mainFrame().evaluateJavaScript("map.getExtent();")
        debug("Resulting OL extent: %s" % olextent, 3)
        if olextent is None or not hasattr(olextent, '__getitem__'):
            debug("map.zoomToExtent failed")
            # map.setCenter and other operations throw "undefined[0]:
            # TypeError: 'null' is not an object" on first page load
            # We ignore that and render the initial map with wrong extents
            # self.emitErrorImage()
            # return
        else:
            reloffset = abs((self.page.extent.yMaximum() - olextent["top"]) /
                            self.page.extent.xMinimum())
            debug("relative offset yMaximum %f" % reloffset, 3)
            if reloffset > 0.1:
                debug("OL extent shift failure: %s" % reloffset)
                self.emitErrorImage()
                return
        self.mapFinished = False
        self.timer.start()
        self.timerMax.start()

    def checkMapUpdate(self):
        if self.layerType.emitsLoadEnd:
            # wait for OpenLayers to finish loading
            loadEndOL = self.page.mainFrame().evaluateJavaScript("loadEnd")
            debug(
                "waiting for loadEnd: renderingStopped=%r loadEndOL=%r" %
                (self.context.renderingStopped(), loadEndOL), 4)
            if loadEndOL is not None:
                self.mapFinished = loadEndOL
            else:
                debug("OpenlayersLayer Warning: Could not get loadEnd")

        if self.mapFinished:
            self.timerMax.stop()
            self.timer.stop()

            self.renderMap()

            self.finished.emit()

    def renderMap(self):
        rendererContext = self.context
        if rendererContext.painter().device().logicalDpiX() != int(
                self.outputDpi):
            printScale = 25.4 / self.outputDpi  # OL DPI to printer pixels
            rendererContext.painter().scale(printScale, printScale)

        # render OpenLayers to image
        painter = QPainter(self.img)
        self.page.mainFrame().render(painter)
        painter.end()

        if self.img.size() != self.targetSize:
            targetWidth = self.targetSize.width()
            targetHeight = self.targetSize.height()
            # scale using QImage for better quality
            debug(
                "    scale image: %i x %i -> %i x %i" %
                (self.img.width(), self.img.height(), targetWidth,
                 targetHeight), 3)
            self.img = self.img.scaled(targetWidth, targetHeight,
                                       Qt.KeepAspectRatio,
                                       Qt.SmoothTransformation)

        # save current state
        self.page.lastExtent = rendererContext.extent()
        self.page.lastViewPortSize = rendererContext.painter().viewport().size(
        )
        self.page.lastLogicalDpi = rendererContext.painter().device(
        ).logicalDpiX()
        self.page.lastOutputDpi = self.outputDpi
        self.page.lastMapUnitsPerPixel = rendererContext.mapToPixel(
        ).mapUnitsPerPixel()

    def mapTimeout(self):
        debug("mapTimeout reached")
        self.timer.stop()
        # if not self.layerType.emitsLoadEnd:
        self.renderMap()
        self.finished.emit()

    def emitErrorImage(self):
        self.img = QImage()
        self.targetSize = self.img.size
        self.finished.emit()
class OpenlayersController(QObject):
    """
    Helper class that deals with QWebPage.
    The object lives in GUI thread, its request() slot is asynchronously called
    from worker thread.
    See https://github.com/wonder-sk/qgis-mtr-example-plugin for basic example
    1. Load Web Page with OpenLayers map
    2. Update OL map extend according to QGIS canvas extent
    """

    # signal that reports to the worker thread that the image is ready
    finished = pyqtSignal()

    def __init__(self, parent, context, webPage, layerType):
        QObject.__init__(self, parent)

        debug("OpenlayersController.__init__", 3)
        self.context = context
        self.layerType = layerType

        self.img = QImage()

        self.page = webPage
        self.page.loadFinished.connect(self.pageLoaded)
        # initial size for map
        self.page.setViewportSize(QSize(1, 1))

        self.timerMapReady = QTimer()
        self.timerMapReady.setSingleShot(True)
        self.timerMapReady.setInterval(20)
        self.timerMapReady.timeout.connect(self.checkMapReady)

        self.timer = QTimer()
        self.timer.setInterval(100)
        self.timer.timeout.connect(self.checkMapUpdate)

        self.timerMax = QTimer()
        self.timerMax.setSingleShot(True)
        # TODO: different timeouts for map types
        self.timerMax.setInterval(2500)
        self.timerMax.timeout.connect(self.mapTimeout)

    @pyqtSlot()
    def request(self):
        debug("[GUI THREAD] Processing request", 3)
        self.cancelled = False

        if not self.page.loaded:
            self.init_page()
        else:
            self.setup_map()

    def init_page(self):
        url = self.layerType.html_url()
        debug("page file: %s" % url)
        self.page.mainFrame().load(QUrl(url))
        # wait for page to finish loading
        debug("OpenlayersWorker waiting for page to load", 3)

    def pageLoaded(self):
        debug("[GUI THREAD] pageLoaded", 3)
        if self.cancelled:
            self.emitErrorImage()
            return

        # wait until OpenLayers map is ready
        self.checkMapReady()

    def checkMapReady(self):
        debug("[GUI THREAD] checkMapReady", 3)
        if self.page.mainFrame().evaluateJavaScript("map != undefined"):
            # map ready
            self.page.loaded = True
            self.setup_map()
        else:
            # wait for map
            self.timerMapReady.start()

    def setup_map(self):
        rendererContext = self.context

        # FIXME: self.mapSettings.outputDpi()
        self.outputDpi = rendererContext.painter().device().logicalDpiX()
        debug(" extent: %s" % rendererContext.extent().toString(), 3)
        debug(" center: %lf, %lf" % (rendererContext.extent().center().x(),
                                     rendererContext.extent().center().y()), 3)
        debug(" size: %d, %d" % (
            rendererContext.painter().viewport().size().width(),
              rendererContext.painter().viewport().size().height()), 3)
        debug(" logicalDpiX: %d" % rendererContext.painter().
              device().logicalDpiX(), 3)
        debug(" outputDpi: %lf" % self.outputDpi)
        debug(" mapUnitsPerPixel: %f" % rendererContext.mapToPixel().
              mapUnitsPerPixel(), 3)
        # debug(" rasterScaleFactor: %s" % str(rendererContext.
        #                                      rasterScaleFactor()), 3)
        # debug(" outputSize: %d, %d" % (self.iface.mapCanvas().mapRenderer().
        #                                outputSize().width(),
        #                                self.iface.mapCanvas().mapRenderer().
        #                                outputSize().height()), 3)
        # debug(" scale: %lf" % self.iface.mapCanvas().mapRenderer().scale(),
        #                                3)
        #
        # if (self.page.lastExtent == rendererContext.extent()
        #         and self.page.lastViewPortSize == rendererContext.painter().
        #         viewport().size()
        #         and self.page.lastLogicalDpi == rendererContext.painter().
        #         device().logicalDpiX()
        #         and self.page.lastOutputDpi == self.outputDpi
        #         and self.page.lastMapUnitsPerPixel == rendererContext.
        #         mapToPixel().mapUnitsPerPixel()):
        #     self.renderMap()
        #     self.finished.emit()
        #     return

        self.targetSize = rendererContext.painter().viewport().size()
        if rendererContext.painter().device().logicalDpiX() != int(self.outputDpi):
            # use screen dpi for printing
            sizeFact = self.outputDpi / 25.4 / rendererContext.mapToPixel().mapUnitsPerPixel()
            self.targetSize.setWidth(
                rendererContext.extent().width() * sizeFact)
            self.targetSize.setHeight(
                rendererContext.extent().height() * sizeFact)
        debug(" targetSize: %d, %d" % (
            self.targetSize.width(), self.targetSize.height()), 3)

        # find matching OL resolution
        qgisRes = rendererContext.extent().width() / self.targetSize.width()
        olRes = None
        for res in self.page.resolutions():
            if qgisRes >= res:
                olRes = res
                break
        if olRes is None:
            debug("No matching OL resolution found (QGIS resolution: %f)" %
                  qgisRes)
            self.emitErrorImage()
            return

        # adjust OpenLayers viewport to match QGIS extent
        olWidth = rendererContext.extent().width() / olRes
        olHeight = rendererContext.extent().height() / olRes
        debug("    adjust viewport: %f -> %f: %f x %f" % (qgisRes,
                                                          olRes, olWidth,
                                                          olHeight), 3)
        olSize = QSize(int(olWidth), int(olHeight))
        self.page.setViewportSize(olSize)
        self.page.mainFrame().evaluateJavaScript("map.updateSize();")
        self.img = QImage(olSize, QImage.Format_ARGB32_Premultiplied)

        self.page.extent = rendererContext.extent()
        debug("map.zoomToExtent (%f, %f, %f, %f)" % (
            self.page.extent.xMinimum(), self.page.extent.yMinimum(),
            self.page.extent.xMaximum(), self.page.extent.yMaximum()), 3)
        self.page.mainFrame().evaluateJavaScript(
            "map.zoomToExtent(new OpenLayers.Bounds(%f, %f, %f, %f), true);" %
            (self.page.extent.xMinimum(), self.page.extent.yMinimum(),
                self.page.extent.xMaximum(), self.page.extent.yMaximum()))
        olextent = self.page.mainFrame().evaluateJavaScript("map.getExtent();")
        debug("Resulting OL extent: %s" % olextent, 3)
        if olextent is None or not hasattr(olextent, '__getitem__'):
            debug("map.zoomToExtent failed")
            # map.setCenter and other operations throw "undefined[0]:
            # TypeError: 'null' is not an object" on first page load
            # We ignore that and render the initial map with wrong extents
            # self.emitErrorImage()
            # return
        else:
            reloffset = abs((self.page.extent.yMaximum()-olextent[
                "top"])/self.page.extent.xMinimum())
            debug("relative offset yMaximum %f" % reloffset, 3)
            if reloffset > 0.1:
                debug("OL extent shift failure: %s" % reloffset)
                self.emitErrorImage()
                return
        self.mapFinished = False
        self.timer.start()
        self.timerMax.start()

    def checkMapUpdate(self):
        if self.layerType.emitsLoadEnd:
            # wait for OpenLayers to finish loading
            loadEndOL = self.page.mainFrame().evaluateJavaScript("loadEnd")
            debug("waiting for loadEnd: renderingStopped=%r loadEndOL=%r" % (
                  self.context.renderingStopped(), loadEndOL), 4)
            if loadEndOL is not None:
                self.mapFinished = loadEndOL
            else:
                debug("OpenlayersLayer Warning: Could not get loadEnd")

        if self.mapFinished:
            self.timerMax.stop()
            self.timer.stop()

            self.renderMap()

            self.finished.emit()

    def renderMap(self):
        rendererContext = self.context
        if rendererContext.painter().device().logicalDpiX() != int(self.outputDpi):
            printScale = 25.4 / self.outputDpi  # OL DPI to printer pixels
            rendererContext.painter().scale(printScale, printScale)

        # render OpenLayers to image
        painter = QPainter(self.img)
        self.page.mainFrame().render(painter)
        painter.end()

        if self.img.size() != self.targetSize:
            targetWidth = self.targetSize.width()
            targetHeight = self.targetSize.height()
            # scale using QImage for better quality
            debug("    scale image: %i x %i -> %i x %i" % (
                self.img.width(), self.img.height(),
                  targetWidth, targetHeight), 3)
            self.img = self.img.scaled(targetWidth, targetHeight,
                                       Qt.KeepAspectRatio,
                                       Qt.SmoothTransformation)

        # save current state
        self.page.lastExtent = rendererContext.extent()
        self.page.lastViewPortSize = rendererContext.painter().viewport().size()
        self.page.lastLogicalDpi = rendererContext.painter().device().logicalDpiX()
        self.page.lastOutputDpi = self.outputDpi
        self.page.lastMapUnitsPerPixel = rendererContext.mapToPixel().mapUnitsPerPixel()

    def mapTimeout(self):
        debug("mapTimeout reached")
        self.timer.stop()
        # if not self.layerType.emitsLoadEnd:
        self.renderMap()
        self.finished.emit()

    def emitErrorImage(self):
        self.img = QImage()
        self.targetSize = self.img.size
        self.finished.emit()
Exemplo n.º 3
0
    def testExportToImage(self):
        md = QgsProject.instance().metadata()
        md.setTitle('proj title')
        md.setAuthor('proj author')
        md.setCreationDateTime(QDateTime(QDate(2011, 5, 3), QTime(9, 4, 5), QTimeZone(36000)))
        md.setIdentifier('proj identifier')
        md.setAbstract('proj abstract')
        md.setKeywords({'kw': ['kw1', 'kw2'], 'KWx': ['kw3', 'kw4']})
        QgsProject.instance().setMetadata(md)
        l = QgsLayout(QgsProject.instance())
        l.initializeDefaults()

        # add a second page
        page2 = QgsLayoutItemPage(l)
        page2.setPageSize('A5')
        l.pageCollection().addPage(page2)

        # add some items
        item1 = QgsLayoutItemShape(l)
        item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
        fill = QgsSimpleFillSymbolLayer()
        fill_symbol = QgsFillSymbol()
        fill_symbol.changeSymbolLayer(0, fill)
        fill.setColor(Qt.green)
        fill.setStrokeStyle(Qt.NoPen)
        item1.setSymbol(fill_symbol)
        l.addItem(item1)

        item2 = QgsLayoutItemShape(l)
        item2.attemptSetSceneRect(QRectF(10, 20, 100, 150))
        item2.attemptMove(QgsLayoutPoint(10, 20), page=1)
        fill = QgsSimpleFillSymbolLayer()
        fill_symbol = QgsFillSymbol()
        fill_symbol.changeSymbolLayer(0, fill)
        fill.setColor(Qt.cyan)
        fill.setStrokeStyle(Qt.NoPen)
        item2.setSymbol(fill_symbol)
        l.addItem(item2)

        exporter = QgsLayoutExporter(l)
        # setup settings
        settings = QgsLayoutExporter.ImageExportSettings()
        settings.dpi = 80

        rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagedpi.png')
        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)

        self.assertTrue(self.checkImage('exporttoimagedpi_page1', 'exporttoimagedpi_page1', rendered_file_path))
        page2_path = os.path.join(self.basetestpath, 'test_exporttoimagedpi_2.png')
        self.assertTrue(self.checkImage('exporttoimagedpi_page2', 'exporttoimagedpi_page2', page2_path))

        for f in (rendered_file_path, page2_path):
            d = gdal.Open(f)
            metadata = d.GetMetadata()
            self.assertEqual(metadata['Author'], 'proj author')
            self.assertEqual(metadata['Created'], '2011-05-03T09:04:05+10:00')
            self.assertEqual(metadata['Keywords'], 'KWx: kw3,kw4;kw: kw1,kw2')
            self.assertEqual(metadata['Subject'], 'proj abstract')
            self.assertEqual(metadata['Title'], 'proj title')

        # crop to contents
        settings.cropToContents = True
        settings.cropMargins = QgsMargins(10, 20, 30, 40)

        rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagecropped.png')
        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)

        self.assertTrue(self.checkImage('exporttoimagecropped_page1', 'exporttoimagecropped_page1', rendered_file_path))
        page2_path = os.path.join(self.basetestpath, 'test_exporttoimagecropped_2.png')
        self.assertTrue(self.checkImage('exporttoimagecropped_page2', 'exporttoimagecropped_page2', page2_path))

        # specific pages
        settings.cropToContents = False
        settings.pages = [1]

        rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagepages.png')
        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)

        self.assertFalse(os.path.exists(rendered_file_path))
        page2_path = os.path.join(self.basetestpath, 'test_exporttoimagepages_2.png')
        self.assertTrue(self.checkImage('exporttoimagedpi_page2', 'exporttoimagedpi_page2', page2_path))

        # image size
        settings.imageSize = QSize(600, 851)
        rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagesize.png')
        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
        self.assertFalse(os.path.exists(rendered_file_path))
        page2_path = os.path.join(self.basetestpath, 'test_exporttoimagesize_2.png')
        self.assertTrue(self.checkImage('exporttoimagesize_page2', 'exporttoimagesize_page2', page2_path))

        # image size with incorrect aspect ratio
        # this can happen as a result of data defined page sizes
        settings.imageSize = QSize(851, 600)
        rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagesizebadaspect.png')
        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)

        page2_path = os.path.join(self.basetestpath, 'test_exporttoimagesizebadaspect_2.png')
        im = QImage(page2_path)
        self.assertTrue(self.checkImage('exporttoimagesize_badaspect', 'exporttoimagedpi_page2', page2_path), '{}x{}'.format(im.width(), im.height()))
Exemplo n.º 4
0
    def testExportToImage(self):
        md = QgsProject.instance().metadata()
        md.setTitle('proj title')
        md.setAuthor('proj author')
        md.setCreationDateTime(QDateTime(QDate(2011, 5, 3), QTime(9, 4, 5), QTimeZone(36000)))
        md.setIdentifier('proj identifier')
        md.setAbstract('proj abstract')
        md.setKeywords({'kw': ['kw1', 'kw2'], 'KWx': ['kw3', 'kw4']})
        QgsProject.instance().setMetadata(md)
        l = QgsLayout(QgsProject.instance())
        l.initializeDefaults()

        # add a second page
        page2 = QgsLayoutItemPage(l)
        page2.setPageSize('A5')
        l.pageCollection().addPage(page2)

        # add some items
        item1 = QgsLayoutItemShape(l)
        item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
        fill = QgsSimpleFillSymbolLayer()
        fill_symbol = QgsFillSymbol()
        fill_symbol.changeSymbolLayer(0, fill)
        fill.setColor(Qt.green)
        fill.setStrokeStyle(Qt.NoPen)
        item1.setSymbol(fill_symbol)
        l.addItem(item1)

        item2 = QgsLayoutItemShape(l)
        item2.attemptSetSceneRect(QRectF(10, 20, 100, 150))
        item2.attemptMove(QgsLayoutPoint(10, 20), page=1)
        fill = QgsSimpleFillSymbolLayer()
        fill_symbol = QgsFillSymbol()
        fill_symbol.changeSymbolLayer(0, fill)
        fill.setColor(Qt.cyan)
        fill.setStrokeStyle(Qt.NoPen)
        item2.setSymbol(fill_symbol)
        l.addItem(item2)

        exporter = QgsLayoutExporter(l)
        # setup settings
        settings = QgsLayoutExporter.ImageExportSettings()
        settings.dpi = 80

        rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagedpi.png')
        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)

        self.assertTrue(self.checkImage('exporttoimagedpi_page1', 'exporttoimagedpi_page1', rendered_file_path))
        page2_path = os.path.join(self.basetestpath, 'test_exporttoimagedpi_2.png')
        self.assertTrue(self.checkImage('exporttoimagedpi_page2', 'exporttoimagedpi_page2', page2_path))

        for f in (rendered_file_path, page2_path):
            d = gdal.Open(f)
            metadata = d.GetMetadata()
            self.assertEqual(metadata['Author'], 'proj author')
            self.assertEqual(metadata['Created'], '2011-05-03T09:04:05+10:00')
            self.assertEqual(metadata['Keywords'], 'KWx: kw3,kw4;kw: kw1,kw2')
            self.assertEqual(metadata['Subject'], 'proj abstract')
            self.assertEqual(metadata['Title'], 'proj title')

        # crop to contents
        settings.cropToContents = True
        settings.cropMargins = QgsMargins(10, 20, 30, 40)

        rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagecropped.png')
        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)

        self.assertTrue(self.checkImage('exporttoimagecropped_page1', 'exporttoimagecropped_page1', rendered_file_path))
        page2_path = os.path.join(self.basetestpath, 'test_exporttoimagecropped_2.png')
        self.assertTrue(self.checkImage('exporttoimagecropped_page2', 'exporttoimagecropped_page2', page2_path))

        # specific pages
        settings.cropToContents = False
        settings.pages = [1]

        rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagepages.png')
        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)

        self.assertFalse(os.path.exists(rendered_file_path))
        page2_path = os.path.join(self.basetestpath, 'test_exporttoimagepages_2.png')
        self.assertTrue(self.checkImage('exporttoimagedpi_page2', 'exporttoimagedpi_page2', page2_path))

        # image size
        settings.imageSize = QSize(600, 851)
        rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagesize.png')
        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)
        self.assertFalse(os.path.exists(rendered_file_path))
        page2_path = os.path.join(self.basetestpath, 'test_exporttoimagesize_2.png')
        self.assertTrue(self.checkImage('exporttoimagesize_page2', 'exporttoimagesize_page2', page2_path))

        # image size with incorrect aspect ratio
        # this can happen as a result of data defined page sizes
        settings.imageSize = QSize(851, 600)
        rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagesizebadaspect.png')
        self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success)

        page2_path = os.path.join(self.basetestpath, 'test_exporttoimagesizebadaspect_2.png')
        im = QImage(page2_path)
        self.assertTrue(self.checkImage('exporttoimagesize_badaspect', 'exporttoimagedpi_page2', page2_path), '{}x{}'.format(im.width(), im.height()))