[{"data":1,"prerenderedAt":1326},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fexport-map-canvas-to-image-pyqgis":3},{"id":4,"title":5,"body":6,"description":1315,"extension":1316,"meta":1317,"navigation":413,"path":1322,"seo":1323,"stem":1324,"__hash__":1325},"docs\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fexport-map-canvas-to-image-pyqgis\u002Findex.md","Export the Map Canvas to a PNG with PyQGIS",{"type":7,"value":8,"toc":1303},"minimark",[9,13,22,31,166,171,198,202,205,242,260,263,373,380,384,390,551,578,587,591,597,673,678,682,685,787,985,1006,1010,1016,1081,1091,1095,1160,1164,1175,1179,1193,1212,1221,1234,1250,1256,1262,1272,1276,1299],[10,11,5],"h1",{"id":12},"export-the-map-canvas-to-a-png-with-pyqgis",[14,15,16,17,21],"p",{},"Saving what is on screen looks like a one-liner, and for a genuine screenshot it is. The trouble starts the moment a script changes the extent, adds a layer or toggles visibility and then immediately grabs the image — because the canvas renders asynchronously, what lands in the file is the ",[18,19,20],"em",{},"previous"," frame. The result is an export that is correct on a slow machine and wrong on a fast one, or the reverse, which makes it maddening to debug.",[14,23,24,25,30],{},"This page is a focused recipe within ",[26,27,29],"a",{"href":28},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002F","Map Canvas Control and Image Export in PyQGIS",". It covers the direct canvas save, waiting for the render to complete, and when to abandon the canvas entirely in favour of a render job that produces the same image at any resolution you ask for.",[14,32,33],{},[34,35,40,44,48,55,72,81,88,114,124,131,140,145,150,153,156,159,162],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 250","img","A timeline showing that saveAsImage called immediately after setExtent captures the previous frame, whereas waiting for renderComplete captures the intended one","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"Why an immediate save captures the wrong frame",[45,46,47],"desc",{},"A timeline runs left to right. setExtent is called, then refresh schedules a repaint, and rendering proceeds over several hundred milliseconds. A saveAsImage call placed immediately after refresh captures the old frame still on screen. A saveAsImage call placed after the renderComplete signal captures the new frame.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","250","#f6f3ea",[56,57,58],"defs",{},[59,60,67],"marker",{"id":61,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"canvasTimeArrow","0 0 10 10","8","5","7","auto-start-reverse",[68,69],"path",{"d":70,"fill":71},"M0 0 L10 5 L0 10 z","#59645f",[73,74,80],"text",{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The canvas keeps drawing after your line of code returns",[82,83],"line",{"x1":84,"y1":85,"x2":86,"y2":85,"stroke":71,"style":87},"40","128","720","stroke-width:2.5;marker-end:url(#canvasTimeArrow)",[89,90,91,96,101,104,107,111],"g",{},[92,93],"circle",{"cx":94,"cy":85,"r":63,"fill":95},"96","#2563eb",[73,97,100],{"x":94,"y":98,"style":99,"fill":95,"textAnchor":79},"106","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","setExtent()",[92,102],{"cx":103,"cy":85,"r":63,"fill":95},"196",[73,105,106],{"x":103,"y":98,"style":99,"fill":95,"textAnchor":79},"refresh()",[92,108],{"cx":109,"cy":85,"r":63,"fill":110},"560","#15803d",[73,112,113],{"x":109,"y":98,"style":99,"fill":110,"textAnchor":79},"renderComplete",[49,115],{"x":116,"y":117,"width":118,"height":119,"rx":120,"fill":121,"fillOpacity":122,"stroke":121,"style":123},"204","140","348","20","4","#0f766e",0.25,"stroke-width:1.5",[73,125,130],{"x":126,"y":127,"style":128,"fill":129,"textAnchor":79},"378","155","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#2f3b35","rendering in progress — old frame still on screen",[49,132],{"x":133,"y":134,"width":135,"height":136,"rx":63,"fill":137,"stroke":138,"style":139},"120","180","200","52","#fffdf7","#b91c1c","stroke-width:2.5",[73,141,144],{"x":142,"y":143,"style":99,"fill":138,"textAnchor":79},"220","202","saveAsImage() here",[73,146,149],{"x":142,"y":147,"style":148,"fill":129,"textAnchor":79},"221","text-anchor:middle;font-size:11px;font-family:sans-serif","writes the previous extent",[82,151],{"x1":142,"y1":134,"x2":142,"y2":117,"stroke":138,"style":152},"stroke-width:2;stroke-dasharray:4 3;marker-end:url(#canvasTimeArrow)",[49,154],{"x":155,"y":134,"width":135,"height":136,"rx":63,"fill":137,"stroke":110,"style":139},"470",[73,157,144],{"x":158,"y":143,"style":99,"fill":110,"textAnchor":79},"570",[73,160,161],{"x":158,"y":147,"style":148,"fill":129,"textAnchor":79},"writes what you asked for",[82,163],{"x1":158,"y1":134,"x2":164,"y2":117,"stroke":110,"style":165},"562","stroke-width:2;marker-end:url(#canvasTimeArrow)",[167,168,170],"h2",{"id":169},"prerequisites","Prerequisites",[172,173,174,187,190],"ul",{},[175,176,177,181,182,186],"li",{},[178,179,180],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer, running as a desktop application — ",[183,184,185],"code",{},"saveAsImage()"," needs a real canvas.",[175,188,189],{},"A project with visible layers, and write permission to the output directory.",[175,191,192,193,197],{},"For the render-job alternative, no GUI is needed at all; see ",[26,194,196],{"href":195},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Frender-layer-to-image-without-gui-pyqgis\u002F","Render a Layer to an Image Headless in PyQGIS",".",[167,199,201],{"id":200},"the-direct-save","The direct save",[14,203,204],{},"When the canvas already shows what you want, one call is enough.",[206,207,212],"pre",{"className":208,"code":209,"language":210,"meta":211,"style":211},"language-python shiki shiki-themes github-dark","canvas = iface.mapCanvas()\ncanvas.saveAsImage(\"\u002Fdata\u002Fexports\u002Fcurrent_view.png\")\n","python","",[183,213,214,229],{"__ignoreMap":211},[215,216,218,222,226],"span",{"class":82,"line":217},1,[215,219,221],{"class":220},"s95oV","canvas ",[215,223,225],{"class":224},"snl16","=",[215,227,228],{"class":220}," iface.mapCanvas()\n",[215,230,232,235,239],{"class":82,"line":231},2,[215,233,234],{"class":220},"canvas.saveAsImage(",[215,236,238],{"class":237},"sU2Wk","\"\u002Fdata\u002Fexports\u002Fcurrent_view.png\"",[215,240,241],{"class":220},")\n",[14,243,244,247,248,251,252,255,256,259],{},[178,245,246],{},"Breakdown:"," The image is written at exactly the canvas widget's pixel size, so the resolution depends on the user's window and screen. A ",[183,249,250],{},".pgw"," world file is written alongside it automatically, which means the PNG can be dropped back into QGIS or any other GIS as a georeferenced raster. The format is inferred from the extension — ",[183,253,254],{},".jpg"," and ",[183,257,258],{},".tif"," both work, though PNG is the right default for map output because it is lossless and handles flat colour well.",[14,261,262],{},"The world file is what makes the result more than a picture. It records the pixel size and the coordinates of the top-left corner, so any GIS can place the image back on the map at the right scale and position.",[14,264,265],{},[34,266,269,272,275,278,285,288,294,300,307,311,316,318,321,325,329,368],{"viewBox":267,"role":37,"ariaLabel":268,"xmlns":39},"0 0 760 232","A PNG and its accompanying world file, whose six lines record the pixel ground size, rotation terms and the coordinates of the centre of the top-left pixel",[41,270,271],{},"What the world file next to the PNG contains",[45,273,274],{},"A saved PNG is paired with a six-line world file. The first and fourth lines give the ground size of one pixel in x and y, with the y value negative because image rows run downward. The middle two lines are rotation terms, normally zero. The final two lines give the map coordinates of the centre of the top-left pixel.",[49,276],{"x":51,"y":51,"width":52,"height":277,"fill":54},"232",[56,279,280],{},[59,281,283],{"id":282,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"canvasPgwArrow",[68,284],{"d":70,"fill":121},[73,286,287],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"saveAsImage() writes two files, not one",[49,289],{"x":290,"y":136,"width":291,"height":292,"rx":293,"fill":137,"stroke":95,"style":139},"16","252","160","10",[73,295,299],{"x":296,"y":297,"style":298,"fill":95,"textAnchor":79},"142","76","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","current_view.png",[49,301],{"x":302,"y":303,"width":304,"height":305,"fill":306,"stroke":71,"style":123},"48","92","188","94","#e7e2d4",[68,308],{"d":309,"fill":121,"fillOpacity":122,"stroke":121,"style":310},"M64 176 L64 110 L118 104 L150 138 L112 180 Z","stroke-width:1.8",[68,312],{"d":313,"fill":314,"fillOpacity":315,"stroke":314,"style":310},"M170 178 L166 116 L228 110 L232 174 Z","#b45309",0.22,[92,317],{"cx":302,"cy":303,"r":64,"fill":138},[73,319,320],{"x":296,"y":116,"style":148,"fill":129,"textAnchor":79},"pixels, no georeferencing",[49,322],{"x":323,"y":136,"width":324,"height":292,"rx":293,"fill":137,"stroke":110,"style":139},"332","412",[73,326,328],{"x":327,"y":297,"style":298,"fill":110,"textAnchor":79},"538","current_view.pgw",[89,330,332,337,342,346,349,352,354,358,361,365],{"style":331},"font-size:11px;font-family:monospace",[73,333,336],{"x":334,"y":335,"fill":129},"356","102","2.4500000",[73,338,341],{"x":339,"y":335,"style":340,"fill":71},"500","font-size:10.5px;font-family:sans-serif","← metres per pixel, x",[73,343,345],{"x":334,"y":344,"fill":129},"124","0.0000000",[73,347,348],{"x":339,"y":344,"style":340,"fill":71},"← rotation, normally 0",[73,350,345],{"x":334,"y":351,"fill":129},"146",[73,353,348],{"x":339,"y":351,"style":340,"fill":71},[73,355,357],{"x":334,"y":356,"fill":129},"168","-2.4500000",[73,359,360],{"x":339,"y":356,"style":340,"fill":71},"← metres per pixel, y (negative)",[73,362,364],{"x":334,"y":363,"fill":129},"190","504812.25",[73,366,367],{"x":339,"y":363,"style":340,"fill":71},"← x of the top-left pixel centre",[82,369],{"x1":370,"y1":94,"x2":371,"y2":94,"stroke":121,"style":372},"268","328","stroke-width:2;marker-end:url(#canvasPgwArrow)",[14,374,375,376,379],{},"The y scale is negative because image rows run downward while map coordinates run upward — a sign error there is the usual reason a re-imported export appears mirrored. The file carries no CRS of its own, so a ",[183,377,378],{},".prj"," alongside it is worth writing when the image will travel.",[167,381,383],{"id":382},"wait-for-the-render-to-finish","Wait for the render to finish",[14,385,386,387,389],{},"As soon as the script changes anything, the save has to wait. The canvas emits ",[183,388,113],{}," when it has finished, and connecting to it once is the reliable pattern.",[206,391,393],{"className":208,"code":392,"language":210,"meta":211,"style":211},"from qgis.core import QgsProject\n\ncanvas = iface.mapCanvas()\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\n\n\ndef save_once(painter):\n    canvas.renderComplete.disconnect(save_once)\n    canvas.saveAsImage(\"\u002Fdata\u002Fexports\u002Fparcels_view.png\")\n    print(\"written\")\n\n\nextent = layer.extent()\nextent.scale(1.1)\ncanvas.setExtent(extent)\ncanvas.renderComplete.connect(save_once)\ncanvas.refreshAllLayers()\n",[183,394,395,409,415,424,447,452,457,470,476,487,501,506,511,522,533,539,545],{"__ignoreMap":211},[215,396,397,400,403,406],{"class":82,"line":217},[215,398,399],{"class":224},"from",[215,401,402],{"class":220}," qgis.core ",[215,404,405],{"class":224},"import",[215,407,408],{"class":220}," QgsProject\n",[215,410,411],{"class":82,"line":231},[215,412,414],{"emptyLinePlaceholder":413},true,"\n",[215,416,418,420,422],{"class":82,"line":417},3,[215,419,221],{"class":220},[215,421,225],{"class":224},[215,423,228],{"class":220},[215,425,427,430,432,435,438,441,444],{"class":82,"line":426},4,[215,428,429],{"class":220},"layer ",[215,431,225],{"class":224},[215,433,434],{"class":220}," QgsProject.instance().mapLayersByName(",[215,436,437],{"class":237},"\"parcels\"",[215,439,440],{"class":220},")[",[215,442,51],{"class":443},"sDLfK",[215,445,446],{"class":220},"]\n",[215,448,450],{"class":82,"line":449},5,[215,451,414],{"emptyLinePlaceholder":413},[215,453,455],{"class":82,"line":454},6,[215,456,414],{"emptyLinePlaceholder":413},[215,458,460,463,467],{"class":82,"line":459},7,[215,461,462],{"class":224},"def",[215,464,466],{"class":465},"svObZ"," save_once",[215,468,469],{"class":220},"(painter):\n",[215,471,473],{"class":82,"line":472},8,[215,474,475],{"class":220},"    canvas.renderComplete.disconnect(save_once)\n",[215,477,479,482,485],{"class":82,"line":478},9,[215,480,481],{"class":220},"    canvas.saveAsImage(",[215,483,484],{"class":237},"\"\u002Fdata\u002Fexports\u002Fparcels_view.png\"",[215,486,241],{"class":220},[215,488,490,493,496,499],{"class":82,"line":489},10,[215,491,492],{"class":443},"    print",[215,494,495],{"class":220},"(",[215,497,498],{"class":237},"\"written\"",[215,500,241],{"class":220},[215,502,504],{"class":82,"line":503},11,[215,505,414],{"emptyLinePlaceholder":413},[215,507,509],{"class":82,"line":508},12,[215,510,414],{"emptyLinePlaceholder":413},[215,512,514,517,519],{"class":82,"line":513},13,[215,515,516],{"class":220},"extent ",[215,518,225],{"class":224},[215,520,521],{"class":220}," layer.extent()\n",[215,523,525,528,531],{"class":82,"line":524},14,[215,526,527],{"class":220},"extent.scale(",[215,529,530],{"class":443},"1.1",[215,532,241],{"class":220},[215,534,536],{"class":82,"line":535},15,[215,537,538],{"class":220},"canvas.setExtent(extent)\n",[215,540,542],{"class":82,"line":541},16,[215,543,544],{"class":220},"canvas.renderComplete.connect(save_once)\n",[215,546,548],{"class":82,"line":547},17,[215,549,550],{"class":220},"canvas.refreshAllLayers()\n",[14,552,553,555,556,558,559,562,563,566,567,570,571,573,574,577],{},[178,554,246],{}," ",[183,557,113],{}," passes a ",[183,560,561],{},"QPainter"," to its handlers, which is why ",[183,564,565],{},"save_once"," takes an argument it never uses. Disconnecting inside the handler is what stops the save firing on every subsequent pan and zoom for the rest of the session — a leaked connection here is a classic plugin bug. ",[183,568,569],{},"refreshAllLayers()"," rather than ",[183,572,106],{}," forces the layers to re-read from their providers, which matters when the data changed rather than just the view. Connecting ",[18,575,576],{},"after"," setting the extent avoids catching a repaint triggered by something else.",[14,579,580,581,583,584,586],{},"This is inherently asynchronous, so the script continues past ",[183,582,569],{}," immediately. Anything that must happen after the file exists belongs inside ",[183,585,565],{},", not below the connect call — a distinction that trips up scripts written as a linear sequence.",[167,588,590],{"id":589},"control-the-output-size","Control the output size",[14,592,593,594,596],{},"Because ",[183,595,185],{}," follows the widget, the only way to control resolution through the canvas is to resize the widget itself. That works and is ugly:",[206,598,600],{"className":208,"code":599,"language":210,"meta":211,"style":211},"from qgis.PyQt.QtCore import QSize\n\ncanvas = iface.mapCanvas()\noriginal = canvas.size()\n\ncanvas.resize(QSize(1600, 1200))\ncanvas.refresh()\n# ... wait for renderComplete, then save ...\ncanvas.resize(original)\n",[183,601,602,614,618,626,636,640,657,662,668],{"__ignoreMap":211},[215,603,604,606,609,611],{"class":82,"line":217},[215,605,399],{"class":224},[215,607,608],{"class":220}," qgis.PyQt.QtCore ",[215,610,405],{"class":224},[215,612,613],{"class":220}," QSize\n",[215,615,616],{"class":82,"line":231},[215,617,414],{"emptyLinePlaceholder":413},[215,619,620,622,624],{"class":82,"line":417},[215,621,221],{"class":220},[215,623,225],{"class":224},[215,625,228],{"class":220},[215,627,628,631,633],{"class":82,"line":426},[215,629,630],{"class":220},"original ",[215,632,225],{"class":224},[215,634,635],{"class":220}," canvas.size()\n",[215,637,638],{"class":82,"line":449},[215,639,414],{"emptyLinePlaceholder":413},[215,641,642,645,648,651,654],{"class":82,"line":454},[215,643,644],{"class":220},"canvas.resize(QSize(",[215,646,647],{"class":443},"1600",[215,649,650],{"class":220},", ",[215,652,653],{"class":443},"1200",[215,655,656],{"class":220},"))\n",[215,658,659],{"class":82,"line":459},[215,660,661],{"class":220},"canvas.refresh()\n",[215,663,664],{"class":82,"line":472},[215,665,667],{"class":666},"sjoCn","# ... wait for renderComplete, then save ...\n",[215,669,670],{"class":82,"line":478},[215,671,672],{"class":220},"canvas.resize(original)\n",[14,674,675,677],{},[178,676,246],{}," Resizing the canvas visibly disturbs the user's layout and forces two extra renders. It also cannot exceed the screen size in a meaningful way. Restoring the original size afterwards is essential but does not make the approach pleasant. This is the point at which the canvas stops being the right tool.",[167,679,681],{"id":680},"use-a-render-job-instead","Use a render job instead",[14,683,684],{},"For any output whose size, DPI or background must be predictable, build the settings yourself. Nothing about this depends on the canvas, so the same code runs headless.",[14,686,687],{},[34,688,691,694,697,702,705,710,717,722,726,781],{"viewBox":689,"role":37,"ariaLabel":690,"xmlns":39},"155 -2 605 244","A comparison of canvas export and render job export across output size, DPI control, background colour, determinism and whether a GUI is required",[41,692,693],{},"Canvas export versus a render job",[45,695,696],{},"A five-row comparison. Canvas export ties the output size to the widget, offers no DPI control, inherits the project background, depends on the current view state and needs a GUI. A render job sets size and DPI explicitly, sets the background colour, produces identical output on every run and requires no GUI at all.",[49,698],{"x":127,"y":699,"width":700,"height":701,"fill":54},"-2","605","244",[73,703,704],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"Use the canvas for a screenshot, a job for a deliverable",[82,706],{"x1":707,"y1":708,"x2":709,"y2":708,"stroke":71,"style":123},"248","70","744",[73,711,716],{"x":712,"y":713,"style":714,"fill":78,"textAnchor":715},"236","62","text-anchor:end;font-size:11.5px;font-weight:bold;font-family:sans-serif","end","aspect",[73,718,721],{"x":719,"y":713,"style":720,"fill":314,"textAnchor":79},"368","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","canvas.saveAsImage()",[73,723,725],{"x":724,"y":713,"style":720,"fill":110,"textAnchor":79},"620","render job",[89,727,729,734,738,741,745,748,751,755,758,761,765,768,771,775,778],{"style":728},"font-size:11.5px;font-family:sans-serif",[73,730,733],{"x":712,"y":731,"style":732,"fill":129,"textAnchor":715},"98","text-anchor:end","output size",[73,735,737],{"x":719,"y":731,"style":736,"fill":314,"textAnchor":79},"text-anchor:middle","whatever the widget is",[73,739,740],{"x":724,"y":731,"style":736,"fill":110,"textAnchor":79},"setOutputSize()",[73,742,744],{"x":712,"y":743,"style":732,"fill":129,"textAnchor":715},"126","DPI",[73,746,747],{"x":719,"y":743,"style":736,"fill":314,"textAnchor":79},"screen DPI",[73,749,750],{"x":724,"y":743,"style":736,"fill":110,"textAnchor":79},"setOutputDpi()",[73,752,754],{"x":712,"y":753,"style":732,"fill":129,"textAnchor":715},"154","background",[73,756,757],{"x":719,"y":753,"style":736,"fill":314,"textAnchor":79},"project setting",[73,759,760],{"x":724,"y":753,"style":736,"fill":110,"textAnchor":79},"setBackgroundColor()",[73,762,764],{"x":712,"y":763,"style":732,"fill":129,"textAnchor":715},"182","reproducible",[73,766,767],{"x":719,"y":763,"style":736,"fill":314,"textAnchor":79},"depends on the view",[73,769,770],{"x":724,"y":763,"style":736,"fill":110,"textAnchor":79},"identical every run",[73,772,774],{"x":712,"y":773,"style":732,"fill":129,"textAnchor":715},"210","needs a GUI",[73,776,777],{"x":719,"y":773,"style":736,"fill":314,"textAnchor":79},"yes",[73,779,780],{"x":724,"y":773,"style":736,"fill":110,"textAnchor":79},"no",[49,782],{"x":339,"y":783,"width":784,"height":351,"rx":63,"fill":110,"fillOpacity":785,"stroke":110,"style":786},"80","240",0.06,"stroke-width:2;stroke-dasharray:5 4",[206,788,790],{"className":208,"code":789,"language":210,"meta":211,"style":211},"from qgis.core import QgsMapSettings, QgsMapRendererParallelJob, QgsProject\nfrom qgis.PyQt.QtCore import QSize\nfrom qgis.PyQt.QtGui import QColor\n\ncanvas = iface.mapCanvas()\n\nsettings = QgsMapSettings(canvas.mapSettings())   # start from what is on screen\nsettings.setOutputSize(QSize(1920, 1080))\nsettings.setOutputDpi(150)\nsettings.setBackgroundColor(QColor(\"#ffffff\"))\n\nextent = canvas.extent()\nwidth, height = 1920, int(1920 * extent.height() \u002F extent.width())\nsettings.setOutputSize(QSize(width, height))\nsettings.setExtent(extent)\n\njob = QgsMapRendererParallelJob(settings)\njob.start()\njob.waitForFinished()\njob.renderedImage().save(\"\u002Fdata\u002Fexports\u002Fmap.png\", \"png\")\n",[183,791,792,803,813,825,829,837,841,854,869,879,889,893,902,933,938,943,947,957,963,969],{"__ignoreMap":211},[215,793,794,796,798,800],{"class":82,"line":217},[215,795,399],{"class":224},[215,797,402],{"class":220},[215,799,405],{"class":224},[215,801,802],{"class":220}," QgsMapSettings, QgsMapRendererParallelJob, QgsProject\n",[215,804,805,807,809,811],{"class":82,"line":231},[215,806,399],{"class":224},[215,808,608],{"class":220},[215,810,405],{"class":224},[215,812,613],{"class":220},[215,814,815,817,820,822],{"class":82,"line":417},[215,816,399],{"class":224},[215,818,819],{"class":220}," qgis.PyQt.QtGui ",[215,821,405],{"class":224},[215,823,824],{"class":220}," QColor\n",[215,826,827],{"class":82,"line":426},[215,828,414],{"emptyLinePlaceholder":413},[215,830,831,833,835],{"class":82,"line":449},[215,832,221],{"class":220},[215,834,225],{"class":224},[215,836,228],{"class":220},[215,838,839],{"class":82,"line":454},[215,840,414],{"emptyLinePlaceholder":413},[215,842,843,846,848,851],{"class":82,"line":459},[215,844,845],{"class":220},"settings ",[215,847,225],{"class":224},[215,849,850],{"class":220}," QgsMapSettings(canvas.mapSettings())   ",[215,852,853],{"class":666},"# start from what is on screen\n",[215,855,856,859,862,864,867],{"class":82,"line":472},[215,857,858],{"class":220},"settings.setOutputSize(QSize(",[215,860,861],{"class":443},"1920",[215,863,650],{"class":220},[215,865,866],{"class":443},"1080",[215,868,656],{"class":220},[215,870,871,874,877],{"class":82,"line":478},[215,872,873],{"class":220},"settings.setOutputDpi(",[215,875,876],{"class":443},"150",[215,878,241],{"class":220},[215,880,881,884,887],{"class":82,"line":489},[215,882,883],{"class":220},"settings.setBackgroundColor(QColor(",[215,885,886],{"class":237},"\"#ffffff\"",[215,888,656],{"class":220},[215,890,891],{"class":82,"line":503},[215,892,414],{"emptyLinePlaceholder":413},[215,894,895,897,899],{"class":82,"line":508},[215,896,516],{"class":220},[215,898,225],{"class":224},[215,900,901],{"class":220}," canvas.extent()\n",[215,903,904,907,909,912,914,917,919,921,924,927,930],{"class":82,"line":513},[215,905,906],{"class":220},"width, height ",[215,908,225],{"class":224},[215,910,911],{"class":443}," 1920",[215,913,650],{"class":220},[215,915,916],{"class":443},"int",[215,918,495],{"class":220},[215,920,861],{"class":443},[215,922,923],{"class":224}," *",[215,925,926],{"class":220}," extent.height() ",[215,928,929],{"class":224},"\u002F",[215,931,932],{"class":220}," extent.width())\n",[215,934,935],{"class":82,"line":524},[215,936,937],{"class":220},"settings.setOutputSize(QSize(width, height))\n",[215,939,940],{"class":82,"line":535},[215,941,942],{"class":220},"settings.setExtent(extent)\n",[215,944,945],{"class":82,"line":541},[215,946,414],{"emptyLinePlaceholder":413},[215,948,949,952,954],{"class":82,"line":547},[215,950,951],{"class":220},"job ",[215,953,225],{"class":224},[215,955,956],{"class":220}," QgsMapRendererParallelJob(settings)\n",[215,958,960],{"class":82,"line":959},18,[215,961,962],{"class":220},"job.start()\n",[215,964,966],{"class":82,"line":965},19,[215,967,968],{"class":220},"job.waitForFinished()\n",[215,970,972,975,978,980,983],{"class":82,"line":971},20,[215,973,974],{"class":220},"job.renderedImage().save(",[215,976,977],{"class":237},"\"\u002Fdata\u002Fexports\u002Fmap.png\"",[215,979,650],{"class":220},[215,981,982],{"class":237},"\"png\"",[215,984,241],{"class":220},[14,986,987,989,990,993,994,997,998,1001,1002,1005],{},[178,988,246],{}," Copy-constructing ",[183,991,992],{},"QgsMapSettings"," from ",[183,995,996],{},"canvas.mapSettings()"," inherits the current layers, CRS and flags — including ",[183,999,1000],{},"DrawLabeling",", which is the usual reason a hand-built settings object loses its labels. Deriving the height from the extent's proportions stops QGIS silently expanding the extent to match a mismatched aspect ratio. ",[183,1003,1004],{},"waitForFinished()"," blocks until the render is genuinely complete, which is what removes the asynchrony problem entirely rather than working around it.",[167,1007,1009],{"id":1008},"qgis-version-compatibility","QGIS version compatibility",[14,1011,1012,1013,1015],{},"The examples target ",[178,1014,180],{}," (Python 3.12).",[1017,1018,1019,1035],"table",{},[1020,1021,1022],"thead",{},[1023,1024,1025,1029,1032],"tr",{},[1026,1027,1028],"th",{},"QGIS version",[1026,1030,1031],{},"Python",[1026,1033,1034],{},"Notes",[1036,1037,1038,1053,1064],"tbody",{},[1023,1039,1040,1044,1047],{},[1041,1042,1043],"td",{},"3.28 LTR",[1041,1045,1046],{},"3.9",[1041,1048,1049,1050,1052],{},"Identical API. ",[183,1051,185],{}," writes a world file in all 3.x.",[1023,1054,1055,1058,1061],{},[1041,1056,1057],{},"3.34 LTR",[1041,1059,1060],{},"3.12",[1041,1062,1063],{},"Baseline for this page.",[1023,1065,1066,1069,1071],{},[1041,1067,1068],{},"3.40 \u002F 3.44",[1041,1070,1060],{},[1041,1072,1073,1076,1077,1080],{},[183,1074,1075],{},"Qgis.MapSettingsFlag"," replaces ",[183,1078,1079],{},"QgsMapSettings.Flag","; the old spelling still resolves.",[14,1082,1083,650,1085,255,1087,1090],{},[183,1084,185],{},[183,1086,113],{},[183,1088,1089],{},"QgsMapRendererParallelJob"," are unchanged across the 3.x line.",[167,1092,1094],{"id":1093},"troubleshooting","Troubleshooting",[172,1096,1097,1108,1118,1129,1141,1151],{},[175,1098,1099,1102,1103,1105,1106,197],{},[178,1100,1101],{},"The image shows the previous extent."," The save ran before rendering finished. Connect to ",[183,1104,113],{},", or switch to a render job with ",[183,1107,1004],{},[175,1109,1110,1113,1114,1117],{},[178,1111,1112],{},"The file is written but blank."," The canvas had no visible layers, or the render job's ",[183,1115,1116],{},"setLayers()"," list was empty. Print the layer count before rendering.",[175,1119,1120,1123,1124,1126,1127,197],{},[178,1121,1122],{},"The background is transparent."," A hand-built ",[183,1125,992],{}," defaults to transparent. Call ",[183,1128,760],{},[175,1130,1131,1134,1135,1137,1138,1140],{},[178,1132,1133],{},"Labels are missing."," The settings object does not have ",[183,1136,1000],{}," set. Copy-construct from ",[183,1139,996],{}," or set the flag explicitly.",[175,1142,1143,555,1146,1148,1149,197],{},[178,1144,1145],{},"The image resolution is too low.",[183,1147,185],{}," follows the widget size. Use a render job and set ",[183,1150,740],{},[175,1152,1153,1156,1157,1159],{},[178,1154,1155],{},"The export keeps firing on every pan."," The ",[183,1158,113],{}," handler was never disconnected. Disconnect inside the handler.",[167,1161,1163],{"id":1162},"conclusion","Conclusion",[14,1165,1166,1168,1169,1171,1172,1174],{},[183,1167,721],{}," is right for a screenshot of what the user is already looking at. The moment a script changes the view, either wait for ",[183,1170,113],{}," or — better — build a ",[183,1173,992],{}," and render it synchronously, which gives you control over size, DPI and background and produces the same file on every run.",[167,1176,1178],{"id":1177},"frequently-asked-questions","Frequently Asked Questions",[14,1180,1181,1184,1185,1187,1188,1190,1191,197],{},[178,1182,1183],{},"Why does my exported image show the wrong extent?","\nThe canvas renders asynchronously, so a save called immediately after ",[183,1186,100],{}," captures the frame still on screen. Connect to the canvas's ",[183,1189,113],{}," signal and save from the handler, or use a render job and call ",[183,1192,1004],{},[14,1194,1195,1198,1199,1201,1202,1204,1205,255,1207,1209,1210,197],{},[178,1196,1197],{},"How do I control the exported image size?","\nNot through ",[183,1200,185],{},", which follows the canvas widget. Build a ",[183,1203,992],{},", call ",[183,1206,740],{},[183,1208,750],{},", and render it with ",[183,1211,1089],{},[14,1213,1214,1217,1218,1220],{},[178,1215,1216],{},"Does saveAsImage produce a georeferenced file?","\nYes. It writes a world file — ",[183,1219,250],{}," for a PNG — next to the image, so the result can be loaded back as a georeferenced raster.",[14,1222,1223,1226,1227,1229,1230,1233],{},[178,1224,1225],{},"Why is my exported map background transparent?","\nA ",[183,1228,992],{}," built from scratch defaults to a transparent background. Call ",[183,1231,1232],{},"setBackgroundColor(QColor(\"#ffffff\"))",", or copy-construct the settings from the canvas so the project background is inherited.",[14,1235,1236,1198,1239,1241,1242,1244,1245,255,1247,1249],{},[178,1237,1238],{},"Can I export the canvas at a higher resolution without resizing the window?",[183,1240,185],{},", which is tied to the widget's pixel dimensions. Copy the canvas's map settings into a fresh ",[183,1243,992],{},", raise ",[183,1246,740],{},[183,1248,750],{}," on the copy, and render that with a job. The result matches what is on screen but at whatever resolution you asked for.",[14,1251,1252,1255],{},[178,1253,1254],{},"What image formats can I write?","\nAnything Qt's image writer supports — PNG, JPEG, TIFF and BMP among them. PNG is the right default for maps because it is lossless and handles large areas of flat colour efficiently. JPEG introduces visible artefacts around labels and thin lines, and cannot carry transparency at all.",[14,1257,1258,1261],{},[178,1259,1260],{},"Is there a size limit on a rendered image?","\nPractically, yes: the image is held in memory uncompressed, so a 20 000 by 20 000 pixel render needs well over a gigabyte before it is written. For very large outputs, render in tiles and stitch them, or export from a layout to a format that streams.",[14,1263,1264,1267,1268,1271],{},[178,1265,1266],{},"Does the export include layers that are switched off?","\nNo. ",[183,1269,1270],{},"canvas.layers()"," returns only the visible layers, so anything unticked in the layer panel is absent. Build the layer list explicitly from the project if the export should not depend on the panel state.",[167,1273,1275],{"id":1274},"related","Related",[172,1277,1278,1283,1287,1293],{},[175,1279,1280,1282],{},[26,1281,29],{"href":28}," — the guide this recipe belongs to",[175,1284,1285],{},[26,1286,196],{"href":195},[175,1288,1289],{},[26,1290,1292],{"href":1291},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fzoom-to-layer-extent-pyqgis\u002F","Zoom the Canvas to a Layer Extent in PyQGIS",[175,1294,1295],{},[26,1296,1298],{"href":1297},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fset-map-canvas-background-color-pyqgis\u002F","Set the Map Canvas Background Colour in PyQGIS",[1300,1301,1302],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":211,"searchDepth":231,"depth":231,"links":1304},[1305,1306,1307,1308,1309,1310,1311,1312,1313,1314],{"id":169,"depth":231,"text":170},{"id":200,"depth":231,"text":201},{"id":382,"depth":231,"text":383},{"id":589,"depth":231,"text":590},{"id":680,"depth":231,"text":681},{"id":1008,"depth":231,"text":1009},{"id":1093,"depth":231,"text":1094},{"id":1162,"depth":231,"text":1163},{"id":1177,"depth":231,"text":1178},{"id":1274,"depth":231,"text":1275},"Save the QGIS map canvas as an image from Python. Use saveAsImage for a screenshot, wait for rendering to finish, and switch to a render job for reproducible output.","md",{"slug":1318,"type":1319,"breadcrumb":1320,"datePublished":1321,"dateModified":1321},"export-map-canvas-to-image-pyqgis","article","Export Canvas to PNG","2026-08-01","\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fexport-map-canvas-to-image-pyqgis",{"title":5,"description":1315},"pyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fexport-map-canvas-to-image-pyqgis\u002Findex","5WEiR5jDMB0hcYQfwLhrfKRX5-hD72kJoPKDJnljEc0",1785585990471]