12

How do I export specific layers as a png from a QGIS .qgs map?

From the table of contents the script would first export the "boundary, climits, and Div1_Irrig_1956_0" layers as one png. The standalone script would then iterate to export "boundary, climits, and Div1_Irrig_1976_1" layers as the next png and so on. I am working with the script below to begin with but am only getting one png with all of the layers exported.

enter image description here

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from qgis.core import *
from qgis.gui import *
from qgis.utils import iface

import sys, os import glob

qgs = QgsApplication(sys.argv, True) qgis_prefix = "C:\OSGeo4W\apps\qgis" QgsApplication.setPrefixPath(qgis_prefix, True) qgs.initQgis()

#layers = glob.glob((configData['destination_folder'])+"\*.shp") layers = glob.glob(r"E:\IrrigatedLands\FC_qgis*.shp")

for layer in layers: print layer irrig = QgsVectorLayer(layer, "testlayer_shp", "ogr") print irrig.isValid() layerset = [] QgsMapLayerRegistry.instance().addMapLayer(irrig) layerset.append(irrig.id())

# create image
imageType = "png"
pathToFile = "C:\\Users\\James\\Desktop\\"
name = "render"
img = QImage(QSize(800, 600), QImage.Format_ARGB32_Premultiplied)

# set image's background color
color = QColor(255, 255, 255)
img.fill(color.rgb())

# create painter
p = QPainter()
p.begin(img)
p.setRenderHint(QPainter.Antialiasing)

render = QgsMapRenderer()

# set layer set
layer_set = [irrig.id()]  # add ID of every layer
print layer_set
render.setLayerSet(layer_set)

# set extent
rect = QgsRectangle(render.fullExtent())
rect.scale(1.1)
render.setExtent(rect)

# set output size
render.setOutputSize(img.size(), img.logicalDpiX())

# do the rendering
render.render(p)

p.end()

img.save(pathToFile + name + "." + imageType ,imageType)

PolyGeo
  • 65,136
  • 29
  • 109
  • 338
brgionta
  • 507
  • 5
  • 14

4 Answers4

19

In order to solve this question, we need to use timers or something that delays the execution of the script, so the canvas can reflect the layer arrangement at the time the map is exported. In other words, if you don't use timers you'll end up with 3 PNG images with the same content because everything will happen too fast.

In the QGIS map, set the map extent you want to export before running the following script in the QGIS Python Console (adjust the path ):

from PyQt4.QtCore import QTimer

fileName = '/tmp/exported' # exported is a prefix for the file names
boundaryLayer = QgsMapLayerRegistry.instance().mapLayersByName('boundary')[0]
climitsLayer = QgsMapLayerRegistry.instance().mapLayersByName('climits')[0]
otherLayers = ['Div1_Irrig_1956_0', 'Div1_Irrig_1956_1', 'Div1_Irrig_1956_2']
count = 0

iface.legendInterface().setLayerVisible(boundaryLayer, True)
iface.legendInterface().setLayerVisible(climitsLayer, True)

def prepareMap(): # Arrange layers
    iface.actionHideAllLayers().trigger() # make all layers invisible
    iface.legendInterface().setLayerVisible(QgsMapLayerRegistry.instance().mapLayersByName( otherLayers[count] )[0], True)
    QTimer.singleShot(1000, exportMap) # Wait a second and export the map

def exportMap(): # Save the map as a PNG
    global count # We need this because we'll modify its value
    iface.mapCanvas().saveAsImage( fileName + "_" + str(count) + ".png" )
    print "Map with layer",count,"exported!"
    if count < len(otherLayers)-1:
        QTimer.singleShot(1000, prepareMap) # Wait a second and prepare next map
    count += 1

prepareMap() # Let's start the fun

After the execution of the script, you'll end up with 3 (different) PNG images in /tmp/.

If you need to iterate over more layers, you just need to add their names to the otherLayers list, the script will do the rest for you.

----------------------------------------------------------------

EDIT: How to run this as a standalone script (outside of QGIS)?

The following script can be run outside of QGIS. Just make sure you adjust the file paths to your own directory structure and that you use a QGIS prefix that works for your own environment (see this answer for details):

from qgis.core import QgsApplication, QgsMapLayerRegistry, QgsVectorLayer, QgsProject
from qgis.gui import QgsMapCanvas, QgsMapCanvasLayer, QgsLayerTreeMapCanvasBridge
from PyQt4.QtCore import QTimer, QSize

qgisApp = QgsApplication([], True)
qgisApp.setPrefixPath("/usr", True)
qgisApp.initQgis()

