fix(pdf): zes opmaakfouten in de documentexport (#1789-#1794) + documentmaat 12pt #1795

Merged
brenno merged 8 commits from fix/pdf-opmaakbugs-1790-1793 into main 2026-08-26 13:20:27 +00:00
Owner

Waarom

Drie geleverde incidentrapporten teruggelezen als PDF leverde zes opmaakfouten in de documentexport op. Ze zijn ingediend als #1789–#1794 en hier gerepareerd, elk met een regressietoets die eerst rood was tegen de onherstelde code.

Vooraf ging een splitsing: document_pdf_widgets.dart stond op 1025 regels tegen een plafond van 1026. Het kolombreedte-rekenwerk is naar document_pdf_table_widths.dart verhuisd — het raakt geen pw.Widget, kent de stijl niet en was al los getoetst.

Wat er is gerepareerd

Issue Fout Aanpak
#1791 lijstnummer vanaf 10 valt uiteen over twee regels de goot groeit mee met het breedste merkteken van de lijst
#1792 een link van meerdere woorden is per woord onderstreept aangrenzende spans met gelijke opmaak worden in de ontleder samengevoegd
#1790 een tabelkopregel staat verweesd onderaan een blad een tabel die op één blad past blijft heel — deels, zie hieronder
#1793 de kop van de tijdkolom staat boven elke tijdlijnkaart één keer boven de kolom; de tijd lijnt ook bij afbreken rechts uit
#1789 #1794 brede tabellen breken woorden, hashes en IP-adressen middenin af de letter van de tabel krimpt tot elke kolom haar langste woord draagt

Daarnaast: de standaardlettermaat voor documenten gaat van 15,5 naar 12 punt. 15,5 was schermmaat die als bladstandaard is meegereisd zonder ooit tegen papier gehouden te zijn.

Twee restpunten, bewust open

  • #1790 blijft open. Een tabel die over meerdere bladen loopt houdt zijn verweesde kop. pw.Table kent daar geen instelling voor en MultiPage kan een spannende widget niet vragen zich te verplaatsen. Een volledige reparatie vraagt een eigen SpanningWidget die na de opmaak vaststelt dat er alleen herhaalrijen geplaatst zijn en zichzelf terugtrekt — met kans op een oneindige lus wanneer de tabel op geen enkel blad past. Dat risico weegt hier niet op tegen de winst.
  • #1789 houdt een restpunt. Een SHA-512 van 128 tekens is breder dan een A4 ooit kan zijn. Onder minTableFontScale krimpt de letter niet verder, dus die tabel houdt zijn afbrekingen; daar is de tabelvorm zelf de verkeerde keuze.

Bewaker

Aangeroepen voor de wijziging van de standaardlettermaat, omdat die de betekenis van een bestaand .md raakt.

Geen bezwaar op soevereiniteit of uitwisselbaarheid: het bestand verandert niet, het veld blijft gedocumenteerd en binnen bereik, er komt geen partij bij. Wel één punt dat stond: toJson schrijft documentBodyFontSize onvoorwaardelijk, dus elk ooit opgeslagen profiel draagt zijn maat expliciet en verandert niet — geraakt worden alleen stijlen waarin het veld helemaal ontbreekt, van vóór het bestond. Dat is een bewuste herinterpretatie van die bestanden en hoort daarom in de CHANGELOG te staan, niet alleen in een commit. Toegevoegd.

Drie doodlopende wegen, voor de volgende lezer

  • Bij #1792 leek het eerst aan AnnotationUrl te liggen (per span een vers object), daarna aan de lengte van de linktekst. Allebei fout: het bleek de tabelcel, waar de ontleder één span per wóórd maakt terwijl een alinea één span oplevert.
  • Bij #1790 schreef ik eerst een toets die groen bleef mét én zónder fix — de vullengte raakte de bladrand niet. De trigger is uitgezocht, niet geraden.
  • Bij #1794 was de eerste krimp niet genoeg omdat _charWidthFactor in kleine letters meet en op een schreefloze letter geijkt is. Vandaar de marge op de schatting.

Toetsplan

  • make check groen
  • 147 PDF-toetsen, waarvan 6 nieuw; elke nieuwe toets één keer rood geproefd tegen de onherstelde code
  • make check-secrets en make sast
  • visuele keuring — de rapporten worden pas herexporteerd als beide restpunten weg zijn
## Waarom Drie geleverde incidentrapporten teruggelezen als PDF leverde zes opmaakfouten in de documentexport op. Ze zijn ingediend als #1789–#1794 en hier gerepareerd, elk met een regressietoets die eerst rood was tegen de onherstelde code. Vooraf ging een splitsing: `document_pdf_widgets.dart` stond op 1025 regels tegen een plafond van 1026. Het kolombreedte-rekenwerk is naar `document_pdf_table_widths.dart` verhuisd — het raakt geen `pw.Widget`, kent de stijl niet en was al los getoetst. ## Wat er is gerepareerd | Issue | Fout | Aanpak | |---|---|---| | #1791 | lijstnummer vanaf 10 valt uiteen over twee regels | de goot groeit mee met het breedste merkteken van de lijst | | #1792 | een link van meerdere woorden is per woord onderstreept | aangrenzende spans met gelijke opmaak worden in de ontleder samengevoegd | | #1790 | een tabelkopregel staat verweesd onderaan een blad | een tabel die op één blad past blijft heel — **deels**, zie hieronder | | #1793 | de kop van de tijdkolom staat boven elke tijdlijnkaart | één keer boven de kolom; de tijd lijnt ook bij afbreken rechts uit | | #1789 #1794 | brede tabellen breken woorden, hashes en IP-adressen middenin af | de letter van de tabel krimpt tot elke kolom haar langste woord draagt | Daarnaast: de **standaardlettermaat voor documenten gaat van 15,5 naar 12 punt**. 15,5 was schermmaat die als bladstandaard is meegereisd zonder ooit tegen papier gehouden te zijn. ## Twee restpunten, bewust open - **#1790 blijft open.** Een tabel die over meerdere bladen loopt houdt zijn verweesde kop. `pw.Table` kent daar geen instelling voor en `MultiPage` kan een spannende widget niet vragen zich te verplaatsen. Een volledige reparatie vraagt een eigen `SpanningWidget` die na de opmaak vaststelt dat er alleen herhaalrijen geplaatst zijn en zichzelf terugtrekt — met kans op een oneindige lus wanneer de tabel op geen enkel blad past. Dat risico weegt hier niet op tegen de winst. - **#1789 houdt een restpunt.** Een SHA-512 van 128 tekens is breder dan een A4 ooit kan zijn. Onder `minTableFontScale` krimpt de letter niet verder, dus die tabel houdt zijn afbrekingen; daar is de tabelvorm zelf de verkeerde keuze. ## Bewaker Aangeroepen voor de wijziging van de standaardlettermaat, omdat die de betekenis van een bestaand `.md` raakt. Geen bezwaar op soevereiniteit of uitwisselbaarheid: het bestand verandert niet, het veld blijft gedocumenteerd en binnen bereik, er komt geen partij bij. Wel één punt dat stond: `toJson` schrijft `documentBodyFontSize` onvoorwaardelijk, dus elk ooit opgeslagen profiel draagt zijn maat expliciet en verandert niet — geraakt worden alleen stijlen waarin het veld helemaal ontbreekt, van vóór het bestond. Dat is een bewuste herinterpretatie van die bestanden en hoort daarom in de CHANGELOG te staan, niet alleen in een commit. Toegevoegd. ## Drie doodlopende wegen, voor de volgende lezer - Bij #1792 leek het eerst aan `AnnotationUrl` te liggen (per span een vers object), daarna aan de lengte van de linktekst. Allebei fout: het bleek de tabelcel, waar de ontleder één span per wóórd maakt terwijl een alinea één span oplevert. - Bij #1790 schreef ik eerst een toets die groen bleef mét én zónder fix — de vullengte raakte de bladrand niet. De trigger is uitgezocht, niet geraden. - Bij #1794 was de eerste krimp niet genoeg omdat `_charWidthFactor` in kleine letters meet en op een schreefloze letter geijkt is. Vandaar de marge op de schatting. ## Toetsplan - [x] `make check` groen - [x] 147 PDF-toetsen, waarvan 6 nieuw; elke nieuwe toets één keer rood geproefd tegen de onherstelde code - [x] `make check-secrets` en `make sast` - [ ] visuele keuring — de rapporten worden pas herexporteerd als beide restpunten weg zijn
`document_pdf_widgets.dart` stond op 1025 regels tegen een plafond van 1026.
Eén regel lucht is geen ruimte om zes opmaakbugs in te repareren, en de
ratchet vraagt bij die stand zelf om een splitsing.

`pdfTableColumnWidths` en zijn schatters zijn de natuurlijke breuklijn: ze
raken geen enkele `pw.Widget` en kennen de stijl niet. Het is rekenwerk dat
celtekst, corpsgrootte en bladbreedte omzet in een verdeelsleutel — al los
getoetst in `pdf_table_column_widths_test.dart`, alleen niet los ondergebracht.

Geen gedragswijziging: 141 PDF-tests blijven groen. Het plafond van het
tekenbestand zakt van 1026 naar 875 (telling 849).
De goot vóór een lijstpunt stond vast op `indent` (1,4 em). Een enkel cijfer
plus punt past daar net in; vanaf "10." niet meer, en `package:pdf` brak het
nummer dan middenin af — de `1` op de ene regel, de `0.` op de volgende.
Zichtbaar in de CBW-notitie §7.2/7.3 en in §10 van het Q-schijf-rapport.

De goot groeit nu mee met het breedste merkteken van de lijst. Eén maat voor
de hele lijst en niet per punt: anders schuift de tekst bij item tien een
stukje op en loopt de linkermarge uiteen.

De regressietoets leest de tekstlaag terug en eist merkteken en punttekst
aaneen ("10. punt 10"). Alleen op het nummer zoeken gaf een vals alarm: "punt
1" gevolgd door merkteken "2." leest ook als "1 2.".
De Markdown-ontleder levert de tekst van een link in een *tabelcel* als één
stuk per woord aan, terwijl dezelfde link in een alinea één stuk is. Voor de
betekenis maakt dat niets uit, voor het zetwerk wel: `package:pdf` tekent de
onderstreping per stuk en voegt twee stukken alleen samen als hun stijl- én
annotatieobject identiek zijn — en die maakt de renderer per stuk vers aan.
Eén bronverwijzing viel daardoor uiteen in losse onderstreepte woorden met een
gat op elke spatie.

Aangrenzende stukken met gelijke opmaak worden nu samengevoegd, in de
ontleder en niet in de renderer: hoe de ontleder zijn tekstknopen knipt hoort
geen invloed te hebben op wat de lezer ziet, en elk uitvoerpad profiteert mee.

Onderweg twee doodlopende sporen afgelegd en weer opgeruimd: het lag niet aan
`AnnotationUrl` (die per span delen verandert niets) en niet aan de lengte van
de linktekst — een link van vier woorden viel in een tabel ook al uiteen.

De regressietoets leest de getekende lijnstukken terug en telt ze per hoogte:
een tabelrand staat nooit met z'n tweeën op dezelfde hoogte, een per woord
getekende onderstreping wel. Breken over een regeleinde blijft toegestaan.
`package:pdf` plaatst rijen tot er één niet meer past. Past onderaan een blad
alleen de herhaalde kopregel nog, dan tekent hij die daar en begint de inhoud
op het volgende blad — met de kop daar opnieuw. De lezer ziet een lege gele
balk die niets aankondigt.

`Table` kent geen instelling om dat te voorkomen en `MultiPage` kan een
spannende widget niet vragen zich te verplaatsen. Een volledige reparatie
vraagt een eigen `SpanningWidget` die na de opmaak vaststelt dat er alleen
herhaalrijen geplaatst zijn en zichzelf terugtrekt — met kans op een oneindige
lus wanneer de tabel op geen enkel blad past. Dat is hier bewust niet gedaan.

Wat wél kan: een tabel die tóch op één blad past er als geheel op houden. Dat
neemt de wees weg voor de korte tabellen die een rapport vult. Een tabel die
over meerdere bladen loopt houdt het euvel; #1790 blijft daarvoor open.

De grens meet twee dingen, want een tabel kan op twee manieren te hoog worden:
honderd rijen "rij 3 | 3" tellen nauwelijks tekens en zijn tóch meters hoog.
Alleen op tekens meten liet die binden en brak de bestaande toets op een
doorlopende tabel van 120 rijen — die toets deed precies zijn werk.

De regressietoets leest de tekst per blad terug en eist dat elk blad met de
kopregel ook een inhoudsrij draagt. Breken mag; alleen achterblijven niet.
De vullengte (47 alinea's) is uitgezocht, niet geraden: bij kortere vulling
valt de bladrand niet tussen kop en eerste rij en toetst hij niets.
De kop van de eerste kolom stond boven élke kaart. Bij een korte kop ("Tijd")
valt dat niet op; bij "Lokale tijd (CEST, UTC+02:00)" is het vijftig keer
dezelfde regel in acht bladzijden. Een kolomkop hoort bij de kolom, niet bij
de rij — hij staat nu één keer, boven de kolom waar hij over gaat.

Daarnaast lijnt de tijd nu rechts uit, ook wanneer hij over twee regels
breekt. De Column stond al op `end`, maar een afbrekende `Text` vult de volle
breedte en zette zijn eigen regels links — waardoor een korte tijd rechts
stond en een lange links, en de kolom rafelig oogde. De tekst-terugroep van de
tijdlijn draagt daarvoor nu een uitlijning.

De kaartkop ("GEBEURTENIS") blijft staan: die hoort wél bij de kaart.
De verdeelsleutel van #1727 verdeelt de breedte evenredig met het langste woord
per kolom. Dat werkt zolang de som van die langste woorden op het blad past.
Bij zeven prozakolommen is dat niet zo, en dan helpt geen verdeelsleutel meer:
de breedte is op. `package:pdf` brak dan middenin het woord af —
`Veiligheidsvraagstu` / `k`, `Kritie` / `k`, `R-0` / `1` — en middenin een hash
of IP-adres, waar de lezer de waarde niet eens meer kan overnemen.

De letter van de tabel krimpt nu tot elke kolom haar langste woord op één
regel draagt. Dat maakt élk langste woord evenredig smaller en herstelt de
pasvorm zonder aan de verdeling te tornen; zeven kolommen op 8 punt leest een
stuk beter dan zeven op 11 met een afbreking in elk tweede woord. De
vaste-breedteletter krimpt mee, want juist daar staan de hashes.

Twee dingen die het meten leerde:

- De schatting is stelselmatig te optimistisch — `_charWidthFactor` meet in
  kleine letters en is geijkt op een schreefloze letter, terwijl een rapport
  vaak een schreefletter zet. Zonder marge kwam elke kolom één teken tekort en
  brak er alsnog een woord af. Vandaar `_estimateMargin`.
- De standaardmaat voor documenten is 15,5 punt; de RWM-rapporten zetten 11.
  Op 15,5 passen zeven prozakolommen ook gekrompen niet, en dat is geen fout
  maar een eigenschap van het blad.

Onder `minTableFontScale` krimpt de letter niet verder: daaronder wordt een
tabel eerder onleesbaar dan behulpzaam. Een SHA-512 van 128 tekens is breder
dan een A4 ooit kan zijn en houdt dus zijn afbrekingen — voor die tabel is de
tabelvorm zelf de verkeerde keuze. Dat restpunt blijft bij #1789 staan.

Het plafond van het tekenbestand gaat van 875 naar 975; netto blijft het
kleiner dan de 1026 van vóór de splitsing.
15,5 punt is schermmaat, geen documentmaat: het was de maat waarin de lezer
altijd al stond, en die is als standaard voor een *blad* meegereisd zonder dat
iemand hem daar tegen papier hield. Op A4 leest 15,5 als grootletterdruk, en
het maakt brede tabellen onnodig krap — bij het werk aan #1794 bleek dat zeven
prozakolommen op 15,5 ook gekrompen niet passen, terwijl ze op 11 wél passen.

Twaalf punt is een gewone tekstmaat op papier. Het bereik (9-28) blijft, dus
wie de oude maat wil kan hem gewoon zetten; een document dat de maat expliciet
draagt verandert niet.

Drie toetsen pinden het oude getal vast — twee ervan gebruikten `31.0` als
"het dubbele van de standaard". Ze rekenen nu vanaf de constante, zodat een
volgende wijziging van de standaardmaat ze niet opnieuw omgooit.
docs(changelog): Development log voor de zes PDF-fixes en de documentmaat
All checks were successful
scans / scans (pull_request) Successful in 2m9s
static-gate / static-gate (pull_request) Successful in 5m10s
0b5356ac77
De wijziging van de standaardlettermaat verdient hier een eigen plek en niet
alleen een commit: `toJson` schrijft `documentBodyFontSize` onvoorwaardelijk,
dus elk ooit opgeslagen profiel houdt zijn maat — maar een stijl waarin het
veld helemaal ontbreekt rendert voortaan kleiner. Dat is een bewuste
herinterpretatie van bestaande bestanden, en waarde 2 vraagt dat je die
opschrijft waar een gebruiker hem tegenkomt.
brenno merged commit aa6d43bf26 into main 2026-08-26 13:20:27 +00:00
Sign in to join this conversation.
No description provided.