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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e3cc8f418b
commit
156289fe2d
@@ -0,0 +1,115 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user