[{"data":1,"prerenderedAt":2990},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fadd-headers-footers-and-page-numbers-to-an-excel-pdf":3,"surround:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fadd-headers-footers-and-page-numbers-to-an-excel-pdf":2983},{"id":4,"title":5,"body":6,"dateModified":80,"datePublished":80,"description":2960,"extension":2961,"faq":2962,"meta":2974,"navigation":242,"path":2975,"seo":2976,"slug":2979,"stem":2980,"type":2981,"__hash__":2982},"docs\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fadd-headers-footers-and-page-numbers-to-an-excel-pdf\u002Findex.md","Add Headers, Footers and Page Numbers to an Excel PDF",{"type":7,"value":8,"toc":2947},"minimark",[9,19,155,160,189,197,211,214,519,523,526,685,695,783,789,792,851,871,876,916,920,923,954,961,964,1022,1026,1029,1136,1321,1331,1353,1357,1436,1439,1666,1684,1687,1758,1762,1765,2173,2176,2389,2392,2396,2525,2529,2532,2539,2549,2799,2810,2817,2821,2836,2840,2863,2879,2891,2897,2903,2907,2943],[10,11,12,13,18],"p",{},"A workbook exported to PDF straight from the default settings looks like a spreadsheet screenshot: no title, no page numbers, and the column headers on page one only, so page four is a grid of unlabelled numbers. Every one of those is a page-setup property that openpyxl can write, and LibreOffice honours all of them during conversion. This guide sets up the page properly before exporting. It extends ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002F","Exporting Excel Reports to PDF",".",[20,21,30,31,30,35,30,39,30,46,30,55,30,64,30,71,30,76,30,81,30,88,30,93,30,97,30,100,30,103,30,106,30,109,30,115,30,119,30,123,30,128,30,133,30,138,30,142,30,146,30,151],"svg",{"viewBox":22,"role":23,"ariaLabel":24,"ariaLabelledBy":25,"xmlns":28,"style":29},"0 0 800 268","img","A PDF page with a three-section header carrying the report name and date, repeated column headings, the data table, and a footer with a page number.",[26,27],"page-t","page-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[32,33,34],"title",{"id":26},"The anatomy of a well-set-up printed page",[36,37,38],"desc",{"id":27},"A single PDF page. Across the top sits a three-section header: the report name on the left, the period in the centre, and the generation date on the right. Below it the column headings repeat, because print_title_rows was set. The data table fills the body. At the foot a three-section footer carries a confidentiality note on the left and a page number of total on the right. Everything except the table comes from worksheet page-setup properties rather than cells.",[40,41],"rect",{"x":42,"y":42,"width":43,"height":44,"fill":45},"0","800","268","#ffffff",[40,47],{"x":48,"y":49,"width":50,"height":51,"rx":52,"fill":45,"stroke":53,"style":54},"150","20","500","232","6","var(--line,#cdd5e6)","stroke-width:2px",[40,56],{"x":57,"y":58,"width":59,"height":60,"rx":61,"fill":62,"stroke":63,"style":54},"166","34","468","28","4","#ebebfd","var(--brand,#5b5cf0)",[65,66,70],"text",{"x":67,"y":68,"style":69},"180","52","font-size:9.5px;font-weight:700;fill:var(--brand-strong,#4338ca)","Regional Revenue",[65,72,75],{"x":73,"y":68,"style":74},"400","font-size:9.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","August 2026",[65,77,80],{"x":78,"y":68,"style":79},"620","font-size:9.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:end","2026-08-15",[40,82],{"x":57,"y":83,"width":59,"height":84,"rx":85,"fill":86,"stroke":87,"style":54},"72","22","3","#d9f4f1","var(--teal,#0f9488)",[65,89,92],{"x":73,"y":90,"style":91},"88","font-size:9.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","region · units · revenue · target · variance",[40,94],{"x":57,"y":95,"width":59,"height":96,"rx":85,"fill":45,"stroke":53},"98","18",[40,98],{"x":57,"y":99,"width":59,"height":96,"rx":85,"fill":45,"stroke":53},"120",[40,101],{"x":57,"y":102,"width":59,"height":96,"rx":85,"fill":45,"stroke":53},"142",[40,104],{"x":57,"y":105,"width":59,"height":96,"rx":85,"fill":45,"stroke":53},"164",[40,107],{"x":57,"y":108,"width":59,"height":96,"rx":85,"fill":45,"stroke":53},"186",[40,110],{"x":57,"y":111,"width":59,"height":112,"rx":61,"fill":113,"stroke":114,"style":54},"216","24","#fdefd8","var(--gold,#b4740a)",[65,116,118],{"x":67,"y":51,"style":117},"font-size:9px;fill:var(--gold-ink,#7a4e06)","Confidential — internal",[65,120,122],{"x":78,"y":51,"style":121},"font-size:9px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:end","Page 2 of 7",[65,124,127],{"x":125,"y":68,"style":126},"74","font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","oddHeader",[65,129,132],{"x":125,"y":130,"style":131},"70","font-size:9px;fill:var(--muted,#5b6780);text-anchor:middle","left · centre · right",[65,134,137],{"x":125,"y":135,"style":136},"90","font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","print_title_rows",[65,139,141],{"x":125,"y":140,"style":131},"108","repeats on",[65,143,145],{"x":125,"y":144,"style":131},"122","every page",[65,147,150],{"x":148,"y":51,"style":149},"726","font-size:10.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","oddFooter",[65,152,154],{"x":148,"y":153,"style":131},"250","&P of &N",[156,157,159],"h2",{"id":158},"prerequisites","Prerequisites",[161,162,167],"pre",{"className":163,"code":164,"language":165,"meta":166,"style":166},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[168,169,170],"code",{"__ignoreMap":166},[171,172,175,179,183,186],"span",{"class":173,"line":174},"line",1,[171,176,178],{"class":177},"sMTad","pip",[171,180,182],{"class":181},"srMev"," install",[171,184,185],{"class":181}," pandas",[171,187,188],{"class":181}," openpyxl\n",[10,190,191,192,196],{},"Plus LibreOffice installed as a program for the conversion, as in ",[14,193,195],{"href":194},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python\u002F","converting an Excel file to PDF",":",[161,198,200],{"className":163,"code":199,"language":165,"meta":166,"style":166},"soffice --version\n",[168,201,202],{"__ignoreMap":166},[171,203,204,207],{"class":173,"line":174},[171,205,206],{"class":177},"soffice",[171,208,210],{"class":209},"sP0c6"," --version\n",[10,212,213],{},"A report long enough to span pages:",[161,215,219],{"className":216,"code":217,"language":218,"meta":166,"style":166},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nrows = []\nfor region in (\"North\", \"South\", \"West\", \"East\"):\n    for branch in range(1, 26):\n        rows.append({\n            \"region\": region,\n            \"branch\": f\"{region} branch {branch}\",\n            \"units\": 100 + branch,\n            \"revenue\": (100 + branch) * 12.5,\n            \"target\": (100 + branch) * 11.8,\n        })\n\nreport = pd.DataFrame(rows)\nreport[\"variance\"] = report[\"revenue\"] \u002F report[\"target\"] - 1\nreport.to_excel(\"report.xlsx\", index=False)\n","python",[168,220,221,237,244,256,293,320,326,335,374,391,415,436,442,447,458,496],{"__ignoreMap":166},[171,222,223,227,231,234],{"class":173,"line":174},[171,224,226],{"class":225},"s-kum","import",[171,228,230],{"class":229},"skGVy"," pandas ",[171,232,233],{"class":225},"as",[171,235,236],{"class":229}," pd\n",[171,238,240],{"class":173,"line":239},2,[171,241,243],{"emptyLinePlaceholder":242},true,"\n",[171,245,247,250,253],{"class":173,"line":246},3,[171,248,249],{"class":229},"rows ",[171,251,252],{"class":225},"=",[171,254,255],{"class":229}," []\n",[171,257,259,262,265,268,271,274,277,280,282,285,287,290],{"class":173,"line":258},4,[171,260,261],{"class":225},"for",[171,263,264],{"class":229}," region ",[171,266,267],{"class":225},"in",[171,269,270],{"class":229}," (",[171,272,273],{"class":181},"\"North\"",[171,275,276],{"class":229},", ",[171,278,279],{"class":181},"\"South\"",[171,281,276],{"class":229},[171,283,284],{"class":181},"\"West\"",[171,286,276],{"class":229},[171,288,289],{"class":181},"\"East\"",[171,291,292],{"class":229},"):\n",[171,294,296,299,302,304,307,310,313,315,318],{"class":173,"line":295},5,[171,297,298],{"class":225},"    for",[171,300,301],{"class":229}," branch ",[171,303,267],{"class":225},[171,305,306],{"class":209}," range",[171,308,309],{"class":229},"(",[171,311,312],{"class":209},"1",[171,314,276],{"class":229},[171,316,317],{"class":209},"26",[171,319,292],{"class":229},[171,321,323],{"class":173,"line":322},6,[171,324,325],{"class":229},"        rows.append({\n",[171,327,329,332],{"class":173,"line":328},7,[171,330,331],{"class":181},"            \"region\"",[171,333,334],{"class":229},": region,\n",[171,336,338,341,344,347,350,354,357,360,362,364,367,369,371],{"class":173,"line":337},8,[171,339,340],{"class":181},"            \"branch\"",[171,342,343],{"class":229},": ",[171,345,346],{"class":225},"f",[171,348,349],{"class":181},"\"",[171,351,353],{"class":352},"sSjpA","{",[171,355,356],{"class":229},"region",[171,358,359],{"class":352},"}",[171,361,301],{"class":181},[171,363,353],{"class":352},[171,365,366],{"class":229},"branch",[171,368,359],{"class":352},[171,370,349],{"class":181},[171,372,373],{"class":229},",\n",[171,375,377,380,382,385,388],{"class":173,"line":376},9,[171,378,379],{"class":181},"            \"units\"",[171,381,343],{"class":229},[171,383,384],{"class":209},"100",[171,386,387],{"class":225}," +",[171,389,390],{"class":229}," branch,\n",[171,392,394,397,400,402,404,407,410,413],{"class":173,"line":393},10,[171,395,396],{"class":181},"            \"revenue\"",[171,398,399],{"class":229},": (",[171,401,384],{"class":209},[171,403,387],{"class":225},[171,405,406],{"class":229}," branch) ",[171,408,409],{"class":225},"*",[171,411,412],{"class":209}," 12.5",[171,414,373],{"class":229},[171,416,418,421,423,425,427,429,431,434],{"class":173,"line":417},11,[171,419,420],{"class":181},"            \"target\"",[171,422,399],{"class":229},[171,424,384],{"class":209},[171,426,387],{"class":225},[171,428,406],{"class":229},[171,430,409],{"class":225},[171,432,433],{"class":209}," 11.8",[171,435,373],{"class":229},[171,437,439],{"class":173,"line":438},12,[171,440,441],{"class":229},"        })\n",[171,443,445],{"class":173,"line":444},13,[171,446,243],{"emptyLinePlaceholder":242},[171,448,450,453,455],{"class":173,"line":449},14,[171,451,452],{"class":229},"report ",[171,454,252],{"class":225},[171,456,457],{"class":229}," pd.DataFrame(rows)\n",[171,459,461,464,467,470,472,475,478,480,483,485,488,490,493],{"class":173,"line":460},15,[171,462,463],{"class":229},"report[",[171,465,466],{"class":181},"\"variance\"",[171,468,469],{"class":229},"] ",[171,471,252],{"class":225},[171,473,474],{"class":229}," report[",[171,476,477],{"class":181},"\"revenue\"",[171,479,469],{"class":229},[171,481,482],{"class":225},"\u002F",[171,484,474],{"class":229},[171,486,487],{"class":181},"\"target\"",[171,489,469],{"class":229},[171,491,492],{"class":225},"-",[171,494,495],{"class":209}," 1\n",[171,497,499,502,505,507,511,513,516],{"class":173,"line":498},16,[171,500,501],{"class":229},"report.to_excel(",[171,503,504],{"class":181},"\"report.xlsx\"",[171,506,276],{"class":229},[171,508,510],{"class":509},"sa561","index",[171,512,252],{"class":225},[171,514,515],{"class":209},"False",[171,517,518],{"class":229},")\n",[156,520,522],{"id":521},"step-1-set-the-header-and-footer","Step 1 — Set the header and footer",[10,524,525],{},"Headers and footers live on the worksheet's page setup, in three sections each — left, centre and right:",[161,527,529],{"className":216,"code":528,"language":218,"meta":166,"style":166},"from datetime import date\nfrom openpyxl import load_workbook\n\nwb = load_workbook(\"report.xlsx\")\nws = wb.active\n\nws.oddHeader.left.text = \"Regional Revenue\"\nws.oddHeader.center.text = \"August 2026\"\nws.oddHeader.right.text = f\"{date.today():%Y-%m-%d}\"\n\nws.oddFooter.left.text = \"Confidential — internal use only\"\nws.oddFooter.right.text = \"Page &P of &N\"\n\nwb.save(\"report_paged.xlsx\")\n",[168,530,531,544,556,560,574,584,588,598,608,647,651,661,671,675],{"__ignoreMap":166},[171,532,533,536,539,541],{"class":173,"line":174},[171,534,535],{"class":225},"from",[171,537,538],{"class":229}," datetime ",[171,540,226],{"class":225},[171,542,543],{"class":229}," date\n",[171,545,546,548,551,553],{"class":173,"line":239},[171,547,535],{"class":225},[171,549,550],{"class":229}," openpyxl ",[171,552,226],{"class":225},[171,554,555],{"class":229}," load_workbook\n",[171,557,558],{"class":173,"line":246},[171,559,243],{"emptyLinePlaceholder":242},[171,561,562,565,567,570,572],{"class":173,"line":258},[171,563,564],{"class":229},"wb ",[171,566,252],{"class":225},[171,568,569],{"class":229}," load_workbook(",[171,571,504],{"class":181},[171,573,518],{"class":229},[171,575,576,579,581],{"class":173,"line":295},[171,577,578],{"class":229},"ws ",[171,580,252],{"class":225},[171,582,583],{"class":229}," wb.active\n",[171,585,586],{"class":173,"line":322},[171,587,243],{"emptyLinePlaceholder":242},[171,589,590,593,595],{"class":173,"line":328},[171,591,592],{"class":229},"ws.oddHeader.left.text ",[171,594,252],{"class":225},[171,596,597],{"class":181}," \"Regional Revenue\"\n",[171,599,600,603,605],{"class":173,"line":337},[171,601,602],{"class":229},"ws.oddHeader.center.text ",[171,604,252],{"class":225},[171,606,607],{"class":181}," \"August 2026\"\n",[171,609,610,613,615,618,620,622,625,628,631,634,637,639,642,644],{"class":173,"line":376},[171,611,612],{"class":229},"ws.oddHeader.right.text ",[171,614,252],{"class":225},[171,616,617],{"class":225}," f",[171,619,349],{"class":181},[171,621,353],{"class":352},[171,623,624],{"class":229},"date.today():",[171,626,627],{"class":225},"%",[171,629,630],{"class":229},"Y",[171,632,633],{"class":225},"-%",[171,635,636],{"class":229},"m",[171,638,633],{"class":225},[171,640,641],{"class":229},"d",[171,643,359],{"class":352},[171,645,646],{"class":181},"\"\n",[171,648,649],{"class":173,"line":393},[171,650,243],{"emptyLinePlaceholder":242},[171,652,653,656,658],{"class":173,"line":417},[171,654,655],{"class":229},"ws.oddFooter.left.text ",[171,657,252],{"class":225},[171,659,660],{"class":181}," \"Confidential — internal use only\"\n",[171,662,663,666,668],{"class":173,"line":438},[171,664,665],{"class":229},"ws.oddFooter.right.text ",[171,667,252],{"class":225},[171,669,670],{"class":181}," \"Page &P of &N\"\n",[171,672,673],{"class":173,"line":444},[171,674,243],{"emptyLinePlaceholder":242},[171,676,677,680,683],{"class":173,"line":449},[171,678,679],{"class":229},"wb.save(",[171,681,682],{"class":181},"\"report_paged.xlsx\"",[171,684,518],{"class":229},[10,686,687,690,691,694],{},[168,688,689],{},"&P"," and ",[168,692,693],{},"&N"," are Excel's format codes for the current page and the page count. The full set worth knowing:",[696,697,698,711],"table",{},[699,700,701],"thead",{},[702,703,704,708],"tr",{},[705,706,707],"th",{},"Code",[705,709,710],{},"Renders as",[712,713,714,724,733,743,753,763,773],"tbody",{},[702,715,716,721],{},[717,718,719],"td",{},[168,720,689],{},[717,722,723],{},"current page number",[702,725,726,730],{},[717,727,728],{},[168,729,693],{},[717,731,732],{},"total pages",[702,734,735,740],{},[717,736,737],{},[168,738,739],{},"&D",[717,741,742],{},"the date the file was printed",[702,744,745,750],{},[717,746,747],{},[168,748,749],{},"&T",[717,751,752],{},"the time it was printed",[702,754,755,760],{},[717,756,757],{},[168,758,759],{},"&F",[717,761,762],{},"the workbook filename",[702,764,765,770],{},[717,766,767],{},[168,768,769],{},"&A",[717,771,772],{},"the sheet name",[702,774,775,780],{},[717,776,777],{},[168,778,779],{},"&&",[717,781,782],{},"a literal ampersand",[10,784,785,786,788],{},"The ",[168,787,779],{}," matters if any of your text contains one — \"Smith & Co\" in a header renders as \"Smith Co\" unless you write \"Smith && Co\".",[10,790,791],{},"Each section also takes its own font settings:",[161,793,795],{"className":216,"code":794,"language":218,"meta":166,"style":166},"ws.oddHeader.left.size = 11\nws.oddHeader.left.font = \"Calibri,Bold\"\nws.oddHeader.left.color = \"4338CA\"\n\nws.oddFooter.right.size = 9\nws.oddFooter.right.color = \"5B6780\"\n",[168,796,797,807,817,827,831,841],{"__ignoreMap":166},[171,798,799,802,804],{"class":173,"line":174},[171,800,801],{"class":229},"ws.oddHeader.left.size ",[171,803,252],{"class":225},[171,805,806],{"class":209}," 11\n",[171,808,809,812,814],{"class":173,"line":239},[171,810,811],{"class":229},"ws.oddHeader.left.font ",[171,813,252],{"class":225},[171,815,816],{"class":181}," \"Calibri,Bold\"\n",[171,818,819,822,824],{"class":173,"line":246},[171,820,821],{"class":229},"ws.oddHeader.left.color ",[171,823,252],{"class":225},[171,825,826],{"class":181}," \"4338CA\"\n",[171,828,829],{"class":173,"line":258},[171,830,243],{"emptyLinePlaceholder":242},[171,832,833,836,838],{"class":173,"line":295},[171,834,835],{"class":229},"ws.oddFooter.right.size ",[171,837,252],{"class":225},[171,839,840],{"class":209}," 9\n",[171,842,843,846,848],{"class":173,"line":322},[171,844,845],{"class":229},"ws.oddFooter.right.color ",[171,847,252],{"class":225},[171,849,850],{"class":181}," \"5B6780\"\n",[10,852,785,853,856,857,860,861,276,864,276,867,870],{},[168,854,855],{},"font"," string is Excel's own ",[168,858,859],{},"\"Name,Style\""," form — ",[168,862,863],{},"\"Calibri,Bold\"",[168,865,866],{},"\"Calibri,Italic\"",[168,868,869],{},"\"Calibri,Bold Italic\"",". It is not a CSS-like value, and an unrecognised style is ignored silently.",[10,872,873,875],{},[168,874,127],{}," applies to every page unless you enable different first or even pages:",[161,877,879],{"className":216,"code":878,"language":218,"meta":166,"style":166},"# A different header on page one — often no header at all, or a title block.\nws.HeaderFooter.differentFirst = True\nws.firstHeader.center.text = \"Regional Revenue — August 2026\"\nws.firstFooter.right.text = \"Page &P of &N\"\n",[168,880,881,887,897,907],{"__ignoreMap":166},[171,882,883],{"class":173,"line":174},[171,884,886],{"class":885},"s-wDw","# A different header on page one — often no header at all, or a title block.\n",[171,888,889,892,894],{"class":173,"line":239},[171,890,891],{"class":229},"ws.HeaderFooter.differentFirst ",[171,893,252],{"class":225},[171,895,896],{"class":209}," True\n",[171,898,899,902,904],{"class":173,"line":246},[171,900,901],{"class":229},"ws.firstHeader.center.text ",[171,903,252],{"class":225},[171,905,906],{"class":181}," \"Regional Revenue — August 2026\"\n",[171,908,909,912,914],{"class":173,"line":258},[171,910,911],{"class":229},"ws.firstFooter.right.text ",[171,913,252],{"class":225},[171,915,670],{"class":181},[156,917,919],{"id":918},"step-2-repeat-the-column-headings","Step 2 — Repeat the column headings",[10,921,922],{},"This is the single change that most improves a multi-page report. Without it, every page after the first is unlabelled numbers:",[161,924,926],{"className":216,"code":925,"language":218,"meta":166,"style":166},"ws.print_title_rows = \"1:1\"        # repeat row 1 at the top of every page\nws.print_title_cols = \"A:B\"        # and columns A and B on every page\n",[168,927,928,941],{"__ignoreMap":166},[171,929,930,933,935,938],{"class":173,"line":174},[171,931,932],{"class":229},"ws.print_title_rows ",[171,934,252],{"class":225},[171,936,937],{"class":181}," \"1:1\"",[171,939,940],{"class":885},"        # repeat row 1 at the top of every page\n",[171,942,943,946,948,951],{"class":173,"line":239},[171,944,945],{"class":229},"ws.print_title_cols ",[171,947,252],{"class":225},[171,949,950],{"class":181}," \"A:B\"",[171,952,953],{"class":885},"        # and columns A and B on every page\n",[10,955,956,957,960],{},"Set ",[168,958,959],{},"print_title_cols"," only when the table is wide enough to split across page-widths — repeating the label columns is what makes the right-hand pages meaningful. If you have set fit-to-width (next step), the table never splits horizontally and this is unnecessary.",[10,962,963],{},"Define the print area too, so stray cells outside the table do not drag empty pages into the output:",[161,965,967],{"className":216,"code":966,"language":218,"meta":166,"style":166},"from openpyxl.utils import get_column_letter\n\nlast_col = get_column_letter(ws.max_column)\nws.print_area = f\"A1:{last_col}{ws.max_row}\"\n",[168,968,969,981,985,995],{"__ignoreMap":166},[171,970,971,973,976,978],{"class":173,"line":174},[171,972,535],{"class":225},[171,974,975],{"class":229}," openpyxl.utils ",[171,977,226],{"class":225},[171,979,980],{"class":229}," get_column_letter\n",[171,982,983],{"class":173,"line":239},[171,984,243],{"emptyLinePlaceholder":242},[171,986,987,990,992],{"class":173,"line":246},[171,988,989],{"class":229},"last_col ",[171,991,252],{"class":225},[171,993,994],{"class":229}," get_column_letter(ws.max_column)\n",[171,996,997,1000,1002,1004,1007,1009,1012,1015,1018,1020],{"class":173,"line":258},[171,998,999],{"class":229},"ws.print_area ",[171,1001,252],{"class":225},[171,1003,617],{"class":225},[171,1005,1006],{"class":181},"\"A1:",[171,1008,353],{"class":352},[171,1010,1011],{"class":229},"last_col",[171,1013,1014],{"class":352},"}{",[171,1016,1017],{"class":229},"ws.max_row",[171,1019,359],{"class":352},[171,1021,646],{"class":181},[156,1023,1025],{"id":1024},"step-3-control-the-page-geometry","Step 3 — Control the page geometry",[10,1027,1028],{},"Orientation, scaling and margins decide whether the table fits at all.",[20,1030,30,1036,30,1039,30,1042,30,1045,30,1052,30,1057,30,1064,30,1069,30,1073,30,1077,30,1080,30,1084,30,1087,30,1090,30,1095,30,1099,30,1102,30,1107,30,1110,30,1113,30,1116,30,1118,30,1121,30,1124,30,1126,30,1129,30,1133],{"viewBox":1031,"role":23,"ariaLabel":1032,"ariaLabelledBy":1033,"xmlns":28,"style":29},"0 0 800 240","Fit-to-width scaling: without it a wide table splits across two page widths and interleaves, with it every column fits one page and the table simply flows down.",[1034,1035],"fitw-t","fitw-d",[32,1037,1038],{"id":1034},"Why fit-to-width matters more than orientation",[36,1040,1041],{"id":1035},"Without fit-to-width, a wide table splits into left and right halves, so the printed pages interleave: page one holds the left columns of rows one to forty, page two holds the right columns of the same rows, and a reader has to hold two pages side by side. With fitToWidth set to one and fitToHeight set to zero, every column is scaled to fit a single page width and the table simply flows down the pages in reading order.",[40,1043],{"x":42,"y":42,"width":43,"height":1044,"fill":45},"240",[40,1046],{"x":1047,"y":112,"width":1048,"height":1049,"rx":1047,"fill":1050,"stroke":1051,"style":54},"14","368","196","#fee8f2","var(--accent,#f43f8f)",[65,1053,1056],{"x":1054,"y":68,"style":1055},"198","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","no fit-to-width",[40,1058],{"x":1059,"y":1060,"width":1061,"height":1062,"rx":1063,"fill":45,"stroke":1051,"style":54},"46","68","140","76","5",[65,1065,1068],{"x":1066,"y":384,"style":1067},"116","font-size:9.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","page 1",[65,1070,1072],{"x":1066,"y":1071,"style":131},"118","left columns",[65,1074,1076],{"x":1066,"y":1075,"style":131},"132","rows 1–40",[40,1078],{"x":1079,"y":1060,"width":1061,"height":1062,"rx":1063,"fill":45,"stroke":1051,"style":54},"210",[65,1081,1083],{"x":1082,"y":384,"style":1067},"280","page 2",[65,1085,1086],{"x":1082,"y":1071,"style":131},"right columns",[65,1088,1089],{"x":1082,"y":1075,"style":131},"same rows",[65,1091,1094],{"x":1054,"y":1092,"style":1093},"176","font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","the reader holds two pages together",[65,1096,1098],{"x":1054,"y":1054,"style":1097},"font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","and doubles the page count",[40,1100],{"x":1101,"y":112,"width":1048,"height":1049,"rx":1047,"fill":86,"stroke":87,"style":54},"418",[65,1103,1106],{"x":1104,"y":68,"style":1105},"602","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","fitToWidth = 1, fitToHeight = 0",[40,1108],{"x":1109,"y":1060,"width":1061,"height":1062,"rx":1063,"fill":45,"stroke":87,"style":54},"450",[65,1111,1068],{"x":1112,"y":384,"style":91},"520",[65,1114,1115],{"x":1112,"y":1071,"style":131},"all columns",[65,1117,1076],{"x":1112,"y":1075,"style":131},[40,1119],{"x":1120,"y":1060,"width":1061,"height":1062,"rx":1063,"fill":45,"stroke":87,"style":54},"614",[65,1122,1083],{"x":1123,"y":384,"style":91},"684",[65,1125,1115],{"x":1123,"y":1071,"style":131},[65,1127,1128],{"x":1123,"y":1075,"style":131},"rows 41–80",[65,1130,1132],{"x":1104,"y":1092,"style":1131},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","the table flows down in reading order",[65,1134,1135],{"x":1104,"y":1054,"style":1097},"and the header row repeats on each",[161,1137,1139],{"className":216,"code":1138,"language":218,"meta":166,"style":166},"from openpyxl.worksheet.properties import PageSetupProperties\n\nws.page_setup.orientation = \"landscape\"\nws.page_setup.paperSize = ws.PAPERSIZE_A4\n\n# Scale every column onto one page width; let the rows flow down.\nws.page_setup.fitToWidth = 1\nws.page_setup.fitToHeight = 0\nws.sheet_properties.pageSetUpPr = PageSetupProperties(fitToPage=True)\n\nws.page_margins.left = 0.5\nws.page_margins.right = 0.5\nws.page_margins.top = 0.8          # room for the header\nws.page_margins.bottom = 0.8       # room for the footer\nws.page_margins.header = 0.3\nws.page_margins.footer = 0.3\n\nws.print_options.horizontalCentered = True\nws.print_options.gridLines = False\n",[168,1140,1141,1153,1157,1167,1180,1184,1189,1198,1208,1228,1232,1242,1251,1264,1276,1286,1295,1300,1310],{"__ignoreMap":166},[171,1142,1143,1145,1148,1150],{"class":173,"line":174},[171,1144,535],{"class":225},[171,1146,1147],{"class":229}," openpyxl.worksheet.properties ",[171,1149,226],{"class":225},[171,1151,1152],{"class":229}," PageSetupProperties\n",[171,1154,1155],{"class":173,"line":239},[171,1156,243],{"emptyLinePlaceholder":242},[171,1158,1159,1162,1164],{"class":173,"line":246},[171,1160,1161],{"class":229},"ws.page_setup.orientation ",[171,1163,252],{"class":225},[171,1165,1166],{"class":181}," \"landscape\"\n",[171,1168,1169,1172,1174,1177],{"class":173,"line":258},[171,1170,1171],{"class":229},"ws.page_setup.paperSize ",[171,1173,252],{"class":225},[171,1175,1176],{"class":229}," ws.",[171,1178,1179],{"class":209},"PAPERSIZE_A4\n",[171,1181,1182],{"class":173,"line":295},[171,1183,243],{"emptyLinePlaceholder":242},[171,1185,1186],{"class":173,"line":322},[171,1187,1188],{"class":885},"# Scale every column onto one page width; let the rows flow down.\n",[171,1190,1191,1194,1196],{"class":173,"line":328},[171,1192,1193],{"class":229},"ws.page_setup.fitToWidth ",[171,1195,252],{"class":225},[171,1197,495],{"class":209},[171,1199,1200,1203,1205],{"class":173,"line":337},[171,1201,1202],{"class":229},"ws.page_setup.fitToHeight ",[171,1204,252],{"class":225},[171,1206,1207],{"class":209}," 0\n",[171,1209,1210,1213,1215,1218,1221,1223,1226],{"class":173,"line":376},[171,1211,1212],{"class":229},"ws.sheet_properties.pageSetUpPr ",[171,1214,252],{"class":225},[171,1216,1217],{"class":229}," PageSetupProperties(",[171,1219,1220],{"class":509},"fitToPage",[171,1222,252],{"class":225},[171,1224,1225],{"class":209},"True",[171,1227,518],{"class":229},[171,1229,1230],{"class":173,"line":393},[171,1231,243],{"emptyLinePlaceholder":242},[171,1233,1234,1237,1239],{"class":173,"line":417},[171,1235,1236],{"class":229},"ws.page_margins.left ",[171,1238,252],{"class":225},[171,1240,1241],{"class":209}," 0.5\n",[171,1243,1244,1247,1249],{"class":173,"line":438},[171,1245,1246],{"class":229},"ws.page_margins.right ",[171,1248,252],{"class":225},[171,1250,1241],{"class":209},[171,1252,1253,1256,1258,1261],{"class":173,"line":444},[171,1254,1255],{"class":229},"ws.page_margins.top ",[171,1257,252],{"class":225},[171,1259,1260],{"class":209}," 0.8",[171,1262,1263],{"class":885},"          # room for the header\n",[171,1265,1266,1269,1271,1273],{"class":173,"line":449},[171,1267,1268],{"class":229},"ws.page_margins.bottom ",[171,1270,252],{"class":225},[171,1272,1260],{"class":209},[171,1274,1275],{"class":885},"       # room for the footer\n",[171,1277,1278,1281,1283],{"class":173,"line":460},[171,1279,1280],{"class":229},"ws.page_margins.header ",[171,1282,252],{"class":225},[171,1284,1285],{"class":209}," 0.3\n",[171,1287,1288,1291,1293],{"class":173,"line":498},[171,1289,1290],{"class":229},"ws.page_margins.footer ",[171,1292,252],{"class":225},[171,1294,1285],{"class":209},[171,1296,1298],{"class":173,"line":1297},17,[171,1299,243],{"emptyLinePlaceholder":242},[171,1301,1303,1306,1308],{"class":173,"line":1302},18,[171,1304,1305],{"class":229},"ws.print_options.horizontalCentered ",[171,1307,252],{"class":225},[171,1309,896],{"class":209},[171,1311,1313,1316,1318],{"class":173,"line":1312},19,[171,1314,1315],{"class":229},"ws.print_options.gridLines ",[171,1317,252],{"class":225},[171,1319,1320],{"class":209}," False\n",[10,1322,785,1323,1326,1327,1330],{},[168,1324,1325],{},"pageSetUpPr=PageSetupProperties(fitToPage=True)"," line is the one people miss. Without it the ",[168,1328,1329],{},"fitToWidth"," value is stored but ignored, and the table splits across page-widths exactly as if you had never set it.",[10,1332,1333,1334,1338,1339,690,1342,1345,1346,690,1349,1352],{},"Margins are in ",[1335,1336,1337],"strong",{},"inches",", and the ",[168,1340,1341],{},"header",[168,1343,1344],{},"footer"," margins are the distance from the paper edge to the header text — they must be smaller than the ",[168,1347,1348],{},"top",[168,1350,1351],{},"bottom"," margins or the header overlaps the data.",[156,1354,1356],{"id":1355},"step-4-break-pages-at-meaningful-boundaries","Step 4 — Break pages at meaningful boundaries",[20,1358,30,1364,30,1367,30,1370,30,1373,30,1379,30,1383,30,1387,30,1392,30,1396,30,1400,30,1402,30,1406,30,1408,30,1411,30,1414,30,1418,30,1420,30,1423,30,1426,30,1431,30,1433],{"viewBox":1359,"role":23,"ariaLabel":1360,"ariaLabelledBy":1361,"xmlns":28,"style":29},"0 0 800 228","Header and footer format codes: ampersand P and ampersand N produce the page number and total, ampersand D and ampersand T the date and time, and a doubled ampersand a literal one.",[1362,1363],"codes-t","codes-d",[32,1365,1366],{"id":1362},"Format codes and what they render as",[36,1368,1369],{"id":1363},"Four format-code examples paired with their rendered output on page two of a seven-page document. Page ampersand P of ampersand N renders as Page 2 of 7. Ampersand D renders the print date. Ampersand F renders the workbook filename. And a doubled ampersand renders a single literal ampersand, which is what a company name containing one requires.",[40,1371],{"x":42,"y":42,"width":43,"height":1372,"fill":45},"228",[65,1374,1378],{"x":1375,"y":1376,"style":1377},"220","30","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","what you write",[65,1380,1382],{"x":1381,"y":1376,"style":1377},"590","what prints",[40,1384],{"x":1376,"y":1059,"width":1385,"height":1386,"rx":52,"fill":62,"stroke":63,"style":54},"380","36",[65,1388,1391],{"x":1375,"y":1389,"style":1390},"69","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Page &P of &N",[40,1393],{"x":1394,"y":1059,"width":1395,"height":1386,"rx":52,"fill":45,"stroke":53},"426","344",[65,1397,122],{"x":1398,"y":1389,"style":1399},"598","font-size:11px;fill:var(--text,#172033);text-anchor:middle",[40,1401],{"x":1376,"y":135,"width":1385,"height":1386,"rx":52,"fill":62,"stroke":63,"style":54},[65,1403,1405],{"x":1375,"y":1404,"style":1390},"113","Printed &D",[40,1407],{"x":1394,"y":135,"width":1395,"height":1386,"rx":52,"fill":45,"stroke":53},[65,1409,1410],{"x":1398,"y":1404,"style":1399},"Printed 15\u002F08\u002F2026",[40,1412],{"x":1376,"y":1413,"width":1385,"height":1386,"rx":52,"fill":62,"stroke":63,"style":54},"134",[65,1415,1417],{"x":1375,"y":1416,"style":1390},"157","&F — &A",[40,1419],{"x":1394,"y":1413,"width":1395,"height":1386,"rx":52,"fill":45,"stroke":53},[65,1421,1422],{"x":1398,"y":1416,"style":1399},"report.xlsx — Summary",[40,1424],{"x":1376,"y":1425,"width":1385,"height":1386,"rx":52,"fill":113,"stroke":114,"style":54},"178",[65,1427,1430],{"x":1375,"y":1428,"style":1429},"201","font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Smith && Co",[40,1432],{"x":1394,"y":1425,"width":1395,"height":1386,"rx":52,"fill":45,"stroke":114,"style":54},[65,1434,1435],{"x":1398,"y":1428,"style":1399},"Smith & Co — a single ampersand needs two",[10,1437,1438],{},"A section that starts three rows before a page break reads badly. Insert breaks where the data changes group:",[161,1440,1442],{"className":216,"code":1441,"language":218,"meta":166,"style":166},"from openpyxl.worksheet.pagebreak import Break\n\ndef break_on_change(ws, column=1, first_row=2):\n    \"\"\"Start a new page each time the value in `column` changes.\"\"\"\n    previous, inserted = None, 0\n    for (cell,) in ws.iter_rows(min_row=first_row, min_col=column,\n                                max_col=column):\n        if cell.value is None:\n            continue\n        if previous is not None and cell.value != previous:\n            ws.row_breaks.append(Break(id=cell.row - 1))\n            inserted += 1\n        previous = cell.value\n    return inserted\n\nprint(f\"{break_on_change(ws)} page break(s) inserted\")\n",[168,1443,1444,1456,1460,1486,1491,1506,1534,1544,1560,1565,1590,1611,1621,1631,1639,1643],{"__ignoreMap":166},[171,1445,1446,1448,1451,1453],{"class":173,"line":174},[171,1447,535],{"class":225},[171,1449,1450],{"class":229}," openpyxl.worksheet.pagebreak ",[171,1452,226],{"class":225},[171,1454,1455],{"class":229}," Break\n",[171,1457,1458],{"class":173,"line":239},[171,1459,243],{"emptyLinePlaceholder":242},[171,1461,1462,1465,1469,1472,1474,1476,1479,1481,1484],{"class":173,"line":246},[171,1463,1464],{"class":225},"def",[171,1466,1468],{"class":1467},"s_Opv"," break_on_change",[171,1470,1471],{"class":229},"(ws, column",[171,1473,252],{"class":225},[171,1475,312],{"class":209},[171,1477,1478],{"class":229},", first_row",[171,1480,252],{"class":225},[171,1482,1483],{"class":209},"2",[171,1485,292],{"class":229},[171,1487,1488],{"class":173,"line":258},[171,1489,1490],{"class":181},"    \"\"\"Start a new page each time the value in `column` changes.\"\"\"\n",[171,1492,1493,1496,1498,1501,1503],{"class":173,"line":295},[171,1494,1495],{"class":229},"    previous, inserted ",[171,1497,252],{"class":225},[171,1499,1500],{"class":209}," None",[171,1502,276],{"class":229},[171,1504,1505],{"class":209},"0\n",[171,1507,1508,1510,1513,1515,1518,1521,1523,1526,1529,1531],{"class":173,"line":322},[171,1509,298],{"class":225},[171,1511,1512],{"class":229}," (cell,) ",[171,1514,267],{"class":225},[171,1516,1517],{"class":229}," ws.iter_rows(",[171,1519,1520],{"class":509},"min_row",[171,1522,252],{"class":225},[171,1524,1525],{"class":229},"first_row, ",[171,1527,1528],{"class":509},"min_col",[171,1530,252],{"class":225},[171,1532,1533],{"class":229},"column,\n",[171,1535,1536,1539,1541],{"class":173,"line":328},[171,1537,1538],{"class":509},"                                max_col",[171,1540,252],{"class":225},[171,1542,1543],{"class":229},"column):\n",[171,1545,1546,1549,1552,1555,1557],{"class":173,"line":337},[171,1547,1548],{"class":225},"        if",[171,1550,1551],{"class":229}," cell.value ",[171,1553,1554],{"class":225},"is",[171,1556,1500],{"class":209},[171,1558,1559],{"class":229},":\n",[171,1561,1562],{"class":173,"line":376},[171,1563,1564],{"class":225},"            continue\n",[171,1566,1567,1569,1572,1574,1577,1579,1582,1584,1587],{"class":173,"line":393},[171,1568,1548],{"class":225},[171,1570,1571],{"class":229}," previous ",[171,1573,1554],{"class":225},[171,1575,1576],{"class":225}," not",[171,1578,1500],{"class":209},[171,1580,1581],{"class":225}," and",[171,1583,1551],{"class":229},[171,1585,1586],{"class":225},"!=",[171,1588,1589],{"class":229}," previous:\n",[171,1591,1592,1595,1598,1600,1603,1605,1608],{"class":173,"line":417},[171,1593,1594],{"class":229},"            ws.row_breaks.append(Break(",[171,1596,1597],{"class":509},"id",[171,1599,252],{"class":225},[171,1601,1602],{"class":229},"cell.row ",[171,1604,492],{"class":225},[171,1606,1607],{"class":209}," 1",[171,1609,1610],{"class":229},"))\n",[171,1612,1613,1616,1619],{"class":173,"line":438},[171,1614,1615],{"class":229},"            inserted ",[171,1617,1618],{"class":225},"+=",[171,1620,495],{"class":209},[171,1622,1623,1626,1628],{"class":173,"line":444},[171,1624,1625],{"class":229},"        previous ",[171,1627,252],{"class":225},[171,1629,1630],{"class":229}," cell.value\n",[171,1632,1633,1636],{"class":173,"line":449},[171,1634,1635],{"class":225},"    return",[171,1637,1638],{"class":229}," inserted\n",[171,1640,1641],{"class":173,"line":460},[171,1642,243],{"emptyLinePlaceholder":242},[171,1644,1645,1648,1650,1652,1654,1656,1659,1661,1664],{"class":173,"line":498},[171,1646,1647],{"class":209},"print",[171,1649,309],{"class":229},[171,1651,346],{"class":225},[171,1653,349],{"class":181},[171,1655,353],{"class":352},[171,1657,1658],{"class":229},"break_on_change(ws)",[171,1660,359],{"class":352},[171,1662,1663],{"class":181}," page break(s) inserted\"",[171,1665,518],{"class":229},[10,1667,1668,1671,1672,1675,1676,1679,1680,1683],{},[168,1669,1670],{},"Break(id=n)"," puts the break ",[1335,1673,1674],{},"after"," row ",[168,1677,1678],{},"n",", so passing ",[168,1681,1682],{},"cell.row - 1"," starts the new group at the top of a page. Off by one in either direction and every section begins with one orphaned row from the previous one.",[10,1685,1686],{},"Excel caps the number of manual breaks per sheet at 1,026, so a break-per-group on a sheet with thousands of groups silently stops working partway. Guard it when the group count is not known:",[161,1688,1690],{"className":216,"code":1689,"language":218,"meta":166,"style":166},"groups = report[\"region\"].nunique()\nif groups > 1_000:\n    print(f\"{groups} groups — too many for per-group page breaks; skipping\")\nelse:\n    break_on_change(ws)\n",[168,1691,1692,1707,1723,1746,1753],{"__ignoreMap":166},[171,1693,1694,1697,1699,1701,1704],{"class":173,"line":174},[171,1695,1696],{"class":229},"groups ",[171,1698,252],{"class":225},[171,1700,474],{"class":229},[171,1702,1703],{"class":181},"\"region\"",[171,1705,1706],{"class":229},"].nunique()\n",[171,1708,1709,1712,1715,1718,1721],{"class":173,"line":239},[171,1710,1711],{"class":225},"if",[171,1713,1714],{"class":229}," groups ",[171,1716,1717],{"class":225},">",[171,1719,1720],{"class":209}," 1_000",[171,1722,1559],{"class":229},[171,1724,1725,1728,1730,1732,1734,1736,1739,1741,1744],{"class":173,"line":246},[171,1726,1727],{"class":209},"    print",[171,1729,309],{"class":229},[171,1731,346],{"class":225},[171,1733,349],{"class":181},[171,1735,353],{"class":352},[171,1737,1738],{"class":229},"groups",[171,1740,359],{"class":352},[171,1742,1743],{"class":181}," groups — too many for per-group page breaks; skipping\"",[171,1745,518],{"class":229},[171,1747,1748,1751],{"class":173,"line":258},[171,1749,1750],{"class":225},"else",[171,1752,1559],{"class":229},[171,1754,1755],{"class":173,"line":295},[171,1756,1757],{"class":229},"    break_on_change(ws)\n",[156,1759,1761],{"id":1760},"step-5-convert-and-check","Step 5 — Convert and check",[10,1763,1764],{},"The page setup travels with the workbook, so the conversion is unchanged:",[161,1766,1768],{"className":216,"code":1767,"language":218,"meta":166,"style":166},"import shutil, subprocess, tempfile\nfrom pathlib import Path\n\ndef to_pdf(xlsx_path, out_dir=\"delivery\", timeout=180):\n    soffice = shutil.which(\"soffice\") or shutil.which(\"libreoffice\")\n    if soffice is None:\n        raise RuntimeError(\"LibreOffice not found\")\n\n    src = Path(xlsx_path).resolve()\n    out = Path(out_dir).resolve()\n    out.mkdir(parents=True, exist_ok=True)\n\n    with tempfile.TemporaryDirectory() as profile:\n        result = subprocess.run(\n            [soffice, f\"-env:UserInstallation=file:\u002F\u002F{profile}\",\n             \"--headless\", \"--convert-to\", \"pdf\", \"--outdir\", str(out), str(src)],\n            capture_output=True, text=True, timeout=timeout,\n        )\n    if result.returncode != 0:\n        raise RuntimeError(result.stderr.strip())\n\n    pdf = out \u002F (src.stem + \".pdf\")\n    if not pdf.is_file() or pdf.stat().st_size == 0:\n        raise RuntimeError(f\"no usable PDF at {pdf}\")\n    return pdf\n\npdf = to_pdf(\"report_paged.xlsx\")\nprint(\"wrote\", pdf)\n",[168,1769,1770,1777,1789,1793,1817,1843,1857,1872,1876,1886,1896,1919,1923,1936,1946,1967,2000,2027,2032,2046,2056,2061,2085,2107,2132,2140,2145,2160],{"__ignoreMap":166},[171,1771,1772,1774],{"class":173,"line":174},[171,1773,226],{"class":225},[171,1775,1776],{"class":229}," shutil, subprocess, tempfile\n",[171,1778,1779,1781,1784,1786],{"class":173,"line":239},[171,1780,535],{"class":225},[171,1782,1783],{"class":229}," pathlib ",[171,1785,226],{"class":225},[171,1787,1788],{"class":229}," Path\n",[171,1790,1791],{"class":173,"line":246},[171,1792,243],{"emptyLinePlaceholder":242},[171,1794,1795,1797,1800,1803,1805,1808,1811,1813,1815],{"class":173,"line":258},[171,1796,1464],{"class":225},[171,1798,1799],{"class":1467}," to_pdf",[171,1801,1802],{"class":229},"(xlsx_path, out_dir",[171,1804,252],{"class":225},[171,1806,1807],{"class":181},"\"delivery\"",[171,1809,1810],{"class":229},", timeout",[171,1812,252],{"class":225},[171,1814,67],{"class":209},[171,1816,292],{"class":229},[171,1818,1819,1822,1824,1827,1830,1833,1836,1838,1841],{"class":173,"line":295},[171,1820,1821],{"class":229},"    soffice ",[171,1823,252],{"class":225},[171,1825,1826],{"class":229}," shutil.which(",[171,1828,1829],{"class":181},"\"soffice\"",[171,1831,1832],{"class":229},") ",[171,1834,1835],{"class":225},"or",[171,1837,1826],{"class":229},[171,1839,1840],{"class":181},"\"libreoffice\"",[171,1842,518],{"class":229},[171,1844,1845,1848,1851,1853,1855],{"class":173,"line":322},[171,1846,1847],{"class":225},"    if",[171,1849,1850],{"class":229}," soffice ",[171,1852,1554],{"class":225},[171,1854,1500],{"class":209},[171,1856,1559],{"class":229},[171,1858,1859,1862,1865,1867,1870],{"class":173,"line":328},[171,1860,1861],{"class":225},"        raise",[171,1863,1864],{"class":209}," RuntimeError",[171,1866,309],{"class":229},[171,1868,1869],{"class":181},"\"LibreOffice not found\"",[171,1871,518],{"class":229},[171,1873,1874],{"class":173,"line":337},[171,1875,243],{"emptyLinePlaceholder":242},[171,1877,1878,1881,1883],{"class":173,"line":376},[171,1879,1880],{"class":229},"    src ",[171,1882,252],{"class":225},[171,1884,1885],{"class":229}," Path(xlsx_path).resolve()\n",[171,1887,1888,1891,1893],{"class":173,"line":393},[171,1889,1890],{"class":229},"    out ",[171,1892,252],{"class":225},[171,1894,1895],{"class":229}," Path(out_dir).resolve()\n",[171,1897,1898,1901,1904,1906,1908,1910,1913,1915,1917],{"class":173,"line":417},[171,1899,1900],{"class":229},"    out.mkdir(",[171,1902,1903],{"class":509},"parents",[171,1905,252],{"class":225},[171,1907,1225],{"class":209},[171,1909,276],{"class":229},[171,1911,1912],{"class":509},"exist_ok",[171,1914,252],{"class":225},[171,1916,1225],{"class":209},[171,1918,518],{"class":229},[171,1920,1921],{"class":173,"line":438},[171,1922,243],{"emptyLinePlaceholder":242},[171,1924,1925,1928,1931,1933],{"class":173,"line":444},[171,1926,1927],{"class":225},"    with",[171,1929,1930],{"class":229}," tempfile.TemporaryDirectory() ",[171,1932,233],{"class":225},[171,1934,1935],{"class":229}," profile:\n",[171,1937,1938,1941,1943],{"class":173,"line":449},[171,1939,1940],{"class":229},"        result ",[171,1942,252],{"class":225},[171,1944,1945],{"class":229}," subprocess.run(\n",[171,1947,1948,1951,1953,1956,1958,1961,1963,1965],{"class":173,"line":460},[171,1949,1950],{"class":229},"            [soffice, ",[171,1952,346],{"class":225},[171,1954,1955],{"class":181},"\"-env:UserInstallation=file:\u002F\u002F",[171,1957,353],{"class":352},[171,1959,1960],{"class":229},"profile",[171,1962,359],{"class":352},[171,1964,349],{"class":181},[171,1966,373],{"class":229},[171,1968,1969,1972,1974,1977,1979,1982,1984,1987,1989,1992,1995,1997],{"class":173,"line":498},[171,1970,1971],{"class":181},"             \"--headless\"",[171,1973,276],{"class":229},[171,1975,1976],{"class":181},"\"--convert-to\"",[171,1978,276],{"class":229},[171,1980,1981],{"class":181},"\"pdf\"",[171,1983,276],{"class":229},[171,1985,1986],{"class":181},"\"--outdir\"",[171,1988,276],{"class":229},[171,1990,1991],{"class":209},"str",[171,1993,1994],{"class":229},"(out), ",[171,1996,1991],{"class":209},[171,1998,1999],{"class":229},"(src)],\n",[171,2001,2002,2005,2007,2009,2011,2013,2015,2017,2019,2022,2024],{"class":173,"line":1297},[171,2003,2004],{"class":509},"            capture_output",[171,2006,252],{"class":225},[171,2008,1225],{"class":209},[171,2010,276],{"class":229},[171,2012,65],{"class":509},[171,2014,252],{"class":225},[171,2016,1225],{"class":209},[171,2018,276],{"class":229},[171,2020,2021],{"class":509},"timeout",[171,2023,252],{"class":225},[171,2025,2026],{"class":229},"timeout,\n",[171,2028,2029],{"class":173,"line":1302},[171,2030,2031],{"class":229},"        )\n",[171,2033,2034,2036,2039,2041,2044],{"class":173,"line":1312},[171,2035,1847],{"class":225},[171,2037,2038],{"class":229}," result.returncode ",[171,2040,1586],{"class":225},[171,2042,2043],{"class":209}," 0",[171,2045,1559],{"class":229},[171,2047,2049,2051,2053],{"class":173,"line":2048},20,[171,2050,1861],{"class":225},[171,2052,1864],{"class":209},[171,2054,2055],{"class":229},"(result.stderr.strip())\n",[171,2057,2059],{"class":173,"line":2058},21,[171,2060,243],{"emptyLinePlaceholder":242},[171,2062,2064,2067,2069,2072,2074,2077,2080,2083],{"class":173,"line":2063},22,[171,2065,2066],{"class":229},"    pdf ",[171,2068,252],{"class":225},[171,2070,2071],{"class":229}," out ",[171,2073,482],{"class":225},[171,2075,2076],{"class":229}," (src.stem ",[171,2078,2079],{"class":225},"+",[171,2081,2082],{"class":181}," \".pdf\"",[171,2084,518],{"class":229},[171,2086,2088,2090,2092,2095,2097,2100,2103,2105],{"class":173,"line":2087},23,[171,2089,1847],{"class":225},[171,2091,1576],{"class":225},[171,2093,2094],{"class":229}," pdf.is_file() ",[171,2096,1835],{"class":225},[171,2098,2099],{"class":229}," pdf.stat().st_size ",[171,2101,2102],{"class":225},"==",[171,2104,2043],{"class":209},[171,2106,1559],{"class":229},[171,2108,2110,2112,2114,2116,2118,2121,2123,2126,2128,2130],{"class":173,"line":2109},24,[171,2111,1861],{"class":225},[171,2113,1864],{"class":209},[171,2115,309],{"class":229},[171,2117,346],{"class":225},[171,2119,2120],{"class":181},"\"no usable PDF at ",[171,2122,353],{"class":352},[171,2124,2125],{"class":229},"pdf",[171,2127,359],{"class":352},[171,2129,349],{"class":181},[171,2131,518],{"class":229},[171,2133,2135,2137],{"class":173,"line":2134},25,[171,2136,1635],{"class":225},[171,2138,2139],{"class":229}," pdf\n",[171,2141,2143],{"class":173,"line":2142},26,[171,2144,243],{"emptyLinePlaceholder":242},[171,2146,2148,2151,2153,2156,2158],{"class":173,"line":2147},27,[171,2149,2150],{"class":229},"pdf ",[171,2152,252],{"class":225},[171,2154,2155],{"class":229}," to_pdf(",[171,2157,682],{"class":181},[171,2159,518],{"class":229},[171,2161,2163,2165,2167,2170],{"class":173,"line":2162},28,[171,2164,1647],{"class":209},[171,2166,309],{"class":229},[171,2168,2169],{"class":181},"\"wrote\"",[171,2171,2172],{"class":229},", pdf)\n",[10,2174,2175],{},"A cheap structural check catches the case where the page setup produced far more or far fewer pages than expected:",[161,2177,2179],{"className":216,"code":2178,"language":218,"meta":166,"style":166},"import re\n\ndef page_count(pdf_path):\n    \"\"\"Approximate page count without a PDF library.\"\"\"\n    data = pdf_path.read_bytes()\n    counts = [int(m) for m in re.findall(rb\"\u002FCount\\s+(\\d+)\", data)]\n    return max(counts) if counts else len(re.findall(rb\"\u002FType\\s*\u002FPage[^s]\", data))\n\npages = page_count(pdf)\nprint(f\"{pages} page(s)\")\nassert 1 \u003C= pages \u003C= 40, f\"unexpected page count: {pages}\"\n",[168,2180,2181,2188,2192,2202,2207,2217,2267,2319,2323,2333,2355],{"__ignoreMap":166},[171,2182,2183,2185],{"class":173,"line":174},[171,2184,226],{"class":225},[171,2186,2187],{"class":229}," re\n",[171,2189,2190],{"class":173,"line":239},[171,2191,243],{"emptyLinePlaceholder":242},[171,2193,2194,2196,2199],{"class":173,"line":246},[171,2195,1464],{"class":225},[171,2197,2198],{"class":1467}," page_count",[171,2200,2201],{"class":229},"(pdf_path):\n",[171,2203,2204],{"class":173,"line":258},[171,2205,2206],{"class":181},"    \"\"\"Approximate page count without a PDF library.\"\"\"\n",[171,2208,2209,2212,2214],{"class":173,"line":295},[171,2210,2211],{"class":229},"    data ",[171,2213,252],{"class":225},[171,2215,2216],{"class":229}," pdf_path.read_bytes()\n",[171,2218,2219,2222,2224,2227,2230,2233,2235,2238,2240,2243,2246,2249,2252,2254,2257,2259,2262,2264],{"class":173,"line":322},[171,2220,2221],{"class":229},"    counts ",[171,2223,252],{"class":225},[171,2225,2226],{"class":229}," [",[171,2228,2229],{"class":209},"int",[171,2231,2232],{"class":229},"(m) ",[171,2234,261],{"class":225},[171,2236,2237],{"class":229}," m ",[171,2239,267],{"class":225},[171,2241,2242],{"class":229}," re.findall(",[171,2244,2245],{"class":225},"rb",[171,2247,2248],{"class":181},"\"\u002FCount",[171,2250,2251],{"class":209},"\\s",[171,2253,2079],{"class":225},[171,2255,2256],{"class":209},"(\\d",[171,2258,2079],{"class":225},[171,2260,2261],{"class":209},")",[171,2263,349],{"class":181},[171,2265,2266],{"class":229},", data)]\n",[171,2268,2269,2271,2274,2277,2279,2282,2284,2287,2290,2292,2295,2297,2299,2302,2305,2308,2311,2314,2316],{"class":173,"line":328},[171,2270,1635],{"class":225},[171,2272,2273],{"class":209}," max",[171,2275,2276],{"class":229},"(counts) ",[171,2278,1711],{"class":225},[171,2280,2281],{"class":229}," counts ",[171,2283,1750],{"class":225},[171,2285,2286],{"class":209}," len",[171,2288,2289],{"class":229},"(re.findall(",[171,2291,2245],{"class":225},[171,2293,2294],{"class":181},"\"\u002FType",[171,2296,2251],{"class":209},[171,2298,409],{"class":225},[171,2300,2301],{"class":181},"\u002FPage",[171,2303,2304],{"class":209},"[",[171,2306,2307],{"class":225},"^",[171,2309,2310],{"class":352},"s",[171,2312,2313],{"class":209},"]",[171,2315,349],{"class":181},[171,2317,2318],{"class":229},", data))\n",[171,2320,2321],{"class":173,"line":337},[171,2322,243],{"emptyLinePlaceholder":242},[171,2324,2325,2328,2330],{"class":173,"line":376},[171,2326,2327],{"class":229},"pages ",[171,2329,252],{"class":225},[171,2331,2332],{"class":229}," page_count(pdf)\n",[171,2334,2335,2337,2339,2341,2343,2345,2348,2350,2353],{"class":173,"line":393},[171,2336,1647],{"class":209},[171,2338,309],{"class":229},[171,2340,346],{"class":225},[171,2342,349],{"class":181},[171,2344,353],{"class":352},[171,2346,2347],{"class":229},"pages",[171,2349,359],{"class":352},[171,2351,2352],{"class":181}," page(s)\"",[171,2354,518],{"class":229},[171,2356,2357,2360,2362,2365,2368,2371,2374,2376,2378,2381,2383,2385,2387],{"class":173,"line":417},[171,2358,2359],{"class":225},"assert",[171,2361,1607],{"class":209},[171,2363,2364],{"class":225}," \u003C=",[171,2366,2367],{"class":229}," pages ",[171,2369,2370],{"class":225},"\u003C=",[171,2372,2373],{"class":209}," 40",[171,2375,276],{"class":229},[171,2377,346],{"class":225},[171,2379,2380],{"class":181},"\"unexpected page count: ",[171,2382,353],{"class":352},[171,2384,2347],{"class":229},[171,2386,359],{"class":352},[171,2388,646],{"class":181},[10,2390,2391],{},"An unexpectedly high count almost always means fit-to-width did not take effect — the table split horizontally and doubled the pages.",[156,2393,2395],{"id":2394},"common-pitfalls-and-fixes","Common pitfalls and fixes",[696,2397,2398,2411],{},[699,2399,2400],{},[702,2401,2402,2405,2408],{},[705,2403,2404],{},"Symptom",[705,2406,2407],{},"Cause",[705,2409,2410],{},"Fix",[712,2412,2413,2424,2438,2454,2470,2487,2501,2514],{},[702,2414,2415,2418,2421],{},[717,2416,2417],{},"Header does not appear in Excel",[717,2419,2420],{},"It is print-only",[717,2422,2423],{},"Use Print Preview, or check the PDF.",[702,2425,2426,2429,2434],{},[717,2427,2428],{},"Columns split across page-widths",[717,2430,2431,2433],{},[168,2432,1220],{}," not enabled",[717,2435,956,2436,18],{},[168,2437,1325],{},[702,2439,2440,2443,2448],{},[717,2441,2442],{},"Column headings only on page one",[717,2444,2445,2447],{},[168,2446,137],{}," unset",[717,2449,2450,2451,18],{},"Set it to ",[168,2452,2453],{},"\"1:1\"",[702,2455,2456,2462,2465],{},[717,2457,2458,2461],{},[168,2459,2460],{},"&"," missing from header text",[717,2463,2464],{},"Treated as a format code",[717,2466,2467,2468,18],{},"Escape it as ",[168,2469,779],{},[702,2471,2472,2475,2478],{},[717,2473,2474],{},"Header overlaps the data",[717,2476,2477],{},"Header margin ≥ top margin",[717,2479,2480,2481,2484,2485,18],{},"Keep ",[168,2482,2483],{},"page_margins.header"," smaller than ",[168,2486,1348],{},[702,2488,2489,2492,2495],{},[717,2490,2491],{},"Blank pages at the end",[717,2493,2494],{},"Print area larger than the data",[717,2496,956,2497,2500],{},[168,2498,2499],{},"ws.print_area"," explicitly.",[702,2502,2503,2506,2509],{},[717,2504,2505],{},"Each section starts with an orphan row",[717,2507,2508],{},"Break id off by one",[717,2510,2511,18],{},[168,2512,2513],{},"Break(id=row - 1)",[702,2515,2516,2519,2522],{},[717,2517,2518],{},"Later breaks ignored",[717,2520,2521],{},"Over Excel's 1,026-break cap",[717,2523,2524],{},"Break by larger groups, or not at all.",[156,2526,2528],{"id":2527},"performance-and-scale-notes","Performance and scale notes",[10,2530,2531],{},"Page setup costs nothing at write time — every property is a handful of attributes on the sheet. The cost is entirely in the conversion, which scales with page count because LibreOffice lays out and renders each one.",[10,2533,2534,2535,2538],{},"That makes ",[1335,2536,2537],{},"fit-to-width the biggest performance lever available",", since a table that splits horizontally doubles the pages and therefore roughly doubles the render time. Setting it correctly is both a readability fix and a speed fix.",[10,2540,2541,2542,2548],{},"Two further habits for a batch. ",[1335,2543,2544,2545,2547],{},"Convert many files in one ",[168,2546,206],{}," invocation"," so the one-to-two-second start-up is paid once rather than per file:",[161,2550,2552],{"className":216,"code":2551,"language":218,"meta":166,"style":166},"import shutil, subprocess, tempfile\nfrom pathlib import Path\n\ndef to_pdf_batch(paths, out_dir=\"delivery\", timeout=900):\n    soffice = shutil.which(\"soffice\") or shutil.which(\"libreoffice\")\n    out = Path(out_dir).resolve()\n    out.mkdir(parents=True, exist_ok=True)\n    srcs = [str(Path(p).resolve()) for p in paths]\n\n    with tempfile.TemporaryDirectory() as profile:\n        subprocess.run(\n            [soffice, f\"-env:UserInstallation=file:\u002F\u002F{profile}\",\n             \"--headless\", \"--convert-to\", \"pdf\", \"--outdir\", str(out), *srcs],\n            capture_output=True, text=True, timeout=timeout, check=True,\n        )\n    return [out \u002F (Path(s).stem + \".pdf\") for s in srcs]\n",[168,2553,2554,2560,2570,2574,2597,2617,2625,2645,2669,2673,2683,2688,2706,2733,2767,2771],{"__ignoreMap":166},[171,2555,2556,2558],{"class":173,"line":174},[171,2557,226],{"class":225},[171,2559,1776],{"class":229},[171,2561,2562,2564,2566,2568],{"class":173,"line":239},[171,2563,535],{"class":225},[171,2565,1783],{"class":229},[171,2567,226],{"class":225},[171,2569,1788],{"class":229},[171,2571,2572],{"class":173,"line":246},[171,2573,243],{"emptyLinePlaceholder":242},[171,2575,2576,2578,2581,2584,2586,2588,2590,2592,2595],{"class":173,"line":258},[171,2577,1464],{"class":225},[171,2579,2580],{"class":1467}," to_pdf_batch",[171,2582,2583],{"class":229},"(paths, out_dir",[171,2585,252],{"class":225},[171,2587,1807],{"class":181},[171,2589,1810],{"class":229},[171,2591,252],{"class":225},[171,2593,2594],{"class":209},"900",[171,2596,292],{"class":229},[171,2598,2599,2601,2603,2605,2607,2609,2611,2613,2615],{"class":173,"line":295},[171,2600,1821],{"class":229},[171,2602,252],{"class":225},[171,2604,1826],{"class":229},[171,2606,1829],{"class":181},[171,2608,1832],{"class":229},[171,2610,1835],{"class":225},[171,2612,1826],{"class":229},[171,2614,1840],{"class":181},[171,2616,518],{"class":229},[171,2618,2619,2621,2623],{"class":173,"line":322},[171,2620,1890],{"class":229},[171,2622,252],{"class":225},[171,2624,1895],{"class":229},[171,2626,2627,2629,2631,2633,2635,2637,2639,2641,2643],{"class":173,"line":328},[171,2628,1900],{"class":229},[171,2630,1903],{"class":509},[171,2632,252],{"class":225},[171,2634,1225],{"class":209},[171,2636,276],{"class":229},[171,2638,1912],{"class":509},[171,2640,252],{"class":225},[171,2642,1225],{"class":209},[171,2644,518],{"class":229},[171,2646,2647,2650,2652,2654,2656,2659,2661,2664,2666],{"class":173,"line":337},[171,2648,2649],{"class":229},"    srcs ",[171,2651,252],{"class":225},[171,2653,2226],{"class":229},[171,2655,1991],{"class":209},[171,2657,2658],{"class":229},"(Path(p).resolve()) ",[171,2660,261],{"class":225},[171,2662,2663],{"class":229}," p ",[171,2665,267],{"class":225},[171,2667,2668],{"class":229}," paths]\n",[171,2670,2671],{"class":173,"line":376},[171,2672,243],{"emptyLinePlaceholder":242},[171,2674,2675,2677,2679,2681],{"class":173,"line":393},[171,2676,1927],{"class":225},[171,2678,1930],{"class":229},[171,2680,233],{"class":225},[171,2682,1935],{"class":229},[171,2684,2685],{"class":173,"line":417},[171,2686,2687],{"class":229},"        subprocess.run(\n",[171,2689,2690,2692,2694,2696,2698,2700,2702,2704],{"class":173,"line":438},[171,2691,1950],{"class":229},[171,2693,346],{"class":225},[171,2695,1955],{"class":181},[171,2697,353],{"class":352},[171,2699,1960],{"class":229},[171,2701,359],{"class":352},[171,2703,349],{"class":181},[171,2705,373],{"class":229},[171,2707,2708,2710,2712,2714,2716,2718,2720,2722,2724,2726,2728,2730],{"class":173,"line":444},[171,2709,1971],{"class":181},[171,2711,276],{"class":229},[171,2713,1976],{"class":181},[171,2715,276],{"class":229},[171,2717,1981],{"class":181},[171,2719,276],{"class":229},[171,2721,1986],{"class":181},[171,2723,276],{"class":229},[171,2725,1991],{"class":209},[171,2727,1994],{"class":229},[171,2729,409],{"class":225},[171,2731,2732],{"class":229},"srcs],\n",[171,2734,2735,2737,2739,2741,2743,2745,2747,2749,2751,2753,2755,2758,2761,2763,2765],{"class":173,"line":449},[171,2736,2004],{"class":509},[171,2738,252],{"class":225},[171,2740,1225],{"class":209},[171,2742,276],{"class":229},[171,2744,65],{"class":509},[171,2746,252],{"class":225},[171,2748,1225],{"class":209},[171,2750,276],{"class":229},[171,2752,2021],{"class":509},[171,2754,252],{"class":225},[171,2756,2757],{"class":229},"timeout, ",[171,2759,2760],{"class":509},"check",[171,2762,252],{"class":225},[171,2764,1225],{"class":209},[171,2766,373],{"class":229},[171,2768,2769],{"class":173,"line":460},[171,2770,2031],{"class":229},[171,2772,2773,2775,2778,2780,2783,2785,2787,2789,2791,2794,2796],{"class":173,"line":498},[171,2774,1635],{"class":225},[171,2776,2777],{"class":229}," [out ",[171,2779,482],{"class":225},[171,2781,2782],{"class":229}," (Path(s).stem ",[171,2784,2079],{"class":225},[171,2786,2082],{"class":181},[171,2788,1832],{"class":229},[171,2790,261],{"class":225},[171,2792,2793],{"class":229}," s ",[171,2795,267],{"class":225},[171,2797,2798],{"class":229}," srcs]\n",[10,2800,2801,2804,2805,2809],{},[1335,2802,2803],{},"Cap what you print."," A PDF is a document, not a data dump, and a four-hundred-page export of raw transactions serves nobody. Print the summary and publish the full workbook for anyone who needs the rows — the split described in ",[14,2806,2808],{"href":2807},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002F","publishing Excel reports to cloud storage",". Where a long export genuinely must be printed, restricting the print area to the columns that matter cuts both the page count and the render time.",[10,2811,2812,2813,18],{},"Note also that page-setup properties are lost if a later step rewrites the sheet through pandas, so set them last — after all data is written, alongside the other final-pass work in ",[14,2814,2816],{"href":2815},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002F","protecting and sharing Excel workbooks",[156,2818,2820],{"id":2819},"conclusion","Conclusion",[10,2822,2823,2824,2826,2827,2829,2830,2832,2833,2835],{},"Everything that makes an exported PDF look like a document is a worksheet page-setup property, and LibreOffice carries all of it through the conversion. Set a three-section header and footer with ",[168,2825,154],{}," for page numbers, repeat row 1 with ",[168,2828,137],{}," so every page is labelled, and enable fit-to-width — remembering that ",[168,2831,1329],{}," does nothing without ",[168,2834,1325],{},". Add page breaks at group boundaries with the id one row before the new group, set the print area so stray cells do not add blank pages, and check the resulting page count, because an unexpected number almost always means the fit did not take.",[156,2837,2839],{"id":2838},"frequently-asked-questions","Frequently asked questions",[10,2841,2842,2845,2846,690,2849,2852,2853,276,2856,690,2859,2862],{},[1335,2843,2844],{},"Where do Excel headers and footers actually live?","\nOn the worksheet's page-setup properties, not in any cell. openpyxl exposes them as ",[168,2847,2848],{},"ws.oddHeader",[168,2850,2851],{},"ws.oddFooter"," with ",[168,2854,2855],{},"left",[168,2857,2858],{},"center",[168,2860,2861],{},"right"," sections, and they appear only in print and PDF output.",[10,2864,2865,2868,2869,2871,2872,2874,2875,2878],{},[1335,2866,2867],{},"How do I put a page number in the footer?","\nUse the format codes: ",[168,2870,689],{}," for the current page and ",[168,2873,693],{}," for the total. openpyxl's header and footer sections accept them directly, so ",[168,2876,2877],{},"\"Page &P of &N\""," renders as \"Page 2 of 7\".",[10,2880,2881,2884,2885,2887,2888,2890],{},[1335,2882,2883],{},"Why does my column header only appear on the first page?","\nThe header row repeats only if you set ",[168,2886,137],{},". Setting it to ",[168,2889,2453],{}," makes row 1 print at the top of every page, which is what makes a multi-page table readable.",[10,2892,2893,2896],{},[1335,2894,2895],{},"My table splits awkwardly across pages — what can I do?","\nSet fit-to-width so columns never split, and insert manual page breaks at group boundaries so each section starts on a fresh page. Both are page-setup properties openpyxl can write.",[10,2898,2899,2902],{},[1335,2900,2901],{},"Do these settings survive the conversion to PDF?","\nYes. LibreOffice reads the workbook's page setup, so headers, footers, repeated title rows, margins and page breaks all carry through to the PDF.",[156,2904,2906],{"id":2905},"related","Related",[2908,2909,2910,2917,2923,2930,2937],"ul",{},[2911,2912,2913,2914,2916],"li",{},"Up to the parent: ",[14,2915,17],{"href":16}," — the conversion methods compared.",[2911,2918,2919,2922],{},[14,2920,2921],{"href":194},"Convert an Excel File to PDF with Python"," — the headless LibreOffice recipe in full.",[2911,2924,2925,2929],{},[14,2926,2928],{"href":2927},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-only-selected-sheets-of-a-workbook-to-pdf\u002F","Convert Only Selected Sheets of a Workbook to PDF"," — controlling what ends up in the document.",[2911,2931,2932,2936],{},[14,2933,2935],{"href":2934},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fmerge-cells-and-centre-a-report-title-with-openpyxl\u002F","Merge Cells and Centre a Report Title with openpyxl"," — the on-sheet banner the header complements.",[2911,2938,2939,2942],{},[14,2940,2941],{"href":2807},"Publishing Excel Reports to Cloud Storage"," — delivering the finished PDF.",[2944,2945,2946],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":166,"searchDepth":239,"depth":239,"links":2948},[2949,2950,2951,2952,2953,2954,2955,2956,2957,2958,2959],{"id":158,"depth":239,"text":159},{"id":521,"depth":239,"text":522},{"id":918,"depth":239,"text":919},{"id":1024,"depth":239,"text":1025},{"id":1355,"depth":239,"text":1356},{"id":1760,"depth":239,"text":1761},{"id":2394,"depth":239,"text":2395},{"id":2527,"depth":239,"text":2528},{"id":2819,"depth":239,"text":2820},{"id":2838,"depth":239,"text":2839},{"id":2905,"depth":239,"text":2906},"Make an exported PDF look like a document — set headers and footers with openpyxl, repeat the title row on every page, and control margins and page breaks before converting.","md",[2963,2965,2968,2970,2972],{"q":2844,"a":2964},"On the worksheet's page-setup properties, not in any cell. openpyxl exposes them as ws.oddHeader and ws.oddFooter with left, center and right sections, and they appear only in print and PDF output.",{"q":2867,"a":2966},{"Use the format codes":2967},"ampersand P for the current page and ampersand N for the total. openpyxl's HeaderFooterItem accepts them directly, so \"Page &P of &N\" renders as \"Page 2 of 7\".",{"q":2883,"a":2969},"The header row repeats only if you set print_title_rows. Setting it to \"1:1\" makes row 1 print at the top of every page, which is what makes a multi-page table readable.",{"q":2895,"a":2971},"Set fit-to-width so columns never split, and insert manual page breaks at group boundaries so each section starts on a fresh page. Both are page-setup properties openpyxl can write.",{"q":2901,"a":2973},"Yes. LibreOffice reads the workbook's page setup, so headers, footers, repeated title rows, margins and page breaks all carry through to the PDF.",{},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fadd-headers-footers-and-page-numbers-to-an-excel-pdf",{"title":2977,"description":2978},"Excel PDF Headers, Footers and Page Numbers in Python","Set oddHeader and oddFooter with openpyxl's format codes, repeat header rows, control margins, orientation and page breaks, then export to PDF with headless LibreOffice.","add-headers-footers-and-page-numbers-to-an-excel-pdf","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fadd-headers-footers-and-page-numbers-to-an-excel-pdf\u002Findex","how-to","YovW-5bzT4SAzzUpQ03Ao5T6Y-p0YCR0O0MhFVDNJnk",[2984,2987],{"title":17,"path":2985,"stem":2986,"children":-1},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Findex",{"title":2921,"path":2988,"stem":2989,"children":-1},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python\u002Findex",1786800028995]