f24616d342
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m57s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 34s
Testy / Kontrola składni wszystkich warstw (push) Successful in 22s
Ostatnia z osmiu wskazowek partnerow. Nowa usluga services/render sklada raport:
generuje plik posredni .tex i kompiluje go XeLaTeX-em do PDF.
OSOBNY komponent, nie czesc prezentacji — ta sama zasada co przy izolacji
swissepha (LOG-27). TeX Live wazy setki megabajtow; w obrazie produktu
spowalnialby kazdy build, a tak aktualizuje sie niezaleznie i jego awaria nie
kladzie aplikacji, tylko przycisk „Pobierz PDF".
SZYFROWANIE, o ktore prosiles: lacze prezentacja↔render idzie tak samo jak
pozostale, ale z WLASNYM, TRZECIM kluczem (LINK_KEY_PRESENTATION_RENDER). Osobny,
bo tym laczem plynie CALY raport — dane urodzeniowe i opisy z baz — wiec przejecie
go nie moze otwierac lacza do logiki ani do danych. Fail-closed: bez klucza pod
nie wstaje. Test kopii link_crypto obejmuje teraz cztery uslugi.
Uklad PDF wg prosby: imie i nazwisko -> wprowadzone dane -> RYSUNEK kosmogramu
-> interpretacja natalna -> predykcje okresowe. Test pilnuje kolejnosci.
Dwie rzeczy, ktore wyszly dopiero przy skladaniu tego kawalka:
1. KOSMOGRAM NIE NADAWAL SIE DO PDF. Uzywa zmiennych CSS (var(--line)) i klasy
.glyph, ktorej font podaje styles.css — a samodzielny konwerter SVG→PDF nie zna
naszego arkusza. Wyszlyby czarne kreski BEZ SYMBOLI. Stad wariant „print":
konkretne kolory na bialym tle i font glifow wpisany wprost w rysunek. Przy
okazji zalatwia to etap 4 planu kola (wariant do druku).
2. UCIECZKA ZNAKOW LATEXA byla zepsuta — i zlapal to moj wlasny test. Zamiana
„\” na \textbackslash{} szla w tej samej petli co nawiasy, wiec kolejne
podmiany ucieklyby nawiasy dopiero co wstawione: wychodzilo
\textbackslash\{\}. Poprawka: ukosnik chowany pod znacznik zastepczy i
rozwijany na koncu. To nie kosmetyka — niezauwazony „%” komentuje RESZTE LINII,
wiec zdanie od modelu urywaloby sie w polowie, a PDF powstawalby normalnie,
tylko krotszy. Test na wrogim tekscie sprawdza tez, ze \end{document} ani
\input{} nie wyrwa sie z dokumentu.
Usluga nie zapisuje nic poza katalogiem tymczasowym, ktory sprzata po sobie;
w manifescie readOnlyRootFilesystem + emptyDir na /tmp. /health raportuje
obecnosc xelatex i rsvg-convert, zeby zepsuty obraz bylo widac od razu.
Zweryfikowane na zywo (TestClient uslugi render): zadanie bez szyfrowania
z POPRAWNYM tokenem -> 400; obcy klucz -> 400 i tajny opis nie wraca; wlasciwy
klucz -> zadanie dochodzi do aplikacji, tresci baz NIE MA na kablu, odpowiedz
zaszyfrowana. Manifesty przechodza kubectl apply --dry-run=server na zywym
klastrze.
CZEGO NIE SPRAWDZILEM: samej kompilacji PDF. W tym srodowisku nie ma ani TeX
Live, ani dzialajacego runtime'u kontenerow (docker CLI jest, daemon nie) —
probowalem zbudowac obraz i sie nie dalo. Sprawdzone jest wszystko dookola:
generowanie .tex, ucieczka znakow, szyfrowanie, kontrakt API, samowystarczalnosc
SVG. Pierwsze uruchomienie na klastrze trzeba obejrzec — dlatego PRE-24 zostaje
jako „W trakcie", nie „Zrobione".
Instrukcja wdrozenia: docs/wdrozenie-render-pdf.md (klucz, obraz, merge,
weryfikacja, znane ograniczenia).
Testy: 15 nowych (render) + 14 (lacze i wariant druku w prezentacji).
Calosc: prezentacja 131, logika 265/1 skip, render 15.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
132 lines
5.2 KiB
Python
132 lines
5.2 KiB
Python
"""Budowa dokumentu LaTeX raportu (PRE-24).
|
||
|
||
Układ wg prośby partnerów:
|
||
1. imię i nazwisko,
|
||
2. wprowadzone dane,
|
||
3. rysunek kosmogramu,
|
||
4. interpretacja natalna,
|
||
5. predykcje okresowe.
|
||
|
||
Silnik: XeLaTeX. Nie pdflatex — tekst zawiera polskie znaki ORAZ glify
|
||
astrologiczne (♄ ♓ ☉ …), a te wymagają silnika unicode'owego z fontem systemowym.
|
||
DejaVu Sans ma jedno i drugie, dlatego jest fontem głównym.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
# Znaki, które w LaTeXu mają własne znaczenie.
|
||
#
|
||
# Odwrotnego ukośnika NIE MA na tej liście celowo. Jego zamiennik
|
||
# (`\textbackslash{}`) sam zawiera nawiasy klamrowe, więc gdyby szedł w tej samej
|
||
# pętli, kolejne podmiany uciekłyby nawiasy, które dopiero co wstawiliśmy —
|
||
# i zamiast ukośnika wyszedłby dosłowny tekst `\textbackslash\{\}`. Dlatego
|
||
# ukośnik chowamy najpierw pod znacznik zastępczy, a rozwijamy na samym końcu.
|
||
_ESCAPES = [
|
||
("{", r"\{"),
|
||
("}", r"\}"),
|
||
("$", r"\$"),
|
||
("&", r"\&"),
|
||
("#", r"\#"),
|
||
("_", r"\_"),
|
||
("%", r"\%"),
|
||
("~", r"\textasciitilde{}"),
|
||
("^", r"\textasciicircum{}"),
|
||
]
|
||
|
||
# Znak spoza tekstu użytkownika — nie da się go wpisać, więc nie da się nim
|
||
# oszukać ucieczki.
|
||
_BACKSLASH_MARK = "\x00astrololo-backslash\x00"
|
||
|
||
|
||
def esc(text: object) -> str:
|
||
"""Ucieka znaki specjalne LaTeXa.
|
||
|
||
Najgroźniejszy jest `%`: bez ucieczki komentuje RESZTĘ LINII, więc tekst od
|
||
modelu urwałby się w środku zdania i nikt by tego nie zauważył — PDF
|
||
powstałby normalnie, tylko krótszy. Dlatego to nie kosmetyka, a warunek
|
||
poprawności raportu.
|
||
"""
|
||
out = "" if text is None else str(text)
|
||
out = out.replace("\\", _BACKSLASH_MARK) # schowaj, żeby nie ucierpiał niżej
|
||
for ch, rep in _ESCAPES:
|
||
out = out.replace(ch, rep)
|
||
return out.replace(_BACKSLASH_MARK, r"\textbackslash{}")
|
||
|
||
|
||
def _paragraphs(text: str) -> str:
|
||
"""Tekst od modelu → akapity LaTeXa.
|
||
|
||
Model oddziela akapity pustą linią. Zamieniamy je na osobne akapity, a same
|
||
złamania wiersza w środku akapitu zostawiamy LaTeXowi do złożenia.
|
||
"""
|
||
blocks = [b.strip() for b in str(text or "").split("\n\n")]
|
||
return "\n\n".join(esc(b).replace("\n", " ") for b in blocks if b)
|
||
|
||
|
||
PREAMBLE = r"""\documentclass[11pt,a4paper]{article}
|
||
\usepackage{fontspec}
|
||
\usepackage[margin=2.4cm]{geometry}
|
||
\usepackage{graphicx}
|
||
% DejaVu Sans ma i polskie znaki, i glify astrologiczne — jeden font na oba
|
||
% zadania oszczędza nam dobierania fontu zapasowego.
|
||
\setmainfont{DejaVu Sans}
|
||
\setlength{\parindent}{0pt}
|
||
\setlength{\parskip}{0.6em}
|
||
\pagestyle{plain}
|
||
\begin{document}
|
||
"""
|
||
|
||
|
||
def build(report: dict, wheel_pdf: str | None = None) -> str:
|
||
"""Składa źródło .tex. `wheel_pdf` to nazwa pliku z kosmogramem (już PDF)."""
|
||
person = str(report.get("person") or "").strip()
|
||
data = report.get("data") or {}
|
||
natal = report.get("natal") or {}
|
||
predictions = report.get("predictions") or []
|
||
|
||
parts = [PREAMBLE]
|
||
|
||
# ── 1. imię i nazwisko ──────────────────────────────────────────────
|
||
parts.append(r"{\Huge\bfseries " + esc(person or "Raport astrologiczny") + r"\par}")
|
||
parts.append(r"\vspace{0.3em}")
|
||
|
||
# ── 2. wprowadzone dane ─────────────────────────────────────────────
|
||
rows = [
|
||
("Data urodzenia", data.get("date")),
|
||
("Godzina (lokalna)", data.get("time")),
|
||
("Offset względem GMT", data.get("tz_offset")),
|
||
("Szerokość", data.get("lat")),
|
||
("Długość", data.get("lon")),
|
||
("Miejsce", data.get("place")),
|
||
("System domów", data.get("house_system")),
|
||
("Zodiak", data.get("zodiac")),
|
||
]
|
||
parts.append(r"\begin{tabular}{@{}ll@{}}")
|
||
for label, value in rows:
|
||
if value not in (None, ""):
|
||
parts.append(esc(label) + r" & " + esc(value) + r"\\")
|
||
parts.append(r"\end{tabular}")
|
||
parts.append(r"\vspace{1em}")
|
||
|
||
# ── 3. kosmogram ────────────────────────────────────────────────────
|
||
if wheel_pdf:
|
||
parts.append(r"\begin{center}")
|
||
parts.append(r"\includegraphics[width=0.82\textwidth]{" + wheel_pdf + r"}")
|
||
parts.append(r"\end{center}")
|
||
parts.append(r"\vspace{0.5em}")
|
||
|
||
# ── 4. interpretacja natalna ────────────────────────────────────────
|
||
if natal.get("text"):
|
||
parts.append(r"\section*{Interpretacja natalna}")
|
||
parts.append(_paragraphs(natal["text"]))
|
||
|
||
# ── 5. predykcje okresowe ───────────────────────────────────────────
|
||
if predictions:
|
||
parts.append(r"\section*{Predykcje okresowe}")
|
||
for p in predictions:
|
||
span = f"{p.get('from_date', '')} – {p.get('to_date', '')}"
|
||
parts.append(r"\subsection*{" + esc(span) + r"}")
|
||
parts.append(_paragraphs(p.get("text")))
|
||
|
||
parts.append(r"\end{document}")
|
||
return "\n".join(parts) + "\n"
|