Files
html-to-pdf/documentation/architektura.md
T
JiriUhlirandClaude Opus 5 156289fe2d Implementace prevodu HTML na PDF
Sluzba prijme adresu HTML dokumentu nebo HTML v tele requestu a vrati PDF.
Navrzena pro dokumenty o stovkach az tisicich stranek.

Rendering:
- WeasyPrint jako vychozi engine, spravne CSS Paged Media, nizka pametova
  narocnost, bez JavaScriptu
- Chromium pres Playwright pro dokumenty dokreslovane skripty
- rezim auto s detekci skriptu a fallbackem pri selhani WeasyPrintu

Velke dokumenty:
- deleni na casti na strukturalnich hranicich, rez nikdy uvnitr tabulky
  nebo odstavce
- dvoupruchodovy render obsahu se skutecnymi cisly stranek, pozice nadpisu
  se ctou z kotev hlasenych u kazde stranky
- cislovani stranek bud pres CSS countery, nebo pres cislovaci vrstvu
  nastampovanou na hotove PDF, rozmer stranky se cte z vysledneho souboru
- Chromium se restartuje po N jobech, nikdy vsak behem beziciho renderu

API:
- POST /convert synchronne, POST /jobs asynchronne se sledovanim stavu,
  stahovanim vysledku, rusenim a volitelnym callbackem
- GET /health s overenim dostupnosti obou enginu a stavem fronty
- OpenAPI respektuje prefix reverse proxy pres root_path

Bezpecnost a provoz:
- SSRF kontrola po DNS resolvu, na kazdem presmerovani a u vsech pozadavku
  prohlizece
- nedostupne assety render nezastavi, ale hlasi se v odpovedi i v logu
- fronta s omezenym poctem workeru, rozpracovane joby se pri ukonceni
  oznaci jako failed, nezmizi potichu
- strukturovane JSON logovani s job_id
- vsechny limity vypnute ve vychozim stavu

Dockerfile je dvoufazovy, obsahuje zavislosti WeasyPrintu, Chromium
a fonty s ceskou diakritikou.

Autentizace zamerne neni implementovana, zpusob predavani neni domluveny.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 14:50:10 +02:00

116 lines
4.8 KiB
Markdown

