11

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

  • I have also referenced several web pages, 1 and 2. I tried to add these to be post, but my rep isn't high enough – EastWest Jun 29 '17 at 11:33
  • In your second last line, try replacing mapCanv.mapCanvasRefreshed.connect(custFunc) with mapCanv.renderComplete.connect(custFunc)? – Joseph Jun 29 '17 at 11:35
  • @Joseph Unfortunately, it didn't seem to make a difference. I still get the same result as above – EastWest Jun 29 '17 at 11:41
  • Perhaps try commiting the features you've added to the layer? (i.e. layerP .commitChanges()). Although I don't see why it should help since you're only saving the image but worth a try I guess. Otherwise hopefully others can advise :) – Joseph Jun 29 '17 at 11:48
  • @Joseph I tried commitChanges(), but no luck, unfortunately. Thanks for the suggestion. – EastWest Jun 29 '17 at 12:03
5
+100

There are different issues surfacing here

Rendering on screen vs rendering to an image

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.

Layer registry vs map canvas

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.

Show me the code

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)
  • 1
    Any idea about the renderComplete signal not working? – Joseph Jul 6 '17 at 10:43
  • Thanks for all the information! Unfortunately, I have tried inserting the suggested code into my script, completely replacing the map composer section, and am still encountering the same issue. The saved image does not have the line or point layers included, only the pre-loaded raster. – EastWest Jul 6 '17 at 10:50
  • I think the signal is emitted between the job is complete and the image is drawn to the screen. There's a parameter painter emitted with it on which you can still draw additional things that will end up on the final image (and from which you probably could also take the final image to make this approach working). – Matthias Kuhn Jul 6 '17 at 10:50
  • 1
    This looks like the solution. Thanks for all your help. One quick note if anyone else ends up finding this - to get the QTimer to work properly, leave out the parenthesis after render_image, or else python throws a warning. Should read QTimer.singleShot(10, render_image) – EastWest Jul 6 '17 at 15:32
  • Oops, of course. Fixed in the code above – Matthias Kuhn Jul 6 '17 at 15:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.