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):
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)
RunThis works but it's a dangerous call to use (sometimes leads to weird crashes) and therefore should be avoided.QApplication.processEvents()
after adding the layer.
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