Files
pdf/gateway/app/services/convert/backends/pdf_exporter.py
T

96 lines
3.6 KiB
Python

"""Office → PDF via reportlab only (concurrent-safe, no LibreOffice).
LibreOffice was removed: heavy, poor multi-user concurrency, and out of
primary stack policy. Improve fidelity in pdf_from_docx instead.
"""
from __future__ import annotations
import os
from typing import Callable
import httpx
from app.services.convert.validation import convert_timeout_seconds, run_with_timeout
from app.services.convert.writers import pdf_from_docx as reportlab_pdf
def _gotenberg_url() -> str:
return os.environ.get("GOTENBERG_URL", "http://127.0.0.1:3000").rstrip("/")
def _try_gotenberg_convert(data: bytes, source_ext: str, timeout: float = 30.0) -> bytes | None:
"""Attempt high-fidelity Office->PDF conversion via Gotenberg LibreOffice endpoint."""
url = f"{_gotenberg_url()}/forms/libreoffice/convert"
ext = source_ext.lstrip(".").lower()
filename = f"document.{ext}"
mime_types = {
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"doc": "application/msword",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"xls": "application/vnd.ms-excel",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"ppt": "application/vnd.ms-powerpoint",
}
content_type = mime_types.get(ext, "application/octet-stream")
try:
with httpx.Client(timeout=timeout) as client:
files = {"files": (filename, data, content_type)}
response = client.post(url, files=files)
if response.status_code == 200 and len(response.content) >= 10 and response.content.startswith(b"%PDF"):
return response.content
except Exception:
# Fail-open contract: Gotenberg absence or network glitch must never crash conversion
pass
return None
def export_office_to_pdf(
data: bytes,
*,
source_ext: str,
reportlab_fn: Callable[[bytes], bytes] | None = None,
timeout: float | None = None,
) -> tuple[bytes, list[str]]:
"""
Convert DOCX/XLSX/PPTX bytes to PDF.
First attempts high-fidelity Gotenberg if available; seamlessly falls back
to in-process ReportLab.
Enforces CONVERT_TIMEOUT_SECONDS (or explicit timeout).
"""
limit = timeout if timeout is not None else convert_timeout_seconds(120.0)
ext = source_ext.lstrip(".").lower()
# 1. Try Gotenberg for maximum Office layout fidelity
gotenberg_pdf = _try_gotenberg_convert(data, ext, timeout=min(limit, 30.0))
if gotenberg_pdf is not None:
return gotenberg_pdf, [
"PDF converted via Gotenberg (high-fidelity LibreOffice microservice).",
]
# 2. Seamless fallback to in-process ReportLab
warnings = [
"PDF via reportlab (in-process; concurrent-safe). Complex Word/Excel layout may differ.",
]
def _run() -> tuple[bytes, list[str]]:
if reportlab_fn is not None:
return reportlab_fn(data), []
if ext in ("docx", "doc"):
return reportlab_pdf.docx_to_pdf(data), []
if ext in ("xlsx", "xls"):
out, extra = reportlab_pdf.xlsx_to_pdf(data)
return out, list(extra)
if ext in ("pptx", "ppt"):
from app.services.convert.writers.pdf_from_pptx import pptx_to_pdf
out, extra = pptx_to_pdf(data)
return out, list(extra)
raise ValueError(f"No PDF export path for .{ext}")
try:
out, extra = run_with_timeout(_run, limit, label="Office→PDF reportlab")
except TimeoutError:
raise
return out, warnings + list(extra)