ofdkit

OFD 预览工具包:目录导航 + 字体兜底 + 按页懒渲染 + 功能可插拔,渲染引擎可切换(默认 Apache-2.0 的 DLTech21/ofd.js),默认零全局污染
744
Install
npm install ofdkit
Latest Version:0.6.1
Vue:3
License:MIT
Last Updated:Oct 16, 2026
Links: npm
Maintainer: ahao_na

ofdkit

npm version npm downloads npm unpacked size minzip size peer: vue license

Vue 3 的 OFD 版式文档预览工具包:左侧目录导航、缩略图侧栏、页面水印、字体兜底、按页懒渲染、文本搜索,功能全部可插拔。渲染内核为 DLTech21/ofd.js(Apache-2.0,源码内置于包内),零外部静态资源、零全局污染。

  • 开箱即用:一个 <OfdPreview> 组件就是完整阅读器
  • 按页懒渲染:只画视口附近的页,大文档不卡界面
  • 左侧栏:图标列常驻(目录 / 缩略图 / 书签 / 文档信息),点图标展开对应面板,再点一次收起;缩略图懒渲染、滚远卸载、当前页高亮、点击跳页
  • 目录导航:层级树、可折叠、点击跳页;来源 = ofd:Outlines(书签 ofd:Bookmarks 是独立侧栏面板,不并入目录树,两源并存不重复),条目透出目标视图模式 items[].dest(tooltip 可见),无目录时显示空态
  • 文本搜索:全文索引 + 命中高亮 + 上/下一个命中跳转
  • 签章透出:每枚章的签名值原始字节 / 签署者证书 / 签名算法值,宿主可验签(onVerifySignature)、可下载
  • 链接注释:URI 外链 / Goto 跳页 / GotoA 等动作可点,外链打开方式可接管
  • 页面水印:文字/图片、平铺/居中,随内容滚动、可打印
  • 主题:亮色 / 暗色 / 跟随系统(只翻外壳,纸张始终白底)
  • 可插拔:每个功能都能用 controls 关掉、用 toolbarItems 重排、用插槽替换

目录

安装

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 仍能执行」)。print / 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} 页'),覆盖时保留占位符即可;
  • 未知键丢弃 + 控制台告警(同键只告警一次);
  • 三层优先级:messages prop > 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- 前缀,多实例互不干扰。

Related Packages

vue-markdown-kit

A small, auditable Markdown preview toolkit for Vue 2.

524 0
vue3-markdown-kit

A small, auditable Markdown preview toolkit for Vue 3.5.

195 0
ofd-lzy

Browser-side OFD (GB/T 33190) document viewer with e-invoice rendering, digital-...

708 0
@noirmd/previewer

Framework-agnostic markdown renderer: one vanilla DOM engine with thin React/Vue...

4,457 0

Version History

Version Released Vue License
0.6.1 ^3.3.0 MIT
0.6.0 ^3.3.0 MIT
0.2.0 ^3.3.0 MIT
0.1.2 ^3.3.0 MIT
0.1.1 ^3.3.0 MIT
Pre-releases (1)
Version Released License
0.0.0-stage