Files
RAG-CUT/backend/rag_cut/parsers/office_to_pdf.py
T

165 lines
4.7 KiB
Python
Raw Normal View History

2026-07-16 11:12:17 +08:00
"""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