"""Kompilacja raportu do PDF (PRE-24). Dwa kroki zewnętrzne, oba w katalogu tymczasowym, który po sobie sprzątamy: 1. SVG kosmogramu → PDF (`rsvg-convert`). LaTeX nie wstawia SVG wprost; zamiana na PDF zachowuje wektor, więc rysunek zostaje ostry przy dowolnym powiększeniu. 2. .tex → PDF (`xelatex`). Świadomie NIE logujemy treści raportu — to dane urodzeniowe i opisy z baz. Do logu idzie wyłącznie ogon komunikatów LaTeXa przy błędzie, bo bez niego diagnoza jest niemożliwa. """ from __future__ import annotations import logging import pathlib import shutil import subprocess import tempfile from app.latex import build log = logging.getLogger("astrololo.render") WHEEL_SVG = "wheel.svg" WHEEL_PDF = "wheel.pdf" DOC = "report" TIMEOUT = 120 # xelatex na dużym tekście potrzebuje chwili class RenderError(Exception): """Kompilacja się nie udała — z komunikatem nadającym się do pokazania.""" def _run(cmd: list[str], cwd: pathlib.Path) -> subprocess.CompletedProcess: return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=TIMEOUT) def _svg_to_pdf(svg: str, work: pathlib.Path) -> str | None: """Zwraca nazwę pliku PDF z kosmogramem albo None, gdy nie ma czego rysować.""" if not svg or not svg.strip(): return None (work / WHEEL_SVG).write_text(svg, encoding="utf-8") proc = _run(["rsvg-convert", "-f", "pdf", "-o", WHEEL_PDF, WHEEL_SVG], work) if proc.returncode != 0 or not (work / WHEEL_PDF).exists(): # Brak rysunku nie może wywalić całego raportu — tekst jest ważniejszy. log.warning("konwersja kosmogramu nie udała się: %s", (proc.stderr or "")[-300:]) return None return WHEEL_PDF def to_pdf(report: dict) -> bytes: """Raport (dict) → bajty PDF. Rzuca RenderError, gdy LaTeX nie zbuduje pliku.""" with tempfile.TemporaryDirectory(prefix="astrololo-render-") as tmp: work = pathlib.Path(tmp) wheel = _svg_to_pdf(report.get("wheel_svg") or "", work) (work / f"{DOC}.tex").write_text(build(report, wheel), encoding="utf-8") proc = _run( ["xelatex", "-interaction=nonstopmode", "-halt-on-error", f"{DOC}.tex"], work, ) pdf = work / f"{DOC}.pdf" if not pdf.exists(): tail = (proc.stdout or "")[-1200:] log.error("xelatex nie zbudował PDF-a; ogon logu: %s", tail) raise RenderError("LaTeX nie zbudował dokumentu. Ogon logu: " + tail[-400:]) return pdf.read_bytes() def toolchain_ready() -> dict: """Czy narzędzia są na miejscu — do /health, żeby awarię obrazu widzieć od razu, a nie przy pierwszym raporcie.""" return { "xelatex": bool(shutil.which("xelatex")), "rsvg-convert": bool(shutil.which("rsvg-convert")), }