ofdkit
v0.6.1
Published
OFD 预览工具包:目录导航 + 字体兜底 + 按页懒渲染 + 功能可插拔,渲染引擎可切换(默认 Apache-2.0 的 DLTech21/ofd.js),默认零全局污染
Maintainers
Readme
ofdkit
Vue 3 的 OFD 版式文档预览工具包:左侧目录导航、缩略图侧栏、页面水印、字体兜底、按页懒渲染、文本搜索,功能全部可插拔。渲染内核为 DLTech21/ofd.js(Apache-2.0,源码内置于包内),零外部静态资源、零全局污染。
- 开箱即用:一个
<OfdPreview>组件就是完整阅读器 - 按页懒渲染:只画视口附近的页,大文档不卡界面
- 左侧栏:图标列常驻(目录 / 缩略图 / 书签 / 文档信息),点图标展开对应面板,再点一次收起;缩略图懒渲染、滚远卸载、当前页高亮、点击跳页
- 目录导航:层级树、可折叠、点击跳页;来源 =
ofd:Outlines(书签ofd:Bookmarks是独立侧栏面板,不并入目录树,两源并存不重复),条目透出目标视图模式items[].dest(tooltip 可见),无目录时显示空态 - 文本搜索:全文索引 + 命中高亮 + 上/下一个命中跳转
- 签章透出:每枚章的签名值原始字节 / 签署者证书 / 签名算法值,宿主可验签(
onVerifySignature)、可下载 - 链接注释:URI 外链 / Goto 跳页 / GotoA 等动作可点,外链打开方式可接管
- 页面水印:文字/图片、平铺/居中,随内容滚动、可打印
- 主题:亮色 / 暗色 / 跟随系统(只翻外壳,纸张始终白底)
- 可插拔:每个功能都能用
controls关掉、用toolbarItems重排、用插槽替换
目录
- 安装
- 产物形态(默认 / light)
- 快速开始
- Props
- controls 细项开关
- 键盘快捷键
- 文档信息与签章
- 链接注释交互
- 导出当前页 PNG
- 加密 OFD 文件
- 权限控制(permission)
- 工具条编排(toolbarItems)
- 侧边栏(目录 / 缩略图)
- 状态栏编排(statusItems)
- 事件
- 插槽
- 命令式 API
- TypeScript
- 水印
- 全局配置(configureOfdkit)
- 文案定制(messages)
- a11y(无障碍)
- 组合式 API(不依赖完整组件)
- 引擎与许可
- 常见问题
安装
npm install ofdkit
# 运行时依赖:仅 jszip(随包自动安装)
# peer 依赖:vue ^3.3.0(随宿主项目安装)包内不依赖任何图标库:内置图标的路径数据已内联(
lucide,ISC,见NOTICE), 安装时不会把vue传递安装给宿主(vue由宿主自备)。
样式是单独文件,二选一引入:
import 'ofdkit/style.css';<link rel="stylesheet" href=".../node_modules/ofdkit/dist/ofdkit.css" />产物形态
dist/ 里有两种形态,默认入口是单文件(zip/XML 解析的 Web Worker 以内联方式打进包内,零配置可用):
| 入口 | 文件 | 主包体积 | 适用 |
|---|---|---|---|
| ofdkit(默认) | dist/ofdkit.js + dist/ofdkit.umd.cjs | ~1.2MB(gzip ~300KB) | 打包器宿主(vite/webpack)、<script> 直引 —— worker 保证生效 |
| ofdkit/light | dist/light/ofdkit.js + 2 个 ofdkit-*.worker.js | ~615KB(gzip ~132KB) | CDN / 静态部署,对首屏体积敏感的场景(主包 -50%) |
light 形态把两个解析 worker 独立成伴生文件,与 ofdkit.js 同目录、相对定位——把整个 dist/light/ 一起部署即可,无需配置;worker 加载失败(如文件没拷齐)会自动回退主线程解析,功能不损失、只是大文件解析会阻塞 UI。
⚠️ 通过打包器(vite / webpack)使用时请选默认入口:light 的伴生 worker 文件不会被宿主
构建自动带上(除非自行把 worker 文件拷贝到部署根目录),解析会退回主线程(功能不损失,
大文件解析会阻塞 UI)。CDN / 静态部署不受影响。
// light 形态(CDN / 静态部署)
import { OfdPreview } from 'ofdkit/light';
// 样式复用主包的即可(内容一致):import 'ofdkit/style.css';快速开始
全局注册 + 按 URL 加载:
// main.js
import { createApp } from 'vue';
import { configureOfdkit, OfdPreview } from 'ofdkit';
import 'ofdkit/style.css';
configureOfdkit({ theme: 'light' }); // 可选:全局默认主题
const app = createApp(App);
app.component('OfdPreview', OfdPreview);
app.mount('#app');<template>
<div style="height: 100vh">
<OfdPreview src="/files/report.ofd" watermark="内部资料" @load="onLoad" />
</div>
</template>
<script setup>
const onLoad = ({ pageCount, parseMode }) => {
console.log(`共 ${pageCount} 页,解析路径:${parseMode}`);
};
</script>按需局部注册(推荐,tree-shaking 更干净):
<script setup>
import { OfdPreview } from 'ofdkit';
import 'ofdkit/style.css';
</script>
<template>
<OfdPreview src="/files/report.ofd" />
</template>加载本地文件(File / ArrayBuffer):
<template>
<OfdPreview ref="viewer" />
<button @click="pick">打开 OFD</button>
</template>
<script setup>
import { ref } from 'vue';
import { OfdPreview } from 'ofdkit';
const viewer = ref();
function pick() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.ofd';
input.onchange = async () => {
const file = input.files?.[0];
if (file) await viewer.value.openBytes(await file.arrayBuffer(), file.name);
};
input.click();
}
</script>
src需要同源 URL(或目标服务端已开 CORS)。拖拽打开(controls.dragDrop)默认开启。
Props(<OfdPreview>)
| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| src | String | '' | OFD 文件地址(同源 URL)。为空时等待 openBytes() 或用户手动打开 |
| engineId | String | 'dltech' | 渲染引擎 id。当前内置 dltech(DLTech21/ofd.js) |
| controls | Object | {} | 细项开关,见下表。只传想改的项即可 |
| toolbarItems | Array | null | null | 工具条编排,见下文。null = 内置顺序 |
| statusItems | Array | null | null | 底部状态栏段落编排:内置段 id 数组,可增删/重排/重复 |
| watermark | Boolean | String | Object | null | 宿主水印配置,见下文。false / 不传 = 不加水印 |
| outlineWidth | Number | String | null | 左侧栏面板区宽度(px 数字或 CSS 长度) |
| outlineTitle | String | '目录' | 目录面板标题 |
| thumbnailWidth | Number | 132 | 缩略图目标宽度(px)。引擎实际按此宽度等比绘制 |
| defaultSidebarOpen | Boolean | true | 侧栏面板区初始是否展开(图标列常驻,只收放面板区) |
| defaultSidebarTab | String | 'outline' | 侧栏初始面板:'outline' | 'thumbnails' | 'bookmarks' | 'docinfo' |
| textSmoothing | String | 'auto' | 文字渲染档位:'auto' | 'antialiased' | 'subpixel-antialiased' |
| autoLoad | Boolean | true | 挂载后是否自动加载 src |
| zoomOptions | Array | null | null | 缩放档位(如 ['width','50%','100%','200%'])。null = 引擎默认档位 |
| theme | String | '' | 主题:'light' | 'dark' | 'auto'。留空用 configureOfdkit({ theme }) 的全局值 |
| messages | Object | null | null | 文案覆盖(键见下文「文案定制」),与 configureOfdkit({ messages }) 合并(prop 优先) |
controls 细项开关
默认值取向是「库形态」:渲染相关都开,诊断类默认关。宿主只传差异项:
<OfdPreview :controls="{ print: false, diagnostics: true }" />| 开关 | 默认 | 说明 |
| --- | --- | --- |
| toolbar | true | 顶部工具条总闸:关掉后整条工具条(含 #toolbar 及其分段插槽)都不渲染 |
| sidebar | true | 左侧边栏总闸:关掉后整个侧栏(图标列 + 面板区)都不渲染 |
| openFile | true | 打开本地文件(工具条按钮 / 拖拽 / api.openFile()) |
| download | true | 「下载原始文件」按钮(把当前文档的原始字节存盘 / api.download()) |
| export | true | 「导出当前页 PNG」按钮(api.exportPage() 同受门控,见「导出当前页 PNG」) |
| dragDrop | true | 拖拽文件到窗口即打开 |
| outline | true | 侧栏「目录」面板(无内置目录的文件也会展开面板,显示空态文案) |
| outlineCollapse | true | 目录层级折叠交互(关掉 = 树始终全展开) |
| outlineDefaultCollapsedLevel | 0 | 目录初始折叠层级,0 = 全展开 |
| thumbnails | true | 侧栏「缩略图」面板(需文档已加载且引擎支持按页渲染,否则不出现) |
| bookmarks | true | 侧栏「书签」面板(<ofd:Bookmarks> 独立来源,复用目录树渲染;无书签显示空态文案) |
| watermark | true | 水印总开关(注意:真要出现水印还需传 :watermark 配置) |
| zoom | true | 缩放(缩小 / 档位 / 放大) |
| search | true | 文本搜索。关掉后不建索引、按钮不出现、api.search() 变 no-op |
| paging | true | 翻页(首 / 上 / 页码 / 下 / 尾) |
| print | true | 打印(含 api.print({ range }) 的页范围) |
| documentInfo | true | 侧栏「文档信息」面板总开关(元数据 + 签章;不受 diagnostics 门控) |
| docinfoMeta | true | 文档信息面板里的「元数据」块(与签章块可独立插拔;两块都关 = 面板显示空态提示) |
| docinfoSignatures | true | 文档信息面板里的「签章」块(关闭后头部签章计数与验签提示一并隐藏) |
| linkAnnotations | true | 链接注释交互:OFD 里的 Link/URI/Goto 动作是否可点。关掉后页面 DOM 仍带 data-ofd-* 标记,但不挂手势、不抛事件;运行期切换立即生效 |
| statusBar | true | 底部状态栏(整条关掉即不渲染) |
| diagnostics | false | 状态栏里的诊断信息(渲染耗时 / PageID 映射 / 字体面板等) |
开关对命令式入口同样生效:
api.print()/api.openFile()在被对应开关关闭时不执行任何操作并console.warn(不再是「UI 隐藏了但api仍能执行」)。openFile是数据与输出入口,属安全相关。
controls里的未知键会被丢弃并告警(防:controls="{ serach: false }"这类拼错静默失效)。源地址默认脱敏:状态栏
source段与getState()里的源地址只保留origin + 文件名, 去掉可能携带?token=/?sign=的 query;需要完整 URL 用configureOfdkit({ exposeSourceUrl: true })。
键盘快捷键
内置一套文件级快捷键。监听在 document 上,但只在「焦点在本查看器内」时生效 ——
同页多个查看器只有被点过的那个响应。输入框里的按键一律不劫持。
| 按键 | 动作 | 受哪个开关约束 |
| --- | --- | --- |
| PageDown / PageUp | 下一页 / 上一页 | paging |
| Home / End | 首页 / 尾页 | paging |
| Ctrl/Cmd + F | 打开搜索条 | search(且引擎支持文本索引) |
| Ctrl/Cmd + P | 打印全部页 | print |
| + / = / - | 放大 / 缩小 | zoom |
| Esc | 关闭搜索条 | — |
快捷键走静默门控:对应开关关掉时按键无反应,不
console.warn、不抛@capability-denied(长按不会刷屏)。不劫持纯方向键 —— 本组件是滚动容器,↑↓←→的原生滚动必须保留。
文档信息与签章
左侧栏第 4 个图标「文档信息」(受 controls.documentInfo 门控):面板内是元数据 + 签章卡片
(印章图、验签标签、签署时间、签名方式、覆盖页、签名值/证书原始字节大小);
也可用 api.showDocumentInfo() / api.showDocInfo() 打开、getState().documentInfo / .signatures 读数据。
<OfdPreview :controls="{ documentInfo: false }" /> <!-- 整块关掉(状态栏段与面板都不出现) -->
<OfdPreview :controls="{ docinfoMeta: false }" /> <!-- 只关元数据块,签章块保留 -->
<OfdPreview :controls="{ docinfoSignatures: false }" /> <!-- 只关签章块(头部计数与验签提示一并隐藏) -->面板整体也可用 #docinfo 插槽替换(作用域:documentInfo / signatures / downloadSignature / state / api):
<OfdPreview :controls="{ documentInfo: true }">
<template #docinfo="{ documentInfo, signatures }">
<MyDocInfoPanel :info="documentInfo" :sigs="signatures" />
</template>
</OfdPreview>侧栏版签章卡是上下布局:印章大图(92px 高、按原始比例完整呈现)居中在上,印章名与验签标签居中, 明细行(提供方 / 签署时间 / 签名方式 / 覆盖页 / 字节大小)左对齐铺满整行。
- 元数据来自
Document.xml的DocumentInfo:标题 / 作者 / 主题 / 生成工具 / 创建与修改时间 / 自定义标签。 ⚠️ 多数业务文件没有该节点 ⇒ 字段可能全空,这不代表文件异常,面板会给兜底文案。 - 签章列出每枚章的印章名、印章图片、签署时间、签名方式与覆盖页,并给出签名值原始字节:
signedValue(逐字节等于 zip 内的SignedValue.dat全文)、签署者证书signerCert与签名算法值signatureValue(三份都是Uint8Array)。签章卡上显示字节数并提供下载按钮(复用controls.download门控,同时抛@download-signature);命令式入口api.getSignatures()/api.downloadSignature(id)。 - ⚠️ 库内不做验签(与「不做鉴权」同一条线):引擎的
verificationStatus恒为not-verified, 但宿主可以接入自己的验签——configureOfdkit({ onVerifySignature }),回调签名(record, originalBytes) => { status, detail },record即上面那条含三份原始字节的记录。 返回的status会替换面板上的「未验签」标签('valid'/'invalid'有内置样式,其余原样显示); 不配置 / 返回空 ⇒ 维持「未验签」。换文档后迟到的结论一律丢弃,结论同步进getState().signatureVerifications({ id, status, detail })。
宿主自定义:读
getState().signatures(含三份原始字节)自行渲染;#toolbar-info插槽可以放自己的验签结论。
链接注释交互
OFD 里的 Link / Action 注释(挂在正文图元或注释容器上的 URI / Goto / GotoA / Sound / Movie /
Goto→Bookmark 动作)默认可点。引擎把带动作的图元/注释摊平成 data-ofd-* 标记挂到页面 DOM 上,
并提供可枚举的 annotations() 契约(宿主不必刮 DOM);点击由壳侧在页面容器上统一委托处理。
| 动作 | 默认行为 |
| --- | --- |
| URI | window.open(uri, target \| '_blank', 'noopener')(可用 openExternalLink 接管或禁止) |
| Goto | 目标记的是 PageID 不是页码,自动映射后 goToPage() 跳页;悬停显示「跳转到第 N 页」 |
| GotoA / Sound / Movie / Goto→Bookmark | 只抛 @annot-activate,无默认行为 |
- 只认
Event="CLICK":DO/PO(打开文档 / 页面时自动触发的动作)会出现在annotations()清单里, 但不挂手势、不静默执行。 - ⚠️ 具名
Bookmark跳转暂不支持(点击只抛事件)。@annot-activate在默认行为之外抛出,载荷{ kind, pageIndex, pageId, annotType, element, uri?, target?, dest?, bookmarkName? },宿主可观测 / 审计 / 整体接管。 - 视觉只加
cursor:pointer与title,不改 appearance(版式保真优先)。
<!-- 只读标记、不挂手势(DOM 上仍有 data-ofd-* 属性) -->
<OfdPreview :controls="{ linkAnnotations: false }" />
<!-- 审计或接管外链 -->
<OfdPreview @annot-activate="({ kind, uri }) => track(kind, uri)" />configureOfdkit({
// 函数 = 宿主接管(自建跳转 / 审计 / 兜底浏览器);false = 一律禁止跳转(只抛事件)
openExternalLink: (uri, target) => myRouter.open(uri, target),
});导出当前页 PNG
把任意一页离屏光栅化为 PNG(100% 基准 × scale),两条入口:
// ① 命令式:index 0 起;返回 { blob, width, height, name } 或 null(文档未开 / 门控拒绝 / 渲染失败)
const out = await viewer.value.exportPage(0, { scale: 2 }); // scale 默认 2,护栏 [0.25, 8]
// out.blob 是 image/png Blob;name 形如 '51637-p1.png'
// ② save: false = 只出 Blob 不存盘、不抛事件(宿主自己决定怎么用)
const raw = await viewer.value.exportPage(4, { save: false });工具条「导出当前页 PNG」按钮(受 controls.export 门控)存盘当前页并抛 @export-page
({ index, name, width, height, byteLength },1 起页码)——库已自行存盘,宿主可换成
自己的保存方式(入库 / 走服务端):
<OfdPreview @export-page="({ name, byteLength }) => track(name, byteLength)" />实现要点与边界:
| 事项 | 说明 |
| --- | --- |
| 渲染基准 | 目标页按 100% 缩放离屏重渲染再合成,结果尺寸固定为 页宽100% × scale,与当前视口 / 懒加载进度无关 |
| 合成内容 | 页内三类图元(矢量 svg / 图片 img / canvas)按文档序绘制,含旋转矩阵与注释层;背景白底 |
| 字体边界 | 导出图内的文字字形由系统字体决定(SVG-as-image 不继承页面的 @font-face 与宿主字体栈),与屏幕显示可能存在字形差异 |
| 权限门控 | export 进数据出口能力表:permission 为 deny 时 api.exportPage() 返回 null 并 warn |
加密 OFD 文件
打开加密 OFD 时(jszip 不支持加密 zip,解析在 loadAsync 阶段即失败),库明确分类而不是
落进通用的「打开失败」:主体区显示加密态卡片(#encrypted 插槽可整体替换),不抛 @error。
- 库内不做密码学:没有密码框、不存密码、不弹对话框(与「不做鉴权」同一条线)。
- 解密由宿主接入:
configureOfdkit({ decryptProvider })——(bytes) => plainBytes,把加密字节 解成明文 zip 字节(ArrayBuffer/Uint8Array/Blob均可)返回;返回null/ 抛错 = 解锁失败(卡片内联提示,可重试)。成功后原地重开,无感衔接。
configureOfdkit({
decryptProvider: async (bytes) => {
const plain = await myServer.decrypt(bytes); // 解密放服务端 / 专用 SDK,别在前端放密钥
return plain; // 明文 zip 字节;失败返回 null 或抛错
},
});没有配置
decryptProvider时只显示加密提示,不给解锁按钮(宿主可在#encrypted插槽里 自建任何解锁 UI,作用域参数:file-name, reason, error, busy, can-unlock, unlock)。
权限控制(permission)
⚠️ 本包不做鉴权:库内不认用户身份、不调权限服务、不存 token,也无法阻止用户直接访问原始 URL。 真正的准入必须在服务端(文件接口鉴权 / 签名 URL)。
permission解决的是客户端这一侧必须做对的三件事: ① 无权时不发出请求(省一次注定 403 的往返,也不泄露「这个 URL 存在」);② 无权时不把数据搬进内存与 DOM; ③ 权限在运行中变化(异步到位 / 被回收)时,界面与内存同步收敛。
<!-- 全放行(默认,与不传等价) / 全拒绝 -->
<OfdPreview src="/f/a.ofd" :permission="true" />
<OfdPreview src="/f/a.ofd" :permission="false" />
<!-- 只写要拒的能力键(其余放行);可带展示文案与异步裁决器 -->
<OfdPreview :permission="{ print: false, reason: '当前角色无打印权限' }" />
<OfdPreview :permission="{ resolver: () => authApi.check(), fallback: 'deny' }" />能力 id 与 controls 键同名(另有主闸门 view):view / toolbar / sidebar / openFile / download /
dragDrop / outline / outlineCollapse / thumbnails / watermark / zoom / search / paging / print /
documentInfo / linkAnnotations / statusBar / diagnostics。合成是 controls ∧ permission
(deny-wins:任一来源说「不」就是「不」),只需改一处,工具条 / 侧栏 / 搜索 / 打印全部自动跟随。
| 形态 | 语义 |
| --- | --- |
| 不传 / true / null | 全放行(= 不启用权限层,与旧版本零差异) |
| false | 全拒绝 |
| 对象 | 只写要拒的键(未提到的键默认放行)。⚠️ 不提供「白名单列表」形态:能力键与 controls 同名,列表按白名单解释会把工具条 / 侧栏 / 状态栏一起关掉 |
| 对象字段 | reason(展示文案)/ resolver(异步裁决器,挂载后立即执行)/ fallback: 'deny' \| 'allow'(裁决失败/超时的兜底,默认 deny) |
三态:pending(异步裁决中,渲染骨架屏 —— 还没裁决就报无权限 = 误伤)/ denied(无权限态)/ allowed。
pending 期间不发请求;裁决为 allowed 后自动续跑被挂起的加载。view 被拒时整壳替换为无权限态
(#denied 插槽可整体替换),此时数据入口已全部拦下(无请求、无解析、无页面 DOM)。
<OfdPreview :permission="perm">
<template #denied="{ permission, reason, retry, requestAccess }">
<MyDeniedCard :reason="reason" @retry="retry" @apply="requestAccess" />
</template>
</OfdPreview>| 相关 API | 说明 |
| --- | --- |
| :permission prop / configureOfdkit({ permission }) | 单实例 / 全局默认。三来源(覆盖 > prop > 全局)deny-wins 合成 |
| api.setPermission(next) | 运行期覆盖(WebSocket 推送 / 切角色);null 释放覆盖、回落 prop。覆盖期间 prop 变化仍参与合成 |
| api.refreshPermission() | 强制重跑 resolver |
| api.requestAccess(reason?) | 抛 @access-request,申请流程由宿主接管 |
| getState().permission | { state, caps, reason, denied, checkedAt } |
| @permission-change / @access-request | 裁决落定(按语义键去重)/ 用户点了「申请权限」 |
被回收(
allowed → denied)时:清空本实例的 DOM 与数据(调度器 / 页面 / 目录 / 搜索 / 统计), 但不清理同页多实例共用的解析缓存与 worker —— 清理属宿主决策。 重新授权(denied → allowed)默认不自动加载;无权限卡上的「重试」会重跑裁决并在放行后补一次加载。不做:库内鉴权 / token 存储刷新 / 401 拦截 / 页级·行级权限 / 新增 provide-inject 或全局 store。
工具条编排(toolbarItems)
内置项用 key 引用,可局部覆盖(只改标题/图标,不动行为);自定义项用 type: 'custom';另支持 spacer(弹性占位,把后续项推到右侧)与 separator(分隔线):
<OfdPreview
:toolbar-items="[
{ key: 'open-file' },
{ type: 'spacer' },
{ key: 'zoom', title: '缩放' },
{ type: 'separator' },
{ key: 'print' },
{ type: 'custom', key: 'sign', title: '验签', icon: 'shield', onClick: () => verify() },
]"
/>内置 key:open-file / search / zoom-out / zoom / zoom-in / first / prev / page-info / next / last / print。
自定义项可传 icon(内置图标名)或 component(Vue 组件),点击后同时广播 action 事件,方便统一埋点。
侧栏交互(目录 / 缩略图)不走工具条:侧栏左侧有一列常驻图标,点图标展开对应面板、再点一次收起。整栏的显隐与面板切换见下文「侧边栏」。 水印同样不占工具条位:它是宿主配置驱动的(
:watermark),运行期开关用api.toggleWatermark()/api.setWatermark()。
侧边栏(目录 / 缩略图)
左侧栏采用 WPS 式图标列:最左一列常驻图标,点图标展开对应面板,再点一次同一个图标收起面板区(图标列不消失);展开态点另一个图标 = 切换面板。
侧栏是三层可插拔,宿主可以逐层控制:
| 层 | 手段 | 说明 |
| --- | --- | --- |
| 总闸 | :controls="{ sidebar: false }" | 整个左侧栏(图标列 + 面板区)都不渲染 |
| 面板可用性 | :controls="{ outline: false }" / :controls="{ thumbnails: false }" | 图标列只剩对应图标;两个都关 = 等效于总闸关闭 |
| 初始状态 | :default-sidebar-open="false" / :default-sidebar-tab="'thumbnails'" | 挂载时的展开状态与落点面板 |
| 尺寸 | :outline-width="280" / :thumbnail-width="160" | 面板区宽度 / 缩略图绘制宽度 |
| 运行期 | api.toggleSidebar() / api.showOutline() / api.showThumbnails() / api.showBookmarks() | 命令式控制;#outline 插槽的 panel-toggle / panel-select 同效 |
| 整体替换 | #outline 插槽 | 用宿主自己的侧栏实现整套换掉 |
侧栏任何显隐/切换都会广播 sidebar-change 事件({ open, panel }),方便宿主同步状态或持久化用户偏好:
<OfdPreview
:default-sidebar-tab="'thumbnails'"
:default-sidebar-open="false"
@sidebar-change="({ open, panel }) => save({ open, panel })"
/>缩略图面板本身:每页一张小图 + 页码角标、懒渲染(只画视口附近的)、滚远自动卸载、当前页高亮并自动滚进视野、点击跳页。面板常驻 DOM(v-show),切走再切回保留滚动位置。
状态栏编排(statusItems)
内置段 id:page-count / document-info / render / outline / page-map / fonts / engine / page-metrics / zoom / parse-cost / source / notice / warnings。
其中 page-count 与 document-info 不受 controls.diagnostics 门控(前者是页数、后者是签章与元数据,都是业务读者要看的),document-info 另有自己的 controls.documentInfo。
<OfdPreview :status-items="['page-count', 'zoom', 'source']" />事件
| 事件 | 载荷 | 触发时机 |
| --- | --- | --- |
| load | { documents, pageCount, source, parseMode } | 文档解析完成。parseMode 为 'worker' 或 'main'(worker 不可用时自动回退主线程) |
| error | Error | 打开/解析失败 |
| page-change | pageIndex(Number,1 起) | 当前页变化 |
| zoom-change | { zoom, effective } | 缩放档位变化 |
| outline-change | { items, tree, bookmarkItems, bookmarkTree, warnings } | 目录与书签解析完成(条目含 dest: { type, left, top, zoom } \| null 目标视图模式;书签是<ofd:Bookmarks>独立来源,自成 bookmarkTree,不并入目录树) |
| outline-collapse | { collapsed } | 目录折叠状态变化 |
| watermark-change | { on, visible, config } | 水印开关/配置变化 |
| search | { keyword, total, activeIndex, hit } | 搜索执行或在命中间移动 |
| action | { key, event } | 工具条按钮被点击(含自定义项) |
| download | { name, bytes } | 触发「下载原始文件」。库已自行存盘,宿主可在此换成自己的保存方式(入库 / 走服务端) |
| download-signature | { id, name, bytes, byteLength } | 下载某枚签章的签名值原始字节。库已自行存盘,宿主可换成自己的保存方式 |
| annot-activate | { kind, pageIndex, pageId, annotType, element, uri?, target?, dest?, bookmarkName? } | 链接注释被点击(URI/Goto 的默认行为之外抛出,见「链接注释交互」) |
| export-page | { index, name, width, height, byteLength } | 导出当前页 PNG 已存盘(index 为 1 起页码;save: false 的命令式调用不抛)。宿主可换成自己的保存方式 |
| sidebar-change | { open, panel } | 侧栏展开/收起或面板切换(panel 为 'outline' | 'thumbnails') |
插槽
区域级插槽一律给全量 state + api(与模板 ref 的公开面同源);面板内的分段插槽额外附加该段的参数:
| 插槽 | 作用域参数 | 用途 |
| --- | --- | --- |
| toolbar | state, api | 整体替换工具条 |
| toolbar-prepend | state, api | 工具条左段前插内容 |
| toolbar-info | state, api | 工具条中间段(如宿主自渲染验签结论) |
| toolbar-append | state, api | 工具条右段后插内容 |
| outline | state, api + tree, items, current-page, collapsed-ids, select, panel, open, panel-select, panel-toggle | 整体替换左侧栏(目录 / 缩略图) |
| outline-header / outline-item / outline-footer | 该段参数(item / active / toggle / select / collapsed-count …) | 目录窗格分段定制 |
| outline-empty | text | 自定义目录空态 |
| watermark | config, style-text | 替换 viewer 作用域水印层 |
| loading | text | 自定义加载态 |
| empty | text | 自定义空态 |
| denied | permission, reason, retry, requestAccess | 整体替换无权限态(见上节「权限控制」) |
| encrypted | file-name, reason, error, busy, can-unlock, unlock | 整体替换加密态(见「加密 OFD 文件」) |
| status | state, api | 整体替换状态栏 |
| status-append | state, api | 状态栏追加内容 |
| overlay | state, api | 浮层(如自定义右键菜单)。⚠️ 不受 controls 门控,宿主自负 |
命令式 API
通过模板 ref 调用(也可从任意插槽的 api 作用域参数取得同一对象):
const viewer = ref();
viewer.value.goToPage(5);
viewer.value.search('合计'); // 打开搜索条并执行
viewer.value.setWatermark({ text: '新水印' });
viewer.value.toggleWatermark(false); // 临时关闭
const { pageIndex, pageCount } = viewer.value.getState();模板 ref 的公开面与
api一致:下表所有方法(含openFile/reload/zoomIn/collapseAll…) 都可直接挂在 ref 上调用,viewer.value.openFile()与viewer.value.api.openFile()等价。 另有getState()/getMainEl()两个不在api里的辅助方法。
| 方法 | 说明 |
| --- | --- |
| goToPage(n) | 跳转到第 n 页(0 起,首页为 0;如 goToPage(0) = 第 1 页)。与 page-change / getState().pageIndex 的「1 起」错开,避免跳页差一页 |
| openBytes(buf, name?) | 用字节打开文档。ArrayBuffer / File / Blob,以及 Uint8Array(含 Node Buffer)、DataView 等视图(内部自动归一化) |
| print(options?) | 打印。options.range = 'all'(默认)| 'current' | 页码(1 起)| { from, to } |
| download() | 下载当前文档的原始字节(同时抛 @download)。controls.download=false 时 no-op |
| exportPage(index, options?) | 导出第 index 页(0 起)为 PNG:{ scale?, save? },scale 默认 2(护栏 [0.25, 8]);save: false 只返回 Blob 不存盘不抛事件。返回 { blob, width, height, name } 或 null(controls.export=false / 无文档 / 渲染失败时) |
| getSignatures() | 签章列表,每条含 signedValue / signerCert / signatureValue 三份原始字节(Uint8Array) |
| downloadSignature(id) | 下载某枚签章的签名值原始字节(同时抛 @download-signature)。controls.download=false 时 no-op |
| setSmoothing(mode) | 文字渲染档位('auto' | 'antialiased' | 'subpixel-antialiased') |
| setWatermark(cfg \| null) | 运行时换水印(null 恢复 props 配置)。优先级高于 watermark prop |
| toggleWatermark(on?) | 显示/隐藏水印 |
| refreshWatermark() | 水印配置未变但需要重算时手动刷新 |
| search(keyword) | 打开搜索条并执行搜索(keyword 为空 = 只开条) |
| clearSearch() | 清搜索结果与高亮 |
| getState() | 当前状态快照:页码(1 起)/页数、缩放、侧栏状态(sidebar.open / sidebar.panel)、水印状态、搜索状态、渲染统计、文档元数据、签章与宿主验签结论(documentInfo / signatures / signatureVerifications)、告警等 |
| getMainEl() | 滚动主容器 DOM |
| showOutline() / showThumbnails() / showBookmarks() | 展开侧栏并显示目录 / 缩略图 / 书签面板(showDocInfo() = 文档信息面板) |
| showDocumentInfo() | 打开侧栏「文档信息」面板(controls.documentInfo=false 时 no-op) |
| toggleOutline() / toggleSidebar() | 侧栏面板区显隐(图标列常驻,两个方法等价) |
| api | 完整命令对象(与模板 ref 的公开面同源);其余方法:openFile / reload / next / prev / first / last / zoomIn / zoomOut / setZoom / toggleStatusDetail / collapseAll / expandAll |
TypeScript
包内自带类型声明(dist/types/),exports 的 types 条件已接好,import { OfdPreview } from 'ofdkit' 开箱有补全:
import OfdPreview from 'ofdkit';
import type { OfdPreviewApi, OfdViewerState, OfdSignature, OfdControls } from 'ofdkit';
const viewer = ref<InstanceType<typeof OfdPreview>>();
const api: OfdPreviewApi = viewer.value!.api;
const state: OfdViewerState = viewer.value!.getState();
const seals: OfdSignature[] = state.signatures;- props 有约束(含
theme/defaultSidebarTab这类字面量联合)、事件有载荷类型(@load/@download…)、 模板 ref 上的api与getState()都有类型。 - 导出类型:
OfdControls/OfdPreviewProps/OfdPreviewEmits/OfdPreviewApi/OfdViewerState/OfdSignature/OfdSignatureVerification/OfdAnnotActivatePayload/OfdDocumentInfo/OfdPermissionInput,以及各独立组件的XxxProps/XxxEmits。 - ⚠️ 插槽作用域参数不会被
h()/ JSX 自动推导(Vue 的类型机制所限),需要时用导出的OfdPreviewSlots等接口显式标注。
水印
三种写法,语义是「宿主叠加的水印」——OFD 文件自带的那个水印由引擎从文档注释渲染,本包不碰它:
<OfdPreview :watermark="'内部资料'" /> <!-- 快捷文字水印 -->
<OfdPreview :watermark="{ text: '内部资料', opacity: 0.2, rotate: -30 }" />
<OfdPreview :watermark="false" /> <!-- 关闭 -->| 配置项 | 默认 | 说明 |
| --- | --- | --- |
| enabled | true | 总开关 |
| type | 'text' | 'text' | 'image'(给了图片没给文字时自动推断为图片) |
| text | '内部资料 请勿外传' | 文字内容,\n 多行;也可传函数实现每页差异化文案 |
| image | '' | 图片水印地址(同源 URL 或 data:) |
| imageWidth / imageHeight | 120 / 0 | 图片尺寸(px);高度 0 = 按宽度等比 |
| opacity | 0.12 | 不透明度 0~1 |
| color | '#000000' | 文字颜色 |
| fontSize | 22 | 字号(数字按 px,也可传 '22px' / '1.4em') |
| fontFamily / fontWeight | 系统栈 / 400 | 字体与字重 |
| rotate | -30 | 旋转角度(度,逆时针为正) |
| gapX / gapY | 200 / 140 | 平铺间距(px) |
| position | 'tile' | 'tile'(平铺)| 'center'(单个居中,配合 scale) |
| scale | 0.9 | 居中模式大小:相对层宽的倍数 |
| lineHeight | 1.4 | 多行行距 |
| letterSpacing | 0 | 字距(px) |
| stroke / strokeWidth | '' / 2 | 描边色与宽度(浅底/花底提高可读性) |
| offsetX / offsetY | 0 | 平铺相位偏移(px,可为负) |
| blendMode | '' | mix-blend-mode,如 'multiply' |
| guard | false | 防篡改守护:水印层被删/样式被改时自动补挂 |
| zIndex | 200000 | 层级(默认高于文档自带注释层) |
| scope | 'page' | 'page':每页叠一层,随滚动、可打印;'viewer':覆盖可视区的屏幕水印,不随滚动、不进打印 |
| className | '' | 附加类名(宿主样式钩子) |
要完全自定义 page 水印的形态,用 render 回调自建节点(页面 DOM 由引擎创建,不支持 Vue 插槽):
:watermark="{ text: '内部资料', render: (pageEl, ctx) => { /* 返回 HTMLElement */ } }"全局配置(configureOfdkit)
基础预览不需要任何配置。需要时在应用启动早期调用一次:
import { configureOfdkit } from 'ofdkit';
configureOfdkit({
theme: 'auto',
workerParse: true,
fontBase: '/static/ofd-fonts',
fontSlots: [
{ id: 'xiaobiaosong', label: '小标宋',
system: ['FZXiaoBiaoSong-B05S', '方正小标宋简体'],
keywords: ['xiaobiaosong', '小标宋'] },
],
});| 配置项 | 默认 | 说明 |
| --- | --- | --- |
| theme | 'light' | 全局默认主题,<OfdPreview :theme> prop 优先 |
| workerParse | true | 是否优先在 Web Worker 里解析(zip 解压 + XML→JSON 是 CPU 密集操作)。worker 不可用时自动静默回退主线程;false 可调试对齐解析行为 |
| fontBase | null | 本地兜底字体目录(其下放 simsun.woff / simhei.woff / simkai.woff)。本包不附带任何字体文件,需宿主自备;不配置 = 纯系统字体 |
| fontSlots | [] | 追加/覆盖字体槽位:{ id, label, system: string[], local?, degrade?, keywords: string[] }。同 id 覆盖,新 id 插到最前(最高优先级)——宿主有内部字库(如方正全套)时让「小标宋」不再降级到宋体 |
| exposeSourceUrl | false | 是否把完整源 URL(含 query)透出到状态栏 source 段与 getState()。默认只显示 origin + 末段文件名,避免内网 ?token= / ?sign= 明文外泄 |
| openExternalLink | null | 链接注释里 URI 动作的打开方式:null = window.open(uri, target \| '_blank', 'noopener');函数 (uri, target) => void = 宿主接管;false = 一律禁止跳转(只抛 @annot-activate)。只作用于 URI,Goto 跳页不受管 |
| onVerifySignature | null | 宿主自有验签回调 (record, originalBytes) => { status, detail };record 含 signedValue / signerCert / signatureValue 三份原始字节。返回的 status 替换签章卡上的「未验签」标签,结论同步进 getState().signatureVerifications |
| decryptProvider | null | 加密 OFD 的宿主解密回调 (bytes) => plainBytes:把加密字节解成明文 zip 字节返回(ArrayBuffer / Uint8Array / Blob),库据此原地重开;返回 null / 抛错 = 解锁失败(加密卡片内联提示)。库内不做密码学、不存密码,见「加密 OFD 文件」 |
| messages | null | 全局文案覆盖表(键见下文「文案定制」)。组件级 messages prop 优先于本全局层;未知键丢弃 + 控制台告警 |
文案定制
库的全部用户可见 UI 文案(工具条 / 搜索条 / 目录 / 状态栏与诊断 / 文档信息与签章 / 空态加载错误 / 加密态 / 权限态等约 110 键)都集中在默认文案表,可逐键覆盖、不做 i18n (OFD 是国标、受众中文,真实需求是术语可换而非多语言):
// 全局层:所有实例生效
configureOfdkit({ messages: { 'toolbar.print': '打印文档', 'outline.empty': '本文件没有目录' } });
// 组件层(优先于全局层)
<OfdPreview :messages="{ 'outline.title': '书签' }" />- 占位符
{n}形式(如'outline.pageLabel': '第 {n} 页'),覆盖时保留占位符即可; - 未知键丢弃 + 控制台告警(同键只告警一次);
- 三层优先级:
messagesprop >configureOfdkit({ messages })> 默认表。
a11y(无障碍)
- 翻页区
role="region"(可聚焦,PageUp/PageDown 与方向键翻页见「键盘快捷键」); - 目录树
role="tree"/treeitem,当前项aria-current,禁用项aria-disabled; - 诊断抽屉与文档信息浮层
role="dialog"+aria-modal+ 焦点圈定(打开移焦、Tab 循环、关闭归还); - 工具条与侧栏图标列支持方向键移动焦点(roving)。
- 上述区域语义标签同样走文案表(
a11y.*键)。
组合式 API(不依赖完整组件)
以下能力可单独引入,用于自建界面:
import {
parseOfdOutline, // 目录解析(worker 优先,失败自动回退主线程)
createPageScheduler, // 按页懒渲染调度器
OfdToolbar, // 工具条组件(可被 OfdPreview 的 #toolbar 插槽复用)
OfdOutlinePanel, // 目录面板组件
OfdIcon, // 图标组件(内置图标名 + 宿主原始 SVG 字符串)
normalizeWatermark, // 水印配置归一化
applyWatermarkToPages,// 往页面节点批量挂水印
ENGINES, // 引擎注册表
} from 'ofdkit';开发 / 贡献
源码、测试与开发文档在仓库内(不随 npm 包发布)。
引擎与许可
- 本库代码 MIT;渲染内核 DLTech21/ofd.js 为 Apache-2.0(源码内置,声明见包内
NOTICE); 图标图形来自 lucide(ISC),仅内联其路径数据,不随包引入 lucide 依赖。 - 当前内置唯一引擎
dltech。ENGINES注册表开放扩展:实现引擎契约即可换成自己的渲染内核。 - 已知边界:JPX(JPEG2000)内嵌图与 OFD 内嵌字体文件暂不支持解码(终端系统字体齐全时无感知);跨 TextObject / 跨行的连续选择与搜索暂不支持;多 Document 文件只渲染第 1 个(状态栏会明示)。
常见问题
文件在服务端被更新后,重新打开还是旧内容?
库内对目录解析只做「在途去重」(并发加载共享一次解析,完成后即弃),不做跨次打开的持久缓存;
src 加载走协商缓存(有 ETag 时 304 省流量,否则回源)。api.reload() 会真正重新拉取解析。
若仍看到旧内容,检查服务端 / CDN 的缓存策略(或给 URL 加版本参数)。
文字显示不正确 / 缺字?
OFD 引用的是「宋体 / 黑体 / 楷体」等逻辑字体名,实际显示取决于终端系统字体。内网 Windows 一般齐全;缺字体时用 fontBase 指向自备字体目录,或用 fontSlots 注入内部字库。
搜索不到跨行的内容? 搜索基于引擎抽取的文本段(按行合并)。跨 TextObject、跨行的连续文本暂不在索引范围内。
打印样式与屏幕不一致?
打印会先渲染全部页再克隆 DOM,-webkit-font-smoothing 等观感项在不同浏览器可能有差异;打印走独立打印窗口,字体栈为行内样式不受影响。
一个页面放两个查看器?
支持。组件不写 window、不注入全局 CSS、类名统一 ofd- 前缀,多实例互不干扰。
