Formules, mermaid en grafieken worden getekend in de PDF van een document #1612

Merged
brenno merged 5 commits from feat/document-pdf-rasterize into main 2026-08-20 15:53:57 +00:00
Owner

De vorige PR liet formules, mermaid-diagrammen en grafieken als bron in de PDF staan: een PDF kent geen JavaScript, en de HTML-export tekent die drie juist daarmee. Dat bleek maar half waar. Alle drie de renderers leveren SVG, en package:pdf kan SVG plaatsen.

Ze gaan er daarom als vector in en niet als plaatje. Dat is in dit exportpad het verschil dat telt: scherp op elke zoomstand, klein in bytes, en de tekst in een grafiek — titel, aslabels, legenda — blijft echte tekst. Een document dat zijn tekstlaag als belofte draagt, hoort daar geen bitmaps in te leggen.

Waar de tekeningen vandaan komen

Grafieken van MarpHtmlService.chartSpecSvg — zuivere Dart, en dezelfde generator die de HTML-export en de documentweergave op het scherm al gebruiken. Er komt dus geen renderwereld bij; er verdwijnt er eerder één, want die drie kunnen nu niet meer uit de pas lopen. Een test leest de aslabels terug uit het geleverde bestand om dat vast te houden.

Mermaid uit de verborgen WebView die de voorvertoning en de raster-export al voedde. Die levert geschoonde SVG waarin de stijl al is ingelijnd en de pijlpunten al echte driehoeken zijn (#862) — precies de vereenvoudiging die een eenvoudige SVG-lezer nodig heeft. Dat de HTML- en de PDF-uitvoer zo van hetzelfde plaatje leven is winst, geen toeval.

Formules via MathJax tex-svg, dat al gebundeld lag voor de HTML-export en geen lettertypebestanden ophaalt — de glyphs komen als paden mee, dus de uitkomst staat op zichzelf. Hij draait in dezelfde verborgen pagina als mermaid en niet in een tweede: het monteren van de host, het wachten op de bootstrap en het serialiseren van de wachtrij zijn juist de delen die daar moeizaam goed zijn gekregen (#882), en die twee keer hebben is twee keer hetzelfde kunnen breken. De cache sleutelt daarom op soort én bron.

De naam van die dienst is er smaller door geworden dan wat hij doet. Hernoemen raakt eenendertig plekken en hoort in een eigen wijziging thuis, niet verstopt in deze; zolang staat het in het kopcommentaar.

Wat de eerste echte render liet zien

Drie fouten die geen enkele test met verwachtingswaarden zou hebben gevangen, en die alle drie hetzelfde soort zijn: een SVG die in een browser klopt, klopt niet vanzelf voor een lezer die geen CSS kent en geen eenheden omrekent.

  1. Uitgerekt. Een stroomdiagram van drie vakjes vulde het halve blad, omdat de tekening op bladbreedte werd gezet. De maat gaat er nu af en wordt zelf uitgerekend, zodat een tekening nooit groter wordt opgeblazen dan ze bedoelt.
  2. Verdwenen. De formule was er helemaal niet. MathJax meet in ex; de lezer las 2.5ex als tweeënhalve punt. En MathJax kleurt met currentColor — een kleur die de lezer niet kent, en een onbekende kleur tekent niets. (Bij de eerste reparatie daarvan kwam er #22222ff uit, de doorzichtigheid uit toHex(), en dat is óók een kleur die niets tekent. Daar staat nu een test op.)
  3. Zonder lijnen. Daarna stond het diagram er mét vakjes en pijlpunten maar zónder één lijn ertussen. Mermaid hangt aan elke verbindingslijn stroke-dasharray: 0px. Een browser negeert dat — de specificatie zegt dat een patroon zonder positieve waarde genegeerd hoort te worden — maar deze lezer neemt het letterlijk en tekent streepjes van lengte nul met tussenruimtes van lengte nul.

Alle drie zitten ze nu in document_pdf_svg.dart, met per geval opgeschreven waarom het misging.

De terugval blijft het dragende deel

Wat níet getekend kan worden toont nog steeds zijn bron in een kader met een aanduiding erboven. Dat gebeurt bij een grafiek zonder cijfers (die staan in een los data/*.json dat niet meekwam), bij een renderer die niets oplevert, bij een tekening die de SVG-lezer niet kan ontleden, en altijd op web, waar de app als CanvasKit-pagina zonder die WebView draait.

Er zit een plafond op het wachten. De renderer heeft er zelf een, maar dat helpt niet tegen het geval dat hier telt: een verzoek dat de wachtrij nooit verlaat omdat de host niet gemonteerd is. Zonder plafond hangt de export dan op één diagram — en een export die blijft hangen is erger dan een diagram dat als bron in het bestand komt.

Afweging (bewaker)

Er komt niets bij in het .md — dit pad leest alleen — en geen nieuwe partij. MathJax lag al gebundeld en draaide al: in elk geëxporteerd HTML-bestand, bij de ontvanger. Nieuw is dat hij nu ín de app draait, in de pagina die mermaid al gebruikte: default-src 'none', geen netwerk, geen eval, alleen de meegeleverde bundel. De TeX die erin gaat is al door OciWacht geprojecteerd, en wat eruit komt gaat door dezelfde zeef als mermaid — die hoefde niet verruimd te worden, want defs, use, symbol en href stonden er al in voor mermaid.

De botsing, hardop: een JS-bundel in het proces laten draaien om een formule te zetten is meer aanvalsoppervlak dan de bron afdrukken. Ik laat gemak hier voorgaan omdat de pagina al bestond en dichtstaat, de uitvoer gezeefd wordt, en alles faalt-dicht. Ik zou van gedachten veranderen als de zeef verruimd had moeten worden om MathJax werkend te krijgen; dat hoefde niet.

En de belofte van gisteren is weer om. De documentatie zei "deze drie staan erin als bron". Dat is nu de terugval en niet de regel. USER_GUIDE en KNOWN_LIMITATIONS (nl + en) zijn ingeperkt in plaats van overschreven, DOCUMENT_MODE §11.5 houdt de oude zin zichtbaar naast de nieuwe, en de dialoogtekst is in alle 31 talen vervangen — met de vervallen regel uit alle taaltabellen gehaald, want de wezenpoort ziet die.

Toetsen

  • make check groen op de gerebasede boom (dekking 87,2%, per-bestand-vloer 0).
  • make check-secrets (gitleaks + trufflehog, werkboom én historie): geen bevindingen. make sast (semgrep): 0 findings.
  • DAST (ZAP) niet gedraaid: deze wijziging raakt het geserveerde weboppervlak niet.
  • Een integratietest voor het stuk dat onder flutter test niet bestaat. Mermaid en MathJax draaien in een verborgen WebView; zonder die WebView valt alles terug op de bron en zou een groene suite niets zeggen over de functie zelf. integration_test/document_pdf_graphics_test.dart draait de échte renderers op macOS, meet de dasharray-reparatie op de échte uitvoer van mermaid (mét de controle dat die uitvoer het patroon werkelijk bevat, anders meet de assertie niets), en schrijft de PDF weg om er met eigen ogen naar te kijken.
  • 96 tests in test/pdf/, waaronder: een grafiek die getekend wordt in plaats van afgedrukt, elke terugvalgrond apart, een onleesbare SVG die de export niet afbreekt, hetzelfde diagram dat één keer gerenderd wordt, en de drie SVG-reparaties elk op zichzelf.
  • Vijf nieuwe tests op de renderketen zelf, met de bestaande neppe WebView: de pagina draagt de formulerenderer, een formule gaat naar de juiste ingang, de TeX gaat als JSON de aanroep in, en een formule en een diagram met dezelfde tekst botsen niet in de cache.
  • Met eigen ogen nagekeken in de draaiende app: het stroomdiagram met zijn lijnen en pijlpunten, de gezette formule, en de grafiek op ware breedte.
De vorige PR liet formules, mermaid-diagrammen en grafieken als bron in de PDF staan: een PDF kent geen JavaScript, en de HTML-export tekent die drie juist daarmee. Dat bleek maar half waar. Alle drie de renderers leveren **SVG**, en `package:pdf` kan SVG plaatsen. Ze gaan er daarom als **vector** in en niet als plaatje. Dat is in dit exportpad het verschil dat telt: scherp op elke zoomstand, klein in bytes, en de tekst in een grafiek — titel, aslabels, legenda — blijft echte tekst. Een document dat zijn tekstlaag als belofte draagt, hoort daar geen bitmaps in te leggen. ## Waar de tekeningen vandaan komen **Grafieken** van `MarpHtmlService.chartSpecSvg` — zuivere Dart, en dezelfde generator die de HTML-export en de documentweergave op het scherm al gebruiken. Er komt dus geen renderwereld bij; er verdwijnt er eerder één, want die drie kunnen nu niet meer uit de pas lopen. Een test leest de aslabels terug uit het geleverde bestand om dat vast te houden. **Mermaid** uit de verborgen WebView die de voorvertoning en de raster-export al voedde. Die levert geschoonde SVG waarin de stijl al is ingelijnd en de pijlpunten al echte driehoeken zijn (#862) — precies de vereenvoudiging die een eenvoudige SVG-lezer nodig heeft. Dat de HTML- en de PDF-uitvoer zo van hetzelfde plaatje leven is winst, geen toeval. **Formules** via MathJax `tex-svg`, dat al gebundeld lag voor de HTML-export en geen lettertypebestanden ophaalt — de glyphs komen als paden mee, dus de uitkomst staat op zichzelf. Hij draait in dezelfde verborgen pagina als mermaid en niet in een tweede: het monteren van de host, het wachten op de bootstrap en het serialiseren van de wachtrij zijn juist de delen die daar moeizaam goed zijn gekregen (#882), en die twee keer hebben is twee keer hetzelfde kunnen breken. De cache sleutelt daarom op soort én bron. De naam van die dienst is er smaller door geworden dan wat hij doet. Hernoemen raakt eenendertig plekken en hoort in een eigen wijziging thuis, niet verstopt in deze; zolang staat het in het kopcommentaar. ## Wat de eerste echte render liet zien Drie fouten die geen enkele test met verwachtingswaarden zou hebben gevangen, en die alle drie hetzelfde soort zijn: **een SVG die in een browser klopt, klopt niet vanzelf voor een lezer die geen CSS kent en geen eenheden omrekent.** 1. **Uitgerekt.** Een stroomdiagram van drie vakjes vulde het halve blad, omdat de tekening op bladbreedte werd gezet. De maat gaat er nu af en wordt zelf uitgerekend, zodat een tekening nooit groter wordt opgeblazen dan ze bedoelt. 2. **Verdwenen.** De formule was er helemaal niet. MathJax meet in `ex`; de lezer las `2.5ex` als tweeënhalve punt. En MathJax kleurt met `currentColor` — een kleur die de lezer niet kent, en een onbekende kleur tekent niets. (Bij de eerste reparatie daarvan kwam er `#22222ff` uit, de doorzichtigheid uit `toHex()`, en dat is óók een kleur die niets tekent. Daar staat nu een test op.) 3. **Zonder lijnen.** Daarna stond het diagram er mét vakjes en pijlpunten maar zónder één lijn ertussen. Mermaid hangt aan elke verbindingslijn `stroke-dasharray: 0px`. Een browser negeert dat — de specificatie zegt dat een patroon zonder positieve waarde genegeerd hoort te worden — maar deze lezer neemt het letterlijk en tekent streepjes van lengte nul met tussenruimtes van lengte nul. Alle drie zitten ze nu in `document_pdf_svg.dart`, met per geval opgeschreven waarom het misging. ## De terugval blijft het dragende deel Wat níet getekend kan worden toont nog steeds zijn bron in een kader met een aanduiding erboven. Dat gebeurt bij een grafiek zonder cijfers (die staan in een los `data/*.json` dat niet meekwam), bij een renderer die niets oplevert, bij een tekening die de SVG-lezer niet kan ontleden, en altijd op **web**, waar de app als CanvasKit-pagina zonder die WebView draait. Er zit een plafond op het wachten. De renderer heeft er zelf een, maar dat helpt niet tegen het geval dat hier telt: een verzoek dat de wachtrij nooit verlaat omdat de host niet gemonteerd is. Zonder plafond hangt de export dan op één diagram — en een export die blijft hangen is erger dan een diagram dat als bron in het bestand komt. ## Afweging (bewaker) **Er komt niets bij in het `.md`** — dit pad leest alleen — **en geen nieuwe partij.** MathJax lag al gebundeld en draaide al: in elk geëxporteerd HTML-bestand, bij de ontvanger. Nieuw is dat hij nu ín de app draait, in de pagina die mermaid al gebruikte: `default-src 'none'`, geen netwerk, geen `eval`, alleen de meegeleverde bundel. De TeX die erin gaat is al door OciWacht geprojecteerd, en wat eruit komt gaat door dezelfde zeef als mermaid — die hoefde niet verruimd te worden, want `defs`, `use`, `symbol` en `href` stonden er al in voor mermaid. **De botsing, hardop:** een JS-bundel in het proces laten draaien om een formule te zetten is meer aanvalsoppervlak dan de bron afdrukken. Ik laat gemak hier voorgaan omdat de pagina al bestond en dichtstaat, de uitvoer gezeefd wordt, en alles faalt-dicht. Ik zou van gedachten veranderen als de zeef verruimd had moeten worden om MathJax werkend te krijgen; dat hoefde niet. **En de belofte van gisteren is weer om.** De documentatie zei "deze drie staan erin als bron". Dat is nu de terugval en niet de regel. USER_GUIDE en KNOWN_LIMITATIONS (nl + en) zijn ingeperkt in plaats van overschreven, DOCUMENT_MODE §11.5 houdt de oude zin zichtbaar naast de nieuwe, en de dialoogtekst is in alle 31 talen vervangen — met de vervallen regel uit alle taaltabellen gehaald, want de wezenpoort ziet die. ## Toetsen - `make check` groen op de gerebasede boom (dekking 87,2%, per-bestand-vloer 0). - `make check-secrets` (gitleaks + trufflehog, werkboom én historie): geen bevindingen. `make sast` (semgrep): 0 findings. - DAST (ZAP) niet gedraaid: deze wijziging raakt het geserveerde weboppervlak niet. - **Een integratietest voor het stuk dat onder `flutter test` niet bestaat.** Mermaid en MathJax draaien in een verborgen WebView; zonder die WebView valt alles terug op de bron en zou een groene suite niets zeggen over de functie zelf. `integration_test/document_pdf_graphics_test.dart` draait de échte renderers op macOS, meet de dasharray-reparatie op de échte uitvoer van mermaid (mét de controle dat die uitvoer het patroon werkelijk bevat, anders meet de assertie niets), en schrijft de PDF weg om er met eigen ogen naar te kijken. - 96 tests in `test/pdf/`, waaronder: een grafiek die getekend wordt in plaats van afgedrukt, elke terugvalgrond apart, een onleesbare SVG die de export niet afbreekt, hetzelfde diagram dat één keer gerenderd wordt, en de drie SVG-reparaties elk op zichzelf. - Vijf nieuwe tests op de renderketen zelf, met de bestaande neppe WebView: de pagina draagt de formulerenderer, een formule gaat naar de juiste ingang, de TeX gaat als JSON de aanroep in, en een formule en een diagram met dezelfde tekst botsen niet in de cache. - Met eigen ogen nagekeken in de draaiende app: het stroomdiagram met zijn lijnen en pijlpunten, de gezette formule, en de grafiek op ware breedte.
brenno force-pushed feat/document-pdf-rasterize from df3a8524c8
All checks were successful
scans / scans (pull_request) Successful in 3m30s
static-gate / static-gate (pull_request) Successful in 7m24s
to bd45620c7d
All checks were successful
scans / scans (pull_request) Successful in 2m55s
static-gate / static-gate (pull_request) Successful in 6m32s
2026-08-20 15:47:05 +00:00
Compare
brenno merged commit 55ea984f55 into main 2026-08-20 15:53:57 +00:00
Sign in to join this conversation.
No description provided.