Sunday 26 November 2017

pyqgis - Wait for canvas to finish rendering before saving image


I am attempting to write a script that will save a rendering of several layers using the map composer. The problem I am encountering is that the script saves before qgis has finished rendering all layers.



Based on several other answers (1, 2, 3), I have attempted to use iface.mapCanvas.mapCanvasRefreshed.connect() and put the image saving inside a function, but I am still encountering the same problem - the images do not include all layers.


The code that I am using, as well as images of what the main window and the renderings look like are listed below.


I have noticed that if I have the console window open and uncomment the three print layerList lines, that the program will wait for rendering to finish before saving the images. I am not sure if this is due to the increased processing time, or if it is changing how the program executes.


How do I properly implement this so all layers are included in the image?


from qgis.core import *
from qgis.utils import *
from qgis.gui import *
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import os.path


##StackExchange Version=name
##Map_Save_Folder=folder
##Map_Save_Name=string roadmap

# Create save file location
mapName = "%s.png" %Map_Save_Name
outfile = os.path.join(Map_Save_Folder,mapName)
pdfName = "%s.pdf" %Map_Save_Name
outPDF = os.path.join(Map_Save_Folder,pdfName)


# Create point and line layers for later
URIstrP = "Point?crs=EPSG:3035"
layerP = QgsVectorLayer(URIstrP,"pointsPath","memory")
provP = layerP.dataProvider()
URIstrL = "LineString?crs=EPSG:3035"
layerL = QgsVectorLayer(URIstrL,"linePath","memory")
provL = layerL.dataProvider()

# Add points to point layer

feat1 = QgsFeature()
feat2 = QgsFeature()
feat3 = QgsFeature()
feat1.setGeometry(QgsGeometry.fromPoint(QgsPoint(5200000,2600000)))
feat2.setGeometry(QgsGeometry.fromPoint(QgsPoint(5300000,2800000)))
provP.addFeatures([feat1, feat2])

# Add line to line layer
feat3.setGeometry(QgsGeometry.fromPolyline([feat1.geometry().asPoint(),feat2.geometry().asPoint()]))
provL.addFeatures([feat3])


# Set symbology for line layer
symReg = QgsSymbolLayerV2Registry.instance()
metaRegL = symReg.symbolLayerMetadata("SimpleLine")
symLayL = QgsSymbolV2.defaultSymbol(layerL.geometryType())
metaL = metaRegL.createSymbolLayer({'width':'1','color':'0,0,0'})
symLayL.deleteSymbolLayer(0)
symLayL.appendSymbolLayer(metaL)
symRendL = QgsSingleSymbolRendererV2(symLayL)
layerL.setRendererV2(symRendL)


# Set symbology for point layer
metaRegP = symReg.symbolLayerMetadata("SimpleMarker")
symLayP = QgsSymbolV2.defaultSymbol(layerP.geometryType())
metaP = metaRegP.createSymbolLayer({'size':'3','color':'0,0,0'})
symLayP.deleteSymbolLayer(0)
symLayP.appendSymbolLayer(metaP)
symRendP = QgsSingleSymbolRendererV2(symLayP)
layerP.setRendererV2(symRendP)


# Load the layers
QgsMapLayerRegistry.instance().addMapLayer(layerP)
QgsMapLayerRegistry.instance().addMapLayer(layerL)
iface.mapCanvas().refresh()


# --------------------- Using Map Composer -----------------
def custFunc():
mapComp.exportAsPDF(outPDF)
mapImage.save(outfile,"png")

mapCanv.mapCanvasRefreshed.disconnect(custFunc)
return

layerList = []
for layer in QgsMapLayerRegistry.instance().mapLayers().values():
layerList.append(layer.id())
#print layerList
#print layerList
#print layerList


mapCanv = iface.mapCanvas()
bound = layerP.extent()
bound.scale(1.25)
mapCanv.setExtent(bound)

mapRend = mapCanv.mapRenderer()
mapComp = QgsComposition(mapRend)
mapComp.setPaperSize(250,250)
mapComp.setPlotStyle(QgsComposition.Print)


x, y = 0, 0
w, h = mapComp.paperWidth(), mapComp.paperHeight()

composerMap = QgsComposerMap(mapComp, x, y, w, h)
composerMap.zoomToExtent(bound)
mapComp.addItem(composerMap)
#mapComp.exportAsPDF(outPDF)

mapRend.setLayerSet(layerList)
mapRend.setExtent(bound)


dpmm = dpmm = mapComp.printResolution() / 25.4
mapImage = QImage(QSize(int(dpmm*w),int(dpmm*h)), QImage.Format_ARGB32)
mapImage.setDotsPerMeterX(dpmm * 1000)
mapImage.setDotsPerMeterY(dpmm * 1000)

mapPaint = QPainter()
mapPaint.begin(mapImage)

mapRend.render(mapPaint)


mapComp.renderPage(mapPaint,0)
mapPaint.end()
mapCanv.mapCanvasRefreshed.connect(custFunc)
#mapImage.save(outfile,"png")

What it looks like in QGIS main window (there is a random raster map it is being displayed on): enter image description here


What is saved: enter image description here


As further information, I am using QGIS 2.18.7 on Windows 7



Answer




There are different issues surfacing here



The signal mapCanvasRefreshed is emitted repeatedly while the canvas is being rendered to screen. For on-screen-display this gives a quicker feedback which can be nice for a user to see something going on or help in navigation.


For off screen rendering like saving to a file, this is not reliable (as you will only have a complete image if the rendering was fast enough).


What can be done: we don't need the map canvas to render your image. We can just copy the QgsMapSettings from the map canvas. These settings are the parameters that are sent to the renderer and define what exactly and how exactly things should be converted from all the data providers to a raster image.



Layers added to the registry don't end up on the canvas immediately but only in the next run of the event loop. Therefore you are better off doing one of the following two things




  • Start the image rendering in a timer. QTimer.singleShot(10, render_image)





  • Run QApplication.processEvents() after adding the layer. This works but it's a dangerous call to use (sometimes leads to weird crashes) and therefore should be avoided.





The following code does this (slightly adjusted from QFieldSync, have a look in there if you are interested in more customization)


from PyQt4.QtGui import QImage, QPainter

def render_image():

size = iface.mapCanvas().size()
image = QImage(size, QImage.Format_RGB32)

painter = QPainter(image)
settings = iface.mapCanvas().mapSettings()

# You can fine tune the settings here for different
# dpi, extent, antialiasing...
# Just make sure the size of the target image matches


# You can also add additional layers. In the case here,
# this helps to add layers that haven't been added to the
# canvas yet
layers = settings.layers()
settings.setLayers([layerP.id(), layerL.id()] + layers)

job = QgsMapRendererCustomPainterJob(settings, painter)
job.renderSynchronously()
painter.end()
image.save('/tmp/image.png')


# If you don't want to add additional layers manually to the
# mapSettings() you can also do this:
# Give QGIS give a tiny bit of time to bring the layers from
# the registry to the canvas (the 10 ms do not matter, the important
# part is that it's posted to the event loop)

# QTimer.singleShot(10, render_image)

No comments:

Post a Comment

arcpy - Changing output name when exporting data driven pages to JPG?

Is there a way to save the output JPG, changing the output file name to the page name, instead of page number? I mean changing the script fo...