[{"data":1,"prerenderedAt":2105},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf":3,"surround:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf":2097},{"id":4,"title":5,"body":6,"description":2088,"extension":2089,"meta":2090,"navigation":209,"path":2091,"seo":2092,"stem":2095,"__hash__":2096},"docs\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Findex.md","Exporting Excel Reports to PDF",{"type":7,"value":8,"toc":2077},"minimark",[9,33,38,41,139,147,151,163,573,594,598,613,1086,1104,1108,1116,1229,1240,1244,1247,1633,1636,1640,1646,1965,1972,1976,1985,1991,2014,2033,2039,2043,2046,2050,2073],[10,11,12,13,22,23,26,27,32],"p",{},"PDF is the format people actually open. It renders identically on every machine, it cannot be accidentally edited, and it prints predictably — which is why the final step of most reporting jobs is converting the workbook to PDF before it is emailed or filed. The catch is that ",[14,15,16,17,21],"strong",{},"no pure-pip Python library renders an ",[18,19,20],"code",{},".xlsx"," to PDF faithfully."," openpyxl writes spreadsheets but cannot export them; pandas reads and writes data, not page layout. Anyone who tells you ",[18,24,25],{},"df.to_pdf()"," exists is wrong. This page lays out the three approaches that genuinely work, when to reach for each, and the openpyxl page-setup step that makes the difference between a clean one-page report and a mess that spills columns across six sheets. It is the export stage of the broader ",[28,29,31],"a",{"href":30},"\u002Fautomating-reporting-workflows\u002F","Automating Reporting Workflows"," pipeline.",[34,35,37],"h2",{"id":36},"the-three-real-options","The three real options",[10,39,40],{},"There is no single best method — the right one depends on your platform and how much fidelity you need.",[42,43,44,66],"table",{},[45,46,47],"thead",{},[48,49,50,54,57,60,63],"tr",{},[51,52,53],"th",{},"Method",[51,55,56],{},"Platform",[51,58,59],{},"Fidelity",[51,61,62],{},"Dependencies",[51,64,65],{},"Best for",[67,68,69,94,117],"tbody",{},[48,70,71,82,85,88,91],{},[72,73,74,77,78,81],"td",{},[14,75,76],{},"LibreOffice headless"," (",[18,79,80],{},"soffice --convert-to pdf",")",[72,83,84],{},"Linux \u002F macOS \u002F Windows",[72,86,87],{},"High — renders the real workbook, styles, charts",[72,89,90],{},"LibreOffice installed (not pip)",[72,92,93],{},"Servers, cron jobs, CI; the portable default",[48,95,96,102,105,108,114],{},[72,97,98,101],{},[14,99,100],{},"Excel COM"," via xlwings \u002F pywin32",[72,103,104],{},"Windows + Excel only",[72,106,107],{},"Pixel-perfect — Excel renders it",[72,109,110,111],{},"Microsoft Excel + ",[18,112,113],{},"pip install xlwings",[72,115,116],{},"Desktops where Excel is already licensed",[48,118,119,125,128,131,136],{},[72,120,121,124],{},[14,122,123],{},"reportlab"," (build the PDF directly)",[72,126,127],{},"Any",[72,129,130],{},"Total control, but you rebuild the layout",[72,132,133],{},[18,134,135],{},"pip install reportlab",[72,137,138],{},"When you control the design and skip Excel rendering",[10,140,141,142,146],{},"The first two render an existing workbook. The third skips the workbook entirely and draws the PDF from your data — more work, but no Excel or LibreOffice required. For the step-by-step portable recipe, see ",[28,143,145],{"href":144},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python\u002F","Convert an Excel File to PDF with Python",".",[34,148,150],{"id":149},"set-up-the-page-before-you-convert","Set up the page before you convert",[10,152,153,154,157,158,162],{},"The single biggest source of ugly PDFs is converting a workbook that was never told how to print. A wide sheet paginates into a left half and a right half on separate pages; a tall sheet breaks columns awkwardly. Fix this ",[14,155,156],{},"in the workbook"," with openpyxl's page-setup attributes ",[159,160,161],"em",{},"before"," you hand the file to any converter — every method above respects them.",[164,165,170],"pre",{"className":166,"code":167,"language":168,"meta":169,"style":169},"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 = \"Summary\"\n\n# Some sample content wide enough to need fitting.\nws.append([\"Region\", \"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY Total\", \"YoY %\", \"Notes\"])\nfor r in range(1, 31):\n    ws.append([f\"Branch {r}\", 100 + r, 120 + r, 95 + r, 130 + r,\n               445 + 4 * r, 3.2, \"on track\"])\n\n# Landscape so wide tables get more horizontal room.\nws.page_setup.orientation = \"landscape\"\n\n# Fit ALL columns onto one page width; let height flow over pages.\nws.page_setup.fitToWidth = 1\nws.page_setup.fitToHeight = 0           # 0 = unlimited pages tall\nws.sheet_properties.pageSetUpPr = PageSetupProperties(fitToPage=True)\n\n# Restrict the export to the data region (skip stray cells far to the right).\nws.print_area = \"A1:H31\"\n\n# Repeat the header row on every printed page.\nws.print_title_rows = \"1:1\"\n\nwb.save(\"report.xlsx\")\nprint(\"Saved report.xlsx with print layout configured\")\n","python","",[18,171,172,191,204,211,223,234,246,251,258,306,336,393,419,424,430,441,446,452,463,477,500,505,511,522,527,533,544,549,560],{"__ignoreMap":169},[173,174,177,181,185,188],"span",{"class":175,"line":176},"line",1,[173,178,180],{"class":179},"szBVR","from",[173,182,184],{"class":183},"sVt8B"," openpyxl ",[173,186,187],{"class":179},"import",[173,189,190],{"class":183}," Workbook\n",[173,192,194,196,199,201],{"class":175,"line":193},2,[173,195,180],{"class":179},[173,197,198],{"class":183}," openpyxl.worksheet.properties ",[173,200,187],{"class":179},[173,202,203],{"class":183}," PageSetupProperties\n",[173,205,207],{"class":175,"line":206},3,[173,208,210],{"emptyLinePlaceholder":209},true,"\n",[173,212,214,217,220],{"class":175,"line":213},4,[173,215,216],{"class":183},"wb ",[173,218,219],{"class":179},"=",[173,221,222],{"class":183}," Workbook()\n",[173,224,226,229,231],{"class":175,"line":225},5,[173,227,228],{"class":183},"ws ",[173,230,219],{"class":179},[173,232,233],{"class":183}," wb.active\n",[173,235,237,240,242],{"class":175,"line":236},6,[173,238,239],{"class":183},"ws.title ",[173,241,219],{"class":179},[173,243,245],{"class":244},"sZZnC"," \"Summary\"\n",[173,247,249],{"class":175,"line":248},7,[173,250,210],{"emptyLinePlaceholder":209},[173,252,254],{"class":175,"line":253},8,[173,255,257],{"class":256},"sJ8bj","# Some sample content wide enough to need fitting.\n",[173,259,261,264,267,270,273,275,278,280,283,285,288,290,293,295,298,300,303],{"class":175,"line":260},9,[173,262,263],{"class":183},"ws.append([",[173,265,266],{"class":244},"\"Region\"",[173,268,269],{"class":183},", ",[173,271,272],{"class":244},"\"Q1\"",[173,274,269],{"class":183},[173,276,277],{"class":244},"\"Q2\"",[173,279,269],{"class":183},[173,281,282],{"class":244},"\"Q3\"",[173,284,269],{"class":183},[173,286,287],{"class":244},"\"Q4\"",[173,289,269],{"class":183},[173,291,292],{"class":244},"\"FY Total\"",[173,294,269],{"class":183},[173,296,297],{"class":244},"\"YoY %\"",[173,299,269],{"class":183},[173,301,302],{"class":244},"\"Notes\"",[173,304,305],{"class":183},"])\n",[173,307,309,312,315,318,322,325,328,330,333],{"class":175,"line":308},10,[173,310,311],{"class":179},"for",[173,313,314],{"class":183}," r ",[173,316,317],{"class":179},"in",[173,319,321],{"class":320},"sj4cs"," range",[173,323,324],{"class":183},"(",[173,326,327],{"class":320},"1",[173,329,269],{"class":183},[173,331,332],{"class":320},"31",[173,334,335],{"class":183},"):\n",[173,337,339,342,345,348,351,354,357,360,362,365,368,371,374,376,378,381,383,385,388,390],{"class":175,"line":338},11,[173,340,341],{"class":183},"    ws.append([",[173,343,344],{"class":179},"f",[173,346,347],{"class":244},"\"Branch ",[173,349,350],{"class":320},"{",[173,352,353],{"class":183},"r",[173,355,356],{"class":320},"}",[173,358,359],{"class":244},"\"",[173,361,269],{"class":183},[173,363,364],{"class":320},"100",[173,366,367],{"class":179}," +",[173,369,370],{"class":183}," r, ",[173,372,373],{"class":320},"120",[173,375,367],{"class":179},[173,377,370],{"class":183},[173,379,380],{"class":320},"95",[173,382,367],{"class":179},[173,384,370],{"class":183},[173,386,387],{"class":320},"130",[173,389,367],{"class":179},[173,391,392],{"class":183}," r,\n",[173,394,396,399,401,404,407,409,412,414,417],{"class":175,"line":395},12,[173,397,398],{"class":320},"               445",[173,400,367],{"class":179},[173,402,403],{"class":320}," 4",[173,405,406],{"class":179}," *",[173,408,370],{"class":183},[173,410,411],{"class":320},"3.2",[173,413,269],{"class":183},[173,415,416],{"class":244},"\"on track\"",[173,418,305],{"class":183},[173,420,422],{"class":175,"line":421},13,[173,423,210],{"emptyLinePlaceholder":209},[173,425,427],{"class":175,"line":426},14,[173,428,429],{"class":256},"# Landscape so wide tables get more horizontal room.\n",[173,431,433,436,438],{"class":175,"line":432},15,[173,434,435],{"class":183},"ws.page_setup.orientation ",[173,437,219],{"class":179},[173,439,440],{"class":244}," \"landscape\"\n",[173,442,444],{"class":175,"line":443},16,[173,445,210],{"emptyLinePlaceholder":209},[173,447,449],{"class":175,"line":448},17,[173,450,451],{"class":256},"# Fit ALL columns onto one page width; let height flow over pages.\n",[173,453,455,458,460],{"class":175,"line":454},18,[173,456,457],{"class":183},"ws.page_setup.fitToWidth ",[173,459,219],{"class":179},[173,461,462],{"class":320}," 1\n",[173,464,466,469,471,474],{"class":175,"line":465},19,[173,467,468],{"class":183},"ws.page_setup.fitToHeight ",[173,470,219],{"class":179},[173,472,473],{"class":320}," 0",[173,475,476],{"class":256},"           # 0 = unlimited pages tall\n",[173,478,480,483,485,488,492,494,497],{"class":175,"line":479},20,[173,481,482],{"class":183},"ws.sheet_properties.pageSetUpPr ",[173,484,219],{"class":179},[173,486,487],{"class":183}," PageSetupProperties(",[173,489,491],{"class":490},"s4XuR","fitToPage",[173,493,219],{"class":179},[173,495,496],{"class":320},"True",[173,498,499],{"class":183},")\n",[173,501,503],{"class":175,"line":502},21,[173,504,210],{"emptyLinePlaceholder":209},[173,506,508],{"class":175,"line":507},22,[173,509,510],{"class":256},"# Restrict the export to the data region (skip stray cells far to the right).\n",[173,512,514,517,519],{"class":175,"line":513},23,[173,515,516],{"class":183},"ws.print_area ",[173,518,219],{"class":179},[173,520,521],{"class":244}," \"A1:H31\"\n",[173,523,525],{"class":175,"line":524},24,[173,526,210],{"emptyLinePlaceholder":209},[173,528,530],{"class":175,"line":529},25,[173,531,532],{"class":256},"# Repeat the header row on every printed page.\n",[173,534,536,539,541],{"class":175,"line":535},26,[173,537,538],{"class":183},"ws.print_title_rows ",[173,540,219],{"class":179},[173,542,543],{"class":244}," \"1:1\"\n",[173,545,547],{"class":175,"line":546},27,[173,548,210],{"emptyLinePlaceholder":209},[173,550,552,555,558],{"class":175,"line":551},28,[173,553,554],{"class":183},"wb.save(",[173,556,557],{"class":244},"\"report.xlsx\"",[173,559,499],{"class":183},[173,561,563,566,568,571],{"class":175,"line":562},29,[173,564,565],{"class":320},"print",[173,567,324],{"class":183},[173,569,570],{"class":244},"\"Saved report.xlsx with print layout configured\"",[173,572,499],{"class":183},[10,574,575,576,579,580,579,583,586,587,590,591,593],{},"The crucial pairing is ",[18,577,578],{},"fitToWidth = 1"," ",[14,581,582],{},"and",[18,584,585],{},"pageSetUpPr = PageSetupProperties(fitToPage=True)",". Setting ",[18,588,589],{},"fitToWidth"," alone does nothing — Excel and LibreOffice ignore it unless the ",[18,592,491],{}," flag on the sheet properties is also on. With both set, the converter scales the columns down to one page wide while letting rows flow over as many pages as needed.",[34,595,597],{"id":596},"method-1-libreoffice-headless-the-portable-default","Method 1 — LibreOffice headless (the portable default)",[10,599,600,601,604,605,608,609,612],{},"LibreOffice ships a headless mode that opens the workbook, renders it exactly as the desktop app would, and writes a PDF — with no GUI. It runs on Linux, macOS, and Windows, costs nothing, and handles styles and charts well, which makes it the right default for any unattended job. Drive it from Python with ",[18,602,603],{},"subprocess",". The binary is ",[18,606,607],{},"soffice"," (or ",[18,610,611],{},"libreoffice"," on some Linux distros).",[164,614,616],{"className":166,"code":615,"language":168,"meta":169,"style":169},"import shutil\nimport subprocess\nfrom pathlib import Path\n\ndef xlsx_to_pdf_libreoffice(xlsx_path, out_dir=None, timeout=120):\n    \"\"\"Convert an .xlsx to PDF via headless LibreOffice.\n\n    Requires LibreOffice installed and `soffice` reachable on PATH.\n    Returns the path to the generated PDF.\n    \"\"\"\n    src = Path(xlsx_path).resolve()\n    if not src.is_file():\n        raise FileNotFoundError(src)\n\n    soffice = shutil.which(\"soffice\") or shutil.which(\"libreoffice\")\n    if soffice is None:\n        raise RuntimeError(\"LibreOffice not found; install it and ensure \"\n                           \"'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    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    if result.returncode != 0:\n        raise RuntimeError(\n            f\"LibreOffice failed (exit {result.returncode}): {result.stderr}\")\n\n    pdf_path = out_dir \u002F (src.stem + \".pdf\")\n    if not pdf_path.is_file():\n        raise RuntimeError(f\"Conversion reported success but {pdf_path} \"\n                           f\"is missing. stdout: {result.stdout}\")\n    return pdf_path\n\n# pdf = xlsx_to_pdf_libreoffice(\"report.xlsx\")\n# print(\"Wrote\", pdf)\n",[18,617,618,625,632,644,648,674,679,683,688,693,698,708,719,730,734,760,776,788,795,799,814,837,841,851,872,890,918,923,937,946,976,981,1006,1016,1040,1060,1069,1074,1080],{"__ignoreMap":169},[173,619,620,622],{"class":175,"line":176},[173,621,187],{"class":179},[173,623,624],{"class":183}," shutil\n",[173,626,627,629],{"class":175,"line":193},[173,628,187],{"class":179},[173,630,631],{"class":183}," subprocess\n",[173,633,634,636,639,641],{"class":175,"line":206},[173,635,180],{"class":179},[173,637,638],{"class":183}," pathlib ",[173,640,187],{"class":179},[173,642,643],{"class":183}," Path\n",[173,645,646],{"class":175,"line":213},[173,647,210],{"emptyLinePlaceholder":209},[173,649,650,653,657,660,662,665,668,670,672],{"class":175,"line":225},[173,651,652],{"class":179},"def",[173,654,656],{"class":655},"sScJk"," xlsx_to_pdf_libreoffice",[173,658,659],{"class":183},"(xlsx_path, out_dir",[173,661,219],{"class":179},[173,663,664],{"class":320},"None",[173,666,667],{"class":183},", timeout",[173,669,219],{"class":179},[173,671,373],{"class":320},[173,673,335],{"class":183},[173,675,676],{"class":175,"line":236},[173,677,678],{"class":244},"    \"\"\"Convert an .xlsx to PDF via headless LibreOffice.\n",[173,680,681],{"class":175,"line":248},[173,682,210],{"emptyLinePlaceholder":209},[173,684,685],{"class":175,"line":253},[173,686,687],{"class":244},"    Requires LibreOffice installed and `soffice` reachable on PATH.\n",[173,689,690],{"class":175,"line":260},[173,691,692],{"class":244},"    Returns the path to the generated PDF.\n",[173,694,695],{"class":175,"line":308},[173,696,697],{"class":244},"    \"\"\"\n",[173,699,700,703,705],{"class":175,"line":338},[173,701,702],{"class":183},"    src ",[173,704,219],{"class":179},[173,706,707],{"class":183}," Path(xlsx_path).resolve()\n",[173,709,710,713,716],{"class":175,"line":395},[173,711,712],{"class":179},"    if",[173,714,715],{"class":179}," not",[173,717,718],{"class":183}," src.is_file():\n",[173,720,721,724,727],{"class":175,"line":421},[173,722,723],{"class":179},"        raise",[173,725,726],{"class":320}," FileNotFoundError",[173,728,729],{"class":183},"(src)\n",[173,731,732],{"class":175,"line":426},[173,733,210],{"emptyLinePlaceholder":209},[173,735,736,739,741,744,747,750,753,755,758],{"class":175,"line":432},[173,737,738],{"class":183},"    soffice ",[173,740,219],{"class":179},[173,742,743],{"class":183}," shutil.which(",[173,745,746],{"class":244},"\"soffice\"",[173,748,749],{"class":183},") ",[173,751,752],{"class":179},"or",[173,754,743],{"class":183},[173,756,757],{"class":244},"\"libreoffice\"",[173,759,499],{"class":183},[173,761,762,764,767,770,773],{"class":175,"line":443},[173,763,712],{"class":179},[173,765,766],{"class":183}," soffice ",[173,768,769],{"class":179},"is",[173,771,772],{"class":320}," None",[173,774,775],{"class":183},":\n",[173,777,778,780,783,785],{"class":175,"line":448},[173,779,723],{"class":179},[173,781,782],{"class":320}," RuntimeError",[173,784,324],{"class":183},[173,786,787],{"class":244},"\"LibreOffice not found; install it and ensure \"\n",[173,789,790,793],{"class":175,"line":454},[173,791,792],{"class":244},"                           \"'soffice' is on PATH\"",[173,794,499],{"class":183},[173,796,797],{"class":175,"line":465},[173,798,210],{"emptyLinePlaceholder":209},[173,800,801,804,806,809,811],{"class":175,"line":479},[173,802,803],{"class":183},"    out_dir ",[173,805,219],{"class":179},[173,807,808],{"class":183}," Path(out_dir ",[173,810,752],{"class":179},[173,812,813],{"class":183}," src.parent).resolve()\n",[173,815,816,819,822,824,826,828,831,833,835],{"class":175,"line":502},[173,817,818],{"class":183},"    out_dir.mkdir(",[173,820,821],{"class":490},"parents",[173,823,219],{"class":179},[173,825,496],{"class":320},[173,827,269],{"class":183},[173,829,830],{"class":490},"exist_ok",[173,832,219],{"class":179},[173,834,496],{"class":320},[173,836,499],{"class":183},[173,838,839],{"class":175,"line":507},[173,840,210],{"emptyLinePlaceholder":209},[173,842,843,846,848],{"class":175,"line":513},[173,844,845],{"class":183},"    result ",[173,847,219],{"class":179},[173,849,850],{"class":183}," subprocess.run(\n",[173,852,853,856,859,861,864,866,869],{"class":175,"line":524},[173,854,855],{"class":183},"        [soffice, ",[173,857,858],{"class":244},"\"--headless\"",[173,860,269],{"class":183},[173,862,863],{"class":244},"\"--convert-to\"",[173,865,269],{"class":183},[173,867,868],{"class":244},"\"pdf\"",[173,870,871],{"class":183},",\n",[173,873,874,877,879,882,885,887],{"class":175,"line":529},[173,875,876],{"class":244},"         \"--outdir\"",[173,878,269],{"class":183},[173,880,881],{"class":320},"str",[173,883,884],{"class":183},"(out_dir), ",[173,886,881],{"class":320},[173,888,889],{"class":183},"(src)],\n",[173,891,892,895,897,899,901,904,906,908,910,913,915],{"class":175,"line":535},[173,893,894],{"class":490},"        capture_output",[173,896,219],{"class":179},[173,898,496],{"class":320},[173,900,269],{"class":183},[173,902,903],{"class":490},"text",[173,905,219],{"class":179},[173,907,496],{"class":320},[173,909,269],{"class":183},[173,911,912],{"class":490},"timeout",[173,914,219],{"class":179},[173,916,917],{"class":183},"timeout,\n",[173,919,920],{"class":175,"line":546},[173,921,922],{"class":183},"    )\n",[173,924,925,927,930,933,935],{"class":175,"line":551},[173,926,712],{"class":179},[173,928,929],{"class":183}," result.returncode ",[173,931,932],{"class":179},"!=",[173,934,473],{"class":320},[173,936,775],{"class":183},[173,938,939,941,943],{"class":175,"line":562},[173,940,723],{"class":179},[173,942,782],{"class":320},[173,944,945],{"class":183},"(\n",[173,947,949,952,955,957,960,962,965,967,970,972,974],{"class":175,"line":948},30,[173,950,951],{"class":179},"            f",[173,953,954],{"class":244},"\"LibreOffice failed (exit ",[173,956,350],{"class":320},[173,958,959],{"class":183},"result.returncode",[173,961,356],{"class":320},[173,963,964],{"class":244},"): ",[173,966,350],{"class":320},[173,968,969],{"class":183},"result.stderr",[173,971,356],{"class":320},[173,973,359],{"class":244},[173,975,499],{"class":183},[173,977,979],{"class":175,"line":978},31,[173,980,210],{"emptyLinePlaceholder":209},[173,982,984,987,989,992,995,998,1001,1004],{"class":175,"line":983},32,[173,985,986],{"class":183},"    pdf_path ",[173,988,219],{"class":179},[173,990,991],{"class":183}," out_dir ",[173,993,994],{"class":179},"\u002F",[173,996,997],{"class":183}," (src.stem ",[173,999,1000],{"class":179},"+",[173,1002,1003],{"class":244}," \".pdf\"",[173,1005,499],{"class":183},[173,1007,1009,1011,1013],{"class":175,"line":1008},33,[173,1010,712],{"class":179},[173,1012,715],{"class":179},[173,1014,1015],{"class":183}," pdf_path.is_file():\n",[173,1017,1019,1021,1023,1025,1027,1030,1032,1035,1037],{"class":175,"line":1018},34,[173,1020,723],{"class":179},[173,1022,782],{"class":320},[173,1024,324],{"class":183},[173,1026,344],{"class":179},[173,1028,1029],{"class":244},"\"Conversion reported success but ",[173,1031,350],{"class":320},[173,1033,1034],{"class":183},"pdf_path",[173,1036,356],{"class":320},[173,1038,1039],{"class":244}," \"\n",[173,1041,1043,1046,1049,1051,1054,1056,1058],{"class":175,"line":1042},35,[173,1044,1045],{"class":179},"                           f",[173,1047,1048],{"class":244},"\"is missing. stdout: ",[173,1050,350],{"class":320},[173,1052,1053],{"class":183},"result.stdout",[173,1055,356],{"class":320},[173,1057,359],{"class":244},[173,1059,499],{"class":183},[173,1061,1063,1066],{"class":175,"line":1062},36,[173,1064,1065],{"class":179},"    return",[173,1067,1068],{"class":183}," pdf_path\n",[173,1070,1072],{"class":175,"line":1071},37,[173,1073,210],{"emptyLinePlaceholder":209},[173,1075,1077],{"class":175,"line":1076},38,[173,1078,1079],{"class":256},"# pdf = xlsx_to_pdf_libreoffice(\"report.xlsx\")\n",[173,1081,1083],{"class":175,"line":1082},39,[173,1084,1085],{"class":256},"# print(\"Wrote\", pdf)\n",[10,1087,1088,1089,1092,1093,1095,1096,1099,1100,1103],{},"Always check ",[18,1090,1091],{},"returncode"," and that the expected output file exists — LibreOffice occasionally exits zero while writing nothing if the input is corrupt. The ",[18,1094,912],{}," guards against a hung process in a scheduled job; if it fires, ",[18,1097,1098],{},"subprocess.run"," raises ",[18,1101,1102],{},"TimeoutExpired",", which your job wrapper should catch and log.",[34,1105,1107],{"id":1106},"method-2-excel-com-on-windows","Method 2 — Excel COM on Windows",[10,1109,1110,1111,1115],{},"On a Windows box with Excel installed, you can ask Excel itself to export — the result is pixel-perfect because it is Excel's own renderer. ",[28,1112,1114],{"href":1113},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002F","xlwings"," wraps the COM API cleanly:",[164,1117,1119],{"className":166,"code":1118,"language":168,"meta":169,"style":169},"# Windows + Microsoft Excel only.  pip install xlwings\nimport xlwings as xw\n\ndef xlsx_to_pdf_excel(xlsx_path, pdf_path):\n    \"\"\"Pixel-perfect export using Excel's own engine. Windows + Excel only.\"\"\"\n    app = xw.App(visible=False)\n    try:\n        wb = app.books.open(xlsx_path)\n        wb.to_pdf(pdf_path)        # xlwings >= 0.21 convenience wrapper\n        wb.close()\n    finally:\n        app.quit()\n\n# xlsx_to_pdf_excel(\"report.xlsx\", \"report.pdf\")\n",[18,1120,1121,1126,1139,1143,1153,1158,1178,1185,1195,1203,1208,1215,1220,1224],{"__ignoreMap":169},[173,1122,1123],{"class":175,"line":176},[173,1124,1125],{"class":256},"# Windows + Microsoft Excel only.  pip install xlwings\n",[173,1127,1128,1130,1133,1136],{"class":175,"line":193},[173,1129,187],{"class":179},[173,1131,1132],{"class":183}," xlwings ",[173,1134,1135],{"class":179},"as",[173,1137,1138],{"class":183}," xw\n",[173,1140,1141],{"class":175,"line":206},[173,1142,210],{"emptyLinePlaceholder":209},[173,1144,1145,1147,1150],{"class":175,"line":213},[173,1146,652],{"class":179},[173,1148,1149],{"class":655}," xlsx_to_pdf_excel",[173,1151,1152],{"class":183},"(xlsx_path, pdf_path):\n",[173,1154,1155],{"class":175,"line":225},[173,1156,1157],{"class":244},"    \"\"\"Pixel-perfect export using Excel's own engine. Windows + Excel only.\"\"\"\n",[173,1159,1160,1163,1165,1168,1171,1173,1176],{"class":175,"line":236},[173,1161,1162],{"class":183},"    app ",[173,1164,219],{"class":179},[173,1166,1167],{"class":183}," xw.App(",[173,1169,1170],{"class":490},"visible",[173,1172,219],{"class":179},[173,1174,1175],{"class":320},"False",[173,1177,499],{"class":183},[173,1179,1180,1183],{"class":175,"line":248},[173,1181,1182],{"class":179},"    try",[173,1184,775],{"class":183},[173,1186,1187,1190,1192],{"class":175,"line":253},[173,1188,1189],{"class":183},"        wb ",[173,1191,219],{"class":179},[173,1193,1194],{"class":183}," app.books.open(xlsx_path)\n",[173,1196,1197,1200],{"class":175,"line":260},[173,1198,1199],{"class":183},"        wb.to_pdf(pdf_path)        ",[173,1201,1202],{"class":256},"# xlwings >= 0.21 convenience wrapper\n",[173,1204,1205],{"class":175,"line":308},[173,1206,1207],{"class":183},"        wb.close()\n",[173,1209,1210,1213],{"class":175,"line":338},[173,1211,1212],{"class":179},"    finally",[173,1214,775],{"class":183},[173,1216,1217],{"class":175,"line":395},[173,1218,1219],{"class":183},"        app.quit()\n",[173,1221,1222],{"class":175,"line":421},[173,1223,210],{"emptyLinePlaceholder":209},[173,1225,1226],{"class":175,"line":426},[173,1227,1228],{"class":256},"# xlsx_to_pdf_excel(\"report.xlsx\", \"report.pdf\")\n",[10,1230,1231,1232,1235,1236,1239],{},"Under the hood ",[18,1233,1234],{},"to_pdf()"," calls ",[18,1237,1238],{},"ExportAsFixedFormat"," on the COM workbook. This is the highest-fidelity option, but it only runs where Excel is licensed and installed — it is a non-starter on a Linux server, so do not build a server pipeline around it.",[34,1241,1243],{"id":1242},"method-3-build-the-pdf-directly-with-reportlab","Method 3 — Build the PDF directly with reportlab",[10,1245,1246],{},"When you control the layout and do not need the workbook's exact styling, skip Excel rendering entirely and draw the PDF from your data with reportlab. This needs no Excel and no LibreOffice — just a pip install — which is appealing for locked-down environments.",[164,1248,1250],{"className":166,"code":1249,"language":168,"meta":169,"style":169},"# pip install reportlab openpyxl\nfrom openpyxl import load_workbook\nfrom reportlab.lib.pagesizes import landscape, A4\nfrom reportlab.lib import colors\nfrom reportlab.platypus import SimpleDocTemplate, Table, TableStyle\n\ndef sheet_to_pdf_reportlab(xlsx_path, pdf_path, sheet=None):\n    \"\"\"Read a sheet's cells and lay them out as a PDF table. No Excel needed.\"\"\"\n    wb = load_workbook(xlsx_path, data_only=True)\n    ws = wb[sheet] if sheet else wb.active\n    rows = [[(\"\" if c is None else c) for c in row]\n            for row in ws.iter_rows(values_only=True)]\n\n    doc = SimpleDocTemplate(pdf_path, pagesize=landscape(A4))\n    table = Table(rows, repeatRows=1)\n    table.setStyle(TableStyle([\n        (\"BACKGROUND\", (0, 0), (-1, 0), colors.HexColor(\"#1f4e78\")),\n        (\"TEXTCOLOR\", (0, 0), (-1, 0), colors.white),\n        (\"FONTSIZE\", (0, 0), (-1, -1), 8),\n        (\"GRID\", (0, 0), (-1, -1), 0.25, colors.grey),\n    ]))\n    doc.build([table])\n\n# sheet_to_pdf_reportlab(\"report.xlsx\", \"report.pdf\")\n",[18,1251,1252,1257,1268,1280,1292,1304,1308,1324,1329,1348,1369,1407,1430,1434,1452,1471,1476,1515,1543,1579,1614,1619,1624,1628],{"__ignoreMap":169},[173,1253,1254],{"class":175,"line":176},[173,1255,1256],{"class":256},"# pip install reportlab openpyxl\n",[173,1258,1259,1261,1263,1265],{"class":175,"line":193},[173,1260,180],{"class":179},[173,1262,184],{"class":183},[173,1264,187],{"class":179},[173,1266,1267],{"class":183}," load_workbook\n",[173,1269,1270,1272,1275,1277],{"class":175,"line":206},[173,1271,180],{"class":179},[173,1273,1274],{"class":183}," reportlab.lib.pagesizes ",[173,1276,187],{"class":179},[173,1278,1279],{"class":183}," landscape, A4\n",[173,1281,1282,1284,1287,1289],{"class":175,"line":213},[173,1283,180],{"class":179},[173,1285,1286],{"class":183}," reportlab.lib ",[173,1288,187],{"class":179},[173,1290,1291],{"class":183}," colors\n",[173,1293,1294,1296,1299,1301],{"class":175,"line":225},[173,1295,180],{"class":179},[173,1297,1298],{"class":183}," reportlab.platypus ",[173,1300,187],{"class":179},[173,1302,1303],{"class":183}," SimpleDocTemplate, Table, TableStyle\n",[173,1305,1306],{"class":175,"line":236},[173,1307,210],{"emptyLinePlaceholder":209},[173,1309,1310,1312,1315,1318,1320,1322],{"class":175,"line":248},[173,1311,652],{"class":179},[173,1313,1314],{"class":655}," sheet_to_pdf_reportlab",[173,1316,1317],{"class":183},"(xlsx_path, pdf_path, sheet",[173,1319,219],{"class":179},[173,1321,664],{"class":320},[173,1323,335],{"class":183},[173,1325,1326],{"class":175,"line":253},[173,1327,1328],{"class":244},"    \"\"\"Read a sheet's cells and lay them out as a PDF table. No Excel needed.\"\"\"\n",[173,1330,1331,1334,1336,1339,1342,1344,1346],{"class":175,"line":260},[173,1332,1333],{"class":183},"    wb ",[173,1335,219],{"class":179},[173,1337,1338],{"class":183}," load_workbook(xlsx_path, ",[173,1340,1341],{"class":490},"data_only",[173,1343,219],{"class":179},[173,1345,496],{"class":320},[173,1347,499],{"class":183},[173,1349,1350,1353,1355,1358,1361,1364,1367],{"class":175,"line":308},[173,1351,1352],{"class":183},"    ws ",[173,1354,219],{"class":179},[173,1356,1357],{"class":183}," wb[sheet] ",[173,1359,1360],{"class":179},"if",[173,1362,1363],{"class":183}," sheet ",[173,1365,1366],{"class":179},"else",[173,1368,233],{"class":183},[173,1370,1371,1374,1376,1379,1382,1385,1388,1390,1392,1395,1398,1400,1402,1404],{"class":175,"line":338},[173,1372,1373],{"class":183},"    rows ",[173,1375,219],{"class":179},[173,1377,1378],{"class":183}," [[(",[173,1380,1381],{"class":244},"\"\"",[173,1383,1384],{"class":179}," if",[173,1386,1387],{"class":183}," c ",[173,1389,769],{"class":179},[173,1391,772],{"class":320},[173,1393,1394],{"class":179}," else",[173,1396,1397],{"class":183}," c) ",[173,1399,311],{"class":179},[173,1401,1387],{"class":183},[173,1403,317],{"class":179},[173,1405,1406],{"class":183}," row]\n",[173,1408,1409,1412,1415,1417,1420,1423,1425,1427],{"class":175,"line":395},[173,1410,1411],{"class":179},"            for",[173,1413,1414],{"class":183}," row ",[173,1416,317],{"class":179},[173,1418,1419],{"class":183}," ws.iter_rows(",[173,1421,1422],{"class":490},"values_only",[173,1424,219],{"class":179},[173,1426,496],{"class":320},[173,1428,1429],{"class":183},")]\n",[173,1431,1432],{"class":175,"line":421},[173,1433,210],{"emptyLinePlaceholder":209},[173,1435,1436,1439,1441,1444,1447,1449],{"class":175,"line":426},[173,1437,1438],{"class":183},"    doc ",[173,1440,219],{"class":179},[173,1442,1443],{"class":183}," SimpleDocTemplate(pdf_path, ",[173,1445,1446],{"class":490},"pagesize",[173,1448,219],{"class":179},[173,1450,1451],{"class":183},"landscape(A4))\n",[173,1453,1454,1457,1459,1462,1465,1467,1469],{"class":175,"line":432},[173,1455,1456],{"class":183},"    table ",[173,1458,219],{"class":179},[173,1460,1461],{"class":183}," Table(rows, ",[173,1463,1464],{"class":490},"repeatRows",[173,1466,219],{"class":179},[173,1468,327],{"class":320},[173,1470,499],{"class":183},[173,1472,1473],{"class":175,"line":443},[173,1474,1475],{"class":183},"    table.setStyle(TableStyle([\n",[173,1477,1478,1481,1484,1487,1490,1492,1494,1497,1500,1502,1504,1506,1509,1512],{"class":175,"line":448},[173,1479,1480],{"class":183},"        (",[173,1482,1483],{"class":244},"\"BACKGROUND\"",[173,1485,1486],{"class":183},", (",[173,1488,1489],{"class":320},"0",[173,1491,269],{"class":183},[173,1493,1489],{"class":320},[173,1495,1496],{"class":183},"), (",[173,1498,1499],{"class":179},"-",[173,1501,327],{"class":320},[173,1503,269],{"class":183},[173,1505,1489],{"class":320},[173,1507,1508],{"class":183},"), colors.HexColor(",[173,1510,1511],{"class":244},"\"#1f4e78\"",[173,1513,1514],{"class":183},")),\n",[173,1516,1517,1519,1522,1524,1526,1528,1530,1532,1534,1536,1538,1540],{"class":175,"line":454},[173,1518,1480],{"class":183},[173,1520,1521],{"class":244},"\"TEXTCOLOR\"",[173,1523,1486],{"class":183},[173,1525,1489],{"class":320},[173,1527,269],{"class":183},[173,1529,1489],{"class":320},[173,1531,1496],{"class":183},[173,1533,1499],{"class":179},[173,1535,327],{"class":320},[173,1537,269],{"class":183},[173,1539,1489],{"class":320},[173,1541,1542],{"class":183},"), colors.white),\n",[173,1544,1545,1547,1550,1552,1554,1556,1558,1560,1562,1564,1566,1568,1570,1573,1576],{"class":175,"line":465},[173,1546,1480],{"class":183},[173,1548,1549],{"class":244},"\"FONTSIZE\"",[173,1551,1486],{"class":183},[173,1553,1489],{"class":320},[173,1555,269],{"class":183},[173,1557,1489],{"class":320},[173,1559,1496],{"class":183},[173,1561,1499],{"class":179},[173,1563,327],{"class":320},[173,1565,269],{"class":183},[173,1567,1499],{"class":179},[173,1569,327],{"class":320},[173,1571,1572],{"class":183},"), ",[173,1574,1575],{"class":320},"8",[173,1577,1578],{"class":183},"),\n",[173,1580,1581,1583,1586,1588,1590,1592,1594,1596,1598,1600,1602,1604,1606,1608,1611],{"class":175,"line":479},[173,1582,1480],{"class":183},[173,1584,1585],{"class":244},"\"GRID\"",[173,1587,1486],{"class":183},[173,1589,1489],{"class":320},[173,1591,269],{"class":183},[173,1593,1489],{"class":320},[173,1595,1496],{"class":183},[173,1597,1499],{"class":179},[173,1599,327],{"class":320},[173,1601,269],{"class":183},[173,1603,1499],{"class":179},[173,1605,327],{"class":320},[173,1607,1572],{"class":183},[173,1609,1610],{"class":320},"0.25",[173,1612,1613],{"class":183},", colors.grey),\n",[173,1615,1616],{"class":175,"line":502},[173,1617,1618],{"class":183},"    ]))\n",[173,1620,1621],{"class":175,"line":507},[173,1622,1623],{"class":183},"    doc.build([table])\n",[173,1625,1626],{"class":175,"line":513},[173,1627,210],{"emptyLinePlaceholder":209},[173,1629,1630],{"class":175,"line":524},[173,1631,1632],{"class":256},"# sheet_to_pdf_reportlab(\"report.xlsx\", \"report.pdf\")\n",[10,1634,1635],{},"You trade fidelity for control and zero external dependencies. Charts, merged-cell layouts, and conditional formatting do not come across — you rebuild whatever you want on the page yourself. For data-only summaries this is fast and fully self-contained.",[34,1637,1639],{"id":1638},"automating-the-conversion-in-a-scheduled-job","Automating the conversion in a scheduled job",[10,1641,1642,1643,1645],{},"In production the conversion is one step in an unattended pipeline: generate the workbook, convert it, then email it. For LibreOffice headless this means two precautions. First, give each run its own user-profile directory so a desktop LibreOffice instance (or a previous run) cannot lock the conversion. Second, set a ",[18,1644,912],{}," and treat both a non-zero exit and a missing output file as failures your scheduler will alert on.",[164,1647,1649],{"className":166,"code":1648,"language":168,"meta":169,"style":169},"import tempfile, shutil, subprocess\nfrom pathlib import Path\n\ndef convert_isolated(xlsx_path, out_dir, 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_dir = Path(out_dir).resolve()\n    out_dir.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\",\n             \"--outdir\", str(out_dir), str(src)],\n            capture_output=True, text=True, timeout=timeout,\n        )\n    if result.returncode != 0:\n        raise RuntimeError(f\"convert failed: {result.stderr}\")\n    pdf = out_dir \u002F (src.stem + \".pdf\")\n    if not pdf.is_file():\n        raise RuntimeError(\"no PDF produced\")\n    return pdf\n",[18,1650,1651,1658,1668,1672,1688,1693,1713,1725,1738,1746,1755,1775,1779,1792,1801,1822,1837,1852,1877,1882,1894,1917,1936,1945,1958],{"__ignoreMap":169},[173,1652,1653,1655],{"class":175,"line":176},[173,1654,187],{"class":179},[173,1656,1657],{"class":183}," tempfile, shutil, subprocess\n",[173,1659,1660,1662,1664,1666],{"class":175,"line":193},[173,1661,180],{"class":179},[173,1663,638],{"class":183},[173,1665,187],{"class":179},[173,1667,643],{"class":183},[173,1669,1670],{"class":175,"line":206},[173,1671,210],{"emptyLinePlaceholder":209},[173,1673,1674,1676,1679,1682,1684,1686],{"class":175,"line":213},[173,1675,652],{"class":179},[173,1677,1678],{"class":655}," convert_isolated",[173,1680,1681],{"class":183},"(xlsx_path, out_dir, timeout",[173,1683,219],{"class":179},[173,1685,373],{"class":320},[173,1687,335],{"class":183},[173,1689,1690],{"class":175,"line":225},[173,1691,1692],{"class":244},"    \"\"\"LibreOffice conversion with a throwaway profile dir — safe under cron.\"\"\"\n",[173,1694,1695,1697,1699,1701,1703,1705,1707,1709,1711],{"class":175,"line":236},[173,1696,738],{"class":183},[173,1698,219],{"class":179},[173,1700,743],{"class":183},[173,1702,746],{"class":244},[173,1704,749],{"class":183},[173,1706,752],{"class":179},[173,1708,743],{"class":183},[173,1710,757],{"class":244},[173,1712,499],{"class":183},[173,1714,1715,1717,1719,1721,1723],{"class":175,"line":248},[173,1716,712],{"class":179},[173,1718,766],{"class":183},[173,1720,769],{"class":179},[173,1722,772],{"class":320},[173,1724,775],{"class":183},[173,1726,1727,1729,1731,1733,1736],{"class":175,"line":253},[173,1728,723],{"class":179},[173,1730,782],{"class":320},[173,1732,324],{"class":183},[173,1734,1735],{"class":244},"\"install LibreOffice; 'soffice' not on PATH\"",[173,1737,499],{"class":183},[173,1739,1740,1742,1744],{"class":175,"line":260},[173,1741,702],{"class":183},[173,1743,219],{"class":179},[173,1745,707],{"class":183},[173,1747,1748,1750,1752],{"class":175,"line":308},[173,1749,803],{"class":183},[173,1751,219],{"class":179},[173,1753,1754],{"class":183}," Path(out_dir).resolve()\n",[173,1756,1757,1759,1761,1763,1765,1767,1769,1771,1773],{"class":175,"line":338},[173,1758,818],{"class":183},[173,1760,821],{"class":490},[173,1762,219],{"class":179},[173,1764,496],{"class":320},[173,1766,269],{"class":183},[173,1768,830],{"class":490},[173,1770,219],{"class":179},[173,1772,496],{"class":320},[173,1774,499],{"class":183},[173,1776,1777],{"class":175,"line":395},[173,1778,210],{"emptyLinePlaceholder":209},[173,1780,1781,1784,1787,1789],{"class":175,"line":421},[173,1782,1783],{"class":179},"    with",[173,1785,1786],{"class":183}," tempfile.TemporaryDirectory() ",[173,1788,1135],{"class":179},[173,1790,1791],{"class":183}," profile:\n",[173,1793,1794,1797,1799],{"class":175,"line":426},[173,1795,1796],{"class":183},"        result ",[173,1798,219],{"class":179},[173,1800,850],{"class":183},[173,1802,1803,1806,1808,1811,1813,1816,1818,1820],{"class":175,"line":432},[173,1804,1805],{"class":183},"            [soffice, ",[173,1807,344],{"class":179},[173,1809,1810],{"class":244},"\"-env:UserInstallation=file:\u002F\u002F",[173,1812,350],{"class":320},[173,1814,1815],{"class":183},"profile",[173,1817,356],{"class":320},[173,1819,359],{"class":244},[173,1821,871],{"class":183},[173,1823,1824,1827,1829,1831,1833,1835],{"class":175,"line":443},[173,1825,1826],{"class":244},"             \"--headless\"",[173,1828,269],{"class":183},[173,1830,863],{"class":244},[173,1832,269],{"class":183},[173,1834,868],{"class":244},[173,1836,871],{"class":183},[173,1838,1839,1842,1844,1846,1848,1850],{"class":175,"line":448},[173,1840,1841],{"class":244},"             \"--outdir\"",[173,1843,269],{"class":183},[173,1845,881],{"class":320},[173,1847,884],{"class":183},[173,1849,881],{"class":320},[173,1851,889],{"class":183},[173,1853,1854,1857,1859,1861,1863,1865,1867,1869,1871,1873,1875],{"class":175,"line":454},[173,1855,1856],{"class":490},"            capture_output",[173,1858,219],{"class":179},[173,1860,496],{"class":320},[173,1862,269],{"class":183},[173,1864,903],{"class":490},[173,1866,219],{"class":179},[173,1868,496],{"class":320},[173,1870,269],{"class":183},[173,1872,912],{"class":490},[173,1874,219],{"class":179},[173,1876,917],{"class":183},[173,1878,1879],{"class":175,"line":465},[173,1880,1881],{"class":183},"        )\n",[173,1883,1884,1886,1888,1890,1892],{"class":175,"line":479},[173,1885,712],{"class":179},[173,1887,929],{"class":183},[173,1889,932],{"class":179},[173,1891,473],{"class":320},[173,1893,775],{"class":183},[173,1895,1896,1898,1900,1902,1904,1907,1909,1911,1913,1915],{"class":175,"line":502},[173,1897,723],{"class":179},[173,1899,782],{"class":320},[173,1901,324],{"class":183},[173,1903,344],{"class":179},[173,1905,1906],{"class":244},"\"convert failed: ",[173,1908,350],{"class":320},[173,1910,969],{"class":183},[173,1912,356],{"class":320},[173,1914,359],{"class":244},[173,1916,499],{"class":183},[173,1918,1919,1922,1924,1926,1928,1930,1932,1934],{"class":175,"line":507},[173,1920,1921],{"class":183},"    pdf ",[173,1923,219],{"class":179},[173,1925,991],{"class":183},[173,1927,994],{"class":179},[173,1929,997],{"class":183},[173,1931,1000],{"class":179},[173,1933,1003],{"class":244},[173,1935,499],{"class":183},[173,1937,1938,1940,1942],{"class":175,"line":513},[173,1939,712],{"class":179},[173,1941,715],{"class":179},[173,1943,1944],{"class":183}," pdf.is_file():\n",[173,1946,1947,1949,1951,1953,1956],{"class":175,"line":524},[173,1948,723],{"class":179},[173,1950,782],{"class":320},[173,1952,324],{"class":183},[173,1954,1955],{"class":244},"\"no PDF produced\"",[173,1957,499],{"class":183},[173,1959,1960,1962],{"class":175,"line":529},[173,1961,1065],{"class":179},[173,1963,1964],{"class":183}," pdf\n",[10,1966,1967,1968,1971],{},"The ",[18,1969,1970],{},"-env:UserInstallation"," flag is the key line — it points LibreOffice at a fresh profile per invocation, which is what makes headless conversion reliable when another instance might be running. On a headless server, also install the font packages your reports use, or text falls back to substitutes and the PDF looks wrong.",[34,1973,1975],{"id":1974},"frequently-asked-questions","Frequently asked questions",[10,1977,1978,1981,1982,1984],{},[14,1979,1980],{},"Can openpyxl or pandas export a PDF directly?","\nNo. openpyxl writes ",[18,1983,20],{}," files and pandas handles tabular data; neither renders a workbook to PDF. You need LibreOffice, Excel, or a PDF library like reportlab. Treat any claim otherwise as a red flag.",[10,1986,1987,1990],{},[14,1988,1989],{},"Which method should I default to?","\nLibreOffice headless. It is free, cross-platform, renders the real workbook faithfully, and runs unattended on a server — the only cost is installing LibreOffice. Reach for Excel COM only when you specifically need Excel's pixel-perfect output on Windows, and reportlab when you want zero external programs and control the layout yourself.",[10,1992,1993,1996,1997,269,2000,2003,2004,2007,2008,2010,2011,2013],{},[14,1994,1995],{},"My PDF splits columns across two pages — how do I stop it?","\nSet the page layout in the workbook before converting: ",[18,1998,1999],{},"ws.page_setup.orientation = \"landscape\"",[18,2001,2002],{},"ws.page_setup.fitToWidth = 1",", and ",[18,2005,2006],{},"ws.sheet_properties.pageSetUpPr = PageSetupProperties(fitToPage=True)",". ",[18,2009,589],{}," is ignored unless ",[18,2012,491],{}," is also enabled.",[10,2015,2016,2019,2020,2022,2023,2025,2026,2029,2030,2032],{},[14,2017,2018],{},"LibreOffice runs fine locally but hangs or fails under cron — why?","\nAlmost always a locked profile or PATH issue. Use a throwaway ",[18,2021,1970],{}," profile per run so a running instance does not block it, resolve ",[18,2024,607],{}," with ",[18,2027,2028],{},"shutil.which"," rather than assuming it is on the cron PATH, and set a ",[18,2031,912],{}," so a hung process fails loudly.",[10,2034,2035,2038],{},[14,2036,2037],{},"Can I convert a single sheet instead of the whole workbook?","\nLibreOffice and Excel export every visible sheet. To export one sheet, either hide the others before converting or load just that sheet and use the reportlab approach, which reads whichever sheet you name.",[34,2040,2042],{"id":2041},"conclusion","Conclusion",[10,2044,2045],{},"There is no magic pip package that turns a spreadsheet into a faithful PDF — but there are three solid paths. LibreOffice headless is the portable default for unattended jobs, Excel COM gives pixel-perfect output where Excel is installed, and reportlab builds the document directly when you control the design. Whichever you pick, configure the page layout with openpyxl first — orientation, fit-to-width, and print area — so the result paginates cleanly instead of fragmenting. Then wrap the conversion with a timeout, an isolated profile, and explicit output-file checks so it survives scheduling.",[34,2047,2049],{"id":2048},"where-to-go-next","Where to go next",[10,2051,2052,2053,2055,2056,2060,2061,2065,2066,2068,2069,146],{},"Work through the portable recipe end to end in ",[28,2054,145],{"href":144},". Then connect this step to the rest of the pipeline: ",[28,2057,2059],{"href":2058},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002F","Emailing Excel Reports with smtplib"," sends the finished PDF, and ",[28,2062,2064],{"href":2063},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002F","Scheduling Python Excel Scripts with Cron"," runs the whole generate-convert-send job unattended. For the upstream stages, return to ",[28,2067,31],{"href":30}," or polish the workbook first with ",[28,2070,2072],{"href":2071},"\u002Fformatting-and-charting-excel-reports-with-python\u002F","Formatting and Charting Excel Reports with Python",[2074,2075,2076],"style",{},"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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":169,"searchDepth":193,"depth":193,"links":2078},[2079,2080,2081,2082,2083,2084,2085,2086,2087],{"id":36,"depth":193,"text":37},{"id":149,"depth":193,"text":150},{"id":596,"depth":193,"text":597},{"id":1106,"depth":193,"text":1107},{"id":1242,"depth":193,"text":1243},{"id":1638,"depth":193,"text":1639},{"id":1974,"depth":193,"text":1975},{"id":2041,"depth":193,"text":2042},{"id":2048,"depth":193,"text":2049},"Convert .xlsx reports to PDF from Python the honest way: LibreOffice headless, Excel COM, or reportlab — plus openpyxl page setup so the output paginates cleanly.","md",{},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf",{"title":2093,"description":2094},"Export Excel Reports to PDF with Python","Turn Excel workbooks into PDF from Python using LibreOffice headless, Excel COM via xlwings, or reportlab. Set print area and fit-to-page first for clean pages.","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Findex","0FIXslmI9MBf_UkI4ExKDh97PYnVcGTeoePK5OUNoOc",[2098,2102],{"title":2099,"path":2100,"stem":2101,"children":-1},"Send an Excel Report to Multiple Recipients in Python","\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-excel-report-to-multiple-recipients-python","automating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-excel-report-to-multiple-recipients-python\u002Findex",{"title":145,"path":2103,"stem":2104,"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",1781773160643]