Runtime PPTX 导出升级规划
规划日期:2026-10-13;最近复核:2026-10-14。状态:S0 至 S5 的首批实现及复核修复已落地,各阶段仍按下文退出条件验收。评估基线:caf5d4bc85553aa476829235460a659b3dca0bc2;本次复核覆盖 393573a 至 78e1e81 的六个实施提交。本地 PptxGenJS 版本为 4.0.1。
本次升级保留 DOM 采集、单页中间表示和文档写入的现有分层,先解决取消后仍下载、离屏投影失真和内容丢失,再完善视觉映射、性能和原生编辑能力。原生图表、对象组合和母版分别验收,不作为基础修复的发布前提。性能收益以固定样本的实测结果为准。
1 目标与修改范围
升级后的导出应满足以下目标:
- 取消任务后不触发文件下载,旧任务不能更新或清理新任务的状态。
- 导出使用固定画布中的实际页面状态,动画最终态、响应式样式和资源上下文保持一致。
- 可见文本、换行、表格内容和页面顺序保持完整;无法原生表达的视觉效果有明确降级结果。
- 每个源对象、生成图元、降级原因和阶段耗时可关联,报告能区分正常图片与能力降级。
- 常见文本、形状和表格保持可编辑;原生列表、组合和图表按明确支持范围逐步增加。
主要修改范围为 runtime/src/core/services/PPTXExportService.ts、services/export/、services/pptx/、runtime-shell/export/ 及相关测试。涉及图表数据契约时联动 Runtime Kit viewer,涉及公开能力变更时才联动 manifest 和 Backend 契约。
PPTX 浏览器导出不新增 Backend 浏览器执行,不改变 Renderer 截图队列或部署角色。共享捕获和任务模块的改动必须回归 PDF、浏览器打印与已有截图调用方。新增导出内部能力不进入 @runtime-kit 公开能力目录。
2 两份评估的核实结果
可以采纳的结论
| 事项 | 已核实的现状 | 规划决定 |
|---|---|---|
| 现有架构 | 已有 PptxSlideIR、报告构建器、图元投射器和包写入器 | 保留分层,收紧输入、输出和每任务状态 |
| 离屏投影 | outerHTML 克隆和宿主样式注入会重新执行动画、按宿主视口解释媒体查询 | 优先改为在 iframe 文档中测量、提取和截图 |
| 渐变 | 只识别四个正交方向;根背景、圆角及重复背景存在额外缺陷 | 先统一视觉分派,再增加任意角度 |
| 阴影和行距 | 没有原生 box-shadow 映射,正文未写入行距选项 | 支持有界的单层外阴影和明确行高,完成应用侧对拍 |
| 缩放 | 非单位缩放进入不支持分支并截图 | 先支持正值等比缩放;非等比、镜像和倾斜继续降级 |
| 编辑语义 | 图表输出 SVG 或 PNG;没有原生列表和真正的对象组合 | 分别建立段落、组合、图表语义,避免从截图反推数据 |
| 多页容错 | 未捕获的单页异常会终止整个导出;局部截图已有跳过报告 | 增加受约束的页面容错,保留失败和取消的区别 |
| 诊断 | 当前报告主要是类型计数与文本明细 | 增加严重级别、稳定原因码、对象关联和脱敏 JSON |
PptxGenJS 4.0.1 的本地类型和实现已提供 lineSpacing、lineSpacingMultiple、shadow、段落 bullet、addChart 和 defineSlideMaster。这只确认底层入口存在,不代表 HTML 样式可以直接无损映射,也不代表 Runtime 已支持这些能力。
需要纠正或限定的结论
- 每页赋值
frame.src不等于每页冷启动。 离屏舞台只修改当前地址的 hash,项目路由使用createWebHashHistory()。调用真实舞台实现的受控 Chromium 实验中,三次切页的 iframe 启动标识和performance.timeOrigin相同,文档请求共两次,分别为宿主和首次 iframe 加载。因此不把“每页全量下载 JS/CSS”作为当前瓶颈,也不新增通用__ROUTER__全局入口作为首个优化。 - 2~5 倍提速、全屏重排和内存泄漏尚无基准支持。 宿主样式注入、克隆和重复资源读取有优化价值,但具体成本、释放结果和收益须分别测量。
- 不支持的渐变不一定退成纯色。 无文字的小渐变块会截图;含文字或大面积容器的处理取决于分派和面积规则。已用
45deg无文字样例确认截图路径。实际问题是不同分支会产生截图、效果丢失或无提示近似,需要统一策略。 - 复杂容器的背景、边框和圆角并非一律消失。 转换器记录
skipped后仍继续文本和形状转换。滤镜样例仍输出带底色、边框、圆角的文本形状;丢失的是滤镜等合成效果。不能通过机械补一张背景截图解决所有filter、backdrop-filter和混合模式。 - 渐变角度不能直接用 CSS 角度乘以 60000。 CSS 的零度指向上方;DrawingML 的方向向量采用另一套基准。对已解析的 CSS 数值角度、且写入
scaled="0"的路径,方向候选为((cssAngle - 90) % 360 + 360) % 360,之后才转换为 OOXML 单位。角落关键字还依赖背景盒宽高,必须连同停靠点和渐变范围验证。依据 CSS Images 线性渐变定义和 DrawingML LinearGradientFill。 - 缩放不能直接再次乘到实测宽高。
getBoundingClientRect()已包含变换效果。新实现应从明确的局部坐标和仿射变换计算最终几何,字号、padding、边框和阴影只应用一次缩放。 - 组合不是简单包裹 XML 标签。 需要稳定图元 ID、正确的组坐标、子坐标和绘制顺序;不连续图层不能随意归组。DrawingML 组变换显式包含
off/ext/chOff/chExt,见 TransformGroup。
应新增到升级范围的问题
| 编号 | 问题和触发条件 | 影响 | 首次处理阶段 |
|---|---|---|---|
| D01 | 单页取消后异步转换继续,仍调用文件写入;旧 finally 清理新任务 | 意外下载、任务状态串扰 | S0 |
| D02 | iframe 最终 DOM 投影回宿主 | 动画重新开始、媒体查询变化,可能漏导动画文本 | S1 |
| D03 | 按 DOM 顺序输出;普通负坐标归零;缺少完整裁切和组透明度处理 | 遮挡关系、越界装饰和透明度失真 | S2 |
| D04 | 表格直接读取 textContent;安全 inline 未过滤隐藏内容 | 表格换行和局部样式丢失,隐藏文字进入 PPT | S0 |
| D05 | 根背景只读纯色;渐变固定为 rect;继续遍历时再次输出纯色底 | 背景遗漏、圆角丢失、渐变被覆盖 | S0 |
| D06 | 已识别媒体或背景后绕过其他视觉能力判断;大面积复杂容器被豁免 | 滤镜、遮罩等效果丢失或近似结果缺少说明 | S2 |
| D07 | 图片和 SVG fetch 无明确任务期限与取消信号,缺少任务内缓存 | 慢资源拖住导出、重复读取 | S0 和 S3 |
| D08 | 渐变补丁同步解压和重压整个包;未命中目标可静默返回 | 主线程阻塞、峰值内存和产物诊断不足 | S3 |
| D09 | 同一文本形状计入文本与形状两类;对象名主要用于报告分组 | 对象数不等于真实图元数,不能直接计算质量百分比 | S0 和 S3 |
| D10 | quality 参数未传入实际截图;PNG 降级固定默认 scale | 选项契约与效果不一致 | S3 |
3 目标架构与模块边界
flowchart LR
A[任务上下文和配置快照] --> B[iframe 页面句柄和就绪协议]
B --> C[页面文档内测量与提取]
C --> D[验证单页 IR 和选择降级]
D --> E[投射到 PPTX 文档]
E --> F[打包和 OOXML 后处理]
F --> G[任务复核后下载]
C --> H[对象诊断和阶段指标]
D --> H
F --> H每任务上下文
任务上下文持有 ID、取消信号、总期限、阶段状态和不可变导出快照。快照包含页面顺序、完整路由及查询参数、画布尺寸、主题和字体上下文、资源上下文、备注与文件名。配置变化不应改变正在生成的幻灯片比例或内容。
cancel() 发出取消信号并进入释放过程,互斥占用保持到清理完成;界面在可重试前展示释放状态。更新状态、上报进度、提交文件与清理都核对任务 ID。转换器不复用跨任务的可变 options/reportPage/layout,富文本等缓存按页面生命周期重建。
写入器拆分“生成字节”和“下载文件”。异步生成结束后再复核任务,取消、过期或旧任务结果均不能触发下载;不能中断的底层操作可以自然结束,但不得提交结果。共享 ExportTaskRunner 的变更必须保持其他导出服务的生命周期语义。
页面文档内采集
舞台提供页面句柄,包含目标元素、所属 Document/Window、请求标识、就绪状态和释放能力。布局、颜色、富文本、SVG、Canvas、图片与截图统一通过页面环境读取,避免主窗口 instanceof HTMLElement、全局 document.createRange() 或 window.getComputedStyle() 判错跨窗口节点。
就绪校验绑定本次请求、完整路由和当前目标页面,不能只凭上一页留下的布尔就绪标记。复用 render-ready.v1的字体、资源和错误语义,必要时增量加入切页请求回执;保留有限动画等待和无限动画静态化规则。字体、图片、SVG 源读取和截图均受任务总期限约束。
优先移除 PPTX 的宿主 outerHTML 投影与全局样式注入。局部截图也要在目标文档的环境中执行;不能把读取 iframe DOM 的改造停在布局层,再把截图送回依赖主文档的旧路径。PDF 使用的共享舞台以兼容适配方式迁移,回归资源代理、外部 SVG defs、Canvas、视频封面、透明背景和清理。
中间表示和视觉决策
IR 保持纯数据和可序列化。按图元定义几何、文本段落、填充、边框、图像和资源引用,不把 DOM、ECharts 实例或 PptxGenJS 内部对象存进去。生产路径逐步收紧 Record<string, unknown>,底层类型和字段转换集中在 writer 适配层。
增加唯一 sourceId、primitiveId 与诊断关联;多个能力记录可以指向同一个真实图元。PPT 对象名继续用于选择窗格展示,不能因截断或同名而承担唯一身份。新视觉决策先解析元素整体效果,再选择原生、近似或截图路径,避免媒体、背景、渐变和文本各自重复绘制。
一页先提取并验证 IR,再创建并投射 slide。允许页面降级时,失败 IR 整页丢弃后再生成截图 IR,不能在已写入部分内容的 slide 上叠加全页图片。投射或打包阶段无法原子恢复的错误按文档失败处理。
改动归属
| 职责 | 主要实现位置 | 关键边界 |
|---|---|---|
| 任务和下载提交 | PPTXExportService.ts、export/ExportTaskRunner.ts、writer/PptxPackageWriter.ts | 编排持有任务,writer 生成字节,最终提交单独复核 |
| 页面环境与捕获 | export/OffscreenPageStage.ts、export/ElementRasterizer.ts、PageCaptureService.ts | 统一目标文档,不把 iframe 节点交给未经适配的主窗口判断 |
| 文本和表格 | PPTXDomRichTextParser.ts、PPTXDomConverterText.ts、PPTXDomConverterTable.ts | 共用字符和段落语义,单元格边框及几何继续由表格模块负责 |
| 几何与合成 | PPTXDomConverterLayout.ts、PPTXDomConverterTransform.ts、layout/ComplexCssDetector.ts、渐变和媒体 helper | 视觉决策先于分派,复杂子树降级有统一职责 |
| IR 和 OOXML | ir/PptxSlideIR.types.ts、writer/PptxSlideProjector.ts、writer/OoxmlGradientPatcher.ts | 有类型的纯数据、稳定图元 ID、明确补丁命中结果 |
| 报告与交互 | core/types/pptx-export.ts、ir/PptxReportBuilder.ts、runtime-shell/export/ | 保持结果隔离,按源对象关联诊断和真实图元 |
| 语义数据契约 | Runtime Kit viewer、PPTXRuntimeKitExportDom.ts 和契约文档 | 版本化生产消费对拍,不耦合组件私有实例 |
上表中无前缀的 PPTX helper 位于 runtime/src/core/services/pptx/,编排与捕获位于 runtime/src/core/services/;类型和 shell 路径相对 runtime/src/。新增页面环境、段落、图表或后处理模块按职责拆分,不继续向主转换器堆叠分支。
诊断的增量字段在 S0 固定:sourceId、primitiveIds、severity、code、stage、bounds 和 fallbackScope;严重级别使用 info/warning/error,可读原因与稳定原因码分别保存。保留现有结果类别的兼容读取,新增内容遗漏和整页降级语义时同步类型、报告聚合、UI 与测试。
4 分阶段工作包
阶段编号表示实施顺序,P0/P1/P2 表示优先级。阶段完成以退出条件和产物证据为准,不以代码合并或用例数量作为唯一依据。
| 阶段 | 优先级 | 主要结果 | 前置条件 |
|---|---|---|---|
| S0 任务和内容修复 | P0 | 生命周期隔离、已确认内容缺陷修复、固定回归样本 | 无 |
| S1 页面环境迁移 | P0 | 在真实 iframe 文档提取和截图,移除 PPTX 宿主投影 | S0 任务隔离与测试样本 |
| S2 视觉和排版映射 | P1 | 绘制顺序、裁切、背景、行距、阴影和受约束缩放 | S1 页面环境稳定 |
| S3 性能和诊断 | P1 | 页面容错、资源复用、打包 Worker、可核验报告 | S0 至 S2 的 IR 与降级语义 |
| S4 原生编辑语义 | P2 | 原生列表、对象组合、常见图表 | 稳定段落和图元身份,S3 后处理能力 |
| S5 主题和母版 | P3 | 可复用的 PPT 主题及母版策略 | S4 与目标应用兼容性结果 |
S0 任务和内容修复
建议拆为生命周期、文本、背景三个提交单元,每项附可复现样例。
- 将取消信号和任务 ID 校验贯穿准备、转换、资源读取、打包和下载。给图片、SVG 源与字体等待增加有界期限;取消不记为页面失败或截图降级。
- 隔离转换器状态和页面缓存;导出快照固定尺寸、路由、备注和资源上下文。
- 修复隐藏 inline、
br、连续空行和空白语义;让表格复用字符级解析,并扩展当前只允许字符串的单元格类型,保留局部字体样式。 - 将根背景纳入视觉提取;渐变复用圆角几何,阻止重复纯色填充覆盖背景。保留正圆、圆角方框、胶囊和 padding 的现有约束。
- 给报告加入源对象与真实图元身份,先区分交付成功、能力降级与内容遗漏。
- 将本次临时探针迁入长期测试,并建立性能样本与阶段计时,供 S3 比较。
退出条件:取消期间零下载;取消后重试状态不串扰;表格“甲换行乙”及局部加粗保留;隐藏文字不进入 XML;根渐变、圆角渐变和有子内容的渐变容器正确输出,无重复遮盖。
S1 页面环境迁移
- 建立内部页面环境适配层,使所有 DOM 读取与类型判断使用目标文档;不通过随意注入全局 Vue Router 实例解决跨窗口问题。
- 在复用 iframe 的 hash 导航上增加本次请求与就绪状态关联,覆盖 query 变化、相同路径重复导出、资源失败和超时。
- 完成 PPTX 原生采集与局部截图的页面环境迁移,取消时终止舞台导航或等待并释放资源。
- 保留必要的兼容适配层,联动回归 PDF 和普通页面捕获;确认主窗口路由、样式、主题和滚动状态没有副作用。
退出条件:同一页面在不同宿主视口下生成相同几何;动画最终态不重新开始;三页切换无上一页残留;字体、Canvas、SVG defs、封面图和资源代理均可用;成功、失败、取消后无遗留舞台或投影节点。
S2 视觉和排版映射
本阶段先完成基础背景与文本,再推进需要子树合成的层级和裁切。
- 建立受约束的层叠上下文模型,考虑定位、负层级、Flex/Grid 重排和合成效果。不能只对全部节点全局排序
z-index。不支持的层叠关系降级到最小可独立截图的子树。 - 保留负坐标和原始几何,由页面边界决定可见范围。PPTX 以可编辑性优先,普通
overflow越界和圆角裁切不得触发截图降级;暂未原生映射的容器裁切允许视觉近似。显式遮罩和复杂路径仍可按能力降级,不能通过归零位置模拟裁切。 - 明确祖先透明度与组混合效果。简单不重叠图元可计算有效透明度;重叠子树不能用逐个乘 alpha 等价替代组透明度,应整体合成或使用已验证的组能力。
- 统一纯色、图片、渐变、边框与圆角的绘制职责;处理媒体自身和祖先的效果。移除大面积复杂容器的无报告豁免,按视觉能力和合成范围决定降级,不再仅凭面积跳过效果。
- 增加
deg/rad/turn和角落关键字渐变,按最终背景盒解析方向和停靠点;验证非正方形、透明停靠点、边界停靠点和旋转。无法表达的多层背景保持明确截图降级。 - 映射明确的
line-height与段落间距。固定行高优先验证lineSpacing的物理点值,倍数行高验证lineSpacingMultiple;混合字号和字体映射场景单独校准,不直接假设lineHeight/fontSize无损。 - 支持单层、无 spread 的外阴影,偏移和模糊通过现有 px 到 pt 坐标换算;多层、内阴影、spread 或依赖背景采样的效果继续有界降级。
- 首批支持正值等比缩放及其与旋转的组合,统一换算文字、padding、线宽和阴影。非等比文字缩放、镜像、倾斜和 3D 保持截图路径。
- 对
filter/backdrop-filter/mix-blend-mode按真实合成范围处理。能独立分离背景时保留背景和可编辑文字;涉及文字滤镜或背景采样时优先保证视觉,并明确说明截图范围。
退出条件:固定样本的遮挡顺序、裁切、透明度、渐变、行距、阴影和缩放符合预期;未支持样例均有明确报告;既有原生表格、旋转、正圆、公式多段 SVG 和连接线紧边界回归通过。
S3 性能和诊断
- 资源解析统一缓存 Promise 和解析后的内容类型、尺寸、字节数据;缓存仅在任务内复用,键包含资源身份和必要上下文,取消或完成后释放。资源预取采用有界并发,页面 DOM 采集保持串行。
- 只对静态测量阶段缓存样式与盒模型,变换准备、DOM 修改或截图操作使相关缓存失效,避免缓存旧位置。
- PNG 截图用
rasterScale或明确像素预算表达清晰度;将现有未生效的quality契约迁移或明确弃用,不能继续暗示 PNG 质量参数已生效。 - 把 OOXML 后处理移入 Worker 或其他已验证的异步打包路径,减少压缩后的整包重复压缩和无必要复制;渐变按稳定图元 ID 定位,验证命中数量和 XML,不允许缺失目标静默通过。
- 提供阶段进度、耗时、资源字节、截图次数、产物大小与取消延迟。正常图片归为正常结果,图表栅格化、效果近似、截图失败和内容遗漏分别归类。
- 报告支持按页、严重级别与原因筛选,复制或下载诊断 JSON;资源 URL 去除令牌等凭证。真实图元计数与能力计数分开,原生图片不计入“失败”;暂不把简单对象占比命名为视觉质量分数。
- 增加
fail-fast与rasterize-ready-page页面错误策略。首次上线默认保持fail-fast;完成截图容错验收后由界面提供明确选择,结果标注哪些页面变为整页图片。
整页容错只处理已就绪且仍可捕获页面的可恢复提取错误。取消、权限或资源上下文失效、目标页面未就绪、全页截图失败、不可恢复投射与打包错误不吞掉。失败页不静默删除,不保存空白占位页;成功交付截图文稿时必须显示降级状态和原因。
退出条件:可恢复页面失败后产物页数、顺序和备注正确,失败页仅有一份完整内容;取消不触发容错或下载;重复资源明显减少读取次数;性能预算与改造前后的阶段指标有可复核记录;诊断 JSON 可关联真实产物。
S4 原生编辑语义
按原生列表、对象组合、图表三个独立工作包交付。
- 列表:增加段落 IR,支持简单
ul/ol/li、起始编号和分级缩进,保留段落内 rich text。自定义 marker、图片 marker、复杂布局或无法表达的编号形式采用明确降级;验证在目标应用回车后仍保持列表语义。 - 组合:增加显式组节点与成员图元 ID,按语义容器选择组合,不把所有 HTML 布局容器自动归组。保持绘制顺序与绝对位置,正确写入组和子坐标,覆盖嵌套组、负坐标、旋转、超链接、图片关系和拆组编辑。跨层交错对象不强行组合。
- 图表:先冻结版本化导出数据 Schema,再支持简单柱状图、折线图、饼图和环形图。通过 Runtime Kit 明确输出经过归一化的系列和标签数据,不读取私有 ECharts 实例,不从 SVG 反向恢复数据。载荷有大小和类型边界;复杂 dataset transform、函数 formatter、多坐标轴、组合图、渐变系列和扩展图形继续 SVG/PNG 路径。
新增图表语义载荷遵循现有 DOM 契约,不改写媒体 v1 的既有识别含义。需要不兼容演进时增加版本并保留历史类名兜底;组件公开 API 发生不兼容变化时新增 .v2 并联动 manifest 和三端校验。
退出条件:列表可继续编号,卡片可整组移动和拆组,图表数据可在 PowerPoint/WPS 中修改并保存;重新打开后无修复提示,不支持的图表继续保持视觉及准确报告。
S5 主题和母版
先定义 Runtime 主题色、字体与 PPT 主题槽位的映射,明确哪些颜色保持页面显式值、哪些引用主题槽位。母版仅用于可确认相同的公共背景、页码和装饰,不把页面内容或动态资源错误提升为共享对象。
验收包括主题切换、工作空间字体缺失、不同页面背景、母版编辑和再次保存。字体嵌入、任意 HTML 无损转换、原生视频及完整三维编辑不纳入本轮承诺;现有字体别名映射与视觉降级继续可用。
5 回归样本和验收分层
固定样本矩阵
| 类别 | 必须覆盖的样例 | 主要验收层 |
|---|---|---|
| 生命周期 | 准备中、fetch 中、最后一页转换中、打包中取消;取消后重试;迟到结果 | 服务单测和真实下载 E2E |
| 页面状态 | 多页切换、同路径 query 变化、不同宿主视口、有限动画最终态、无限动画 | Chromium 舞台测试 |
| 文本 | 隐藏 inline、br、连续空行、pre 空白、混合字号、中文和拉丁文本、超链接、上下标 | IR、OOXML 和应用排版 |
| 表格 | HTML/DataTable、换行、富文本、合并单元格、四边框与无边框 | 真实 OOXML 和应用编辑 |
| 视觉 | 根背景、圆角渐变、背景图片、负坐标、层叠上下文、overflow、透明度、滤镜 | Chromium 几何和应用渲染 |
| 媒体 | 图片 cover/contain、SVG defs/foreignObject、公式多段、Connector、Canvas、封面图 | 资源关系与真实产物 |
| 容错 | 中间页提取失败、截图失败、资源超时、投射或打包失败 | 错误策略和产物完整性 |
| 编辑能力 | 原生列表、嵌套组合、常见图表及不支持图表 | OOXML 与 PowerPoint/WPS 操作 |
| 性能 | 1/10/30 页,纯文本、重复资源、图片密集、截图密集、渐变密集 | 固定环境的阶段基准 |
证据口径
- Vitest 验证解析、生命周期和 IR;jsdom 几何回退只属于测试环境能力,不能替代浏览器布局证据。
- Chromium 验证真实文档、视口、动画、资源状态、取消后零下载和 DOM 释放。基础样例可用受控同源页面,发布验收必须包含真实 Runtime 项目。
- OOXML 解包验证页数、文本、段落、合并关系、图元位置、关系文件和备注;增强现有只检查 ZIP、首张条目与体积的 page-export smoke。
- 目标应用 在固定版本的 PowerPoint 和 WPS 中打开、渲染、编辑、保存并重开,确认无修复提示。替代渲染器结果单独标注,不能据此声明 Office/WPS 已验收。
- 性能 固定页面源码、锁文件、浏览器版本、主机和视口,冷缓存和热缓存分开,至少重复十次记录中位数、最慢值、阶段耗时、长任务、可测峰值内存和文件大小。S0 建立基线,S3 实施前冻结具体预算;不预先承诺固定倍数提升。
视觉验收使用统一渲染尺寸、固定字体条件和区域对比,先校准数值容差再冻结阈值。可见文本遗漏、页数错误、层级反转、意外下载和 XML 损坏属于硬失败;抗锯齿和字体差异按固定规则评估,不能靠放宽整页像素阈值掩盖内容错误。
基准和 E2E 证据放入 test-results/e2e/。固定样本代码纳入测试,下载的 PPTX、渲染图片、性能临时文件和可再生成大文件不提交。服务已启动时先复用现有测试环境,重置和播种只使用专用测试数据。
验证入口
现有定向回归:
pnpm --filter web-runtime-vue test src/core/services/pptx src/core/services/PPTXExportService.test.ts src/core/services/export/OffscreenPageStage.test.ts src/runtime-shell/export/useExportRunner.test.ts实施阶段完整门禁与已有真实下载 smoke:
pnpm run test:runtime:gate
pnpm run test:repository
# 已准备专用 E2E 数据和服务时执行;此命令不负责准备环境
pnpm exec playwright test --config tests/config/playwright.config.ts --project=smoke page-export.spec.ts新增舞台、视觉和性能用例放入合适的 runtime-heavy 测试范围,命令随用例落地再登记到测试文档。涉及 Runtime Kit 公开 API、manifest 或 previewSchema 时补 Backend 契约;涉及远程截图链路时增加 render-e2e。仅文档变更无需启动服务或运行完整渲染链路。
6 发布和回退条件
每个阶段形成独立、可回退的改动,保留既有页面与媒体识别兼容性。跨窗口环境迁移完成真实项目、PDF 和截图回归后再移除旧适配;回退不能重新引入取消后下载或已确认的内容遗漏。
S0 可单独发布基础修复;S1 至 S3 分别附浏览器、产物和性能证据。S4 的每项能力独立开通,只有完成目标应用编辑和保存验收才能宣称原生可编辑。S5 不阻塞此前阶段。
阶段交付记录包含完整 commit、依赖版本、测试命令、样本、目标应用版本、基准结果、降级范围和未退出事项。导出报告 UI 更新遵循既有组件边界;涉及 Editor 入口时遵循根 DESIGN.md。
实施时同步页面添加指南、主题使用指南和导出 DOM 契约。新支持范围完成验收后再更新用户文档;产品能力或顶层导航发生变化时同步根 README,开发协作边界变化时同步根 AGENTS。
7 当前证据与进度
2026-10-13 的现状基线如下,均不代表升级能力已经实现:
- 现有定向测试:12 个文件、159 条用例通过,覆盖转换、IR、备注、舞台调用和 UI 结果隔离。
- 11 条临时边界探针确认取消后写入、旧任务清理新任务、根渐变遗漏、DOM 顺序、负坐标归零、祖先透明度遗漏、渐变圆角和背景冲突、滤镜分派、表格文本及隐藏 inline 问题。
- 2 条追加探针确认不支持角度的小渐变块走截图,以及带文字滤镜容器仍保留可识别的底色、边框和圆角。
- 使用真实
OffscreenPageStage实现的受控 Chromium 页面确认:源动画透明度1投影后为0;源媒体查询宽度800px投影后为400px,颜色由蓝变红;三次 hash 切页复用一个 iframe 文档。这些是舞台边界证据,完整项目行为由 S1 发布门禁覆盖。 - 临时探针已移除,S0 必须补入长期回归;完整 Runtime 门禁、真实项目导出 E2E 和 PowerPoint/WPS 视觉验收尚未作为本次评估证据执行。
规划工作进度:
- [x] 核实两份评估并纠正冷启动、降级和角度换算结论。
- [x] 确定分阶段范围、模块边界、依赖与退出条件。
实施进度与阶段验收分开记录:
| 阶段 | 已落地实现与复核修复 | 尚未退出事项 |
|---|---|---|
| S0 | 任务隔离、取消信号与资源期限、取消后延迟释放任务锁、内容与背景修复 | 真实下载链路各取消时点的零下载和释放验收 |
| S1 | iframe 文档内提取与截图、跨窗口节点守卫、按 route 和 capture ticket 识别新页面 | 真实 Runtime 项目、共享 PDF 与截图链路 E2E |
| S2 | 简单渐变、阴影、行距、等比缩放、同父级排序与复杂子树截图 | 完整跨层叠上下文模型、复杂组合视觉和目标应用排版对拍 |
| S3 | 任务内资源缓存、配置快照、原子页面容错、诊断数据和单次解压压缩 | Worker 打包、固定样本性能基准、报告筛选和诊断 JSON 的用户界面 |
| S4 | 简单列表、连续图元组合、受限图表数据载荷与原生图表 | PowerPoint/WPS 中继续编号、移动拆组、数据修改、保存重开验收 |
| S5 | 真实 theme XML 色板与字体、同色不透明背景提升 | 主题切换、缺失字体、母版编辑与目标应用保存重开验收 |
8 实现复核与修复记录
2026-10-14 复核发现,原先 S0 至 S5 定向单测通过不能证明升级完成:实现存在类型检查失败、真实 iframe 行为遗漏和实际 OOXML 写入缺陷。下表的问题均已修改,并补充或纠正长期回归。
| 问题 | 触发与影响 | 修复边界 |
|---|---|---|
| 跨窗口节点与样式 | iframe 节点无法通过宿主 instanceof,嵌套文本、表格、图片和 SVG 被跳过;CSS 变量可能读错文档 | 统一按节点类型与命名空间识别,测量、字体、CSS 变量和 Range 使用节点所属文档 |
| 多页就绪 | 第二页清除仅启动时设置的 ready 标记,导致永久等待;同路径 query 更新无法识别 | 保留启动标记,使用路径与逐次 capture ticket 复核页面,准备支持取消和总期限 |
| 取消与重试 | 旧转换未结束就释放锁,且 fetch body、截图或舞台准备无法及时中止 | PPTX 锁保留到 finally;资源请求及 body 同时受期限与信号约束;迟到结果丢弃 |
| 失败页原子性 | 先创建 slide 再提取,异常后再加截图页,产物多出空白或半成品页 | 完整 IR 或整页截图成功后才提交 slide;仅提取错误可整页降级,投射与打包错误向上抛出 |
| 导出快照 | 多页过程中画布、标题或当前路由变化,导致尺寸和备注串页 | 每任务保存画布配置,每页异步操作前保存路由、标题与备注 |
| 组合和渐变补丁 | 正则跨图元匹配、对象名或 ID 重复,可能误包非成员、丢图元或破坏 XML | 扫描 spTree 直接图元边界;仅组合唯一且连续成员;组 ID 从实际最大值递增;渐变未唯一命中即失败 |
| 列表与文本 | 脱离文档的 li 克隆丢 CSS;breakLine 写在首 run 使富文本拆段;无上下文缓存丢缩放与透明度 | 从已挂载 li 解析并排除嵌套列表,在段末换行,按实际上下文解析 rich text 与预格式空白 |
| 图表数据 | 未知类型、不等长系列、非法数值或复杂 ECharts 选项进入原生路径 | 冻结并校验 v1 载荷;拒绝复杂配置,继续 SVG/PNG;环形图空洞按内外半径比例计算 |
| 子树合成 | 已识别渐变或媒体会绕过滤镜判断,叠加未滤镜的文字造成视觉错误 | 合成判断优先;无法拆分的滤镜、显式遮罩和重叠组透明度整子树截图;普通 overflow 越界保留原生对象 |
| 动画透明帧 | 无限循环动画处于 opacity 0 时,先判隐藏再恢复静态样式,遗漏文字 | 先冻结无限动画和过渡,再判断可见性;结束后恢复原样式 |
| 主题与母版 | 色板与母版策略只在 helper 中存在,没有进入实际包;字体变量名不匹配;移除页背景可能露出布局底色 | 写入实际主题 XML;提升时同步移除默认布局的 bg1 引用;仅至少两页、单一母版、布局无独立背景、一致不透明纯色背景提升;字体优先读现有主题变量 |
| 验证不足 | 部分测试只断言实例存在或 mock 调用,未检查实际内容,且测试代码本身有类型错误 | 修复类型错误,增加 iframe、XML 图元、列表段落、页数与备注、主题关系文件的实包断言 |
复核验证记录:
pnpm run test:runtime:gate通过:类型检查、110 个测试文件(765 通过、3 跳过)和生产构建通过,包含无限动画、默认布局继承与自定义布局保护回归。pnpm run test:contracts通过:13 个测试文件、59 条用例。pnpm run test:repository通过:4 个测试文件、16 条用例;git diff --check通过。- 使用真实
OffscreenPageStage、PPTXDomConverter、PageCaptureService的受控 Chromium 页面,连续准备/one、/two、/two?version=2。三次 iframe 启动标识一致,动画最终透明度均为1,IR 包含 text、table、shape;宿主视口宽 800px、iframe 宽 1920px 时,实际截图像素仍为蓝色[0, 0, 255, 255],没有改用宿主媒体查询的红色。临时证据位于.tmp/pptx-review-browser/evidence.json,不入库。 - 长期新增回归为
ExportReviewRegression.test.ts、ExportCancellation.test.ts,并补充 S5 的真实 ZIP 内容检查。受控页面实验不等同于 Backend、Editor、Runtime、Renderer 的完整项目 E2E。
上述复核未执行 PowerPoint/WPS 打开、编辑、保存重开验收、真实项目下载 E2E、Renderer E2E 或 1/10/30 页性能基准;真实项目下载的追加证据见下节。后处理仍使用主线程同步压缩,不宣称已完成 Worker 迁移或具体倍数提速。当前仅验证同父级绘制顺序,完整跨层叠上下文能力仍在 S2 范围内。页码、公共装饰自动提升和报告筛选界面也不作为已交付能力。
9 截图降级判定追加复核
2026-10-14 在一个 18 页真实项目中复现:17 页的整个正文区退成局部截图,只留下标题栏等少量原生对象。触发点是插在静态 overflow:hidden 正文容器中的绝对定位页码。页码实际相对外层页面定位,落在正文边界外;旧代码把所有越界后代都视为被正文裁切,导致父容器整体截图。
当前导出策略以可编辑性优先:
DomCompositionPolicy不再因普通overflow:hidden/clip/auto/scroll的后代越界而截图,定位包含块和越界像素数都不影响此决策。正文、页码、卡片和表格保留原生对象与实测坐标。ComplexCssDetector不再将圆角加 overflow 视为复杂 CSS;圆角卡片保持原生形状,子元素即使触及圆角边缘也继续输出为可编辑对象。- 尚未映射为 PPT 原生裁切的容器边缘允许视觉近似,不提高像素阈值,也不保留另一套 overflow 截图兜底。滤镜、显式遮罩、复杂变换等已有能力降级边界继续生效。
验证证据:
OverflowEditability.test.ts检查定位页码、四种 overflow、真实越界文本形状、圆角和表格的实包结果。读取a:t、p:sp与a:tbl,同时断言没有截图、跳过或 DOM 修改,不能以图片 altText 中包含文字作为可编辑证据。pnpm run test:runtime:gate通过:类型检查、111 个测试文件(773 通过、3 跳过)和生产构建;新增的 8 条可编辑性回归包含在其中。- 真实项目的 MSV 页存在约 9.45px 的内容越界,继续输出为原生对象;项目源码保持原样。逐页复核与完整项目下载证据位于
.tmp/pptx-rasterization/,不入库。 - 通过项目预览的导出按钮下载完整 18 页 PPTX,报告为 225 个可编辑文本、137 个可编辑形状、4 个可编辑表格、0 个截图降级、0 个跳过对象。MSV 页的原生
a:t数由 3 个增加到 11 个,浏览器没有未捕获异常。
这次真实项目下载验证不代替 PowerPoint/WPS 打开、编辑和保存重开验收,也不扩展到 Renderer E2E 或页面性能基准。
10 Flex 实测坐标追加复核
2026-10-14 在 MSV 页复现小标题下移:浏览器中“原图 2(b) · MULTI-SAMPLE VERIFICATION”的文本框顶部为 153px,实际 PPTX 的文本框顶部却对应约 210px。JsdomFlexFallback 把首项与父容器顶部相同视为缺少真实排版,再按 justify-content:center 估算位置;该估算没有计入已由浏览器处理的 gap 与 margin,产生约 57px 的额外下移。
修复后,仅当 getBoundingClientRect() 没有任何实测尺寸时才使用无布局 Flex 回退。真实浏览器的坐标保持原样,不对已完成布局再次推算。新增 FlexMeasurement.test.ts,覆盖 MSV 标题的 IR/实包坐标、横向 Flex 首项坐标及无布局回退。
真实导出按钮生成的修复前后包对拍:MSV 标题 a:off/@y 从 1333470 EMU 修正为 971550 EMU,对应顶部从约 210px 恢复到 153px;18 页导出的可编辑对象数量保持不变,截图降级与跳过对象均为 0。证据位于 .tmp/pptx-rasterization/msv-position-package.json 和相关临时产物,不入库。该坐标对拍不代替 PowerPoint/WPS 字形、排版与保存重开验收。
本次 pnpm run test:runtime:gate 通过:类型检查、112 个测试文件(776 通过、3 跳过)和生产构建;pnpm run test:repository 的 16 条用例通过。
11 字宽误差与文本扩宽边界
2026-10-14 复核当前项目字体替换后的引号字宽:同一浏览器中,38px 字号的弯引号在 Microsoft YaHei UI 下约 18.74px,在导出目标 Microsoft YaHei 下为 38px;一对引号增加约 38.5px。修复只提高文本对字宽差异的容忍,不要求精确字体映射、作者声明换行意图或固定浏览器行布局。
TextPunctuationGuard按字号和引号、括号数量计算余量,既有显式换行只用于逐行取最大值,不生成新换行。弯引号按每个 0.5em 预留,其他引号和括号使用较小余量,总量上限为 2.5em,且不超过普通文本框宽度的 15% 或文本形状宽度的 10%。这些是有界工程参数,不承诺所有字体和应用完全相同。TextExpansionBounds以容器内容区(扣除 padding 与 border)、同高度相邻对象及页面边界限制扩宽。透明、内容驱动的 inline/横向 Flex 包裹可利用外层空白;网格单元、显式尺寸与可见卡片维持边界。只约束新增宽度,不缩窄已有越界对象。- 居中优先保持原锚点,贴边时允许有界的非对称扩宽;右对齐保持右端位置,内容驱动且恰好贴右边的左对齐文本可向左借位。已有越界保留原框,预算不足时减少余量。直属文本节点用 Range 识别相邻文字;无布局环境的零坐标不作为图元重叠证据。字体、字号、文本内容、文本框高度、自然换行模式及既有显式换行不因该策略改变;扩宽本身可能改变自动折行的位置。
验证记录:
- 新增
TextWidthBudget.test.ts的 9 条回归,覆盖标点保护、原生实包、字体/字号保持、紧列、padding/border、跨透明包裹相邻对象、对齐锚点、余量上限和原有越界。 - 真实 Chromium 临界样例中,532px 文本框原来的保护后宽度为 562.4px,目标字体仍排成两行;新保护后宽度约 573.05px,恢复为一行。紧列和相邻图元样例分别限制在约 540px 与 545px,保持字号并继续自然换行。证据位于
.tmp/pptx-rasterization/width-budget-browser.json,不入库。 - 真实项目导出按钮生成的 18 页 PPTX 保持 225 个可编辑文本、137 个形状、4 个表格、0 个截图降级、0 个跳过对象。与上一版逐一比较 225 个原生文本对象,文本、字符样式和高度保持原样。
pnpm run test:runtime:gate通过:类型检查、113 个测试文件(785 通过、3 跳过)和生产构建。
11.1 多行段落保留原宽度
多行文本扩宽即使不碰撞其他对象,也可能减少行数、改变段落形状和留白。现将两条扩宽路径统一限制为单行:多行文本不增加标点、通用或复杂 token 宽度余量,也不扩展到祖先容器剩余空间;同时保留对齐位置、字号、文本框高度及原生可编辑性。
TextLineLayout 在节点所属文档中读取文字节点的 Range 行片段,以垂直重叠识别同一行,兼容行内富文本与直属文字。优先使用实际行布局,避免固定高度、padding 或高文本框误导判断;已有显式换行直接保留,无有效 Range 的环境才按盒高和行高回退,pre-wrap / pre-line 不再直接认定为单行。该策略不生成浏览器换行符,也不保证字体替换后行数完全一致。
新增 TextMultilineLayout.test.ts 的 9 条回归覆盖固定高度中实际两行、三种对齐、富文本跨节点自动换行、不同字号的实际单行、高文本框、字形矩形重叠的紧行距、多行圆角文本形状、横向 Flex 直属片段和无布局回退;普通单行的标点宽度保护继续保留。
验证记录:
- 隔离 Chromium/iframe 的 10 类样例均通过:多行宽度及位置与实测框保持一致,单行仍获得有界补偿,字号保持 19pt,截图降级为 0;同时生成并检查原生 PPTX 实包。证据位于
.tmp/pptx-rasterization/multiline-browser.json和multiline-browser.pptx,不入库。 pnpm run test:runtime:gate通过:类型检查、114 个测试文件(794 通过、3 跳过)和生产构建;pnpm run test:repository的 16 条用例通过。- 本次未复导业务项目或执行 PowerPoint/WPS 实机视觉验收;浏览器几何与 OOXML 验证不代替 Office 字体替换后的排版验收。
该保护减少有空白时的意外换行;空间不足时仍允许正常换行,不承诺跨设备字体度量完全一致。浏览器对照实验和实包检查不代替 PowerPoint/WPS 的视觉、编辑及保存重开验收。
11.2 扩宽安全边界追加复核
2026-10-14 review 后修正以下问题:
- 障碍测量以实际可见绘制内容为依据,
pointer-events:none不作为不可见判断。透明包裹继续扫描可见后代和直属文字;SVG、图片及其他固有绘制对象保留实测矩形。无 Range 环境只允许采用有真实尺寸的叶子文字元素,不使用透明祖先的大矩形回退。 - 祖先剩余空间扩展和字宽补偿都先检查原框是否已越过当前边界;已有越界保留原位置、宽度和字号,恰好贴边的内容驱动文本仍可有界借位。
- 胶囊内缩与总预算收口到
CapsuleWidthBudget。CSS 圆角只换算一次,随后使用 PPT 英寸几何计算;所有内容复杂度、标点及字距补偿合并后统一接受胶囊总上限,最多扩宽自身宽度的 12%。 - 修复数学表达式测试的文本类型收窄,避免
string/ 富文本数组及unknown的includes调用阻断类型检查。旧反色文字用例的测量坐标补计父容器 1px 边框,保持原来的扩宽与位置断言。
TextWidthSafety.test.ts 新增 13 条回归,覆盖事件穿透 SVG、透明包裹的图元与文字、空整页包裹、三种对齐的已有越界、四种小胶囊内容,以及 0.5 / 1 / 2 倍预览的圆角、字号与扩宽一致性,并验证原生 PPTX 文本和自然换行属性。
真实 Chromium 的修复前后对拍:可见 SVG 与卡片的约 35px 重叠消除;越界标签 x 从错误的约 188.5px 恢复到 200px;60px 小胶囊从约 77.3px 收紧到 67.2px;200px 胶囊在 1 倍和 0.5 倍预览下的导出宽度均约 222px。证据位于 .tmp/pptx-rasterization/review-width-browser-fixed.json,不入库。
追加验收:
pnpm run test:runtime:gate通过:类型检查、115 个测试文件(814 通过、3 跳过)及生产构建;pnpm run test:repository的 16 条检查通过。- 业务项目
PRJ20260814001(项目 76)的页面 391 经现有预览服务实际导出单页可编辑 PPTX。两个顶部胶囊、两个底部页脚在浏览器均为单行,按导出字号及目标字体测量的文字宽度均小于导出可用文字区;其中第一胶囊约为 204.2px / 206.1px。产物包含 15 个可编辑文本、10 个可编辑形状,截图与跳过数均为 0,四段文字均保留原生<a:t>。 - 浏览器测量与 PPTX 包结构已验证;本次未执行 PowerPoint/WPS 实机视觉验收。证据及单页 PPTX 位于
.tmp/pptx-rasterization/page391-fixed.json和.tmp/pptx-rasterization/PRJ20260814001-page391-fixed.pptx,不入库。
11.3 符号、数字与空格的累计字体差异
2026-10-14 页面 395 的 Flex 列表项暴露另一类缺口:浏览器整句文字宽约 642.3px,统一成导出字体并按导出字号折算后约为 684.2px,原预算仅扩宽约 24.3px。两个弯双引号贡献约 28.2px,破折号、数字、空格和字号取整共同贡献其余差异;只补破折号不能覆盖整句误差。
TextPunctuationGuard重组为TextTypographyWidthGuard,把同一行的符号、数字与空格预算相加后统一封顶。弯双引号从每个 0.5em 调整为 0.7em,弯单引号为 0.85em;补充破折号、连接号、箭头、数学比号与其他数学符号、半角标点、项目符号、百分比、单位、货币及常用特殊符号。数字每个预留 0.06em,普通空格、NBSP 和窄 NBSP 每个预留 0.03em。规则保留 2.5em、普通文本框 15% / 文本形状 10% 上限,不对宽度稳定的全角标点扩大预算。- 中文加数字纳入既有混排判断,无英文字母也能进入单行 Flex/list 剩余空间扩展;祖先已扩展的空间抵扣后续补偿,避免重复扩宽后向左侧圆点借位。多行、显式宽度、原有越界、相邻图元与胶囊总预算边界继续沿用原约束。
- 新增纯预算与 Flex 直属文本回归,覆盖页面 395、单/双引号低估、各类遗漏符号、数字和空格累计、逐行最大预算、上限、紧列、同高度相邻对象及已有多行。字距与数学表达式的既有回归同步校验累计预算上限。
验证记录:
- 页面 395 的真实单页预览在相同浏览器和字体下对拍:目标字体从两行恢复为一行,导出框从约 666.6px 扩展到所在行可用的约 957.0px,覆盖目标文字宽度约 684.2px;文字起点约 849.0px、字号 11.3pt 保持原样。原生 PPTX XML 保留完整
<a:t>与自动折行,没有插入换行或自动缩字。证据位于.tmp/page395-analysis/baseline-evidence.json、evidence.json和target.pptx,不入库。 - Runtime 类型检查、生产构建与全部单测通过:117 个测试文件、839 条通过、3 条跳过;PPTX 相关 272 条通过。
test:repository的 16 条文档和仓库约束检查通过。 - 本次未执行 PowerPoint/WPS 实机视觉验收;浏览器目标字体复测及 PPTX 实包检查不代替目标应用验收。
11.4 内容宽度 Flex 包裹的最小宽度语义
页面 396 的“由对称性,每一对的和都相等,共 n 对”在浏览器占约 385.5px,按导出字体和字号测量需要约 389.7px。现有规则已算出 18px 余量,但父级透明 min-w-0 包裹被误判为固定宽度,导致余量无法应用;该标题下面还有公式段落,祖先剩余空间路径也不会跨过后续有效节点。
- 新增
TextWidthConstraints,统一字宽保护与祖先边界对作者尺寸的判断:最小宽度只提供下限,实际width、max-width与flex-basis继续限制扩宽,支持 Tailwind 响应式和状态前缀。当前内容宽度不再作为透明 Flex 包裹的硬上界。 - 有背景、边框、padding 或横向裁切/滚动的包裹,以及网格单元、已经分配宽度的 Flex 子项,继续保留自身边界。祖先剩余空间的节点顺序约束保持原样,页面 396 仅用既有有界补偿恢复单行,不把标题文本框拉满整张卡片。
TextContentSizedBounds.test.ts新增 18 条回归,覆盖类名和内联最小宽度、真正的固定/最大宽度与 Flex 基准尺寸、响应式尺寸、视觉和裁切边界、网格/Flex 列宽、编号与同高度相邻对象,以及实际多行。
验证记录:
- 页面 396 的真实单页预览复测:文本框从约 385.5px 增至 403.5px,覆盖导出字体所需约 389.7px;目标字体从两行恢复一行,原始 X/Y、高度与 11.3pt 字号保持原样。原生 PPTX XML 保留完整文本与自动折行,没有插入换行或自动缩字。页面 395 同时复核,约 957.0px 文本框仍为一行。临时证据位于
.tmp/page396-analysis/baseline-evidence.json、evidence.json和target.pptx,不入库。 - Runtime 类型检查、生产构建和全量单测通过:118 个测试文件、857 条通过、3 条跳过;PPTX 相关 290 条通过。
test:repository的 16 条检查通过。 - 本次未执行 PowerPoint/WPS 实机视觉验收。