Regressie: een pijl in een diagramlabel breekt de PDF-export opnieuw (#1968) #1987

Closed
opened 2026-09-04 09:03:36 +00:00 by brenno · 2 comments
Owner

Wat er misgaat

laag → hoog in een diagram- of grafieklabel laat de PDF-export afbreken met
Null check operator used on a null value. Dat is dezelfde klasse fout die
#1942 dichtzette, opnieuw geopend door #1968.

package:pdf/src/pdf/font/ttf_writer.dart 96:40  TtfWriter.withChars
package:pdf/src/pdf/obj/ttffont.dart 122:28     PdfTtfFont._buildType0
package:pdf/src/pdf/document.dart 234:10        PdfDocument._write
package:pdf/src/widgets/document.dart 149:27    Document.save

Waarom

Twee dingen samen.

1. svgTypesetting kiest het symbolen-font zodra Roboto één teken mist.

final hasUncoveredByPrimary = svg.runes.any(
  (rune) => rune > 0xFF && !primaryCoverage.containsKey(rune),
);
final font = hasUncoveredByPrimary && fallback.length > 1 ? fallback[1] : unicode;

Nagemeten:

tekening gekozen snede
laag → hoog NotoSansMath
Критик высокий Roboto
Критик → высокий NotoSansMath

2. NotoSansMath-subset.ttf heeft geen letters en geen spatie. Het laagste
codepunt in zijn cmap is U+2190. Geen ASCII, geen U+0020.

De SVG-lezer van package:pdf zet alle tekst van de tekening met die ene
snede. TtfWriter.withChars doet voor de spatie een onvoorwaardelijke
ttf.charToGlyphIndexMap[32]! — en dat is null. Weg export.

Zelfs zonder die crash was de keuze verkeerd: elke letter van laag → hoog zou
in een wiskundefont gezet worden, dus als leeg blokje. Vóór #1968 stond er een
leesbaar label met één blokje op de plek van de pijl; nu is het hele document
weg.

Reproductie

Diagram- of grafieklabel met een pijl en een spatie, dan exporteren naar PDF.
Bijvoorbeeld een mermaid-label laag → hoog of een grafiekreeks
Kritiek → hoog.

Voorstel

  1. Kies de snede op dekking van de tékst van de tekening, niet op "mist het
    primaire font iets". De lezer gebruikt één snede voor álles, dus telt hoeveel
    van de tekens die er staan een kandidaat kan zetten — inclusief letters en
    spaties. laag → hoog komt dan op Roboto uit (8 van de 9), met de pijl als
    blokje en gemeld.
  2. Een kandidaat die de spatie niet dekt terwijl de tekst er een heeft, valt
    af.
    Dat is precies de crash hierboven, en het is deterministisch te
    vermijden. Blijft er niets over, dan de bron tonen — niet werpen.
  3. Inter-Variable.ttf als extra terugval. Die zit al in de app (het is de
    interfaceletter) en dekt → ← ↑ ↓ ↔ ⇒ ✓ ✗ ★ ▪ én alle letters. Dan wordt
    laag → hoog gewoon volledig gezet in plaats van met een blokje. Kost geen
    byte extra en geen licentieregel.
  4. Meld onzetbare tekens per tekening tegen de gekozen snede, niet tegen de
    unie van alle terugvalfonts. Nu zegt de unie dat een teken gedekt is terwijl
    de gekozen snede het niet kan zetten — dan is het stil verlies.

Regressietest

Een export met een diagramlabel laag → hoog moet bytes opleveren. Die staat nu
rood — hij werpt.

## Wat er misgaat `laag → hoog` in een diagram- of grafieklabel laat de PDF-export afbreken met `Null check operator used on a null value`. Dat is dezelfde klasse fout die #1942 dichtzette, opnieuw geopend door #1968. ``` package:pdf/src/pdf/font/ttf_writer.dart 96:40 TtfWriter.withChars package:pdf/src/pdf/obj/ttffont.dart 122:28 PdfTtfFont._buildType0 package:pdf/src/pdf/document.dart 234:10 PdfDocument._write package:pdf/src/widgets/document.dart 149:27 Document.save ``` ## Waarom Twee dingen samen. **1. `svgTypesetting` kiest het symbolen-font zodra Roboto één teken mist.** ```dart final hasUncoveredByPrimary = svg.runes.any( (rune) => rune > 0xFF && !primaryCoverage.containsKey(rune), ); final font = hasUncoveredByPrimary && fallback.length > 1 ? fallback[1] : unicode; ``` Nagemeten: | tekening | gekozen snede | |---|---| | `laag → hoog` | **NotoSansMath** | | `Критик высокий` | Roboto | | `Критик → высокий` | **NotoSansMath** | **2. `NotoSansMath-subset.ttf` heeft geen letters en geen spatie.** Het laagste codepunt in zijn `cmap` is U+2190. Geen ASCII, geen `U+0020`. De SVG-lezer van `package:pdf` zet *alle* tekst van de tekening met die ene snede. `TtfWriter.withChars` doet voor de spatie een onvoorwaardelijke `ttf.charToGlyphIndexMap[32]!` — en dat is `null`. Weg export. Zelfs zonder die crash was de keuze verkeerd: elke letter van `laag → hoog` zou in een wiskundefont gezet worden, dus als leeg blokje. Vóór #1968 stond er een leesbaar label met één blokje op de plek van de pijl; nu is het hele document weg. ## Reproductie Diagram- of grafieklabel met een pijl en een spatie, dan exporteren naar PDF. Bijvoorbeeld een mermaid-label `laag → hoog` of een grafiekreeks `Kritiek → hoog`. ## Voorstel 1. **Kies de snede op dekking van de tékst van de tekening**, niet op "mist het primaire font iets". De lezer gebruikt één snede voor álles, dus telt hoeveel van de tekens die er staan een kandidaat kan zetten — inclusief letters en spaties. `laag → hoog` komt dan op Roboto uit (8 van de 9), met de pijl als blokje en gemeld. 2. **Een kandidaat die de spatie niet dekt terwijl de tekst er een heeft, valt af.** Dat is precies de crash hierboven, en het is deterministisch te vermijden. Blijft er niets over, dan de bron tonen — niet werpen. 3. **`Inter-Variable.ttf` als extra terugval.** Die zit al in de app (het is de interfaceletter) en dekt `→ ← ↑ ↓ ↔ ⇒ ✓ ✗ ★ ▪` én alle letters. Dan wordt `laag → hoog` gewoon volledig gezet in plaats van met een blokje. Kost geen byte extra en geen licentieregel. 4. **Meld onzetbare tekens per tekening tegen de gekozen snede**, niet tegen de unie van alle terugvalfonts. Nu zegt de unie dat een teken gedekt is terwijl de gekozen snede het niet kan zetten — dan is het stil verlies. ## Regressietest Een export met een diagramlabel `laag → hoog` moet bytes opleveren. Die staat nu rood — hij werpt.
Author
Owner

Opgepakt. Tak: fix/pdf-svg-fontkeuze-1987. Verwachte reikwijdte: lib/services/pdf/document_pdf_fonts.dart, document_pdf_export.dart, lib/widgets/parts/document_export_pdf_support.dart, document_editor_screen.dart, pubspec.yaml (Inter als exportasset), plus tests.

Opgepakt. Tak: fix/pdf-svg-fontkeuze-1987. Verwachte reikwijdte: lib/services/pdf/document_pdf_fonts.dart, document_pdf_export.dart, lib/widgets/parts/document_export_pdf_support.dart, document_editor_screen.dart, pubspec.yaml (Inter als exportasset), plus tests.
Author
Owner

Gerepareerd en op main (merge 3f85f0a7f, PR #1989).

  • De terugvalfonts zijn één geordende lijst (fallbackFonts) in plaats van twee benoemde rollen. De vorige vorm gaf geen plek aan de vraag wélke snede een tekening krijgt.
  • Inter-Variable.ttf staat erin, tussen Roboto en het wiskundefont. Zat al in de app als interfaceletter, dekt → ← ↑ ↓ ↔ ⇒ ✓ ✗ ★ ▪ én alle letters — geen byte extra, geen licentieregel, geen SBOM-wijziging.
  • Een tekening krijgt de eerste snede die haar hélemaal kan zetten, of anders haar bron. Reden staat in de doc-comment: een ontbrekende glyph wordt in TtfWriter.withChars niet betrouwbaar een blokje (laag ⨁ ∮ hoog → blokjes, a ⨁ ∮ → worp), en op zo'n grens valt geen regel te bouwen.

Acht regressietests. De belangrijkste les zit niet in de code maar in de toets: de exporttests bouwden hun fonts met Roboto alleen, terwijl de app met drie fonts exporteert. Daardoor was de hele klasse fouten die uit de fontkeuze komt onbereikbaar voor de test. Ze gebruiken nu dezelfde lijst en volgorde als de schil.

Eén bestaande test uit #1968 legde bovendien het foute gedrag vást: hij toetste wélk font gekozen werd (fallback[1]) in plaats van of de gekozen snede het label kán zetten. Omgeschreven naar de eigenschap.

Met eigen ogen bekeken: een export met → ✓ ≤, gedachtestreepje en krul-apostrofs in kop, alinea, grafiektitel, aslabels en legenda staat er volledig, en unsupportedCharacters is leeg.

Gerepareerd en op `main` (merge 3f85f0a7f, PR #1989). - De terugvalfonts zijn één geordende lijst (`fallbackFonts`) in plaats van twee benoemde rollen. De vorige vorm gaf geen plek aan de vraag wélke snede een tekening krijgt. - `Inter-Variable.ttf` staat erin, tussen Roboto en het wiskundefont. Zat al in de app als interfaceletter, dekt `→ ← ↑ ↓ ↔ ⇒ ✓ ✗ ★ ▪` én alle letters — geen byte extra, geen licentieregel, geen SBOM-wijziging. - Een tekening krijgt de eerste snede die haar hélemaal kan zetten, of anders haar bron. Reden staat in de doc-comment: een ontbrekende glyph wordt in `TtfWriter.withChars` niet betrouwbaar een blokje (`laag ⨁ ∮ hoog` → blokjes, `a ⨁ ∮` → worp), en op zo'n grens valt geen regel te bouwen. Acht regressietests. De belangrijkste les zit niet in de code maar in de toets: de exporttests bouwden hun fonts met **Roboto alleen**, terwijl de app met drie fonts exporteert. Daardoor was de hele klasse fouten die uit de fontkeuze komt onbereikbaar voor de test. Ze gebruiken nu dezelfde lijst en volgorde als de schil. Eén bestaande test uit #1968 legde bovendien het foute gedrag vást: hij toetste *wélk* font gekozen werd (`fallback[1]`) in plaats van of de gekozen snede het label kán zetten. Omgeschreven naar de eigenschap. Met eigen ogen bekeken: een export met `→ ✓ ≤`, gedachtestreepje en krul-apostrofs in kop, alinea, grafiektitel, aslabels en legenda staat er volledig, en `unsupportedCharacters` is leeg.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
LibreKAT/Ocideck#1987
No description provided.