零服务端 · 零框架 · 文件不出浏览器

纯浏览器渲染 PPT

打开 .pptx / .ppt 不需要任何后端,也不用装 Office。
自研 OOXML、CFB/OfficeArt、EMF/WMF/PICT 解析,唯一运行时依赖是 fflate。

npm i @web-ppt/core

就在这个页面里跑

下面这个查看器用的就是 npm 上的 @web-ppt/core。拖一个自己的文件进来——它不会被上传到任何地方。

松手即可渲染
— / —
载入中…

解析与渲染全部发生在你的设备上,没有任何网络请求把文件送出去。

能力矩阵

两种格式各自走完全独立的解析链路,渲染层共用同一份 Schema。

能力.pptx.ppt
预设几何187 个预设(规范全集)MSOSPT 全表
自定义几何custGeom + 公式求值 + arcTopVertices / pSegmentInfo
填充纯色 / 渐变 / 图片 / 平铺 / 图案 / 主题色变换纯色 / 渐变 / 图片
描边虚线 / 箭头 / 端点 / 连接虚线 / 箭头
效果外阴影 / 内阴影 / 发光 / 柔化 / 倒影忽略
立体 3D挤出 / 斜角 / 轮廓 / 材质 / 视角忽略
样式继承母版 → 版式 → 占位符 → 段落 → runTxMasterStyle → 形状
文本完整 + 15 种艺术字变形字号 / 颜色 / 粗斜下划线 / 对齐 / 项目符号
图片裁剪 / 裁进形状 / 透明度 / 灰度Pictures 流 + DEFLATE
EMF / WMF / PICT自研 GDI / QuickDraw 解释器 → SVG同左(PICT 为 Mac 版所存)
表格tableStyles / 条纹 / 合并 / 边框表格属性 + 网格还原
图表13 类 · 次坐标轴 · 3D经内嵌 EMF 预览
切换效果41 种(含 p14 扩展与 morph 逐元素形变)经 SSSlideInfoAtom
元素动画入场 / 退场 / 强调 / 运动路径,按点击分批同上
SmartArt缓存 drawing part;无缓存时按数据模型自行排布
媒体封面帧 + 播放标识,可选嵌真实播放器
墨迹 · 评论 · 节支持
OLE 嵌入对象内嵌 p:pic 预览 / 旧式 VML 快照
隐藏页sld@show="0"SSSlideInfoAtom
数学公式 OMML分式 / 根式 / 上下标 / 大算符 / 矩阵 / 定界符
加密文档标准 AES-ECB / 敏捷 AES-CBCRC4 CryptoAPI(40/56/128 位)
备注 · 超链接支持支持

上手

渲染一页

import { parse, renderSlideToSvg } from '@web-ppt/core';

const pres = await parse(file);   // File | Blob | ArrayBuffer
el.innerHTML = renderSlideToSvg(pres, pres.slides[0]);

完整查看器

import { Viewer } from '@web-ppt/viewer-core';

const v = new Viewer(box, pres, { animate: true });
v.next();          // 先播动画,播完才翻页
v.setZoom(1.5);
await v.exportPng(2);

接你自己的 UI

import { PresentationState } from '@web-ppt/viewer-core';

// 无 DOM 的状态机,React / Vue 直接驱动
const st = new PresentationState(pres);
st.subscribe(setState);
st.next(); st.search('关键词');

不阻塞主线程

import { parseInWorker } from '@web-ppt/core';

const w = new Worker(
  new URL('@web-ppt/core/worker', import.meta.url),
  { type: 'module' });
const pres = await parseInWorker(w, buf);

导出

await slideToPng(pres, slide, 2);        // → Blob
await slideToSvgFile(pres, slide);       // → string
await presentationToPrintableHtml(pres); // 浏览器打印即 PDF

按需裁体积

// 图表解析器与图元文件解码器都经 hook 注入
setChartParser(myChartParser);
setMetafileDecoder(undefined);  // 省 ~15KB gzip

架构

解析层与渲染层完全解耦——渲染层只认 Schema,不认文件格式。

格式按魔数识别PK → pptx,D0CF11E0 → ppt。调用方不需要关心扩展名。
两条文本渲染路径预览用 foreignObject(文本可选中);导出用原生 <text> + 自实现测量断行与分栏——Chrome 会把含 foreignObject 的 SVG 判为污染画布,无法 toBlob
惰性解析每页首次访问时才解析,210 页首屏 376ms → 42ms。

保真 & 性能

保真不靠"看着差不多"

拿 LibreOffice 的实际渲染做 ground truth 逐档比对。主题色 shade/tint 必须在线性 RGB 里算:

档位LibreOfficesRGB 直乘线性 RGB
shade 20%33,56,97 Δ69 Δ8
tint 60%176,187,222 Δ37 Δ1

快照测试只能挡住"变了",挡不住"一开始就错了"——所以要有外部真值。

性能

浏览器实测,210 页 / 11280 元素:

惰性首屏(解析 + 首页渲染)42ms
全量解析376ms(1.8ms/页)
单页渲染0.09ms
JS 堆40MB(0.19MB/页)
优化效果
惰性解析(默认)首屏 9×
Worker 解析主线程零阻塞
缩略图虚拟化210 页只渲 7 个

疑难杂症

下面每一格都是实际踩过的坑,不是编出来的教学例子。左边是天真做法的结果,右边是本引擎实时渲染的同一份 .pptx

组合附加符号

OMML 的 m:acc@chr 存的是组合码位(U+20D7 等)。它本该附着在前一个字符上,单独当一个字形画出来,浏览器要么画成点状圈里的符号、要么位置漂移。

v⃗·xy̅
天真做法
本引擎

可伸缩定界符

括号按内容高度拉伸时不能绕基线等比放大。矩阵整体压在数学轴上、上下不对称,等比放大会让括号下缘冲出内容一大截。

() ab cd
天真做法
本引擎

分数线的基准

分数线要压在数学轴(约 x 高度的一半)上,不是压在基线上。单层分式看不出差别,一嵌套就整个塌下去。

1 1+ 1 x
天真做法
本引擎

零面积描线

标注引线是三点开放子路径。SVG 填充会把它自动闭合,于是引线被补成一块实心楔形。正确做法是正向走一遍再反向走回来——穿越数 +2,在 evenodd 下不改变填充,描边却照样可见。

天真做法
本引擎

evenodd 互相挖空

云形若用一堆重叠的椭圆拼,在 evenodd 填充规则下重叠处穿越数为偶,会被挖成洞。必须收敛成单条闭合轮廓。

天真做法
本引擎

内圈绕向

要在实心图形里挖洞,内圈必须反向绕。绕向相同时 nonzero 下洞根本不出现,换 evenodd 又会牵连别处的自交图形。

天真做法
本引擎

语料来自 Apache POI 的 242 个公开测试文件。排除 22 个故意损坏的模糊测试样本后,真实文件成功率 210/220;余下 10 个是截断文件、空文件、PowerPoint 95/4.0 与未知口令的加密文件。

作用依赖体积
@web-ppt/core解析 / 渲染 / 导出,无框架fflate84KB gzip
@web-ppt/viewer-coreheadless 状态机 + 播放层@web-ppt/core6.8KB gzip

想接 React / Vue / Svelte,直接驱动 PresentationState,不需要等官方封装。