import * as fs from 'fs'; import * as path from 'path'; import { PDFDocument, rgb } from 'pdf-lib'; const FONT_SEARCH_PATHS = [ path.join(__dirname, '..', '..', '..', 'assets', 'fonts', 'NotoSansSC-VF.ttf'), 'C:/Windows/Fonts/NotoSansSC-VF.ttf', 'C:/Windows/Fonts/msyh.ttc', 'C:/Windows/Fonts/simsun.ttc', '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc', '/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc', ]; let cachedFontBytes: Buffer | null = null; function findFont(): Buffer { if (cachedFontBytes) return cachedFontBytes; for (const p of FONT_SEARCH_PATHS) { try { if (fs.existsSync(p)) { cachedFontBytes = fs.readFileSync(p); return cachedFontBytes; } } catch { } } return Buffer.alloc(0); } interface PdfReportOptions { title: string; subtitle?: string; sections: PdfSection[]; } interface PdfSection { title: string; lines: string[]; } function textToHex(str: string): string { let hex = ''; for (let i = 0; i < str.length; i++) { hex += str.charCodeAt(i).toString(16).toUpperCase().padStart(4, '0'); } return hex; } export async function generateAssessmentPdf(options: PdfReportOptions): Promise { const doc = await PDFDocument.create(); const fontBytes = findFont(); if (fontBytes.length === 0) { throw new Error('No CJK font found. Install Noto Sans SC or similar.'); } const FONT_DESC_REF = { ref: null as any }; const FONT_REF = { ref: null as any }; function prepareFont(page: any) { const fontProgRef = ctx.nextRef(); ctx.assign(fontProgRef, ctx.flateStream(fontBytes)); const fontDescRef = ctx.nextRef(); ctx.assign(fontDescRef, ctx.obj({ Type: 'FontDescriptor', FontName: 'CJKFont', Flags: 4, FontBBox: [0, -300, 1000, 1000], ItalicAngle: 0, Ascent: 900, Descent: -200, CapHeight: 800, StemV: 80, FontFile2: fontProgRef, })); FONT_DESC_REF.ref = fontDescRef; const cidFontRef = ctx.nextRef(); ctx.assign(cidFontRef, ctx.obj({ Type: 'Font', Subtype: 'CIDFontType2', BaseFont: 'CJKFont', CIDSystemInfo: ctx.obj({ Registry: 'Adobe', Ordering: 'Identity', Supplement: 0, }), FontDescriptor: fontDescRef, W: [0, [500]], })); const fontRef = ctx.nextRef(); ctx.assign(fontRef, ctx.obj({ Type: 'Font', Subtype: 'Type0', BaseFont: 'CJKFont', Encoding: 'Identity-H', DescendantFonts: [cidFontRef], })); FONT_REF.ref = fontRef; const fontKey = page.node.newFontDictionaryKey('F1'); page.node.setFontDictionary(fontKey, fontRef); } const margin = 50; const pageWidth = 595.28; const pageHeight = 841.89; const ctx = doc.context as any; let currentPage = doc.addPage([pageWidth, pageHeight]); let contentOps = ''; let y = pageHeight - 50; prepareFont(currentPage); function addFontToPage(page: any) { const fontKey = page.node.newFontDictionaryKey('F1'); page.node.setFontDictionary(fontKey, FONT_REF.ref); } function ensureSpace(needed: number) { const bottomMargin = 60; if (y - needed < bottomMargin) { const contentObj = ctx.flateStream(contentOps); const contentRef = ctx.nextRef(); ctx.assign(contentRef, contentObj); currentPage.node.addContentStream(contentRef); currentPage = doc.addPage([pageWidth, pageHeight]); addFontToPage(currentPage); contentOps = ''; y = pageHeight - 50; } } function addLine(text: string, size: number, bold: boolean = false) { const lines = text.split('\n'); for (const line of lines) { ensureSpace(size * 1.5); const hex = textToHex(line); contentOps += `BT\n/F1 ${size} Tf\n1 0 0 1 ${margin} ${y} Tm\n<${hex}> Tj\nET\n`; y -= size * 1.5; } } function addSeparator() { ensureSpace(12); let hex = ''; for (let i = 0; i < 55; i++) hex += '002D'; contentOps += `BT\n/F1 8 Tf\n1 0 0 1 ${margin} ${y} Tm\n<${hex}> Tj\nET\n`; y -= 12; } addLine(options.title, 22); y -= 4; if (options.subtitle) { addLine(options.subtitle, 10); y -= 4; } addSeparator(); for (const section of options.sections) { ensureSpace(30); addLine(section.title, 12); for (const line of section.lines) { addLine(line, 10); } y -= 6; } addSeparator(); addLine('--- End of Report ---', 9); const contentObj = ctx.flateStream(contentOps); const contentRef = ctx.nextRef(); ctx.assign(contentRef, contentObj); currentPage.node.addContentStream(contentRef); return Buffer.from(await doc.save()); }