# Required variables with your shapefile paths and names
pngsPath = '/tmp/'
boundaryLayer = QgsVectorLayer('/docs/geodata/colombia/colombia_wgs84.shp', 'boundary', 'ogr')
climitsLayer = QgsVectorLayer('/docs/geodata/colombia/colombia-geofabrik/railways.shp', 'climits', 'ogr')
otherLayers = {'Div1_Irrig_1956_0': QgsVectorLayer('/docs/geodata/colombia/colombia-geofabrik/points.shp', 'Div1_Irrig_1956_0', 'ogr'), 
    'Div1_Irrig_1956_1':QgsVectorLayer('/docs/geodata/colombia/colombia-geofabrik/places.shp', 'Div1_Irrig_1956_1', 'ogr'), 
    'Div1_Irrig_1956_2': QgsVectorLayer('/docs/geodata/colombia/colombia-geofabrik/natural.shp', 'Div1_Irrig_1956_2', 'ogr')}
count = 0    

canvas = QgsMapCanvas()
canvas.resize(QSize(500, 500)) # You can adjust this values to alter image dimensions
canvas.show()

# Add layers to map canvas taking the order into account
QgsMapLayerRegistry.instance().addMapLayer( boundaryLayer)
QgsMapLayerRegistry.instance().addMapLayers( otherLayers.values() )
QgsMapLayerRegistry.instance().addMapLayer( climitsLayer )
layerSet = [QgsMapCanvasLayer(climitsLayer)]
layerSet.extend([QgsMapCanvasLayer(l) for l in otherLayers.values() ])
layerSet.append(QgsMapCanvasLayer(boundaryLayer))
canvas.setLayerSet( layerSet )

# Link Layer Tree Root and Canvas
root = QgsProject.instance().layerTreeRoot()
bridge = QgsLayerTreeMapCanvasBridge(root, canvas) 

def prepareMap(): # Arrange layers
    for lyr in otherLayers.values(): # make all layers invisible
        root.findLayer( lyr.id() ).setVisible(0) # Unchecked
    root.findLayer( otherLayers.values()[count].id() ).setVisible(2) # Checked
    canvas.zoomToFullExtent()
    QTimer.singleShot(1000, exportMap) # Wait a second and export the map

def exportMap(): # Save the map as a PNG
    global count # We need this because we'll modify its value
    canvas.saveAsImage( pngsPath + otherLayers.keys()[count] + ".png" )
    print "Map with layer",otherLayers.keys()[count],"exported!"
    if count < len(otherLayers)-1:
        QTimer.singleShot(1000, prepareMap) # Wait a second and prepare next map
    else: # Time to close everything
        qgisApp.exitQgis()
        qgisApp.exit() 
    count += 1

prepareMap() # Let's start the fun
qgisApp.exec_()

Again, if you need to iterate over more layers, just add them to the otherLayers dictionary, the script will do the rest.

The resulting PNG image file names will correspond to your layers.

Germán Carrillo
  • 36,307
  • 5
  • 123
  • 178
  • thank you for the suggestions! Could I run this as a standalone script and execute it in OSGeo4W shell? – brgionta Apr 19 '16 at 05:15
  • 2
    Mmm, you've changed your question title making the post to become a different question. Please be clear when writing a question or you'll make people to waste precious time. Since the new question is still interesting, I've edited my answer, appending a standalone script. See above. – Germán Carrillo Apr 19 '16 at 17:10
  • The script renders all of the correct layers when showing the canvas window and exports the individual Div1_Irrig... layers with the boundary layer as separate .pngs but the climits layer is missing in all of the .pngs and the Irrig layers aren't fully rendered. Also there is a fatal error when running the script, causing python to crash. I encountered the same issue when working with the qgisApp.initQgis() lines. In my example script above I was able to resolve the issue without python crashing. I used my Qgis initialization lines with your script but the code still crashed!? – brgionta Apr 20 '16 at 08:03
  • Can you upload somewhere the whole script that is crashing in your computer? I'll have a look at it. – Germán Carrillo Apr 20 '16 at 11:51
  • I've uploaded the script (Layer6.py) along with the shapefiles here: https://drive.google.com/folderview?id=0B2d3dXKt1_QjS1dxeDZBTFowVjA&usp=sharing. Thanks! – brgionta Apr 20 '16 at 15:38
  • Were you able to access the files? The script prints out "Map with layer Div1_Irrig_1976_1 exported" first then "Map with layer Div1_Irrig_1956_0 exported" and lastly "Map with layer Div1_Irrig_1987_2 exported". Sometimes an error results before Python crashes: Error1: fseek(xxxxx) failed on DBF file – brgionta Apr 26 '16 at 00:09
  • I've checked the script with your data on Windows. Your climits layer has a different Reference System, that's why it doesn't appear in your images. Try with this reprojected climits layer instead. I had no other particular issues nor any crashes. Tested on Windows 7, QGIS 2.14.1. If you still have crashes, try increasing the time from 1000 (ms) to 3000 or so, in the script. This is the final script I ended up using on Windows. – Germán Carrillo Apr 29 '16 at 14:28
  • These are the images I got. Hope you can get the result you expect. – Germán Carrillo Apr 29 '16 at 14:29
  • 1
    The PNGs look correct. I had to set my PATH and PYTHONPATH variables within the OSGeo4W shell rather than defining them in the control panel environment variables. Thank you again! – brgionta May 02 '16 at 03:13
  • Great that it finally worked for you! – Germán Carrillo May 02 '16 at 04:26
  • I tried using time instead of QTimer but failed. – natsuapo Mar 13 '18 at 04:42
