From 1d9abcb0a0735261d5d06efa49eafd195cd04834 Mon Sep 17 00:00:00 2001 From: bolbol Date: Sat, 22 Aug 2026 08:17:48 +0000 Subject: [PATCH] deploy: runbook pdfkit widthOfString corruption flux (rla-api) --- ...kit-widthofstring-corruption-troncature.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 common/pdfkit-widthofstring-corruption-troncature.md diff --git a/common/pdfkit-widthofstring-corruption-troncature.md b/common/pdfkit-widthofstring-corruption-troncature.md new file mode 100644 index 0000000..20ddc98 --- /dev/null +++ b/common/pdfkit-widthofstring-corruption-troncature.md @@ -0,0 +1,50 @@ +# PDFKit — doc.widthOfString() en boucle corrompt silencieusement le flux de contenu (rla-api) + +**Date** : 2026-08-22 +**Contexte** : export PDF `rla-api`, vue Synthèse. Le tableau "Top 10 Alertes Délais" n'affichait qu'une seule ligne sur dix, sans aucune erreur ni exception — le PDF généré restait parfaitement valide. + +## Symptôme + +Une table PDFKit dessinée via une boucle `for (const row of rows) { ... doc.text(...) ... }` s'arrêtait silencieusement après la première ligne dans certains cas, alors que : +- les données en entrée étaient complètes et correctes (vérifié par log) +- la boucle JS s'exécutait bien pour toutes les itérations (vérifié par log, `y` s'incrémentait normalement) +- le PDF produit restait valide (pas de crash, pas d'erreur HTTP, xref/trailer corrects) +- `pdftotext` sur le PDF final ne retrouvait qu'une seule ligne de données + +Une table adjacente sur la même page (3 lignes, valeurs courtes) et une autre vue du même export (30 lignes sur 2 pages) fonctionnaient normalement avec le même code `table()`. + +## Cause racine + +Une fonction de troncature de texte (`truncateToWidth`) appelait `doc.widthOfString()` en boucle `while` (retirant un caractère à la fois jusqu'à ce que le texte tienne dans la largeur de colonne) pour mesurer précisément le texte à afficher dans chaque cellule. Pour des colonnes étroites (~64pt) avec des valeurs longues ("AO 58/2024 : Lot10 - CSC Sfax Medina"), ça représentait 20-30 appels à `widthOfString()` par cellule, répétés sur plusieurs cellules du même tableau. + +Au-delà d'un certain volume cumulé d'appels à `doc.widthOfString()` sur le même document PDFKit, le flux de contenu interne se corrompt silencieusement — les opérations de dessin (`doc.text()`, `doc.rect().fill()`) suivantes cessent d'être écrites dans le flux final, sans qu'aucune exception ne soit levée. Le seuil exact n'a pas été déterminé, mais le pattern est net : les tables avec peu/pas de troncature réelle (valeurs courtes) ne déclenchent jamais le bug ; celles qui tronquent beaucoup de texte, oui. + +Une première tentative de fix avait utilisé `{ height, ellipsis: true }` (l'option native de PDFKit) plutôt qu'une mesure manuelle — même symptôme, cohérent avec l'hypothèse que l'ellipsis native de PDFKit fait le même genre de mesure de largeur en interne. + +## Diagnostic — méthode + +Sans accès à `docker exec`/CLI Docker (uniquement l'API Docker via Portainer depuis ce contexte), le diagnostic s'est fait par instrumentation live du conteneur en production : +1. Édition du fichier source dans le conteneur via l'API Docker (`PUT .../containers/{id}/archive?path=...`) pour ajouter des `console.error()` de debug. +2. Restart du conteneur, appel de l'endpoint concerné, lecture des logs (`GET .../containers/{id}/logs`). +3. Log juste avant l'appel à `table()` → confirme les données d'entrée correctes. +4. Log au début de chaque itération de la boucle → confirme que le JS exécute bien toutes les itérations. +5. Extraction du texte réel du PDF final via `pdftotext -layout` (fiable, lit le flux de contenu, contrairement à une capture d'écran qui ne montre que ce qui est visuellement rendu) → ne montre qu'une ligne malgré les logs confirmant 10 itérations JS. +6. Test isolant la cause : neutraliser temporairement la fonction de troncature (retourner le texte tel quel, aucun appel à `widthOfString`) → les 10 lignes réapparaissent. Confirme la cause. +7. **Important** : après ce type de patch de debug live, restaurer immédiatement le fichier propre depuis le checkout git avant de continuer — ne jamais laisser une version de debug/bypass tourner en production entre deux messages. + +## Fix + +Remplacer la mesure exacte par `widthOfString()` par une heuristique caractère/police (pas d'appel à `widthOfString` du tout) : +```js +function truncateToWidth(text, maxWidth, fontSize) { + const avgCharWidth = fontSize * 0.52; // Helvetica, approximation + const maxChars = Math.max(1, Math.floor(maxWidth / avgCharWidth)); + if (text.length <= maxChars) return text; + return text.slice(0, maxChars - 1) + '…'; +} +``` +Moins précis au pixel près qu'une mesure exacte, mais élimine complètement la classe de bug. Vérifié avec `pdftotext -layout` (pas seulement une capture d'écran) sur la vue synthèse (10/10 lignes) et la vue alertes complète (30/30 lignes sur 2 pages) — aucune régression. + +## Règle à retenir + +Sur ce projet (`rla-api`, PDFKit), **éviter `doc.widthOfString()` en boucle serrée** (mesure caractère par caractère, ou tout usage répété sur un même document pour des besoins de troncature). Préférer une heuristique par nombre de caractères. Si une mesure précise est vraiment nécessaire, limiter drastiquement le nombre d'appels (ex. recherche dichotomique plutôt que linéaire — log2(n) au lieu de n appels), et tester avec `pdftotext -layout` sur un jeu de données réaliste (valeurs longues, colonnes étroites, plusieurs tables sur une même page) avant de considérer un fix PDFKit validé — un rendu visuel/capture d'écran ne suffit pas à détecter ce type de perte silencieuse.