# Architektura
## Struktura projektu
```text
app/
main.py vytvoreni aplikace, lifespan, obsluha chyb
config.py konfigurace z environment variables
models.py Pydantic modely requestu a odpovedi
errors.py typove chyby a mapovani na HTTP kody
logging_setup.py strukturovane JSON logovani
deps.py kontejner sluzeb sestaveny pri startu
routers/
health.py /health a /version
convert.py synchronni /convert
jobs.py asynchronni /jobs
services/
security.py SSRF kontroly, resolv adres
fetcher.py stahovani dokumentu a assetu, hlaseni vypadku
pipeline.py cely prubeh konverze
jobs.py fronta jobu a workeri
storage.py docasne ulozeni vysledku
engines/
base.py spolecne rozhrani enginu
weasy.py WeasyPrint
chromium.py Playwright a headless Chromium
pdf/
document.py parsovani, nadpisy, obsah
chunker.py deleni HTML na casti
merger.py slucovani PDF
paginator.py cislovaci vrstva
styles.py generovani stylu stranky
```
## Prubeh konverze
1. Nacteni zdroje. U `source.url` se dokument stahne, kazdy hop presmerovani
projde SSRF kontrolou. U `source.html` se pouzije telo requestu.
2. Volba enginu. Rezim `auto` hleda aktivni skripty, jinak bere WeasyPrint.
3. Volba rezimu cislovani stranek. Musi padnout pred injektazi stylu, protoze
styl se lisi podle toho, jestli cisla resi CSS countery nebo cislovaci vrstva.
4. Sestaveni dokumentu. Doplni se `base`, prida se styl stranky, nadpisy dostanou
`id` a pokud je zapnuty obsah, vlozi se jeho zastupna verze.
5. Deleni na casti.
6. Prvni pruchod renderu. Vysledkem jsou hotova PDF casti, jejich pocty stranek
a pozice kotev.
7. Druhy pruchod, pokud se generuje obsah. Zastupna cisla se nahradi skutecnymi.
8. Slouceni casti.
9. Nastampovani cislovaci vrstvy, pokud cisla neresi CSS countery.
## Proc dvoupruchodovy render
Cisla stranek v obsahu nejsou znama drive, nez se dokument vyrenderuje. Zaroven
plati, ze vlozeni obsahu posune cisla stranek, ktera obsah uvadi.
Reseni je vlozit obsah uz v prvnim pruchodu, jen s pevne sirokou vyplni misto
cisel. Rozvrzeni je proto v obou pruchodech stejne a cisla zjistena v prvnim
pruchodu plati i po druhem. Cislo stranky je v tabulce zarovnane doprava ve
sloupci pevne sirky, takze ani jiny pocet cislic rozvrzeni nezmeni.
Pozice nadpisu se ctou z kotev, ktere WeasyPrint hlasi u kazde stranky. To je
presnejsi nez odhad z poradi zalozek.
## Proc se cisla stranek u clenenych dokumentu dopisuji az nakonec
CSS counter `page` se v kazde renderovane casti restartuje od jednicky a
`counter(pages)` zna jen pocet stranek dane casti. Ve sloucenem dokumentu by
proto cislovani bylo nesmyslne.
Cislovaci vrstva je samostatny dokument o stejnem poctu stranek a stejnem
rozmeru, ktery obsahuje jen cisla v okraji. Ten se pres hotove PDF nastampuje
stranku po strance. Rozmer stranky se cte z prvni stranky vysledneho PDF, takze
vrstva sedi i kdyz si dokument nastavil vlastni `@page size`.
Fonty vrstvy kresli WeasyPrint a vklada je do souboru, vysledek proto nezavisi
na fontech v systemu, ktery PDF otevira.
## Pamet
Kriticky bod u tisicistrankovych dokumentu.
- Render probiha po castech, v pameti je vzdy jen jedna cast.
- Chromium se restartuje po `CHROMIUM_RESTART_AFTER_JOBS` jobech, protoze
postupne unika pamet.
- Slucovani pouziva pypdf, ktere drzi stranky vysledku v pameti. U velmi velkych
dokumentu je to nejnarocnejsi krok cele konverze. V image je nainstalovany
i `qpdf`, ktery by slo pouzit jako nahradu, pokud by pamet prestala stacit.
- Vysledek se ke klientovi streamuje, nenacita se cely do pameti.
## Fronta
Fronta je v pameti procesu. Zadna databaze, zadny broker.
- Soubezne bezi `WORKERS` jobu, ostatni cekaji ve fronte.
- Plna fronta vraci 503 s kodem `queue_full`.
- Pri ukonceni sluzby se rozpracovane joby oznaci jako failed s kodem
`service_restarted`. Nezmizi potichu.
- Vysledky se po `JOB_RESULT_TTL_SECONDS` smazou a job se odstrani.
- Pri startu se smazou adresare jobu, ktere po restartu zustaly bez zaznamu.
## Bezpecnost
Sluzba na pozadani stahuje libovolnou adresu, coz je presne tvar SSRF
zranitelnosti.
- Povolena jsou jen schemata `http` a `https`.
- Kontrola probiha az po DNS resolvu, takze verejna domena mirici na 127.0.0.1
neprojde.
- Kontrola se opakuje na kazdem presmerovani.
- Blokovane jsou loopback, privatni rozsahy, link local vcetne 169.254.169.254,
CGNAT, multicast a rezervovane rozsahy.
- U Chromia jde kazdy pozadavek prohlizece pres `context.route`, takze stejnou
kontrolou projdou i assety, ktere si stranka dotahne sama.
- Seznam blokovanych rozsahu jde rozsirit i zuzit konfiguraci. Vychozi stav je
blokovat.