2

I had difficulty getting the composer to work and Germán Carrillo's answer is outdated.

Germán's answer also assumes that the entire layer can be rendered within a fixed time interval that is the same for each layer. In my use case, this couldn't be done reliably. Therefore, I used the following code:

prefix="probe_"
layers = []
for layer in QgsProject.instance().mapLayers().values():
    if layer.name().startswith("probe_"):
        layers.append(layer)

p = QgsProject().instance()
layout = QgsPrintLayout(p)
layout.initializeDefaults()
map = QgsLayoutItemMap(layout)
map.setRect(20, 20, 20, 20)
print(layers[0].extent())
map.setExtent(layers[0].extent())
layout.addLayoutItem(map)
map.attemptMove(QgsLayoutPoint(0, 0, QgsUnitTypes.LayoutInches))
map.attemptResize(QgsLayoutSize(8.5,  11, QgsUnitTypes.LayoutInches))
exporter = QgsLayoutExporter(layout)
settings = QgsLayoutExporter.ImageExportSettings()
settings.dpi      = 300
current_layer_idx = 0

def hide_layers(layers):
    root = QgsProject.instance().layerTreeRoot()
    for layer in layers:
        node = root.findLayer(layer.id())
        node.setItemVisibilityChecked(Qt.Unchecked)

def show_layer(layer):
    root = QgsProject.instance().layerTreeRoot()
    node = root.findLayer(layer.id())
    node.setItemVisibilityChecked(Qt.Checked)   

def prepare():
    global layer_name, out_name
    hide_layers(layers)
    show_layer(layers[current_layer_idx])
    layer_name = layers[current_layer_idx].name()
    out_name = layer_name + ".png"

def export():
    global current_layer_idx
    print("Saving ",out_name)
    ret = exporter.exportToImage(out_name, settings)
    assert ret==0
    current_layer_idx += 1

And manually entered prepare() and export() cyclicly as appropriate.

Richard
  • 1,831
  • 2
  • 21
  • 34
1

This actually doesn't require scripting. You can use print composer and atlas serial printing for that. It allows to iterate both over geographical features or also over layers or visibility preset groups. See http://www.qgis.org/en/site/forusers/visualchangelog212/index.html#feature-data-defined-control-over-map-layers-and-style-presets for more details.

1

I updated Germán Carillo’s response to PyQGIS 3 and added the prefix concept offered by Richard. In my case, I have over 50 pairs of point and vector layers. Each pair has a unique prefix to the layer name. I want to save a PNG file of each pair. Because of the number of PNG files to create, I prefer using the timer approach to avoid repeatedly typing “prepare” and “export”. Setting the timer to 1000 milliseconds if fine for my case. If it can’t be rendered in one second and exported in one second, then the timer value can be increased.

from PyQt5.QtCore import QTimer

filePath = 'C:\\myPath\\' # exported is a prefix for the file names
prefixes = ['1000', '2000', '3000', '4000', '5000'] 
count = 0

def prepareMap(): # Arrange layers
    layers = []
    layers = QgsProject.instance().mapLayers()

    # First, turn off all layers
    for layer in layers:
        QgsProject.instance().layerTreeRoot().findLayer(layer).setItemVisibilityChecked(False)

    # Second, get the layer to export
    exportLayers = []
    for layer in QgsProject.instance().mapLayers().values():
        if layer.name().startswith(prefixes[count]):
            QgsProject.instance().layerTreeRoot().findLayer(layer).setItemVisibilityChecked(True)

    QTimer.singleShot(1000, exportMap) # Wait a second and export the map

def exportMap(): # Save the map as a PNG
    global count # We need this because we'll modify its value
    iface.mapCanvas().saveAsImage( filePath + prefixes[count] + ".png" )
    print("Map with layer",count,"exported!")
    if count < len(prefixes)-1:
        QTimer.singleShot(1000, prepareMap) # Wait a second and prepare next map
    count += 1

prepareMap() # Let's start the fun
slalomchip
  • 251
  • 1
  • 9