前言
在开发过程中要求对 PDF
类型的发票提供 预览 和 下载 功能,PDF
类型文件的来源又包括 H5 移动端
和 PC 端
,而针对这两个不同端的处理会有些许不同,下文会有所提及。
针对 PDF 预览
的文章不在少数,但似乎都没有提及可能遇到的问题,或是提供对应的具体需求场景下如何选择,因此,本文的核心就是结合实际需求场景下,看看目前各种实现方案到底哪一个更适合,当然希望大家可以在评论区对文中的内容进行斧正,或是提供更优质的方案。
基本要求:
- 支持
pdf 文件
内容的 完整预览 多页 pdf 文件
支持分页查看
PC 端
和移动端
都需支持 下载 和 预览
产品要求:
- PC 端 的预览要支持在 当前页 进行预览
pdf 文件
预览时的字体要 和 实际文件的 字体保证一致性
PDF 预览
先抛开上面的各种要求,咱们先总结下目前实现 PDF
预览的几种常用方式:
- 借助各种类库,基于代码实现预览,如基于
pdfjs-dist
的包 - 直接基于各个浏览器内置的
PDF
预览插件,如<iframe src="xxx">、<embed src="xxx" >
- 服务端将
PDF
文件转换成图片
接下来分别看看以上方案如何实现,以及是否符合上述提供的要求!
<embed> / <iframe>
实现预览
<embed>
标签
<embed>
元素 将外部内容嵌入文档中的指定位置,此内容由 外部应用程序 或 其他交互式内容源(如 浏览器插件)提供。
说简单点,就是使用 <embed>
来展示的资源是完全交由它所在的环境提供的展示功能,即如果当前的应用环境支持这个资源的展示那么就可以正常展示,如果不支持那就无法展示。
使用起来也是非常简单:
<embed type="application/pdf" :src="pdfUrl" width="800" height="600" /> 复制代码
多数现代浏览器已经弃用并取消了对浏览器插件的支持,现在已经不建议使用
<embed>
标签,但可以使用<img>、<iframe>、<video>、<audio>
等标签代替。
<iframe>
标签
基于 <iframe>
的方式和以上差不多,整体效果也一致,这里这就不在额外展示:
<iframe :src="pdfUrl" width="800" height="600" /> 复制代码
值得注意的是,即便使用的是 <iframe>
但实际展开其内层结构后你会发现:
其内部还是 <embed>
标签?这是怎么回事,不是说最好不建议使用 <embed>
吗?
首先来在 caniuse
查看兼容情况,如下:
我们再找一个不支持 <embed>
的浏览器,比如 IE
,来试试效果:
换成 <iframe>
试试,如下:
显然,<embed>
在不兼容的环境直接无法显示,而 <iframe>
是能够正常识别的,只不过 <iframe>
加载的资源无法被 IE
浏览器处理,即本质原因是 IE
浏览器根本就不支持对类似 PDF
等文件的预览,比如当尝试直接在地址栏中输入 http://127.0.0.1:3000/src/assets/2.pdf
时会得到:
因此,通常情况下当浏览器不支持内联 PDF
时,应该提供一个 PDF
的回退链接,即以下载的方式来实现,而这就是 pdfobject 做的事情,实际上它的源码内容比较简单,核心就是 PDFObject 会检测浏览器对内联/嵌入 PDF 的支持,如果支持嵌入,则嵌入 PDF,如果浏览器不支持嵌入,则不会嵌入 PDF,并提供一个指向 PDF 的回退链接,例如在 IE
中的表现:
事实上,这其实只是帮我们少写了一些兼容性的代码而已,也不一定符合大部分人的场景,在这里提到只是因为其与 <embed>
之间存在的联系。
vue3-pdfjs 实现预览
为什么不直接使用 pdfjs-dist
?
pdf.js 几个明显的可吐槽的点:
- 包名称不统一,
npm
上的包名叫pdfjs-dist
,然而在Readme
中自己又称其为pdf.js
- 没有清晰的文档作为指引,只能通过其仓库中的
examples
目录的内容作为参考 - 官方示例不够友好,例如没有提供
vue/react
等相关的示例 - 直接使用需要引入很多文档没有指明的内容
- 有时展示的
pdf
内容文字模糊或缺少部分等 - ...
因此,既然已经有基于 vue/react
封装好的包,这里就直接用来作为演示。
具体使用
安装和使用过程可参考 vue3-pdfjs
,具体 Vue3
示例代码如下:
<script setup lang="ts"> import { onMounted, ref } from 'vue' import { VuePdf, createLoadingTask } from 'vue3-pdfjs/esm' import type { VuePdfPropsType } from 'vue3-pdfjs/components/vue-pdf/vue-pdf-props' // Prop type definitions can also be imported import type { PDFDocumentProxy } from 'pdfjs-dist/types/src/display/api' import pdfUrl from './assets/You-Dont-Know-JS.pdf' const pdfSrc = ref<VuePdfPropsType['src']>(pdfUrl) const numOfPages = ref(0) onMounted(() => { const loadingTask = createLoadingTask(pdfSrc.value) loadingTask.promise.then((pdf: PDFDocumentProxy) => { numOfPages.value = pdf.numPages }) }) </script> <template> <VuePdf v-for="page in numOfPages" :key="page" :src="pdfSrc" :page="page" /> </template> <style> @import '@/assets/base.css'; </style> 复制代码
效果如下:
存在问题
看上去加载正常的 pdf 文档
似乎没啥大问题,来试试加载 pdf 发票
看看,但由于实际发票敏感信息较多,这里就不贴出原本的发票内容,直接来看预览后的发票内容:
- 显然整体发票的 内容缺失得非常多,虽然某些发票大部分能够展示,但如 发票抬头 和 印章 部分可能无法正常显示等
【注意】无法显示完整的内容是因为
pdf.js
是需要一些字体库的支持,如果原 PDF 文件
中部分字体没有匹配到字体库将无法在pdf.js
中显示,而字体库存放在cmaps
文件夹下
- 另外,预览的字体 和 实际的字体 是 不一致 的,而由于发票的特殊性,对字体的一致性是有较大的要求,毕竟如果同一张发票字体不一致会缺乏 规范性 和 合法性(
)被要求字体一致时的说法
常见的解决方案: 解决 pdf.js 无法完全显示 pdf 文件内容的问题,实际上还是根据执行环境的错误信息进行分析,需要强行修改源码内容。
Mozilla Firefox(火狐浏览器)
Mozilla Firefox 内置的 PDF 阅读器实际就是 pdf.js
,你可以直接用火狐浏览器预览一下 pdf
文件,如下:
并且大多基于 pdf.js
二次封装的库 vue-pdf、vue3-pdfjs
等在预览 pdf
文件的发票时通常无法显示完整内容,需要或多或少的涉及对源码的更改,而在 Firefox
中内置的 pdf.js
却能够完整的显示对应的 pdf
文件的内容。
PDF
转 图片
实现预览
这种方式应该不用多说了,核心是服务端在响应 pdf
文件时,先转换成图片类型再返回,前端直接展示具体图片内容即可。
具体实现
下面通过用 node
来模拟:
const pdf = require('pdf-poppler') const path = require('path') const Koa = require('koa') const koaStatic = require('koa-static') const cors = require('koa-cors') const app = new Koa() // 跨域 app.use(cors()) // 静态资源 app.use(koaStatic('./server')) function getFileName(filePath) { return filePath .split('/') .pop() .replace(/\.[^/.]+$/, '') } function pdf2png(filePath) { // 获取文件名 const fileName = getFileName(filePath); const dir = path.dirname(filePath); // 配置参数 const options = { format: 'png', out_dir: dir, out_prefix: fileName, page: null, } // pdf 转换 png return pdf .convert(filePath, options) .then((res) => { console.log('Successfully converted !') return `http://127.0.0.1:4000${dir.replace('./server','')}/${fileName}-1.png` }) .catch((error) => { console.error(error) }) } // 响应 app.use(async (ctx) => { if(ctx.path.endsWith('/getPdf')){ const url = await pdf2png('./server/pdf/2.pdf') ctx.body = { url } }else{ ctx.body = 'hello world!' } }) app.listen(4000) 复制代码