npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ofdkit

v0.6.1

Published

OFD 预览工具包:目录导航 + 字体兜底 + 按页懒渲染 + 功能可插拔,渲染引擎可切换(默认 Apache-2.0 的 DLTech21/ofd.js),默认零全局污染

Readme

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- 前缀,多实例互不干扰。