[{"data":1,"prerenderedAt":1925},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python":3,"surround:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python":1917},{"id":4,"title":5,"body":6,"description":1908,"extension":1909,"meta":1910,"navigation":163,"path":1911,"seo":1912,"stem":1915,"__hash__":1916},"docs\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python\u002Findex.md","Convert an Excel File to PDF with Python",{"type":7,"value":8,"toc":1896},"minimark",[9,33,38,74,77,102,108,112,122,505,516,520,539,1131,1135,1161,1216,1220,1339,1342,1649,1653,1668,1672,1680,1786,1790,1801,1821,1834,1850,1859,1863,1873,1877,1892],[10,11,12,13,18,19,23,24,28,29,32],"p",{},"This is the portable, do-it-once recipe for turning an Excel workbook into a PDF from Python — the practical companion to ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002F","Exporting Excel Reports to PDF",", which compares the methods. The default here is ",[20,21,22],"strong",{},"headless LibreOffice"," driven through ",[25,26,27],"code",{},"subprocess",", because it runs the same on Linux, macOS, and Windows, costs nothing, and renders the real workbook including styles and charts. Remember the hard truth: no pure-pip library converts ",[25,30,31],{},".xlsx"," to PDF faithfully, so the conversion step always shells out to a renderer. You will build a sample workbook, set its page layout so the PDF paginates cleanly, run the conversion safely, and find the output — with a Windows-only Excel alternative at the end.",[34,35,37],"h2",{"id":36},"prerequisites","Prerequisites",[39,40,41,52],"ul",{},[42,43,44,47,48,51],"li",{},[20,45,46],{},"Python 3"," with openpyxl: ",[25,49,50],{},"pip install openpyxl",".",[42,53,54,57,58,61,62,65,66,69,70,73],{},[20,55,56],{},"LibreOffice installed",", with the ",[25,59,60],{},"soffice"," binary reachable. On Debian\u002FUbuntu, ",[25,63,64],{},"sudo apt install libreoffice-calc","; on macOS, ",[25,67,68],{},"brew install --cask libreoffice","; on Windows, the standard installer. LibreOffice is ",[20,71,72],{},"not"," a pip package — it is a separate program your script calls.",[10,75,76],{},"Verify the binary is found before going further:",[78,79,84],"pre",{"className":80,"code":81,"language":82,"meta":83,"style":83},"language-bash shiki shiki-themes github-light github-dark","soffice --version    # or: libreoffice --version\n","bash","",[25,85,86],{"__ignoreMap":83},[87,88,91,94,98],"span",{"class":89,"line":90},"line",1,[87,92,60],{"class":93},"sScJk",[87,95,97],{"class":96},"sj4cs"," --version",[87,99,101],{"class":100},"sJ8bj","    # or: libreoffice --version\n",[10,103,104,105,51],{},"If that prints a version, you are ready. If not, note where LibreOffice installed and add it to your ",[25,106,107],{},"PATH",[34,109,111],{"id":110},"step-1-create-a-sample-workbook-with-clean-page-layout","Step 1 — Create a sample workbook with clean page layout",[10,113,114,115,117,118,121],{},"So the script stands alone, generate a small ",[25,116,31],{},". The important part is the page setup: configure orientation, fit-to-width, and print area ",[20,119,120],{},"before"," converting, or wide tables fragment across pages in the PDF.",[78,123,127],{"className":124,"code":125,"language":126,"meta":83,"style":83},"language-python shiki shiki-themes github-light github-dark","from openpyxl import Workbook\nfrom openpyxl.worksheet.properties import PageSetupProperties\n\nwb = Workbook()\nws = wb.active\nws.title = \"Sales\"\n\nws.append([\"Region\", \"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY Total\", \"YoY %\", \"Owner\"])\nfor i in range(1, 26):\n    ws.append([f\"Branch {i}\", 100 + i, 118 + i, 96 + i, 131 + i,\n               445 + 4 * i, 2.7, \"team-a\"])\n\n# Landscape gives wide tables room.\nws.page_setup.orientation = \"landscape\"\n\n# Scale all columns to ONE page wide; height flows over pages.\nws.page_setup.fitToWidth = 1\nws.page_setup.fitToHeight = 0\nws.sheet_properties.pageSetUpPr = PageSetupProperties(fitToPage=True)\n\n# Only export the data region, and repeat the header on each page.\nws.print_area = \"A1:H26\"\nws.print_title_rows = \"1:1\"\n\nwb.save(\"sales_report.xlsx\")\nprint(\"Wrote sales_report.xlsx\")\n","python",[25,128,129,145,158,165,177,188,200,205,253,282,339,365,370,376,387,392,398,409,420,443,448,454,465,476,481,492],{"__ignoreMap":83},[87,130,131,135,139,142],{"class":89,"line":90},[87,132,134],{"class":133},"szBVR","from",[87,136,138],{"class":137},"sVt8B"," openpyxl ",[87,140,141],{"class":133},"import",[87,143,144],{"class":137}," Workbook\n",[87,146,148,150,153,155],{"class":89,"line":147},2,[87,149,134],{"class":133},[87,151,152],{"class":137}," openpyxl.worksheet.properties ",[87,154,141],{"class":133},[87,156,157],{"class":137}," PageSetupProperties\n",[87,159,161],{"class":89,"line":160},3,[87,162,164],{"emptyLinePlaceholder":163},true,"\n",[87,166,168,171,174],{"class":89,"line":167},4,[87,169,170],{"class":137},"wb ",[87,172,173],{"class":133},"=",[87,175,176],{"class":137}," Workbook()\n",[87,178,180,183,185],{"class":89,"line":179},5,[87,181,182],{"class":137},"ws ",[87,184,173],{"class":133},[87,186,187],{"class":137}," wb.active\n",[87,189,191,194,196],{"class":89,"line":190},6,[87,192,193],{"class":137},"ws.title ",[87,195,173],{"class":133},[87,197,199],{"class":198},"sZZnC"," \"Sales\"\n",[87,201,203],{"class":89,"line":202},7,[87,204,164],{"emptyLinePlaceholder":163},[87,206,208,211,214,217,220,222,225,227,230,232,235,237,240,242,245,247,250],{"class":89,"line":207},8,[87,209,210],{"class":137},"ws.append([",[87,212,213],{"class":198},"\"Region\"",[87,215,216],{"class":137},", ",[87,218,219],{"class":198},"\"Q1\"",[87,221,216],{"class":137},[87,223,224],{"class":198},"\"Q2\"",[87,226,216],{"class":137},[87,228,229],{"class":198},"\"Q3\"",[87,231,216],{"class":137},[87,233,234],{"class":198},"\"Q4\"",[87,236,216],{"class":137},[87,238,239],{"class":198},"\"FY Total\"",[87,241,216],{"class":137},[87,243,244],{"class":198},"\"YoY %\"",[87,246,216],{"class":137},[87,248,249],{"class":198},"\"Owner\"",[87,251,252],{"class":137},"])\n",[87,254,256,259,262,265,268,271,274,276,279],{"class":89,"line":255},9,[87,257,258],{"class":133},"for",[87,260,261],{"class":137}," i ",[87,263,264],{"class":133},"in",[87,266,267],{"class":96}," range",[87,269,270],{"class":137},"(",[87,272,273],{"class":96},"1",[87,275,216],{"class":137},[87,277,278],{"class":96},"26",[87,280,281],{"class":137},"):\n",[87,283,285,288,291,294,297,300,303,306,308,311,314,317,320,322,324,327,329,331,334,336],{"class":89,"line":284},10,[87,286,287],{"class":137},"    ws.append([",[87,289,290],{"class":133},"f",[87,292,293],{"class":198},"\"Branch ",[87,295,296],{"class":96},"{",[87,298,299],{"class":137},"i",[87,301,302],{"class":96},"}",[87,304,305],{"class":198},"\"",[87,307,216],{"class":137},[87,309,310],{"class":96},"100",[87,312,313],{"class":133}," +",[87,315,316],{"class":137}," i, ",[87,318,319],{"class":96},"118",[87,321,313],{"class":133},[87,323,316],{"class":137},[87,325,326],{"class":96},"96",[87,328,313],{"class":133},[87,330,316],{"class":137},[87,332,333],{"class":96},"131",[87,335,313],{"class":133},[87,337,338],{"class":137}," i,\n",[87,340,342,345,347,350,353,355,358,360,363],{"class":89,"line":341},11,[87,343,344],{"class":96},"               445",[87,346,313],{"class":133},[87,348,349],{"class":96}," 4",[87,351,352],{"class":133}," *",[87,354,316],{"class":137},[87,356,357],{"class":96},"2.7",[87,359,216],{"class":137},[87,361,362],{"class":198},"\"team-a\"",[87,364,252],{"class":137},[87,366,368],{"class":89,"line":367},12,[87,369,164],{"emptyLinePlaceholder":163},[87,371,373],{"class":89,"line":372},13,[87,374,375],{"class":100},"# Landscape gives wide tables room.\n",[87,377,379,382,384],{"class":89,"line":378},14,[87,380,381],{"class":137},"ws.page_setup.orientation ",[87,383,173],{"class":133},[87,385,386],{"class":198}," \"landscape\"\n",[87,388,390],{"class":89,"line":389},15,[87,391,164],{"emptyLinePlaceholder":163},[87,393,395],{"class":89,"line":394},16,[87,396,397],{"class":100},"# Scale all columns to ONE page wide; height flows over pages.\n",[87,399,401,404,406],{"class":89,"line":400},17,[87,402,403],{"class":137},"ws.page_setup.fitToWidth ",[87,405,173],{"class":133},[87,407,408],{"class":96}," 1\n",[87,410,412,415,417],{"class":89,"line":411},18,[87,413,414],{"class":137},"ws.page_setup.fitToHeight ",[87,416,173],{"class":133},[87,418,419],{"class":96}," 0\n",[87,421,423,426,428,431,435,437,440],{"class":89,"line":422},19,[87,424,425],{"class":137},"ws.sheet_properties.pageSetUpPr ",[87,427,173],{"class":133},[87,429,430],{"class":137}," PageSetupProperties(",[87,432,434],{"class":433},"s4XuR","fitToPage",[87,436,173],{"class":133},[87,438,439],{"class":96},"True",[87,441,442],{"class":137},")\n",[87,444,446],{"class":89,"line":445},20,[87,447,164],{"emptyLinePlaceholder":163},[87,449,451],{"class":89,"line":450},21,[87,452,453],{"class":100},"# Only export the data region, and repeat the header on each page.\n",[87,455,457,460,462],{"class":89,"line":456},22,[87,458,459],{"class":137},"ws.print_area ",[87,461,173],{"class":133},[87,463,464],{"class":198}," \"A1:H26\"\n",[87,466,468,471,473],{"class":89,"line":467},23,[87,469,470],{"class":137},"ws.print_title_rows ",[87,472,173],{"class":133},[87,474,475],{"class":198}," \"1:1\"\n",[87,477,479],{"class":89,"line":478},24,[87,480,164],{"emptyLinePlaceholder":163},[87,482,484,487,490],{"class":89,"line":483},25,[87,485,486],{"class":137},"wb.save(",[87,488,489],{"class":198},"\"sales_report.xlsx\"",[87,491,442],{"class":137},[87,493,495,498,500,503],{"class":89,"line":494},26,[87,496,497],{"class":96},"print",[87,499,270],{"class":137},[87,501,502],{"class":198},"\"Wrote sales_report.xlsx\"",[87,504,442],{"class":137},[10,506,507,508,511,512,515],{},"The ",[25,509,510],{},"fitToWidth = 1"," line only takes effect because ",[25,513,514],{},"pageSetUpPr=PageSetupProperties(fitToPage=True)"," is set alongside it. Without that flag the renderer ignores the fit and the table spills across two page-widths.",[34,517,519],{"id":518},"step-2-convert-with-libreoffice-via-subprocess","Step 2 — Convert with LibreOffice via subprocess",[10,521,522,523,526,527,530,531,534,535,538],{},"Now call ",[25,524,525],{},"soffice --headless --convert-to pdf",". Resolve the binary with ",[25,528,529],{},"shutil.which",", give the run a ",[25,532,533],{},"timeout",", check ",[25,536,537],{},"returncode",", and confirm the output file actually appeared — a clean exit alone is not proof of success.",[78,540,542],{"className":124,"code":541,"language":126,"meta":83,"style":83},"import shutil\nimport subprocess\nfrom pathlib import Path\n\ndef convert_to_pdf(xlsx_path, out_dir=None, timeout=120):\n    \"\"\"Convert an .xlsx to PDF using headless LibreOffice.\n\n    Requires LibreOffice installed with `soffice` on PATH.\n    Returns the Path of the generated PDF.\n    \"\"\"\n    src = Path(xlsx_path).resolve()\n    if not src.is_file():\n        raise FileNotFoundError(f\"Input not found: {src}\")\n\n    soffice = shutil.which(\"soffice\") or shutil.which(\"libreoffice\")\n    if soffice is None:\n        raise RuntimeError(\n            \"LibreOffice not found. Install it and ensure 'soffice' is on PATH.\")\n\n    out_dir = Path(out_dir or src.parent).resolve()\n    out_dir.mkdir(parents=True, exist_ok=True)\n\n    try:\n        result = subprocess.run(\n            [soffice, \"--headless\", \"--convert-to\", \"pdf\",\n             \"--outdir\", str(out_dir), str(src)],\n            capture_output=True, text=True, timeout=timeout,\n        )\n    except subprocess.TimeoutExpired:\n        raise RuntimeError(\n            f\"LibreOffice timed out after {timeout}s converting {src.name}\")\n\n    if result.returncode != 0:\n        raise RuntimeError(\n            f\"LibreOffice exited {result.returncode}: {result.stderr.strip()}\")\n\n    pdf_path = out_dir \u002F (src.stem + \".pdf\")\n    if not pdf_path.is_file():\n        raise RuntimeError(\n            f\"Exit 0 but no PDF at {pdf_path}. stdout: {result.stdout.strip()}\")\n    return pdf_path\n\nif __name__ == \"__main__\":\n    pdf = convert_to_pdf(\"sales_report.xlsx\")\n    print(\"Created\", pdf, f\"({pdf.stat().st_size} bytes)\")\n",[25,543,544,551,558,570,574,600,605,609,614,619,624,634,645,671,675,701,717,727,734,738,753,776,780,787,797,818,836,864,870,879,888,917,922,938,947,976,981,1006,1016,1025,1054,1063,1068,1085,1100],{"__ignoreMap":83},[87,545,546,548],{"class":89,"line":90},[87,547,141],{"class":133},[87,549,550],{"class":137}," shutil\n",[87,552,553,555],{"class":89,"line":147},[87,554,141],{"class":133},[87,556,557],{"class":137}," subprocess\n",[87,559,560,562,565,567],{"class":89,"line":160},[87,561,134],{"class":133},[87,563,564],{"class":137}," pathlib ",[87,566,141],{"class":133},[87,568,569],{"class":137}," Path\n",[87,571,572],{"class":89,"line":167},[87,573,164],{"emptyLinePlaceholder":163},[87,575,576,579,582,585,587,590,593,595,598],{"class":89,"line":179},[87,577,578],{"class":133},"def",[87,580,581],{"class":93}," convert_to_pdf",[87,583,584],{"class":137},"(xlsx_path, out_dir",[87,586,173],{"class":133},[87,588,589],{"class":96},"None",[87,591,592],{"class":137},", timeout",[87,594,173],{"class":133},[87,596,597],{"class":96},"120",[87,599,281],{"class":137},[87,601,602],{"class":89,"line":190},[87,603,604],{"class":198},"    \"\"\"Convert an .xlsx to PDF using headless LibreOffice.\n",[87,606,607],{"class":89,"line":202},[87,608,164],{"emptyLinePlaceholder":163},[87,610,611],{"class":89,"line":207},[87,612,613],{"class":198},"    Requires LibreOffice installed with `soffice` on PATH.\n",[87,615,616],{"class":89,"line":255},[87,617,618],{"class":198},"    Returns the Path of the generated PDF.\n",[87,620,621],{"class":89,"line":284},[87,622,623],{"class":198},"    \"\"\"\n",[87,625,626,629,631],{"class":89,"line":341},[87,627,628],{"class":137},"    src ",[87,630,173],{"class":133},[87,632,633],{"class":137}," Path(xlsx_path).resolve()\n",[87,635,636,639,642],{"class":89,"line":367},[87,637,638],{"class":133},"    if",[87,640,641],{"class":133}," not",[87,643,644],{"class":137}," src.is_file():\n",[87,646,647,650,653,655,657,660,662,665,667,669],{"class":89,"line":372},[87,648,649],{"class":133},"        raise",[87,651,652],{"class":96}," FileNotFoundError",[87,654,270],{"class":137},[87,656,290],{"class":133},[87,658,659],{"class":198},"\"Input not found: ",[87,661,296],{"class":96},[87,663,664],{"class":137},"src",[87,666,302],{"class":96},[87,668,305],{"class":198},[87,670,442],{"class":137},[87,672,673],{"class":89,"line":378},[87,674,164],{"emptyLinePlaceholder":163},[87,676,677,680,682,685,688,691,694,696,699],{"class":89,"line":389},[87,678,679],{"class":137},"    soffice ",[87,681,173],{"class":133},[87,683,684],{"class":137}," shutil.which(",[87,686,687],{"class":198},"\"soffice\"",[87,689,690],{"class":137},") ",[87,692,693],{"class":133},"or",[87,695,684],{"class":137},[87,697,698],{"class":198},"\"libreoffice\"",[87,700,442],{"class":137},[87,702,703,705,708,711,714],{"class":89,"line":394},[87,704,638],{"class":133},[87,706,707],{"class":137}," soffice ",[87,709,710],{"class":133},"is",[87,712,713],{"class":96}," None",[87,715,716],{"class":137},":\n",[87,718,719,721,724],{"class":89,"line":400},[87,720,649],{"class":133},[87,722,723],{"class":96}," RuntimeError",[87,725,726],{"class":137},"(\n",[87,728,729,732],{"class":89,"line":411},[87,730,731],{"class":198},"            \"LibreOffice not found. Install it and ensure 'soffice' is on PATH.\"",[87,733,442],{"class":137},[87,735,736],{"class":89,"line":422},[87,737,164],{"emptyLinePlaceholder":163},[87,739,740,743,745,748,750],{"class":89,"line":445},[87,741,742],{"class":137},"    out_dir ",[87,744,173],{"class":133},[87,746,747],{"class":137}," Path(out_dir ",[87,749,693],{"class":133},[87,751,752],{"class":137}," src.parent).resolve()\n",[87,754,755,758,761,763,765,767,770,772,774],{"class":89,"line":450},[87,756,757],{"class":137},"    out_dir.mkdir(",[87,759,760],{"class":433},"parents",[87,762,173],{"class":133},[87,764,439],{"class":96},[87,766,216],{"class":137},[87,768,769],{"class":433},"exist_ok",[87,771,173],{"class":133},[87,773,439],{"class":96},[87,775,442],{"class":137},[87,777,778],{"class":89,"line":456},[87,779,164],{"emptyLinePlaceholder":163},[87,781,782,785],{"class":89,"line":467},[87,783,784],{"class":133},"    try",[87,786,716],{"class":137},[87,788,789,792,794],{"class":89,"line":478},[87,790,791],{"class":137},"        result ",[87,793,173],{"class":133},[87,795,796],{"class":137}," subprocess.run(\n",[87,798,799,802,805,807,810,812,815],{"class":89,"line":483},[87,800,801],{"class":137},"            [soffice, ",[87,803,804],{"class":198},"\"--headless\"",[87,806,216],{"class":137},[87,808,809],{"class":198},"\"--convert-to\"",[87,811,216],{"class":137},[87,813,814],{"class":198},"\"pdf\"",[87,816,817],{"class":137},",\n",[87,819,820,823,825,828,831,833],{"class":89,"line":494},[87,821,822],{"class":198},"             \"--outdir\"",[87,824,216],{"class":137},[87,826,827],{"class":96},"str",[87,829,830],{"class":137},"(out_dir), ",[87,832,827],{"class":96},[87,834,835],{"class":137},"(src)],\n",[87,837,839,842,844,846,848,851,853,855,857,859,861],{"class":89,"line":838},27,[87,840,841],{"class":433},"            capture_output",[87,843,173],{"class":133},[87,845,439],{"class":96},[87,847,216],{"class":137},[87,849,850],{"class":433},"text",[87,852,173],{"class":133},[87,854,439],{"class":96},[87,856,216],{"class":137},[87,858,533],{"class":433},[87,860,173],{"class":133},[87,862,863],{"class":137},"timeout,\n",[87,865,867],{"class":89,"line":866},28,[87,868,869],{"class":137},"        )\n",[87,871,873,876],{"class":89,"line":872},29,[87,874,875],{"class":133},"    except",[87,877,878],{"class":137}," subprocess.TimeoutExpired:\n",[87,880,882,884,886],{"class":89,"line":881},30,[87,883,649],{"class":133},[87,885,723],{"class":96},[87,887,726],{"class":137},[87,889,891,894,897,899,901,903,906,908,911,913,915],{"class":89,"line":890},31,[87,892,893],{"class":133},"            f",[87,895,896],{"class":198},"\"LibreOffice timed out after ",[87,898,296],{"class":96},[87,900,533],{"class":137},[87,902,302],{"class":96},[87,904,905],{"class":198},"s converting ",[87,907,296],{"class":96},[87,909,910],{"class":137},"src.name",[87,912,302],{"class":96},[87,914,305],{"class":198},[87,916,442],{"class":137},[87,918,920],{"class":89,"line":919},32,[87,921,164],{"emptyLinePlaceholder":163},[87,923,925,927,930,933,936],{"class":89,"line":924},33,[87,926,638],{"class":133},[87,928,929],{"class":137}," result.returncode ",[87,931,932],{"class":133},"!=",[87,934,935],{"class":96}," 0",[87,937,716],{"class":137},[87,939,941,943,945],{"class":89,"line":940},34,[87,942,649],{"class":133},[87,944,723],{"class":96},[87,946,726],{"class":137},[87,948,950,952,955,957,960,962,965,967,970,972,974],{"class":89,"line":949},35,[87,951,893],{"class":133},[87,953,954],{"class":198},"\"LibreOffice exited ",[87,956,296],{"class":96},[87,958,959],{"class":137},"result.returncode",[87,961,302],{"class":96},[87,963,964],{"class":198},": ",[87,966,296],{"class":96},[87,968,969],{"class":137},"result.stderr.strip()",[87,971,302],{"class":96},[87,973,305],{"class":198},[87,975,442],{"class":137},[87,977,979],{"class":89,"line":978},36,[87,980,164],{"emptyLinePlaceholder":163},[87,982,984,987,989,992,995,998,1001,1004],{"class":89,"line":983},37,[87,985,986],{"class":137},"    pdf_path ",[87,988,173],{"class":133},[87,990,991],{"class":137}," out_dir ",[87,993,994],{"class":133},"\u002F",[87,996,997],{"class":137}," (src.stem ",[87,999,1000],{"class":133},"+",[87,1002,1003],{"class":198}," \".pdf\"",[87,1005,442],{"class":137},[87,1007,1009,1011,1013],{"class":89,"line":1008},38,[87,1010,638],{"class":133},[87,1012,641],{"class":133},[87,1014,1015],{"class":137}," pdf_path.is_file():\n",[87,1017,1019,1021,1023],{"class":89,"line":1018},39,[87,1020,649],{"class":133},[87,1022,723],{"class":96},[87,1024,726],{"class":137},[87,1026,1028,1030,1033,1035,1038,1040,1043,1045,1048,1050,1052],{"class":89,"line":1027},40,[87,1029,893],{"class":133},[87,1031,1032],{"class":198},"\"Exit 0 but no PDF at ",[87,1034,296],{"class":96},[87,1036,1037],{"class":137},"pdf_path",[87,1039,302],{"class":96},[87,1041,1042],{"class":198},". stdout: ",[87,1044,296],{"class":96},[87,1046,1047],{"class":137},"result.stdout.strip()",[87,1049,302],{"class":96},[87,1051,305],{"class":198},[87,1053,442],{"class":137},[87,1055,1057,1060],{"class":89,"line":1056},41,[87,1058,1059],{"class":133},"    return",[87,1061,1062],{"class":137}," pdf_path\n",[87,1064,1066],{"class":89,"line":1065},42,[87,1067,164],{"emptyLinePlaceholder":163},[87,1069,1071,1074,1077,1080,1083],{"class":89,"line":1070},43,[87,1072,1073],{"class":133},"if",[87,1075,1076],{"class":96}," __name__",[87,1078,1079],{"class":133}," ==",[87,1081,1082],{"class":198}," \"__main__\"",[87,1084,716],{"class":137},[87,1086,1088,1091,1093,1096,1098],{"class":89,"line":1087},44,[87,1089,1090],{"class":137},"    pdf ",[87,1092,173],{"class":133},[87,1094,1095],{"class":137}," convert_to_pdf(",[87,1097,489],{"class":198},[87,1099,442],{"class":137},[87,1101,1103,1106,1108,1111,1114,1116,1119,1121,1124,1126,1129],{"class":89,"line":1102},45,[87,1104,1105],{"class":96},"    print",[87,1107,270],{"class":137},[87,1109,1110],{"class":198},"\"Created\"",[87,1112,1113],{"class":137},", pdf, ",[87,1115,290],{"class":133},[87,1117,1118],{"class":198},"\"(",[87,1120,296],{"class":96},[87,1122,1123],{"class":137},"pdf.stat().st_size",[87,1125,302],{"class":96},[87,1127,1128],{"class":198}," bytes)\"",[87,1130,442],{"class":137},[34,1132,1134],{"id":1133},"step-3-locate-and-verify-the-output","Step 3 — Locate and verify the output",[10,1136,1137,1138,1141,1142,1145,1146,1149,1150,1153,1154,1157,1158,1160],{},"LibreOffice names the PDF after the input stem and drops it in ",[25,1139,1140],{},"--outdir",". So ",[25,1143,1144],{},"sales_report.xlsx"," becomes ",[25,1147,1148],{},"sales_report.pdf"," in the same folder unless you pass a different ",[25,1151,1152],{},"out_dir",". The function returns that ",[25,1155,1156],{},"Path","; checking ",[25,1159,1123],{}," is a cheap sanity test that the file has real content before you email or archive it.",[78,1162,1164],{"className":124,"code":1163,"language":126,"meta":83,"style":83},"pdf = convert_to_pdf(\"sales_report.xlsx\", out_dir=\"output\")\nprint(\"PDF ready:\", pdf)          # output\u002Fsales_report.pdf\nassert pdf.stat().st_size > 0\n",[25,1165,1166,1188,1203],{"__ignoreMap":83},[87,1167,1168,1171,1173,1175,1177,1179,1181,1183,1186],{"class":89,"line":90},[87,1169,1170],{"class":137},"pdf ",[87,1172,173],{"class":133},[87,1174,1095],{"class":137},[87,1176,489],{"class":198},[87,1178,216],{"class":137},[87,1180,1152],{"class":433},[87,1182,173],{"class":133},[87,1184,1185],{"class":198},"\"output\"",[87,1187,442],{"class":137},[87,1189,1190,1192,1194,1197,1200],{"class":89,"line":147},[87,1191,497],{"class":96},[87,1193,270],{"class":137},[87,1195,1196],{"class":198},"\"PDF ready:\"",[87,1198,1199],{"class":137},", pdf)          ",[87,1201,1202],{"class":100},"# output\u002Fsales_report.pdf\n",[87,1204,1205,1208,1211,1214],{"class":89,"line":160},[87,1206,1207],{"class":133},"assert",[87,1209,1210],{"class":137}," pdf.stat().st_size ",[87,1212,1213],{"class":133},">",[87,1215,419],{"class":96},[34,1217,1219],{"id":1218},"pitfalls-and-fixes","Pitfalls and fixes",[1221,1222,1223,1239],"table",{},[1224,1225,1226],"thead",{},[1227,1228,1229,1233,1236],"tr",{},[1230,1231,1232],"th",{},"Symptom",[1230,1234,1235],{},"Cause",[1230,1237,1238],{},"Fix",[1240,1241,1242,1261,1276,1296,1313,1328],"tbody",{},[1227,1243,1244,1250,1255],{},[1245,1246,1247],"td",{},[25,1248,1249],{},"RuntimeError: LibreOffice not found",[1245,1251,1252,1254],{},[25,1253,60],{}," not on the script's PATH (common under cron)",[1245,1256,1257,1258,1260],{},"Resolve with ",[25,1259,529],{},"; add the install dir to PATH, or pass the absolute binary path.",[1227,1262,1263,1266,1269],{},[1245,1264,1265],{},"Conversion hangs or produces no file",[1245,1267,1268],{},"Another LibreOffice instance holds the user profile lock",[1245,1270,1271,1272,1275],{},"Pass ",[25,1273,1274],{},"-env:UserInstallation=file:\u002F\u002F\u002Ftmp\u002Flo_profile_xyz"," to use a separate, throwaway profile dir per run.",[1227,1277,1278,1281,1284],{},[1245,1279,1280],{},"Columns split across two page-widths",[1245,1282,1283],{},"Sheet not fit to page before converting",[1245,1285,1286,1287,1290,1291,1290,1294,51],{},"Set ",[25,1288,1289],{},"ws.page_setup.fitToWidth = 1"," ",[20,1292,1293],{},"and",[25,1295,514],{},[1227,1297,1298,1304,1307],{},[1245,1299,1300,1303],{},[25,1301,1302],{},"TimeoutExpired"," raised",[1245,1305,1306],{},"Large workbook, or a hung headless process",[1245,1308,1309,1310,1312],{},"Raise ",[25,1311,533],{},", or kill and retry once; never let it block a scheduled job forever.",[1227,1314,1315,1318,1321],{},[1245,1316,1317],{},"Garbled \u002F missing text on a server",[1245,1319,1320],{},"Fonts used by the report are not installed",[1245,1322,1323,1324,1327],{},"Install the needed font packages on the headless box (e.g. the relevant ",[25,1325,1326],{},"fonts-*"," packages on Linux).",[1227,1329,1330,1333,1336],{},[1245,1331,1332],{},"Exit 0 but empty\u002Fblank PDF",[1245,1334,1335],{},"Corrupt or zero-byte input workbook",[1245,1337,1338],{},"Validate the input exists and is non-empty before converting.",[10,1340,1341],{},"The locked-profile case is the most common production surprise. The fix is one extra argument that points LibreOffice at a fresh profile, so headless runs never collide with each other or with a desktop instance:",[78,1343,1345],{"className":124,"code":1344,"language":126,"meta":83,"style":83},"import tempfile, shutil, subprocess\nfrom pathlib import Path\n\ndef convert_isolated(xlsx_path, out_dir=\"output\", timeout=120):\n    \"\"\"LibreOffice conversion with a throwaway profile dir — safe under cron.\"\"\"\n    soffice = shutil.which(\"soffice\") or shutil.which(\"libreoffice\")\n    if soffice is None:\n        raise RuntimeError(\"install LibreOffice; 'soffice' not on PATH\")\n    src = Path(xlsx_path).resolve()\n    out = Path(out_dir).resolve()\n    out.mkdir(parents=True, exist_ok=True)\n    with tempfile.TemporaryDirectory() as profile:\n        r = subprocess.run(\n            [soffice, f\"-env:UserInstallation=file:\u002F\u002F{profile}\",\n             \"--headless\", \"--convert-to\", \"pdf\",\n             \"--outdir\", str(out), str(src)],\n            capture_output=True, text=True, timeout=timeout,\n        )\n    if r.returncode != 0:\n        raise RuntimeError(r.stderr.strip())\n    pdf = out \u002F (src.stem + \".pdf\")\n    if not pdf.is_file():\n        raise RuntimeError(\"no PDF produced\")\n    return pdf\n",[25,1346,1347,1354,1364,1368,1389,1394,1414,1426,1439,1447,1457,1478,1492,1501,1521,1536,1551,1575,1579,1592,1601,1620,1629,1642],{"__ignoreMap":83},[87,1348,1349,1351],{"class":89,"line":90},[87,1350,141],{"class":133},[87,1352,1353],{"class":137}," tempfile, shutil, subprocess\n",[87,1355,1356,1358,1360,1362],{"class":89,"line":147},[87,1357,134],{"class":133},[87,1359,564],{"class":137},[87,1361,141],{"class":133},[87,1363,569],{"class":137},[87,1365,1366],{"class":89,"line":160},[87,1367,164],{"emptyLinePlaceholder":163},[87,1369,1370,1372,1375,1377,1379,1381,1383,1385,1387],{"class":89,"line":167},[87,1371,578],{"class":133},[87,1373,1374],{"class":93}," convert_isolated",[87,1376,584],{"class":137},[87,1378,173],{"class":133},[87,1380,1185],{"class":198},[87,1382,592],{"class":137},[87,1384,173],{"class":133},[87,1386,597],{"class":96},[87,1388,281],{"class":137},[87,1390,1391],{"class":89,"line":179},[87,1392,1393],{"class":198},"    \"\"\"LibreOffice conversion with a throwaway profile dir — safe under cron.\"\"\"\n",[87,1395,1396,1398,1400,1402,1404,1406,1408,1410,1412],{"class":89,"line":190},[87,1397,679],{"class":137},[87,1399,173],{"class":133},[87,1401,684],{"class":137},[87,1403,687],{"class":198},[87,1405,690],{"class":137},[87,1407,693],{"class":133},[87,1409,684],{"class":137},[87,1411,698],{"class":198},[87,1413,442],{"class":137},[87,1415,1416,1418,1420,1422,1424],{"class":89,"line":202},[87,1417,638],{"class":133},[87,1419,707],{"class":137},[87,1421,710],{"class":133},[87,1423,713],{"class":96},[87,1425,716],{"class":137},[87,1427,1428,1430,1432,1434,1437],{"class":89,"line":207},[87,1429,649],{"class":133},[87,1431,723],{"class":96},[87,1433,270],{"class":137},[87,1435,1436],{"class":198},"\"install LibreOffice; 'soffice' not on PATH\"",[87,1438,442],{"class":137},[87,1440,1441,1443,1445],{"class":89,"line":255},[87,1442,628],{"class":137},[87,1444,173],{"class":133},[87,1446,633],{"class":137},[87,1448,1449,1452,1454],{"class":89,"line":284},[87,1450,1451],{"class":137},"    out ",[87,1453,173],{"class":133},[87,1455,1456],{"class":137}," Path(out_dir).resolve()\n",[87,1458,1459,1462,1464,1466,1468,1470,1472,1474,1476],{"class":89,"line":341},[87,1460,1461],{"class":137},"    out.mkdir(",[87,1463,760],{"class":433},[87,1465,173],{"class":133},[87,1467,439],{"class":96},[87,1469,216],{"class":137},[87,1471,769],{"class":433},[87,1473,173],{"class":133},[87,1475,439],{"class":96},[87,1477,442],{"class":137},[87,1479,1480,1483,1486,1489],{"class":89,"line":367},[87,1481,1482],{"class":133},"    with",[87,1484,1485],{"class":137}," tempfile.TemporaryDirectory() ",[87,1487,1488],{"class":133},"as",[87,1490,1491],{"class":137}," profile:\n",[87,1493,1494,1497,1499],{"class":89,"line":372},[87,1495,1496],{"class":137},"        r ",[87,1498,173],{"class":133},[87,1500,796],{"class":137},[87,1502,1503,1505,1507,1510,1512,1515,1517,1519],{"class":89,"line":378},[87,1504,801],{"class":137},[87,1506,290],{"class":133},[87,1508,1509],{"class":198},"\"-env:UserInstallation=file:\u002F\u002F",[87,1511,296],{"class":96},[87,1513,1514],{"class":137},"profile",[87,1516,302],{"class":96},[87,1518,305],{"class":198},[87,1520,817],{"class":137},[87,1522,1523,1526,1528,1530,1532,1534],{"class":89,"line":389},[87,1524,1525],{"class":198},"             \"--headless\"",[87,1527,216],{"class":137},[87,1529,809],{"class":198},[87,1531,216],{"class":137},[87,1533,814],{"class":198},[87,1535,817],{"class":137},[87,1537,1538,1540,1542,1544,1547,1549],{"class":89,"line":394},[87,1539,822],{"class":198},[87,1541,216],{"class":137},[87,1543,827],{"class":96},[87,1545,1546],{"class":137},"(out), ",[87,1548,827],{"class":96},[87,1550,835],{"class":137},[87,1552,1553,1555,1557,1559,1561,1563,1565,1567,1569,1571,1573],{"class":89,"line":400},[87,1554,841],{"class":433},[87,1556,173],{"class":133},[87,1558,439],{"class":96},[87,1560,216],{"class":137},[87,1562,850],{"class":433},[87,1564,173],{"class":133},[87,1566,439],{"class":96},[87,1568,216],{"class":137},[87,1570,533],{"class":433},[87,1572,173],{"class":133},[87,1574,863],{"class":137},[87,1576,1577],{"class":89,"line":411},[87,1578,869],{"class":137},[87,1580,1581,1583,1586,1588,1590],{"class":89,"line":422},[87,1582,638],{"class":133},[87,1584,1585],{"class":137}," r.returncode ",[87,1587,932],{"class":133},[87,1589,935],{"class":96},[87,1591,716],{"class":137},[87,1593,1594,1596,1598],{"class":89,"line":445},[87,1595,649],{"class":133},[87,1597,723],{"class":96},[87,1599,1600],{"class":137},"(r.stderr.strip())\n",[87,1602,1603,1605,1607,1610,1612,1614,1616,1618],{"class":89,"line":450},[87,1604,1090],{"class":137},[87,1606,173],{"class":133},[87,1608,1609],{"class":137}," out ",[87,1611,994],{"class":133},[87,1613,997],{"class":137},[87,1615,1000],{"class":133},[87,1617,1003],{"class":198},[87,1619,442],{"class":137},[87,1621,1622,1624,1626],{"class":89,"line":456},[87,1623,638],{"class":133},[87,1625,641],{"class":133},[87,1627,1628],{"class":137}," pdf.is_file():\n",[87,1630,1631,1633,1635,1637,1640],{"class":89,"line":467},[87,1632,649],{"class":133},[87,1634,723],{"class":96},[87,1636,270],{"class":137},[87,1638,1639],{"class":198},"\"no PDF produced\"",[87,1641,442],{"class":137},[87,1643,1644,1646],{"class":89,"line":478},[87,1645,1059],{"class":133},[87,1647,1648],{"class":137}," pdf\n",[34,1650,1652],{"id":1651},"a-note-on-running-headless-on-a-server","A note on running headless on a server",[10,1654,1655,1656,1659,1660,1663,1664,1667],{},"On a server with no display the ",[25,1657,1658],{},"--headless"," flag is enough — you do not need ",[25,1661,1662],{},"xvfb"," for ",[25,1665,1666],{},"--convert-to",". What you do need is the fonts your reports use installed system-wide, and an isolated profile per run as shown above. With those two things in place the same code that works on your laptop works under cron.",[34,1669,1671],{"id":1670},"short-windows-only-alternative","Short Windows-only alternative",[10,1673,1674,1675,1679],{},"If you are on Windows with Excel installed and want pixel-perfect output, let Excel render it via ",[14,1676,1678],{"href":1677},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002F","xlwings",". This only works where Excel is licensed and installed — not on a Linux server.",[78,1681,1683],{"className":124,"code":1682,"language":126,"meta":83,"style":83},"# Windows + Microsoft Excel only.  pip install xlwings\nimport xlwings as xw\n\ndef convert_with_excel(xlsx_path, pdf_path):\n    app = xw.App(visible=False)\n    try:\n        wb = app.books.open(xlsx_path)\n        wb.to_pdf(pdf_path)      # wraps Excel's ExportAsFixedFormat\n        wb.close()\n    finally:\n        app.quit()\n\n# convert_with_excel(\"sales_report.xlsx\", \"sales_report.pdf\")\n",[25,1684,1685,1690,1702,1706,1716,1736,1742,1752,1760,1765,1772,1777,1781],{"__ignoreMap":83},[87,1686,1687],{"class":89,"line":90},[87,1688,1689],{"class":100},"# Windows + Microsoft Excel only.  pip install xlwings\n",[87,1691,1692,1694,1697,1699],{"class":89,"line":147},[87,1693,141],{"class":133},[87,1695,1696],{"class":137}," xlwings ",[87,1698,1488],{"class":133},[87,1700,1701],{"class":137}," xw\n",[87,1703,1704],{"class":89,"line":160},[87,1705,164],{"emptyLinePlaceholder":163},[87,1707,1708,1710,1713],{"class":89,"line":167},[87,1709,578],{"class":133},[87,1711,1712],{"class":93}," convert_with_excel",[87,1714,1715],{"class":137},"(xlsx_path, pdf_path):\n",[87,1717,1718,1721,1723,1726,1729,1731,1734],{"class":89,"line":179},[87,1719,1720],{"class":137},"    app ",[87,1722,173],{"class":133},[87,1724,1725],{"class":137}," xw.App(",[87,1727,1728],{"class":433},"visible",[87,1730,173],{"class":133},[87,1732,1733],{"class":96},"False",[87,1735,442],{"class":137},[87,1737,1738,1740],{"class":89,"line":190},[87,1739,784],{"class":133},[87,1741,716],{"class":137},[87,1743,1744,1747,1749],{"class":89,"line":202},[87,1745,1746],{"class":137},"        wb ",[87,1748,173],{"class":133},[87,1750,1751],{"class":137}," app.books.open(xlsx_path)\n",[87,1753,1754,1757],{"class":89,"line":207},[87,1755,1756],{"class":137},"        wb.to_pdf(pdf_path)      ",[87,1758,1759],{"class":100},"# wraps Excel's ExportAsFixedFormat\n",[87,1761,1762],{"class":89,"line":255},[87,1763,1764],{"class":137},"        wb.close()\n",[87,1766,1767,1770],{"class":89,"line":284},[87,1768,1769],{"class":133},"    finally",[87,1771,716],{"class":137},[87,1773,1774],{"class":89,"line":341},[87,1775,1776],{"class":137},"        app.quit()\n",[87,1778,1779],{"class":89,"line":367},[87,1780,164],{"emptyLinePlaceholder":163},[87,1782,1783],{"class":89,"line":372},[87,1784,1785],{"class":100},"# convert_with_excel(\"sales_report.xlsx\", \"sales_report.pdf\")\n",[34,1787,1789],{"id":1788},"frequently-asked-questions","Frequently asked questions",[10,1791,1792,1795,1796,1798,1799,51],{},[20,1793,1794],{},"Do I have to install LibreOffice — can't pip do it?","\nYou have to install LibreOffice (or use Excel\u002Freportlab). No pip package renders an ",[25,1797,31],{}," to PDF; the LibreOffice approach shells out to the installed program through ",[25,1800,27],{},[10,1802,1803,1806,1807,1809,1810,1813,1814,1817,1818,1820],{},[20,1804,1805],{},"Where does the PDF end up?","\nIn the directory passed as ",[25,1808,1140],{},", named after the input file's stem: ",[25,1811,1812],{},"report.xlsx"," to ",[25,1815,1816],{},"report.pdf",". The helper returns that exact ",[25,1819,1156],{}," so you do not have to guess.",[10,1822,1823,1829,1830,1833],{},[20,1824,1825,1826,1828],{},"Why check ",[25,1827,537],{}," and the output file when there is a return code?","\nLibreOffice can exit ",[25,1831,1832],{},"0"," while writing nothing on certain malformed inputs. Verifying the file exists and is non-empty turns a silent failure into a clear error your job can act on.",[10,1835,1836,1839,1840,1842,1843,1845,1846,1849],{},[20,1837,1838],{},"The conversion works locally but not under cron — what changed?","\nThe cron environment has a minimal PATH and may collide with a running LibreOffice. Resolve ",[25,1841,60],{}," with ",[25,1844,529],{},", and run each conversion with its own ",[25,1847,1848],{},"-env:UserInstallation"," profile directory.",[10,1851,1852,1855,1856,1858],{},[20,1853,1854],{},"Can I convert several workbooks at once?","\nYes — call the function in a loop, or pass multiple file arguments to one ",[25,1857,60],{}," command. For volume, a fresh profile per batch keeps runs from interfering with each other.",[34,1860,1862],{"id":1861},"conclusion","Conclusion",[10,1864,1865,1866,1869,1870,1872],{},"Converting Excel to PDF in Python is two parts: prepare the page layout with openpyxl so the output paginates well, then shell out to a real renderer. Headless LibreOffice through ",[25,1867,1868],{},"subprocess.run"," — with a resolved binary, a timeout, a ",[25,1871,537],{}," check, an output-file check, and an isolated profile — is the portable default that works from your laptop to a cron job. On Windows with Excel, the COM path via xlwings gives pixel-perfect results when you need them.",[34,1874,1876],{"id":1875},"where-to-go-next","Where to go next",[10,1878,1879,1880,1882,1883,1887,1888,51],{},"Step back to the overview and method comparison in ",[14,1881,17],{"href":16},". Then wire this into a pipeline: send the finished PDF with ",[14,1884,1886],{"href":1885},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002F","Emailing Excel Reports with smtplib",", and run the whole generate-convert-send job unattended with ",[14,1889,1891],{"href":1890},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002F","Scheduling Python Excel Scripts with Cron",[1893,1894,1895],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":83,"searchDepth":147,"depth":147,"links":1897},[1898,1899,1900,1901,1902,1903,1904,1905,1906,1907],{"id":36,"depth":147,"text":37},{"id":110,"depth":147,"text":111},{"id":518,"depth":147,"text":519},{"id":1133,"depth":147,"text":1134},{"id":1218,"depth":147,"text":1219},{"id":1651,"depth":147,"text":1652},{"id":1670,"depth":147,"text":1671},{"id":1788,"depth":147,"text":1789},{"id":1861,"depth":147,"text":1862},{"id":1875,"depth":147,"text":1876},"Convert .xlsx to PDF in Python with LibreOffice headless and subprocess: set page layout via openpyxl, run soffice safely with a timeout, plus a Windows COM path.","md",{},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python",{"title":1913,"description":1914},"Convert Excel to PDF in Python (LibreOffice)","Step-by-step: create an .xlsx, set print layout with openpyxl, convert to PDF via headless LibreOffice and subprocess.run, handle errors, and a Windows-Excel option.","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python\u002Findex","evyVT7fQf_L4TDprqN_QpvW_Rz1E9WnlGZh_Ztaz7Zk",[1918,1921],{"title":17,"path":1919,"stem":1920,"children":-1},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Findex",{"title":1922,"path":1923,"stem":1924,"children":-1},"Generating Excel Reports from Templates","\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Findex",1781773160974]