docs(code): 补齐第二阶段前后端中文注释

This commit is contained in:
2026-09-07 15:00:37 +08:00
parent ca5b52bc8a
commit 6cd8913d31
24 changed files with 66 additions and 47 deletions
+7 -5
View File
@@ -1,4 +1,4 @@
"""Raster-only resources; PDF bypasses export quotas but retains path/format validation."""
"""处理栅格资源;PDF 不受导出配额限制,但仍执行路径和格式校验。"""
import base64
import hashlib
import threading
@@ -9,7 +9,7 @@ from app.errors import ApiError
_math_lock = threading.Lock()
def enrich_document(document, file_path=None, unlimited=False, options=None, preserve_alpha=False):
"""Embed Vault images and MathText, with format-specific quotas and palette."""
"""内嵌 Vault 图片和 MathText,并按导出格式应用配额与主题配色。"""
from app.config import get_settings
from urllib.parse import unquote, urlsplit
vault = get_settings().vault_path.resolve()
@@ -51,7 +51,7 @@ def enrich_document(document, file_path=None, unlimited=False, options=None, pre
if not unlimited and pixels > 16_000_000: raise ValueError('document pixels')
if not unlimited and image.width * image.height > 4_000_000: raise ValueError('image dimensions')
out = BytesIO()
# Composite transparency over the PDF theme or the print/Word white surface.
# 透明像素按 PDF 主题表面色合成;打印 HTML 与 Word 使用白色底色。
rgba=image.convert('RGBA'); background=Image.new('RGBA',rgba.size,palette['surface'] if palette else 'white')
background.alpha_composite(rgba); (rgba if preserve_alpha else background.convert('RGB')).save(out,'PNG')
png=out.getvalue();total += len(png)
@@ -70,6 +70,7 @@ def source_hash(source):
return hashlib.sha256(source.strip().encode()).hexdigest()
def validate_assets(assets, unlimited=False):
"""校验前端静态资源并解码为 PNG;PDF 仅解除容量限制,不放宽格式要求。"""
result = {}
total = pixels = 0
for asset in assets:
@@ -98,6 +99,7 @@ def validate_assets(assets, unlimited=False):
return result
def attach_assets(document, assets):
"""按资源类型和源码哈希把已验证图片挂载到对应文档节点。"""
def visit(node):
source = node.attributes.get('src', '') if node.type == 'image' else node.text
key = (node.type, source_hash(source))
@@ -109,7 +111,7 @@ def attach_assets(document, assets):
visit(child)
def plot_png(plot):
"""DOCX consumes the same clipped geometry as SVG/PDF, rendered at 2x."""
"""按 SVG/PDF 共用的裁剪几何,以二倍分辨率生成 DOCX 图像。"""
from app.plot.render import compute_geometry, _sx, _sy, _fmt_num
from PIL import ImageDraw, ImageFont
geo = compute_geometry(plot)
@@ -135,7 +137,7 @@ def plot_png(plot):
if geo.xlabel:
draw.text((geo.width, (geo.height - 18)*2), geo.xlabel, fill='#1f2328', font=font, anchor='mm')
if geo.ylabel:
# Horizontal at the upper-left margin keeps CJK labels readable in Word.
# 纵轴标题横排在左上边距,避免 CJK 文本在 Word 中旋转后不可读。
draw.text((24, 24), geo.ylabel, fill='#1f2328', font=font)
for index, expression in enumerate(plot.expressions):
draw.text((48+(index%2)*620,geo.height*2+index//2*48),expression.label or 'y = '+expression.expression,fill=geo.colors[index],font=font)
+6 -4
View File
@@ -1,8 +1,7 @@
"""Print the app's self-contained theme snapshot with a real browser engine.
"""使用真实浏览器引擎打印应用生成的自包含主题快照。
A child process isolates Playwright's Windows event loop from Uvicorn and keeps
browser lifecycle scoped to one export. Snapshot scripts/network/file loads are
blocked; fonts and images must already be embedded by the client.
子进程隔离 Playwright Windows 上的事件循环与 Uvicorn,并把浏览器生命周期限制在
单次导出内。快照禁止脚本、网络和文件加载,字体与图片必须由客户端提前内嵌。
"""
from pathlib import Path
import os
@@ -14,6 +13,7 @@ from app.export.document import ExportResult
def browser_executable():
"""优先使用显式配置,再查找系统已安装的 Chromium 系浏览器。"""
configured = os.environ.get('APP_PDF_BROWSER')
if configured:
return configured
@@ -28,6 +28,7 @@ def browser_executable():
def render_snapshot(snapshot: str, page_size: str) -> ExportResult:
"""在隔离子进程中打印快照,避免阻塞或污染服务进程的事件循环。"""
with tempfile.TemporaryDirectory(prefix='notes-pdf-') as directory:
source = Path(directory) / 'snapshot.html'
output = Path(directory) / 'document.pdf'
@@ -42,6 +43,7 @@ def render_snapshot(snapshot: str, page_size: str) -> ExportResult:
def print_snapshot(source: Path, output: Path, page_size: str):
"""在离线、禁用 JavaScript 的上下文中将自包含 HTML 打印为 PDF。"""
from playwright.sync_api import sync_playwright
with sync_playwright() as runtime:
browser = runtime.chromium.launch(executable_path=browser_executable(), headless=True)
+1 -1
View File
@@ -117,7 +117,7 @@ class DocxExporter:
with Image.open(BytesIO(png)) as image:
section = self._doc.sections[-1]
available_width = (section.page_width - section.left_margin - section.right_margin) / 914400
# Leave room for Word's containing paragraph line/spacing.
# 为 Word 外层段落的行高和间距预留空间,避免图片跨出页面。
available_height = (section.page_height - section.top_margin - section.bottom_margin) / 914400 - 0.25
width = min(5.8, available_width,
image.width / (180 if node.type == 'math_block' else 96),
+1 -1
View File
@@ -285,7 +285,7 @@ class PdfExporter:
parts.append(self._render_inline(child.children, warnings))
elif hasattr(self, f"_block_{child.type}"):
flush()
# Keep block content inside the list frame, including tables and callouts.
# 表格、警告框等块级内容也要保持在列表缩进框内。
story.append(Indenter(left=indent))
self._render_block(child, story, warnings)
story.append(Indenter(left=-indent))
+2 -1
View File
@@ -1,4 +1,4 @@
"""Embed an available CJK TrueType font; retain the portable CID fallback."""
"""嵌入可用的 CJK TrueType 字体,找不到时保留可移植的 CID 字体回退。"""
import os
from pathlib import Path
from reportlab.pdfbase import pdfmetrics
@@ -6,6 +6,7 @@ from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
def register_font():
"""按显式配置、系统字体、Linux 字体的顺序注册 PDF 中文字体。"""
candidates = [os.getenv('APP_EXPORT_FONT',''),
str(Path(os.getenv('WINDIR','C:/Windows'))/'Fonts/simsun.ttc'),
'/usr/share/fonts/truetype/arphic/uming.ttc']
+1
View File
@@ -179,6 +179,7 @@ class _AstMapper:
children=self.map_inline(token.get("children", [])),
)
if kind == "inline_html":
# 保留行内 HTML 的来源标记,仅供 PDF 资源扫描识别 img;最终 HTML 仍由前端净化。
return DocumentNode(type="text", node_id=self.next_id(), text=token.get("raw", ""), attributes={"raw_html": True})
if kind == "codespan":
return DocumentNode(type="codespan", node_id=self.next_id(), text=token.get("raw", ""))
+3 -1
View File
@@ -406,7 +406,7 @@ async def wait_for_export(job_id: str) -> ExportJob | None:
async def preview_resources(request: ExportRequest):
"""Prepare Vault images and vector plots for the shared browser renderer."""
"""为浏览器渲染器准备通过 Vault 校验的图片和静态函数图。"""
import base64
from app.export.assets import enrich_document
from app.plot.parser import parse_source
@@ -418,6 +418,8 @@ async def preview_resources(request: ExportRequest):
document = parse_document(markdown)
images, plots = [], []
class HtmlImages(HTMLParser):
# 原始 HTML 只提取 img.src;路径、扩展名和图片格式仍交给 enrich_document 校验。
# 行内代码和代码块在 AST 中不是 HTML 节点,因此不会误当作图片资源。
def handle_starttag(self, tag, attrs):
if tag == 'img':
src = dict(attrs).get('src')