[{"data":1,"prerenderedAt":1502},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fexport-atlas-pages-to-individual-pdfs-pyqgis":3},{"id":4,"title":5,"body":6,"description":1491,"extension":1492,"meta":1493,"navigation":217,"path":1498,"seo":1499,"stem":1500,"__hash__":1501},"docs\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fexport-atlas-pages-to-individual-pdfs-pyqgis\u002Findex.md","Export Atlas Pages to Individual PDFs",{"type":7,"value":8,"toc":1478},"minimark",[9,13,17,26,145,150,181,185,401,424,428,431,481,498,502,505,589,598,699,703,708,792,806,890,894,1194,1206,1210,1278,1288,1292,1353,1357,1369,1373,1386,1399,1405,1415,1421,1427,1433,1443,1447,1474],[10,11,5],"h1",{"id":12},"export-atlas-pages-to-individual-pdfs",[14,15,16],"p",{},"A single two-hundred-page PDF is the right output when somebody is going to print a book. It is the wrong output when each ward officer needs their own map, when the files feed a document management system, or when a web page links to one sheet per site. For those, the atlas should produce two hundred files with meaningful names — which is one call, and a handful of decisions around it.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002F","Automating Atlas Map Series",". It covers exporting one file per feature, controlling the names and the output settings, exporting a subset, and running the whole thing unattended with useful progress and error reporting.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,90,96,105,110,114,118,123,127,130,133,141],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 268","img","Comparison of a single combined atlas PDF against one file per feature, showing which downstream uses each suits","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"One document, or one file each",[40,41,42],"desc",{},"A combined export produces a single multi-page PDF suited to printing and to reading as a book. A per-feature export produces one named file per coverage feature, suited to distribution, to linking from a web page, and to loading into a document system. The same atlas configuration drives both; only the export call differs.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","268","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"atlasExpArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#0f766e",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Same atlas, two export calls",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":66,"style":84},"288","48","184","56","10","#eef7f4","stroke-width:2.5",[68,86,89],{"x":70,"y":87,"style":88,"fill":66,"textAnchor":74},"72","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","configured atlas",[68,91,95],{"x":70,"y":92,"style":93,"fill":94,"textAnchor":74},"92","text-anchor:middle;font-size:11px;font-family:sans-serif","#2f3b35","200 pages",[44,97],{"x":98,"y":99,"width":100,"height":101,"rx":82,"fill":102,"stroke":103,"style":104},"20","146","330","98","#eff3ff","#2563eb","stroke-width:2",[68,106,109],{"x":107,"y":108,"style":88,"fill":103,"textAnchor":74},"185","172","exportToPdf — one document",[68,111,113],{"x":107,"y":112,"style":93,"fill":94,"textAnchor":74},"196","printing, binding, reading through",[68,115,117],{"x":107,"y":116,"style":93,"fill":94,"textAnchor":74},"218","one file, 180 MB",[44,119],{"x":120,"y":99,"width":100,"height":101,"rx":82,"fill":121,"stroke":122,"style":84},"410","#edf8e9","#15803d",[68,124,126],{"x":125,"y":108,"style":88,"fill":122,"textAnchor":74},"575","exportToPdfs — one per feature",[68,128,129],{"x":125,"y":112,"style":93,"fill":94,"textAnchor":74},"distribution, linking, filing",[68,131,132],{"x":125,"y":116,"style":93,"fill":94,"textAnchor":74},"200 named files",[134,135],"line",{"x1":136,"y1":137,"x2":138,"y2":139,"stroke":66,"style":140},"340","104","220","140","stroke-width:2;marker-end:url(#atlasExpArrow)",[134,142],{"x1":143,"y1":137,"x2":144,"y2":139,"stroke":66,"style":140},"420","540",[146,147,149],"h2",{"id":148},"prerequisites","Prerequisites",[151,152,153,161,169],"ul",{},[154,155,156,160],"li",{},[157,158,159],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[154,162,163,164,168],{},"A layout with a configured atlas — see ",[21,165,167],{"href":166},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fconfigure-atlas-coverage-layer-pyqgis\u002F","Configure an Atlas Coverage Layer in PyQGIS",".",[154,170,171,172,176,177,180],{},"A file name expression on the atlas, or every file will be called ",[173,174,175],"code",{},"output_1",", ",[173,178,179],{},"output_2"," and so on.",[146,182,184],{"id":183},"export-one-file-per-feature","Export one file per feature",[186,187,192],"pre",{"className":188,"code":189,"language":190,"meta":191,"style":191},"language-python shiki shiki-themes github-dark","from qgis.core import (QgsProject, QgsLayoutExporter)\n\nproject = QgsProject.instance()\nlayout = project.layoutManager().layoutByName(\"Ward maps\")\natlas = layout.atlas()\n\nsettings = QgsLayoutExporter.PdfExportSettings()\nsettings.dpi = 300\nsettings.rasterizeWholeImage = False\nsettings.forceVectorOutput = True\n\nexporter = QgsLayoutExporter(layout)\nresult, error = QgsLayoutExporter.exportToPdfs(\n    atlas, \"\u002Fdata\u002Fexports\u002Fwards\", settings)\n\nif result != QgsLayoutExporter.Success:\n    raise RuntimeError(f\"atlas export failed: {error}\")\n","python","",[173,193,194,212,219,231,249,260,265,276,288,299,310,315,326,337,349,354,369],{"__ignoreMap":191},[195,196,198,202,206,209],"span",{"class":134,"line":197},1,[195,199,201],{"class":200},"snl16","from",[195,203,205],{"class":204},"s95oV"," qgis.core ",[195,207,208],{"class":200},"import",[195,210,211],{"class":204}," (QgsProject, QgsLayoutExporter)\n",[195,213,215],{"class":134,"line":214},2,[195,216,218],{"emptyLinePlaceholder":217},true,"\n",[195,220,222,225,228],{"class":134,"line":221},3,[195,223,224],{"class":204},"project ",[195,226,227],{"class":200},"=",[195,229,230],{"class":204}," QgsProject.instance()\n",[195,232,234,237,239,242,246],{"class":134,"line":233},4,[195,235,236],{"class":204},"layout ",[195,238,227],{"class":200},[195,240,241],{"class":204}," project.layoutManager().layoutByName(",[195,243,245],{"class":244},"sU2Wk","\"Ward maps\"",[195,247,248],{"class":204},")\n",[195,250,252,255,257],{"class":134,"line":251},5,[195,253,254],{"class":204},"atlas ",[195,256,227],{"class":200},[195,258,259],{"class":204}," layout.atlas()\n",[195,261,263],{"class":134,"line":262},6,[195,264,218],{"emptyLinePlaceholder":217},[195,266,268,271,273],{"class":134,"line":267},7,[195,269,270],{"class":204},"settings ",[195,272,227],{"class":200},[195,274,275],{"class":204}," QgsLayoutExporter.PdfExportSettings()\n",[195,277,279,282,284],{"class":134,"line":278},8,[195,280,281],{"class":204},"settings.dpi ",[195,283,227],{"class":200},[195,285,287],{"class":286},"sDLfK"," 300\n",[195,289,291,294,296],{"class":134,"line":290},9,[195,292,293],{"class":204},"settings.rasterizeWholeImage ",[195,295,227],{"class":200},[195,297,298],{"class":286}," False\n",[195,300,302,305,307],{"class":134,"line":301},10,[195,303,304],{"class":204},"settings.forceVectorOutput ",[195,306,227],{"class":200},[195,308,309],{"class":286}," True\n",[195,311,313],{"class":134,"line":312},11,[195,314,218],{"emptyLinePlaceholder":217},[195,316,318,321,323],{"class":134,"line":317},12,[195,319,320],{"class":204},"exporter ",[195,322,227],{"class":200},[195,324,325],{"class":204}," QgsLayoutExporter(layout)\n",[195,327,329,332,334],{"class":134,"line":328},13,[195,330,331],{"class":204},"result, error ",[195,333,227],{"class":200},[195,335,336],{"class":204}," QgsLayoutExporter.exportToPdfs(\n",[195,338,340,343,346],{"class":134,"line":339},14,[195,341,342],{"class":204},"    atlas, ",[195,344,345],{"class":244},"\"\u002Fdata\u002Fexports\u002Fwards\"",[195,347,348],{"class":204},", settings)\n",[195,350,352],{"class":134,"line":351},15,[195,353,218],{"emptyLinePlaceholder":217},[195,355,357,360,363,366],{"class":134,"line":356},16,[195,358,359],{"class":200},"if",[195,361,362],{"class":204}," result ",[195,364,365],{"class":200},"!=",[195,367,368],{"class":204}," QgsLayoutExporter.Success:\n",[195,370,372,375,378,381,384,387,390,393,396,399],{"class":134,"line":371},17,[195,373,374],{"class":200},"    raise",[195,376,377],{"class":286}," RuntimeError",[195,379,380],{"class":204},"(",[195,382,383],{"class":200},"f",[195,385,386],{"class":244},"\"atlas export failed: ",[195,388,389],{"class":286},"{",[195,391,392],{"class":204},"error",[195,394,395],{"class":286},"}",[195,397,398],{"class":244},"\"",[195,400,248],{"class":204},[14,402,403,406,407,410,411,415,416,419,420,423],{},[157,404,405],{},"Breakdown:"," ",[173,408,409],{},"exportToPdfs()"," — plural — takes the atlas rather than the layout and a ",[412,413,414],"em",{},"directory"," rather than a file path, and writes one PDF per feature named from the atlas file name expression. ",[173,417,418],{},"dpi"," at 300 is print quality; 150 halves the file size and is fine for screen distribution. ",[173,421,422],{},"forceVectorOutput"," keeps text and lines as vectors so the PDF stays sharp and searchable, which is what you want unless the map contains blend modes or transparency that must be rasterised to render correctly. The result is an enumeration, not a boolean, and the error string names the file it stopped on — both worth surfacing in a scheduled job.",[146,425,427],{"id":426},"get-the-file-names-right","Get the file names right",[14,429,430],{},"The names come from the atlas configuration, not the export call:",[186,432,434],{"className":188,"code":433,"language":190,"meta":191,"style":191},"atlas.setFilenameExpression(\n    \"'ward_' || lower(replace(\\\"ward_name\\\", ' ', '_')) || '_' || \\\"ward_code\\\"\")\nprint(atlas.nameForPage(0))\n",[173,435,436,441,468],{"__ignoreMap":191},[195,437,438],{"class":134,"line":197},[195,439,440],{"class":204},"atlas.setFilenameExpression(\n",[195,442,443,446,449,452,454,457,459,462,464,466],{"class":134,"line":214},[195,444,445],{"class":244},"    \"'ward_' || lower(replace(",[195,447,448],{"class":286},"\\\"",[195,450,451],{"class":244},"ward_name",[195,453,448],{"class":286},[195,455,456],{"class":244},", ' ', '_')) || '_' || ",[195,458,448],{"class":286},[195,460,461],{"class":244},"ward_code",[195,463,448],{"class":286},[195,465,398],{"class":244},[195,467,248],{"class":204},[195,469,470,473,476,478],{"class":134,"line":221},[195,471,472],{"class":286},"print",[195,474,475],{"class":204},"(atlas.nameForPage(",[195,477,46],{"class":286},[195,479,480],{"class":204},"))\n",[14,482,483,485,486,489,490,493,494,497],{},[157,484,405],{}," Checking ",[173,487,488],{},"nameForPage(0)"," before exporting is the cheapest way to catch a name expression that produces something unusable — a ",[173,491,492],{},"NULL"," field yields an empty name, and every page then overwrites the same file, leaving one PDF where two hundred were expected. Including a code as well as a readable name guarantees uniqueness where two wards share a name, which happens more often than seems reasonable. Avoid characters that are illegal in file names on any platform your outputs might reach: slashes, colons and quotes are the usual offenders, and ",[173,495,496],{},"replace()"," chains handle them.",[146,499,501],{"id":500},"export-a-subset","Export a subset",[14,503,504],{},"For a long-running series, exporting only what changed is often the difference between a two-minute job and an hour.",[186,506,508],{"className":188,"code":507,"language":190,"meta":191,"style":191},"atlas.setFilterFeatures(True)\natlas.setFilterExpression(\"\\\"updated_on\\\" >= '2026-08-01'\")\n\nprint(atlas.count(), \"pages to export\")\nresult, error = QgsLayoutExporter.exportToPdfs(\n    atlas, \"\u002Fdata\u002Fexports\u002Fwards\", settings)\n\natlas.setFilterFeatures(False)      # restore for the next caller\n",[173,509,510,520,539,543,555,563,571,575],{"__ignoreMap":191},[195,511,512,515,518],{"class":134,"line":197},[195,513,514],{"class":204},"atlas.setFilterFeatures(",[195,516,517],{"class":286},"True",[195,519,248],{"class":204},[195,521,522,525,527,529,532,534,537],{"class":134,"line":214},[195,523,524],{"class":204},"atlas.setFilterExpression(",[195,526,398],{"class":244},[195,528,448],{"class":286},[195,530,531],{"class":244},"updated_on",[195,533,448],{"class":286},[195,535,536],{"class":244}," >= '2026-08-01'\"",[195,538,248],{"class":204},[195,540,541],{"class":134,"line":221},[195,542,218],{"emptyLinePlaceholder":217},[195,544,545,547,550,553],{"class":134,"line":233},[195,546,472],{"class":286},[195,548,549],{"class":204},"(atlas.count(), ",[195,551,552],{"class":244},"\"pages to export\"",[195,554,248],{"class":204},[195,556,557,559,561],{"class":134,"line":251},[195,558,331],{"class":204},[195,560,227],{"class":200},[195,562,336],{"class":204},[195,564,565,567,569],{"class":134,"line":262},[195,566,342],{"class":204},[195,568,345],{"class":244},[195,570,348],{"class":204},[195,572,573],{"class":134,"line":267},[195,574,218],{"emptyLinePlaceholder":217},[195,576,577,579,582,585],{"class":134,"line":278},[195,578,514],{"class":204},[195,580,581],{"class":286},"False",[195,583,584],{"class":204},")      ",[195,586,588],{"class":587},"sjoCn","# restore for the next caller\n",[14,590,591,593,594,597],{},[157,592,405],{}," The atlas filter is the mechanism for a partial export — there is no \"export pages 5 to 12\" argument, and using the filter is better anyway because it works on meaning rather than position. Restoring the filter afterwards matters when the same project is used by other jobs or by a person; a script that leaves a filter enabled produces a puzzling half-empty atlas the next time somebody opens it. In a scheduled job it is usually cleaner to read the project into a standalone ",[173,595,596],{},"QgsProject()"," so the change is discarded with the object.",[14,599,600],{},[29,601,604,607,610,613,620,623,631,637,641,645,648,651,654,658,661,665,669,672,678,682,686,691,695],{"viewBox":602,"role":32,"ariaLabel":603,"xmlns":34},"0 0 760 250","Diagram of a nightly export that filters the atlas to features updated since the last run, exports only those, and records the run time for the next comparison",[36,605,606],{},"Exporting only what changed",[40,608,609],{},"A scheduled export reads the timestamp of the previous run, filters the coverage layer to features updated since then, exports only those pages, and records the new timestamp. A full export runs on a weekly schedule so that any page missed by the incremental logic is eventually refreshed.",[44,611],{"x":46,"y":46,"width":47,"height":612,"fill":49},"250",[51,614,615],{},[54,616,618],{"id":617,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"incArrow",[63,619],{"d":65,"fill":94},[68,621,622],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Two hundred pages nightly is rarely necessary",[44,624],{"x":625,"y":626,"width":627,"height":628,"rx":58,"fill":629,"stroke":630,"style":104},"14","60","164","66","#fffdf7","#59645f",[68,632,636],{"x":633,"y":634,"style":635,"fill":73,"textAnchor":74},"96","88","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","read last run time",[68,638,640],{"x":633,"y":639,"style":93,"fill":94,"textAnchor":74},"108","from a state file",[44,642],{"x":643,"y":626,"width":644,"height":628,"rx":58,"fill":83,"stroke":66,"style":104},"200","176",[68,646,647],{"x":78,"y":634,"style":635,"fill":66,"textAnchor":74},"filter the atlas",[68,649,650],{"x":78,"y":639,"style":93,"fill":94,"textAnchor":74},"updated since then",[44,652],{"x":653,"y":626,"width":644,"height":628,"rx":58,"fill":121,"stroke":122,"style":104},"398",[68,655,657],{"x":656,"y":634,"style":635,"fill":122,"textAnchor":74},"486","export 6 pages",[68,659,660],{"x":656,"y":639,"style":93,"fill":94,"textAnchor":74},"instead of 200",[44,662],{"x":663,"y":626,"width":664,"height":628,"rx":58,"fill":629,"stroke":630,"style":104},"596","152",[68,666,668],{"x":667,"y":634,"style":635,"fill":73,"textAnchor":74},"672","record the time",[68,670,671],{"x":667,"y":639,"style":93,"fill":94,"textAnchor":74},"for the next run",[134,673],{"x1":674,"y1":675,"x2":676,"y2":675,"stroke":94,"style":677},"178","93","194","stroke-width:2;marker-end:url(#incArrow)",[134,679],{"x1":680,"y1":675,"x2":681,"y2":675,"stroke":94,"style":677},"376","392",[134,683],{"x1":684,"y1":675,"x2":685,"y2":675,"stroke":94,"style":677},"574","590",[44,687],{"x":139,"y":627,"width":688,"height":626,"rx":58,"fill":689,"stroke":690,"style":104},"480","#fdf2e2","#b45309",[68,692,694],{"x":70,"y":693,"style":635,"fill":690,"textAnchor":74},"188","and a full export weekly",[68,696,698],{"x":70,"y":697,"style":93,"fill":94,"textAnchor":74},"208","so a page the incremental logic missed is eventually refreshed",[146,700,702],{"id":701},"report-progress-on-a-long-run","Report progress on a long run",[14,704,705,707],{},[173,706,409],{}," blocks until every page is written, which on a large atlas can be many minutes with no output at all. Passing a feedback object gives progress:",[186,709,711],{"className":188,"code":710,"language":190,"meta":191,"style":191},"from qgis.core import QgsFeedback\n\nfeedback = QgsFeedback()\nfeedback.progressChanged.connect(lambda p: print(f\"{p:.0f}%\"))\n\nresult, error = QgsLayoutExporter.exportToPdfs(\n    atlas, \"\u002Fdata\u002Fexports\u002Fwards\", settings, QgsLayoutExporter.PdfExportSettings(), feedback)\n",[173,712,713,724,728,738,771,775,783],{"__ignoreMap":191},[195,714,715,717,719,721],{"class":134,"line":197},[195,716,201],{"class":200},[195,718,205],{"class":204},[195,720,208],{"class":200},[195,722,723],{"class":204}," QgsFeedback\n",[195,725,726],{"class":134,"line":214},[195,727,218],{"emptyLinePlaceholder":217},[195,729,730,733,735],{"class":134,"line":221},[195,731,732],{"class":204},"feedback ",[195,734,227],{"class":200},[195,736,737],{"class":204}," QgsFeedback()\n",[195,739,740,743,746,749,751,753,755,757,759,761,764,766,769],{"class":134,"line":233},[195,741,742],{"class":204},"feedback.progressChanged.connect(",[195,744,745],{"class":200},"lambda",[195,747,748],{"class":204}," p: ",[195,750,472],{"class":286},[195,752,380],{"class":204},[195,754,383],{"class":200},[195,756,398],{"class":244},[195,758,389],{"class":286},[195,760,14],{"class":204},[195,762,763],{"class":200},":.0f",[195,765,395],{"class":286},[195,767,768],{"class":244},"%\"",[195,770,480],{"class":204},[195,772,773],{"class":134,"line":251},[195,774,218],{"emptyLinePlaceholder":217},[195,776,777,779,781],{"class":134,"line":262},[195,778,331],{"class":204},[195,780,227],{"class":200},[195,782,336],{"class":204},[195,784,785,787,789],{"class":134,"line":267},[195,786,342],{"class":204},[195,788,345],{"class":244},[195,790,791],{"class":204},", settings, QgsLayoutExporter.PdfExportSettings(), feedback)\n",[14,793,794,796,797,800,801,805],{},[157,795,405],{}," The feedback object reports overall progress across the atlas and, importantly, can be cancelled — ",[173,798,799],{},"feedback.cancel()"," from another thread or a timer stops the export cleanly rather than leaving a half-written file. In a plugin this belongs on a background task so the interface stays responsive, following ",[21,802,804],{"href":803},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Frun-background-task-with-qgstask-pyqgis\u002F","Run a Background Task with QgsTask in PyQGIS",". In a scheduled script, printing progress to a log gives the operator something to look at when a run takes longer than usual, and it is the difference between \"the job is slow\" and \"the job is stuck\".",[14,807,808],{},[29,809,812,815,818,821,828,831,835,839,843,847,850,853,856,859,862,866,869,872,877,881,887],{"viewBox":810,"role":32,"ariaLabel":811,"xmlns":34},"0 0 760 248","Diagram of the checks that make an unattended atlas export trustworthy, covering the page count before the run, the feedback during it, and the file count afterwards",[36,813,814],{},"Three checks around a long export",[40,816,817],{},"Before the run, the page count confirms the filter did what was intended. During the run, feedback reports progress so a slow export can be distinguished from a stuck one. After the run, counting the files written catches a collapsed file name expression, which is the one failure that produces a successful return and almost no output.",[44,819],{"x":46,"y":46,"width":47,"height":820,"fill":49},"248",[51,822,823],{},[54,824,826],{"id":825,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"expChkArrow",[63,827],{"d":65,"fill":94},[68,829,830],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Check before, during and after — each catches a different failure",[44,832],{"x":833,"y":626,"width":834,"height":633,"rx":82,"fill":83,"stroke":66,"style":84},"16","228",[68,836,838],{"x":837,"y":634,"style":635,"fill":66,"textAnchor":74},"130","before — atlas.count()",[68,840,842],{"x":837,"y":841,"style":93,"fill":94,"textAnchor":74},"112","catches a filter that",[68,844,846],{"x":837,"y":845,"style":93,"fill":94,"textAnchor":74},"132","excludes everything",[44,848],{"x":849,"y":626,"width":834,"height":633,"rx":82,"fill":102,"stroke":103,"style":84},"266",[68,851,852],{"x":70,"y":634,"style":635,"fill":103,"textAnchor":74},"during — feedback",[68,854,855],{"x":70,"y":841,"style":93,"fill":94,"textAnchor":74},"distinguishes slow",[68,857,858],{"x":70,"y":845,"style":93,"fill":94,"textAnchor":74},"from stuck",[44,860],{"x":861,"y":626,"width":834,"height":633,"rx":82,"fill":121,"stroke":122,"style":84},"516",[68,863,865],{"x":864,"y":634,"style":635,"fill":122,"textAnchor":74},"630","after — count the files",[68,867,868],{"x":864,"y":841,"style":93,"fill":94,"textAnchor":74},"catches a collapsed",[68,870,871],{"x":864,"y":845,"style":93,"fill":94,"textAnchor":74},"name expression",[134,873],{"x1":874,"y1":639,"x2":875,"y2":639,"stroke":94,"style":876},"244","260","stroke-width:2;marker-end:url(#expChkArrow)",[134,878],{"x1":879,"y1":639,"x2":880,"y2":639,"stroke":94,"style":876},"494","510",[44,882],{"x":883,"y":884,"width":885,"height":886,"rx":58,"fill":689,"stroke":690,"style":104},"120","180","520","46",[68,888,889],{"x":70,"y":697,"style":93,"fill":94,"textAnchor":74},"Only the third catches a run that reports success and wrote three files",[146,891,893],{"id":892},"wrap-it-as-an-unattended-job","Wrap it as an unattended job",[186,895,897],{"className":188,"code":896,"language":190,"meta":191,"style":191},"from pathlib import Path\nfrom qgis.core import QgsProject\n\ndef export_ward_maps(project_path, output_dir):\n    project = QgsProject()\n    if not project.read(project_path):\n        raise RuntimeError(f\"cannot read {project_path}\")\n\n    layout = project.layoutManager().layoutByName(\"Ward maps\")\n    if layout is None:\n        raise RuntimeError(\"layout 'Ward maps' not found\")\n\n    Path(output_dir).mkdir(parents=True, exist_ok=True)\n\n    settings = QgsLayoutExporter.PdfExportSettings()\n    settings.dpi = 300\n\n    result, error = QgsLayoutExporter.exportToPdfs(\n        layout.atlas(), output_dir, settings)\n    if result != QgsLayoutExporter.Success:\n        raise RuntimeError(f\"export failed: {error}\")\n\n    produced = sorted(Path(output_dir).glob(\"*.pdf\"))\n    print(f\"wrote {len(produced)} files\")\n    return produced\n",[173,898,899,911,922,926,938,948,959,984,988,1001,1017,1030,1034,1058,1062,1071,1080,1084,1094,1100,1111,1135,1140,1159,1185],{"__ignoreMap":191},[195,900,901,903,906,908],{"class":134,"line":197},[195,902,201],{"class":200},[195,904,905],{"class":204}," pathlib ",[195,907,208],{"class":200},[195,909,910],{"class":204}," Path\n",[195,912,913,915,917,919],{"class":134,"line":214},[195,914,201],{"class":200},[195,916,205],{"class":204},[195,918,208],{"class":200},[195,920,921],{"class":204}," QgsProject\n",[195,923,924],{"class":134,"line":221},[195,925,218],{"emptyLinePlaceholder":217},[195,927,928,931,935],{"class":134,"line":233},[195,929,930],{"class":200},"def",[195,932,934],{"class":933},"svObZ"," export_ward_maps",[195,936,937],{"class":204},"(project_path, output_dir):\n",[195,939,940,943,945],{"class":134,"line":251},[195,941,942],{"class":204},"    project ",[195,944,227],{"class":200},[195,946,947],{"class":204}," QgsProject()\n",[195,949,950,953,956],{"class":134,"line":262},[195,951,952],{"class":200},"    if",[195,954,955],{"class":200}," not",[195,957,958],{"class":204}," project.read(project_path):\n",[195,960,961,964,966,968,970,973,975,978,980,982],{"class":134,"line":267},[195,962,963],{"class":200},"        raise",[195,965,377],{"class":286},[195,967,380],{"class":204},[195,969,383],{"class":200},[195,971,972],{"class":244},"\"cannot read ",[195,974,389],{"class":286},[195,976,977],{"class":204},"project_path",[195,979,395],{"class":286},[195,981,398],{"class":244},[195,983,248],{"class":204},[195,985,986],{"class":134,"line":278},[195,987,218],{"emptyLinePlaceholder":217},[195,989,990,993,995,997,999],{"class":134,"line":290},[195,991,992],{"class":204},"    layout ",[195,994,227],{"class":200},[195,996,241],{"class":204},[195,998,245],{"class":244},[195,1000,248],{"class":204},[195,1002,1003,1005,1008,1011,1014],{"class":134,"line":301},[195,1004,952],{"class":200},[195,1006,1007],{"class":204}," layout ",[195,1009,1010],{"class":200},"is",[195,1012,1013],{"class":286}," None",[195,1015,1016],{"class":204},":\n",[195,1018,1019,1021,1023,1025,1028],{"class":134,"line":312},[195,1020,963],{"class":200},[195,1022,377],{"class":286},[195,1024,380],{"class":204},[195,1026,1027],{"class":244},"\"layout 'Ward maps' not found\"",[195,1029,248],{"class":204},[195,1031,1032],{"class":134,"line":317},[195,1033,218],{"emptyLinePlaceholder":217},[195,1035,1036,1039,1043,1045,1047,1049,1052,1054,1056],{"class":134,"line":328},[195,1037,1038],{"class":204},"    Path(output_dir).mkdir(",[195,1040,1042],{"class":1041},"s9osk","parents",[195,1044,227],{"class":200},[195,1046,517],{"class":286},[195,1048,176],{"class":204},[195,1050,1051],{"class":1041},"exist_ok",[195,1053,227],{"class":200},[195,1055,517],{"class":286},[195,1057,248],{"class":204},[195,1059,1060],{"class":134,"line":339},[195,1061,218],{"emptyLinePlaceholder":217},[195,1063,1064,1067,1069],{"class":134,"line":351},[195,1065,1066],{"class":204},"    settings ",[195,1068,227],{"class":200},[195,1070,275],{"class":204},[195,1072,1073,1076,1078],{"class":134,"line":356},[195,1074,1075],{"class":204},"    settings.dpi ",[195,1077,227],{"class":200},[195,1079,287],{"class":286},[195,1081,1082],{"class":134,"line":371},[195,1083,218],{"emptyLinePlaceholder":217},[195,1085,1087,1090,1092],{"class":134,"line":1086},18,[195,1088,1089],{"class":204},"    result, error ",[195,1091,227],{"class":200},[195,1093,336],{"class":204},[195,1095,1097],{"class":134,"line":1096},19,[195,1098,1099],{"class":204},"        layout.atlas(), output_dir, settings)\n",[195,1101,1103,1105,1107,1109],{"class":134,"line":1102},20,[195,1104,952],{"class":200},[195,1106,362],{"class":204},[195,1108,365],{"class":200},[195,1110,368],{"class":204},[195,1112,1114,1116,1118,1120,1122,1125,1127,1129,1131,1133],{"class":134,"line":1113},21,[195,1115,963],{"class":200},[195,1117,377],{"class":286},[195,1119,380],{"class":204},[195,1121,383],{"class":200},[195,1123,1124],{"class":244},"\"export failed: ",[195,1126,389],{"class":286},[195,1128,392],{"class":204},[195,1130,395],{"class":286},[195,1132,398],{"class":244},[195,1134,248],{"class":204},[195,1136,1138],{"class":134,"line":1137},22,[195,1139,218],{"emptyLinePlaceholder":217},[195,1141,1143,1146,1148,1151,1154,1157],{"class":134,"line":1142},23,[195,1144,1145],{"class":204},"    produced ",[195,1147,227],{"class":200},[195,1149,1150],{"class":286}," sorted",[195,1152,1153],{"class":204},"(Path(output_dir).glob(",[195,1155,1156],{"class":244},"\"*.pdf\"",[195,1158,480],{"class":204},[195,1160,1162,1165,1167,1169,1172,1175,1178,1180,1183],{"class":134,"line":1161},24,[195,1163,1164],{"class":286},"    print",[195,1166,380],{"class":204},[195,1168,383],{"class":200},[195,1170,1171],{"class":244},"\"wrote ",[195,1173,1174],{"class":286},"{len",[195,1176,1177],{"class":204},"(produced)",[195,1179,395],{"class":286},[195,1181,1182],{"class":244}," files\"",[195,1184,248],{"class":204},[195,1186,1188,1191],{"class":134,"line":1187},25,[195,1189,1190],{"class":200},"    return",[195,1192,1193],{"class":204}," produced\n",[14,1195,1196,1198,1199,1201,1202,168],{},[157,1197,405],{}," Reading into a standalone ",[173,1200,596],{}," keeps the job independent of anything else running, and means any filter it sets is discarded when the function returns. Creating the output directory first is necessary — the exporter does not, and the failure message about a directory is not always obvious. Counting the files afterwards is the check that matters in an unattended run: a successful return with three files where two hundred were expected means the name expression collapsed, and only the count reveals it. Logging that number alongside the run is what turns a silent regression into an alert, as described in ",[21,1203,1205],{"href":1204},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fhandle-errors-and-logging-in-unattended-scripts\u002F","Handle Errors and Logging in Unattended Scripts",[146,1207,1209],{"id":1208},"qgis-version-compatibility","QGIS version compatibility",[1211,1212,1213,1229],"table",{},[1214,1215,1216],"thead",{},[1217,1218,1219,1223,1226],"tr",{},[1220,1221,1222],"th",{},"QGIS version",[1220,1224,1225],{},"Python",[1220,1227,1228],{},"Notes",[1230,1231,1232,1247,1257,1268],"tbody",{},[1217,1233,1234,1238,1241],{},[1235,1236,1237],"td",{},"3.22 LTR",[1235,1239,1240],{},"3.9",[1235,1242,1243,1246],{},[173,1244,1245],{},"exportToPdfs"," present with the same signature.",[1217,1248,1249,1252,1254],{},[1235,1250,1251],{},"3.28 LTR",[1235,1253,1240],{},[1235,1255,1256],{},"Identical.",[1217,1258,1259,1262,1265],{},[1235,1260,1261],{},"3.34 LTR",[1235,1263,1264],{},"3.12",[1235,1266,1267],{},"Baseline for this page; geospatial PDF options available in the settings object.",[1217,1269,1270,1273,1275],{},[1235,1271,1272],{},"3.40 \u002F 3.44",[1235,1274,1264],{},[1235,1276,1277],{},"Identical; newer releases add more PDF metadata and layered-export options.",[14,1279,1280,1283,1284,1287],{},[173,1281,1282],{},"exportToImage()"," and ",[173,1285,1286],{},"exportToSvg()"," have matching plural forms for atlases with the same shape, so switching output format changes one call and one settings class.",[146,1289,1291],{"id":1290},"troubleshooting","Troubleshooting",[151,1293,1294,1307,1316,1325,1333,1343],{},[154,1295,1296,1299,1300,1302,1303,1306],{},[157,1297,1298],{},"All pages overwrite one file."," The file name expression yields the same value — often because the field is ",[173,1301,492],{},". Check ",[173,1304,1305],{},"nameForPage()"," first.",[154,1308,1309,1312,1313,1315],{},[157,1310,1311],{},"The export fails on one page."," The error string names it. A ",[173,1314,492],{}," in a label expression or an unreachable remote layer is the usual cause.",[154,1317,1318,1321,1322,168],{},[157,1319,1320],{},"Files land in the wrong place."," The second argument is a directory for the plural form and a file path for the singular. Mixing them up produces a directory called ",[173,1323,1324],{},"wards.pdf",[154,1326,1327,406,1330,1332],{},[157,1328,1329],{},"Text is fuzzy in the output.",[173,1331,422],{}," is off, or a blend mode forced rasterisation. Check the layer styling.",[154,1334,1335,1338,1339,168],{},[157,1336,1337],{},"The export takes far longer than expected."," Remote layers are being fetched per page. Cache them locally first — see ",[21,1340,1342],{"href":1341},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002F","Web Services and Remote Data in PyQGIS",[154,1344,1345,1348,1349,1352],{},[157,1346,1347],{},"Nothing is written and no error appears."," The atlas filter excludes every feature. Print ",[173,1350,1351],{},"atlas.count()"," before exporting.",[146,1354,1356],{"id":1355},"conclusion","Conclusion",[14,1358,1359,1360,1362,1363,1283,1366,1368],{},"Use ",[173,1361,409],{}," with the atlas and an output directory to get one named file per feature, and drive the names from an atlas expression that is unique and file-system safe. Check ",[173,1364,1365],{},"count()",[173,1367,1305],{}," before a long run, use the filter to export only what changed, pass a feedback object so a long export reports progress and can be cancelled, and count the files produced afterwards — that count is the only check that catches a silently collapsed name expression.",[146,1370,1372],{"id":1371},"frequently-asked-questions","Frequently Asked Questions",[14,1374,1375,1378,1379,1283,1382,1385],{},[157,1376,1377],{},"Can I export to formats other than PDF?","\nYes — ",[173,1380,1381],{},"exportToImages()",[173,1383,1384],{},"exportToSvgs()"," take the same arguments with their own settings classes.",[14,1387,1388,1391,1392,1283,1395,1398],{},[157,1389,1390],{},"How do I add a page number or a total to each sheet?","\nUse the ",[173,1393,1394],{},"@atlas_featurenumber",[173,1396,1397],{},"@atlas_totalfeatures"," variables in a label expression. They are populated during the export.",[14,1400,1401,1404],{},[157,1402,1403],{},"Can I export a single named feature?","\nSet a filter expression matching that one feature and export as usual. It is the same mechanism as any other subset.",[14,1406,1407,1410,1411,1414],{},[157,1408,1409],{},"Does the export honour layer scale-based visibility?","\nYes, and at the atlas's per-page scale, which is worth remembering when ",[173,1412,1413],{},"Auto"," scaling puts some pages outside a layer's visible range.",[14,1416,1417,1420],{},[157,1418,1419],{},"Where do the files go if the name expression is empty?","\nThe exporter falls back to a numbered name, which is exactly the outcome the expression exists to avoid. Set one.",[14,1422,1423,1426],{},[157,1424,1425],{},"How large will the files be?","\nMostly determined by the raster content: a vector-only sheet is typically well under a megabyte, while one carrying aerial imagery at 300 dpi can be twenty times that. Lowering the dpi or clipping the imagery to the frame are the two levers that matter.",[14,1428,1429,1432],{},[157,1430,1431],{},"Can I produce a geospatial PDF?","\nYes — the export settings carry georeferencing options that embed the coordinate system and layer structure, so the result opens as a locatable map in readers that support it. It increases the file size and is worth it only when somebody downstream uses the feature.",[14,1434,1435,1438,1439,168],{},[157,1436,1437],{},"Can I run this without QGIS desktop?","\nYes — it is an ordinary headless export. See ",[21,1440,1442],{"href":1441},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002F","Headless QGIS and Server Automation",[146,1444,1446],{"id":1445},"related","Related",[151,1448,1449,1454,1458,1464,1470],{},[154,1450,1451,1453],{},[21,1452,24],{"href":23}," — the guide this recipe belongs to",[154,1455,1456],{},[21,1457,167],{"href":166},[154,1459,1460],{},[21,1461,1463],{"href":1462},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fgenerate-atlas-pdf-pyqgis\u002F","Export an Atlas to PDF in PyQGIS",[154,1465,1466],{},[21,1467,1469],{"href":1468},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fexporting-multiple-qgis-layouts-to-pdf\u002F","Exporting Multiple QGIS Layouts to PDF",[154,1471,1472],{},[21,1473,1205],{"href":1204},[1475,1476,1477],"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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":191,"searchDepth":214,"depth":214,"links":1479},[1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490],{"id":148,"depth":214,"text":149},{"id":183,"depth":214,"text":184},{"id":426,"depth":214,"text":427},{"id":500,"depth":214,"text":501},{"id":701,"depth":214,"text":702},{"id":892,"depth":214,"text":893},{"id":1208,"depth":214,"text":1209},{"id":1290,"depth":214,"text":1291},{"id":1355,"depth":214,"text":1356},{"id":1371,"depth":214,"text":1372},{"id":1445,"depth":214,"text":1446},"Produce one file per atlas feature instead of a single document — the exportToPdfs call, per-page settings and file names, exporting a subset, and reporting progress on a long unattended run.","md",{"slug":1494,"type":1495,"breadcrumb":1496,"datePublished":1497,"dateModified":1497},"export-atlas-pages-to-individual-pdfs-pyqgis","article","One PDF per Page","2026-08-15","\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fexport-atlas-pages-to-individual-pdfs-pyqgis",{"title":5,"description":1491},"spatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fexport-atlas-pages-to-individual-pdfs-pyqgis\u002Findex","OmccNgRjysVUEOrEYgCq53h8jVvFy8DJxU6QNgzCGiI",1786789586006]