[{"data":1,"prerenderedAt":2337},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export":3},{"id":4,"title":5,"body":6,"description":2326,"extension":2327,"meta":2328,"navigation":286,"path":2333,"seo":2334,"stem":2335,"__hash__":2336},"docs\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Findex.md","Map Canvas Control and Image Export in PyQGIS",{"type":7,"value":8,"toc":2309},"minimark",[9,13,26,35,205,210,244,248,257,355,389,396,400,403,516,541,643,646,680,688,692,699,723,732,736,746,892,1087,1121,1126,1129,1194,1199,1203,1210,1217,1379,1393,1397,1402,1450,1472,1483,1487,1490,1599,1612,1623,1626,1688,1707,1711,1714,2023,2036,2047,2051,2054,2122,2126,2189,2193,2209,2224,2230,2240,2253,2257,2305],[10,11,5],"h1",{"id":12},"map-canvas-control-and-image-export-in-pyqgis",[14,15,16,17,21,22,25],"p",{},"Turning a styled project into a picture is the last step of most cartographic automation, and it is the step where scripts most often diverge from what the author saw on screen: the wrong extent, an unexpected scale, a transparent background, or labels that were never drawn because the render finished before they were placed. All of those come from the same root cause — confusing the ",[18,19,20],"em",{},"canvas",", which is a widget showing a view, with the ",[18,23,24],{},"map settings",", which are the recipe a renderer follows.",[14,27,28,29,34],{},"This guide sits inside ",[30,31,33],"a",{"href":32},"\u002Fpyqgis-cartography-visualization\u002F","PyQGIS Cartography & Data Visualization",". It covers what the canvas actually is, how to control the visible extent and scale, how to export exactly what is on screen, and — the part that matters most for automation — how to render a map to an image with no canvas and no GUI at all, which is what any scheduled or server-side job needs.",[14,36,37],{},[38,39,44,48,52,59,76,85,94,100,105,109,117,123,126,129,132,137,141,145,148,151,158,163,166,170,174,178,182,185,192,196,201],"svg",{"viewBox":40,"role":41,"ariaLabel":42,"xmlns":43},"0 0 760 262","img","The rendering pipeline: layers and their styles feed QgsMapSettings, which a render job turns into pixels, delivered either to the on-screen canvas or straight to an image file","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[45,46,47],"title",{},"Canvas and headless export share one pipeline",[49,50,51],"desc",{},"Layers with their renderers feed into QgsMapSettings, which holds the extent, output size, DPI, CRS and background colour. A render job consumes those settings and produces pixels. Those pixels are either painted onto the on-screen QgsMapCanvas widget or written directly to a PNG file, with no GUI involved.",[53,54],"rect",{"x":55,"y":55,"width":56,"height":57,"fill":58},"0","760","262","#f6f3ea",[60,61,62],"defs",{},[63,64,71],"marker",{"id":65,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"canvasPipeArrow","0 0 10 10","8","5","7","auto-start-reverse",[72,73],"path",{"d":74,"fill":75},"M0 0 L10 5 L0 10 z","#2f3b35",[77,78,84],"text",{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The canvas is one consumer of the pipeline, not the pipeline itself",[53,86],{"x":87,"y":88,"width":89,"height":88,"rx":90,"fill":91,"stroke":92,"style":93},"16","86","150","10","#fffdf7","#2563eb","stroke-width:2.5",[77,95,99],{"x":96,"y":97,"style":98,"fill":82,"textAnchor":83},"91","114","text-anchor:middle;font-size:12.5px;font-weight:bold;font-family:sans-serif","layers",[77,101,104],{"x":96,"y":102,"style":103,"fill":75,"textAnchor":83},"136","text-anchor:middle;font-size:11.5px;font-family:sans-serif","+ renderers",[77,106,108],{"x":96,"y":107,"style":103,"fill":92,"textAnchor":83},"154","+ label settings",[53,110],{"x":111,"y":112,"width":113,"height":114,"rx":90,"fill":115,"stroke":116,"style":93},"206","70","196","118","#26322d","#0f766e",[77,118,122],{"x":119,"y":120,"style":98,"fill":121,"textAnchor":83},"304","96","#d9f99d","QgsMapSettings",[77,124,125],{"x":119,"y":114,"style":103,"fill":121,"textAnchor":83},"extent · output size",[77,127,128],{"x":119,"y":102,"style":103,"fill":121,"textAnchor":83},"DPI · destination CRS",[77,130,131],{"x":119,"y":107,"style":103,"fill":121,"textAnchor":83},"background · flags",[77,133,136],{"x":119,"y":134,"style":135,"fill":121,"textAnchor":83},"174","text-anchor:middle;font-size:11px;font-family:sans-serif","the whole recipe",[53,138],{"x":139,"y":88,"width":89,"height":88,"rx":90,"fill":91,"stroke":140,"style":93},"442","#b45309",[77,142,144],{"x":143,"y":97,"style":98,"fill":82,"textAnchor":83},"517","render job",[77,146,147],{"x":143,"y":102,"style":103,"fill":75,"textAnchor":83},"draws to a QImage",[77,149,150],{"x":143,"y":107,"style":103,"fill":140,"textAnchor":83},"parallel or sequential",[53,152],{"x":153,"y":154,"width":155,"height":156,"rx":90,"fill":91,"stroke":157,"style":93},"632","46","112","72","#59645f",[77,159,20],{"x":160,"y":161,"style":162,"fill":82,"textAnchor":83},"688","76","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif",[77,164,165],{"x":160,"y":120,"style":103,"fill":75,"textAnchor":83},"on screen",[77,167,169],{"x":160,"y":168,"style":135,"fill":157,"textAnchor":83},"110","needs a GUI",[53,171],{"x":153,"y":172,"width":155,"height":156,"rx":90,"fill":91,"stroke":173,"style":93},"142","#15803d",[77,175,177],{"x":160,"y":176,"style":162,"fill":173,"textAnchor":83},"172","PNG file",[77,179,181],{"x":160,"y":180,"style":103,"fill":75,"textAnchor":83},"192","headless",[77,183,184],{"x":160,"y":111,"style":135,"fill":173,"textAnchor":83},"no GUI needed",[186,187],"line",{"x1":188,"y1":189,"x2":190,"y2":189,"stroke":75,"style":191},"166","128","202","stroke-width:2.5;marker-end:url(#canvasPipeArrow)",[186,193],{"x1":194,"y1":189,"x2":195,"y2":189,"stroke":75,"style":191},"402","438",[186,197],{"x1":198,"y1":155,"x2":199,"y2":200,"stroke":157,"style":191},"592","628","90",[186,202],{"x1":198,"y1":203,"x2":199,"y2":204,"stroke":173,"style":191},"146","170",[206,207,209],"h2",{"id":208},"prerequisites","Prerequisites",[211,212,213,221,229,237],"ul",{},[214,215,216,220],"li",{},[217,218,219],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[214,222,223,224,228],{},"A project with at least one styled layer. If styling is what you are automating, start with ",[30,225,227],{"href":226},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002F","Programmatic Layer Styling",".",[214,230,231,232,236],{},"For the canvas sections, QGIS Desktop with ",[233,234,235],"code",{},"iface"," available — the Python Console or a plugin.",[214,238,239,240,228],{},"For the headless sections, a working standalone environment; see ",[30,241,243],{"href":242},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Frunning-python-scripts-outside-qgis-desktop\u002F","Running Python Scripts Outside QGIS Desktop",[206,245,247],{"id":246},"the-canvas-is-a-view-not-the-map","The canvas is a view, not the map",[14,249,250,253,254,256],{},[233,251,252],{},"QgsMapCanvas"," is a Qt widget. It owns a ",[233,255,122],{}," describing what to draw, and it repaints itself when that changes. Almost everything you might want to \"do to the map\" is really a change to those settings followed by a refresh.",[258,259,264],"pre",{"className":260,"code":261,"language":262,"meta":263,"style":263},"language-python shiki shiki-themes github-dark","canvas = iface.mapCanvas()\n\nprint(canvas.extent().toString(2))          # current visible rectangle\nprint(canvas.scale())                        # denominator, e.g. 25000.0\nprint(canvas.mapUnitsPerPixel())\nprint([layer.name() for layer in canvas.layers()])\nprint(canvas.mapSettings().destinationCrs().authid())\n","python","",[233,265,266,281,288,308,319,327,347],{"__ignoreMap":263},[267,268,270,274,278],"span",{"class":186,"line":269},1,[267,271,273],{"class":272},"s95oV","canvas ",[267,275,277],{"class":276},"snl16","=",[267,279,280],{"class":272}," iface.mapCanvas()\n",[267,282,284],{"class":186,"line":283},2,[267,285,287],{"emptyLinePlaceholder":286},true,"\n",[267,289,291,295,298,301,304],{"class":186,"line":290},3,[267,292,294],{"class":293},"sDLfK","print",[267,296,297],{"class":272},"(canvas.extent().toString(",[267,299,300],{"class":293},"2",[267,302,303],{"class":272},"))          ",[267,305,307],{"class":306},"sjoCn","# current visible rectangle\n",[267,309,311,313,316],{"class":186,"line":310},4,[267,312,294],{"class":293},[267,314,315],{"class":272},"(canvas.scale())                        ",[267,317,318],{"class":306},"# denominator, e.g. 25000.0\n",[267,320,322,324],{"class":186,"line":321},5,[267,323,294],{"class":293},[267,325,326],{"class":272},"(canvas.mapUnitsPerPixel())\n",[267,328,330,332,335,338,341,344],{"class":186,"line":329},6,[267,331,294],{"class":293},[267,333,334],{"class":272},"([layer.name() ",[267,336,337],{"class":276},"for",[267,339,340],{"class":272}," layer ",[267,342,343],{"class":276},"in",[267,345,346],{"class":272}," canvas.layers()])\n",[267,348,350,352],{"class":186,"line":349},7,[267,351,294],{"class":293},[267,353,354],{"class":272},"(canvas.mapSettings().destinationCrs().authid())\n",[14,356,357,360,361,364,365,368,369,372,373,376,377,380,381,384,385,388],{},[217,358,359],{},"Breakdown:"," ",[233,362,363],{},"extent()"," returns a ",[233,366,367],{},"QgsRectangle"," in the canvas CRS — ",[233,370,371],{},"toString(2)"," formats it to two decimals for logging. ",[233,374,375],{},"scale()"," returns the denominator, so 25000.0 means 1:25 000. ",[233,378,379],{},"layers()"," returns only the ",[18,382,383],{},"visible"," layers in draw order, which is often different from ",[233,386,387],{},"QgsProject.instance().mapLayers()"," and is the usual explanation for a layer that \"isn't in the export\".",[14,390,391,392,395],{},"The canvas draws asynchronously. A script that changes the extent and immediately grabs an image will capture the previous frame, because the render has not finished. This is the single most common bug in canvas export code, and the fix is to either wait for the ",[233,393,394],{},"renderComplete"," signal or bypass the canvas entirely with a render job — covered below.",[206,397,399],{"id":398},"controlling-the-visible-extent","Controlling the visible extent",[14,401,402],{},"Three methods cover almost every need, and choosing the right one avoids a lot of arithmetic.",[258,404,406],{"className":260,"code":405,"language":262,"meta":263,"style":263},"from qgis.core import QgsProject, QgsRectangle\n\ncanvas = iface.mapCanvas()\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\n\ncanvas.setExtent(layer.extent())            # fit the whole layer\ncanvas.zoomScale(10000)                      # jump to 1:10 000, keeping the centre\ncanvas.setExtent(QgsRectangle(500000, 5600000, 510000, 5610000))\ncanvas.refresh()\n",[233,407,408,422,426,434,456,460,468,482,510],{"__ignoreMap":263},[267,409,410,413,416,419],{"class":186,"line":269},[267,411,412],{"class":276},"from",[267,414,415],{"class":272}," qgis.core ",[267,417,418],{"class":276},"import",[267,420,421],{"class":272}," QgsProject, QgsRectangle\n",[267,423,424],{"class":186,"line":283},[267,425,287],{"emptyLinePlaceholder":286},[267,427,428,430,432],{"class":186,"line":290},[267,429,273],{"class":272},[267,431,277],{"class":276},[267,433,280],{"class":272},[267,435,436,439,441,444,448,451,453],{"class":186,"line":310},[267,437,438],{"class":272},"layer ",[267,440,277],{"class":276},[267,442,443],{"class":272}," QgsProject.instance().mapLayersByName(",[267,445,447],{"class":446},"sU2Wk","\"parcels\"",[267,449,450],{"class":272},")[",[267,452,55],{"class":293},[267,454,455],{"class":272},"]\n",[267,457,458],{"class":186,"line":321},[267,459,287],{"emptyLinePlaceholder":286},[267,461,462,465],{"class":186,"line":329},[267,463,464],{"class":272},"canvas.setExtent(layer.extent())            ",[267,466,467],{"class":306},"# fit the whole layer\n",[267,469,470,473,476,479],{"class":186,"line":349},[267,471,472],{"class":272},"canvas.zoomScale(",[267,474,475],{"class":293},"10000",[267,477,478],{"class":272},")                      ",[267,480,481],{"class":306},"# jump to 1:10 000, keeping the centre\n",[267,483,485,488,491,494,497,499,502,504,507],{"class":186,"line":484},8,[267,486,487],{"class":272},"canvas.setExtent(QgsRectangle(",[267,489,490],{"class":293},"500000",[267,492,493],{"class":272},", ",[267,495,496],{"class":293},"5600000",[267,498,493],{"class":272},[267,500,501],{"class":293},"510000",[267,503,493],{"class":272},[267,505,506],{"class":293},"5610000",[267,508,509],{"class":272},"))\n",[267,511,513],{"class":186,"line":512},9,[267,514,515],{"class":272},"canvas.refresh()\n",[14,517,518,360,520,523,524,527,528,532,533,536,537,540],{},[217,519,359],{},[233,521,522],{},"layer.extent()"," is in the ",[18,525,526],{},"layer's"," CRS. If the project CRS differs, the rectangle must be transformed first or the canvas jumps somewhere unexpected — the recipe in ",[30,529,531],{"href":530},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fzoom-to-layer-extent-pyqgis\u002F","Zoom the Canvas to a Layer Extent"," handles that. ",[233,534,535],{},"zoomScale()"," sets the scale denominator directly and is what you want for consistent output across a map series. ",[233,538,539],{},"refresh()"," schedules the repaint; without it the canvas may keep showing the old frame until something else triggers a redraw.",[14,542,543],{},[38,544,547,550,553,556,559,595,619],{"viewBox":545,"role":41,"ariaLabel":546,"xmlns":43},"0 0 760 250","A layer extent shown tightly fitted, then with a ten percent margin added, then constrained to a fixed scale, illustrating why a raw extent usually needs padding",[45,548,549],{},"Fitting an extent, padding it, and pinning a scale",[49,551,552],{},"Three panels show the same cluster of parcel polygons. In the first the extent hugs the data with features touching the frame edge. In the second the extent is scaled by 1.1 leaving a visible margin. In the third the extent has been widened to satisfy a fixed 1:10000 scale, so the data sits centred with generous space around it.",[53,554],{"x":55,"y":55,"width":56,"height":555,"fill":58},"250",[77,557,558],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"A raw layer extent touches the frame — pad it before exporting",[560,561,562,567,571,578,583,588,591],"g",{},[53,563],{"x":87,"y":564,"width":565,"height":566,"rx":90,"fill":91,"stroke":140,"style":93},"44","232","164",[77,568,522],{"x":569,"y":570,"style":98,"fill":140,"textAnchor":83},"132","68",[53,572],{"x":573,"y":574,"width":575,"height":576,"fill":58,"stroke":157,"style":577},"38","84","188","104","stroke-width:1.5;stroke-dasharray:4 3",[72,579],{"d":580,"fill":581,"stroke":92,"style":582},"M 38 140 L 82 100 L 136 116 L 168 84 L 226 108","none","stroke-width:3",[72,584],{"d":585,"fill":116,"fillOpacity":586,"stroke":116,"style":587},"M 54 188 L 54 150 L 118 150 L 118 188 Z",0.3,"stroke-width:2",[72,589],{"d":590,"fill":116,"fillOpacity":586,"stroke":116,"style":587},"M 158 188 L 158 158 L 226 158 L 226 188 Z",[77,592,594],{"x":569,"y":593,"style":135,"fill":140,"textAnchor":83},"204","features clipped at the edge",[560,596,597,600,604,607,610,613,616],{},[53,598],{"x":599,"y":564,"width":565,"height":566,"rx":90,"fill":91,"stroke":173,"style":93},"268",[77,601,603],{"x":602,"y":570,"style":98,"fill":173,"textAnchor":83},"384","extent.scaled(1.1)",[53,605],{"x":606,"y":574,"width":575,"height":576,"fill":58,"stroke":157,"style":577},"290",[72,608],{"d":609,"fill":581,"stroke":92,"style":582},"M 304 138 L 342 104 L 388 118 L 416 92 L 464 112",[72,611],{"d":612,"fill":116,"fillOpacity":586,"stroke":116,"style":587},"M 318 180 L 318 148 L 372 148 L 372 180 Z",[72,614],{"d":615,"fill":116,"fillOpacity":586,"stroke":116,"style":587},"M 408 180 L 408 154 L 464 154 L 464 180 Z",[77,617,618],{"x":602,"y":593,"style":135,"fill":173,"textAnchor":83},"a clean margin all round",[560,620,621,624,628,631,634,637,640],{},[53,622],{"x":623,"y":564,"width":565,"height":566,"rx":90,"fill":91,"stroke":92,"style":93},"520",[77,625,627],{"x":626,"y":570,"style":98,"fill":92,"textAnchor":83},"636","zoomScale(10000)",[53,629],{"x":630,"y":574,"width":575,"height":576,"fill":58,"stroke":157,"style":577},"542",[72,632],{"d":633,"fill":581,"stroke":92,"style":93},"M 588 132 L 612 112 L 640 120 L 658 106 L 686 116",[72,635],{"d":636,"fill":116,"fillOpacity":586,"stroke":116,"style":587},"M 596 162 L 596 142 L 628 142 L 628 162 Z",[72,638],{"d":639,"fill":116,"fillOpacity":586,"stroke":116,"style":587},"M 650 162 L 650 146 L 684 146 L 684 162 Z",[77,641,642],{"x":626,"y":593,"style":135,"fill":92,"textAnchor":83},"identical scale across a series",[14,644,645],{},"Padding an extent is a one-liner that is worth making a habit, because a rectangle that exactly bounds the data always clips symbols and labels at the frame:",[258,647,649],{"className":260,"code":648,"language":262,"meta":263,"style":263},"extent = layer.extent()\nextent.scale(1.1)          # 10% margin, in place\ncanvas.setExtent(extent)\n",[233,650,651,661,675],{"__ignoreMap":263},[267,652,653,656,658],{"class":186,"line":269},[267,654,655],{"class":272},"extent ",[267,657,277],{"class":276},[267,659,660],{"class":272}," layer.extent()\n",[267,662,663,666,669,672],{"class":186,"line":283},[267,664,665],{"class":272},"extent.scale(",[267,667,668],{"class":293},"1.1",[267,670,671],{"class":272},")          ",[267,673,674],{"class":306},"# 10% margin, in place\n",[267,676,677],{"class":186,"line":290},[267,678,679],{"class":272},"canvas.setExtent(extent)\n",[14,681,682,360,684,687],{},[217,683,359],{},[233,685,686],{},"QgsRectangle.scale()"," mutates the rectangle around its centre, so grab a copy first if the original is still needed. A factor of 1.05–1.15 covers most symbol overhang; for point layers with large markers, more.",[206,689,691],{"id":690},"exporting-what-is-on-screen","Exporting what is on screen",[14,693,694,695,698],{},"When you genuinely want the on-screen view — a plugin's \"save this view\" button — ",[233,696,697],{},"canvas.saveAsImage()"," is the direct route, and it writes a world file alongside the image so the result stays georeferenced.",[258,700,702],{"className":260,"code":701,"language":262,"meta":263,"style":263},"canvas = iface.mapCanvas()\ncanvas.saveAsImage(\"\u002Fdata\u002Fexports\u002Fcurrent_view.png\")\n",[233,703,704,712],{"__ignoreMap":263},[267,705,706,708,710],{"class":186,"line":269},[267,707,273],{"class":272},[267,709,277],{"class":276},[267,711,280],{"class":272},[267,713,714,717,720],{"class":186,"line":283},[267,715,716],{"class":272},"canvas.saveAsImage(",[267,718,719],{"class":446},"\"\u002Fdata\u002Fexports\u002Fcurrent_view.png\"",[267,721,722],{"class":272},")\n",[14,724,725,727,728,731],{},[217,726,359],{}," The image is exactly the canvas widget's pixel size, which means the output resolution depends on the user's window. That is fine for a screenshot and unacceptable for a deliverable, which is the reason the next section exists. A ",[233,729,730],{},".pgw"," world file is written next to the PNG automatically.",[206,733,735],{"id":734},"rendering-headless-with-a-render-job","Rendering headless with a render job",[14,737,738,739,741,742,745],{},"For anything reproducible — a scheduled export, a map per feature, a server-side thumbnail — build ",[233,740,122],{}," yourself and hand it to a render job. Nothing here touches the GUI, so it runs under ",[233,743,744],{},"cron"," or in a container.",[14,747,748],{},[38,749,752,755,758,761,768,771,796,812,830,848,865,871,875,879,883,888],{"viewBox":750,"role":41,"ariaLabel":751,"xmlns":43},"0 0 760 216","The five steps of a headless render: build map settings, create a parallel render job, start it, wait for completion, then save the resulting image",[45,753,754],{},"The headless render sequence",[49,756,757],{},"Five numbered steps run left to right. First configure QgsMapSettings with layers, extent, size, DPI and background. Second construct a QgsMapRendererParallelJob from those settings. Third call start. Fourth call waitForFinished, which blocks until every layer has drawn. Fifth call renderedImage and save it. A note warns that skipping the wait step produces a blank or partial image.",[53,759],{"x":55,"y":55,"width":56,"height":760,"fill":58},"216",[60,762,763],{},[63,764,766],{"id":765,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"canvasJobArrow",[72,767],{"d":74,"fill":116},[77,769,770],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"Five steps, and step four is the one everyone forgets",[560,772,773,778,783,787,792],{},[53,774],{"x":775,"y":776,"width":569,"height":777,"rx":90,"fill":91,"stroke":92,"style":93},"12","52","80",[779,780],"circle",{"cx":781,"cy":782,"r":775,"fill":92},"34","74",[77,784,786],{"x":781,"y":785,"style":162,"fill":91,"textAnchor":83},"79","1",[77,788,122],{"x":789,"y":790,"style":791,"fill":82,"textAnchor":83},"78","102","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif",[77,793,795],{"x":789,"y":794,"style":135,"fill":75,"textAnchor":83},"120","layers, extent, size",[560,797,798,800,803,805,809],{},[53,799],{"x":566,"y":776,"width":569,"height":777,"rx":90,"fill":91,"stroke":92,"style":93},[779,801],{"cx":802,"cy":782,"r":775,"fill":92},"186",[77,804,300],{"x":802,"y":785,"style":162,"fill":91,"textAnchor":83},[77,806,808],{"x":807,"y":790,"style":791,"fill":82,"textAnchor":83},"230","ParallelJob(...)",[77,810,811],{"x":807,"y":794,"style":135,"fill":75,"textAnchor":83},"build the job",[560,813,814,817,820,823,827],{},[53,815],{"x":816,"y":776,"width":569,"height":777,"rx":90,"fill":91,"stroke":92,"style":93},"316",[779,818],{"cx":819,"cy":782,"r":775,"fill":92},"338",[77,821,822],{"x":819,"y":785,"style":162,"fill":91,"textAnchor":83},"3",[77,824,826],{"x":825,"y":790,"style":791,"fill":82,"textAnchor":83},"382","start()",[77,828,829],{"x":825,"y":794,"style":135,"fill":75,"textAnchor":83},"returns immediately",[560,831,832,835,838,841,845],{},[53,833],{"x":834,"y":776,"width":569,"height":777,"rx":90,"fill":91,"stroke":140,"style":582},"468",[779,836],{"cx":837,"cy":782,"r":775,"fill":140},"490",[77,839,840],{"x":837,"y":785,"style":162,"fill":91,"textAnchor":83},"4",[77,842,844],{"x":843,"y":790,"style":791,"fill":140,"textAnchor":83},"534","waitForFinished()",[77,846,847],{"x":843,"y":794,"style":135,"fill":75,"textAnchor":83},"blocks until drawn",[560,849,850,853,856,858,862],{},[53,851],{"x":852,"y":776,"width":189,"height":777,"rx":90,"fill":91,"stroke":173,"style":93},"620",[779,854],{"cx":855,"cy":782,"r":775,"fill":173},"642",[77,857,68],{"x":855,"y":785,"style":162,"fill":91,"textAnchor":83},[77,859,861],{"x":860,"y":790,"style":791,"fill":173,"textAnchor":83},"684","renderedImage()",[77,863,864],{"x":860,"y":794,"style":135,"fill":75,"textAnchor":83},"save it",[186,866],{"x1":867,"y1":868,"x2":869,"y2":868,"stroke":116,"style":870},"144","92","160","stroke-width:2.5;marker-end:url(#canvasJobArrow)",[186,872],{"x1":873,"y1":868,"x2":874,"y2":868,"stroke":116,"style":870},"296","312",[186,876],{"x1":877,"y1":868,"x2":878,"y2":868,"stroke":116,"style":870},"448","464",[186,880],{"x1":881,"y1":868,"x2":882,"y2":868,"stroke":116,"style":870},"600","616",[53,884],{"x":566,"y":885,"width":886,"height":154,"rx":67,"fill":91,"stroke":887,"style":587},"152","436","#b91c1c",[77,889,891],{"x":825,"y":890,"style":103,"fill":75,"textAnchor":83},"180","skip step 4 and renderedImage() returns a blank or half-drawn frame",[258,893,895],{"className":260,"code":894,"language":262,"meta":263,"style":263},"from qgis.core import QgsMapSettings, QgsMapRendererParallelJob, QgsProject\nfrom qgis.PyQt.QtCore import QSize\nfrom qgis.PyQt.QtGui import QColor\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\n\nsettings = QgsMapSettings()\nsettings.setLayers([layer])\nsettings.setBackgroundColor(QColor(\"#ffffff\"))\nsettings.setOutputSize(QSize(1600, 1200))\nsettings.setOutputDpi(300)\nsettings.setDestinationCrs(layer.crs())\n\nextent = layer.extent()\nextent.scale(1.1)\nsettings.setExtent(extent)\n\njob = QgsMapRendererParallelJob(settings)\njob.start()\njob.waitForFinished()\njob.renderedImage().save(\"\u002Fdata\u002Fexports\u002Fparcels.png\", \"png\")\n",[233,896,897,908,920,932,936,952,956,966,971,981,997,1008,1014,1019,1028,1037,1043,1048,1059,1065,1071],{"__ignoreMap":263},[267,898,899,901,903,905],{"class":186,"line":269},[267,900,412],{"class":276},[267,902,415],{"class":272},[267,904,418],{"class":276},[267,906,907],{"class":272}," QgsMapSettings, QgsMapRendererParallelJob, QgsProject\n",[267,909,910,912,915,917],{"class":186,"line":283},[267,911,412],{"class":276},[267,913,914],{"class":272}," qgis.PyQt.QtCore ",[267,916,418],{"class":276},[267,918,919],{"class":272}," QSize\n",[267,921,922,924,927,929],{"class":186,"line":290},[267,923,412],{"class":276},[267,925,926],{"class":272}," qgis.PyQt.QtGui ",[267,928,418],{"class":276},[267,930,931],{"class":272}," QColor\n",[267,933,934],{"class":186,"line":310},[267,935,287],{"emptyLinePlaceholder":286},[267,937,938,940,942,944,946,948,950],{"class":186,"line":321},[267,939,438],{"class":272},[267,941,277],{"class":276},[267,943,443],{"class":272},[267,945,447],{"class":446},[267,947,450],{"class":272},[267,949,55],{"class":293},[267,951,455],{"class":272},[267,953,954],{"class":186,"line":329},[267,955,287],{"emptyLinePlaceholder":286},[267,957,958,961,963],{"class":186,"line":349},[267,959,960],{"class":272},"settings ",[267,962,277],{"class":276},[267,964,965],{"class":272}," QgsMapSettings()\n",[267,967,968],{"class":186,"line":484},[267,969,970],{"class":272},"settings.setLayers([layer])\n",[267,972,973,976,979],{"class":186,"line":512},[267,974,975],{"class":272},"settings.setBackgroundColor(QColor(",[267,977,978],{"class":446},"\"#ffffff\"",[267,980,509],{"class":272},[267,982,984,987,990,992,995],{"class":186,"line":983},10,[267,985,986],{"class":272},"settings.setOutputSize(QSize(",[267,988,989],{"class":293},"1600",[267,991,493],{"class":272},[267,993,994],{"class":293},"1200",[267,996,509],{"class":272},[267,998,1000,1003,1006],{"class":186,"line":999},11,[267,1001,1002],{"class":272},"settings.setOutputDpi(",[267,1004,1005],{"class":293},"300",[267,1007,722],{"class":272},[267,1009,1011],{"class":186,"line":1010},12,[267,1012,1013],{"class":272},"settings.setDestinationCrs(layer.crs())\n",[267,1015,1017],{"class":186,"line":1016},13,[267,1018,287],{"emptyLinePlaceholder":286},[267,1020,1022,1024,1026],{"class":186,"line":1021},14,[267,1023,655],{"class":272},[267,1025,277],{"class":276},[267,1027,660],{"class":272},[267,1029,1031,1033,1035],{"class":186,"line":1030},15,[267,1032,665],{"class":272},[267,1034,668],{"class":293},[267,1036,722],{"class":272},[267,1038,1040],{"class":186,"line":1039},16,[267,1041,1042],{"class":272},"settings.setExtent(extent)\n",[267,1044,1046],{"class":186,"line":1045},17,[267,1047,287],{"emptyLinePlaceholder":286},[267,1049,1051,1054,1056],{"class":186,"line":1050},18,[267,1052,1053],{"class":272},"job ",[267,1055,277],{"class":276},[267,1057,1058],{"class":272}," QgsMapRendererParallelJob(settings)\n",[267,1060,1062],{"class":186,"line":1061},19,[267,1063,1064],{"class":272},"job.start()\n",[267,1066,1068],{"class":186,"line":1067},20,[267,1069,1070],{"class":272},"job.waitForFinished()\n",[267,1072,1074,1077,1080,1082,1085],{"class":186,"line":1073},21,[267,1075,1076],{"class":272},"job.renderedImage().save(",[267,1078,1079],{"class":446},"\"\u002Fdata\u002Fexports\u002Fparcels.png\"",[267,1081,493],{"class":272},[267,1083,1084],{"class":446},"\"png\"",[267,1086,722],{"class":272},[14,1088,1089,360,1091,1094,1095,1098,1099,1102,1103,1106,1107,1109,1110,1112,1113,1115,1116,1120],{},[217,1090,359],{},[233,1092,1093],{},"setLayers()"," takes the layers in ",[18,1096,1097],{},"draw order",", with the first item drawn on top — the reverse of what many people assume, and worth checking when a basemap covers everything. ",[233,1100,1101],{},"setBackgroundColor()"," avoids the transparent background that catches people out when the image lands in a document. ",[233,1104,1105],{},"setOutputDpi()"," does not resize the image; it tells the renderer how to interpret millimetre-based symbol and font sizes, so raising it makes symbols render proportionally rather than shrinking them. ",[233,1108,844],{}," is mandatory in a script — ",[233,1111,826],{}," returns immediately and ",[233,1114,861],{}," before completion gives you a partial frame. ",[30,1117,1119],{"href":1118},"\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"," shows the full standalone script including application startup.",[1122,1123,1125],"h3",{"id":1124},"getting-the-aspect-ratio-right","Getting the aspect ratio right",[14,1127,1128],{},"If the output size and the extent disagree about aspect ratio, QGIS expands the extent to fit and your carefully chosen bounds silently change. Derive one from the other instead:",[258,1130,1132],{"className":260,"code":1131,"language":262,"meta":263,"style":263},"extent = layer.extent()\nextent.scale(1.1)\nwidth = 1600\nheight = int(width * extent.height() \u002F extent.width())\nsettings.setOutputSize(QSize(width, height))\nsettings.setExtent(extent)\n",[233,1133,1134,1142,1150,1160,1185,1190],{"__ignoreMap":263},[267,1135,1136,1138,1140],{"class":186,"line":269},[267,1137,655],{"class":272},[267,1139,277],{"class":276},[267,1141,660],{"class":272},[267,1143,1144,1146,1148],{"class":186,"line":283},[267,1145,665],{"class":272},[267,1147,668],{"class":293},[267,1149,722],{"class":272},[267,1151,1152,1155,1157],{"class":186,"line":290},[267,1153,1154],{"class":272},"width ",[267,1156,277],{"class":276},[267,1158,1159],{"class":293}," 1600\n",[267,1161,1162,1165,1167,1170,1173,1176,1179,1182],{"class":186,"line":310},[267,1163,1164],{"class":272},"height ",[267,1166,277],{"class":276},[267,1168,1169],{"class":293}," int",[267,1171,1172],{"class":272},"(width ",[267,1174,1175],{"class":276},"*",[267,1177,1178],{"class":272}," extent.height() ",[267,1180,1181],{"class":276},"\u002F",[267,1183,1184],{"class":272}," extent.width())\n",[267,1186,1187],{"class":186,"line":321},[267,1188,1189],{"class":272},"settings.setOutputSize(QSize(width, height))\n",[267,1191,1192],{"class":186,"line":329},[267,1193,1042],{"class":272},[14,1195,1196,1198],{},[217,1197,359],{}," Fixing the width and computing the height from the extent's own proportions guarantees no expansion happens. For a fixed-size output — a thumbnail grid, say — invert it: fix the size and expand the extent yourself so you know exactly what was added.",[206,1200,1202],{"id":1201},"scale-dpi-and-symbol-size","Scale, DPI and symbol size",[14,1204,1205,1206,1209],{},"The relationship between these three trips up nearly everyone the first time. Symbol and label sizes in QGIS are specified in millimetres or points, both of which are ",[18,1207,1208],{},"physical"," units. Converting them to pixels requires a DPI. So the same map rendered at 96 DPI and 300 DPI with the same pixel dimensions shows symbols at very different apparent sizes relative to the map content.",[14,1211,1212,1213,1216],{},"The practical rules are short. To make an image bigger at the same visual design, raise the pixel size ",[18,1214,1215],{},"and"," the DPI together. To make symbols look smaller relative to the data, raise pixel size alone. And to guarantee a specific map scale in the output, compute the extent from the scale rather than the other way round:",[258,1218,1220],{"className":260,"code":1219,"language":262,"meta":263,"style":263},"def extent_for_scale(centre, scale, size_px, dpi):\n    \"\"\"Rectangle that renders at exactly 1:scale for the given output size.\"\"\"\n    from qgis.core import QgsRectangle\n    inches_wide = size_px.width() \u002F dpi\n    metres_wide = inches_wide * 0.0254 * scale\n    metres_high = (size_px.height() \u002F dpi) * 0.0254 * scale\n    return QgsRectangle(\n        centre.x() - metres_wide \u002F 2, centre.y() - metres_high \u002F 2,\n        centre.x() + metres_wide \u002F 2, centre.y() + metres_high \u002F 2,\n    )\n",[233,1221,1222,1234,1239,1251,1266,1287,1310,1318,1349,1374],{"__ignoreMap":263},[267,1223,1224,1227,1231],{"class":186,"line":269},[267,1225,1226],{"class":276},"def",[267,1228,1230],{"class":1229},"svObZ"," extent_for_scale",[267,1232,1233],{"class":272},"(centre, scale, size_px, dpi):\n",[267,1235,1236],{"class":186,"line":283},[267,1237,1238],{"class":446},"    \"\"\"Rectangle that renders at exactly 1:scale for the given output size.\"\"\"\n",[267,1240,1241,1244,1246,1248],{"class":186,"line":290},[267,1242,1243],{"class":276},"    from",[267,1245,415],{"class":272},[267,1247,418],{"class":276},[267,1249,1250],{"class":272}," QgsRectangle\n",[267,1252,1253,1256,1258,1261,1263],{"class":186,"line":310},[267,1254,1255],{"class":272},"    inches_wide ",[267,1257,277],{"class":276},[267,1259,1260],{"class":272}," size_px.width() ",[267,1262,1181],{"class":276},[267,1264,1265],{"class":272}," dpi\n",[267,1267,1268,1271,1273,1276,1278,1281,1284],{"class":186,"line":321},[267,1269,1270],{"class":272},"    metres_wide ",[267,1272,277],{"class":276},[267,1274,1275],{"class":272}," inches_wide ",[267,1277,1175],{"class":276},[267,1279,1280],{"class":293}," 0.0254",[267,1282,1283],{"class":276}," *",[267,1285,1286],{"class":272}," scale\n",[267,1288,1289,1292,1294,1297,1299,1302,1304,1306,1308],{"class":186,"line":329},[267,1290,1291],{"class":272},"    metres_high ",[267,1293,277],{"class":276},[267,1295,1296],{"class":272}," (size_px.height() ",[267,1298,1181],{"class":276},[267,1300,1301],{"class":272}," dpi) ",[267,1303,1175],{"class":276},[267,1305,1280],{"class":293},[267,1307,1283],{"class":276},[267,1309,1286],{"class":272},[267,1311,1312,1315],{"class":186,"line":349},[267,1313,1314],{"class":276},"    return",[267,1316,1317],{"class":272}," QgsRectangle(\n",[267,1319,1320,1323,1326,1329,1331,1334,1337,1339,1342,1344,1346],{"class":186,"line":484},[267,1321,1322],{"class":272},"        centre.x() ",[267,1324,1325],{"class":276},"-",[267,1327,1328],{"class":272}," metres_wide ",[267,1330,1181],{"class":276},[267,1332,1333],{"class":293}," 2",[267,1335,1336],{"class":272},", centre.y() ",[267,1338,1325],{"class":276},[267,1340,1341],{"class":272}," metres_high ",[267,1343,1181],{"class":276},[267,1345,1333],{"class":293},[267,1347,1348],{"class":272},",\n",[267,1350,1351,1353,1356,1358,1360,1362,1364,1366,1368,1370,1372],{"class":186,"line":512},[267,1352,1322],{"class":272},[267,1354,1355],{"class":276},"+",[267,1357,1328],{"class":272},[267,1359,1181],{"class":276},[267,1361,1333],{"class":293},[267,1363,1336],{"class":272},[267,1365,1355],{"class":276},[267,1367,1341],{"class":272},[267,1369,1181],{"class":276},[267,1371,1333],{"class":293},[267,1373,1348],{"class":272},[267,1375,1376],{"class":186,"line":983},[267,1377,1378],{"class":272},"    )\n",[14,1380,1381,1383,1384,1388,1389,228],{},[217,1382,359],{}," The chain is pixels → inches (divide by DPI) → metres on paper (× 0.0254) → metres on the ground (× the scale denominator). It only holds in a projected CRS whose units are metres, which is another reason to reproject before rendering — see ",[30,1385,1387],{"href":1386},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002F","Coordinate Reference Systems in PyQGIS",". This helper is the core of any consistent map series, and pairs naturally with the atlas workflow in ",[30,1390,1392],{"href":1391},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002F","Automating Atlas Map Series",[206,1394,1396],{"id":1395},"background-antialiasing-and-other-render-flags","Background, antialiasing and other render flags",[14,1398,1399,1401],{},[233,1400,122],{}," carries a flag set that controls quality and behaviour. The two worth setting explicitly in export code are antialiasing, which is on by default on the canvas but easy to lose in a hand-built settings object, and label drawing.",[258,1403,1405],{"className":260,"code":1404,"language":262,"meta":263,"style":263},"from qgis.core import Qgis, QgsMapSettings\n\nsettings.setFlag(Qgis.MapSettingsFlag.Antialiasing, True)\nsettings.setFlag(Qgis.MapSettingsFlag.DrawLabeling, True)\nsettings.setFlag(Qgis.MapSettingsFlag.UseAdvancedEffects, True)\n",[233,1406,1407,1418,1422,1432,1441],{"__ignoreMap":263},[267,1408,1409,1411,1413,1415],{"class":186,"line":269},[267,1410,412],{"class":276},[267,1412,415],{"class":272},[267,1414,418],{"class":276},[267,1416,1417],{"class":272}," Qgis, QgsMapSettings\n",[267,1419,1420],{"class":186,"line":283},[267,1421,287],{"emptyLinePlaceholder":286},[267,1423,1424,1427,1430],{"class":186,"line":290},[267,1425,1426],{"class":272},"settings.setFlag(Qgis.MapSettingsFlag.Antialiasing, ",[267,1428,1429],{"class":293},"True",[267,1431,722],{"class":272},[267,1433,1434,1437,1439],{"class":186,"line":310},[267,1435,1436],{"class":272},"settings.setFlag(Qgis.MapSettingsFlag.DrawLabeling, ",[267,1438,1429],{"class":293},[267,1440,722],{"class":272},[267,1442,1443,1446,1448],{"class":186,"line":321},[267,1444,1445],{"class":272},"settings.setFlag(Qgis.MapSettingsFlag.UseAdvancedEffects, ",[267,1447,1429],{"class":293},[267,1449,722],{"class":272},[14,1451,1452,360,1454,1457,1458,1460,1461,1464,1465,1467,1468,1471],{},[217,1453,359],{},[233,1455,1456],{},"DrawLabeling"," is the answer to \"why are my labels missing from the export\" — a fresh ",[233,1459,122],{}," does not always inherit the canvas's flags. ",[233,1462,1463],{},"UseAdvancedEffects"," enables blend modes and layer effects, which are silently dropped without it, so a map that uses a multiply blend for hillshading will look flat. On QGIS 3.28 and earlier these constants live on ",[233,1466,122],{}," rather than ",[233,1469,1470],{},"Qgis","; both spellings work on 3.34.",[14,1473,1474,1475,1478,1479,228],{},"For the background, prefer an explicit colour over relying on the default. ",[233,1476,1477],{},"QColor(Qt.transparent)"," gives a transparent PNG when you want to composite the map into something else — see ",[30,1480,1482],{"href":1481},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fset-map-canvas-background-color-pyqgis\u002F","Set the Map Canvas Background Colour in PyQGIS",[206,1484,1486],{"id":1485},"rendering-only-what-the-map-needs","Rendering only what the map needs",[14,1488,1489],{},"A render draws every feature of every layer you hand it, whether or not that feature is inside the extent or visible at the current scale. On a large project that is the difference between a two-second export and a two-minute one, and the fix is to be explicit about the subset.",[258,1491,1493],{"className":260,"code":1492,"language":262,"meta":263,"style":263},"from qgis.core import QgsMapSettings, QgsProject\n\nproject = QgsProject.instance()\nsettings = QgsMapSettings()\n\n# Only the layers this map is about, in explicit draw order (first = on top).\nwanted = [\"parcels\", \"roads\", \"basemap\"]\nsettings.setLayers([project.mapLayersByName(name)[0] for name in wanted])\n\n# Honour the scale ranges configured on each layer.\nsettings.setFlag(Qgis.MapSettingsFlag.UseRenderingOptimization, True)\n",[233,1494,1495,1506,1510,1520,1528,1532,1537,1561,1581,1585,1590],{"__ignoreMap":263},[267,1496,1497,1499,1501,1503],{"class":186,"line":269},[267,1498,412],{"class":276},[267,1500,415],{"class":272},[267,1502,418],{"class":276},[267,1504,1505],{"class":272}," QgsMapSettings, QgsProject\n",[267,1507,1508],{"class":186,"line":283},[267,1509,287],{"emptyLinePlaceholder":286},[267,1511,1512,1515,1517],{"class":186,"line":290},[267,1513,1514],{"class":272},"project ",[267,1516,277],{"class":276},[267,1518,1519],{"class":272}," QgsProject.instance()\n",[267,1521,1522,1524,1526],{"class":186,"line":310},[267,1523,960],{"class":272},[267,1525,277],{"class":276},[267,1527,965],{"class":272},[267,1529,1530],{"class":186,"line":321},[267,1531,287],{"emptyLinePlaceholder":286},[267,1533,1534],{"class":186,"line":329},[267,1535,1536],{"class":306},"# Only the layers this map is about, in explicit draw order (first = on top).\n",[267,1538,1539,1542,1544,1547,1549,1551,1554,1556,1559],{"class":186,"line":349},[267,1540,1541],{"class":272},"wanted ",[267,1543,277],{"class":276},[267,1545,1546],{"class":272}," [",[267,1548,447],{"class":446},[267,1550,493],{"class":272},[267,1552,1553],{"class":446},"\"roads\"",[267,1555,493],{"class":272},[267,1557,1558],{"class":446},"\"basemap\"",[267,1560,455],{"class":272},[267,1562,1563,1566,1568,1571,1573,1576,1578],{"class":186,"line":484},[267,1564,1565],{"class":272},"settings.setLayers([project.mapLayersByName(name)[",[267,1567,55],{"class":293},[267,1569,1570],{"class":272},"] ",[267,1572,337],{"class":276},[267,1574,1575],{"class":272}," name ",[267,1577,343],{"class":276},[267,1579,1580],{"class":272}," wanted])\n",[267,1582,1583],{"class":186,"line":512},[267,1584,287],{"emptyLinePlaceholder":286},[267,1586,1587],{"class":186,"line":983},[267,1588,1589],{"class":306},"# Honour the scale ranges configured on each layer.\n",[267,1591,1592,1595,1597],{"class":186,"line":999},[267,1593,1594],{"class":272},"settings.setFlag(Qgis.MapSettingsFlag.UseRenderingOptimization, ",[267,1596,1429],{"class":293},[267,1598,722],{"class":272},[14,1600,1601,1603,1604,1607,1608,1611],{},[217,1602,359],{}," Building the layer list by name from the project rather than copying ",[233,1605,1606],{},"canvas.layers()"," makes the export independent of whatever the user has ticked in the layer panel — a script that produces different output depending on GUI state is not reproducible. ",[233,1609,1610],{},"UseRenderingOptimization"," lets QGIS apply per-layer simplification and skip geometry that cannot affect the visible pixels; it is on by default for the canvas and easy to lose in a hand-built settings object.",[14,1613,1614,1615,1618,1619,1622],{},"Scale-dependent visibility is honoured automatically, which is worth knowing in both directions: a layer with a minimum scale of 1:5 000 will silently not appear in a 1:50 000 export, and that is usually correct but occasionally baffling. Check with ",[233,1616,1617],{},"layer.hasScaleBasedVisibility()"," and ",[233,1620,1621],{},"layer.minimumScale()"," when a layer you passed explicitly refuses to draw.",[14,1624,1625],{},"For very large vector layers, a provider-side filter is more effective than any render flag, because it prevents the features being read at all:",[258,1627,1629],{"className":260,"code":1628,"language":262,"meta":263,"style":263},"parcels = project.mapLayersByName(\"parcels\")[0]\nparcels.setSubsetString('\"status\" = \\'active\\'')\n# ... render ...\nparcels.setSubsetString(\"\")        # always restore it\n",[233,1630,1631,1649,1670,1675],{"__ignoreMap":263},[267,1632,1633,1636,1638,1641,1643,1645,1647],{"class":186,"line":269},[267,1634,1635],{"class":272},"parcels ",[267,1637,277],{"class":276},[267,1639,1640],{"class":272}," project.mapLayersByName(",[267,1642,447],{"class":446},[267,1644,450],{"class":272},[267,1646,55],{"class":293},[267,1648,455],{"class":272},[267,1650,1651,1654,1657,1660,1663,1665,1668],{"class":186,"line":283},[267,1652,1653],{"class":272},"parcels.setSubsetString(",[267,1655,1656],{"class":446},"'\"status\" = ",[267,1658,1659],{"class":293},"\\'",[267,1661,1662],{"class":446},"active",[267,1664,1659],{"class":293},[267,1666,1667],{"class":446},"'",[267,1669,722],{"class":272},[267,1671,1672],{"class":186,"line":290},[267,1673,1674],{"class":306},"# ... render ...\n",[267,1676,1677,1679,1682,1685],{"class":186,"line":310},[267,1678,1653],{"class":272},[267,1680,1681],{"class":446},"\"\"",[267,1683,1684],{"class":272},")        ",[267,1686,1687],{"class":306},"# always restore it\n",[14,1689,1690,360,1692,1695,1696,1699,1700,1181,1703,1706],{},[217,1691,359],{},[233,1693,1694],{},"setSubsetString()"," applies a provider-level filter that behaves like a permanent ",[233,1697,1698],{},"WHERE"," clause — it affects rendering, iteration, and the attribute table alike. That power is also its danger: forgetting to clear it leaves the layer filtered for the rest of the session and quietly changes every later result. Wrapping the render in a ",[233,1701,1702],{},"try",[233,1704,1705],{},"finally"," that restores the previous subset is the safe form.",[206,1708,1710],{"id":1709},"exporting-one-image-per-feature","Exporting one image per feature",[14,1712,1713],{},"The pattern that pays for learning all of this is the per-feature export: a thumbnail for every parcel, a locator map for every site, a preview for every row in a report. Because the settings object is cheap to reuse, the loop is short.",[258,1715,1717],{"className":260,"code":1716,"language":262,"meta":263,"style":263},"from pathlib import Path\nfrom qgis.core import QgsMapSettings, QgsMapRendererParallelJob, QgsProject\nfrom qgis.PyQt.QtCore import QSize\nfrom qgis.PyQt.QtGui import QColor\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\ncontext = QgsProject.instance().mapLayersByName(\"basemap\")[0]\nout_dir = Path(\"\u002Fdata\u002Fexports\u002Fthumbnails\")\nout_dir.mkdir(parents=True, exist_ok=True)\n\nsettings = QgsMapSettings()\nsettings.setLayers([layer, context])          # parcels drawn over the basemap\nsettings.setBackgroundColor(QColor(\"#ffffff\"))\nsettings.setOutputSize(QSize(640, 480))\nsettings.setOutputDpi(96)\nsettings.setDestinationCrs(layer.crs())\n\nfor feature in layer.getFeatures():\n    extent = feature.geometry().boundingBox()\n    if extent.isEmpty():\n        continue\n    extent.scale(1.6)                          # generous context around the subject\n    settings.setExtent(extent)\n\n    job = QgsMapRendererParallelJob(settings)\n    job.start()\n    job.waitForFinished()\n    job.renderedImage().save(str(out_dir \u002F f\"{feature['parcel_id']}.png\"), \"png\")\n",[233,1718,1719,1731,1741,1751,1761,1765,1781,1798,1813,1837,1841,1849,1857,1865,1879,1887,1891,1895,1907,1917,1925,1930,1945,1951,1956,1966,1972,1978],{"__ignoreMap":263},[267,1720,1721,1723,1726,1728],{"class":186,"line":269},[267,1722,412],{"class":276},[267,1724,1725],{"class":272}," pathlib ",[267,1727,418],{"class":276},[267,1729,1730],{"class":272}," Path\n",[267,1732,1733,1735,1737,1739],{"class":186,"line":283},[267,1734,412],{"class":276},[267,1736,415],{"class":272},[267,1738,418],{"class":276},[267,1740,907],{"class":272},[267,1742,1743,1745,1747,1749],{"class":186,"line":290},[267,1744,412],{"class":276},[267,1746,914],{"class":272},[267,1748,418],{"class":276},[267,1750,919],{"class":272},[267,1752,1753,1755,1757,1759],{"class":186,"line":310},[267,1754,412],{"class":276},[267,1756,926],{"class":272},[267,1758,418],{"class":276},[267,1760,931],{"class":272},[267,1762,1763],{"class":186,"line":321},[267,1764,287],{"emptyLinePlaceholder":286},[267,1766,1767,1769,1771,1773,1775,1777,1779],{"class":186,"line":329},[267,1768,438],{"class":272},[267,1770,277],{"class":276},[267,1772,443],{"class":272},[267,1774,447],{"class":446},[267,1776,450],{"class":272},[267,1778,55],{"class":293},[267,1780,455],{"class":272},[267,1782,1783,1786,1788,1790,1792,1794,1796],{"class":186,"line":349},[267,1784,1785],{"class":272},"context ",[267,1787,277],{"class":276},[267,1789,443],{"class":272},[267,1791,1558],{"class":446},[267,1793,450],{"class":272},[267,1795,55],{"class":293},[267,1797,455],{"class":272},[267,1799,1800,1803,1805,1808,1811],{"class":186,"line":484},[267,1801,1802],{"class":272},"out_dir ",[267,1804,277],{"class":276},[267,1806,1807],{"class":272}," Path(",[267,1809,1810],{"class":446},"\"\u002Fdata\u002Fexports\u002Fthumbnails\"",[267,1812,722],{"class":272},[267,1814,1815,1818,1822,1824,1826,1828,1831,1833,1835],{"class":186,"line":512},[267,1816,1817],{"class":272},"out_dir.mkdir(",[267,1819,1821],{"class":1820},"s9osk","parents",[267,1823,277],{"class":276},[267,1825,1429],{"class":293},[267,1827,493],{"class":272},[267,1829,1830],{"class":1820},"exist_ok",[267,1832,277],{"class":276},[267,1834,1429],{"class":293},[267,1836,722],{"class":272},[267,1838,1839],{"class":186,"line":983},[267,1840,287],{"emptyLinePlaceholder":286},[267,1842,1843,1845,1847],{"class":186,"line":999},[267,1844,960],{"class":272},[267,1846,277],{"class":276},[267,1848,965],{"class":272},[267,1850,1851,1854],{"class":186,"line":1010},[267,1852,1853],{"class":272},"settings.setLayers([layer, context])          ",[267,1855,1856],{"class":306},"# parcels drawn over the basemap\n",[267,1858,1859,1861,1863],{"class":186,"line":1016},[267,1860,975],{"class":272},[267,1862,978],{"class":446},[267,1864,509],{"class":272},[267,1866,1867,1869,1872,1874,1877],{"class":186,"line":1021},[267,1868,986],{"class":272},[267,1870,1871],{"class":293},"640",[267,1873,493],{"class":272},[267,1875,1876],{"class":293},"480",[267,1878,509],{"class":272},[267,1880,1881,1883,1885],{"class":186,"line":1030},[267,1882,1002],{"class":272},[267,1884,120],{"class":293},[267,1886,722],{"class":272},[267,1888,1889],{"class":186,"line":1039},[267,1890,1013],{"class":272},[267,1892,1893],{"class":186,"line":1045},[267,1894,287],{"emptyLinePlaceholder":286},[267,1896,1897,1899,1902,1904],{"class":186,"line":1050},[267,1898,337],{"class":276},[267,1900,1901],{"class":272}," feature ",[267,1903,343],{"class":276},[267,1905,1906],{"class":272}," layer.getFeatures():\n",[267,1908,1909,1912,1914],{"class":186,"line":1061},[267,1910,1911],{"class":272},"    extent ",[267,1913,277],{"class":276},[267,1915,1916],{"class":272}," feature.geometry().boundingBox()\n",[267,1918,1919,1922],{"class":186,"line":1067},[267,1920,1921],{"class":276},"    if",[267,1923,1924],{"class":272}," extent.isEmpty():\n",[267,1926,1927],{"class":186,"line":1073},[267,1928,1929],{"class":276},"        continue\n",[267,1931,1933,1936,1939,1942],{"class":186,"line":1932},22,[267,1934,1935],{"class":272},"    extent.scale(",[267,1937,1938],{"class":293},"1.6",[267,1940,1941],{"class":272},")                          ",[267,1943,1944],{"class":306},"# generous context around the subject\n",[267,1946,1948],{"class":186,"line":1947},23,[267,1949,1950],{"class":272},"    settings.setExtent(extent)\n",[267,1952,1954],{"class":186,"line":1953},24,[267,1955,287],{"emptyLinePlaceholder":286},[267,1957,1959,1962,1964],{"class":186,"line":1958},25,[267,1960,1961],{"class":272},"    job ",[267,1963,277],{"class":276},[267,1965,1058],{"class":272},[267,1967,1969],{"class":186,"line":1968},26,[267,1970,1971],{"class":272},"    job.start()\n",[267,1973,1975],{"class":186,"line":1974},27,[267,1976,1977],{"class":272},"    job.waitForFinished()\n",[267,1979,1981,1984,1987,1990,1992,1995,1998,2001,2004,2007,2010,2013,2016,2019,2021],{"class":186,"line":1980},28,[267,1982,1983],{"class":272},"    job.renderedImage().save(",[267,1985,1986],{"class":293},"str",[267,1988,1989],{"class":272},"(out_dir ",[267,1991,1181],{"class":276},[267,1993,1994],{"class":276}," f",[267,1996,1997],{"class":446},"\"",[267,1999,2000],{"class":293},"{",[267,2002,2003],{"class":272},"feature[",[267,2005,2006],{"class":446},"'parcel_id'",[267,2008,2009],{"class":272},"]",[267,2011,2012],{"class":293},"}",[267,2014,2015],{"class":446},".png\"",[267,2017,2018],{"class":272},"), ",[267,2020,1084],{"class":446},[267,2022,722],{"class":272},[14,2024,2025,2027,2028,2031,2032,2035],{},[217,2026,359],{}," The settings object is configured once and only its extent changes per iteration, which avoids re-resolving layers and styles on every pass. ",[233,2029,2030],{},"boundingBox()"," on the geometry gives the feature's own extent — checking ",[233,2033,2034],{},"isEmpty()"," skips features with no geometry, which would otherwise produce an image of nothing. A scale factor of 1.6 is deliberately larger than the 1.1 used for a full-layer export, because a single feature filling its frame reads as a diagram rather than a map. A fresh job per feature is correct: jobs are single-use, and reusing one silently returns the first render.",[14,2037,2038,2039,1618,2043,228],{},"For a hundred features this runs in seconds; for a hundred thousand, batch it and write to a fast local disk rather than a network share. When the output needs a title, legend and scale bar rather than a bare map, the layout system is the better tool — see ",[30,2040,2042],{"href":2041},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002F","Automated Map Layout Generation",[30,2044,2046],{"href":2045},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fexporting-multiple-qgis-layouts-to-pdf\u002F","Exporting Multiple QGIS Layouts to PDF",[206,2048,2050],{"id":2049},"troubleshooting-canvas-and-export-problems","Troubleshooting canvas and export problems",[14,2052,2053],{},"Most export failures fall into a handful of recognisable shapes, and the symptom usually names the cause.",[211,2055,2056,2072,2084,2090,2096,2105],{},[214,2057,2058,2061,2062,2064,2065,2067,2068,2071],{},[217,2059,2060],{},"The image is empty but the file is the right size."," Either ",[233,2063,844],{}," was skipped, or ",[233,2066,1093],{}," was given an empty list. Print ",[233,2069,2070],{},"len(settings.layers())"," before starting the job.",[214,2073,2074,360,2077,2079,2080,2083],{},[217,2075,2076],{},"Only some layers appear.",[233,2078,1606],{}," returns visible layers only, so a copied list inherits whatever was unticked in the layer panel. Build the list from ",[233,2081,2082],{},"QgsProject.instance().mapLayersByName()"," when you want a specific set regardless of the tree state.",[214,2085,2086,2089],{},[217,2087,2088],{},"The extent is not what you asked for."," The output size and extent disagreed on aspect ratio and QGIS expanded the extent to fit. Derive one from the other as shown above.",[214,2091,2092,2095],{},[217,2093,2094],{},"Symbols are tiny or enormous."," The DPI does not match the one the style was designed at. Rendering a 96 DPI design at 300 DPI without changing the pixel size shrinks everything by about a third relative to the frame.",[214,2097,2098,360,2101,2104],{},[217,2099,2100],{},"The export is misaligned with other data.",[233,2102,2103],{},"setDestinationCrs()"," was never called, so the render used whatever the settings object defaulted to rather than the CRS the extent was expressed in.",[214,2106,2107,360,2110,2113,2114,2117,2118,2121],{},[217,2108,2109],{},"The script hangs in a standalone run.",[233,2111,2112],{},"QgsMapRendererParallelJob"," needs a running Qt event loop for its worker threads. In a headless script, either call ",[233,2115,2116],{},"QgsApplication.processEvents()"," in a wait loop or use ",[233,2119,2120],{},"QgsMapRendererSequentialJob",", which does not.",[206,2123,2125],{"id":2124},"key-takeaways","Key takeaways",[211,2127,2128,2137,2143,2151,2162,2172,2178],{},[214,2129,2130,2136],{},[217,2131,2132,2133,2135],{},"The canvas is a widget; ",[233,2134,122],{}," is the recipe."," Anything reproducible should build settings directly rather than driving the canvas.",[214,2138,2139,2142],{},[217,2140,2141],{},"The canvas renders asynchronously."," Grabbing an image straight after changing the extent captures the old frame; use a render job instead.",[214,2144,2145,2150],{},[217,2146,2147,2149],{},[233,2148,1093],{}," order is top-first."," The first layer in the list draws on top of the rest.",[214,2152,2153,2158,2159,2161],{},[217,2154,2155,2157],{},[233,2156,844],{}," is not optional in a script."," Without it ",[233,2160,861],{}," returns a blank or partially drawn frame.",[214,2163,2164,2167,2168,2171],{},[217,2165,2166],{},"Pad your extents."," A rectangle that exactly bounds the data clips symbols and labels at the frame edge; ",[233,2169,2170],{},"extent.scale(1.1)"," is the habit to build.",[214,2173,2174,2177],{},[217,2175,2176],{},"DPI scales symbols, not the image."," Raise pixel size and DPI together to enlarge a map without changing its design.",[214,2179,2180,2188],{},[217,2181,2182,2183,1618,2185,2187],{},"Set ",[233,2184,1456],{},[233,2186,1463],{}," explicitly"," when building settings by hand, or labels and blend modes vanish from the output.",[206,2190,2192],{"id":2191},"frequently-asked-questions","Frequently Asked Questions",[14,2194,2195,2198,2201,2202,2205,2206,2208],{},[217,2196,2197],{},"Why is my exported image blank or only partly drawn?",[233,2199,2200],{},"job.start()"," returns immediately and rendering continues in the background. Call ",[233,2203,2204],{},"job.waitForFinished()"," before ",[233,2207,861],{},". The same asynchrony explains a canvas screenshot that shows the previous extent.",[14,2210,2211,2214,2215,2217,2218,2220,2221,228],{},[217,2212,2213],{},"Why are labels missing from a headless export?","\nA hand-built ",[233,2216,122],{}," does not necessarily have the ",[233,2219,1456],{}," flag set. Enable it explicitly with ",[233,2222,2223],{},"settings.setFlag(Qgis.MapSettingsFlag.DrawLabeling, True)",[14,2225,2226,2229],{},[217,2227,2228],{},"How do I export at a specific map scale?","\nCompute the extent from the scale rather than setting a scale on the output. Convert output pixels to inches with the DPI, then to ground metres by multiplying by 0.0254 and the scale denominator, and build a rectangle of that size around your centre point.",[14,2231,2232,2235,2236,2239],{},[217,2233,2234],{},"Does raising the DPI make the image larger?","\nNo. DPI only tells the renderer how to convert millimetre-based symbol and font sizes into pixels. To produce a larger image you must also raise ",[233,2237,2238],{},"setOutputSize()","; raising both together keeps the design proportions identical.",[14,2241,2242,2245,2246,2248,2249,2252],{},[217,2243,2244],{},"Why does my exported map have a transparent background?","\nBecause no background colour was set. ",[233,2247,122],{}," defaults to transparent when constructed directly. Call ",[233,2250,2251],{},"setBackgroundColor(QColor(\"#ffffff\"))"," — or any colour — before rendering.",[206,2254,2256],{"id":2255},"related","Related",[211,2258,2259,2264,2269,2275,2280,2286,2291,2295,2299],{},[214,2260,2261,2263],{},[30,2262,33],{"href":32}," — the guide this page belongs to",[214,2265,2266],{},[30,2267,2268],{"href":226},"Programmatic Layer Styling in PyQGIS",[214,2270,2271],{},[30,2272,2274],{"href":2273},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002F","Labeling and Annotations in PyQGIS",[214,2276,2277],{},[30,2278,2279],{"href":2041},"Automated Map Layout Generation in PyQGIS",[214,2281,2282],{},[30,2283,2285],{"href":2284},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fexport-map-canvas-to-image-pyqgis\u002F","Export the Map Canvas to a PNG with PyQGIS",[214,2287,2288],{},[30,2289,2290],{"href":530},"Zoom the Canvas to a Layer Extent in PyQGIS",[214,2292,2293],{},[30,2294,1119],{"href":1118},[214,2296,2297],{},[30,2298,1482],{"href":1481},[214,2300,2301],{},[30,2302,2304],{"href":2303},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fadd-scalebar-and-north-arrow-pyqgis\u002F","Add a Scale Bar and North Arrow to a Layout in PyQGIS",[2306,2307,2308],"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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":263,"searchDepth":283,"depth":283,"links":2310},[2311,2312,2313,2314,2315,2318,2319,2320,2321,2322,2323,2324,2325],{"id":208,"depth":283,"text":209},{"id":246,"depth":283,"text":247},{"id":398,"depth":283,"text":399},{"id":690,"depth":283,"text":691},{"id":734,"depth":283,"text":735,"children":2316},[2317],{"id":1124,"depth":290,"text":1125},{"id":1201,"depth":283,"text":1202},{"id":1395,"depth":283,"text":1396},{"id":1485,"depth":283,"text":1486},{"id":1709,"depth":283,"text":1710},{"id":2049,"depth":283,"text":2050},{"id":2124,"depth":283,"text":2125},{"id":2191,"depth":283,"text":2192},{"id":2255,"depth":283,"text":2256},"Drive the QGIS map canvas from Python. Control the extent and scale, export the view to an image, and render maps headless with QgsMapSettings and a render job.","md",{"slug":2329,"type":2330,"breadcrumb":2331,"datePublished":2332,"dateModified":2332},"map-canvas-and-image-export","guide","Map Canvas & Image Export","2026-08-01","\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export",{"title":5,"description":2326},"pyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Findex","_EsP-ObH9upQltKGpbua1VVtesInozTsOBCIKeTpc9s",1785585990471]