feat(render): raport PDF przez LaTeX jako osobna usluga (PRE-24)
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
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>
This commit is contained in:
@@ -49,6 +49,22 @@ MIN_SEP = 8.0
|
||||
# muted kolory żywiołów — czytelne na ciemnym tle, spójne z paletą aplikacji
|
||||
_ELEMENT_COLOR = ["#d98a6a", "#8fae7a", "#cfc06a", "#6aa6c9"] # ogień, ziemia, powietrze, woda
|
||||
|
||||
# ── motywy ───────────────────────────────────────────────────────────────
|
||||
# „screen" korzysta ze zmiennych CSS strony, więc koło samo dostraja się do
|
||||
# motywu aplikacji. „print" MUSI mieć konkretne kolory: SVG idzie wtedy do
|
||||
# osobnego konwertera (rsvg → PDF, PRE-24), który o naszym arkuszu nic nie wie
|
||||
# i zmiennych `var(--…)` nie rozwiąże — wyszłyby czarne albo żadne kreski.
|
||||
# Przy okazji odwracamy kontrast: raport drukuje się na białym papierze.
|
||||
_THEMES = {
|
||||
"screen": {"line": "var(--line)", "ink": "var(--ink)", "muted": "var(--muted)",
|
||||
"accent": "var(--accent)", "disc": "#12132a", "retro": "#ff9b6a",
|
||||
"elements": _ELEMENT_COLOR},
|
||||
"print": {"line": "#b9bcc9", "ink": "#111318", "muted": "#5d6270",
|
||||
"accent": "#5a49c8", "disc": "#ffffff", "retro": "#b2452a",
|
||||
# ciemniejsze warianty żywiołów — na białym tle jasne byłyby niewidoczne
|
||||
"elements": ["#a8442a", "#3f6b33", "#8a6d10", "#1f5c80"]},
|
||||
}
|
||||
|
||||
|
||||
def _pt(lon: float, asc: float, r: float) -> tuple[float, float]:
|
||||
phi = math.radians(180.0 - (lon - asc))
|
||||
@@ -61,10 +77,20 @@ def _line(x1: float, y1: float, x2: float, y2: float, stroke: str, w: float = 1.
|
||||
f'stroke="{stroke}" stroke-width="{w}" opacity="{opacity}"/>')
|
||||
|
||||
|
||||
def _text(x: float, y: float, s: str, *, size: float, fill: str, cls: str = "") -> str:
|
||||
# Stack fontów z glifami astrologicznymi. Na stronie dostarcza go klasa CSS
|
||||
# `.glyph`, ale w wariancie do druku SVG jedzie do SAMODZIELNEGO konwertera, który
|
||||
# naszego arkusza nie widzi — wtedy trzeba wpisać font wprost do elementu, inaczej
|
||||
# rysunek wychodzi BEZ symboli (konwerter bierze domyślny font, a ten ich nie ma).
|
||||
GLYPH_FONT = ("Apple Symbols, Segoe UI Symbol, Noto Sans Symbols2, "
|
||||
"DejaVu Sans, sans-serif")
|
||||
|
||||
|
||||
def _text(x: float, y: float, s: str, *, size: float, fill: str, cls: str = "",
|
||||
font: str = "") -> str:
|
||||
c = f' class="{cls}"' if cls else ""
|
||||
f = f' font-family="{font}"' if font else ""
|
||||
return (f'<text x="{x:.2f}" y="{y:.2f}" font-size="{size}" fill="{fill}"'
|
||||
f' text-anchor="middle" dominant-baseline="central"{c}>{escape(s)}</text>')
|
||||
f' text-anchor="middle" dominant-baseline="central"{c}{f}>{escape(s)}</text>')
|
||||
|
||||
|
||||
def spread(lons: list[float], min_sep: float = MIN_SEP, passes: int = 300) -> list[float]:
|
||||
@@ -126,8 +152,16 @@ def _delta(a: float, b: float) -> float:
|
||||
return abs(((a - b + 180.0) % 360.0) - 180.0)
|
||||
|
||||
|
||||
def render(chart: dict) -> str:
|
||||
"""Zwraca SVG koła albo '' gdy brakuje danych (silnik bez osi/domów)."""
|
||||
def render(chart: dict, theme: str = "screen") -> str:
|
||||
"""Zwraca SVG koła albo '' gdy brakuje danych (silnik bez osi/domów).
|
||||
|
||||
theme='screen' — kolory ze zmiennych CSS aplikacji (dostraja się do motywu).
|
||||
theme='print' — konkretne kolory na białym tle; konieczne, gdy SVG trafia
|
||||
do samodzielnego konwertera (PDF), który zmiennych CSS nie rozwiąże.
|
||||
"""
|
||||
T = _THEMES.get(theme, _THEMES["screen"])
|
||||
# tylko dla druku — na stronie font podaje CSS (.glyph)
|
||||
glyph_font = GLYPH_FONT if theme == "print" else ""
|
||||
angles = chart.get("angles")
|
||||
cusps = chart.get("cusps")
|
||||
signs = chart.get("sign_glyphs")
|
||||
@@ -138,26 +172,26 @@ def render(chart: dict) -> str:
|
||||
parts: list[str] = []
|
||||
|
||||
# tło i okręgi
|
||||
parts.append(f'<circle cx="{_CX}" cy="{_CY}" r="{R_OUT}" fill="#12132a" stroke="var(--line)" stroke-width="1.5"/>')
|
||||
parts.append(f'<circle cx="{_CX}" cy="{_CY}" r="{R_ZOD}" fill="none" stroke="var(--line)" stroke-width="1"/>')
|
||||
parts.append(f'<circle cx="{_CX}" cy="{_CY}" r="{R_HUB}" fill="none" stroke="var(--line)" stroke-width="1"/>')
|
||||
parts.append(f'<circle cx="{_CX}" cy="{_CY}" r="{R_OUT}" fill="{T["disc"]}" stroke="{T["line"]}" stroke-width="1.5"/>')
|
||||
parts.append(f'<circle cx="{_CX}" cy="{_CY}" r="{R_ZOD}" fill="none" stroke="{T["line"]}" stroke-width="1"/>')
|
||||
parts.append(f'<circle cx="{_CX}" cy="{_CY}" r="{R_HUB}" fill="none" stroke="{T["line"]}" stroke-width="1"/>')
|
||||
|
||||
# drobne podziałki co 5°, mocniejsze co 10° (na pasie znaków)
|
||||
for deg in range(0, 360, 5):
|
||||
r_in = R_TICK if deg % 10 else R_TICK - 4
|
||||
x1, y1 = _pt(deg, asc, R_ZOD)
|
||||
x2, y2 = _pt(deg, asc, r_in)
|
||||
parts.append(_line(x1, y1, x2, y2, "var(--line)", 0.6, 0.7))
|
||||
parts.append(_line(x1, y1, x2, y2, T["line"], 0.6, 0.7))
|
||||
|
||||
# pas znaków: granice co 30° + glif znaku w środku sektora, kolorem żywiołu
|
||||
for i, sg in enumerate(signs):
|
||||
b = 30.0 * i
|
||||
x1, y1 = _pt(b, asc, R_ZOD)
|
||||
x2, y2 = _pt(b, asc, R_OUT)
|
||||
parts.append(_line(x1, y1, x2, y2, "var(--line)", 1.0))
|
||||
parts.append(_line(x1, y1, x2, y2, T["line"], 1.0))
|
||||
gx, gy = _pt(b + 15.0, asc, R_SIGN)
|
||||
parts.append(_text(gx, gy, sg.get("glyph") or "", size=17,
|
||||
fill=_ELEMENT_COLOR[i % 4], cls="glyph"))
|
||||
fill=T["elements"][i % 4], cls="glyph", font=glyph_font))
|
||||
|
||||
# szprychy domów (od pasa do piasty) + numery domów w środku każdego domu
|
||||
n = len(cusps)
|
||||
@@ -165,24 +199,24 @@ def render(chart: dict) -> str:
|
||||
lon = float(c["decimal"])
|
||||
x1, y1 = _pt(lon, asc, R_ZOD)
|
||||
x2, y2 = _pt(lon, asc, R_HUB)
|
||||
parts.append(_line(x1, y1, x2, y2, "var(--line)", 0.8, 0.85))
|
||||
parts.append(_line(x1, y1, x2, y2, T["line"], 0.8, 0.85))
|
||||
nxt = float(cusps[(i + 1) % n]["decimal"])
|
||||
span = (nxt - lon) % 360.0 or 360.0
|
||||
mid = lon + span / 2.0
|
||||
hx, hy = _pt(mid, asc, R_HNUM)
|
||||
parts.append(_text(hx, hy, str(c["house"]), size=11, fill="var(--muted)"))
|
||||
parts.append(_text(hx, hy, str(c["house"]), size=11, fill=T["muted"]))
|
||||
|
||||
# osie: Asc–Dsc i MC–IC przez całe koło, wyróżnione akcentem
|
||||
for a, b, la, lb in (("Asc", "Dsc", "AC", "DC"), ("MC", "IC", "MC", "IC")):
|
||||
lon_a = float(angles[a]["decimal"])
|
||||
ax, ay = _pt(lon_a, asc, R_ZOD)
|
||||
bx, by = _pt(lon_a + 180.0, asc, R_ZOD)
|
||||
parts.append(_line(ax, ay, bx, by, "var(--accent)", 1.6, 0.9))
|
||||
parts.append(_line(ax, ay, bx, by, T["accent"], 1.6, 0.9))
|
||||
# etykiety POZA kołem — w środku kolidowały z glifami obiektów
|
||||
lax, lay = _pt(lon_a, asc, R_AXIS_LABEL)
|
||||
lbx, lby = _pt(lon_a + 180.0, asc, R_AXIS_LABEL)
|
||||
parts.append(_text(lax, lay, la, size=10, fill="var(--accent)"))
|
||||
parts.append(_text(lbx, lby, lb, size=10, fill="var(--accent)"))
|
||||
parts.append(_text(lax, lay, la, size=10, fill=T["accent"]))
|
||||
parts.append(_text(lbx, lby, lb, size=10, fill=T["accent"]))
|
||||
|
||||
# ── obiekty: glify na pozycjach, stopień w znaku, retrogradacja ─────
|
||||
# Glify rozsuwamy, ale kreska na pasie zostaje w PRAWDZIWEJ pozycji —
|
||||
@@ -197,15 +231,15 @@ def render(chart: dict) -> str:
|
||||
|
||||
mx1, my1 = _pt(true_lon, asc, R_ZOD)
|
||||
mx2, my2 = _pt(true_lon, asc, R_MARK)
|
||||
parts.append(_line(mx1, my1, mx2, my2, "var(--ink)", 1.0, 0.75))
|
||||
parts.append(_line(mx1, my1, mx2, my2, T["ink"], 1.0, 0.75))
|
||||
|
||||
if _delta(draw_lon, true_lon) > 0.4:
|
||||
lx, ly = _pt(draw_lon, asc, R_PLANET + 12)
|
||||
parts.append(_line(mx2, my2, lx, ly, "var(--line)", 0.7, 0.9))
|
||||
parts.append(_line(mx2, my2, lx, ly, T["line"], 0.7, 0.9))
|
||||
|
||||
gx, gy = _pt(draw_lon, asc, R_PLANET)
|
||||
parts.append(_text(gx, gy, obj.get("glyph") or "", size=16,
|
||||
fill="var(--ink)", cls="glyph"))
|
||||
fill=T["ink"], cls="glyph", font=glyph_font))
|
||||
|
||||
# Sam stopień w znaku, bez „°" — tak robią programy astrologiczne i tylko
|
||||
# tak podpisy mieszczą się obok siebie w skupiskach. Retrogradacja: ℞ oraz
|
||||
@@ -213,7 +247,7 @@ def render(chart: dict) -> str:
|
||||
dx, dy = _pt(draw_lon, asc, R_DEG)
|
||||
label = f"{int(true_lon % 30)}" + ("℞" if retro else "")
|
||||
parts.append(_text(dx, dy, label, size=8,
|
||||
fill="#ff9b6a" if retro else "var(--muted)"))
|
||||
fill=T["retro"] if retro else T["muted"]))
|
||||
|
||||
body = "".join(parts)
|
||||
return (f'<svg class="wheel" viewBox="0 0 {_VIEW} {_VIEW}" role="img" '
|
||||
|
||||
Reference in New Issue
Block a user