Een gedachtestreepje in een grafiektitel kostte de hele PDF-export (#1942) #1966

Merged
brenno merged 5 commits from fix/pdf-svg-unicode-1942 into main 2026-09-03 12:13:43 +00:00
Owner

Wat er misging

De SVG-lezer van package:pdf kiest voor elke <text> in een ingesloten tekening hardgecodeerd een van de veertien standaardsneden (src/svg/painter.dart:117 — Helvetica, Times of Courier naar font-family), zonder terugvallijst. Die sneden reiken tot Latin-1, en PdfFont.stringMetrics werpt op alles daarboven.

Gevolg: niet dat ene diagram viel weg, maar het hele document. En het gaat niet om exotische schriften — nagemeten met een kale pw.SvgImage:

in de SVG-tekst uitkomst
Start OK
(em-streepje) ArgumentError
ArgumentError
Cyrillisch / Grieks / ł ArgumentError

Dat zijn precies de tekens die een tekstverwerker vanzelf maakt. Grafieken zetten de titel, reeksnamen en aslabels van de auteur letterlijk in <text>, dus de weg ernaartoe was kort.

De bestaande try in _graphic ving het niet: die dekt de constructor, terwijl de worp uit SvgImage.paint komt — tijdens document.save().

Wat er verandert

  • DocumentPdfFonts.svgTypesetting zegt per tekening wat er moet gebeuren: standaardsneden volstaan, dít Unicode-font moet het doen, of de tekening is niet te zetten. Het gebundelde Roboto dat de export toch al als fontFallback meedraagt, gaat via customFontLookup mee.
  • Geen bijwerking op het gewone geval. Een tekening zonder zulke tekens blijft op de standaardsneden, want díe dragen een echte vette en cursieve snede — de reden waarom document_pdf_fonts.dart ze koos.
  • Is er geen Unicode-font, dan valt de tekening terug op haar bron in plaats van de export af te breken. Dezelfde afweging die er voor een onleesbare SVG al stond; alleen kon de try hem hier niet maken.
  • De melding over onzetbare tekens hield op bij de rand van elke tekening. Zonder dat erbij zou deze reparatie een luide fout in stil verlies veranderen: een pijl () staat níét in het gebundelde Roboto en wordt dan een leeg blokje. svgTextContent leest nu ook de <text>- en <tspan>-knopen, zodat unsupportedCharacters hem meldt.
  • Meegenomen: sanitizeMermaidSvg liet een <defs> leeg achter (mermaid vult hem met <marker> en <style>, allebei gaan ze eruit). De serializer schrijft dat als <defs/>, en juist die zelfsluitende vorm handelt vector_graphics_compiler niet af — vandaar unhandled element <defs/> in elke debug-run. Er ging niets verloren, maar een melding die niets betekent leert je de meldingen negeren die wél iets betekenen.

Twee kanten op benaderd, met opzet

svgTypesetting leest de héle SVG: een scan die één plek mist waar tekst kan staan, zet de afbreker terug. Te ruim kiezen kost daar hooguit een ander font.

svgTextContent leest juist krap, alleen de tekstknopen: een melding die een teken noemt dat wél gewoon in het bestand staat, leert de gebruiker de melding negeren. Een teken minder gemeld is beter dan een teken ten onrechte.

Regressietests

Acht nieuwe tests, waarvan er vier rood stonden tegen de onherstelde code, elk op de eigenschap zelf:

  • een gedachtestreepje in een grafiektitel breekt de export niet
  • de tekening komt dan op het Unicode-font (afgelezen aan /BaseFont in het bestand)
  • een Cyrillisch diagramlabel breekt de export niet
  • zonder Unicode-font valt de tekening terug op haar bron
  • een tekening zónder zulke tekens blijft op de standaardsneden (het gewone geval)
  • een teken dat ook Roboto niet kent () wordt gemeld in plaats van stil weggelaten
  • inline-wiskunde: zonder passende snede blijft de TeX in de zin staan
  • svgTextContent leest tekstknopen en géén attribuutwaarden

Met eigen ogen bekeken

Een echte export met een gedachtestreepje in de kop, de alinea, de grafiektitel en krul-apostrofs in de aslabels, gerasterd met QuickLook: alles staat er. (De eerste rasteraar liet de lopende tekst weg — die had de standaardsneden niet, die niet ingebed worden. Dat was de rasteraar, niet het bestand.)

Bewaker

Deze wijziging raakt geen bestandsformaat, geen opslag, geen afhankelijkheid, geen uitgaand verkeer en geen publieke belofte — het gaat om de uitvoer van één exportroute. Bewaker-stap expliciet overgeslagen.

Test plan

  • make check groen (opmaak, analyse, conventies, volledige suite, dekkingsvloeren, golden)
  • make check-secrets groen (gitleaks + trufflehog)
  • make sast groen (semgrep)
  • Handmatig: PDF geëxporteerd en de pagina bekeken

Closes #1942

🤖 Generated with Claude Code

## Wat er misging De SVG-lezer van `package:pdf` kiest voor elke `<text>` in een ingesloten tekening hardgecodeerd een van de veertien standaardsneden (`src/svg/painter.dart:117` — Helvetica, Times of Courier naar `font-family`), **zonder terugvallijst**. Die sneden reiken tot Latin-1, en `PdfFont.stringMetrics` werpt op alles daarboven. Gevolg: niet dat ene diagram viel weg, maar het hele document. En het gaat niet om exotische schriften — nagemeten met een kale `pw.SvgImage`: | in de SVG-tekst | uitkomst | |---|---| | `Start` | OK | | `—` (em-streepje) | `ArgumentError` | | `’` `…` `→` | `ArgumentError` | | Cyrillisch / Grieks / `ł` | `ArgumentError` | Dat zijn precies de tekens die een tekstverwerker vanzelf maakt. Grafieken zetten de titel, reeksnamen en aslabels van de auteur letterlijk in `<text>`, dus de weg ernaartoe was kort. De bestaande `try` in `_graphic` ving het niet: die dekt de constructor, terwijl de worp uit `SvgImage.paint` komt — tijdens `document.save()`. ## Wat er verandert - **`DocumentPdfFonts.svgTypesetting`** zegt per tekening wat er moet gebeuren: standaardsneden volstaan, dít Unicode-font moet het doen, of de tekening is niet te zetten. Het gebundelde Roboto dat de export toch al als `fontFallback` meedraagt, gaat via `customFontLookup` mee. - **Geen bijwerking op het gewone geval.** Een tekening zonder zulke tekens blijft op de standaardsneden, want díe dragen een echte vette en cursieve snede — de reden waarom `document_pdf_fonts.dart` ze koos. - **Is er geen Unicode-font, dan valt de tekening terug op haar bron** in plaats van de export af te breken. Dezelfde afweging die er voor een onleesbare SVG al stond; alleen kon de `try` hem hier niet maken. - **De melding over onzetbare tekens hield op bij de rand van elke tekening.** Zonder dat erbij zou deze reparatie een luide fout in stil verlies veranderen: een pijl (`→`) staat níét in het gebundelde Roboto en wordt dan een leeg blokje. `svgTextContent` leest nu ook de `<text>`- en `<tspan>`-knopen, zodat `unsupportedCharacters` hem meldt. - **Meegenomen:** `sanitizeMermaidSvg` liet een `<defs>` leeg achter (mermaid vult hem met `<marker>` en `<style>`, allebei gaan ze eruit). De serializer schrijft dat als `<defs/>`, en juist die zelfsluitende vorm handelt `vector_graphics_compiler` niet af — vandaar `unhandled element <defs/>` in elke debug-run. Er ging niets verloren, maar een melding die niets betekent leert je de meldingen negeren die wél iets betekenen. ## Twee kanten op benaderd, met opzet `svgTypesetting` leest de héle SVG: een scan die één plek mist waar tekst kan staan, zet de afbreker terug. Te ruim kiezen kost daar hooguit een ander font. `svgTextContent` leest juist krap, alleen de tekstknopen: een melding die een teken noemt dat wél gewoon in het bestand staat, leert de gebruiker de melding negeren. Een teken minder gemeld is beter dan een teken ten onrechte. ## Regressietests Acht nieuwe tests, waarvan er vier rood stonden tegen de onherstelde code, elk op de eigenschap zelf: - een gedachtestreepje in een grafiektitel breekt de export niet - de tekening komt dan op het Unicode-font (afgelezen aan `/BaseFont` in het bestand) - een Cyrillisch diagramlabel breekt de export niet - zonder Unicode-font valt de tekening terug op haar bron - een tekening zónder zulke tekens blijft op de standaardsneden (het gewone geval) - een teken dat ook Roboto niet kent (`→`) wordt gemeld in plaats van stil weggelaten - inline-wiskunde: zonder passende snede blijft de TeX in de zin staan - `svgTextContent` leest tekstknopen en géén attribuutwaarden ## Met eigen ogen bekeken Een echte export met een gedachtestreepje in de kop, de alinea, de grafiektitel en krul-apostrofs in de aslabels, gerasterd met QuickLook: alles staat er. (De eerste rasteraar liet de lopende tekst weg — die had de standaardsneden niet, die niet ingebed worden. Dat was de rasteraar, niet het bestand.) ## Bewaker Deze wijziging raakt geen bestandsformaat, geen opslag, geen afhankelijkheid, geen uitgaand verkeer en geen publieke belofte — het gaat om de uitvoer van één exportroute. Bewaker-stap expliciet overgeslagen. #### Test plan - [x] `make check` groen (opmaak, analyse, conventies, volledige suite, dekkingsvloeren, golden) - [x] `make check-secrets` groen (gitleaks + trufflehog) - [x] `make sast` groen (semgrep) - [x] Handmatig: PDF geëxporteerd en de pagina bekeken Closes #1942 🤖 Generated with [Claude Code](https://claude.com/claude-code)
De SVG-lezer van `package:pdf` kiest voor elke `<text>` in een ingesloten
tekening hardgecodeerd een van de veertien standaardsneden, zonder
terugvallijst. Die reiken tot Latin-1, en `stringMetrics` werpt op alles
daarboven — vanuit `SvgImage.paint`, dus tijdens `document.save()` en ruim
voorbij de `try` rond de tekening zelf. Niet dat ene diagram viel weg maar het
hele document, en dat al bij een gedachtestreepje of een krul-apostrof in een
grafiektitel.

Een tekening met zulke tekens gaat nu via `customFontLookup` op het gebundelde
Unicode-font dat de export toch al als terugval meedraagt; de rest blijft op de
standaardsneden, want die dragen een echte vette en cursieve snede. Is er geen
Unicode-font, dan valt de tekening terug op haar bron in plaats van de export af
te breken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
De opschoning haalt `<marker>` en `<style>` uit de `defs` van mermaid weg omdat
flutter_svg ze toch niet leest, en de serializer schrijft wat overblijft als
`<defs/>`. Juist die zelfsluitende vorm handelt `vector_graphics_compiler` niet
af, vandaar "unhandled element <defs/>" in elke debug-run. Er ging niets
verloren — de defs was al leeg — maar een melding die niets betekent leert je de
meldingen negeren die wel iets betekenen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vier ervan stonden rood op de reparatie hiervoor, elk op de eigenschap zelf:
een gedachtestreepje in een grafiektitel, een Cyrillisch diagramlabel, de snede
waarop de tekening terechtkomt, en de terugval naar de bron wanneer er geen
Unicode-snede is. De vijfde bewaakt het gewone geval: een tekening zonder zulke
tekens blijft op de standaardsneden staan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(pdf): meld ook de tekens die in een tekening niet gezet konden worden (#1942)
All checks were successful
scans / scans (pull_request) Successful in 8m48s
static-gate / static-gate (pull_request) Successful in 12m28s
109e93b78a
De reparatie hiervoor verandert een luide fout in stil verlies: een teken dat
ook het gebundelde Roboto niet kent (een pijl, bijvoorbeeld) werpt niet meer
maar wordt een leeg blokje in de tekening. De melding daarvoor bestond al, maar
hield op bij de rand van elke grafiek en elk diagram.

`svgTextContent` leest de tekst uit de `<text>`- en `<tspan>`-knopen en voegt
die toe aan wat `unsupportedRunes` weegt. Met opzet krap gelezen — precies
andersom dan `svgTypesetting`: te ruim kiezen kost daar een ander font, maar
een teken hier ten onrechte melden leert de gebruiker de melding negeren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
brenno force-pushed fix/pdf-svg-unicode-1942 from 109e93b78a
All checks were successful
scans / scans (pull_request) Successful in 8m48s
static-gate / static-gate (pull_request) Successful in 12m28s
to e89263beeb
All checks were successful
scans / scans (pull_request) Successful in 3m41s
static-gate / static-gate (pull_request) Successful in 10m29s
2026-09-03 11:40:36 +00:00
Compare
brenno merged commit 8c7280d82b into main 2026-09-03 12:13:43 +00:00
Sign in to join this conversation.
No description provided.