From 0b0da09d4bc967ba4bfa71d969751ad71794ace1 Mon Sep 17 00:00:00 2001 From: Developer Date: Thu, 21 May 2026 16:12:38 +0800 Subject: [PATCH] fix: use pdf-lib embedFont with proper pagination for CJK PDF --- .../src/assessment/services/pdf-generator.ts | 86 +++++++------------ 1 file changed, 32 insertions(+), 54 deletions(-) diff --git a/server/src/assessment/services/pdf-generator.ts b/server/src/assessment/services/pdf-generator.ts index 03b98c7..fef869e 100644 --- a/server/src/assessment/services/pdf-generator.ts +++ b/server/src/assessment/services/pdf-generator.ts @@ -4,11 +4,9 @@ import { PDFDocument, rgb, StandardFonts, PageSizes } from 'pdf-lib'; const FONT_SEARCH_PATHS = [ path.join(__dirname, '..', '..', '..', 'assets', 'fonts', 'NotoSansSC-VF.ttf'), - 'C:/Windows/Fonts/msyh.ttc', - 'C:/Windows/Fonts/msyhbd.ttc', - 'C:/Windows/Fonts/simsun.ttc', + 'C:/Windows/Fonts/msyh.ttf', + 'C:/Windows/Fonts/simsun.ttf', '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc', - '/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc', ]; let cachedFontBytes: Buffer | null = null; @@ -61,59 +59,39 @@ export async function generateAssessmentPdf(options: PdfReportOptions): Promise< const sectionSize = 13; const lineHeight = fontSize * 1.6; - function drawPage(title: string, titleSize: number, sections: PdfSection[]): void { - const page = doc.addPage([pageWidth, pageHeight]); - let y = pageHeight - margin; + let page = doc.addPage([pageWidth, pageHeight]); + let y = pageHeight - margin; - page.drawText(title, { x: margin, y, size: titleSize, font, color: rgb(0, 0, 0) }); - y -= titleSize * 1.8; - - if (options.subtitle) { - page.drawText(options.subtitle, { x: margin, y, size: 9, font, color: rgb(0.4, 0.4, 0.4) }); - y -= 16; - } - - for (const section of sections) { - y -= 8; - if (y < margin + sectionSize * 2) { - y = pageHeight - margin; - } - page.drawText(section.title, { x: margin, y, size: sectionSize, font, color: rgb(0.1, 0.1, 0.1) }); - y -= sectionSize * 1.8; - - for (const line of section.lines) { - const chunks = wrapLine(line, font, fontSize, pageWidth - margin * 2); - for (const chunk of chunks) { - if (y < margin + lineHeight) { - y = pageHeight - margin; - } - page.drawText(chunk, { x: margin, y, size: fontSize, font, color: rgb(0.2, 0.2, 0.2) }); - y -= lineHeight; - } - } - } - - page.drawText('--- End of Report ---', { x: margin, y: margin, size: 8, font, color: rgb(0.6, 0.6, 0.6) }); + function newPage() { + page = doc.addPage([pageWidth, pageHeight]); + y = pageHeight - margin; } - drawPage(options.title, titleSize, options.sections); + function drawText(text: string, size: number, color: any, offsetY: number) { + if (y < margin + offsetY) newPage(); + page.drawText(text, { x: margin, y, size, font, color }); + y -= offsetY; + } + + drawText(options.title, titleSize, rgb(0, 0, 0), titleSize * 1.8); + + if (options.subtitle) { + drawText(options.subtitle, 9, rgb(0.4, 0.4, 0.4), 16); + } + + for (const section of options.sections) { + y -= 8; + drawText(section.title, sectionSize, rgb(0.1, 0.1, 0.1), sectionSize * 1.8); + + for (const line of section.lines) { + if (!line) continue; + for (const chunk of line.split('\n')) { + drawText(chunk || ' ', fontSize, rgb(0.2, 0.2, 0.2), lineHeight); + } + } + } + + drawText('--- End of Report ---', 8, rgb(0.6, 0.6, 0.6), 20); return Buffer.from(await doc.save()); } - -function wrapLine(text: string, _font: any, fontSize: number, maxWidth: number): string[] { - if (!text || text.length === 0) return ['']; - const chunks: string[] = []; - const lines = text.split('\n'); - for (const line of lines) { - if (line.length === 0) { - chunks.push(''); - continue; - } - const estimatedCharsPerLine = Math.max(1, Math.floor(maxWidth / (fontSize * 0.6))); - for (let i = 0; i < line.length; i += estimatedCharsPerLine) { - chunks.push(line.substring(i, i + estimatedCharsPerLine)); - } - } - return chunks.length > 0 ? chunks : [text]; -}