feat(export): PDF 内嵌函数图像矢量图 #39

Closed
yxx wants to merge 0 commits from feat/export-service into main
Collaborator

PDF 导出器内嵌函数图像矢量图,与 HTML 的静态 SVG 视觉一致。

  • 抽取 render.py 共享几何:新增 PlotGeometry + compute_geometryrender_svg 改为薄序列化层,SVG 输出与重构前逐字节一致(8 组用例回归验证)
  • 新增 app/plot/render_reportlab.py:消费共享几何产出 reportlab 矢量 Drawing(网格/坐标轴 Line、曲线 PolyLine、刻度/标签 String、ylabel Group 旋转),复用 STSong-Light 渲染中文,按页面内容宽 renderScale 缩放
  • pdf.py_block_function_plot 改为内嵌矢量图(解析/渲染失败或超预算回退占位,单图失败不阻断整篇);mermaid 仍占位
  • 抽取 FunctionPlotBudget + format_plot_diagnostic_common.py,HTML/PDF 共用
  • 文档同步:PDF 已内嵌函数图像,DOCX 仍占位(栅格化范围外)

新增 6 个测试(共享几何、reportlab Drawing、PDF 内嵌/回退/超限),717 测试全部通过。

🤖 Generated with Claude Code

PDF 导出器内嵌函数图像矢量图,与 HTML 的静态 SVG 视觉一致。 - 抽取 `render.py` 共享几何:新增 `PlotGeometry` + `compute_geometry`,`render_svg` 改为薄序列化层,SVG 输出与重构前逐字节一致(8 组用例回归验证) - 新增 `app/plot/render_reportlab.py`:消费共享几何产出 reportlab 矢量 `Drawing`(网格/坐标轴 `Line`、曲线 `PolyLine`、刻度/标签 `String`、ylabel `Group` 旋转),复用 STSong-Light 渲染中文,按页面内容宽 `renderScale` 缩放 - `pdf.py` 的 `_block_function_plot` 改为内嵌矢量图(解析/渲染失败或超预算回退占位,单图失败不阻断整篇);mermaid 仍占位 - 抽取 `FunctionPlotBudget` + `format_plot_diagnostic` 到 `_common.py`,HTML/PDF 共用 - 文档同步:PDF 已内嵌函数图像,DOCX 仍占位(栅格化范围外) 新增 6 个测试(共享几何、reportlab Drawing、PDF 内嵌/回退/超限),717 测试全部通过。 🤖 Generated with [Claude Code](https://claude.com/claude-code)
yxx added 16 commits 2026-09-06 21:29:14 +08:00
实现 Export Service 完整生命周期:mistune AST → Document AST → HtmlExporter 渲染完整 HTML5,异步任务注册表 + 取消 + 24h 产物过期。新增 5 个 /api/exports 端点与 15 项测试;pdf/docx 与函数图像静态渲染留待后续 PR。
- P1 链接/图片 URL 协议白名单校验,危险协议降级为纯文本 + warning
- P2 图片 AST 字段映射(src=attrs.url,alt 取 children 文本)
- P2 原始 HTML 块转义保留,正文不丢失 + warning
- P2 过期/淘汰/重启清理导出产物文件
- P2 解析与渲染移入 asyncio.to_thread,运行中取消生效
- P2 function-plot 围栏别名补全
- 回归测试覆盖全部修复

Co-Authored-By: Claude Code <noreply@anthropic.com>
- 新增 app/plot 包:白名单表达式解析(ast 无 eval)+ FunctionPlot 模型 + 静态 SVG 渲染
- HtmlExporter 的 function_plot 节点解析并内嵌 SVG,解析失败回退占位并转诊断
- 新增 test_plot.py(13 个测试)覆盖表达式安全、指令解析、SVG 输出与导出链路集成

Co-Authored-By: Claude Code <noreply@anthropic.com>
- P1 浮点刻度死循环:_ticks 改为有上限的整数索引推进并校验步长推进
- P2 求值异常:白名单函数校验参数数量;负数底非整数指数按断点处理;采样容错复数
- P2 无效纵轴范围:退化/非有限 range 丢弃并自动采样重算;渲染异常回退占位不阻断导出
- 补 6 个回归测试

Co-Authored-By: Claude Code <noreply@anthropic.com>
同步 main(054f704),解决 contracts.py / main.py / README.md / 技术栈说明 的合并冲突。
- contracts.py:保留 pydantic 多行导入并新增 RequestOverride
- main.py:合并 lifespan(导出孤儿清理 + 转写/本地模型生命周期)
- README.md / 技术栈说明:文档取 main 最新版本

Co-Authored-By: Claude Code <noreply@anthropic.com>
- P2 复杂表达式绕过异常回退:解析与渲染共同纳入局部异常回退;
  AST 深度/节点数上限拦截 RecursionError
- P2 极端有限范围生成 nan SVG:校验坐标跨度有限且 >0,回退安全范围;
  _polyline 拒绝非有限像素坐标
- P3 更新接口契约文档:function-plot 静态 SVG 已实现

Co-Authored-By: Claude Code <noreply@anthropic.com>
针对 PR 审阅「函数数量没有限制,可能生成数百 MB 的 SVG」:

- parser: 单块 function-plot 表达式上限 _MAX_EXPRESSIONS=16,超限整块回退
- html: 单篇文档函数图像上限 _MAX_FUNCTION_PLOTS=16,超出回退源码占位
- service: 输入源 MAX_MARKDOWN_CHARS、产物 MAX_EXPORT_BYTES,超限分别
  拒绝创建或标记 failed(EXPORT_OUTPUT_TOO_LARGE)
- 补充 4 条回归测试与文档说明

Co-Authored-By: Claude Code <noreply@anthropic.com>
针对 PR 审阅 P1「组合复杂度仍可长时间占满导出线程」与 P3「EXPORT_OUTPUT_TOO_LARGE 误标 HTTP 413」:

- plot: FunctionPlot 记录整块 AST 节点数(node_count),parser 累计
- html: 单篇文档累计节点预算 _MAX_TOTAL_PLOT_NODES=8000,超限回退占位
- service: 并发渲染信号量 MAX_CONCURRENT_RENDERS=2,超限额任务排队等待
- docs: 错误码区分同步 HTTP 错误与异步任务错误,EXPORT_OUTPUT_TOO_LARGE 由
  error_code 返回而非 HTTP 413
- 补充节点预算与并发限制两条回归测试(全量 627 通过)

Co-Authored-By: Claude Code <noreply@anthropic.com>
# Conflicts:
#	backend/app/routes.py
- 新增 PdfExporter(reportlab)与 DocxExporter(python-docx),实现与
  HtmlExporter 一致的同步 render + 异步 export,v1 文本优先(标题/段落/
  行内强调与链接/列表/引用/表格/代码块/数学文本),function_plot 与 mermaid
  保留源码占位并记 warning。
- service 层加 _EXPORTERS 注册表按格式分发,删除 format!=html 硬限制,
  扩展名/MIME/产物清理泛化到 html/pdf/docx 三种格式。
- 新增 app/plot/renderer.py:StaticRenderRequest + StaticRenderer Protocol +
  FunctionPlotStaticRenderer + MermaidStaticRenderer;HtmlExporter 改经
  FunctionPlotStaticRenderer 消费,去除对 render_svg 的直接依赖。
- 补齐 PDF/DOCX 魔法字节、CJK 字体、占位 warning 与 StaticRenderer 契约测试。
- 更新 Export开发说明.md。

Co-Authored-By: Claude Code <noreply@anthropic.com>
# Conflicts:
#	.gitignore
#	backend/app/main.py
- 引用块直接子节点为块级节点,PDF/DOCX 改为逐个渲染并继承缩进/颜色,
  不再交给行内渲染器导致正文丢失
- PDF 嵌套列表先输出父级正文再输出子列表,修复顺序颠倒
- 等待渲染槽位期间保持 queued 并监听取消,取消即时生效
- DOCX 列表项补处理直接 text 子节点,避免正文被块级渲染器丢弃
- 补充引用块/嵌套列表/排队取消的结构内容回归测试
- 接口契约同步 html/pdf/docx 三格式均已实现,移除 EXPORT_FORMAT_UNSUPPORTED

Co-Authored-By: Claude Code <noreply@anthropic.com>
- pdf.py `_block_list_item` 改为按 AST 顺序逐段输出:正文暂存为行内标记文本,
  遇嵌套列表先 flush 再递归,之后继续后续正文,保持「父段—子列表—后续段」原始顺序
- pdf.py/docx.py 列表项直接行内节点改走 `_render_inline_node`,保留加粗/链接语义,
  不再只渲染 children 而丢掉格式(PDF 链接以 /URI 注解保留,DOCX 写入 w:hyperlink)
- docx.py `_render_inline_node` 增加 bold/italic 默认值,便于列表项直接调用
- 契约文档 StaticRenderer 状态「计划新增」→「已实现」
- 新增回归测试:混合嵌套顺序、PDF/DOCX 列表项行内语义

Co-Authored-By: Claude Code <noreply@anthropic.com>
- 抽取 render.py 共享几何:新增 PlotGeometry + compute_geometry,render_svg
  改为薄序列化层,SVG 输出与重构前逐字节一致(8 组用例回归验证)
- 新增 app/plot/render_reportlab.py:消费共享几何产出 reportlab 矢量 Drawing
  (网格/坐标轴 Line、曲线 PolyLine、刻度/标签 String、ylabel Group 旋转),
  复用 STSong-Light 渲染中文,按页面内容宽 renderScale 缩放
- pdf.py _block_function_plot 改为内嵌矢量图(解析/渲染失败或超预算回退占位,
  单图失败不阻断整篇);mermaid 仍占位
- 抽取 FunctionPlotBudget + format_plot_diagnostic 到 _common.py,html/pdf 共用
- 文档同步:PDF 已内嵌函数图像,DOCX 仍占位(栅格化范围外)

Co-Authored-By: Claude Code <noreply@anthropic.com>
Owner

审阅结论

暂不建议合并,仍有以下问题需要修复:

P2:显式范围外的曲线未裁剪

函数图像采样仅排除了非有限坐标,没有将曲线裁剪到绘图区。

例如设置 range: -1, 1 并绘制 y = 10*x 时,生成的曲线坐标会远超图表和页面范围。ReportLab 不会像 SVG viewport 一样自动裁剪,可能导致曲线覆盖 PDF 中的其他内容。

建议:

  • 将线段裁剪到绘图矩形范围内,或在 ReportLab 渲染层设置裁剪路径。
  • 增加曲线超出显式 range 时的回归测试。

P2:PDF 纵轴标签被旋转到画布之外

纵轴标签先使用绝对坐标定位,随后又以相同坐标作为旋转中心,造成坐标重复变换。实测标签边界落在负数横坐标区域,因此导出的 PDF 中无法正常显示纵轴标签。

建议:

  • 使用组内局部坐标创建文本,再对分组执行旋转和平移。
  • 增加断言,确保纵轴标签的最终边界位于 Drawing 范围内。

P3:PDF 导出器说明已经过时

当前实现已经支持把 Function Plot 作为矢量图嵌入 PDF,但 pdf.py 的模块说明仍称函数图像使用占位内容,请同步更新。

已确认修复

上一轮提出的问题均已解决:

  • PDF 混合嵌套列表顺序正确。
  • PDF/DOCX 列表项保留加粗、链接等行内语义。
  • Function Plot 静态渲染的文档状态已经更新。

验证结果

  • 后端完整测试:717 passed
  • git diff --check:通过
  • 与当前 main:无合并冲突

另外,本 PR 后续加入了 PDF 函数图像矢量渲染,但标题和描述尚未体现,测试数量仍写为 711。建议修复上述问题后同步更新 PR 描述和测试结果。

## 审阅结论 暂不建议合并,仍有以下问题需要修复: ### P2:显式范围外的曲线未裁剪 函数图像采样仅排除了非有限坐标,没有将曲线裁剪到绘图区。 例如设置 `range: -1, 1` 并绘制 `y = 10*x` 时,生成的曲线坐标会远超图表和页面范围。ReportLab 不会像 SVG viewport 一样自动裁剪,可能导致曲线覆盖 PDF 中的其他内容。 建议: - 将线段裁剪到绘图矩形范围内,或在 ReportLab 渲染层设置裁剪路径。 - 增加曲线超出显式 `range` 时的回归测试。 ### P2:PDF 纵轴标签被旋转到画布之外 纵轴标签先使用绝对坐标定位,随后又以相同坐标作为旋转中心,造成坐标重复变换。实测标签边界落在负数横坐标区域,因此导出的 PDF 中无法正常显示纵轴标签。 建议: - 使用组内局部坐标创建文本,再对分组执行旋转和平移。 - 增加断言,确保纵轴标签的最终边界位于 Drawing 范围内。 ### P3:PDF 导出器说明已经过时 当前实现已经支持把 Function Plot 作为矢量图嵌入 PDF,但 `pdf.py` 的模块说明仍称函数图像使用占位内容,请同步更新。 ## 已确认修复 上一轮提出的问题均已解决: - PDF 混合嵌套列表顺序正确。 - PDF/DOCX 列表项保留加粗、链接等行内语义。 - Function Plot 静态渲染的文档状态已经更新。 ## 验证结果 - 后端完整测试:`717 passed` - `git diff --check`:通过 - 与当前 `main`:无合并冲突 另外,本 PR 后续加入了 PDF 函数图像矢量渲染,但标题和描述尚未体现,测试数量仍写为 711。建议修复上述问题后同步更新 PR 描述和测试结果。
Kronecker closed this pull request 2026-09-06 21:33:31 +08:00

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Kronecker/NotesAgentic#39