diff --git a/frontend/src/lib/report/report-pdf.tsx b/frontend/src/lib/report/report-pdf.tsx index c08ef130..a7d57ea5 100644 --- a/frontend/src/lib/report/report-pdf.tsx +++ b/frontend/src/lib/report/report-pdf.tsx @@ -3,40 +3,63 @@ import { marked } from 'marked'; import { Log } from '@/lib/log'; -// Register Noto Sans (covers Latin + Cyrillic + Greek + many other scripts) -Font.register({ - family: 'NotoSans', - fonts: [ - { fontStyle: 'normal', fontWeight: 'normal', src: '/fonts/NotoSans-Regular.ttf' }, - { fontStyle: 'normal', fontWeight: 'bold', src: '/fonts/NotoSans-Bold.ttf' }, - { fontStyle: 'italic', fontWeight: 'normal', src: '/fonts/NotoSans-Italic.ttf' }, - { fontStyle: 'italic', fontWeight: 'bold', src: '/fonts/NotoSans-BoldItalic.ttf' }, - ], -}); +const CJK_RE = /[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\u3000-\u303F\uFF01-\uFF60\uFFE0-\uFFE6]+/gu; +const HAS_CJK_RE = /[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\u3000-\u303F\uFF01-\uFF60\uFFE0-\uFFE6]/u; -// Register Noto Sans Mono (covers Latin + Cyrillic for code blocks) -Font.register({ - family: 'NotoSansMono', - fonts: [ - { fontStyle: 'normal', fontWeight: 'normal', src: '/fonts/NotoSansMono-Regular.ttf' }, - { fontStyle: 'normal', fontWeight: 'bold', src: '/fonts/NotoSansMono-Bold.ttf' }, - ], -}); +let hasBaseFonts = false; +let hasCJKFonts = false; -// Register Noto Sans SC (Simplified Chinese, covers CJK + Latin) -Font.register({ - family: 'NotoSansSC', - fonts: [ - { fontStyle: 'normal', fontWeight: 'normal', src: '/fonts/NotoSansSC-Regular.otf' }, - { fontStyle: 'normal', fontWeight: 'bold', src: '/fonts/NotoSansSC-Bold.otf' }, - ], -}); +const registerBaseFonts = (): void => { + if (hasBaseFonts) { + return; + } -// Disable word hyphenation (breaks CJK and Cyrillic incorrectly) -Font.registerHyphenationCallback((word) => [word]); + Font.register({ + family: 'NotoSans', + fonts: [ + { fontStyle: 'normal', fontWeight: 'normal', src: '/fonts/NotoSans-Regular.ttf' }, + { fontStyle: 'normal', fontWeight: 'bold', src: '/fonts/NotoSans-Bold.ttf' }, + { fontStyle: 'italic', fontWeight: 'normal', src: '/fonts/NotoSans-Italic.ttf' }, + { fontStyle: 'italic', fontWeight: 'bold', src: '/fonts/NotoSans-BoldItalic.ttf' }, + ], + }); -// Regex that matches any CJK unified ideographs or CJK punctuation/fullwidth chars -const CJK_RE = /[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\u3000-\u303F\uFF01-\uFF60\uFFE0-\uFFE6]+/g; + Font.register({ + family: 'NotoSansMono', + fonts: [ + { fontStyle: 'normal', fontWeight: 'normal', src: '/fonts/NotoSansMono-Regular.ttf' }, + { fontStyle: 'normal', fontWeight: 'bold', src: '/fonts/NotoSansMono-Bold.ttf' }, + ], + }); + + Font.registerHyphenationCallback((word) => [word]); + + hasBaseFonts = true; +}; + +const registerCJKFonts = (): void => { + if (hasCJKFonts) { + return; + } + + Font.register({ + family: 'NotoSansSC', + fonts: [ + { fontStyle: 'normal', fontWeight: 'normal', src: '/fonts/NotoSansSC-Regular.otf' }, + { fontStyle: 'normal', fontWeight: 'bold', src: '/fonts/NotoSansSC-Bold.otf' }, + ], + }); + + hasCJKFonts = true; +}; + +const ensureFonts = (content: string): void => { + registerBaseFonts(); + + if (HAS_CJK_RE.test(content)) { + registerCJKFonts(); + } +}; interface TextSegment { isCJK: boolean; @@ -610,6 +633,8 @@ function PDFReportDocument({ content }: { content: string }) { export const generatePDFFromMarkdownNew = async (content: string, fileName: string): Promise => { try { + ensureFonts(content); + const doc = ; const blob = await pdf(doc).toBlob(); @@ -630,6 +655,8 @@ export const generatePDFFromMarkdownNew = async (content: string, fileName: stri export const generatePDFBlobNew = async (content: string): Promise => { try { + ensureFonts(content); + const doc = ; return await pdf(doc).toBlob();