Add an "Export to PDF" Button and Define the Font for Exporting to PDF

This commit is contained in:
马一丁
2025-11-18 01:13:25 +08:00
parent fdd836bf2e
commit dffe1618d5
+39 -1
View File
@@ -10,6 +10,7 @@ import html
import json import json
import os import os
import re import re
import base64
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict, List
from loguru import logger from loguru import logger
@@ -74,6 +75,7 @@ class HTMLRenderer:
self.toc_rendered = False self.toc_rendered = False
self.hero_kpi_signature: tuple | None = None self.hero_kpi_signature: tuple | None = None
self._lib_cache: Dict[str, str] = {} self._lib_cache: Dict[str, str] = {}
self._pdf_font_base64: str | None = None
# 初始化图表验证和修复器 # 初始化图表验证和修复器
self.chart_validator = create_chart_validator() self.chart_validator = create_chart_validator()
@@ -97,6 +99,11 @@ class HTMLRenderer:
"""获取第三方库文件的目录路径""" """获取第三方库文件的目录路径"""
return Path(__file__).parent / "libs" return Path(__file__).parent / "libs"
@staticmethod
def _get_font_path() -> Path:
"""返回PDF导出所需字体的路径"""
return Path(__file__).parent / "assets" / "fonts" / "SourceHanSerifSC-Medium.otf"
def _load_lib(self, filename: str) -> str: def _load_lib(self, filename: str) -> str:
""" """
加载指定的第三方库文件内容 加载指定的第三方库文件内容
@@ -123,6 +130,22 @@ class HTMLRenderer:
print(f"警告: 读取库文件 {filename} 时出错: {e}") print(f"警告: 读取库文件 {filename} 时出错: {e}")
return "" return ""
def _load_pdf_font_data(self) -> str:
"""加载PDF字体的Base64数据,避免重复读取大型文件"""
if self._pdf_font_base64 is not None:
return self._pdf_font_base64
font_path = self._get_font_path()
try:
data = font_path.read_bytes()
self._pdf_font_base64 = base64.b64encode(data).decode("ascii")
return self._pdf_font_base64
except FileNotFoundError:
logger.warning("PDF字体文件缺失:%s", font_path)
except Exception as exc:
logger.warning("读取PDF字体文件失败:%s (%s)", font_path, exc)
self._pdf_font_base64 = ""
return self._pdf_font_base64
# ====== 公共入口 ====== # ====== 公共入口 ======
def render(self, document_ir: Dict[str, Any]) -> str: def render(self, document_ir: Dict[str, Any]) -> str:
@@ -221,6 +244,8 @@ class HTMLRenderer:
str: head片段HTML。 str: head片段HTML。
""" """
css = self._build_css(theme_tokens) css = self._build_css(theme_tokens)
pdf_font_b64 = self._load_pdf_font_data()
pdf_font_literal = json.dumps(pdf_font_b64)
# 加载第三方库 # 加载第三方库
chartjs = self._load_lib("chart.js") chartjs = self._load_lib("chart.js")
@@ -262,6 +287,10 @@ class HTMLRenderer:
<style> <style>
{css} {css}
</style> </style>
<script>
// 预载 PDF 字体 Base64 数据,后续由 jspdf addFileToVFS 使用
window.pdfFontData = {pdf_font_literal};
</script>
<script> <script>
document.documentElement.classList.remove('no-js'); document.documentElement.classList.remove('no-js');
document.documentElement.classList.add('js-ready'); document.documentElement.classList.add('js-ready');
@@ -330,7 +359,7 @@ class HTMLRenderer:
<div class="header-actions"> <div class="header-actions">
<button id="theme-toggle" class="action-btn" type="button">🌗 主题切换</button> <button id="theme-toggle" class="action-btn" type="button">🌗 主题切换</button>
<button id="print-btn" class="action-btn" type="button">🖨️ 打印</button> <button id="print-btn" class="action-btn" type="button">🖨️ 打印</button>
<!-- <button id="export-btn" class="action-btn" type="button">⬇️ 导出PDF</button> --> <button id="export-btn" class="action-btn" type="button">⬇️ 导出PDF</button>
</div> </div>
</header> </header>
""".strip() """.strip()
@@ -2793,6 +2822,15 @@ function exportPdf() {
} }
showExportOverlay('正在导出PDF,请稍候...'); showExportOverlay('正在导出PDF,请稍候...');
const pdf = new jspdf.jsPDF('p', 'mm', 'a4'); const pdf = new jspdf.jsPDF('p', 'mm', 'a4');
try {
if (window.pdfFontData) {
pdf.addFileToVFS('SourceHanSerifSC-Medium.otf', window.pdfFontData);
pdf.addFont('SourceHanSerifSC-Medium.otf', 'SourceHanSerif', 'normal');
pdf.setFont('SourceHanSerif');
}
} catch (err) {
console.warn('Custom PDF font setup failed, fallback to default', err);
}
const pageWidth = pdf.internal.pageSize.getWidth(); const pageWidth = pdf.internal.pageSize.getWidth();
const pxWidth = Math.max(target.scrollWidth, document.documentElement.scrollWidth); const pxWidth = Math.max(target.scrollWidth, document.documentElement.scrollWidth);
const restoreButton = () => { const restoreButton = () => {