Add an "Export to PDF" Button and Define the Font for Exporting to PDF
Showing
1 changed file
with
39 additions
and
1 deletions
| @@ -10,6 +10,7 @@ import html | @@ -10,6 +10,7 @@ import html | ||
| 10 | import json | 10 | import json |
| 11 | import os | 11 | import os |
| 12 | import re | 12 | import re |
| 13 | +import base64 | ||
| 13 | from pathlib import Path | 14 | from pathlib import Path |
| 14 | from typing import Any, Dict, List | 15 | from typing import Any, Dict, List |
| 15 | from loguru import logger | 16 | from loguru import logger |
| @@ -74,6 +75,7 @@ class HTMLRenderer: | @@ -74,6 +75,7 @@ class HTMLRenderer: | ||
| 74 | self.toc_rendered = False | 75 | self.toc_rendered = False |
| 75 | self.hero_kpi_signature: tuple | None = None | 76 | self.hero_kpi_signature: tuple | None = None |
| 76 | self._lib_cache: Dict[str, str] = {} | 77 | self._lib_cache: Dict[str, str] = {} |
| 78 | + self._pdf_font_base64: str | None = None | ||
| 77 | 79 | ||
| 78 | # 初始化图表验证和修复器 | 80 | # 初始化图表验证和修复器 |
| 79 | self.chart_validator = create_chart_validator() | 81 | self.chart_validator = create_chart_validator() |
| @@ -97,6 +99,11 @@ class HTMLRenderer: | @@ -97,6 +99,11 @@ class HTMLRenderer: | ||
| 97 | """获取第三方库文件的目录路径""" | 99 | """获取第三方库文件的目录路径""" |
| 98 | return Path(__file__).parent / "libs" | 100 | return Path(__file__).parent / "libs" |
| 99 | 101 | ||
| 102 | + @staticmethod | ||
| 103 | + def _get_font_path() -> Path: | ||
| 104 | + """返回PDF导出所需字体的路径""" | ||
| 105 | + return Path(__file__).parent / "assets" / "fonts" / "SourceHanSerifSC-Medium.otf" | ||
| 106 | + | ||
| 100 | def _load_lib(self, filename: str) -> str: | 107 | def _load_lib(self, filename: str) -> str: |
| 101 | """ | 108 | """ |
| 102 | 加载指定的第三方库文件内容 | 109 | 加载指定的第三方库文件内容 |
| @@ -123,6 +130,22 @@ class HTMLRenderer: | @@ -123,6 +130,22 @@ class HTMLRenderer: | ||
| 123 | print(f"警告: 读取库文件 {filename} 时出错: {e}") | 130 | print(f"警告: 读取库文件 {filename} 时出错: {e}") |
| 124 | return "" | 131 | return "" |
| 125 | 132 | ||
| 133 | + def _load_pdf_font_data(self) -> str: | ||
| 134 | + """加载PDF字体的Base64数据,避免重复读取大型文件""" | ||
| 135 | + if self._pdf_font_base64 is not None: | ||
| 136 | + return self._pdf_font_base64 | ||
| 137 | + font_path = self._get_font_path() | ||
| 138 | + try: | ||
| 139 | + data = font_path.read_bytes() | ||
| 140 | + self._pdf_font_base64 = base64.b64encode(data).decode("ascii") | ||
| 141 | + return self._pdf_font_base64 | ||
| 142 | + except FileNotFoundError: | ||
| 143 | + logger.warning("PDF字体文件缺失:%s", font_path) | ||
| 144 | + except Exception as exc: | ||
| 145 | + logger.warning("读取PDF字体文件失败:%s (%s)", font_path, exc) | ||
| 146 | + self._pdf_font_base64 = "" | ||
| 147 | + return self._pdf_font_base64 | ||
| 148 | + | ||
| 126 | # ====== 公共入口 ====== | 149 | # ====== 公共入口 ====== |
| 127 | 150 | ||
| 128 | def render(self, document_ir: Dict[str, Any]) -> str: | 151 | def render(self, document_ir: Dict[str, Any]) -> str: |
| @@ -221,6 +244,8 @@ class HTMLRenderer: | @@ -221,6 +244,8 @@ class HTMLRenderer: | ||
| 221 | str: head片段HTML。 | 244 | str: head片段HTML。 |
| 222 | """ | 245 | """ |
| 223 | css = self._build_css(theme_tokens) | 246 | css = self._build_css(theme_tokens) |
| 247 | + pdf_font_b64 = self._load_pdf_font_data() | ||
| 248 | + pdf_font_literal = json.dumps(pdf_font_b64) | ||
| 224 | 249 | ||
| 225 | # 加载第三方库 | 250 | # 加载第三方库 |
| 226 | chartjs = self._load_lib("chart.js") | 251 | chartjs = self._load_lib("chart.js") |
| @@ -263,6 +288,10 @@ class HTMLRenderer: | @@ -263,6 +288,10 @@ class HTMLRenderer: | ||
| 263 | {css} | 288 | {css} |
| 264 | </style> | 289 | </style> |
| 265 | <script> | 290 | <script> |
| 291 | + // 预载 PDF 字体 Base64 数据,后续由 jspdf addFileToVFS 使用 | ||
| 292 | + window.pdfFontData = {pdf_font_literal}; | ||
| 293 | + </script> | ||
| 294 | + <script> | ||
| 266 | document.documentElement.classList.remove('no-js'); | 295 | document.documentElement.classList.remove('no-js'); |
| 267 | document.documentElement.classList.add('js-ready'); | 296 | document.documentElement.classList.add('js-ready'); |
| 268 | </script> | 297 | </script> |
| @@ -330,7 +359,7 @@ class HTMLRenderer: | @@ -330,7 +359,7 @@ class HTMLRenderer: | ||
| 330 | <div class="header-actions"> | 359 | <div class="header-actions"> |
| 331 | <button id="theme-toggle" class="action-btn" type="button">🌗 主题切换</button> | 360 | <button id="theme-toggle" class="action-btn" type="button">🌗 主题切换</button> |
| 332 | <button id="print-btn" class="action-btn" type="button">🖨️ 打印</button> | 361 | <button id="print-btn" class="action-btn" type="button">🖨️ 打印</button> |
| 333 | - <!-- <button id="export-btn" class="action-btn" type="button">⬇️ 导出PDF</button> --> | 362 | + <button id="export-btn" class="action-btn" type="button">⬇️ 导出PDF</button> |
| 334 | </div> | 363 | </div> |
| 335 | </header> | 364 | </header> |
| 336 | """.strip() | 365 | """.strip() |
| @@ -2793,6 +2822,15 @@ function exportPdf() { | @@ -2793,6 +2822,15 @@ function exportPdf() { | ||
| 2793 | } | 2822 | } |
| 2794 | showExportOverlay('正在导出PDF,请稍候...'); | 2823 | showExportOverlay('正在导出PDF,请稍候...'); |
| 2795 | const pdf = new jspdf.jsPDF('p', 'mm', 'a4'); | 2824 | const pdf = new jspdf.jsPDF('p', 'mm', 'a4'); |
| 2825 | + try { | ||
| 2826 | + if (window.pdfFontData) { | ||
| 2827 | + pdf.addFileToVFS('SourceHanSerifSC-Medium.otf', window.pdfFontData); | ||
| 2828 | + pdf.addFont('SourceHanSerifSC-Medium.otf', 'SourceHanSerif', 'normal'); | ||
| 2829 | + pdf.setFont('SourceHanSerif'); | ||
| 2830 | + } | ||
| 2831 | + } catch (err) { | ||
| 2832 | + console.warn('Custom PDF font setup failed, fallback to default', err); | ||
| 2833 | + } | ||
| 2796 | const pageWidth = pdf.internal.pageSize.getWidth(); | 2834 | const pageWidth = pdf.internal.pageSize.getWidth(); |
| 2797 | const pxWidth = Math.max(target.scrollWidth, document.documentElement.scrollWidth); | 2835 | const pxWidth = Math.max(target.scrollWidth, document.documentElement.scrollWidth); |
| 2798 | const restoreButton = () => { | 2836 | const restoreButton = () => { |
-
Please register or login to post a comment