[{"data":1,"prerenderedAt":1997},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fexport-temporal-animation-frames-pyqgis":3},{"id":4,"title":5,"body":6,"description":1986,"extension":1987,"meta":1988,"navigation":300,"path":1993,"seo":1994,"stem":1995,"__hash__":1996},"docs\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fexport-temporal-animation-frames-pyqgis\u002Findex.md","Export Temporal Animation Frames in PyQGIS",{"type":7,"value":8,"toc":1971},"minimark",[9,13,17,26,200,205,231,235,614,642,649,653,656,778,786,790,869,1047,1079,1083,1086,1181,1197,1201,1204,1469,1474,1477,1481,1598,1617,1621,1624,1627,1630,1633,1819,1824,1828,1844,1848,1893,1897,1903,1907,1916,1922,1928,1934,1938,1967],[10,11,5],"h1",{"id":12},"export-temporal-animation-frames-in-pyqgis",[14,15,16],"p",{},"Stepping the temporal controller and grabbing the canvas produces a folder of identical images. The canvas renders asynchronously, so a loop that sets a frame and immediately saves gets whatever happened to be on screen — usually the first frame, repeated. Exporting an animation correctly means rendering each frame through a job you can wait on, and that also happens to be the approach that works with no GUI at all.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002F","Temporal & 3D Visualization in PyQGIS",". It covers a synchronous render per frame, the layout route when you need a legend and a title, running headless, and turning the frames into a video.",[14,27,28],{},[29,30,35,39,43,50,67,76,86,92,101,107,114,117,121,126,133,138,143,147,151,155,159,163,166,170,173,178,181,184,188,196],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 316","img","Two frame export loops compared: grabbing the canvas after setting a frame produces duplicates because rendering is asynchronous, while a render job that is waited on produces correct frames","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Why the obvious loop produces identical frames",[40,41,42],"desc",{},"Setting the current frame on the controller schedules a canvas redraw and returns immediately, so a save that follows captures the previous image. Rendering through a map renderer job and waiting for it to finish produces the correct image for each frame, at the cost of writing the render code yourself.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","316","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"etaJobArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Asynchronous by default; synchronous on request",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":84,"style":85},"24","46","344","200","10","#fffdf7","#b91c1c","stroke-width:2.5",[68,87,91],{"x":88,"y":89,"style":90,"fill":84,"textAnchor":74},"196","72","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","canvas grab — wrong",[44,93],{"x":79,"y":94,"width":95,"height":96,"rx":97,"fill":98,"stroke":99,"style":100},"92","130","32","6","#f3e9d8","#b45309","stroke-width:1.8",[68,102,106],{"x":103,"y":104,"style":105,"fill":66,"textAnchor":74},"111","113","text-anchor:middle;font-size:10px;font-family:sans-serif","setCurrentFrame",[108,109],"line",{"x1":110,"y1":111,"x2":112,"y2":111,"stroke":66,"style":113},"176","108","198","stroke-width:1.8;marker-end:url(#etaJobArrow)",[44,115],{"x":116,"y":94,"width":95,"height":96,"rx":97,"fill":98,"stroke":99,"style":100},"206",[68,118,120],{"x":119,"y":104,"style":105,"fill":66,"textAnchor":74},"271","saveAsImage",[68,122,125],{"x":88,"y":123,"style":105,"fill":124,"textAnchor":74},"152","#59645f","the redraw has not happened yet",[44,127],{"x":79,"y":128,"width":129,"height":130,"rx":97,"fill":131,"stroke":124,"style":132},"168","290","30","#efeadd","stroke-width:1.6",[68,134,137],{"x":135,"y":136,"style":105,"fill":66,"textAnchor":74},"191","188","render finishes later, unobserved",[68,139,142],{"x":88,"y":140,"style":141,"fill":84,"textAnchor":74},"224","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","every file is the same image",[44,144],{"x":145,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":146,"style":85},"392","#15803d",[68,148,150],{"x":149,"y":89,"style":90,"fill":146,"textAnchor":74},"564","render job — right",[44,152],{"x":153,"y":94,"width":95,"height":96,"rx":97,"fill":154,"stroke":146,"style":100},"414","#e8efe6",[68,156,158],{"x":157,"y":104,"style":105,"fill":66,"textAnchor":74},"479","settings.setRange",[108,160],{"x1":161,"y1":111,"x2":162,"y2":111,"stroke":66,"style":113},"544","566",[44,164],{"x":165,"y":94,"width":95,"height":96,"rx":97,"fill":154,"stroke":146,"style":100},"574",[68,167,169],{"x":168,"y":104,"style":105,"fill":66,"textAnchor":74},"639","job.start()",[44,171],{"x":153,"y":172,"width":129,"height":130,"rx":97,"fill":154,"stroke":146,"style":100},"140",[68,174,177],{"x":175,"y":176,"style":105,"fill":66,"textAnchor":74},"559","160","job.waitForFinished()",[44,179],{"x":153,"y":180,"width":129,"height":130,"rx":97,"fill":154,"stroke":146,"style":100},"178",[68,182,183],{"x":175,"y":112,"style":105,"fill":66,"textAnchor":74},"renderedImage().save(path)",[68,185,187],{"x":149,"y":186,"style":141,"fill":146,"textAnchor":74},"230","one correct file per frame",[44,189],{"x":190,"y":191,"width":192,"height":193,"rx":58,"fill":194,"stroke":99,"style":195},"120","268","520","34","#fdf2e2","stroke-width:2",[68,197,199],{"x":70,"y":129,"style":198,"fill":66,"textAnchor":74},"text-anchor:middle;font-size:10.5px;font-family:sans-serif","the job route needs no canvas, so the same code runs headless",[201,202,204],"h2",{"id":203},"prerequisites","Prerequisites",[206,207,208,216,224],"ul",{},[209,210,211,215],"li",{},[212,213,214],"strong",{},"QGIS 3.34 LTR"," or newer.",[209,217,218,219,223],{},"Layers with temporal properties configured — see ",[21,220,222],{"href":221},"\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fset-layer-temporal-properties-pyqgis\u002F","setting layer temporal properties",".",[209,225,226,230],{},[227,228,229],"code",{},"ffmpeg"," on the path if you want a video at the end.",[201,232,234],{"id":233},"render-each-frame-through-a-job","Render each frame through a job",[236,237,242],"pre",{"className":238,"code":239,"language":240,"meta":241,"style":241},"language-python shiki shiki-themes github-dark","import os\nfrom qgis.core import (\n    QgsMapSettings, QgsMapRendererParallelJob, QgsDateTimeRange, QgsProject,\n)\nfrom qgis.PyQt.QtCore import QSize, QDateTime, QDate, QTime\n\nproject = QgsProject.instance()\nlayers = [project.mapLayersByName(n)[0] for n in (\"depth\", \"buildings\", \"basemap\")]\n\nsettings = QgsMapSettings()\nsettings.setLayers(layers)\nsettings.setDestinationCrs(project.crs())\nsettings.setExtent(layers[0].extent())\nsettings.setOutputSize(QSize(1920, 1080))\nsettings.setBackgroundColor(project.backgroundColor())\nsettings.setIsTemporal(True)\n\nstart = QDateTime(QDate(2026, 6, 1), QTime(0, 0))\nout_dir = \"\u002Fdata\u002Foutput\u002Fframes\"\nos.makedirs(out_dir, exist_ok=True)\n\nfor frame in range(168):\n    frame_start = start.addSecs(frame * 3600)\n    settings.setTemporalRange(QgsDateTimeRange(frame_start, frame_start.addSecs(3600)))\n\n    job = QgsMapRendererParallelJob(settings)\n    job.start()\n    job.waitForFinished()\n    job.renderedImage().save(os.path.join(out_dir, f\"frame_{frame:04d}.png\"))\n","python","",[227,243,244,256,270,276,282,295,302,314,361,366,377,383,389,400,417,423,434,439,473,484,500,505,526,545,557,562,573,579,585],{"__ignoreMap":241},[245,246,248,252],"span",{"class":108,"line":247},1,[245,249,251],{"class":250},"snl16","import",[245,253,255],{"class":254},"s95oV"," os\n",[245,257,259,262,265,267],{"class":108,"line":258},2,[245,260,261],{"class":250},"from",[245,263,264],{"class":254}," qgis.core ",[245,266,251],{"class":250},[245,268,269],{"class":254}," (\n",[245,271,273],{"class":108,"line":272},3,[245,274,275],{"class":254},"    QgsMapSettings, QgsMapRendererParallelJob, QgsDateTimeRange, QgsProject,\n",[245,277,279],{"class":108,"line":278},4,[245,280,281],{"class":254},")\n",[245,283,285,287,290,292],{"class":108,"line":284},5,[245,286,261],{"class":250},[245,288,289],{"class":254}," qgis.PyQt.QtCore ",[245,291,251],{"class":250},[245,293,294],{"class":254}," QSize, QDateTime, QDate, QTime\n",[245,296,298],{"class":108,"line":297},6,[245,299,301],{"emptyLinePlaceholder":300},true,"\n",[245,303,305,308,311],{"class":108,"line":304},7,[245,306,307],{"class":254},"project ",[245,309,310],{"class":250},"=",[245,312,313],{"class":254}," QgsProject.instance()\n",[245,315,317,320,322,325,328,331,334,337,340,343,347,350,353,355,358],{"class":108,"line":316},8,[245,318,319],{"class":254},"layers ",[245,321,310],{"class":250},[245,323,324],{"class":254}," [project.mapLayersByName(n)[",[245,326,46],{"class":327},"sDLfK",[245,329,330],{"class":254},"] ",[245,332,333],{"class":250},"for",[245,335,336],{"class":254}," n ",[245,338,339],{"class":250},"in",[245,341,342],{"class":254}," (",[245,344,346],{"class":345},"sU2Wk","\"depth\"",[245,348,349],{"class":254},", ",[245,351,352],{"class":345},"\"buildings\"",[245,354,349],{"class":254},[245,356,357],{"class":345},"\"basemap\"",[245,359,360],{"class":254},")]\n",[245,362,364],{"class":108,"line":363},9,[245,365,301],{"emptyLinePlaceholder":300},[245,367,369,372,374],{"class":108,"line":368},10,[245,370,371],{"class":254},"settings ",[245,373,310],{"class":250},[245,375,376],{"class":254}," QgsMapSettings()\n",[245,378,380],{"class":108,"line":379},11,[245,381,382],{"class":254},"settings.setLayers(layers)\n",[245,384,386],{"class":108,"line":385},12,[245,387,388],{"class":254},"settings.setDestinationCrs(project.crs())\n",[245,390,392,395,397],{"class":108,"line":391},13,[245,393,394],{"class":254},"settings.setExtent(layers[",[245,396,46],{"class":327},[245,398,399],{"class":254},"].extent())\n",[245,401,403,406,409,411,414],{"class":108,"line":402},14,[245,404,405],{"class":254},"settings.setOutputSize(QSize(",[245,407,408],{"class":327},"1920",[245,410,349],{"class":254},[245,412,413],{"class":327},"1080",[245,415,416],{"class":254},"))\n",[245,418,420],{"class":108,"line":419},15,[245,421,422],{"class":254},"settings.setBackgroundColor(project.backgroundColor())\n",[245,424,426,429,432],{"class":108,"line":425},16,[245,427,428],{"class":254},"settings.setIsTemporal(",[245,430,431],{"class":327},"True",[245,433,281],{"class":254},[245,435,437],{"class":108,"line":436},17,[245,438,301],{"emptyLinePlaceholder":300},[245,440,442,445,447,450,453,455,457,459,462,465,467,469,471],{"class":108,"line":441},18,[245,443,444],{"class":254},"start ",[245,446,310],{"class":250},[245,448,449],{"class":254}," QDateTime(QDate(",[245,451,452],{"class":327},"2026",[245,454,349],{"class":254},[245,456,97],{"class":327},[245,458,349],{"class":254},[245,460,461],{"class":327},"1",[245,463,464],{"class":254},"), QTime(",[245,466,46],{"class":327},[245,468,349],{"class":254},[245,470,46],{"class":327},[245,472,416],{"class":254},[245,474,476,479,481],{"class":108,"line":475},19,[245,477,478],{"class":254},"out_dir ",[245,480,310],{"class":250},[245,482,483],{"class":345}," \"\u002Fdata\u002Foutput\u002Fframes\"\n",[245,485,487,490,494,496,498],{"class":108,"line":486},20,[245,488,489],{"class":254},"os.makedirs(out_dir, ",[245,491,493],{"class":492},"s9osk","exist_ok",[245,495,310],{"class":250},[245,497,431],{"class":327},[245,499,281],{"class":254},[245,501,503],{"class":108,"line":502},21,[245,504,301],{"emptyLinePlaceholder":300},[245,506,508,510,513,515,518,521,523],{"class":108,"line":507},22,[245,509,333],{"class":250},[245,511,512],{"class":254}," frame ",[245,514,339],{"class":250},[245,516,517],{"class":327}," range",[245,519,520],{"class":254},"(",[245,522,128],{"class":327},[245,524,525],{"class":254},"):\n",[245,527,529,532,534,537,540,543],{"class":108,"line":528},23,[245,530,531],{"class":254},"    frame_start ",[245,533,310],{"class":250},[245,535,536],{"class":254}," start.addSecs(frame ",[245,538,539],{"class":250},"*",[245,541,542],{"class":327}," 3600",[245,544,281],{"class":254},[245,546,548,551,554],{"class":108,"line":547},24,[245,549,550],{"class":254},"    settings.setTemporalRange(QgsDateTimeRange(frame_start, frame_start.addSecs(",[245,552,553],{"class":327},"3600",[245,555,556],{"class":254},")))\n",[245,558,560],{"class":108,"line":559},25,[245,561,301],{"emptyLinePlaceholder":300},[245,563,565,568,570],{"class":108,"line":564},26,[245,566,567],{"class":254},"    job ",[245,569,310],{"class":250},[245,571,572],{"class":254}," QgsMapRendererParallelJob(settings)\n",[245,574,576],{"class":108,"line":575},27,[245,577,578],{"class":254},"    job.start()\n",[245,580,582],{"class":108,"line":581},28,[245,583,584],{"class":254},"    job.waitForFinished()\n",[245,586,588,591,594,597,600,603,606,609,612],{"class":108,"line":587},29,[245,589,590],{"class":254},"    job.renderedImage().save(os.path.join(out_dir, ",[245,592,593],{"class":250},"f",[245,595,596],{"class":345},"\"frame_",[245,598,599],{"class":327},"{",[245,601,602],{"class":254},"frame",[245,604,605],{"class":250},":04d",[245,607,608],{"class":327},"}",[245,610,611],{"class":345},".png\"",[245,613,416],{"class":254},[14,615,616,619,620,623,624,627,628,631,632,634,635,638,639,223],{},[212,617,618],{},"Breakdown:"," ",[227,621,622],{},"setIsTemporal(True)"," is the switch that makes the render honour the range at all — without it the temporal range is stored and ignored, and every frame comes out identical, which is the same symptom as the canvas bug and a different cause. ",[227,625,626],{},"waitForFinished()"," is what makes the job synchronous; ",[227,629,630],{},"QgsMapRendererParallelJob"," still uses several threads internally, so you get the speed without the asynchrony. Zero-padding the filename to four digits is not cosmetic — ",[227,633,229],{}," and most image sequence readers sort lexically, and ",[227,636,637],{},"frame_10.png"," sorts before ",[227,640,641],{},"frame_2.png",[14,643,644,645,648],{},"Reusing one ",[227,646,647],{},"QgsMapSettings"," across frames and only changing the range is deliberate: constructing it per frame re-resolves layers and re-reads styles for no benefit.",[201,650,652],{"id":651},"adding-a-date-label","Adding a date label",[14,654,655],{},"An animation without a visible clock is much harder to read, and the simplest way to add one is to draw it onto the image.",[236,657,659],{"className":238,"code":658,"language":240,"meta":241,"style":241},"from qgis.PyQt.QtGui import QPainter, QFont, QColor\n\nimage = job.renderedImage()\npainter = QPainter(image)\npainter.setFont(QFont(\"Sans\", 28, QFont.Bold))\npainter.setPen(QColor(20, 20, 20))\npainter.drawText(40, 70, frame_start.toString(\"d MMMM yyyy  HH:mm\"))\npainter.end()\nimage.save(os.path.join(out_dir, f\"frame_{frame:04d}.png\"))\n",[227,660,661,673,677,687,697,713,731,752,757],{"__ignoreMap":241},[245,662,663,665,668,670],{"class":108,"line":247},[245,664,261],{"class":250},[245,666,667],{"class":254}," qgis.PyQt.QtGui ",[245,669,251],{"class":250},[245,671,672],{"class":254}," QPainter, QFont, QColor\n",[245,674,675],{"class":108,"line":258},[245,676,301],{"emptyLinePlaceholder":300},[245,678,679,682,684],{"class":108,"line":272},[245,680,681],{"class":254},"image ",[245,683,310],{"class":250},[245,685,686],{"class":254}," job.renderedImage()\n",[245,688,689,692,694],{"class":108,"line":278},[245,690,691],{"class":254},"painter ",[245,693,310],{"class":250},[245,695,696],{"class":254}," QPainter(image)\n",[245,698,699,702,705,707,710],{"class":108,"line":284},[245,700,701],{"class":254},"painter.setFont(QFont(",[245,703,704],{"class":345},"\"Sans\"",[245,706,349],{"class":254},[245,708,709],{"class":327},"28",[245,711,712],{"class":254},", QFont.Bold))\n",[245,714,715,718,721,723,725,727,729],{"class":108,"line":297},[245,716,717],{"class":254},"painter.setPen(QColor(",[245,719,720],{"class":327},"20",[245,722,349],{"class":254},[245,724,720],{"class":327},[245,726,349],{"class":254},[245,728,720],{"class":327},[245,730,416],{"class":254},[245,732,733,736,739,741,744,747,750],{"class":108,"line":304},[245,734,735],{"class":254},"painter.drawText(",[245,737,738],{"class":327},"40",[245,740,349],{"class":254},[245,742,743],{"class":327},"70",[245,745,746],{"class":254},", frame_start.toString(",[245,748,749],{"class":345},"\"d MMMM yyyy  HH:mm\"",[245,751,416],{"class":254},[245,753,754],{"class":108,"line":316},[245,755,756],{"class":254},"painter.end()\n",[245,758,759,762,764,766,768,770,772,774,776],{"class":108,"line":363},[245,760,761],{"class":254},"image.save(os.path.join(out_dir, ",[245,763,593],{"class":250},[245,765,596],{"class":345},[245,767,599],{"class":327},[245,769,602],{"class":254},[245,771,605],{"class":250},[245,773,608],{"class":327},[245,775,611],{"class":345},[245,777,416],{"class":254},[14,779,780,619,782,785],{},[212,781,618],{},[227,783,784],{},"QPainter"," must be ended before the image is saved, or the last drawing operations may not be flushed — an easy omission that produces frames where the label appears intermittently. Drawing after the render rather than as a layer keeps the label crisp at any output size and avoids the label participating in the map's own scaling. For anything more elaborate than a date — a legend, a scale bar, a north arrow — the layout route below is the better answer.",[201,787,789],{"id":788},"the-layout-route","The layout route",[14,791,792],{},[29,793,796,799,802,805,808,812,815,820,823,827,831,835,838,842,848,852,856,859,862,865],{"viewBox":794,"role":32,"ariaLabel":795,"xmlns":34},"0 0 760 300","Comparison of exporting frames from map settings versus from a print layout, weighing speed against the map furniture a layout provides",[36,797,798],{},"Speed, or everything else",[40,800,801],{},"Rendering from map settings is fast and produces a bare map image. Rendering from a print layout is slower but brings a title, a legend, a scale bar and a north arrow, all of which update with the frame if they are driven by expressions.",[44,803],{"x":46,"y":46,"width":47,"height":804,"fill":49},"300",[68,806,807],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Pick the one whose output you can actually publish",[44,809],{"x":78,"y":79,"width":80,"height":88,"rx":82,"fill":810,"stroke":811,"style":85},"#eef7f4","#0f766e",[68,813,814],{"x":88,"y":89,"style":90,"fill":811,"textAnchor":74},"map settings",[44,816],{"x":817,"y":818,"width":819,"height":818,"fill":154,"stroke":811,"style":195},"52","90","288",[68,821,822],{"x":88,"y":172,"style":198,"fill":66,"textAnchor":74},"the map, and nothing else",[68,824,826],{"x":88,"y":825,"style":198,"fill":66,"textAnchor":74},"204","fast — a few hundred ms per frame",[68,828,830],{"x":88,"y":829,"style":105,"fill":124,"textAnchor":74},"226","add furniture with QPainter",[44,832],{"x":145,"y":79,"width":80,"height":88,"rx":82,"fill":833,"stroke":834,"style":85},"#eff3ff","#2563eb",[68,836,837],{"x":149,"y":89,"style":90,"fill":834,"textAnchor":74},"print layout",[44,839],{"x":840,"y":818,"width":819,"height":818,"fill":841,"stroke":834,"style":195},"420","#dbeafe",[44,843],{"x":844,"y":845,"width":846,"height":817,"fill":833,"stroke":834,"style":847},"430","98","180","stroke-width:1.4",[44,849],{"x":850,"y":845,"width":851,"height":193,"fill":833,"stroke":834,"style":847},"620","78",[44,853],{"x":844,"y":854,"width":845,"height":855,"fill":833,"stroke":834,"style":847},"158","14",[68,857,858],{"x":149,"y":825,"style":198,"fill":66,"textAnchor":74},"slower — legend, title, scale bar",[68,860,861],{"x":149,"y":829,"style":105,"fill":124,"textAnchor":74},"labels follow the frame via expressions",[44,863],{"x":190,"y":864,"width":192,"height":130,"rx":97,"fill":194,"stroke":99,"style":100},"262",[68,866,868],{"x":70,"y":867,"style":198,"fill":66,"textAnchor":74},"282","168 frames at 2 s each is six minutes — usually worth it",[236,870,872],{"className":238,"code":871,"language":240,"meta":241,"style":241},"from qgis.core import QgsLayoutExporter\n\nlayout = project.layoutManager().layoutByName(\"flood_frame\")\nmap_item = layout.itemById(\"main_map\")\nmap_item.setIsTemporal(True)\n\nexporter = QgsLayoutExporter(layout)\nimage_settings = QgsLayoutExporter.ImageExportSettings()\nimage_settings.dpi = 150\n\nfor frame in range(168):\n    frame_start = start.addSecs(frame * 3600)\n    map_item.setTemporalRange(\n        QgsDateTimeRange(frame_start, frame_start.addSecs(3600))\n    )\n    exporter.exportToImage(\n        os.path.join(out_dir, f\"layout_{frame:04d}.png\"), image_settings\n    )\n",[227,873,874,885,889,904,919,928,932,942,952,962,966,982,996,1001,1010,1015,1020,1043],{"__ignoreMap":241},[245,875,876,878,880,882],{"class":108,"line":247},[245,877,261],{"class":250},[245,879,264],{"class":254},[245,881,251],{"class":250},[245,883,884],{"class":254}," QgsLayoutExporter\n",[245,886,887],{"class":108,"line":258},[245,888,301],{"emptyLinePlaceholder":300},[245,890,891,894,896,899,902],{"class":108,"line":272},[245,892,893],{"class":254},"layout ",[245,895,310],{"class":250},[245,897,898],{"class":254}," project.layoutManager().layoutByName(",[245,900,901],{"class":345},"\"flood_frame\"",[245,903,281],{"class":254},[245,905,906,909,911,914,917],{"class":108,"line":278},[245,907,908],{"class":254},"map_item ",[245,910,310],{"class":250},[245,912,913],{"class":254}," layout.itemById(",[245,915,916],{"class":345},"\"main_map\"",[245,918,281],{"class":254},[245,920,921,924,926],{"class":108,"line":284},[245,922,923],{"class":254},"map_item.setIsTemporal(",[245,925,431],{"class":327},[245,927,281],{"class":254},[245,929,930],{"class":108,"line":297},[245,931,301],{"emptyLinePlaceholder":300},[245,933,934,937,939],{"class":108,"line":304},[245,935,936],{"class":254},"exporter ",[245,938,310],{"class":250},[245,940,941],{"class":254}," QgsLayoutExporter(layout)\n",[245,943,944,947,949],{"class":108,"line":316},[245,945,946],{"class":254},"image_settings ",[245,948,310],{"class":250},[245,950,951],{"class":254}," QgsLayoutExporter.ImageExportSettings()\n",[245,953,954,957,959],{"class":108,"line":363},[245,955,956],{"class":254},"image_settings.dpi ",[245,958,310],{"class":250},[245,960,961],{"class":327}," 150\n",[245,963,964],{"class":108,"line":368},[245,965,301],{"emptyLinePlaceholder":300},[245,967,968,970,972,974,976,978,980],{"class":108,"line":379},[245,969,333],{"class":250},[245,971,512],{"class":254},[245,973,339],{"class":250},[245,975,517],{"class":327},[245,977,520],{"class":254},[245,979,128],{"class":327},[245,981,525],{"class":254},[245,983,984,986,988,990,992,994],{"class":108,"line":385},[245,985,531],{"class":254},[245,987,310],{"class":250},[245,989,536],{"class":254},[245,991,539],{"class":250},[245,993,542],{"class":327},[245,995,281],{"class":254},[245,997,998],{"class":108,"line":391},[245,999,1000],{"class":254},"    map_item.setTemporalRange(\n",[245,1002,1003,1006,1008],{"class":108,"line":402},[245,1004,1005],{"class":254},"        QgsDateTimeRange(frame_start, frame_start.addSecs(",[245,1007,553],{"class":327},[245,1009,416],{"class":254},[245,1011,1012],{"class":108,"line":419},[245,1013,1014],{"class":254},"    )\n",[245,1016,1017],{"class":108,"line":425},[245,1018,1019],{"class":254},"    exporter.exportToImage(\n",[245,1021,1022,1025,1027,1030,1032,1034,1036,1038,1040],{"class":108,"line":436},[245,1023,1024],{"class":254},"        os.path.join(out_dir, ",[245,1026,593],{"class":250},[245,1028,1029],{"class":345},"\"layout_",[245,1031,599],{"class":327},[245,1033,602],{"class":254},[245,1035,605],{"class":250},[245,1037,608],{"class":327},[245,1039,611],{"class":345},[245,1041,1042],{"class":254},"), image_settings\n",[245,1044,1045],{"class":108,"line":441},[245,1046,1014],{"class":254},[14,1048,1049,619,1051,1054,1055,1058,1059,1063,1064,1067,1068,1070,1071,1074,1075,223],{},[212,1050,618],{},[227,1052,1053],{},"itemById"," finds the map item by the id set in the layout's item properties, so give it one rather than relying on ",[227,1056,1057],{},"itemsByType"," and index order. Setting the range on the ",[1060,1061,1062],"em",{},"map item",", not on the layout, is what makes the map's layers filter; a label showing the date reads ",[227,1065,1066],{},"@map_start_time"," and follows automatically, which is why the layout route needs no ",[227,1069,784],{},". ",[227,1072,1073],{},"exportToImage"," is synchronous, so no waiting is needed. The full mechanics of driving layouts are covered in ",[21,1076,1078],{"href":1077},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fexporting-multiple-qgis-layouts-to-pdf\u002F","exporting multiple layouts to PDF",[201,1080,1082],{"id":1081},"running-headless","Running headless",[14,1084,1085],{},"Everything above except the layout's optional GUI dependencies works with no display, because none of it touches the canvas. In a standalone script the only extra work is starting the application and loading the project.",[236,1087,1089],{"className":238,"code":1088,"language":240,"meta":241,"style":241},"from qgis.core import QgsApplication\n\nQgsApplication.setPrefixPath(\"\u002Fusr\", True)\napp = QgsApplication([], False)\napp.initQgis()\n\nproject = QgsProject.instance()\nproject.read(\"\u002Fdata\u002Fprojects\u002Fflood.qgz\")\n\n# ... the frame loop from above ...\n\napp.exitQgis()\n",[227,1090,1091,1102,1106,1120,1135,1140,1144,1152,1162,1166,1172,1176],{"__ignoreMap":241},[245,1092,1093,1095,1097,1099],{"class":108,"line":247},[245,1094,261],{"class":250},[245,1096,264],{"class":254},[245,1098,251],{"class":250},[245,1100,1101],{"class":254}," QgsApplication\n",[245,1103,1104],{"class":108,"line":258},[245,1105,301],{"emptyLinePlaceholder":300},[245,1107,1108,1111,1114,1116,1118],{"class":108,"line":272},[245,1109,1110],{"class":254},"QgsApplication.setPrefixPath(",[245,1112,1113],{"class":345},"\"\u002Fusr\"",[245,1115,349],{"class":254},[245,1117,431],{"class":327},[245,1119,281],{"class":254},[245,1121,1122,1125,1127,1130,1133],{"class":108,"line":278},[245,1123,1124],{"class":254},"app ",[245,1126,310],{"class":250},[245,1128,1129],{"class":254}," QgsApplication([], ",[245,1131,1132],{"class":327},"False",[245,1134,281],{"class":254},[245,1136,1137],{"class":108,"line":284},[245,1138,1139],{"class":254},"app.initQgis()\n",[245,1141,1142],{"class":108,"line":297},[245,1143,301],{"emptyLinePlaceholder":300},[245,1145,1146,1148,1150],{"class":108,"line":304},[245,1147,307],{"class":254},[245,1149,310],{"class":250},[245,1151,313],{"class":254},[245,1153,1154,1157,1160],{"class":108,"line":316},[245,1155,1156],{"class":254},"project.read(",[245,1158,1159],{"class":345},"\"\u002Fdata\u002Fprojects\u002Fflood.qgz\"",[245,1161,281],{"class":254},[245,1163,1164],{"class":108,"line":363},[245,1165,301],{"emptyLinePlaceholder":300},[245,1167,1168],{"class":108,"line":368},[245,1169,1171],{"class":1170},"sjoCn","# ... the frame loop from above ...\n",[245,1173,1174],{"class":108,"line":379},[245,1175,301],{"emptyLinePlaceholder":300},[245,1177,1178],{"class":108,"line":385},[245,1179,1180],{"class":254},"app.exitQgis()\n",[14,1182,1183,1185,1186,1188,1189,1192,1193,223],{},[212,1184,618],{}," Passing ",[227,1187,1132],{}," as the second argument runs without a GUI, which is what makes this work on a server. The project must be read before the layers are looked up, and the temporal properties travel with the project, so a project configured interactively animates correctly here with no extra setup. Calling ",[227,1190,1191],{},"exitQgis()"," matters in a long-running process; in a short script the operating system tidies up anyway. The wider pattern is described in ",[21,1194,1196],{"href":1195},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Frunning-python-scripts-outside-qgis-desktop\u002F","running Python scripts outside QGIS Desktop",[201,1198,1200],{"id":1199},"making-a-long-export-survivable","Making a long export survivable",[14,1202,1203],{},"A 500-frame layout export is twenty minutes of work, and twenty minutes is long enough that something will interrupt it. Two small habits make that a non-event.",[236,1205,1207],{"className":238,"code":1206,"language":240,"meta":241,"style":241},"import time\n\nstarted = time.monotonic()\nfor frame in range(total_frames):\n    path = os.path.join(out_dir, f\"frame_{frame:04d}.png\")\n    if os.path.exists(path):\n        continue\n\n    frame_start = start.addSecs(frame * step_seconds)\n    settings.setTemporalRange(\n        QgsDateTimeRange(frame_start, frame_start.addSecs(step_seconds))\n    )\n    job = QgsMapRendererParallelJob(settings)\n    job.start()\n    job.waitForFinished()\n    job.renderedImage().save(path)\n\n    if frame and frame % 25 == 0:\n        rate = (time.monotonic() - started) \u002F (frame + 1)\n        remaining = rate * (total_frames - frame - 1)\n        print(f\"frame {frame}\u002F{total_frames} — about {remaining \u002F 60:.1f} min left\")\n",[227,1208,1209,1216,1220,1230,1243,1269,1277,1282,1286,1299,1304,1309,1313,1321,1325,1329,1334,1338,1364,1394,1419],{"__ignoreMap":241},[245,1210,1211,1213],{"class":108,"line":247},[245,1212,251],{"class":250},[245,1214,1215],{"class":254}," time\n",[245,1217,1218],{"class":108,"line":258},[245,1219,301],{"emptyLinePlaceholder":300},[245,1221,1222,1225,1227],{"class":108,"line":272},[245,1223,1224],{"class":254},"started ",[245,1226,310],{"class":250},[245,1228,1229],{"class":254}," time.monotonic()\n",[245,1231,1232,1234,1236,1238,1240],{"class":108,"line":278},[245,1233,333],{"class":250},[245,1235,512],{"class":254},[245,1237,339],{"class":250},[245,1239,517],{"class":327},[245,1241,1242],{"class":254},"(total_frames):\n",[245,1244,1245,1248,1250,1253,1255,1257,1259,1261,1263,1265,1267],{"class":108,"line":284},[245,1246,1247],{"class":254},"    path ",[245,1249,310],{"class":250},[245,1251,1252],{"class":254}," os.path.join(out_dir, ",[245,1254,593],{"class":250},[245,1256,596],{"class":345},[245,1258,599],{"class":327},[245,1260,602],{"class":254},[245,1262,605],{"class":250},[245,1264,608],{"class":327},[245,1266,611],{"class":345},[245,1268,281],{"class":254},[245,1270,1271,1274],{"class":108,"line":297},[245,1272,1273],{"class":250},"    if",[245,1275,1276],{"class":254}," os.path.exists(path):\n",[245,1278,1279],{"class":108,"line":304},[245,1280,1281],{"class":250},"        continue\n",[245,1283,1284],{"class":108,"line":316},[245,1285,301],{"emptyLinePlaceholder":300},[245,1287,1288,1290,1292,1294,1296],{"class":108,"line":363},[245,1289,531],{"class":254},[245,1291,310],{"class":250},[245,1293,536],{"class":254},[245,1295,539],{"class":250},[245,1297,1298],{"class":254}," step_seconds)\n",[245,1300,1301],{"class":108,"line":368},[245,1302,1303],{"class":254},"    settings.setTemporalRange(\n",[245,1305,1306],{"class":108,"line":379},[245,1307,1308],{"class":254},"        QgsDateTimeRange(frame_start, frame_start.addSecs(step_seconds))\n",[245,1310,1311],{"class":108,"line":385},[245,1312,1014],{"class":254},[245,1314,1315,1317,1319],{"class":108,"line":391},[245,1316,567],{"class":254},[245,1318,310],{"class":250},[245,1320,572],{"class":254},[245,1322,1323],{"class":108,"line":402},[245,1324,578],{"class":254},[245,1326,1327],{"class":108,"line":419},[245,1328,584],{"class":254},[245,1330,1331],{"class":108,"line":425},[245,1332,1333],{"class":254},"    job.renderedImage().save(path)\n",[245,1335,1336],{"class":108,"line":436},[245,1337,301],{"emptyLinePlaceholder":300},[245,1339,1340,1342,1344,1347,1349,1352,1355,1358,1361],{"class":108,"line":441},[245,1341,1273],{"class":250},[245,1343,512],{"class":254},[245,1345,1346],{"class":250},"and",[245,1348,512],{"class":254},[245,1350,1351],{"class":250},"%",[245,1353,1354],{"class":327}," 25",[245,1356,1357],{"class":250}," ==",[245,1359,1360],{"class":327}," 0",[245,1362,1363],{"class":254},":\n",[245,1365,1366,1369,1371,1374,1377,1380,1383,1386,1389,1392],{"class":108,"line":475},[245,1367,1368],{"class":254},"        rate ",[245,1370,310],{"class":250},[245,1372,1373],{"class":254}," (time.monotonic() ",[245,1375,1376],{"class":250},"-",[245,1378,1379],{"class":254}," started) ",[245,1381,1382],{"class":250},"\u002F",[245,1384,1385],{"class":254}," (frame ",[245,1387,1388],{"class":250},"+",[245,1390,1391],{"class":327}," 1",[245,1393,281],{"class":254},[245,1395,1396,1399,1401,1404,1406,1409,1411,1413,1415,1417],{"class":108,"line":486},[245,1397,1398],{"class":254},"        remaining ",[245,1400,310],{"class":250},[245,1402,1403],{"class":254}," rate ",[245,1405,539],{"class":250},[245,1407,1408],{"class":254}," (total_frames ",[245,1410,1376],{"class":250},[245,1412,512],{"class":254},[245,1414,1376],{"class":250},[245,1416,1391],{"class":327},[245,1418,281],{"class":254},[245,1420,1421,1424,1426,1428,1431,1433,1435,1437,1439,1441,1444,1446,1449,1451,1454,1456,1459,1462,1464,1467],{"class":108,"line":502},[245,1422,1423],{"class":327},"        print",[245,1425,520],{"class":254},[245,1427,593],{"class":250},[245,1429,1430],{"class":345},"\"frame ",[245,1432,599],{"class":327},[245,1434,602],{"class":254},[245,1436,608],{"class":327},[245,1438,1382],{"class":345},[245,1440,599],{"class":327},[245,1442,1443],{"class":254},"total_frames",[245,1445,608],{"class":327},[245,1447,1448],{"class":345}," — about ",[245,1450,599],{"class":327},[245,1452,1453],{"class":254},"remaining ",[245,1455,1382],{"class":250},[245,1457,1458],{"class":327}," 60",[245,1460,1461],{"class":250},":.1f",[245,1463,608],{"class":327},[245,1465,1466],{"class":345}," min left\"",[245,1468,281],{"class":254},[14,1470,1471,1473],{},[212,1472,618],{}," The existence check makes the loop resumable, which is worth having for the same reason it is in every batch recipe on this site: a re-run after an interruption costs only the frames that are missing. The progress estimate uses elapsed time over completed frames rather than a per-frame measurement, so it smooths over the first frame's cache warm-up and over frames that happen to be heavier than others. Printing every twenty-fifth frame rather than every frame keeps the log readable in a scheduled job's output.",[14,1475,1476],{},"One caveat on resumability: a frame interrupted mid-save leaves a truncated PNG that passes the existence check. Writing to a temporary name and renaming after the save closes that hole, and renaming within one directory is atomic on every platform that matters.",[201,1478,1480],{"id":1479},"assembling-the-video","Assembling the video",[236,1482,1484],{"className":238,"code":1483,"language":240,"meta":241,"style":241},"import subprocess\n\nsubprocess.run([\n    \"ffmpeg\", \"-y\",\n    \"-framerate\", \"12\",\n    \"-i\", os.path.join(out_dir, \"frame_%04d.png\"),\n    \"-c:v\", \"libx264\", \"-pix_fmt\", \"yuv420p\", \"-crf\", \"20\",\n    \"\u002Fdata\u002Foutput\u002Fflood.mp4\",\n], check=True)\n",[227,1485,1486,1493,1497,1502,1515,1527,1545,1577,1584],{"__ignoreMap":241},[245,1487,1488,1490],{"class":108,"line":247},[245,1489,251],{"class":250},[245,1491,1492],{"class":254}," subprocess\n",[245,1494,1495],{"class":108,"line":258},[245,1496,301],{"emptyLinePlaceholder":300},[245,1498,1499],{"class":108,"line":272},[245,1500,1501],{"class":254},"subprocess.run([\n",[245,1503,1504,1507,1509,1512],{"class":108,"line":278},[245,1505,1506],{"class":345},"    \"ffmpeg\"",[245,1508,349],{"class":254},[245,1510,1511],{"class":345},"\"-y\"",[245,1513,1514],{"class":254},",\n",[245,1516,1517,1520,1522,1525],{"class":108,"line":284},[245,1518,1519],{"class":345},"    \"-framerate\"",[245,1521,349],{"class":254},[245,1523,1524],{"class":345},"\"12\"",[245,1526,1514],{"class":254},[245,1528,1529,1532,1535,1537,1540,1542],{"class":108,"line":297},[245,1530,1531],{"class":345},"    \"-i\"",[245,1533,1534],{"class":254},", os.path.join(out_dir, ",[245,1536,596],{"class":345},[245,1538,1539],{"class":327},"%04d",[245,1541,611],{"class":345},[245,1543,1544],{"class":254},"),\n",[245,1546,1547,1550,1552,1555,1557,1560,1562,1565,1567,1570,1572,1575],{"class":108,"line":304},[245,1548,1549],{"class":345},"    \"-c:v\"",[245,1551,349],{"class":254},[245,1553,1554],{"class":345},"\"libx264\"",[245,1556,349],{"class":254},[245,1558,1559],{"class":345},"\"-pix_fmt\"",[245,1561,349],{"class":254},[245,1563,1564],{"class":345},"\"yuv420p\"",[245,1566,349],{"class":254},[245,1568,1569],{"class":345},"\"-crf\"",[245,1571,349],{"class":254},[245,1573,1574],{"class":345},"\"20\"",[245,1576,1514],{"class":254},[245,1578,1579,1582],{"class":108,"line":316},[245,1580,1581],{"class":345},"    \"\u002Fdata\u002Foutput\u002Fflood.mp4\"",[245,1583,1514],{"class":254},[245,1585,1586,1589,1592,1594,1596],{"class":108,"line":363},[245,1587,1588],{"class":254},"], ",[245,1590,1591],{"class":492},"check",[245,1593,310],{"class":250},[245,1595,431],{"class":327},[245,1597,281],{"class":254},[14,1599,1600,619,1602,1605,1606,1609,1610,1612,1613,1616],{},[212,1601,618],{},[227,1603,1604],{},"-pix_fmt yuv420p"," is the setting that makes the file play in browsers and on phones; without it many players show nothing at all. ",[227,1607,1608],{},"-crf 20"," is a quality-versus-size dial where lower is better, and 18 to 23 is the useful range. The ",[227,1611,1539],{}," pattern must match the zero-padding used when saving. ",[227,1614,1615],{},"check=True"," turns an ffmpeg failure into an exception rather than a silent empty file. For an animated GIF instead, a palette pass produces far better results than a direct conversion, at the cost of two commands.",[201,1618,1620],{"id":1619},"keeping-the-frames-comparable","Keeping the frames comparable",[14,1622,1623],{},"An animation is a comparison across time, and three things silently break that comparison if you let them.",[14,1625,1626],{},"The colour classification is the first. A graduated renderer classified on the currently visible features reclassifies as the animation runs, so a value that was orange in frame 10 is yellow in frame 40 and the viewer reads a change that is not in the data. Classify once against the whole dataset's range before starting the loop and leave it alone.",[14,1628,1629],{},"The extent is the second. Any code that zooms to the visible features per frame produces an animation that pans and scales continuously, which makes movement impossible to judge. Set the extent once from the union of everything the animation will show.",[14,1631,1632],{},"The layer set is the third, and it bites specifically with a raster stack: layers whose temporal ranges do not tile exactly leave gaps where nothing is visible and overlaps where two scenes fight. Checking that the ranges tile — that each ends where the next begins — before rendering five hundred frames is thirty seconds well spent.",[236,1634,1636],{"className":238,"code":1635,"language":240,"meta":241,"style":241},"ranges = sorted(\n    (l.temporalProperties().fixedTemporalRange(), l.name())\n    for l in layers if l.temporalProperties().isActive()\n)\nfor (first, name_a), (second, name_b) in zip(ranges, ranges[1:]):\n    gap = first.end().secsTo(second.begin())\n    if gap > 0:\n        print(f\"gap of {gap}s between {name_a} and {name_b}\")\n    elif gap \u003C 0:\n        print(f\"overlap of {-gap}s between {name_a} and {name_b}\")\n",[227,1637,1638,1651,1656,1675,1679,1699,1709,1723,1766,1780],{"__ignoreMap":241},[245,1639,1640,1643,1645,1648],{"class":108,"line":247},[245,1641,1642],{"class":254},"ranges ",[245,1644,310],{"class":250},[245,1646,1647],{"class":327}," sorted",[245,1649,1650],{"class":254},"(\n",[245,1652,1653],{"class":108,"line":258},[245,1654,1655],{"class":254},"    (l.temporalProperties().fixedTemporalRange(), l.name())\n",[245,1657,1658,1661,1664,1666,1669,1672],{"class":108,"line":272},[245,1659,1660],{"class":250},"    for",[245,1662,1663],{"class":254}," l ",[245,1665,339],{"class":250},[245,1667,1668],{"class":254}," layers ",[245,1670,1671],{"class":250},"if",[245,1673,1674],{"class":254}," l.temporalProperties().isActive()\n",[245,1676,1677],{"class":108,"line":278},[245,1678,281],{"class":254},[245,1680,1681,1683,1686,1688,1691,1694,1696],{"class":108,"line":284},[245,1682,333],{"class":250},[245,1684,1685],{"class":254}," (first, name_a), (second, name_b) ",[245,1687,339],{"class":250},[245,1689,1690],{"class":327}," zip",[245,1692,1693],{"class":254},"(ranges, ranges[",[245,1695,461],{"class":327},[245,1697,1698],{"class":254},":]):\n",[245,1700,1701,1704,1706],{"class":108,"line":297},[245,1702,1703],{"class":254},"    gap ",[245,1705,310],{"class":250},[245,1707,1708],{"class":254}," first.end().secsTo(second.begin())\n",[245,1710,1711,1713,1716,1719,1721],{"class":108,"line":304},[245,1712,1273],{"class":250},[245,1714,1715],{"class":254}," gap ",[245,1717,1718],{"class":250},">",[245,1720,1360],{"class":327},[245,1722,1363],{"class":254},[245,1724,1725,1727,1729,1731,1734,1736,1739,1741,1744,1746,1749,1751,1754,1756,1759,1761,1764],{"class":108,"line":316},[245,1726,1423],{"class":327},[245,1728,520],{"class":254},[245,1730,593],{"class":250},[245,1732,1733],{"class":345},"\"gap of ",[245,1735,599],{"class":327},[245,1737,1738],{"class":254},"gap",[245,1740,608],{"class":327},[245,1742,1743],{"class":345},"s between ",[245,1745,599],{"class":327},[245,1747,1748],{"class":254},"name_a",[245,1750,608],{"class":327},[245,1752,1753],{"class":345}," and ",[245,1755,599],{"class":327},[245,1757,1758],{"class":254},"name_b",[245,1760,608],{"class":327},[245,1762,1763],{"class":345},"\"",[245,1765,281],{"class":254},[245,1767,1768,1771,1773,1776,1778],{"class":108,"line":363},[245,1769,1770],{"class":250},"    elif",[245,1772,1715],{"class":254},[245,1774,1775],{"class":250},"\u003C",[245,1777,1360],{"class":327},[245,1779,1363],{"class":254},[245,1781,1782,1784,1786,1788,1791,1793,1795,1797,1799,1801,1803,1805,1807,1809,1811,1813,1815,1817],{"class":108,"line":368},[245,1783,1423],{"class":327},[245,1785,520],{"class":254},[245,1787,593],{"class":250},[245,1789,1790],{"class":345},"\"overlap of ",[245,1792,599],{"class":327},[245,1794,1376],{"class":250},[245,1796,1738],{"class":254},[245,1798,608],{"class":327},[245,1800,1743],{"class":345},[245,1802,599],{"class":327},[245,1804,1748],{"class":254},[245,1806,608],{"class":327},[245,1808,1753],{"class":345},[245,1810,599],{"class":327},[245,1812,1758],{"class":254},[245,1814,608],{"class":327},[245,1816,1763],{"class":345},[245,1818,281],{"class":254},[14,1820,1821,1823],{},[212,1822,618],{}," Sorting by range puts the layers in temporal order regardless of their order in the tree, which is what makes the pairwise comparison meaningful. Reporting both gaps and overlaps matters because they produce opposite symptoms — a gap is a blank frame, an overlap is a frame where the wrong scene wins — and neither raises an error.",[201,1825,1827],{"id":1826},"qgis-version-compatibility","QGIS version compatibility",[14,1829,1830,1753,1833,1836,1837,1840,1841,1843],{},[227,1831,1832],{},"QgsMapSettings.setIsTemporal",[227,1834,1835],{},"setTemporalRange"," arrived with the temporal framework in 3.14. ",[227,1838,1839],{},"QgsLayoutItemMap.setIsTemporal"," arrived at the same time. ",[227,1842,630],{}," predates all of it and is unchanged. Nothing here differs between 3.34 and 3.44.",[201,1845,1847],{"id":1846},"troubleshooting","Troubleshooting",[206,1849,1850,1858,1864,1870,1878,1884],{},[209,1851,1852,619,1855,1857],{},[212,1853,1854],{},"Every frame is identical.",[227,1856,622],{}," was not set on the map settings or the map item.",[209,1859,1860,1863],{},[212,1861,1862],{},"Frames are blank."," The extent is wrong, or the layers list is empty because the project had not been read yet.",[209,1865,1866,1869],{},[212,1867,1868],{},"Frames appear in the wrong order in the video."," Filenames are not zero-padded.",[209,1871,1872,1875,1876,223],{},[212,1873,1874],{},"The video will not play."," Missing ",[227,1877,1604],{},[209,1879,1880,1883],{},[212,1881,1882],{},"The export is extremely slow."," Layout export at high DPI; drop to 96 for a video, where nobody sees the difference.",[209,1885,1886,1889,1890,1892],{},[212,1887,1888],{},"Labels appear in some frames only."," A ",[227,1891,784],{}," was not ended before saving.",[201,1894,1896],{"id":1895},"conclusion","Conclusion",[14,1898,1899,1900,1902],{},"Render through a job you can wait on, set ",[227,1901,622],{},", zero-pad the filenames, and choose between bare map settings for speed and a layout for everything else. The same loop runs headless without modification, which means an animation can be a scheduled job rather than an afternoon of clicking.",[201,1904,1906],{"id":1905},"frequently-asked-questions","Frequently Asked Questions",[14,1908,1909,1912,1913,1915],{},[212,1910,1911],{},"Can I export straight to video from QGIS?","\nThe GUI's animation export writes frames, not video. Assembling with ",[227,1914,229],{}," is the standard final step either way.",[14,1917,1918,1921],{},[212,1919,1920],{},"How do I export a 3D animation?","\nThrough a layout containing a 3D map item, setting the temporal range on it per frame exactly as above. The 3D canvas itself has no reliable synchronous capture across releases.",[14,1923,1924,1927],{},[212,1925,1926],{},"Why is my first frame different from the rest?","\nLayer rendering caches warm up on the first job. Render frame zero twice and discard the first if the difference is visible.",[14,1929,1930,1933],{},[212,1931,1932],{},"Can I parallelise the frame loop?","\nNot within one process — the render jobs already use the available cores. Splitting frame ranges across separate processes each with its own QGIS application works and scales well.",[201,1935,1937],{"id":1936},"related","Related",[206,1939,1940,1945,1951,1957,1962],{},[209,1941,1942,1944],{},[21,1943,24],{"href":23}," — the guide this recipe belongs to",[209,1946,1947],{},[21,1948,1950],{"href":1949},"\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fanimate-with-temporal-controller-pyqgis\u002F","Animate Layers with the Temporal Controller",[209,1952,1953],{},[21,1954,1956],{"href":1955},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Frender-layer-to-image-without-gui-pyqgis\u002F","Render a Layer to an Image Without the GUI",[209,1958,1959],{},[21,1960,1961],{"href":1077},"Exporting Multiple QGIS Layouts to PDF",[209,1963,1964],{},[21,1965,1966],{"href":1195},"Running Python Scripts Outside QGIS Desktop",[1968,1969,1970],"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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":241,"searchDepth":258,"depth":258,"links":1972},[1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985],{"id":203,"depth":258,"text":204},{"id":233,"depth":258,"text":234},{"id":651,"depth":258,"text":652},{"id":788,"depth":258,"text":789},{"id":1081,"depth":258,"text":1082},{"id":1199,"depth":258,"text":1200},{"id":1479,"depth":258,"text":1480},{"id":1619,"depth":258,"text":1620},{"id":1826,"depth":258,"text":1827},{"id":1846,"depth":258,"text":1847},{"id":1895,"depth":258,"text":1896},{"id":1905,"depth":258,"text":1906},{"id":1936,"depth":258,"text":1937},"Render an animation to PNG frames from Python — synchronous map render jobs, a layout per frame, headless operation with no controller, and assembling the result into a video.","md",{"slug":1989,"type":1990,"breadcrumb":1991,"datePublished":1992,"dateModified":1992},"export-temporal-animation-frames-pyqgis","article","Export Animation Frames","2026-09-05","\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fexport-temporal-animation-frames-pyqgis",{"title":5,"description":1986},"pyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fexport-temporal-animation-frames-pyqgis\u002Findex","A_zsIwnjoTc3v50svrAuy4r_lBPwbLud79YDIgnY5Js",1788563847734]