96 lines
3.6 KiB
Python
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)
|