[{"data":1,"prerenderedAt":1729},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Frender-layer-to-image-without-gui-pyqgis":3},{"id":4,"title":5,"body":6,"description":1718,"extension":1719,"meta":1720,"navigation":372,"path":1725,"seo":1726,"stem":1727,"__hash__":1728},"docs\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Frender-layer-to-image-without-gui-pyqgis\u002Findex.md","Render a Layer to an Image Headless in PyQGIS",{"type":7,"value":8,"toc":1706},"minimark",[9,13,26,35,242,247,269,273,276,706,764,768,771,821,839,843,846,948,989,1005,1011,1015,1018,1152,1432,1447,1450,1454,1460,1518,1521,1525,1595,1599,1612,1616,1624,1636,1647,1660,1669,1675,1679,1702],[10,11,5],"h1",{"id":12},"render-a-layer-to-an-image-headless-in-pyqgis",[14,15,16,17,21,22,25],"p",{},"A map that has to be produced every night, on a server, without anyone opening QGIS, is a different problem from one produced interactively. There is no ",[18,19,20],"code",{},"iface",", no canvas and no window — and the parts of the API that depend on them fail in ways that are hard to read, typically an immediate segmentation fault rather than an exception. The workable subset is ",[18,23,24],{},"qgis.core"," plus a render job, and the initialisation ritual that gets you there is short but unforgiving about order.",[14,27,28,29,34],{},"This page is a focused recipe within ",[30,31,33],"a",{"href":32},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002F","Map Canvas Control and Image Export in PyQGIS",". It covers standalone application startup, building the map settings by hand, running the render job, and the practical details of running the result under a scheduler or in a container.",[14,36,37],{},[38,39,44,48,52,59,76,85,120,139,159,178,197,204,208,212,216,222,227,232,235,239],"svg",{"viewBox":40,"role":41,"ariaLabel":42,"xmlns":43},"0 0 760 268","img","The standalone startup sequence: set the prefix path, initialise QgsApplication with GUI disabled, load layers, render, then exit cleanly","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[45,46,47],"title",{},"The standalone lifecycle, start to finish",[49,50,51],"desc",{},"Five ordered steps. Set the QGIS prefix path, construct QgsApplication with the GUI flag false and call initQgis, load the layers and build map settings, run the render job and save the image, then call exitQgis. A note marks that skipping the final exit leaves the process hanging and skipping the first step causes an immediate crash.",[53,54],"rect",{"x":55,"y":55,"width":56,"height":57,"fill":58},"0","760","268","#f6f3ea",[60,61,62],"defs",{},[63,64,71],"marker",{"id":65,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"headlessArrow","0 0 10 10","8","5","7","auto-start-reverse",[72,73],"path",{"d":74,"fill":75},"M0 0 L10 5 L0 10 z","#0f766e",[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","Order matters — step 1 before step 2, and always reach step 5",[86,87,88,98,103,108,114],"g",{},[53,89],{"x":90,"y":91,"width":92,"height":93,"rx":94,"fill":95,"stroke":96,"style":97},"12","56","136","86","10","#fffdf7","#b45309","stroke-width:2.5",[99,100],"circle",{"cx":101,"cy":102,"r":90,"fill":96},"34","78",[77,104,107],{"x":101,"y":105,"style":106,"fill":95,"textAnchor":83},"83","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","1",[77,109,113],{"x":110,"y":111,"style":112,"fill":82,"textAnchor":83},"80","106","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","setPrefixPath",[77,115,119],{"x":110,"y":116,"style":117,"fill":118,"textAnchor":83},"124","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#2f3b35","where QGIS lives",[86,121,122,126,129,132,136],{},[53,123],{"x":124,"y":91,"width":92,"height":93,"rx":94,"fill":95,"stroke":125,"style":97},"164","#2563eb",[99,127],{"cx":128,"cy":102,"r":90,"fill":125},"186",[77,130,131],{"x":128,"y":105,"style":106,"fill":95,"textAnchor":83},"2",[77,133,135],{"x":134,"y":111,"style":112,"fill":82,"textAnchor":83},"232","initQgis()",[77,137,138],{"x":134,"y":116,"style":117,"fill":118,"textAnchor":83},"GUI flag = False",[86,140,141,145,148,151,156],{},[53,142],{"x":143,"y":91,"width":92,"height":93,"rx":94,"fill":144,"stroke":75,"style":97},"316","#26322d",[99,146],{"cx":147,"cy":102,"r":90,"fill":75},"338",[77,149,150],{"x":147,"y":105,"style":106,"fill":95,"textAnchor":83},"3",[77,152,155],{"x":153,"y":111,"style":112,"fill":154,"textAnchor":83},"384","#d9f99d","layers + settings",[77,157,158],{"x":153,"y":116,"style":117,"fill":154,"textAnchor":83},"no canvas involved",[86,160,161,165,168,171,175],{},[53,162],{"x":163,"y":91,"width":92,"height":93,"rx":94,"fill":95,"stroke":164,"style":97},"468","#15803d",[99,166],{"cx":167,"cy":102,"r":90,"fill":164},"490",[77,169,170],{"x":167,"y":105,"style":106,"fill":95,"textAnchor":83},"4",[77,172,174],{"x":173,"y":111,"style":112,"fill":82,"textAnchor":83},"536","render + save",[77,176,177],{"x":173,"y":116,"style":117,"fill":118,"textAnchor":83},"waitForFinished()",[86,179,180,185,188,190,194],{},[53,181],{"x":182,"y":91,"width":183,"height":93,"rx":94,"fill":95,"stroke":184,"style":97},"620","128","#b91c1c",[99,186],{"cx":187,"cy":102,"r":90,"fill":184},"642",[77,189,68],{"x":187,"y":105,"style":106,"fill":95,"textAnchor":83},[77,191,193],{"x":192,"y":111,"style":112,"fill":82,"textAnchor":83},"684","exitQgis()",[77,195,196],{"x":192,"y":116,"style":117,"fill":118,"textAnchor":83},"release resources",[198,199],"line",{"x1":200,"y1":201,"x2":202,"y2":201,"stroke":75,"style":203},"148","99","160","stroke-width:2.5;marker-end:url(#headlessArrow)",[198,205],{"x1":206,"y1":201,"x2":207,"y2":201,"stroke":75,"style":203},"300","312",[198,209],{"x1":210,"y1":201,"x2":211,"y2":201,"stroke":75,"style":203},"452","464",[198,213],{"x1":214,"y1":201,"x2":215,"y2":201,"stroke":75,"style":203},"604","616",[53,217],{"x":90,"y":218,"width":219,"height":220,"rx":67,"fill":95,"stroke":184,"style":221},"176","352","60","stroke-width:2",[77,223,226],{"x":224,"y":225,"style":112,"fill":184,"textAnchor":83},"188","200","skip step 1",[77,228,231],{"x":224,"y":229,"style":230,"fill":118,"textAnchor":83},"220","text-anchor:middle;font-size:11px;font-family:sans-serif","segmentation fault, no traceback",[53,233],{"x":234,"y":218,"width":219,"height":220,"rx":67,"fill":95,"stroke":96,"style":221},"396",[77,236,238],{"x":237,"y":225,"style":112,"fill":96,"textAnchor":83},"572","skip step 5",[77,240,241],{"x":237,"y":229,"style":230,"fill":118,"textAnchor":83},"process hangs, cron job never completes",[243,244,246],"h2",{"id":245},"prerequisites","Prerequisites",[248,249,250,258,266],"ul",{},[251,252,253,257],"li",{},[254,255,256],"strong",{},"QGIS 3.34 LTR"," installed on the machine that will run the script, with its Python bindings available.",[251,259,260,261,265],{},"The Python interpreter QGIS was built against — mixing interpreters is the most common cause of an import failure. See ",[30,262,264],{"href":263},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Frunning-python-scripts-outside-qgis-desktop\u002F","Running Python Scripts Outside QGIS Desktop",".",[251,267,268],{},"A data source and an output directory the scheduled user can write to.",[243,270,272],{"id":271},"the-complete-standalone-script","The complete standalone script",[14,274,275],{},"Everything below is one file, runnable with the QGIS Python interpreter and nothing else.",[277,278,283],"pre",{"className":279,"code":280,"language":281,"meta":282,"style":282},"language-python shiki shiki-themes github-dark","import sys\nfrom qgis.core import (\n    QgsApplication,\n    QgsMapRendererParallelJob,\n    QgsMapSettings,\n    QgsVectorLayer,\n)\nfrom qgis.PyQt.QtCore import QSize\nfrom qgis.PyQt.QtGui import QColor\n\nQgsApplication.setPrefixPath(\"\u002Fusr\", True)\napp = QgsApplication([], False)\napp.initQgis()\n\ntry:\n    layer = QgsVectorLayer(\"\u002Fdata\u002Fparcels.gpkg|layername=parcels\", \"parcels\", \"ogr\")\n    if not layer.isValid():\n        raise RuntimeError(\"could not load the layer\")\n\n    layer.loadNamedStyle(\"\u002Fdata\u002Fstyles\u002Fparcels.qml\")\n\n    settings = QgsMapSettings()\n    settings.setLayers([layer])\n    settings.setDestinationCrs(layer.crs())\n    settings.setBackgroundColor(QColor(\"#ffffff\"))\n    settings.setOutputDpi(150)\n\n    extent = layer.extent()\n    extent.scale(1.1)\n    width = 1600\n    height = int(width * extent.height() \u002F extent.width())\n    settings.setOutputSize(QSize(width, height))\n    settings.setExtent(extent)\n\n    job = QgsMapRendererParallelJob(settings)\n    job.start()\n    job.waitForFinished()\n    job.renderedImage().save(\"\u002Fdata\u002Fexports\u002Fparcels.png\", \"png\")\n    print(\"rendered\", width, \"x\", height)\nfinally:\n    app.exitQgis()\n","python","",[18,284,285,297,311,317,323,329,335,341,354,367,374,393,410,416,421,430,456,468,485,490,501,506,517,523,529,541,552,557,568,579,590,616,622,628,633,644,650,656,672,692,700],{"__ignoreMap":282},[286,287,289,293],"span",{"class":198,"line":288},1,[286,290,292],{"class":291},"snl16","import",[286,294,296],{"class":295},"s95oV"," sys\n",[286,298,300,303,306,308],{"class":198,"line":299},2,[286,301,302],{"class":291},"from",[286,304,305],{"class":295}," qgis.core ",[286,307,292],{"class":291},[286,309,310],{"class":295}," (\n",[286,312,314],{"class":198,"line":313},3,[286,315,316],{"class":295},"    QgsApplication,\n",[286,318,320],{"class":198,"line":319},4,[286,321,322],{"class":295},"    QgsMapRendererParallelJob,\n",[286,324,326],{"class":198,"line":325},5,[286,327,328],{"class":295},"    QgsMapSettings,\n",[286,330,332],{"class":198,"line":331},6,[286,333,334],{"class":295},"    QgsVectorLayer,\n",[286,336,338],{"class":198,"line":337},7,[286,339,340],{"class":295},")\n",[286,342,344,346,349,351],{"class":198,"line":343},8,[286,345,302],{"class":291},[286,347,348],{"class":295}," qgis.PyQt.QtCore ",[286,350,292],{"class":291},[286,352,353],{"class":295}," QSize\n",[286,355,357,359,362,364],{"class":198,"line":356},9,[286,358,302],{"class":291},[286,360,361],{"class":295}," qgis.PyQt.QtGui ",[286,363,292],{"class":291},[286,365,366],{"class":295}," QColor\n",[286,368,370],{"class":198,"line":369},10,[286,371,373],{"emptyLinePlaceholder":372},true,"\n",[286,375,377,380,384,387,391],{"class":198,"line":376},11,[286,378,379],{"class":295},"QgsApplication.setPrefixPath(",[286,381,383],{"class":382},"sU2Wk","\"\u002Fusr\"",[286,385,386],{"class":295},", ",[286,388,390],{"class":389},"sDLfK","True",[286,392,340],{"class":295},[286,394,396,399,402,405,408],{"class":198,"line":395},12,[286,397,398],{"class":295},"app ",[286,400,401],{"class":291},"=",[286,403,404],{"class":295}," QgsApplication([], ",[286,406,407],{"class":389},"False",[286,409,340],{"class":295},[286,411,413],{"class":198,"line":412},13,[286,414,415],{"class":295},"app.initQgis()\n",[286,417,419],{"class":198,"line":418},14,[286,420,373],{"emptyLinePlaceholder":372},[286,422,424,427],{"class":198,"line":423},15,[286,425,426],{"class":291},"try",[286,428,429],{"class":295},":\n",[286,431,433,436,438,441,444,446,449,451,454],{"class":198,"line":432},16,[286,434,435],{"class":295},"    layer ",[286,437,401],{"class":291},[286,439,440],{"class":295}," QgsVectorLayer(",[286,442,443],{"class":382},"\"\u002Fdata\u002Fparcels.gpkg|layername=parcels\"",[286,445,386],{"class":295},[286,447,448],{"class":382},"\"parcels\"",[286,450,386],{"class":295},[286,452,453],{"class":382},"\"ogr\"",[286,455,340],{"class":295},[286,457,459,462,465],{"class":198,"line":458},17,[286,460,461],{"class":291},"    if",[286,463,464],{"class":291}," not",[286,466,467],{"class":295}," layer.isValid():\n",[286,469,471,474,477,480,483],{"class":198,"line":470},18,[286,472,473],{"class":291},"        raise",[286,475,476],{"class":389}," RuntimeError",[286,478,479],{"class":295},"(",[286,481,482],{"class":382},"\"could not load the layer\"",[286,484,340],{"class":295},[286,486,488],{"class":198,"line":487},19,[286,489,373],{"emptyLinePlaceholder":372},[286,491,493,496,499],{"class":198,"line":492},20,[286,494,495],{"class":295},"    layer.loadNamedStyle(",[286,497,498],{"class":382},"\"\u002Fdata\u002Fstyles\u002Fparcels.qml\"",[286,500,340],{"class":295},[286,502,504],{"class":198,"line":503},21,[286,505,373],{"emptyLinePlaceholder":372},[286,507,509,512,514],{"class":198,"line":508},22,[286,510,511],{"class":295},"    settings ",[286,513,401],{"class":291},[286,515,516],{"class":295}," QgsMapSettings()\n",[286,518,520],{"class":198,"line":519},23,[286,521,522],{"class":295},"    settings.setLayers([layer])\n",[286,524,526],{"class":198,"line":525},24,[286,527,528],{"class":295},"    settings.setDestinationCrs(layer.crs())\n",[286,530,532,535,538],{"class":198,"line":531},25,[286,533,534],{"class":295},"    settings.setBackgroundColor(QColor(",[286,536,537],{"class":382},"\"#ffffff\"",[286,539,540],{"class":295},"))\n",[286,542,544,547,550],{"class":198,"line":543},26,[286,545,546],{"class":295},"    settings.setOutputDpi(",[286,548,549],{"class":389},"150",[286,551,340],{"class":295},[286,553,555],{"class":198,"line":554},27,[286,556,373],{"emptyLinePlaceholder":372},[286,558,560,563,565],{"class":198,"line":559},28,[286,561,562],{"class":295},"    extent ",[286,564,401],{"class":291},[286,566,567],{"class":295}," layer.extent()\n",[286,569,571,574,577],{"class":198,"line":570},29,[286,572,573],{"class":295},"    extent.scale(",[286,575,576],{"class":389},"1.1",[286,578,340],{"class":295},[286,580,582,585,587],{"class":198,"line":581},30,[286,583,584],{"class":295},"    width ",[286,586,401],{"class":291},[286,588,589],{"class":389}," 1600\n",[286,591,593,596,598,601,604,607,610,613],{"class":198,"line":592},31,[286,594,595],{"class":295},"    height ",[286,597,401],{"class":291},[286,599,600],{"class":389}," int",[286,602,603],{"class":295},"(width ",[286,605,606],{"class":291},"*",[286,608,609],{"class":295}," extent.height() ",[286,611,612],{"class":291},"\u002F",[286,614,615],{"class":295}," extent.width())\n",[286,617,619],{"class":198,"line":618},32,[286,620,621],{"class":295},"    settings.setOutputSize(QSize(width, height))\n",[286,623,625],{"class":198,"line":624},33,[286,626,627],{"class":295},"    settings.setExtent(extent)\n",[286,629,631],{"class":198,"line":630},34,[286,632,373],{"emptyLinePlaceholder":372},[286,634,636,639,641],{"class":198,"line":635},35,[286,637,638],{"class":295},"    job ",[286,640,401],{"class":291},[286,642,643],{"class":295}," QgsMapRendererParallelJob(settings)\n",[286,645,647],{"class":198,"line":646},36,[286,648,649],{"class":295},"    job.start()\n",[286,651,653],{"class":198,"line":652},37,[286,654,655],{"class":295},"    job.waitForFinished()\n",[286,657,659,662,665,667,670],{"class":198,"line":658},38,[286,660,661],{"class":295},"    job.renderedImage().save(",[286,663,664],{"class":382},"\"\u002Fdata\u002Fexports\u002Fparcels.png\"",[286,666,386],{"class":295},[286,668,669],{"class":382},"\"png\"",[286,671,340],{"class":295},[286,673,675,678,680,683,686,689],{"class":198,"line":674},39,[286,676,677],{"class":389},"    print",[286,679,479],{"class":295},[286,681,682],{"class":382},"\"rendered\"",[286,684,685],{"class":295},", width, ",[286,687,688],{"class":382},"\"x\"",[286,690,691],{"class":295},", height)\n",[286,693,695,698],{"class":198,"line":694},40,[286,696,697],{"class":291},"finally",[286,699,429],{"class":295},[286,701,703],{"class":198,"line":702},41,[286,704,705],{"class":295},"    app.exitQgis()\n",[14,707,708,711,712,715,716,719,720,723,724,727,728,731,732,735,736,738,739,742,743,746,747,751,752,612,754,756,757,759,760,763],{},[254,709,710],{},"Breakdown:"," ",[18,713,714],{},"setPrefixPath()"," tells QGIS where its resources live — ",[18,717,718],{},"\u002Fusr"," on most Linux distributions, and the path containing ",[18,721,722],{},"bin"," and ",[18,725,726],{},"apps"," on Windows. The second argument to ",[18,729,730],{},"QgsApplication"," is ",[18,733,734],{},"useGui","; passing ",[18,737,407],{}," is what makes the process viable without a display. ",[18,740,741],{},"loadNamedStyle()"," applies a ",[18,744,745],{},".qml"," file saved from the desktop, which is how a carefully designed style reaches a headless run without being rebuilt in code — see ",[30,748,750],{"href":749},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002F","Programmatic Layer Styling"," for the alternative. The ",[18,753,426],{},[18,755,697],{}," guarantees ",[18,758,193],{}," runs even on failure; without it the process can hang after an exception, which under ",[18,761,762],{},"cron"," means a job that never reports.",[243,765,767],{"id":766},"choose-the-right-job-class","Choose the right job class",[14,769,770],{},"There are two render jobs and the difference matters more headless than it does on the desktop.",[277,772,774],{"className":279,"code":773,"language":281,"meta":282,"style":282},"from qgis.core import QgsMapRendererSequentialJob\n\njob = QgsMapRendererSequentialJob(settings)\njob.start()\njob.waitForFinished()\nimage = job.renderedImage()\n",[18,775,776,787,791,801,806,811],{"__ignoreMap":282},[286,777,778,780,782,784],{"class":198,"line":288},[286,779,302],{"class":291},[286,781,305],{"class":295},[286,783,292],{"class":291},[286,785,786],{"class":295}," QgsMapRendererSequentialJob\n",[286,788,789],{"class":198,"line":299},[286,790,373],{"emptyLinePlaceholder":372},[286,792,793,796,798],{"class":198,"line":313},[286,794,795],{"class":295},"job ",[286,797,401],{"class":291},[286,799,800],{"class":295}," QgsMapRendererSequentialJob(settings)\n",[286,802,803],{"class":198,"line":319},[286,804,805],{"class":295},"job.start()\n",[286,807,808],{"class":198,"line":325},[286,809,810],{"class":295},"job.waitForFinished()\n",[286,812,813,816,818],{"class":198,"line":331},[286,814,815],{"class":295},"image ",[286,817,401],{"class":291},[286,819,820],{"class":295}," job.renderedImage()\n",[14,822,823,711,825,828,829,831,832,835,836,838],{},[254,824,710],{},[18,826,827],{},"QgsMapRendererParallelJob"," renders layers concurrently on worker threads, which is faster but needs a functioning Qt event loop to coordinate them. In some minimal container images that loop is not running the way it expects, and ",[18,830,177],{}," blocks forever. ",[18,833,834],{},"QgsMapRendererSequentialJob"," draws the layers one after another on the calling thread — slower on a multi-layer map, but it has no such dependency. If a headless render hangs and you have already confirmed ",[18,837,177],{}," is being called, switching job classes is the first thing to try.",[243,840,842],{"id":841},"render-without-a-display-server","Render without a display server",[14,844,845],{},"On a Linux server with no X or Wayland session, Qt still expects a display unless told otherwise.",[14,847,848],{},[38,849,852,855,858,861,868,871,876,880,884,887,891,894,897,900,903,907,911,915,918,922,925,927,930,933,939,943],{"viewBox":850,"role":41,"ariaLabel":851,"xmlns":43},"0 0 760 246","Three ways to run headless on a server: the offscreen Qt platform plugin, a virtual framebuffer, and a container image with the platform preset",[45,853,854],{},"Three ways to satisfy Qt without a display",[49,856,857],{},"Qt requires a platform plugin. The offscreen plugin, selected by an environment variable, needs no display server and is the simplest option. A virtual framebuffer wraps the command and provides a fake X server, which is heavier but supports code paths that need real widgets. A container image sets the variable in its own environment so the script itself needs no changes.",[53,859],{"x":55,"y":55,"width":56,"height":860,"fill":58},"246",[60,862,863],{},[63,864,866],{"id":865,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"headlessDispArrow",[72,867],{"d":74,"fill":164},[77,869,870],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"Qt needs a platform plugin, not necessarily a screen",[53,872],{"x":873,"y":874,"width":875,"height":124,"rx":94,"fill":95,"stroke":164,"style":97},"16","52","228",[77,877,879],{"x":878,"y":102,"style":106,"fill":164,"textAnchor":83},"130","offscreen plugin",[77,881,883],{"x":878,"y":111,"style":882,"fill":118,"textAnchor":83},"text-anchor:middle;font-size:10.5px;font-family:monospace","QT_QPA_PLATFORM",[77,885,886],{"x":878,"y":116,"style":882,"fill":118,"textAnchor":83},"=offscreen",[77,888,890],{"x":878,"y":889,"style":230,"fill":118,"textAnchor":83},"154","no display server at all",[77,892,893],{"x":878,"y":218,"style":112,"fill":164,"textAnchor":83},"simplest — start here",[53,895],{"x":896,"y":874,"width":875,"height":124,"rx":94,"fill":95,"stroke":125,"style":97},"266",[77,898,899],{"x":79,"y":102,"style":106,"fill":125,"textAnchor":83},"virtual framebuffer",[77,901,902],{"x":79,"y":111,"style":882,"fill":118,"textAnchor":83},"xvfb-run python …",[77,904,906],{"x":79,"y":905,"style":230,"fill":118,"textAnchor":83},"140","a real but invisible",[77,908,910],{"x":79,"y":909,"style":230,"fill":118,"textAnchor":83},"158","X server",[77,912,914],{"x":79,"y":913,"style":230,"fill":125,"textAnchor":83},"180","heavier, but widgets work",[53,916],{"x":917,"y":874,"width":875,"height":124,"rx":94,"fill":95,"stroke":96,"style":97},"516",[77,919,921],{"x":920,"y":102,"style":106,"fill":96,"textAnchor":83},"630","container image",[77,923,924],{"x":920,"y":111,"style":882,"fill":118,"textAnchor":83},"ENV QT_QPA_PLATFORM",[77,926,886],{"x":920,"y":116,"style":882,"fill":118,"textAnchor":83},[77,928,929],{"x":920,"y":889,"style":230,"fill":118,"textAnchor":83},"set once in the image",[77,931,932],{"x":920,"y":218,"style":112,"fill":96,"textAnchor":83},"script needs no changes",[198,934],{"x1":935,"y1":936,"x2":937,"y2":936,"stroke":164,"style":938},"244","134","262","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#headlessDispArrow)",[198,940],{"x1":941,"y1":936,"x2":942,"y2":936,"stroke":164,"style":938},"494","512",[77,944,947],{"x":79,"y":945,"style":230,"fill":946,"textAnchor":83},"236","#59645f","rendering to an image never needs a visible window",[277,949,953],{"className":950,"code":951,"language":952,"meta":282,"style":282},"language-bash shiki shiki-themes github-dark","export QT_QPA_PLATFORM=offscreen\nexport PYTHONPATH=\u002Fusr\u002Fshare\u002Fqgis\u002Fpython\n\u002Fusr\u002Fbin\u002Fpython3 \u002Fopt\u002Fmaps\u002Frender_parcels.py\n","bash",[18,954,955,968,980],{"__ignoreMap":282},[286,956,957,960,963,965],{"class":198,"line":288},[286,958,959],{"class":291},"export",[286,961,962],{"class":295}," QT_QPA_PLATFORM",[286,964,401],{"class":291},[286,966,967],{"class":295},"offscreen\n",[286,969,970,972,975,977],{"class":198,"line":299},[286,971,959],{"class":291},[286,973,974],{"class":295}," PYTHONPATH",[286,976,401],{"class":291},[286,978,979],{"class":295},"\u002Fusr\u002Fshare\u002Fqgis\u002Fpython\n",[286,981,982,986],{"class":198,"line":313},[286,983,985],{"class":984},"svObZ","\u002Fusr\u002Fbin\u002Fpython3",[286,987,988],{"class":382}," \u002Fopt\u002Fmaps\u002Frender_parcels.py\n",[14,990,991,711,993,996,997,1000,1001,1004],{},[254,992,710],{},[18,994,995],{},"QT_QPA_PLATFORM=offscreen"," selects Qt's offscreen platform plugin, which satisfies the requirement for a platform without needing any display server. ",[18,998,999],{},"PYTHONPATH"," points at the QGIS Python packages; the exact path varies by distribution, and ",[18,1002,1003],{},"python3 -c \"import qgis\""," is the quickest way to confirm it. Setting these in the environment rather than in the script keeps the script itself portable between a developer's desktop and the server.",[14,1006,1007,1008,1010],{},"Under ",[18,1009,762],{},", remember that almost none of your interactive environment is present. Export the variables inside the crontab entry or a wrapper script, use absolute paths everywhere, and redirect both streams to a log — a headless render that fails silently at 3 a.m. is otherwise invisible until someone notices a stale image.",[243,1012,1014],{"id":1013},"verify-the-output-before-trusting-it","Verify the output before trusting it",[14,1016,1017],{},"A headless render fails quietly. The layer does not load, the extent is empty, or the style file is missing — and the script still writes a perfectly valid PNG of nothing at all. Four assertions turn that into a job that reports its own failure.",[14,1019,1020],{},[38,1021,1024,1027,1030,1033,1040,1043,1071,1091,1110,1130,1134,1138,1142,1146,1148,1150],{"viewBox":1022,"role":41,"ariaLabel":1023,"xmlns":43},"0 0 760 250","Four checkpoints in a headless render: layer validity, feature count, extent validity and rendered image content, each with the silent failure it prevents",[45,1025,1026],{},"Four checkpoints a scheduled render should assert",[49,1028,1029],{},"A pipeline runs left to right through four checks. Layer validity catches a wrong path. A feature count above zero catches an empty or over-filtered source. A non-null extent catches a layer with no geometry. A non-uniform rendered image catches a style or extent problem. Each check is annotated with the silent failure it would otherwise let through.",[53,1031],{"x":55,"y":55,"width":56,"height":1032,"fill":58},"250",[60,1034,1035],{},[63,1036,1038],{"id":1037,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"hlVerifyArrow",[72,1039],{"d":74,"fill":75},[77,1041,1042],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"Each check turns a blank PNG into a named failure",[86,1044,1045,1049,1055,1059,1063,1067],{},[53,1046],{"x":90,"y":91,"width":1047,"height":1048,"rx":94,"fill":95,"stroke":125,"style":97},"168","72",[77,1050,1054],{"x":1051,"y":1052,"style":1053,"fill":125,"textAnchor":83},"96","82","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","isValid()",[77,1056,1058],{"x":1051,"y":1057,"style":230,"fill":118,"textAnchor":83},"104","the path is right",[53,1060],{"x":90,"y":1061,"width":1047,"height":1062,"rx":67,"fill":95,"stroke":184,"style":221},"146","66",[77,1064,1066],{"x":1051,"y":1065,"style":117,"fill":118,"textAnchor":83},"172","otherwise: a blank",[77,1068,1070],{"x":1051,"y":1069,"style":117,"fill":184,"textAnchor":83},"190","image, no error",[86,1072,1073,1076,1080,1083,1085,1088],{},[53,1074],{"x":1075,"y":91,"width":1047,"height":1048,"rx":94,"fill":95,"stroke":75,"style":97},"204",[77,1077,1079],{"x":1078,"y":1052,"style":1053,"fill":75,"textAnchor":83},"288","featureCount() > 0",[77,1081,1082],{"x":1078,"y":1057,"style":230,"fill":118,"textAnchor":83},"there is data",[53,1084],{"x":1075,"y":1061,"width":1047,"height":1062,"rx":67,"fill":95,"stroke":184,"style":221},[77,1086,1087],{"x":1078,"y":1065,"style":117,"fill":118,"textAnchor":83},"otherwise: a filter",[77,1089,1090],{"x":1078,"y":1069,"style":117,"fill":184,"textAnchor":83},"excluded everything",[86,1092,1093,1095,1099,1102,1104,1107],{},[53,1094],{"x":234,"y":91,"width":1047,"height":1048,"rx":94,"fill":95,"stroke":96,"style":97},[77,1096,1098],{"x":1097,"y":1052,"style":1053,"fill":96,"textAnchor":83},"480","extent not null",[77,1100,1101],{"x":1097,"y":1057,"style":230,"fill":118,"textAnchor":83},"it has geometry",[53,1103],{"x":234,"y":1061,"width":1047,"height":1062,"rx":67,"fill":95,"stroke":184,"style":221},[77,1105,1106],{"x":1097,"y":1065,"style":117,"fill":118,"textAnchor":83},"otherwise: rendered",[77,1108,1109],{"x":1097,"y":1069,"style":117,"fill":184,"textAnchor":83},"at an absurd scale",[86,1111,1112,1115,1119,1122,1124,1127],{},[53,1113],{"x":1114,"y":91,"width":202,"height":1048,"rx":94,"fill":95,"stroke":164,"style":97},"588",[77,1116,1118],{"x":1117,"y":1052,"style":1053,"fill":164,"textAnchor":83},"668","image not uniform",[77,1120,1121],{"x":1117,"y":1057,"style":230,"fill":118,"textAnchor":83},"something drew",[53,1123],{"x":1114,"y":1061,"width":202,"height":1062,"rx":67,"fill":95,"stroke":184,"style":221},[77,1125,1126],{"x":1117,"y":1065,"style":117,"fill":118,"textAnchor":83},"otherwise: a style",[77,1128,1129],{"x":1117,"y":1069,"style":117,"fill":184,"textAnchor":83},"or extent mistake",[198,1131],{"x1":913,"y1":1132,"x2":225,"y2":1132,"stroke":75,"style":1133},"92","stroke-width:2.5;marker-end:url(#hlVerifyArrow)",[198,1135],{"x1":1136,"y1":1132,"x2":1137,"y2":1132,"stroke":75,"style":1133},"372","392",[198,1139],{"x1":1140,"y1":1132,"x2":1141,"y2":1132,"stroke":75,"style":1133},"564","584",[198,1143],{"x1":1051,"y1":183,"x2":1051,"y2":1144,"stroke":184,"style":1145},"142","stroke-width:1.8;stroke-dasharray:4 3",[198,1147],{"x1":1078,"y1":183,"x2":1078,"y2":1144,"stroke":184,"style":1145},[198,1149],{"x1":1097,"y1":183,"x2":1097,"y2":1144,"stroke":184,"style":1145},[198,1151],{"x1":1117,"y1":183,"x2":1117,"y2":1144,"stroke":184,"style":1145},[277,1153,1155],{"className":279,"code":1154,"language":281,"meta":282,"style":282},"def render_checked(layer, settings, out_path):\n    if not layer.isValid():\n        raise RuntimeError(f\"layer did not load: {layer.source()}\")\n    if layer.featureCount() == 0:\n        raise RuntimeError(f\"layer {layer.name()} has no features\")\n\n    extent = layer.extent()\n    if extent.isNull() or extent.isEmpty():\n        raise RuntimeError(f\"layer {layer.name()} has no usable extent\")\n    extent.scale(1.1)\n    settings.setExtent(extent)\n\n    job = QgsMapRendererParallelJob(settings)\n    job.start()\n    job.waitForFinished()\n    image = job.renderedImage()\n\n    corner = image.pixel(0, 0)\n    middle = image.pixel(image.width() \u002F\u002F 2, image.height() \u002F\u002F 2)\n    if corner == middle and image.allGray():\n        raise RuntimeError(\"rendered image looks uniform — nothing drew\")\n\n    image.save(out_path, \"png\")\n    return out_path\n",[18,1156,1157,1168,1176,1204,1219,1244,1248,1256,1269,1292,1300,1304,1308,1316,1320,1324,1333,1337,1355,1380,1398,1411,1415,1424],{"__ignoreMap":282},[286,1158,1159,1162,1165],{"class":198,"line":288},[286,1160,1161],{"class":291},"def",[286,1163,1164],{"class":984}," render_checked",[286,1166,1167],{"class":295},"(layer, settings, out_path):\n",[286,1169,1170,1172,1174],{"class":198,"line":299},[286,1171,461],{"class":291},[286,1173,464],{"class":291},[286,1175,467],{"class":295},[286,1177,1178,1180,1182,1184,1187,1190,1193,1196,1199,1202],{"class":198,"line":313},[286,1179,473],{"class":291},[286,1181,476],{"class":389},[286,1183,479],{"class":295},[286,1185,1186],{"class":291},"f",[286,1188,1189],{"class":382},"\"layer did not load: ",[286,1191,1192],{"class":389},"{",[286,1194,1195],{"class":295},"layer.source()",[286,1197,1198],{"class":389},"}",[286,1200,1201],{"class":382},"\"",[286,1203,340],{"class":295},[286,1205,1206,1208,1211,1214,1217],{"class":198,"line":319},[286,1207,461],{"class":291},[286,1209,1210],{"class":295}," layer.featureCount() ",[286,1212,1213],{"class":291},"==",[286,1215,1216],{"class":389}," 0",[286,1218,429],{"class":295},[286,1220,1221,1223,1225,1227,1229,1232,1234,1237,1239,1242],{"class":198,"line":325},[286,1222,473],{"class":291},[286,1224,476],{"class":389},[286,1226,479],{"class":295},[286,1228,1186],{"class":291},[286,1230,1231],{"class":382},"\"layer ",[286,1233,1192],{"class":389},[286,1235,1236],{"class":295},"layer.name()",[286,1238,1198],{"class":389},[286,1240,1241],{"class":382}," has no features\"",[286,1243,340],{"class":295},[286,1245,1246],{"class":198,"line":331},[286,1247,373],{"emptyLinePlaceholder":372},[286,1249,1250,1252,1254],{"class":198,"line":337},[286,1251,562],{"class":295},[286,1253,401],{"class":291},[286,1255,567],{"class":295},[286,1257,1258,1260,1263,1266],{"class":198,"line":343},[286,1259,461],{"class":291},[286,1261,1262],{"class":295}," extent.isNull() ",[286,1264,1265],{"class":291},"or",[286,1267,1268],{"class":295}," extent.isEmpty():\n",[286,1270,1271,1273,1275,1277,1279,1281,1283,1285,1287,1290],{"class":198,"line":356},[286,1272,473],{"class":291},[286,1274,476],{"class":389},[286,1276,479],{"class":295},[286,1278,1186],{"class":291},[286,1280,1231],{"class":382},[286,1282,1192],{"class":389},[286,1284,1236],{"class":295},[286,1286,1198],{"class":389},[286,1288,1289],{"class":382}," has no usable extent\"",[286,1291,340],{"class":295},[286,1293,1294,1296,1298],{"class":198,"line":369},[286,1295,573],{"class":295},[286,1297,576],{"class":389},[286,1299,340],{"class":295},[286,1301,1302],{"class":198,"line":376},[286,1303,627],{"class":295},[286,1305,1306],{"class":198,"line":395},[286,1307,373],{"emptyLinePlaceholder":372},[286,1309,1310,1312,1314],{"class":198,"line":412},[286,1311,638],{"class":295},[286,1313,401],{"class":291},[286,1315,643],{"class":295},[286,1317,1318],{"class":198,"line":418},[286,1319,649],{"class":295},[286,1321,1322],{"class":198,"line":423},[286,1323,655],{"class":295},[286,1325,1326,1329,1331],{"class":198,"line":432},[286,1327,1328],{"class":295},"    image ",[286,1330,401],{"class":291},[286,1332,820],{"class":295},[286,1334,1335],{"class":198,"line":458},[286,1336,373],{"emptyLinePlaceholder":372},[286,1338,1339,1342,1344,1347,1349,1351,1353],{"class":198,"line":470},[286,1340,1341],{"class":295},"    corner ",[286,1343,401],{"class":291},[286,1345,1346],{"class":295}," image.pixel(",[286,1348,55],{"class":389},[286,1350,386],{"class":295},[286,1352,55],{"class":389},[286,1354,340],{"class":295},[286,1356,1357,1360,1362,1365,1368,1371,1374,1376,1378],{"class":198,"line":487},[286,1358,1359],{"class":295},"    middle ",[286,1361,401],{"class":291},[286,1363,1364],{"class":295}," image.pixel(image.width() ",[286,1366,1367],{"class":291},"\u002F\u002F",[286,1369,1370],{"class":389}," 2",[286,1372,1373],{"class":295},", image.height() ",[286,1375,1367],{"class":291},[286,1377,1370],{"class":389},[286,1379,340],{"class":295},[286,1381,1382,1384,1387,1389,1392,1395],{"class":198,"line":492},[286,1383,461],{"class":291},[286,1385,1386],{"class":295}," corner ",[286,1388,1213],{"class":291},[286,1390,1391],{"class":295}," middle ",[286,1393,1394],{"class":291},"and",[286,1396,1397],{"class":295}," image.allGray():\n",[286,1399,1400,1402,1404,1406,1409],{"class":198,"line":503},[286,1401,473],{"class":291},[286,1403,476],{"class":389},[286,1405,479],{"class":295},[286,1407,1408],{"class":382},"\"rendered image looks uniform — nothing drew\"",[286,1410,340],{"class":295},[286,1412,1413],{"class":198,"line":508},[286,1414,373],{"emptyLinePlaceholder":372},[286,1416,1417,1420,1422],{"class":198,"line":519},[286,1418,1419],{"class":295},"    image.save(out_path, ",[286,1421,669],{"class":382},[286,1423,340],{"class":295},[286,1425,1426,1429],{"class":198,"line":525},[286,1427,1428],{"class":291},"    return",[286,1430,1431],{"class":295}," out_path\n",[14,1433,1434,711,1436,1438,1439,1442,1443,1446],{},[254,1435,710],{},[18,1437,1054],{}," is the first and most valuable check — an invalid layer renders as nothing and reports no error anywhere. ",[18,1440,1441],{},"featureCount() == 0"," catches an over-aggressive ",[18,1444,1445],{},"subsetString"," left behind by earlier code, which is otherwise indistinguishable from a styling problem. Comparing a corner pixel with a centre pixel is a crude but effective smoke test: a real map is almost never uniform, so a match strongly suggests the extent or the style is wrong. Raising rather than logging means the scheduler sees a non-zero exit code, which is what turns a silent 3 a.m. failure into an alert.",[14,1448,1449],{},"For a job that runs unattended, pair these assertions with logging the output file's size and modification time. A PNG that is suspiciously small — a few hundred bytes — is almost always a blank canvas, and noticing that in a log is far cheaper than noticing it in a report a week later.",[243,1451,1453],{"id":1452},"qgis-version-compatibility","QGIS version compatibility",[14,1455,1456,1457,1459],{},"The examples target ",[254,1458,256],{}," (Python 3.12).",[1461,1462,1463,1479],"table",{},[1464,1465,1466],"thead",{},[1467,1468,1469,1473,1476],"tr",{},[1470,1471,1472],"th",{},"QGIS version",[1470,1474,1475],{},"Python",[1470,1477,1478],{},"Notes",[1480,1481,1482,1494,1505],"tbody",{},[1467,1483,1484,1488,1491],{},[1485,1486,1487],"td",{},"3.28 LTR",[1485,1489,1490],{},"3.9",[1485,1492,1493],{},"Identical startup sequence and job classes.",[1467,1495,1496,1499,1502],{},[1485,1497,1498],{},"3.34 LTR",[1485,1500,1501],{},"3.12",[1485,1503,1504],{},"Baseline for this page.",[1467,1506,1507,1510,1512],{},[1485,1508,1509],{},"3.40 \u002F 3.44",[1485,1511,1501],{},[1485,1513,1514,1517],{},[18,1515,1516],{},"QgsApplication.setPrefixPath()"," is optional on some packaged builds that detect it; setting it explicitly remains the safe choice.",[14,1519,1520],{},"The standalone initialisation pattern has been stable throughout 3.x, and is the same one Processing scripts use outside the desktop.",[243,1522,1524],{"id":1523},"troubleshooting","Troubleshooting",[248,1526,1527,1535,1546,1559,1573,1585],{},[251,1528,1529,711,1532,1534],{},[254,1530,1531],{},"Segmentation fault on startup.",[18,1533,714],{}," was not called, or points somewhere without the QGIS resources. There is no traceback because the crash is below Python.",[251,1536,1537,1542,1543,1545],{},[254,1538,1539,265],{},[18,1540,1541],{},"ImportError: No module named qgis"," The interpreter is not the one QGIS was built against, or ",[18,1544,999],{}," is missing the QGIS Python directory.",[251,1547,1548,1553,1554,1556,1557,265],{},[254,1549,1550,1551,265],{},"The script hangs at ",[18,1552,177],{}," Switch from ",[18,1555,827],{}," to ",[18,1558,834],{},[251,1560,1561,1566,1567,1569,1570,265],{},[254,1562,1563,265],{},[18,1564,1565],{},"could not connect to display"," Set ",[18,1568,995],{},", or run the script under ",[18,1571,1572],{},"xvfb-run",[251,1574,1575,711,1578,1580,1581,612,1583,265],{},[254,1576,1577],{},"The process never exits under cron.",[18,1579,193],{}," was skipped, probably because an exception escaped. Wrap the body in ",[18,1582,426],{},[18,1584,697],{},[251,1586,1587,1590,1591,1594],{},[254,1588,1589],{},"The image is blank."," The layer failed to load. Always check ",[18,1592,1593],{},"layer.isValid()"," and raise — an invalid layer renders as nothing, silently.",[243,1596,1598],{"id":1597},"conclusion","Conclusion",[14,1600,1601,1602,1604,1605,1608,1609,1611],{},"Headless rendering is the standalone initialisation ritual plus the render-job pattern: set the prefix path, construct ",[18,1603,730],{}," with the GUI disabled, build ",[18,1606,1607],{},"QgsMapSettings"," by hand, render synchronously, and always reach ",[18,1610,193],{},". Once that skeleton is right, the map itself is exactly the same code you would write interactively.",[243,1613,1615],{"id":1614},"frequently-asked-questions","Frequently Asked Questions",[14,1617,1618,1621,1623],{},[254,1619,1620],{},"Why does my standalone script crash immediately?",[18,1622,1516],{}," was not called before constructing the application, so QGIS cannot find its resources. The failure is a segmentation fault rather than a Python exception, which is why there is no traceback.",[14,1625,1626,1629,1630,1632,1633,1635],{},[254,1627,1628],{},"Do I need a display server to render a map?","\nNo. Set ",[18,1631,995],{}," and Qt uses its offscreen platform plugin. A virtual framebuffer such as ",[18,1634,1572],{}," is only needed for code paths that create real widgets.",[14,1637,1638,1641,1643,1644,1646],{},[254,1639,1640],{},"Why does waitForFinished() never return?",[18,1642,827],{}," coordinates worker threads through the Qt event loop, which some minimal environments do not provide as expected. Use ",[18,1645,834],{}," instead.",[14,1648,1649,1652,1653,1655,1656,1659],{},[254,1650,1651],{},"How do I apply my styling in a headless script?","\nSave the style from QGIS Desktop as a ",[18,1654,745],{}," file and call ",[18,1657,1658],{},"layer.loadNamedStyle()"," after loading the layer. Rebuilding the renderer in code is possible but rarely worth it for a design that already exists.",[14,1661,1662,1665,1666,1668],{},[254,1663,1664],{},"Can I run several headless renders in parallel?","\nRun them as separate processes rather than threads. A single ",[18,1667,730],{}," is not designed to be driven concurrently from multiple Python threads, and sharing one between them produces intermittent crashes rather than errors.",[14,1670,1671,1674],{},[254,1672,1673],{},"Does a headless render honour layer scale-visibility ranges?","\nYes. A layer outside its configured scale range is skipped exactly as it would be on screen, which surprises people whose export comes out missing a detail layer.",[243,1676,1678],{"id":1677},"related","Related",[248,1680,1681,1686,1690,1696],{},[251,1682,1683,1685],{},[30,1684,33],{"href":32}," — the guide this recipe belongs to",[251,1687,1688],{},[30,1689,264],{"href":263},[251,1691,1692],{},[30,1693,1695],{"href":1694},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fexport-map-canvas-to-image-pyqgis\u002F","Export the Map Canvas to a PNG with PyQGIS",[251,1697,1698],{},[30,1699,1701],{"href":1700},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fexporting-multiple-qgis-layouts-to-pdf\u002F","Exporting Multiple QGIS Layouts to PDF",[1703,1704,1705],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":282,"searchDepth":299,"depth":299,"links":1707},[1708,1709,1710,1711,1712,1713,1714,1715,1716,1717],{"id":245,"depth":299,"text":246},{"id":271,"depth":299,"text":272},{"id":766,"depth":299,"text":767},{"id":841,"depth":299,"text":842},{"id":1013,"depth":299,"text":1014},{"id":1452,"depth":299,"text":1453},{"id":1523,"depth":299,"text":1524},{"id":1597,"depth":299,"text":1598},{"id":1614,"depth":299,"text":1615},{"id":1677,"depth":299,"text":1678},"Render a QGIS map to a PNG with no GUI. Initialise QgsApplication standalone, build QgsMapSettings, run a render job, and schedule the script under cron or Docker.","md",{"slug":1721,"type":1722,"breadcrumb":1723,"datePublished":1724,"dateModified":1724},"render-layer-to-image-without-gui-pyqgis","article","Headless Rendering","2026-08-01","\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Frender-layer-to-image-without-gui-pyqgis",{"title":5,"description":1718},"pyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Frender-layer-to-image-without-gui-pyqgis\u002Findex","H4yDdDmz931WzRvcxipfPkvJ329JbuPQ3qAGV3eksr0",1785585990471]