ofd-reader
v0.2.0
Published
A lightweight browser OFD reader component with SVG rendering.
Maintainers
Readme
OFD Reader
轻量、框架无关的 Web OFD 阅读组件。当前项目处于 0.x 预览开发阶段,重点是稳定基础阅读能力和 TypeScript API。
功能状态
- OFD ZIP/XML 解析。
- SVG 页面渲染。
- 本地 OFD 文件加载。
- 缩放、适宽、适页。
- 浏览器打印。
- 页码跳转和当前页事件。
- 缩略图导航。
- 搜索高亮,支持基础 API 模式和内置右侧搜索面板。
- loading、empty、error 状态。
- 基础
Clips裁剪支持。
文本选择、签章展示和验签仍在规划中。
安装
npm install ofd-reader当前 0.x 为预览版,适合先在真实业务环境中验证浏览、缩放、页码跳转、缩略图、打印和搜索能力。
许可证
本项目使用 AGPL-3.0-or-later 许可证。
AGPL 允许商业使用,但如果将本项目或其修改版本用于网络服务、Web 系统或分发场景,需要按照 AGPL 要求向用户提供对应源码。若需要闭源商业授权,请联系维护者另行授权。
基础使用
import { createOFDViewer } from 'ofd-reader'
import 'ofd-reader/style.css'
const container = document.getElementById('app')
if (!container) {
throw new Error('Container not found')
}
const viewer = createOFDViewer(container, {
defaultScale: 'fit-width',
maxFitWidth: 980,
thumbnails: {
enabled: true,
width: 110
}
})
await viewer.load(file)load() 接收浏览器 File、Blob 或 ArrayBuffer。如果你需要从远程地址加载,可以先自行 fetch:
const response = await fetch('/demo.ofd')
const blob = await response.blob()
await viewer.load(blob)自定义工具栏
组件不内置键盘快捷键。业务侧可以通过 API 绑定按钮或快捷键:
zoomInButton.onclick = () => viewer.zoomIn()
zoomOutButton.onclick = () => viewer.zoomOut()
fitWidthButton.onclick = () => viewer.fitWidth()
fitPageButton.onclick = () => viewer.fitPage()
window.addEventListener('keydown', event => {
if (event.ctrlKey && event.key === '=') {
event.preventDefault()
viewer.zoomIn()
}
})打印
文档加载完成后可以调用 viewer.print():
await viewer.print({
title: 'invoice.ofd'
})打印流程会将 OFD 重新渲染到隔离的打印文档中,因此当前阅读缩放比例、滚动位置、缩略图和宿主页面样式不会影响打印页面。建议业务侧在按钮点击等用户操作中调用,降低浏览器拦截打印对话框的概率。
浏览器打印仍受浏览器、操作系统和打印机驱动控制。组件不能实现静默打印,也不能保证硬件边距、混合页面尺寸或打印机专有设置完全可控。
搜索
每个 viewer 实例都提供搜索 API。业务侧可以完全自定义搜索框、按钮和结果列表:
viewer.search('发票')
viewer.nextSearchResult()
viewer.previousSearchResult()
viewer.goToSearchResult(0)
const state = viewer.getSearchState()
const results = viewer.getSearchResults()
const summary = viewer.getSearchSummary()如果需要使用内置右侧搜索面板:
const viewer = createOFDViewer(container, {
search: {
mode: 'panel',
defaultOpen: true,
width: 280,
panel: {
placeholder: '查找',
labels: {
previous: '上一个',
next: '下一个',
clear: '清空',
noDocument: '未加载文档',
noSearch: '输入关键词搜索',
noMatches: '无匹配结果',
page: pageNumber => `第 ${pageNumber} 页`
}
}
}
})内置面板也可以由代码控制。调用 search() 会同步更新高亮、当前命中和内置结果列表:
viewer.showSearchPanel()
viewer.search('发票')
viewer.hideSearchPanel()
viewer.toggleSearchPanel(true)搜索选项:
viewer.search('发票', {
caseSensitive: false,
wholeWord: false,
includeInvisibleText: false,
highlightAll: true,
maxResults: 1000
})getSearchSummary() 返回按页聚合的结果,适合业务侧自定义摘要列表。每条摘要包含全局 resultIndex,可传给 goToSearchResult(resultIndex) 跳转。
配置项
interface OFDViewerOptions {
defaultScale?: number | 'fit-width' | 'fit-page'
minScale?: number
maxScale?: number
scaleStep?: number
maxFitWidth?: number
thumbnails?: boolean | {
enabled?: boolean
width?: number
}
search?: boolean | {
mode?: 'api' | 'panel'
defaultOpen?: boolean
width?: number
panel?: {
placeholder?: string
labels?: {
previous?: string
previousAriaLabel?: string
next?: string
nextAriaLabel?: string
clear?: string
clearAriaLabel?: string
noDocument?: string
noSearch?: string
noMatches?: string
page?: (pageNumber: number, item: OFDSearchSummaryItem) => string
}
}
}
stateMessages?: OFDViewerStateMessages
className?: string
}defaultScale:初始缩放比例,或使用适宽/适页模式。minScale/maxScale:缩放上下限,默认0.25到4。scaleStep:zoomIn()/zoomOut()的步进,默认0.1。maxFitWidth:限制fit-width在大屏下的最大阅读宽度。thumbnails:是否启用缩略图,或配置缩略图宽度。search:启用内置搜索面板,或保持 API 模式让业务侧自定义 UI。stateMessages:自定义 empty/loading/error 文案。className:附加到 viewer 根节点的 class。
事件监听
viewer.on('pagechange', ({ pageIndex, previousPageIndex, pageCount }) => {
console.log(pageIndex + 1, previousPageIndex + 1, pageCount)
})
viewer.on('scalechange', ({ scale, previousScale, mode }) => {
console.log(scale, previousScale, mode)
})
viewer.on('statuschange', ({ status, previousStatus, error }) => {
console.log(status, previousStatus, error)
})
viewer.on('printstart', ({ pageCount }) => {
console.log(`正在准备打印 ${pageCount} 页`)
})
viewer.on('printend', ({ result }) => {
console.log(result.pageCount, result.printedAt)
})
viewer.on('printerror', ({ error }) => {
console.error(error)
})
viewer.on('searchchange', ({ state, summary }) => {
console.log(state.keyword, state.total, summary.length)
})
viewer.on('searchresultchange', ({ state, result }) => {
console.log(state.currentIndex, result?.pageNumber)
})事件 payload:
interface OFDViewerEventMap {
load: {
package: OFDPackage
pageCount: number
}
error: {
error: unknown
}
scalechange: {
scale: number
previousScale: number
mode: 'fit-width' | 'fit-page' | null
}
pagechange: {
pageIndex: number
previousPageIndex: number
pageCount: number
}
statuschange: {
status: 'empty' | 'loading' | 'ready' | 'error'
previousStatus: 'empty' | 'loading' | 'ready' | 'error'
error?: unknown
}
printstart: {
package: OFDPackage
pageCount: number
}
printend: {
result: OFDPrintResult
}
printerror: {
error: unknown
}
searchchange: {
state: OFDSearchState
results: OFDSearchResult[]
summary: OFDSearchSummaryItem[]
}
searchresultchange: {
state: OFDSearchState
result: OFDSearchResult | null
previousIndex: number
}
}公开 API
parse(file: ArrayBuffer | Blob): Promise<OFDPackage>
render(pkg: OFDPackage, container: HTMLElement, options?: RenderOptions): Promise<void>
print(pkg: OFDPackage, options?: OFDPrintOptions): Promise<OFDPrintResult>
createOFDViewer(container: HTMLElement, options?: OFDViewerOptions): OFDViewer普通业务集成优先使用 createOFDViewer。parse 和 render 更适合调试、二次封装或高级场景。
OFDPackage 中的 zip 字段被视为内部资源,公开类型中不会绑定到具体 ZIP 实现。
低层 API 示例:
import { parse, render, print } from 'ofd-reader'
const pkg = await parse(file)
await render(pkg, container)
await print(pkg, {
title: 'invoice.ofd'
})Viewer 方法
load(file)clear()destroy()setScale(scale)zoomIn()zoomOut()fitWidth()fitPage()goToPage(pageIndex)previousPage()nextPage()showThumbnails()hideThumbnails()toggleThumbnails(force?)showSearchPanel()hideSearchPanel()toggleSearchPanel(force?)print(options?)search(keyword, options?)clearSearch()nextSearchResult()previousSearchResult()goToSearchResult(index)getScale()getScaleMode()getCurrentPage()getPageCount()getPackage()getStatus()getSearchState()getSearchResults()getSearchSummary()on(event, handler)off(event, handler)
样式
阅读器默认样式需要单独引入:
import 'ofd-reader/style.css'发布样式仅作用于 .ofd-viewer,不包含本地 demo 的 body、#app、工具栏或文件选择器样式。如需定制主题,可在 .ofd-viewer 或通过 className 传入的自定义类上覆盖 --ofd-* 颜色变量,并按需同步设置浅色背景、进度轨道和选择色。
组件不依赖任何 UI 框架,外层容器的宽高由业务页面控制。viewer 会在传入容器内部创建自己的阅读区域和缩略图区域。
浏览器兼容
当前实现面向现代浏览器,同时对 Chrome 69+ 做了基础兼容处理。主要依赖:
BlobURL.createObjectURLFontFaceResizeObserverIntersectionObserver- SVG 渲染能力
- SVG 文本几何能力,用于搜索高亮
推荐运行环境:
- Chrome / Edge 90+
- Firefox 90+
- Safari 15+
基础兼容目标:
- Chrome 69+
Chrome 69+ 可用于基础阅读、缩放、适宽/适页、页码跳转和缩略图导航。由于老版本浏览器在 CSS、字体和滚动行为上仍可能存在细节差异,发布前建议在目标 WebView 或浏览器中做一次视觉回归检查。
如果目标环境缺少 ResizeObserver 或 IntersectionObserver,基础页面渲染仍可工作,但适宽/适页重算、缩略图懒加载等体验可能降级。组件目前不提供内置 polyfill,业务项目可按目标浏览器自行引入。
已知限制
- 暂无正式单元测试。
- 搜索基于已渲染 SVG 文本,不支持模糊搜索、拼音搜索、正则搜索或未渲染页面虚拟化搜索。
- 文本选择尚未实现。
CompositeObject、复杂颜色管理、PatternReflect等长尾能力尚未完整支持。- 签章展示、签名解析和验签属于后期增强能力。
