调研
优秀的 PDF 阅读器开源库主要有两个:
- pdf.js:是 Firefox 浏览器内置的 PDF 引擎,由 Mozilla 提供支持,目标是创建一个基于 Web 标准的通用 PDF 解析和渲染平台。基于 HTML5,由 JavaScript 编写,可以直接在前端技术中使用
- pdfium:是 Chromium 浏览器内置的 PDF 引擎,由 C/C++ 编写,前端使用比较复杂,需要编译为 WebAssembly 使用(未实践,可行性未知)
所以在前端想要做 PDF 阅读器的需求,那 pdf.js 基本可以算是唯一的选择
虽然 pdf.js 很强大、功能很丰富,但是其作为一个底层通用库,对实用功能封装比较少,而且其 API 文档比较简陋,在摸索上手阶段需要耗费不少的功夫,实际使用也需要进行大量开发工作以支持业务
一些 pdf.js 有用的资料
- 官方文档:对于内部的核心类来说,开发组提供了一个稍显简陋的文档
- pdf.js 常见问题
- pdfjs viwer 路由参数:一些通过地址栏可以指定的参数,比较少,但是聊胜于无
- 一些基于 pdfjs 开发的第三方项目:听起来可能有不少参考价值,但是实际上绝大多数项目都已经停止维护了
- 第三方查看器使用:看起来最有用的,但是也只是讲了下如何监听 pdf viewer 是否加载完成
所以在实际项目中,都会对 pdf.js 进行封装或二次开发,当然也有一些封装的比较好的项目:
- PDF.js Express:
- 做的很优秀,添加了很多开箱即用的功能
- 丰富完善的文档和使用示例,以及官方技术支持
- 缺点:完整的内容需要购买商业许可,且价格较为昂贵
- 做的很优秀,添加了很多开箱即用的功能
- ZoteroReader:
- 是 Zotero 的 PDF/EPUB/HTML reader and annotator,饱受市场检验
- 不仅支持 PDF,还支持 EPUB 和 HTML snapshot
- 完全开源,团队维护积极(现在每天都有 commit)
- 缺点:无文档、无使用示例,只能看源码知道如何使用
- 但是有一些 AI 总结的文档 写的也不错,还可以进行问答
背景
笔者所在公司现在使用 PDF.js Express,但由于 PDF.js Express 收费昂贵且闭源,现调研转向 ZoteroReader 的可能性和迁移成本
常用功能对比
PDF 阅读
都是基于 pdf.js,差别不大
PDF.js Express
ZoteroReader
PDF 标注
PDF.js Express
提供丰富的标注工具:
- 高亮、下划线、删除线、波浪线、荧光笔、文本、path 路径
- 丰富的图形:矩形、圆形、直线、弧线、箭头…
- 插入:附件、图片
ZoteroReader
有限的标注工具,仅有以下工具:
- 高亮、下划线、文本、path 路径、note 标签、矩形
标注的存储与加载
PDF.js Express
const debouncedSave = debounce(() => {
const annotationManager = webViewerCore?.annotationManager as any
annotationManager
.exportAnnotations({
links: false,
widgets: false,
})
.then((xfdfData: any) => {
if (!libraryId) return
ApiSavePDFNote({
libraryId,
note: xfdfData,
}).catch(() => {
message.error(trans(I18N.libraryV2.saveNoteFailed))
})
})
}, 800)
// 监听笔记修改
useEffect(() => {
if (webViewerCore) {
const { documentViewer, annotationManager } = webViewerCore
// 监听文档加载
documentViewer.addEventListener(
webViewerCore.DocumentViewer.Events.DOCUMENT_LOADED,
() => {
annotationManager.addEventListener('annotationChanged', (annotations, action, { imported }) => {
if (['delete', 'modify', 'add'].includes(action) && !imported) {
if (libraryId) {
debouncedSave()
} else {
bookmark(false)
annotationManager.deleteAnnotations(annotations)
}
}
})
},
{ once: true },
)
}
}, [webViewerCore, libraryId])
// 笔记还原
useEffect(() => {
if (webViewerCore && note) {
const { documentViewer, annotationManager } = webViewerCore
// 监听文档加载
documentViewer.addEventListener(
webViewerCore.DocumentViewer.Events.DOCUMENT_LOADED,
() => {
annotationManager
.importAnnotations(note?.trim())
.then(() => {
message.success({ content: trans(I18N.libraryV2.loadNoteSuccess) })
})
.catch(() => {
message.error({ content: trans(I18N.libraryV2.loadNoteFailed) })
})
},
{ once: true },
)
}
}, [webViewerCore, note])
ZoteroReader
const reader = iframeWindow.createReader({
// 监听笔记修改
onSaveAnnotations: async function (annotations) {
console.log('Save annotations', annotations)
},
onDeleteAnnotations: function (ids) {
console.log('Delete annotations', JSON.stringify(ids))
},
// 其他配置...
})
// 笔记还原
reader.setAnnotations(annotations)
I18N
PDF.js Express
支持
instance.UI.setLanguage(isChinese ? 'zh_cn' : 'en')
ZoteroReader
支持,不过官网只有英文,需要开发者编写中文文案并加载
编写 ftl 文件(中文文案),进行加载。源码
划词弹窗自定义功能
PDF.js Express
// 划词 popup 添加自定义按钮
instance.UI.textPopup.add([
// 翻译
{
type: 'customElement',
render: () => <CustomPopupButton buttonType={EPopupButton.TRANSLATE} instance={instance} onButtonClick={onTranslate} />,
},
// AI 分析
{
type: 'customElement',
render: () => <CustomPopupButton buttonType={EPopupButton.AIANALYSIS} instance={instance} onButtonClick={onAIAnalysis} />,
},
])
ZoteroReader
也支持自定义
iframeWindow.addEventListener('customEvent', (event: any) => {
if (event?.detail?.type !== 'renderTextSelectionPopup') {
return
}
const { append } = event.detail
append('2342423432')
})
跳转到指定位置并高亮区域
PDF.js Express
instance.Core.documentViewer.addEventListener('annotationsLoaded', () => {
const { pos, unit } = qs.parse(location.search) as unknown as {
pos: [number, [number, number, number, number]][]
unit: string
}
if (pos?.[0] && unit === 'percent') {
const [page, range] = pos[0]
const newPage = Number(page)
const newRange = range?.map(item => Number(item))
const pageInfo = instance.Core.documentViewer.getDocument().getPageInfo(newPage) || {
width: 0,
height: 0,
}
const x1 = newRange[0] * pageInfo.width
const y1 = newRange[1] * pageInfo.height
const x2 = newRange[2] * pageInfo.width
const y2 = newRange[3] * pageInfo.height
onNavigate({
tl: [x1, y1],
br: [x2, y2],
page: newPage - 1,
})
}
if (pos?.[0] && unit !== 'percent') {
const [page, range] = pos[0]
onNavigate({
tl: [Number(range[0]), Number(range[1])],
br: [Number(range[2]), Number(range[3])],
page: Number(page),
})
}
})
ZoteroReader
实际就是 PDF 跳转到指定位置 + 在该区域添加一个标注,ZoteroReader 都支持
其他
PDF 内链接跳转、快捷键(如 ctrl+z)、目录预览、搜索、缩放等其他功能都支持
总结
- PDF.js Express 现在使用到的功能,ZoteroReader 都能实现,迁移可行性没问题
- 迁移成本上:
- ZoteroReader 少很多标注工具(如波浪线、图片,详见上文),需要开发,成本:⭐️⭐️⭐️⭐️⭐️
- ZoteroReader 和 PDF.js Express 的标注数据格式不同,需要迁移老数据,成本:⭐️⭐️⭐️
- ZoteroReader 仅有英文,中文需要开发添加,成本:⭐️
- 跳转到指定位置并高亮区域未具体实践,需要看下,成本:⭐️⭐️