f5dec15e4d
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 11m13s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m49s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 29s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 20s
build-render / build (push) Successful in 44s
build / build (push) Successful in 54s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m19s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m59s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 34s
Testy / Kontrola składni wszystkich warstw (push) Successful in 25s
Zasada: co składamy, dodajemy i wyświetlamy, MA trafiać do finalnego raportu
(«Skompiluj») i do PDF-a — nie tylko na stronę /chart. Do tej pory do raportu i
druku szło samo koło; aspektarian (etap 5) i deklinacja/antyscja (etap 6) były
tylko w podglądzie horoskopu.
RAPORT «Skompiluj» (podgląd): handler /compile liczy teraz wszystkie cztery
rysunki, a szablon je pokazuje (koło, aspektarian, deklinacja, antyscja).
PDF (usługa render) — uogólnienie z jednego rysunku na LISTĘ:
- Kontrakt raportu: `figures = [{svg, caption}]` (uporządkowana). `wheel_svg`
zostaje dla zgodności wstecznej jako pojedynczy rysunek.
- `compile.py`: każdy SVG osobno przez rsvg-convert → PDF (fig0…figN); braki/
błędy pomijane (rysunek nie może wywalić raportu, tekst ważniejszy).
- `latex.py build()`: rysunki w sekcji 3 (po danych, przed natalną) w PODANEJ
kolejności, każdy z podpisem. Jedna reguła składania: keepaspectratio z limitem
szerokości i wysokości — kwadratowe (koło, aspektarian) ogranicza wysokość,
szerokie (deklinacja, antyscja) szerokość, bez zniekształceń.
- `compile_pdf` (prezentacja): renderuje komplet w motywie DRUKU i wysyła jako
`figures`.
Testy: render +5 (kolejność, podpisy, zgodność wsteczna, pomijanie pustych),
prezentacja +3 (raport pokazuje komplet, PDF składa komplet). Render 32,
prezentacja 173.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
91 lines
3.4 KiB
Python
91 lines
3.4 KiB
Python
"""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")
|
|
|
|
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, stem: str) -> str | None:
|
|
"""SVG → PDF (nazwa `stem`) albo None, gdy nie ma czego rysować lub konwersja
|
|
padła. Brak rysunku nie może wywalić raportu — tekst jest ważniejszy."""
|
|
if not svg or not svg.strip():
|
|
return None
|
|
svg_name, pdf_name = f"{stem}.svg", f"{stem}.pdf"
|
|
(work / svg_name).write_text(svg, encoding="utf-8")
|
|
proc = _run(["rsvg-convert", "-f", "pdf", "-o", pdf_name, svg_name], work)
|
|
if proc.returncode != 0 or not (work / pdf_name).exists():
|
|
log.warning("konwersja rysunku %s nie udała się: %s", stem, (proc.stderr or "")[-300:])
|
|
return None
|
|
return pdf_name
|
|
|
|
|
|
def _figures(report: dict, work: pathlib.Path) -> list[dict]:
|
|
"""Rysunki z raportu → lista PDF-ów z podpisami. Kontrakt: `figures` =
|
|
[{svg, caption}]; dla zgodności wstecznej wpada też pojedynczy `wheel_svg`."""
|
|
src = report.get("figures")
|
|
if not src and report.get("wheel_svg"):
|
|
src = [{"svg": report["wheel_svg"], "caption": "Kosmogram"}]
|
|
out: list[dict] = []
|
|
for i, fig in enumerate(src or []):
|
|
pdf = _svg_to_pdf(fig.get("svg") or "", work, f"fig{i}")
|
|
if pdf:
|
|
out.append({"pdf": pdf, "caption": fig.get("caption") or ""})
|
|
return out
|
|
|
|
|
|
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)
|
|
figures = _figures(report, work)
|
|
(work / f"{DOC}.tex").write_text(build(report, figures=figures), 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")),
|
|
}
|