[{"data":1,"prerenderedAt":1621},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fgenerate-atlas-pdf-pyqgis":3},{"id":4,"title":5,"body":6,"description":1614,"extension":1615,"meta":1616,"navigation":116,"path":1617,"seo":1618,"stem":1619,"__hash__":1620},"docs\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fgenerate-atlas-pdf-pyqgis\u002Findex.md","Generate an Atlas PDF in PyQGIS",{"type":7,"value":8,"toc":1600},"minimark",[9,13,23,31,36,73,76,80,83,317,357,361,368,611,643,647,654,916,953,957,1130,1144,1148,1155,1231,1259,1263,1269,1309,1329,1339,1343,1350,1409,1429,1433,1451,1466,1472,1478,1484,1488,1502,1506,1522,1536,1556,1575,1579,1596],[10,11,5],"h1",{"id":12},"generate-an-atlas-pdf-in-pyqgis",[14,15,16,17,22],"p",{},"This is the concrete, end-to-end recipe: take an existing QGIS project that contains a layout with an atlas-driven map, enable the atlas from code, point it at a coverage layer, optionally filter the features, and export every page to a PDF — either one combined document or one file per feature. The script runs in the QGIS Python Console and, with a small bootstrap, headlessly on a server. It is the applied counterpart to ",[18,19,21],"a",{"href":20},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002F","Automating Atlas Map Series in PyQGIS",", which explains the atlas object model this recipe drives.",[14,24,25,26,30],{},"If your maps live in separate layouts rather than a single atlas, the non-atlas pattern in ",[18,27,29],{"href":28},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fexporting-multiple-qgis-layouts-to-pdf\u002F","exporting multiple QGIS layouts to PDF"," is the better fit. Use this page when one layout fans out into many pages from a coverage layer.",[32,33,35],"h2",{"id":34},"prerequisites","Prerequisites",[37,38,39,47,63,70],"ul",{},[40,41,42,46],"li",{},[43,44,45],"strong",{},"QGIS 3.34 LTR"," (Python 3.12) recommended; works on 3.28 LTR and the 3.40\u002F3.44 line.",[40,48,49,50,54,55,58,59,62],{},"A saved project (",[51,52,53],"code",{},".qgz"," or ",[51,56,57],{},".qgs",") containing a ",[43,60,61],{},"print layout"," with a map item set to \"controlled by atlas\".",[40,64,65,66,69],{},"A ",[43,67,68],{},"coverage layer"," loaded in that project (the layer whose features become pages).",[40,71,72],{},"Write access to an output directory.",[14,74,75],{},"The layout's atlas can already be configured in the project, or you can configure it entirely from code as shown below — the script does not assume the project file has the atlas pre-enabled.",[32,77,79],{"id":78},"step-1-load-the-project-and-find-the-layout","Step 1: Load the Project and Find the Layout",[14,81,82],{},"Read the project, then fetch the layout by name through the layout manager. Fail fast if either is missing.",[84,85,90],"pre",{"className":86,"code":87,"language":88,"meta":89,"style":89},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\n\ndef open_layout(project_path: str, layout_name: str):\n    project = QgsProject.instance()\n    if not project.read(project_path):\n        raise RuntimeError(f\"Could not read project: {project_path}\")\n\n    layout = project.layoutManager().layoutByName(layout_name)\n    if layout is None:\n        available = [l.name() for l in project.layoutManager().layouts()]\n        raise ValueError(\n            f\"Layout '{layout_name}' not found. Available: {available}\"\n        )\n    return project, layout\n","python","",[51,91,92,111,118,123,148,160,172,206,211,222,239,262,273,302,308],{"__ignoreMap":89},[93,94,97,101,105,108],"span",{"class":95,"line":96},"line",1,[93,98,100],{"class":99},"snl16","from",[93,102,104],{"class":103},"s95oV"," qgis.core ",[93,106,107],{"class":99},"import",[93,109,110],{"class":103}," QgsProject\n",[93,112,114],{"class":95,"line":113},2,[93,115,117],{"emptyLinePlaceholder":116},true,"\n",[93,119,121],{"class":95,"line":120},3,[93,122,117],{"emptyLinePlaceholder":116},[93,124,126,129,133,136,140,143,145],{"class":95,"line":125},4,[93,127,128],{"class":99},"def",[93,130,132],{"class":131},"svObZ"," open_layout",[93,134,135],{"class":103},"(project_path: ",[93,137,139],{"class":138},"sDLfK","str",[93,141,142],{"class":103},", layout_name: ",[93,144,139],{"class":138},[93,146,147],{"class":103},"):\n",[93,149,151,154,157],{"class":95,"line":150},5,[93,152,153],{"class":103},"    project ",[93,155,156],{"class":99},"=",[93,158,159],{"class":103}," QgsProject.instance()\n",[93,161,163,166,169],{"class":95,"line":162},6,[93,164,165],{"class":99},"    if",[93,167,168],{"class":99}," not",[93,170,171],{"class":103}," project.read(project_path):\n",[93,173,175,178,181,184,187,191,194,197,200,203],{"class":95,"line":174},7,[93,176,177],{"class":99},"        raise",[93,179,180],{"class":138}," RuntimeError",[93,182,183],{"class":103},"(",[93,185,186],{"class":99},"f",[93,188,190],{"class":189},"sU2Wk","\"Could not read project: ",[93,192,193],{"class":138},"{",[93,195,196],{"class":103},"project_path",[93,198,199],{"class":138},"}",[93,201,202],{"class":189},"\"",[93,204,205],{"class":103},")\n",[93,207,209],{"class":95,"line":208},8,[93,210,117],{"emptyLinePlaceholder":116},[93,212,214,217,219],{"class":95,"line":213},9,[93,215,216],{"class":103},"    layout ",[93,218,156],{"class":99},[93,220,221],{"class":103}," project.layoutManager().layoutByName(layout_name)\n",[93,223,225,227,230,233,236],{"class":95,"line":224},10,[93,226,165],{"class":99},[93,228,229],{"class":103}," layout ",[93,231,232],{"class":99},"is",[93,234,235],{"class":138}," None",[93,237,238],{"class":103},":\n",[93,240,242,245,247,250,253,256,259],{"class":95,"line":241},11,[93,243,244],{"class":103},"        available ",[93,246,156],{"class":99},[93,248,249],{"class":103}," [l.name() ",[93,251,252],{"class":99},"for",[93,254,255],{"class":103}," l ",[93,257,258],{"class":99},"in",[93,260,261],{"class":103}," project.layoutManager().layouts()]\n",[93,263,265,267,270],{"class":95,"line":264},12,[93,266,177],{"class":99},[93,268,269],{"class":138}," ValueError",[93,271,272],{"class":103},"(\n",[93,274,276,279,282,284,287,289,292,294,297,299],{"class":95,"line":275},13,[93,277,278],{"class":99},"            f",[93,280,281],{"class":189},"\"Layout '",[93,283,193],{"class":138},[93,285,286],{"class":103},"layout_name",[93,288,199],{"class":138},[93,290,291],{"class":189},"' not found. Available: ",[93,293,193],{"class":138},[93,295,296],{"class":103},"available",[93,298,199],{"class":138},[93,300,301],{"class":189},"\"\n",[93,303,305],{"class":95,"line":304},14,[93,306,307],{"class":103},"        )\n",[93,309,311,314],{"class":95,"line":310},15,[93,312,313],{"class":99},"    return",[93,315,316],{"class":103}," project, layout\n",[14,318,319,322,323,326,327,329,330,332,333,336,337,340,341,344,345,348,349,352,353,356],{},[43,320,321],{},"Breakdown:"," ",[51,324,325],{},"project.read(project_path)"," loads the ",[51,328,53],{},"\u002F",[51,331,57],{}," into the singleton ",[51,334,335],{},"QgsProject.instance()"," and returns ",[51,338,339],{},"False"," on failure. ",[51,342,343],{},"layoutManager().layoutByName(...)"," returns the named print layout or ",[51,346,347],{},"None","; listing available names in the error makes a typo obvious immediately. Returning both ",[51,350,351],{},"project"," and ",[51,354,355],{},"layout"," lets later steps look up the coverage layer through the same project.",[32,358,360],{"id":359},"step-2-enable-the-atlas-and-set-coverage-filter","Step 2: Enable the Atlas and Set Coverage + Filter",[14,362,363,364,367],{},"Grab the layout's ",[51,365,366],{},"QgsLayoutAtlas",", enable it, bind the coverage layer, and apply an optional filter. Recompute the page list before checking the count.",[84,369,371],{"className":86,"code":370,"language":88,"meta":89,"style":89},"def configure_atlas(project, layout, coverage_name, filter_expr=None):\n    atlas = layout.atlas()\n    atlas.setEnabled(True)\n\n    coverage_layers = project.mapLayersByName(coverage_name)\n    if not coverage_layers:\n        raise ValueError(f\"Coverage layer '{coverage_name}' not in project\")\n    atlas.setCoverageLayer(coverage_layers[0])\n\n    if filter_expr:\n        atlas.setFilterFeatures(True)\n        atlas.setFilterExpression(filter_expr)\n\n    # Name pages so per-feature files sort and read cleanly\n    atlas.setPageNameExpression(\n        \"lpad(@atlas_featurenumber, 3, '0')\"\n    )\n\n    atlas.updateFeatures()\n    page_count = atlas.count()\n    if page_count == 0:\n        raise RuntimeError(\n            \"Atlas has 0 pages. Check the coverage layer and filter expression.\"\n        )\n    print(f\"Atlas ready: {page_count} pages\")\n    return atlas\n",[51,372,373,389,399,409,413,423,432,457,468,472,479,488,493,497,503,508,514,520,525,531,542,558,567,573,578,603],{"__ignoreMap":89},[93,374,375,377,380,383,385,387],{"class":95,"line":96},[93,376,128],{"class":99},[93,378,379],{"class":131}," configure_atlas",[93,381,382],{"class":103},"(project, layout, coverage_name, filter_expr",[93,384,156],{"class":99},[93,386,347],{"class":138},[93,388,147],{"class":103},[93,390,391,394,396],{"class":95,"line":113},[93,392,393],{"class":103},"    atlas ",[93,395,156],{"class":99},[93,397,398],{"class":103}," layout.atlas()\n",[93,400,401,404,407],{"class":95,"line":120},[93,402,403],{"class":103},"    atlas.setEnabled(",[93,405,406],{"class":138},"True",[93,408,205],{"class":103},[93,410,411],{"class":95,"line":125},[93,412,117],{"emptyLinePlaceholder":116},[93,414,415,418,420],{"class":95,"line":150},[93,416,417],{"class":103},"    coverage_layers ",[93,419,156],{"class":99},[93,421,422],{"class":103}," project.mapLayersByName(coverage_name)\n",[93,424,425,427,429],{"class":95,"line":162},[93,426,165],{"class":99},[93,428,168],{"class":99},[93,430,431],{"class":103}," coverage_layers:\n",[93,433,434,436,438,440,442,445,447,450,452,455],{"class":95,"line":174},[93,435,177],{"class":99},[93,437,269],{"class":138},[93,439,183],{"class":103},[93,441,186],{"class":99},[93,443,444],{"class":189},"\"Coverage layer '",[93,446,193],{"class":138},[93,448,449],{"class":103},"coverage_name",[93,451,199],{"class":138},[93,453,454],{"class":189},"' not in project\"",[93,456,205],{"class":103},[93,458,459,462,465],{"class":95,"line":208},[93,460,461],{"class":103},"    atlas.setCoverageLayer(coverage_layers[",[93,463,464],{"class":138},"0",[93,466,467],{"class":103},"])\n",[93,469,470],{"class":95,"line":213},[93,471,117],{"emptyLinePlaceholder":116},[93,473,474,476],{"class":95,"line":224},[93,475,165],{"class":99},[93,477,478],{"class":103}," filter_expr:\n",[93,480,481,484,486],{"class":95,"line":241},[93,482,483],{"class":103},"        atlas.setFilterFeatures(",[93,485,406],{"class":138},[93,487,205],{"class":103},[93,489,490],{"class":95,"line":264},[93,491,492],{"class":103},"        atlas.setFilterExpression(filter_expr)\n",[93,494,495],{"class":95,"line":275},[93,496,117],{"emptyLinePlaceholder":116},[93,498,499],{"class":95,"line":304},[93,500,502],{"class":501},"sAwPA","    # Name pages so per-feature files sort and read cleanly\n",[93,504,505],{"class":95,"line":310},[93,506,507],{"class":103},"    atlas.setPageNameExpression(\n",[93,509,511],{"class":95,"line":510},16,[93,512,513],{"class":189},"        \"lpad(@atlas_featurenumber, 3, '0')\"\n",[93,515,517],{"class":95,"line":516},17,[93,518,519],{"class":103},"    )\n",[93,521,523],{"class":95,"line":522},18,[93,524,117],{"emptyLinePlaceholder":116},[93,526,528],{"class":95,"line":527},19,[93,529,530],{"class":103},"    atlas.updateFeatures()\n",[93,532,534,537,539],{"class":95,"line":533},20,[93,535,536],{"class":103},"    page_count ",[93,538,156],{"class":99},[93,540,541],{"class":103}," atlas.count()\n",[93,543,545,547,550,553,556],{"class":95,"line":544},21,[93,546,165],{"class":99},[93,548,549],{"class":103}," page_count ",[93,551,552],{"class":99},"==",[93,554,555],{"class":138}," 0",[93,557,238],{"class":103},[93,559,561,563,565],{"class":95,"line":560},22,[93,562,177],{"class":99},[93,564,180],{"class":138},[93,566,272],{"class":103},[93,568,570],{"class":95,"line":569},23,[93,571,572],{"class":189},"            \"Atlas has 0 pages. Check the coverage layer and filter expression.\"\n",[93,574,576],{"class":95,"line":575},24,[93,577,307],{"class":103},[93,579,581,584,586,588,591,593,596,598,601],{"class":95,"line":580},25,[93,582,583],{"class":138},"    print",[93,585,183],{"class":103},[93,587,186],{"class":99},[93,589,590],{"class":189},"\"Atlas ready: ",[93,592,193],{"class":138},[93,594,595],{"class":103},"page_count",[93,597,199],{"class":138},[93,599,600],{"class":189}," pages\"",[93,602,205],{"class":103},[93,604,606,608],{"class":95,"line":605},26,[93,607,313],{"class":99},[93,609,610],{"class":103}," atlas\n",[14,612,613,322,615,618,619,622,623,626,627,630,631,634,635,638,639,642],{},[43,614,321],{},[51,616,617],{},"layout.atlas()"," returns the layout's single atlas; ",[51,620,621],{},"setEnabled(True)"," activates it. ",[51,624,625],{},"setCoverageLayer(...)"," binds the loaded layer whose features become pages. An optional ",[51,628,629],{},"filter_expr"," (any valid QGIS expression over coverage fields, e.g. ",[51,632,633],{},"\"region\" = 'North'",") trims the series. ",[51,636,637],{},"updateFeatures()"," is essential — it rebuilds the filtered page list so ",[51,640,641],{},"count()"," is accurate, and a zero-page result is caught before wasting time on an empty export.",[32,644,646],{"id":645},"step-3-export-every-page-to-pdf","Step 3: Export Every Page to PDF",[14,648,649,650,653],{},"Build a ",[51,651,652],{},"QgsLayoutExporter"," and call the atlas overload. Choose a single combined PDF or one PDF per feature.",[84,655,657],{"className":86,"code":656,"language":88,"meta":89,"style":89},"import os\nfrom qgis.core import QgsLayoutExporter\n\n\ndef export_atlas_pdf(layout, atlas, out_dir, base_name, per_feature=False):\n    os.makedirs(out_dir, exist_ok=True)\n    exporter = QgsLayoutExporter(layout)\n\n    settings = QgsLayoutExporter.PdfExportSettings()\n    settings.dpi = 300\n    settings.forceVectorOutput = True\n    settings.rasterizeWholeImage = False\n\n    if per_feature:\n        # One PDF per feature; filenames from pageNameExpression\n        result, error = exporter.exportToPdfs(\n            atlas, os.path.join(out_dir, base_name), settings\n        )\n    else:\n        # One combined multi-page PDF\n        result, error = exporter.exportToPdf(\n            atlas, os.path.join(out_dir, f\"{base_name}.pdf\"), settings\n        )\n\n    if result != QgsLayoutExporter.Success:\n        raise RuntimeError(f\"Atlas PDF export failed: {error}\")\n    print(f\"Export complete -> {out_dir}\")\n",[51,658,659,666,677,681,685,701,716,726,730,740,750,760,770,774,781,786,796,801,805,812,817,826,848,852,856,869,893],{"__ignoreMap":89},[93,660,661,663],{"class":95,"line":96},[93,662,107],{"class":99},[93,664,665],{"class":103}," os\n",[93,667,668,670,672,674],{"class":95,"line":113},[93,669,100],{"class":99},[93,671,104],{"class":103},[93,673,107],{"class":99},[93,675,676],{"class":103}," QgsLayoutExporter\n",[93,678,679],{"class":95,"line":120},[93,680,117],{"emptyLinePlaceholder":116},[93,682,683],{"class":95,"line":125},[93,684,117],{"emptyLinePlaceholder":116},[93,686,687,689,692,695,697,699],{"class":95,"line":150},[93,688,128],{"class":99},[93,690,691],{"class":131}," export_atlas_pdf",[93,693,694],{"class":103},"(layout, atlas, out_dir, base_name, per_feature",[93,696,156],{"class":99},[93,698,339],{"class":138},[93,700,147],{"class":103},[93,702,703,706,710,712,714],{"class":95,"line":162},[93,704,705],{"class":103},"    os.makedirs(out_dir, ",[93,707,709],{"class":708},"s9osk","exist_ok",[93,711,156],{"class":99},[93,713,406],{"class":138},[93,715,205],{"class":103},[93,717,718,721,723],{"class":95,"line":174},[93,719,720],{"class":103},"    exporter ",[93,722,156],{"class":99},[93,724,725],{"class":103}," QgsLayoutExporter(layout)\n",[93,727,728],{"class":95,"line":208},[93,729,117],{"emptyLinePlaceholder":116},[93,731,732,735,737],{"class":95,"line":213},[93,733,734],{"class":103},"    settings ",[93,736,156],{"class":99},[93,738,739],{"class":103}," QgsLayoutExporter.PdfExportSettings()\n",[93,741,742,745,747],{"class":95,"line":224},[93,743,744],{"class":103},"    settings.dpi ",[93,746,156],{"class":99},[93,748,749],{"class":138}," 300\n",[93,751,752,755,757],{"class":95,"line":241},[93,753,754],{"class":103},"    settings.forceVectorOutput ",[93,756,156],{"class":99},[93,758,759],{"class":138}," True\n",[93,761,762,765,767],{"class":95,"line":264},[93,763,764],{"class":103},"    settings.rasterizeWholeImage ",[93,766,156],{"class":99},[93,768,769],{"class":138}," False\n",[93,771,772],{"class":95,"line":275},[93,773,117],{"emptyLinePlaceholder":116},[93,775,776,778],{"class":95,"line":304},[93,777,165],{"class":99},[93,779,780],{"class":103}," per_feature:\n",[93,782,783],{"class":95,"line":310},[93,784,785],{"class":501},"        # One PDF per feature; filenames from pageNameExpression\n",[93,787,788,791,793],{"class":95,"line":510},[93,789,790],{"class":103},"        result, error ",[93,792,156],{"class":99},[93,794,795],{"class":103}," exporter.exportToPdfs(\n",[93,797,798],{"class":95,"line":516},[93,799,800],{"class":103},"            atlas, os.path.join(out_dir, base_name), settings\n",[93,802,803],{"class":95,"line":522},[93,804,307],{"class":103},[93,806,807,810],{"class":95,"line":527},[93,808,809],{"class":99},"    else",[93,811,238],{"class":103},[93,813,814],{"class":95,"line":533},[93,815,816],{"class":501},"        # One combined multi-page PDF\n",[93,818,819,821,823],{"class":95,"line":544},[93,820,790],{"class":103},[93,822,156],{"class":99},[93,824,825],{"class":103}," exporter.exportToPdf(\n",[93,827,828,831,833,835,837,840,842,845],{"class":95,"line":560},[93,829,830],{"class":103},"            atlas, os.path.join(out_dir, ",[93,832,186],{"class":99},[93,834,202],{"class":189},[93,836,193],{"class":138},[93,838,839],{"class":103},"base_name",[93,841,199],{"class":138},[93,843,844],{"class":189},".pdf\"",[93,846,847],{"class":103},"), settings\n",[93,849,850],{"class":95,"line":569},[93,851,307],{"class":103},[93,853,854],{"class":95,"line":575},[93,855,117],{"emptyLinePlaceholder":116},[93,857,858,860,863,866],{"class":95,"line":580},[93,859,165],{"class":99},[93,861,862],{"class":103}," result ",[93,864,865],{"class":99},"!=",[93,867,868],{"class":103}," QgsLayoutExporter.Success:\n",[93,870,871,873,875,877,879,882,884,887,889,891],{"class":95,"line":605},[93,872,177],{"class":99},[93,874,180],{"class":138},[93,876,183],{"class":103},[93,878,186],{"class":99},[93,880,881],{"class":189},"\"Atlas PDF export failed: ",[93,883,193],{"class":138},[93,885,886],{"class":103},"error",[93,888,199],{"class":138},[93,890,202],{"class":189},[93,892,205],{"class":103},[93,894,896,898,900,902,905,907,910,912,914],{"class":95,"line":895},27,[93,897,583],{"class":138},[93,899,183],{"class":103},[93,901,186],{"class":99},[93,903,904],{"class":189},"\"Export complete -> ",[93,906,193],{"class":138},[93,908,909],{"class":103},"out_dir",[93,911,199],{"class":138},[93,913,202],{"class":189},[93,915,205],{"class":103},[14,917,918,920,921,924,925,928,929,932,933,936,937,940,941,944,945,948,949,952],{},[43,919,321],{}," Passing ",[51,922,923],{},"atlas"," as the first argument is what makes the exporter iterate the series rather than render one page. ",[51,926,927],{},"exportToPdf(atlas, path, settings)"," produces one combined document; ",[51,930,931],{},"exportToPdfs(atlas, base_path, settings)"," writes a separate file per feature, named from the ",[51,934,935],{},"pageNameExpression"," set in step two. The atlas overloads return a ",[51,938,939],{},"(result, error)"," tuple — note this differs from the single-layout ",[51,942,943],{},"exportToPdf",", which returns only a status code. ",[51,946,947],{},"forceVectorOutput = True"," keeps text and lines crisp; ",[51,950,951],{},"rasterizeWholeImage = False"," avoids flattening the whole page to a bitmap.",[32,954,956],{"id":955},"step-4-run-it-all-together","Step 4: Run It All Together",[84,958,960],{"className":86,"code":959,"language":88,"meta":89,"style":89},"def main():\n    project, layout = open_layout(\n        \"\u002Fdata\u002Fprojects\u002Fdistricts.qgz\",\n        layout_name=\"DistrictMaps\",\n    )\n    atlas = configure_atlas(\n        project, layout,\n        coverage_name=\"districts\",\n        filter_expr='\"status\" = \\'active\\'',\n    )\n    export_atlas_pdf(\n        layout, atlas,\n        out_dir=\"\u002Fdata\u002Fatlas_out\",\n        base_name=\"district_atlas\",\n        per_feature=False,   # True for one PDF per district\n    )\n\n\n# In the QGIS Python Console:\n# main()\n",[51,961,962,972,982,990,1002,1006,1015,1020,1032,1055,1059,1064,1069,1081,1093,1108,1112,1116,1120,1125],{"__ignoreMap":89},[93,963,964,966,969],{"class":95,"line":96},[93,965,128],{"class":99},[93,967,968],{"class":131}," main",[93,970,971],{"class":103},"():\n",[93,973,974,977,979],{"class":95,"line":113},[93,975,976],{"class":103},"    project, layout ",[93,978,156],{"class":99},[93,980,981],{"class":103}," open_layout(\n",[93,983,984,987],{"class":95,"line":120},[93,985,986],{"class":189},"        \"\u002Fdata\u002Fprojects\u002Fdistricts.qgz\"",[93,988,989],{"class":103},",\n",[93,991,992,995,997,1000],{"class":95,"line":125},[93,993,994],{"class":708},"        layout_name",[93,996,156],{"class":99},[93,998,999],{"class":189},"\"DistrictMaps\"",[93,1001,989],{"class":103},[93,1003,1004],{"class":95,"line":150},[93,1005,519],{"class":103},[93,1007,1008,1010,1012],{"class":95,"line":162},[93,1009,393],{"class":103},[93,1011,156],{"class":99},[93,1013,1014],{"class":103}," configure_atlas(\n",[93,1016,1017],{"class":95,"line":174},[93,1018,1019],{"class":103},"        project, layout,\n",[93,1021,1022,1025,1027,1030],{"class":95,"line":208},[93,1023,1024],{"class":708},"        coverage_name",[93,1026,156],{"class":99},[93,1028,1029],{"class":189},"\"districts\"",[93,1031,989],{"class":103},[93,1033,1034,1037,1039,1042,1045,1048,1050,1053],{"class":95,"line":213},[93,1035,1036],{"class":708},"        filter_expr",[93,1038,156],{"class":99},[93,1040,1041],{"class":189},"'\"status\" = ",[93,1043,1044],{"class":138},"\\'",[93,1046,1047],{"class":189},"active",[93,1049,1044],{"class":138},[93,1051,1052],{"class":189},"'",[93,1054,989],{"class":103},[93,1056,1057],{"class":95,"line":224},[93,1058,519],{"class":103},[93,1060,1061],{"class":95,"line":241},[93,1062,1063],{"class":103},"    export_atlas_pdf(\n",[93,1065,1066],{"class":95,"line":264},[93,1067,1068],{"class":103},"        layout, atlas,\n",[93,1070,1071,1074,1076,1079],{"class":95,"line":275},[93,1072,1073],{"class":708},"        out_dir",[93,1075,156],{"class":99},[93,1077,1078],{"class":189},"\"\u002Fdata\u002Fatlas_out\"",[93,1080,989],{"class":103},[93,1082,1083,1086,1088,1091],{"class":95,"line":304},[93,1084,1085],{"class":708},"        base_name",[93,1087,156],{"class":99},[93,1089,1090],{"class":189},"\"district_atlas\"",[93,1092,989],{"class":103},[93,1094,1095,1098,1100,1102,1105],{"class":95,"line":310},[93,1096,1097],{"class":708},"        per_feature",[93,1099,156],{"class":99},[93,1101,339],{"class":138},[93,1103,1104],{"class":103},",   ",[93,1106,1107],{"class":501},"# True for one PDF per district\n",[93,1109,1110],{"class":95,"line":510},[93,1111,519],{"class":103},[93,1113,1114],{"class":95,"line":516},[93,1115,117],{"emptyLinePlaceholder":116},[93,1117,1118],{"class":95,"line":522},[93,1119,117],{"emptyLinePlaceholder":116},[93,1121,1122],{"class":95,"line":527},[93,1123,1124],{"class":501},"# In the QGIS Python Console:\n",[93,1126,1127],{"class":95,"line":533},[93,1128,1129],{"class":501},"# main()\n",[14,1131,1132,1134,1135,1138,1139,352,1141,1143],{},[43,1133,321],{}," The three functions compose into a linear flow: open, configure, export. Switching ",[51,1136,1137],{},"per_feature"," between ",[51,1140,339],{},[51,1142,406],{}," is the only change needed to toggle between a single book and individual sheets. Because each function raises on its own failure mode, a problem (missing project, missing coverage layer, empty filter, export error) is reported at the exact stage it occurs.",[32,1145,1147],{"id":1146},"running-headless-standalone-script","Running Headless (Standalone Script)",[14,1149,1150,1151,1154],{},"For cron jobs, containers, or CI, bootstrap QGIS before any of the above runs. No GUI or ",[51,1152,1153],{},"iface"," is required for atlas export.",[84,1156,1158],{"className":86,"code":1157,"language":88,"meta":89,"style":89},"from qgis.core import QgsApplication\n\nqgs = QgsApplication([], False)\nqgs.setPrefixPath(\"\u002Fusr\", True)   # adjust to your install prefix\nqgs.initQgis()\n\n# main()  # call the workflow defined above\n\nqgs.exitQgis()\n",[51,1159,1160,1171,1175,1189,1208,1213,1217,1222,1226],{"__ignoreMap":89},[93,1161,1162,1164,1166,1168],{"class":95,"line":96},[93,1163,100],{"class":99},[93,1165,104],{"class":103},[93,1167,107],{"class":99},[93,1169,1170],{"class":103}," QgsApplication\n",[93,1172,1173],{"class":95,"line":113},[93,1174,117],{"emptyLinePlaceholder":116},[93,1176,1177,1180,1182,1185,1187],{"class":95,"line":120},[93,1178,1179],{"class":103},"qgs ",[93,1181,156],{"class":99},[93,1183,1184],{"class":103}," QgsApplication([], ",[93,1186,339],{"class":138},[93,1188,205],{"class":103},[93,1190,1191,1194,1197,1200,1202,1205],{"class":95,"line":125},[93,1192,1193],{"class":103},"qgs.setPrefixPath(",[93,1195,1196],{"class":189},"\"\u002Fusr\"",[93,1198,1199],{"class":103},", ",[93,1201,406],{"class":138},[93,1203,1204],{"class":103},")   ",[93,1206,1207],{"class":501},"# adjust to your install prefix\n",[93,1209,1210],{"class":95,"line":150},[93,1211,1212],{"class":103},"qgs.initQgis()\n",[93,1214,1215],{"class":95,"line":162},[93,1216,117],{"emptyLinePlaceholder":116},[93,1218,1219],{"class":95,"line":174},[93,1220,1221],{"class":501},"# main()  # call the workflow defined above\n",[93,1223,1224],{"class":95,"line":208},[93,1225,117],{"emptyLinePlaceholder":116},[93,1227,1228],{"class":95,"line":213},[93,1229,1230],{"class":103},"qgs.exitQgis()\n",[14,1232,1233,322,1235,1238,1239,1242,1243,1246,1247,1249,1250,1253,1254,1258],{},[43,1234,321],{},[51,1236,1237],{},"QgsApplication([], False)"," starts QGIS with no GUI; ",[51,1240,1241],{},"setPrefixPath"," points it at the install so providers and data paths resolve. ",[51,1244,1245],{},"initQgis()"," must run before reading projects or exporting. Atlas export depends only on the layout and project, not on a map canvas, so it works fully headless. Always pair ",[51,1248,1245],{}," with ",[51,1251,1252],{},"exitQgis()"," to flush and release resources — important for long-running batch servers. This is the same headless pattern used across ",[18,1255,1257],{"href":1256},"\u002Fspatial-data-processing-automation\u002F","Spatial Data Processing & Automation with PyQGIS"," pipelines.",[32,1260,1262],{"id":1261},"optional-per-feature-filenames-and-tuning-dpi","Optional: Per-Feature Filenames and Tuning DPI",[14,1264,1265,1266,1268],{},"When you export one PDF per feature, the filenames come straight from the atlas ",[51,1267,935],{},". Make it produce a readable, sortable, filesystem-safe string and you get a clean folder of named sheets with no extra code.",[84,1270,1272],{"className":86,"code":1271,"language":88,"meta":89,"style":89},"# A descriptive, sortable, safe page name -> \"012_riverside_north\"\natlas.setPageNameExpression(\n    \"lpad(@atlas_featurenumber, 3, '0') || '_' || \"\n    \"lower(replace(\\\"district_name\\\", ' ', '_'))\"\n)\n",[51,1273,1274,1279,1284,1289,1305],{"__ignoreMap":89},[93,1275,1276],{"class":95,"line":96},[93,1277,1278],{"class":501},"# A descriptive, sortable, safe page name -> \"012_riverside_north\"\n",[93,1280,1281],{"class":95,"line":113},[93,1282,1283],{"class":103},"atlas.setPageNameExpression(\n",[93,1285,1286],{"class":95,"line":120},[93,1287,1288],{"class":189},"    \"lpad(@atlas_featurenumber, 3, '0') || '_' || \"\n",[93,1290,1291,1294,1297,1300,1302],{"class":95,"line":125},[93,1292,1293],{"class":189},"    \"lower(replace(",[93,1295,1296],{"class":138},"\\\"",[93,1298,1299],{"class":189},"district_name",[93,1301,1296],{"class":138},[93,1303,1304],{"class":189},", ' ', '_'))\"\n",[93,1306,1307],{"class":95,"line":150},[93,1308,205],{"class":103},[14,1310,1311,322,1313,1316,1317,1320,1321,1324,1325,1328],{},[43,1312,321],{},[51,1314,1315],{},"lpad(@atlas_featurenumber, 3, '0')"," zero-pads the page index so ",[51,1318,1319],{},"012"," sorts before ",[51,1322,1323],{},"100","; concatenating the lower-cased, space-stripped district name yields a filename that is both human-readable and safe across operating systems. Because ",[51,1326,1327],{},"exportToPdfs"," derives each filename from this expression, controlling the expression is the only thing needed to control output naming.",[14,1330,1331,1332,1335,1336,1338],{},"For tuning quality versus size: ",[51,1333,1334],{},"settings.dpi"," drives raster resolution (300 for print, 150 for quick previews), ",[51,1337,947],{}," keeps vector layers sharp regardless of DPI, and for very large series consider exporting per-feature to cap individual file size and memory pressure rather than one giant document.",[32,1340,1342],{"id":1341},"qgis-version-compatibility","QGIS Version Compatibility",[14,1344,1345,1346,1349],{},"Baseline: ",[43,1347,1348],{},"QGIS 3.34 LTR (Python 3.12)",". The atlas export API is stable across the current LTR and latest lines.",[1351,1352,1353,1366],"table",{},[1354,1355,1356],"thead",{},[1357,1358,1359,1363],"tr",{},[1360,1361,1362],"th",{},"QGIS \u002F Python",[1360,1364,1365],{},"Notes",[1367,1368,1369,1387,1397],"tbody",{},[1357,1370,1371,1375],{},[1372,1373,1374],"td",{},"3.28 LTR \u002F Py 3.9",[1372,1376,1377,1378,1380,1381,1383,1384,1386],{},"Atlas overloads of ",[51,1379,652],{}," available and return ",[51,1382,939],{},". ",[51,1385,1327],{}," present.",[1357,1388,1389,1394],{},[1372,1390,1391],{},[43,1392,1393],{},"3.34 LTR \u002F Py 3.12 (baseline)",[1372,1395,1396],{},"Recommended. Behaviour as documented here.",[1357,1398,1399,1402],{},[1372,1400,1401],{},"3.40 \u002F 3.44 \u002F Py 3.12",[1372,1403,1404,1405,1408],{},"Same API. Some ",[51,1406,1407],{},"PdfExportSettings"," fields added; existing fields unchanged.",[14,1410,1411,1412,322,1414,1417,1418,322,1421,1424,1425,1428],{},"The single biggest cross-version gotcha is the return signature: the ",[43,1413,923],{},[51,1415,1416],{},"exportToPdf(atlas, ...)"," returns a tuple, while the ",[43,1419,1420],{},"single-layout",[51,1422,1423],{},"exportToPdf(path, ...)"," returns one status code. Unpack accordingly or you will hit a ",[51,1426,1427],{},"cannot unpack"," \u002F comparison error.",[32,1430,1432],{"id":1431},"troubleshooting","Troubleshooting",[14,1434,1435,1441,1442,1444,1445,1448,1449,1440],{},[43,1436,1437,1440],{},[51,1438,1439],{},"Atlas has 0 pages","."," The filter excluded everything, the coverage layer is empty, or you set the filter without calling ",[51,1443,637],{},". Verify the expression against the coverage layer's fields and confirm ",[51,1446,1447],{},"atlas.count()"," after ",[51,1450,637],{},[14,1452,1453,1459,1460,1462,1463,1465],{},[43,1454,1455,1458],{},[51,1456,1457],{},"cannot unpack non-iterable"," or a comparison failure on the export result."," You used the single-layout call signature. With an atlas, ",[51,1461,1416],{}," returns ",[51,1464,939],{}," — unpack two values.",[14,1467,1468,1471],{},[43,1469,1470],{},"The PDF renders but the map frame does not move per page."," The map item is not atlas-controlled. In the layout, enable \"Controlled by atlas\" on the map item (or set it in code), otherwise every page shows the same extent.",[14,1473,1474,1477],{},[43,1475,1476],{},"Blank pages or missing layers."," A data source path in the project is broken, or layers are toggled off. Re-open the project in QGIS, fix broken sources, and confirm the relevant layers are visible before scripting the export.",[14,1479,1480,1483],{},[43,1481,1482],{},"Export fails with a file error on Windows."," The target PDF is open in a viewer, or antivirus\u002Fcloud-sync locks the folder. Export to a local temp directory, then move the files.",[32,1485,1487],{"id":1486},"conclusion","Conclusion",[14,1489,1490,1491,1494,1495,1497,1498,1501],{},"Generating an atlas PDF from PyQGIS is a tight three-step flow: load the project and layout, enable and configure the atlas (coverage, filter, page names, ",[51,1492,1493],{},"updateFeatures","), then export with the atlas-aware ",[51,1496,652],{}," overloads. Toggle one flag to switch between a combined book and per-feature sheets, and add a ",[51,1499,1500],{},"QgsApplication"," bootstrap to run the whole thing unattended. With the export driven by code, your map book regenerates automatically whenever the coverage data changes — no dialogs, no manual paging.",[32,1503,1505],{"id":1504},"frequently-asked-questions","Frequently Asked Questions",[14,1507,1508,1511,1512,1515,1516,1518,1519,1521],{},[43,1509,1510],{},"How do I produce one PDF per feature instead of a single document?","\nCall ",[51,1513,1514],{},"exporter.exportToPdfs(atlas, base_path, settings)"," instead of ",[51,1517,943],{},". Filenames come from the atlas ",[51,1520,935],{},", so set that to something unique per feature.",[14,1523,1524,1530,1531,352,1533,1535],{},[43,1525,1526,1527,1529],{},"Do I need the QGIS GUI or ",[51,1528,1153],{}," to export an atlas?","\nNo. Atlas export depends only on the project and layout. Bootstrap with ",[51,1532,1237],{},[51,1534,1245],{},", and it runs fully headless.",[14,1537,1538,1541,1542,329,1544,329,1546,1549,1550,1552,1553,1440],{},[43,1539,1540],{},"Why is the export result not a simple success code?","\nThe atlas overloads of ",[51,1543,943],{},[51,1545,1327],{},[51,1547,1548],{},"exportToImage"," return a ",[51,1551,939],{}," tuple. Unpack both, then compare ",[51,1554,1555],{},"result == QgsLayoutExporter.Success",[14,1557,1558,1561,1562,352,1565,1568,1569,1572,1573,1440],{},[43,1559,1560],{},"Can I filter which features become pages?","\nYes. Call ",[51,1563,1564],{},"atlas.setFilterFeatures(True)",[51,1566,1567],{},"atlas.setFilterExpression(expr)"," with any valid QGIS expression over the coverage layer's fields, then ",[51,1570,1571],{},"atlas.updateFeatures()"," and check ",[51,1574,1447],{},[32,1576,1578],{"id":1577},"related","Related",[37,1580,1581,1585,1590],{},[40,1582,1583],{},[18,1584,21],{"href":20},[40,1586,1587],{},[18,1588,1589],{"href":28},"Exporting QGIS Layouts to PDF with PyQGIS",[40,1591,1592],{},[18,1593,1595],{"href":1594},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002F","Create a Choropleth Map in PyQGIS",[1597,1598,1599],"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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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 .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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":89,"searchDepth":113,"depth":113,"links":1601},[1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613],{"id":34,"depth":113,"text":35},{"id":78,"depth":113,"text":79},{"id":359,"depth":113,"text":360},{"id":645,"depth":113,"text":646},{"id":955,"depth":113,"text":956},{"id":1146,"depth":113,"text":1147},{"id":1261,"depth":113,"text":1262},{"id":1341,"depth":113,"text":1342},{"id":1431,"depth":113,"text":1432},{"id":1486,"depth":113,"text":1487},{"id":1504,"depth":113,"text":1505},{"id":1577,"depth":113,"text":1578},"Generate an atlas PDF in PyQGIS by loading a project and layout, enabling the atlas, setting the coverage layer and filter, and exporting all pages.","md",{},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fgenerate-atlas-pdf-pyqgis",{"title":5,"description":1614},"spatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fgenerate-atlas-pdf-pyqgis\u002Findex","I9bhWuEEtDii4uDgRy4SNXj1HBhfY5wqbD0mz6pp7sc",1781781223073]