fix(pdf): sanitise non-WinAnsi chars, handle newlines in wrap

This commit is contained in:
2026-07-28 17:53:04 +08:00
parent 740c6da3ba
commit 3fd8dfe294
+20 -6
View File
@@ -48,6 +48,18 @@ export type IncidentReportData = {
type EmbeddedFont = Awaited<ReturnType<PDFDocument['embedFont']>> type EmbeddedFont = Awaited<ReturnType<PDFDocument['embedFont']>>
type Page = ReturnType<PDFDocument['addPage']> type Page = ReturnType<PDFDocument['addPage']>
// 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 MARGIN = 50
const PAGE_WIDTH = 595 const PAGE_WIDTH = 595
const PAGE_HEIGHT = 842 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[] { function wrapText(text: string, maxWidth: number, font: EmbeddedFont, size: number): string[] {
const words = text.split(' ') const result: string[] = []
const lines: string[] = [] for (const paragraph of text.split('\n')) {
const words = paragraph.split(' ')
let current = '' let current = ''
for (const word of words) { for (const word of words) {
const test = current ? `${current} ${word}` : word const test = current ? `${current} ${word}` : word
if (font.widthOfTextAtSize(test, size) > maxWidth && current) { if (font.widthOfTextAtSize(test, size) > maxWidth && current) {
lines.push(current) result.push(current)
current = word current = word
} else { } else {
current = test current = test
} }
} }
if (current) lines.push(current) if (current) result.push(current)
return lines.length ? lines : [''] }
return result.length ? result : ['']
} }
function drawSection(state: State, title: string): void { 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 { 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 maxW = PAGE_WIDTH - MARGIN * 2 - 120
const lines = wrapText(v, maxW, state.font, BODY_FONT_SIZE) const lines = wrapText(v, maxW, state.font, BODY_FONT_SIZE)
ensureSpace(state, (lines.length + 1) * LINE_HEIGHT) ensureSpace(state, (lines.length + 1) * LINE_HEIGHT)