[{"data":1,"prerenderedAt":1947},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-to-pdf-on-linux-with-libreoffice":3,"surround:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-to-pdf-on-linux-with-libreoffice":1940},{"id":4,"title":5,"body":6,"dateModified":1913,"datePublished":1913,"description":1914,"extension":1915,"faq":1916,"meta":1925,"navigation":263,"path":1933,"seo":1934,"slug":1936,"stem":1937,"type":1938,"__hash__":1939},"docs\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-to-pdf-on-linux-with-libreoffice\u002Findex.md","Convert Excel to PDF on Linux with LibreOffice",{"type":7,"value":8,"toc":1899},"minimark",[9,19,134,139,174,184,188,216,219,609,620,624,724,727,881,900,904,907,1041,1049,1053,1137,1145,1149,1152,1425,1433,1437,1440,1450,1493,1499,1503,1607,1611,1695,1698,1812,1815,1819,1828,1832,1839,1845,1851,1857,1861,1895],[10,11,12,13,18],"p",{},"Converting a workbook to PDF without Excel is the requirement that stops most Linux reporting\npipelines, and headless LibreOffice is the answer that works: no licence, no desktop session, one\ncommand. This guide, part of\n",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002F","Exporting Excel Reports to PDF",",\ncovers the invocation, the page setup that makes the output usable, and the container details that\ndecide whether it is reliable.",[20,21,29,30,29,34,29,38,29,45,29,55,29,62,29,69,29,74,29,78,29,82,29,87,29,92,29,96,29,99,29,102,29,105,29,110,29,115,29,119,29,122,29,125,29,128],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"0 0 760 209","img",[25,26],"lo-routes-t","lo-routes-d","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  ",[31,32,33],"title",{"id":25},"Three ways to turn a workbook into a PDF",[35,36,37],"desc",{"id":26},"Excel through COM produces the most faithful output but needs Windows and a licence, headless LibreOffice needs neither and is close enough, and rendering the data directly gives full control but rebuilds the layout.",[39,40],"rect",{"x":41,"y":41,"width":42,"height":43,"fill":44},"0","760","209","#ffffff",[39,46],{"x":47,"y":48,"width":49,"height":50,"rx":51,"fill":52,"stroke":53,"style":54},"20.0","26","226.7","141","14","#fdefd8","var(--gold,#b4740a)","stroke-width:2px",[56,57,61],"text",{"x":58,"y":59,"style":60},"133.35","52","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Excel via COM",[63,64],"line",{"x1":65,"y1":66,"x2":67,"y2":66,"stroke":53,"style":68},"36.0","62","230.7","stroke-width:1px",[56,70,73],{"x":58,"y":71,"style":72},"84","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","exact pagination",[56,75,77],{"x":58,"y":76,"style":72},"107","Windows and a licence",[56,79,81],{"x":58,"y":80,"style":72},"130","no container",[39,83],{"x":84,"y":48,"width":49,"height":50,"rx":51,"fill":85,"stroke":86,"style":54},"266.7","#d9f4f1","var(--teal,#0f9488)",[56,88,91],{"x":89,"y":59,"style":90},"380.04999999999995","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","LibreOffice headless",[63,93],{"x1":94,"y1":66,"x2":95,"y2":66,"stroke":86,"style":68},"282.7","477.4",[56,97,98],{"x":89,"y":71,"style":72},"no licence, no desktop",[56,100,101],{"x":89,"y":76,"style":72},"very close output",[56,103,104],{"x":89,"y":80,"style":72},"one command",[39,106],{"x":107,"y":48,"width":49,"height":50,"rx":51,"fill":108,"stroke":109,"style":54},"513.4","#f0f4ff","var(--brand,#5b5cf0)",[56,111,114],{"x":112,"y":59,"style":113},"626.75","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","render from data",[63,116],{"x1":117,"y1":66,"x2":118,"y2":66,"stroke":109,"style":68},"529.4","724.0999999999999",[56,120,121],{"x":112,"y":71,"style":72},"total control",[56,123,124],{"x":112,"y":76,"style":72},"rebuilds the layout",[56,126,127],{"x":112,"y":80,"style":72},"most work",[56,129,133],{"x":130,"y":131,"style":132},"380.0","195","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","the container case has one practical answer",[135,136,138],"h2",{"id":137},"prerequisites","Prerequisites",[140,141,146],"pre",{"className":142,"code":143,"language":144,"meta":145,"style":145},"language-bash shiki shiki-themes github-light github-dark-high-contrast","sudo apt-get install -y libreoffice-calc fonts-liberation\n","bash","",[147,148,149],"code",{"__ignoreMap":145},[150,151,153,157,161,164,168,171],"span",{"class":63,"line":152},1,[150,154,156],{"class":155},"sMTad","sudo",[150,158,160],{"class":159},"srMev"," apt-get",[150,162,163],{"class":159}," install",[150,165,167],{"class":166},"sP0c6"," -y",[150,169,170],{"class":159}," libreoffice-calc",[150,172,173],{"class":159}," fonts-liberation\n",[10,175,176,179,180,183],{},[147,177,178],{},"libreoffice-calc"," alone is enough — the full suite is several hundred megabytes more for no benefit\nhere. ",[147,181,182],{},"fonts-liberation"," supplies metric-compatible substitutes for Arial, Times New Roman and\nCourier New, which is what stops the output reflowing when those fonts are missing.",[135,185,187],{"id":186},"the-command","The command",[140,189,191],{"className":142,"code":190,"language":144,"meta":145,"style":145},"soffice --headless --convert-to pdf --outdir \u002Fout \u002Fin\u002Freport.xlsx\n",[147,192,193],{"__ignoreMap":145},[150,194,195,198,201,204,207,210,213],{"class":63,"line":152},[150,196,197],{"class":155},"soffice",[150,199,200],{"class":166}," --headless",[150,202,203],{"class":166}," --convert-to",[150,205,206],{"class":159}," pdf",[150,208,209],{"class":166}," --outdir",[150,211,212],{"class":159}," \u002Fout",[150,214,215],{"class":159}," \u002Fin\u002Freport.xlsx\n",[10,217,218],{},"That is the whole conversion. Wrapping it in Python adds the error handling that a scheduled job\nneeds:",[140,220,224],{"className":221,"code":222,"language":223,"meta":145,"style":145},"language-python shiki shiki-themes github-light github-dark-high-contrast","import shutil\nimport subprocess\nfrom pathlib import Path\n\ndef to_pdf(source: Path, out_dir: Path, profile: Path, timeout: int = 180) -> Path:\n    out_dir.mkdir(parents=True, exist_ok=True)\n    binary = shutil.which(\"soffice\") or shutil.which(\"libreoffice\")\n    if binary is None:\n        raise RuntimeError(\"LibreOffice is not installed or not on PATH\")\n\n    result = subprocess.run(\n        [binary,\n         f\"-env:UserInstallation=file:\u002F\u002F{profile}\",\n         \"--headless\", \"--norestore\", \"--nolockcheck\",\n         \"--convert-to\", \"pdf:calc_pdf_Export\",\n         \"--outdir\", str(out_dir), str(source)],\n        capture_output=True, text=True, timeout=timeout,\n    )\n    target = out_dir \u002F f\"{source.stem}.pdf\"\n    if result.returncode != 0 or not target.exists():\n        raise RuntimeError(f\"conversion failed ({result.returncode}): {result.stderr.strip()}\")\n    return target\n","python",[147,225,226,236,244,258,265,290,319,346,364,381,386,397,403,428,446,459,478,506,512,541,564,600],{"__ignoreMap":145},[150,227,228,232],{"class":63,"line":152},[150,229,231],{"class":230},"s-kum","import",[150,233,235],{"class":234},"skGVy"," shutil\n",[150,237,239,241],{"class":63,"line":238},2,[150,240,231],{"class":230},[150,242,243],{"class":234}," subprocess\n",[150,245,247,250,253,255],{"class":63,"line":246},3,[150,248,249],{"class":230},"from",[150,251,252],{"class":234}," pathlib ",[150,254,231],{"class":230},[150,256,257],{"class":234}," Path\n",[150,259,261],{"class":63,"line":260},4,[150,262,264],{"emptyLinePlaceholder":263},true,"\n",[150,266,268,271,275,278,281,284,287],{"class":63,"line":267},5,[150,269,270],{"class":230},"def",[150,272,274],{"class":273},"s_Opv"," to_pdf",[150,276,277],{"class":234},"(source: Path, out_dir: Path, profile: Path, timeout: ",[150,279,280],{"class":166},"int",[150,282,283],{"class":230}," =",[150,285,286],{"class":166}," 180",[150,288,289],{"class":234},") -> Path:\n",[150,291,293,296,300,303,306,309,312,314,316],{"class":63,"line":292},6,[150,294,295],{"class":234},"    out_dir.mkdir(",[150,297,299],{"class":298},"sa561","parents",[150,301,302],{"class":230},"=",[150,304,305],{"class":166},"True",[150,307,308],{"class":234},", ",[150,310,311],{"class":298},"exist_ok",[150,313,302],{"class":230},[150,315,305],{"class":166},[150,317,318],{"class":234},")\n",[150,320,322,325,327,330,333,336,339,341,344],{"class":63,"line":321},7,[150,323,324],{"class":234},"    binary ",[150,326,302],{"class":230},[150,328,329],{"class":234}," shutil.which(",[150,331,332],{"class":159},"\"soffice\"",[150,334,335],{"class":234},") ",[150,337,338],{"class":230},"or",[150,340,329],{"class":234},[150,342,343],{"class":159},"\"libreoffice\"",[150,345,318],{"class":234},[150,347,349,352,355,358,361],{"class":63,"line":348},8,[150,350,351],{"class":230},"    if",[150,353,354],{"class":234}," binary ",[150,356,357],{"class":230},"is",[150,359,360],{"class":166}," None",[150,362,363],{"class":234},":\n",[150,365,367,370,373,376,379],{"class":63,"line":366},9,[150,368,369],{"class":230},"        raise",[150,371,372],{"class":166}," RuntimeError",[150,374,375],{"class":234},"(",[150,377,378],{"class":159},"\"LibreOffice is not installed or not on PATH\"",[150,380,318],{"class":234},[150,382,384],{"class":63,"line":383},10,[150,385,264],{"emptyLinePlaceholder":263},[150,387,389,392,394],{"class":63,"line":388},11,[150,390,391],{"class":234},"    result ",[150,393,302],{"class":230},[150,395,396],{"class":234}," subprocess.run(\n",[150,398,400],{"class":63,"line":399},12,[150,401,402],{"class":234},"        [binary,\n",[150,404,406,409,412,416,419,422,425],{"class":63,"line":405},13,[150,407,408],{"class":230},"         f",[150,410,411],{"class":159},"\"-env:UserInstallation=file:\u002F\u002F",[150,413,415],{"class":414},"sSjpA","{",[150,417,418],{"class":234},"profile",[150,420,421],{"class":414},"}",[150,423,424],{"class":159},"\"",[150,426,427],{"class":234},",\n",[150,429,431,434,436,439,441,444],{"class":63,"line":430},14,[150,432,433],{"class":159},"         \"--headless\"",[150,435,308],{"class":234},[150,437,438],{"class":159},"\"--norestore\"",[150,440,308],{"class":234},[150,442,443],{"class":159},"\"--nolockcheck\"",[150,445,427],{"class":234},[150,447,449,452,454,457],{"class":63,"line":448},15,[150,450,451],{"class":159},"         \"--convert-to\"",[150,453,308],{"class":234},[150,455,456],{"class":159},"\"pdf:calc_pdf_Export\"",[150,458,427],{"class":234},[150,460,462,465,467,470,473,475],{"class":63,"line":461},16,[150,463,464],{"class":159},"         \"--outdir\"",[150,466,308],{"class":234},[150,468,469],{"class":166},"str",[150,471,472],{"class":234},"(out_dir), ",[150,474,469],{"class":166},[150,476,477],{"class":234},"(source)],\n",[150,479,481,484,486,488,490,492,494,496,498,501,503],{"class":63,"line":480},17,[150,482,483],{"class":298},"        capture_output",[150,485,302],{"class":230},[150,487,305],{"class":166},[150,489,308],{"class":234},[150,491,56],{"class":298},[150,493,302],{"class":230},[150,495,305],{"class":166},[150,497,308],{"class":234},[150,499,500],{"class":298},"timeout",[150,502,302],{"class":230},[150,504,505],{"class":234},"timeout,\n",[150,507,509],{"class":63,"line":508},18,[150,510,511],{"class":234},"    )\n",[150,513,515,518,520,523,526,529,531,533,536,538],{"class":63,"line":514},19,[150,516,517],{"class":234},"    target ",[150,519,302],{"class":230},[150,521,522],{"class":234}," out_dir ",[150,524,525],{"class":230},"\u002F",[150,527,528],{"class":230}," f",[150,530,424],{"class":159},[150,532,415],{"class":414},[150,534,535],{"class":234},"source.stem",[150,537,421],{"class":414},[150,539,540],{"class":159},".pdf\"\n",[150,542,544,546,549,552,555,558,561],{"class":63,"line":543},20,[150,545,351],{"class":230},[150,547,548],{"class":234}," result.returncode ",[150,550,551],{"class":230},"!=",[150,553,554],{"class":166}," 0",[150,556,557],{"class":230}," or",[150,559,560],{"class":230}," not",[150,562,563],{"class":234}," target.exists():\n",[150,565,567,569,571,573,576,579,581,584,586,589,591,594,596,598],{"class":63,"line":566},21,[150,568,369],{"class":230},[150,570,372],{"class":166},[150,572,375],{"class":234},[150,574,575],{"class":230},"f",[150,577,578],{"class":159},"\"conversion failed (",[150,580,415],{"class":414},[150,582,583],{"class":234},"result.returncode",[150,585,421],{"class":414},[150,587,588],{"class":159},"): ",[150,590,415],{"class":414},[150,592,593],{"class":234},"result.stderr.strip()",[150,595,421],{"class":414},[150,597,424],{"class":159},[150,599,318],{"class":234},[150,601,603,606],{"class":63,"line":602},22,[150,604,605],{"class":230},"    return",[150,607,608],{"class":234}," target\n",[10,610,611,612,615,616,619],{},"Three flags earn their place. ",[147,613,614],{},"-env:UserInstallation"," gives this process its own profile directory,\nwhich is what allows concurrent conversions. ",[147,617,618],{},"--norestore"," stops LibreOffice trying to recover a\ndocument after a previous crash — a recovery dialog in a headless process is a hang. And checking\nthat the output file exists matters because LibreOffice sometimes returns zero having produced\nnothing.",[135,621,623],{"id":622},"setting-up-the-page-before-converting","Setting up the page before converting",[20,625,29,630,29,633,29,636,29,639,29,645,29,651,29,656,29,662,29,667,29,670,29,673,29,677,29,681,29,684,29,687,29,690,29,694,29,698,29,702,29,705,29,708,29,712,29,716,29,720],{"viewBox":626,"role":23,"ariaLabelledBy":627,"xmlns":27,"style":28},"0 0 760 308",[628,629],"lo-page-t","lo-page-d",[31,631,632],{"id":628},"The workbook settings that decide whether the PDF is readable",[35,634,635],{"id":629},"Enable fit-to-page on the sheet properties before setting fit-to-width, define the print area, repeat the header row on every page, and add a page footer.",[39,637],{"x":41,"y":41,"width":42,"height":638,"fill":44},"308",[39,640],{"x":641,"y":642,"width":643,"height":59,"rx":644,"fill":85,"stroke":86},"22","28","716","10",[646,647],"circle",{"cx":59,"cy":648,"r":649,"fill":650},"54.0","15","#5b5cf0",[56,652,655],{"x":59,"y":653,"style":654},"59.0","font-size:13px;font-weight:700;fill:#ffffff;text-anchor:middle","1",[56,657,661],{"x":658,"y":659,"style":660},"82","50","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","Enable fitToPage",[56,663,666],{"x":658,"y":664,"style":665},"68","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:start","fitToWidth does nothing without it",[39,668],{"x":641,"y":669,"width":643,"height":59,"rx":644,"fill":85,"stroke":86},"90",[646,671],{"cx":59,"cy":672,"r":649,"fill":650},"116.0",[56,674,676],{"x":59,"y":675,"style":654},"121.0","2",[56,678,680],{"x":658,"y":679,"style":660},"112","Set the print area",[56,682,683],{"x":658,"y":80,"style":665},"otherwise every stray cell is included",[39,685],{"x":641,"y":686,"width":643,"height":59,"rx":644,"fill":85,"stroke":86},"152",[646,688],{"cx":59,"cy":689,"r":649,"fill":650},"178.0",[56,691,693],{"x":59,"y":692,"style":654},"183.0","3",[56,695,697],{"x":658,"y":696,"style":660},"174","Repeat the header row",[56,699,701],{"x":658,"y":700,"style":665},"192","print_title_rows keeps page four readable",[39,703],{"x":641,"y":704,"width":643,"height":59,"rx":644,"fill":85,"stroke":86},"214",[646,706],{"cx":59,"cy":707,"r":649,"fill":650},"240.0",[56,709,711],{"x":59,"y":710,"style":654},"245.0","4",[56,713,715],{"x":658,"y":714,"style":660},"236","Add a footer",[56,717,719],{"x":658,"y":718,"style":665},"254","Page &P of &N, and a generation date",[56,721,723],{"x":130,"y":722,"style":132},"296","the converter is rarely the reason a PDF looks wrong",[10,725,726],{},"The single biggest determinant of output quality is not the converter but the workbook. A sheet with\nno print area and no fit-to-width setting becomes a PDF spread across nine pages with columns split\ndown the middle.",[140,728,730],{"className":221,"code":729,"language":223,"meta":145,"style":145},"from openpyxl import load_workbook\n\nbook = load_workbook(\"report.xlsx\")\nsheet = book[\"Summary\"]\n\nsheet.page_setup.orientation = \"landscape\"\nsheet.page_setup.fitToWidth = 1\nsheet.page_setup.fitToHeight = 0          # as many pages tall as needed\nsheet.sheet_properties.pageSetUpPr.fitToPage = True\nsheet.print_area = f\"A1:H{sheet.max_row}\"\nsheet.print_title_rows = \"1:1\"            # repeat the header on every page\nsheet.oddFooter.center.text = \"Page &P of &N\"\nbook.save(\"report-print-ready.xlsx\")\n",[147,731,732,744,748,763,779,783,793,803,816,826,848,861,871],{"__ignoreMap":145},[150,733,734,736,739,741],{"class":63,"line":152},[150,735,249],{"class":230},[150,737,738],{"class":234}," openpyxl ",[150,740,231],{"class":230},[150,742,743],{"class":234}," load_workbook\n",[150,745,746],{"class":63,"line":238},[150,747,264],{"emptyLinePlaceholder":263},[150,749,750,753,755,758,761],{"class":63,"line":246},[150,751,752],{"class":234},"book ",[150,754,302],{"class":230},[150,756,757],{"class":234}," load_workbook(",[150,759,760],{"class":159},"\"report.xlsx\"",[150,762,318],{"class":234},[150,764,765,768,770,773,776],{"class":63,"line":260},[150,766,767],{"class":234},"sheet ",[150,769,302],{"class":230},[150,771,772],{"class":234}," book[",[150,774,775],{"class":159},"\"Summary\"",[150,777,778],{"class":234},"]\n",[150,780,781],{"class":63,"line":267},[150,782,264],{"emptyLinePlaceholder":263},[150,784,785,788,790],{"class":63,"line":292},[150,786,787],{"class":234},"sheet.page_setup.orientation ",[150,789,302],{"class":230},[150,791,792],{"class":159}," \"landscape\"\n",[150,794,795,798,800],{"class":63,"line":321},[150,796,797],{"class":234},"sheet.page_setup.fitToWidth ",[150,799,302],{"class":230},[150,801,802],{"class":166}," 1\n",[150,804,805,808,810,812],{"class":63,"line":348},[150,806,807],{"class":234},"sheet.page_setup.fitToHeight ",[150,809,302],{"class":230},[150,811,554],{"class":166},[150,813,815],{"class":814},"s-wDw","          # as many pages tall as needed\n",[150,817,818,821,823],{"class":63,"line":366},[150,819,820],{"class":234},"sheet.sheet_properties.pageSetUpPr.fitToPage ",[150,822,302],{"class":230},[150,824,825],{"class":166}," True\n",[150,827,828,831,833,835,838,840,843,845],{"class":63,"line":383},[150,829,830],{"class":234},"sheet.print_area ",[150,832,302],{"class":230},[150,834,528],{"class":230},[150,836,837],{"class":159},"\"A1:H",[150,839,415],{"class":414},[150,841,842],{"class":234},"sheet.max_row",[150,844,421],{"class":414},[150,846,847],{"class":159},"\"\n",[150,849,850,853,855,858],{"class":63,"line":388},[150,851,852],{"class":234},"sheet.print_title_rows ",[150,854,302],{"class":230},[150,856,857],{"class":159}," \"1:1\"",[150,859,860],{"class":814},"            # repeat the header on every page\n",[150,862,863,866,868],{"class":63,"line":399},[150,864,865],{"class":234},"sheet.oddFooter.center.text ",[150,867,302],{"class":230},[150,869,870],{"class":159}," \"Page &P of &N\"\n",[150,872,873,876,879],{"class":63,"line":405},[150,874,875],{"class":234},"book.save(",[150,877,878],{"class":159},"\"report-print-ready.xlsx\"",[150,880,318],{"class":234},[10,882,883,886,887,890,891,894,895,899],{},[147,884,885],{},"fitToPage = True"," on the sheet properties is required for ",[147,888,889],{},"fitToWidth"," to take effect — setting the\nwidth alone silently does nothing, which is the same trap as Excel's zoom-versus-fit interaction.\n",[147,892,893],{},"print_title_rows"," is what keeps the header visible on page four, and it is the difference between a\nPDF somebody reads and one they scroll back and forth in.\n",[14,896,898],{"href":897},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fset-print-area-and-page-setup-with-openpyxl\u002F","Set the Print Area and Page Setup with openpyxl","\ncovers the full property set.",[135,901,903],{"id":902},"converting-only-some-sheets","Converting only some sheets",[10,905,906],{},"LibreOffice converts every sheet with a print area. To produce a PDF of one tab, remove the others\nfirst — from a copy, obviously.",[140,908,910],{"className":221,"code":909,"language":223,"meta":145,"style":145},"from openpyxl import load_workbook\n\ndef single_sheet_copy(source: str, keep: str, target: str) -> str:\n    book = load_workbook(source)\n    for name in list(book.sheetnames):\n        if name != keep:\n            del book[name]\n    book.save(target)\n    return target\n\nto_pdf(Path(single_sheet_copy(\"report.xlsx\", \"Summary\", \"summary-only.xlsx\")), ...)\n",[147,911,912,922,926,955,965,982,994,1002,1007,1013,1017],{"__ignoreMap":145},[150,913,914,916,918,920],{"class":63,"line":152},[150,915,249],{"class":230},[150,917,738],{"class":234},[150,919,231],{"class":230},[150,921,743],{"class":234},[150,923,924],{"class":63,"line":238},[150,925,264],{"emptyLinePlaceholder":263},[150,927,928,930,933,936,938,941,943,946,948,951,953],{"class":63,"line":246},[150,929,270],{"class":230},[150,931,932],{"class":273}," single_sheet_copy",[150,934,935],{"class":234},"(source: ",[150,937,469],{"class":166},[150,939,940],{"class":234},", keep: ",[150,942,469],{"class":166},[150,944,945],{"class":234},", target: ",[150,947,469],{"class":166},[150,949,950],{"class":234},") -> ",[150,952,469],{"class":166},[150,954,363],{"class":234},[150,956,957,960,962],{"class":63,"line":260},[150,958,959],{"class":234},"    book ",[150,961,302],{"class":230},[150,963,964],{"class":234}," load_workbook(source)\n",[150,966,967,970,973,976,979],{"class":63,"line":267},[150,968,969],{"class":230},"    for",[150,971,972],{"class":234}," name ",[150,974,975],{"class":230},"in",[150,977,978],{"class":166}," list",[150,980,981],{"class":234},"(book.sheetnames):\n",[150,983,984,987,989,991],{"class":63,"line":292},[150,985,986],{"class":230},"        if",[150,988,972],{"class":234},[150,990,551],{"class":230},[150,992,993],{"class":234}," keep:\n",[150,995,996,999],{"class":63,"line":321},[150,997,998],{"class":230},"            del",[150,1000,1001],{"class":234}," book[name]\n",[150,1003,1004],{"class":63,"line":348},[150,1005,1006],{"class":234},"    book.save(target)\n",[150,1008,1009,1011],{"class":63,"line":366},[150,1010,605],{"class":230},[150,1012,608],{"class":234},[150,1014,1015],{"class":63,"line":383},[150,1016,264],{"emptyLinePlaceholder":263},[150,1018,1019,1022,1024,1026,1028,1030,1033,1036,1039],{"class":63,"line":388},[150,1020,1021],{"class":234},"to_pdf(Path(single_sheet_copy(",[150,1023,760],{"class":159},[150,1025,308],{"class":234},[150,1027,775],{"class":159},[150,1029,308],{"class":234},[150,1031,1032],{"class":159},"\"summary-only.xlsx\"",[150,1034,1035],{"class":234},")), ",[150,1037,1038],{"class":166},"...",[150,1040,318],{"class":234},[10,1042,1043,1044,1048],{},"Deleting sheets from a loaded copy is cheaper and more predictable than trying to control the\nconversion, and it works identically on every platform — which the alternative,\n",[14,1045,1047],{"href":1046},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-only-selected-sheets-of-a-workbook-to-pdf\u002F","Convert Only Selected Sheets of a Workbook to PDF",",\ndiscusses in the Excel-driven case.",[135,1050,1052],{"id":1051},"running-it-in-a-container","Running it in a container",[140,1054,1058],{"className":1055,"code":1056,"language":1057,"meta":145,"style":145},"language-dockerfile shiki shiki-themes github-light github-dark-high-contrast","FROM python:3.12-slim\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n        libreoffice-calc fonts-liberation \\\n    && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n\nENV LO_PROFILE=\u002Ftmp\u002Floprofile\nRUN mkdir -p $LO_PROFILE && \\\n    soffice -env:UserInstallation=file:\u002F\u002F$LO_PROFILE --headless \\\n            --convert-to pdf --outdir \u002Ftmp \u002Fusr\u002Fshare\u002Fdoc\u002F*\u002Fcopyright 2>\u002Fdev\u002Fnull || true\n\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY . \u002Fapp\nWORKDIR \u002Fapp\nCMD [\"python\", \"report.py\"]\n","dockerfile",[147,1059,1060,1065,1069,1074,1079,1084,1088,1093,1098,1103,1108,1112,1117,1122,1127,1132],{"__ignoreMap":145},[150,1061,1062],{"class":63,"line":152},[150,1063,1064],{},"FROM python:3.12-slim\n",[150,1066,1067],{"class":63,"line":238},[150,1068,264],{"emptyLinePlaceholder":263},[150,1070,1071],{"class":63,"line":246},[150,1072,1073],{},"RUN apt-get update && apt-get install -y --no-install-recommends \\\n",[150,1075,1076],{"class":63,"line":260},[150,1077,1078],{},"        libreoffice-calc fonts-liberation \\\n",[150,1080,1081],{"class":63,"line":267},[150,1082,1083],{},"    && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n",[150,1085,1086],{"class":63,"line":292},[150,1087,264],{"emptyLinePlaceholder":263},[150,1089,1090],{"class":63,"line":321},[150,1091,1092],{},"ENV LO_PROFILE=\u002Ftmp\u002Floprofile\n",[150,1094,1095],{"class":63,"line":348},[150,1096,1097],{},"RUN mkdir -p $LO_PROFILE && \\\n",[150,1099,1100],{"class":63,"line":366},[150,1101,1102],{},"    soffice -env:UserInstallation=file:\u002F\u002F$LO_PROFILE --headless \\\n",[150,1104,1105],{"class":63,"line":383},[150,1106,1107],{},"            --convert-to pdf --outdir \u002Ftmp \u002Fusr\u002Fshare\u002Fdoc\u002F*\u002Fcopyright 2>\u002Fdev\u002Fnull || true\n",[150,1109,1110],{"class":63,"line":388},[150,1111,264],{"emptyLinePlaceholder":263},[150,1113,1114],{"class":63,"line":399},[150,1115,1116],{},"COPY requirements.txt .\n",[150,1118,1119],{"class":63,"line":405},[150,1120,1121],{},"RUN pip install --no-cache-dir -r requirements.txt\n",[150,1123,1124],{"class":63,"line":430},[150,1125,1126],{},"COPY . \u002Fapp\n",[150,1128,1129],{"class":63,"line":448},[150,1130,1131],{},"WORKDIR \u002Fapp\n",[150,1133,1134],{"class":63,"line":461},[150,1135,1136],{},"CMD [\"python\", \"report.py\"]\n",[10,1138,1139,1140,1144],{},"The warm-up line is the one people leave out and then spend an afternoon on: the first LibreOffice\nrun builds a profile and takes ten to twenty seconds, so doing it at build time means the first\nreport of the day is not the slow one. The broader containerisation argument is in\n",[14,1141,1143],{"href":1142},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-a-python-excel-report-in-docker\u002F","Run a Python Excel Report in Docker",".",[135,1146,1148],{"id":1147},"checking-the-pdf-before-it-goes-out","Checking the PDF before it goes out",[10,1150,1151],{},"A conversion that returns successfully can still produce something nobody should send — a single\nblank page because the print area was empty, or forty pages because it was not set. Two checks catch\nboth, and neither needs a PDF library beyond a small one.",[140,1153,1155],{"className":221,"code":1154,"language":223,"meta":145,"style":145},"from pypdf import PdfReader\n\ndef check_pdf(path, min_pages=1, max_pages=20, min_bytes=5_000):\n    size = path.stat().st_size\n    if size \u003C min_bytes:\n        raise ValueError(f\"{path.name} is only {size} bytes — probably empty\")\n    pages = len(PdfReader(str(path)).pages)\n    if not (min_pages \u003C= pages \u003C= max_pages):\n        raise ValueError(f\"{path.name} has {pages} pages, expected {min_pages}-{max_pages}\")\n    first = PdfReader(str(path)).pages[0].extract_text() or \"\"\n    if len(first.strip()) \u003C 40:\n        raise ValueError(f\"{path.name} page 1 has almost no text — check the print area\")\n    return pages\n",[147,1156,1157,1169,1173,1206,1216,1229,1264,1282,1302,1354,1379,1395,1418],{"__ignoreMap":145},[150,1158,1159,1161,1164,1166],{"class":63,"line":152},[150,1160,249],{"class":230},[150,1162,1163],{"class":234}," pypdf ",[150,1165,231],{"class":230},[150,1167,1168],{"class":234}," PdfReader\n",[150,1170,1171],{"class":63,"line":238},[150,1172,264],{"emptyLinePlaceholder":263},[150,1174,1175,1177,1180,1183,1185,1187,1190,1192,1195,1198,1200,1203],{"class":63,"line":246},[150,1176,270],{"class":230},[150,1178,1179],{"class":273}," check_pdf",[150,1181,1182],{"class":234},"(path, min_pages",[150,1184,302],{"class":230},[150,1186,655],{"class":166},[150,1188,1189],{"class":234},", max_pages",[150,1191,302],{"class":230},[150,1193,1194],{"class":166},"20",[150,1196,1197],{"class":234},", min_bytes",[150,1199,302],{"class":230},[150,1201,1202],{"class":166},"5_000",[150,1204,1205],{"class":234},"):\n",[150,1207,1208,1211,1213],{"class":63,"line":260},[150,1209,1210],{"class":234},"    size ",[150,1212,302],{"class":230},[150,1214,1215],{"class":234}," path.stat().st_size\n",[150,1217,1218,1220,1223,1226],{"class":63,"line":267},[150,1219,351],{"class":230},[150,1221,1222],{"class":234}," size ",[150,1224,1225],{"class":230},"\u003C",[150,1227,1228],{"class":234}," min_bytes:\n",[150,1230,1231,1233,1236,1238,1240,1242,1244,1247,1249,1252,1254,1257,1259,1262],{"class":63,"line":292},[150,1232,369],{"class":230},[150,1234,1235],{"class":166}," ValueError",[150,1237,375],{"class":234},[150,1239,575],{"class":230},[150,1241,424],{"class":159},[150,1243,415],{"class":414},[150,1245,1246],{"class":234},"path.name",[150,1248,421],{"class":414},[150,1250,1251],{"class":159}," is only ",[150,1253,415],{"class":414},[150,1255,1256],{"class":234},"size",[150,1258,421],{"class":414},[150,1260,1261],{"class":159}," bytes — probably empty\"",[150,1263,318],{"class":234},[150,1265,1266,1269,1271,1274,1277,1279],{"class":63,"line":321},[150,1267,1268],{"class":234},"    pages ",[150,1270,302],{"class":230},[150,1272,1273],{"class":166}," len",[150,1275,1276],{"class":234},"(PdfReader(",[150,1278,469],{"class":166},[150,1280,1281],{"class":234},"(path)).pages)\n",[150,1283,1284,1286,1288,1291,1294,1297,1299],{"class":63,"line":348},[150,1285,351],{"class":230},[150,1287,560],{"class":230},[150,1289,1290],{"class":234}," (min_pages ",[150,1292,1293],{"class":230},"\u003C=",[150,1295,1296],{"class":234}," pages ",[150,1298,1293],{"class":230},[150,1300,1301],{"class":234}," max_pages):\n",[150,1303,1304,1306,1308,1310,1312,1314,1316,1318,1320,1323,1325,1328,1330,1333,1335,1338,1340,1343,1345,1348,1350,1352],{"class":63,"line":366},[150,1305,369],{"class":230},[150,1307,1235],{"class":166},[150,1309,375],{"class":234},[150,1311,575],{"class":230},[150,1313,424],{"class":159},[150,1315,415],{"class":414},[150,1317,1246],{"class":234},[150,1319,421],{"class":414},[150,1321,1322],{"class":159}," has ",[150,1324,415],{"class":414},[150,1326,1327],{"class":234},"pages",[150,1329,421],{"class":414},[150,1331,1332],{"class":159}," pages, expected ",[150,1334,415],{"class":414},[150,1336,1337],{"class":234},"min_pages",[150,1339,421],{"class":414},[150,1341,1342],{"class":159},"-",[150,1344,415],{"class":414},[150,1346,1347],{"class":234},"max_pages",[150,1349,421],{"class":414},[150,1351,424],{"class":159},[150,1353,318],{"class":234},[150,1355,1356,1359,1361,1364,1366,1369,1371,1374,1376],{"class":63,"line":383},[150,1357,1358],{"class":234},"    first ",[150,1360,302],{"class":230},[150,1362,1363],{"class":234}," PdfReader(",[150,1365,469],{"class":166},[150,1367,1368],{"class":234},"(path)).pages[",[150,1370,41],{"class":166},[150,1372,1373],{"class":234},"].extract_text() ",[150,1375,338],{"class":230},[150,1377,1378],{"class":159}," \"\"\n",[150,1380,1381,1383,1385,1388,1390,1393],{"class":63,"line":388},[150,1382,351],{"class":230},[150,1384,1273],{"class":166},[150,1386,1387],{"class":234},"(first.strip()) ",[150,1389,1225],{"class":230},[150,1391,1392],{"class":166}," 40",[150,1394,363],{"class":234},[150,1396,1397,1399,1401,1403,1405,1407,1409,1411,1413,1416],{"class":63,"line":399},[150,1398,369],{"class":230},[150,1400,1235],{"class":166},[150,1402,375],{"class":234},[150,1404,575],{"class":230},[150,1406,424],{"class":159},[150,1408,415],{"class":414},[150,1410,1246],{"class":234},[150,1412,421],{"class":414},[150,1414,1415],{"class":159}," page 1 has almost no text — check the print area\"",[150,1417,318],{"class":234},[150,1419,1420,1422],{"class":63,"line":405},[150,1421,605],{"class":230},[150,1423,1424],{"class":234}," pages\n",[10,1426,1427,1428,1432],{},"The page-count bound is the most useful of the three, because a report whose length is stable month\nto month makes an excellent canary: a jump from four pages to forty means the print area was lost,\nand a drop to one means the data did not arrive. Wiring that into the pre-send checks described in\n",[14,1429,1431],{"href":1430},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it\u002F","Validate an Excel Report Before Sending It","\nturns a silent formatting regression into a failed run.",[135,1434,1436],{"id":1435},"fonts-and-why-the-output-moves-between-machines","Fonts, and why the output moves between machines",[10,1438,1439],{},"The most frustrating difference between a conversion on a laptop and one in a container is font\nsubstitution. A workbook specifying Calibri renders with Calibri locally and with whatever the\ncontainer decided is closest otherwise — and because the substitute has different metrics, columns\nreflow and the page count changes.",[10,1441,1442,1443,1445,1446,1449],{},"Two fixes work. Install metric-compatible fonts, which is what ",[147,1444,182],{}," provides for the\nMicrosoft core set, and add ",[147,1447,1448],{},"fonts-crosextra-carlito"," for a Calibri equivalent. Or standardise the\nworkbook on a font that is present everywhere, which is less satisfying and completely reliable.",[140,1451,1453],{"className":142,"code":1452,"language":144,"meta":145,"style":145},"apt-get install -y fonts-liberation fonts-crosextra-carlito fonts-crosextra-caladea\nfc-list | grep -i carlito        # confirm it was picked up\n",[147,1454,1455,1473],{"__ignoreMap":145},[150,1456,1457,1460,1462,1464,1467,1470],{"class":63,"line":152},[150,1458,1459],{"class":155},"apt-get",[150,1461,163],{"class":159},[150,1463,167],{"class":166},[150,1465,1466],{"class":159}," fonts-liberation",[150,1468,1469],{"class":159}," fonts-crosextra-carlito",[150,1471,1472],{"class":159}," fonts-crosextra-caladea\n",[150,1474,1475,1478,1481,1484,1487,1490],{"class":63,"line":238},[150,1476,1477],{"class":155},"fc-list",[150,1479,1480],{"class":230}," |",[150,1482,1483],{"class":155}," grep",[150,1485,1486],{"class":166}," -i",[150,1488,1489],{"class":159}," carlito",[150,1491,1492],{"class":814},"        # confirm it was picked up\n",[10,1494,1495,1496,1498],{},"Carlito is metric-compatible with Calibri and Caladea with Cambria, which between them cover the\ndefault fonts of every Excel template anybody is likely to hand you. Confirming with ",[147,1497,1477],{}," during\nthe image build is worth the line — a font package that installed but was not registered produces\nexactly the same reflow as one that was never installed.",[135,1500,1502],{"id":1501},"common-pitfalls","Common pitfalls",[1504,1505,1506,1522],"table",{},[1507,1508,1509],"thead",{},[1510,1511,1512,1516,1519],"tr",{},[1513,1514,1515],"th",{},"Symptom",[1513,1517,1518],{},"Cause",[1513,1520,1521],{},"Fix",[1523,1524,1525,1539,1550,1564,1582,1596],"tbody",{},[1510,1526,1527,1531,1534],{},[1528,1529,1530],"td",{},"Conversion hangs indefinitely",[1528,1532,1533],{},"A recovery or first-run dialog",[1528,1535,1536,1538],{},[147,1537,618],{},", a dedicated profile, and a subprocess timeout",[1510,1540,1541,1544,1547],{},[1528,1542,1543],{},"Exit code 0 but no PDF",[1528,1545,1546],{},"LibreOffice failed silently",[1528,1548,1549],{},"Assert the output file exists",[1510,1551,1552,1555,1558],{},[1528,1553,1554],{},"Fonts look wrong, layout reflows",[1528,1556,1557],{},"Metric-compatible fonts missing",[1528,1559,1560,1561,1563],{},"Install ",[147,1562,182],{},", or embed the fonts you use",[1510,1565,1566,1569,1572],{},[1528,1567,1568],{},"Columns split across pages",[1528,1570,1571],{},"No fit-to-width setting",[1528,1573,1574,1575,1578,1579,1581],{},"Set ",[147,1576,1577],{},"fitToPage"," and ",[147,1580,889],{}," before converting",[1510,1583,1584,1587,1590],{},[1528,1585,1586],{},"Concurrent conversions interfere",[1528,1588,1589],{},"Shared user profile",[1528,1591,1592,1593,1595],{},"One ",[147,1594,614],{}," directory per worker",[1510,1597,1598,1601,1604],{},[1528,1599,1600],{},"First conversion of the day is slow",[1528,1602,1603],{},"Cold profile",[1528,1605,1606],{},"Warm the profile during the image build",[135,1608,1610],{"id":1609},"performance-and-scale","Performance and scale",[20,1612,29,1618,29,1621,29,1624,29,1628,29,1633,29,1640,29,1649,29,1655,29,1659,29,1661,29,1665,29,1670,29,1674,29,1677,29,1681,29,1686,29,1690],{"viewBox":1613,"role":23,"ariaLabelledBy":1614,"xmlns":27,"style":1617},"0 0 720 196",[1615,1616],"lo-batch-t","lo-batch-d","width:100%;max-width:720px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[31,1619,1620],{"id":1615},"Startup dominates the cost of a conversion",[35,1622,1623],{"id":1616},"Launching LibreOffice takes one to three seconds while converting a modest workbook takes a fraction of one, so converting thirty files in a single invocation is far cheaper than thirty invocations.",[39,1625],{"x":41,"y":41,"width":1626,"height":1627,"fill":44},"720","196",[56,1629,1632],{"x":1194,"y":1630,"style":1631},"56","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:start","30 separate runs",[39,1634],{"x":1635,"y":1636,"width":130,"height":48,"rx":1637,"fill":1638,"stroke":1639},"200","40","6","#e7ebef","var(--line,#cdd5e6)",[39,1641],{"x":1642,"y":1643,"width":1644,"height":1645,"rx":1646,"fill":1647,"stroke":1648},"201","41","378.0","24","5","#fee8f2","var(--accent,#d81b73)",[56,1650,1654],{"x":1651,"y":1652,"style":1653},"592.0","58","font-size:12px;font-weight:700;fill:var(--accent,#d81b73);text-anchor:start","30 startups",[56,1656,1658],{"x":1194,"y":1657,"style":1631},"100","batches of 10",[39,1660],{"x":1635,"y":71,"width":130,"height":48,"rx":1637,"fill":1638,"stroke":1639},[39,1662],{"x":1642,"y":1663,"width":1664,"height":1645,"rx":1646,"fill":52,"stroke":53},"85","133.5",[56,1666,1669],{"x":1651,"y":1667,"style":1668},"102","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","3 startups",[56,1671,1673],{"x":1194,"y":1672,"style":1631},"144","one invocation",[39,1675],{"x":1635,"y":1676,"width":130,"height":48,"rx":1637,"fill":1638,"stroke":1639},"128",[39,1678],{"x":1642,"y":1679,"width":1680,"height":1645,"rx":1646,"fill":85,"stroke":86},"129","41.9",[56,1682,1685],{"x":1651,"y":1683,"style":1684},"146","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","1 startup",[56,1687,1689],{"x":1194,"y":1194,"style":1688},"font-size:11.5px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:start","relative cost",[56,1691,1694],{"x":1692,"y":1693,"style":132},"360.0","186","batching trades error granularity for wall-clock time",[10,1696,1697],{},"LibreOffice startup dominates: roughly one to three seconds per invocation once the profile is warm,\nagainst a fraction of a second for the conversion itself on a modest workbook. Converting thirty\nreports therefore benefits enormously from passing them to one invocation rather than thirty.",[140,1699,1701],{"className":221,"code":1700,"language":223,"meta":145,"style":145},"subprocess.run(\n    [binary, f\"-env:UserInstallation=file:\u002F\u002F{profile}\", \"--headless\", \"--norestore\",\n     \"--convert-to\", \"pdf\", \"--outdir\", str(out_dir), *[str(p) for p in paths]],\n    check=True, capture_output=True, timeout=900,\n)\n",[147,1702,1703,1708,1736,1779,1808],{"__ignoreMap":145},[150,1704,1705],{"class":63,"line":152},[150,1706,1707],{"class":234},"subprocess.run(\n",[150,1709,1710,1713,1715,1717,1719,1721,1723,1725,1727,1730,1732,1734],{"class":63,"line":238},[150,1711,1712],{"class":234},"    [binary, ",[150,1714,575],{"class":230},[150,1716,411],{"class":159},[150,1718,415],{"class":414},[150,1720,418],{"class":234},[150,1722,421],{"class":414},[150,1724,424],{"class":159},[150,1726,308],{"class":234},[150,1728,1729],{"class":159},"\"--headless\"",[150,1731,308],{"class":234},[150,1733,438],{"class":159},[150,1735,427],{"class":234},[150,1737,1738,1741,1743,1746,1748,1751,1753,1755,1757,1760,1763,1765,1768,1771,1774,1776],{"class":63,"line":246},[150,1739,1740],{"class":159},"     \"--convert-to\"",[150,1742,308],{"class":234},[150,1744,1745],{"class":159},"\"pdf\"",[150,1747,308],{"class":234},[150,1749,1750],{"class":159},"\"--outdir\"",[150,1752,308],{"class":234},[150,1754,469],{"class":166},[150,1756,472],{"class":234},[150,1758,1759],{"class":230},"*",[150,1761,1762],{"class":234},"[",[150,1764,469],{"class":166},[150,1766,1767],{"class":234},"(p) ",[150,1769,1770],{"class":230},"for",[150,1772,1773],{"class":234}," p ",[150,1775,975],{"class":230},[150,1777,1778],{"class":234}," paths]],\n",[150,1780,1781,1784,1786,1788,1790,1793,1795,1797,1799,1801,1803,1806],{"class":63,"line":260},[150,1782,1783],{"class":298},"    check",[150,1785,302],{"class":230},[150,1787,305],{"class":166},[150,1789,308],{"class":234},[150,1791,1792],{"class":298},"capture_output",[150,1794,302],{"class":230},[150,1796,305],{"class":166},[150,1798,308],{"class":234},[150,1800,500],{"class":298},[150,1802,302],{"class":230},[150,1804,1805],{"class":166},"900",[150,1807,427],{"class":234},[150,1809,1810],{"class":63,"line":267},[150,1811,318],{"class":234},[10,1813,1814],{},"The trade is error granularity — one failure in a batch is harder to attribute — so a reasonable\nmiddle ground is batches of five or ten with a per-batch timeout. For genuine parallelism, run\nseveral processes each with its own profile directory; sharing one is the reliable way to produce\nintermittent, unreproducible failures.",[135,1816,1818],{"id":1817},"conclusion","Conclusion",[10,1820,1821,1824,1825,1827],{},[147,1822,1823],{},"soffice --headless --convert-to pdf"," converts a workbook without Excel, a licence or a desktop, and\nit is the standard answer for a Linux reporting container. Give every process its own profile\ndirectory, pass ",[147,1826,618],{}," and a timeout, assert the output file exists rather than trusting the\nexit code, and — most importantly — set the print area and fit-to-width on the workbook before\nconverting, because that is what decides whether the PDF is readable.",[135,1829,1831],{"id":1830},"frequently-asked-questions","Frequently asked questions",[10,1833,1834,1838],{},[1835,1836,1837],"strong",{},"Does LibreOffice render exactly like Excel?","\nClose, not identical. Page breaks, some fonts and a few chart styles differ. For a report you generate yourself, setting the page setup explicitly — fit to width, defined print area, standard fonts — removes almost all of the visible difference.",[10,1840,1841,1844],{},[1835,1842,1843],{},"Why does the first conversion take so long?","\nLibreOffice builds a user profile on first run. Point it at a dedicated profile directory with the -env switch and warm it once during the container build so the first real conversion is not the slow one.",[10,1846,1847,1850],{},[1835,1848,1849],{},"Can I convert several files at once?","\nYou can pass several paths to one invocation, which amortises the startup cost. What you cannot do is run several soffice processes sharing one user profile — they collide. Give each concurrent worker its own profile directory.",[10,1852,1853,1856],{},[1835,1854,1855],{},"Is a headless LibreOffice licence-free for server use?","\nYes, it is free software under the Mozilla Public License, with no per-seat or server licensing. That is the main reason it is the standard answer for PDF conversion in a container, where an Excel licence would not be.",[135,1858,1860],{"id":1859},"related","Related",[1862,1863,1864,1871,1878,1883,1890],"ul",{},[1865,1866,1867,1868,1870],"li",{},"Up one level: ",[14,1869,17],{"href":16}," — the wider set of conversion routes.",[1865,1872,1873,1877],{},[14,1874,1876],{"href":1875},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python\u002F","Convert an Excel File to PDF with Python"," — the Windows and Excel-driven alternatives.",[1865,1879,1880,1882],{},[14,1881,1047],{"href":1046}," — controlling what ends up in the file.",[1865,1884,1885,1889],{},[14,1886,1888],{"href":1887},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fadd-headers-footers-and-page-numbers-to-an-excel-pdf\u002F","Add Headers, Footers and Page Numbers to an Excel PDF"," — the page furniture this conversion carries through.",[1865,1891,1892,1894],{},[14,1893,1143],{"href":1142}," — packaging the converter with the job.",[1896,1897,1898],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}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 .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":145,"searchDepth":238,"depth":238,"links":1900},[1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912],{"id":137,"depth":238,"text":138},{"id":186,"depth":238,"text":187},{"id":622,"depth":238,"text":623},{"id":902,"depth":238,"text":903},{"id":1051,"depth":238,"text":1052},{"id":1147,"depth":238,"text":1148},{"id":1435,"depth":238,"text":1436},{"id":1501,"depth":238,"text":1502},{"id":1609,"depth":238,"text":1610},{"id":1817,"depth":238,"text":1818},{"id":1830,"depth":238,"text":1831},{"id":1859,"depth":238,"text":1860},"2026-09-04","Headless LibreOffice converts a workbook to PDF with no Excel, licence or desktop session — with the profile, timeout and page-setup details that make it reliable in a container.","md",[1917,1919,1921,1923],{"q":1837,"a":1918},"Close, not identical. Page breaks, some fonts and a few chart styles differ. For a report you generate yourself, setting the page setup explicitly — fit to width, defined print area, standard fonts — removes almost all of the visible difference.",{"q":1843,"a":1920},"LibreOffice builds a user profile on first run. Point it at a dedicated profile directory with the -env switch and warm it once during the container build so the first real conversion is not the slow one.",{"q":1849,"a":1922},"You can pass several paths to one invocation, which amortises the startup cost. What you cannot do is run several soffice processes sharing one user profile — they collide. Give each concurrent worker its own profile directory.",{"q":1855,"a":1924},"Yes, it is free software under the Mozilla Public License, with no per-seat or server licensing. That is the main reason it is the standard answer for PDF conversion in a container, where an Excel licence would not be.",{"breadcrumb":1926},[1927,1929,1932],{"name":1928,"item":525},"Home",{"name":1930,"item":1931},"Automating Reporting Workflows","\u002Fautomating-reporting-workflows\u002F",{"name":17,"item":16},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-to-pdf-on-linux-with-libreoffice",{"title":5,"description":1935},"Use soffice --headless --convert-to pdf from Python: per-process user profiles, --norestore, output verification, fit-to-width page setup and a Docker image that warms the profile.","convert-excel-to-pdf-on-linux-with-libreoffice","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-to-pdf-on-linux-with-libreoffice\u002Findex","how-to","FfPmwhnbwagTHvWiOkGn87pwhkAU9pmR-j7CkTtrnmc",[1941,1944],{"title":1876,"path":1942,"stem":1943,"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",{"title":1047,"path":1945,"stem":1946,"children":-1},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-only-selected-sheets-of-a-workbook-to-pdf","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-only-selected-sheets-of-a-workbook-to-pdf\u002Findex",1788710163023]