Files
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

4.8 KiB

Architektura

Struktura projektu

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.