"""Convert Office documents to PDF for downstream parsing.""" from __future__ import annotations import platform import shutil import subprocess from pathlib import Path _PPT_EXTENSIONS = {".ppt", ".pptx", ".ppsx"} _WORD_EXTENSIONS = {".doc", ".docx"} def _find_soffice() -> Path | None: for candidate in ( "soffice", r"C:\Program Files\LibreOffice\program\soffice.exe", r"C:\Program Files (x86)\LibreOffice\program\soffice.exe", "/usr/bin/libreoffice", "/usr/bin/soffice", ): if candidate in ("soffice", "libreoffice"): found = shutil.which(candidate) if found: return Path(found) elif Path(candidate).is_file(): return Path(candidate) return None def _convert_via_libreoffice(source: Path, out_dir: Path) -> Path: soffice = _find_soffice() if soffice is None: raise RuntimeError("LibreOffice (soffice) not found") out_dir.mkdir(parents=True, exist_ok=True) subprocess.run( [ str(soffice), "--headless", "--convert-to", "pdf", "--outdir", str(out_dir), str(source.resolve()), ], check=True, capture_output=True, text=True, timeout=180, ) pdf_path = out_dir / f"{source.stem}.pdf" if not pdf_path.is_file(): raise RuntimeError(f"LibreOffice did not produce PDF: {pdf_path}") return pdf_path def _convert_via_powerpoint(source: Path, out_dir: Path) -> Path: if platform.system() != "Windows": raise RuntimeError("PowerPoint COM conversion is only available on Windows") out_dir.mkdir(parents=True, exist_ok=True) pdf_path = out_dir / f"{source.stem}.pdf" src = str(source.resolve()) dst = str(pdf_path.resolve()) script = f""" $ErrorActionPreference = 'Stop' $pp = New-Object -ComObject PowerPoint.Application try {{ $pres = $pp.Presentations.Open('{src.replace("'", "''")}', $true, $true, $false) try {{ $pres.SaveAs('{dst.replace("'", "''")}', 32) }} finally {{ $pres.Close() }} }} finally {{ $pp.Quit() }} """ subprocess.run( ["powershell", "-NoProfile", "-NonInteractive", "-Command", script], check=True, capture_output=True, text=True, timeout=180, ) if not pdf_path.is_file(): raise RuntimeError(f"PowerPoint did not produce PDF: {pdf_path}") return pdf_path def _convert_via_word(source: Path, out_dir: Path) -> Path: if platform.system() != "Windows": raise RuntimeError("Word COM conversion is only available on Windows") out_dir.mkdir(parents=True, exist_ok=True) pdf_path = out_dir / f"{source.stem}.pdf" src = str(source.resolve()) dst = str(pdf_path.resolve()) script = f""" $ErrorActionPreference = 'Stop' $word = New-Object -ComObject Word.Application $word.Visible = $false try {{ $doc = $word.Documents.Open('{src.replace("'", "''")}', $false, $true) try {{ $doc.SaveAs2('{dst.replace("'", "''")}', 17) }} finally {{ $doc.Close([ref]$false) }} }} finally {{ $word.Quit() }} """ subprocess.run( ["powershell", "-NoProfile", "-NonInteractive", "-Command", script], check=True, capture_output=True, text=True, timeout=180, ) if not pdf_path.is_file(): raise RuntimeError(f"Word did not produce PDF: {pdf_path}") return pdf_path def convert_to_pdf(source: Path, out_dir: Path) -> Path: """Convert an Office file to PDF. Tries LibreOffice, then format-specific COM on Windows.""" source = source.resolve() if not source.is_file(): raise FileNotFoundError(source) errors: list[str] = [] try: return _convert_via_libreoffice(source, out_dir) except Exception as exc: # noqa: BLE001 — collect and try next backend errors.append(f"libreoffice: {exc}") ext = source.suffix.lower() fallbacks: list = [] if ext in _PPT_EXTENSIONS: fallbacks.append(_convert_via_powerpoint) elif ext in _WORD_EXTENSIONS: fallbacks.append(_convert_via_word) for converter in fallbacks: try: return converter(source, out_dir) except Exception as exc: # noqa: BLE001 errors.append(f"{converter.__name__}: {exc}") detail = "; ".join(errors) if errors else "no converter available" raise RuntimeError( f"Failed to convert {source.name} to PDF. " f"Install LibreOffice or Microsoft Office. Details: {detail}" ) def is_presentation(path: Path) -> bool: return path.suffix.lower() in _PPT_EXTENSIONS def is_word_document(path: Path) -> bool: return path.suffix.lower() in _WORD_EXTENSIONS