From 3fd8dfe294111e0a93476326a109e5e855b8fd01 Mon Sep 17 00:00:00 2001 From: weeihan Date: Tue, 28 Jul 2026 17:53:04 +0800 Subject: [PATCH] fix(pdf): sanitise non-WinAnsi chars, handle newlines in wrap --- lib/pdf/incident-report.ts | 40 +++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/lib/pdf/incident-report.ts b/lib/pdf/incident-report.ts index 2ba3c31..aa16433 100644 --- a/lib/pdf/incident-report.ts +++ b/lib/pdf/incident-report.ts @@ -48,6 +48,18 @@ export type IncidentReportData = { type EmbeddedFont = Awaited> type Page = ReturnType +// WinAnsi (Windows-1252) cannot encode chars outside its range. +// Replace common typographic Unicode with ASCII equivalents, strip the rest. +function sanitize(str: string): string { + return str + .replace(/[‘’]/g, "'") + .replace(/[“”]/g, '"') + .replace(/–|—/g, '-') + .replace(/…/g, '...') + .replace(/ /g, ' ') + .replace(/[^\x20-\xFF]/g, '?') +} + const MARGIN = 50 const PAGE_WIDTH = 595 const PAGE_HEIGHT = 842 @@ -81,20 +93,22 @@ function ensureSpace(state: State, needed: number): void { } function wrapText(text: string, maxWidth: number, font: EmbeddedFont, size: number): string[] { - const words = text.split(' ') - const lines: string[] = [] - let current = '' - for (const word of words) { - const test = current ? `${current} ${word}` : word - if (font.widthOfTextAtSize(test, size) > maxWidth && current) { - lines.push(current) - current = word - } else { - current = test + const result: string[] = [] + for (const paragraph of text.split('\n')) { + const words = paragraph.split(' ') + let current = '' + for (const word of words) { + const test = current ? `${current} ${word}` : word + if (font.widthOfTextAtSize(test, size) > maxWidth && current) { + result.push(current) + current = word + } else { + current = test + } } + if (current) result.push(current) } - if (current) lines.push(current) - return lines.length ? lines : [''] + return result.length ? result : [''] } function drawSection(state: State, title: string): void { @@ -117,7 +131,7 @@ function drawSection(state: State, title: string): void { } function drawField(state: State, label: string, value: string | null): void { - const v = value ?? '—' + const v = sanitize(value ?? '—') const maxW = PAGE_WIDTH - MARGIN * 2 - 120 const lines = wrapText(v, maxW, state.font, BODY_FONT_SIZE) ensureSpace(state, (lines.length + 1) * LINE_HEIGHT)