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