前言
在开发过程中要求对 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)
防止踩一些坑
坑一:不举荐 pdf-image
在实现服务端将 pdf
文件转换成图片时须要依赖到一些第三方包,一开始应用了 pdf-image
这个包,但在理论转换时产生较多的异样谬误,顺着谬误查看源码后发现其外部须要依赖一些额定的工具,因为其中须要应用 pdfinfo xxx
相干命令,并且其对应的 issue
上也存在着一些相似问题,但都试了试最初还是没有胜利!
因而,更举荐应用 pdf-poppler
其中附带了一个 pdftocairo
的程序能够实现 pdf
到 图片 的转换能力,不过它目前版本反对 Windows 和 Mac OS,如下:
坑二:path.basename not a function
在上述的代码内容中须要获取文件的名称,实际上咱们能够简略间接的应用 Node Api
中 path.basename(path[, suffix])
来达到目标:
然而在程序运行时产生了如下 异样,对应的 代码内容 和 运行后果 如下:
// 配置参数
const options = {
format: 'png',
out_dir: dir,
out_prefix: path.baseName(filePath, path.extname(filePath)), // 产生异样
page: null,
}
这个临时没有找到是什么起因,只能本人简略实现了一个 getFileName
办法用于获取文件的名称。
报错起因:太依赖编辑器的主动提醒,将 basename 输入成 baseName,没错就是 n 和 N 的区别.
坑三:细节
上述内容通过 koa
启动模仿业务服务,因为 业务服务(http://127.0.0.1:4000
) 和 应用服务 (http://127.0.0.1:3000
) 间的端口不统一,因而会产生 跨域,此时能够通过 koa-cors
来解决,值得注意的是有时候的那个业务服务器重启时 koa-cors
可能不起作用。
因为响应的内容间接在 koa
通用中间件中返回,因而,如果你须要反对业务服务提供 动态资源 的拜访能力,就能够通过 koa-static
来实现,值得注意的是,当你通过 koa-static
指定动态文件资源后,如 **`
app.use(koaStatic(‘./static’))**,此时如果你间接通过
http://127.0.0.1:4000/static/pdf/xxx.png 时,那么会失去 **404 Not Found** 的谬误,起因在于 **
koa-static** 是间接把 **/static/** 设置成了 ** 根门路 **,因而正确的拜访门路为:
http://127.0.0.1:4000/pdf/xxx.png`。
成果演示
发票内容不不便展现这里就不间接展现了,只须要关注生成的图片和门路即可:
PDF 下载
这里的下载理论不仅指 pdf
的下载,而是客户端方面所能反对的下载方式,最常见的如下几种:
- a 标签,例如
<a href="xxxx" download="xxx"> 下载 </a>
- location.href,例如
window.location.href = xxx
- window.open,例如
window.open(xxx)
- Content-disposition,例如
Content-disposition:attachment;filename="xxx"
<a>
实现下载
<a>
的 download
属性用于批示浏览器 下载 href 指定的 URL,而不是导航到该资源,通常会提醒用户将其保留为本地文件,如果 download
属性有指定内容,这个值就会在下载保留过程中作为 预填充的文件名,次要是因为如下起因:
- 这个值可能会通过
JavaScript
进行动静批改 - 或者
Content-Disposition
中指定的download
属性优先级高于a.download
这种应该是大家最相熟的形式了,但相熟归相熟,还有一些值得注意的点:
-
download
属性只实用于 同源 URL- 同源 URL 会进行 下载 操作
- 非同源 URL 会进行 导航 操作
- 非同源的资源 仍须要进行下载,那么能够将其转换为
blob: URL
和data: URL
模式
- 若 HTTP 响应头中的
Content-Disposition
属性中指定了一个不同的文件名,那么会优先应用Content-Disposition
中的内容 - HTTP 若 HTTP 响应头中的
Content-Disposition
被设置为Content-Disposition='inline'
,那么在 Firefox 中会优先应用Content-Disposition
的download
属性
动态形式:
<a href="http://127.0.0.1:4000/pdf/2-1.png" download="2.pdf"> 下载 </a>
动静形式:
function download(url, filename){const a = document.createElement("a"); // 创立 a 标签
a.href = url; // 下载门路
a.download = filename; // 下载属性,文件名
a.style.display = "none"; // 不可见
document.body.appendChild(a); // 挂载
a.click(); // 触发点击事件
document.body.removeChild(a); // 移除
}
Blob 形式
if (reqConf.responseType == 'blob') {
// 返回文件名
let contentDisposition = config.headers['content-disposition'];
if (!contentDisposition) {contentDisposition = `;filename=${decodeURI(config.headers.filename)}`;
}
const fileName = window.decodeURI(contentDisposition.split(`filename=`)[1]);
// 文件类型
const suffix = fileName.split('.')[1];
// 创立 blob 对象
const blob = new Blob([config.data], {type: FileType[suffix],
});
const link = document.createElement('a');
link.style.display = 'none';
link.href = URL.createObjectURL(blob); // 创立 url 对象
link.download = fileName; // 下载后文件名
document.body.appendChild(link);
link.click();
document.body.removeChild(link); // 移除暗藏的 a 标签
URL.revokeObjectURL(link.href); // 销毁 url 对象
}
Content-disposition
和 location.href/window.open
实现下载
这看似是三种下载方式,但实际上就是一种,而且还是以 Content-disposition
为准。
Content-Disposition
响应头 批示回复的内容该以何种模式展现,是以 内联 的模式(即网页或页面的一部分)展现,还是以 附件 的模式 下载 并保留到本地,如下:
-
inline
: 是 默认值,示意回复中的音讯领会以页面的一部分或者整个页面的模式展现Content-Disposition: inline
-
attachment
: 设置为此值意味着音讯体应该被下载到本地,大多数浏览器会出现一个 “ 保留为 ” 的对话框,并将filename
的值预填为下载后的文件名Content-Disposition: attachment; filename="filename.jpg"
因而,基于
location.href='xxx'
和window.open(xxx)
的形式能实现下载就是基于Content-Disposition: attachment; filename="filename.jpg"
的模式,又或者说是触发了浏览器自身的下载行为,满足了这个条件,无论是通过a
标签跳转 、location.href 导航、window.open 关上新页面、 间接在地址栏上输出 URL 等都能够实现下载。
H5 挪动端的下载
H5
挪动端针对于 预览 操作而言基于以上的形式都是能够实现,然而 下载 操作可就不同了,因为这是要辨别场景:
- 基于 手机浏览器
- 基于 微信内置浏览器
基于 手机浏览器 的下载方式和上述提到的内容大抵上也是统一的,实质上只有所在的客户端反对下载那就没有问题,然而在 微信内置浏览器 中你应用惯例的下载方式可能达不到预期:
- 在
Android
中应用惯例的下载方式,通常会弹出对话框,询问你是否须要唤醒 手机浏览器 来实现对应资源的下载,局部机型却不会 - 在
IOS
中以上形式都 无奈实现下载 ,因而通常状况下会关上一个新的webview
来提供预览,局部机型在新的页面中反对 长按屏幕 的形式进行保留操作,但并不是所有机型都反对
实质起因是在 微信内置浏览器 中屏蔽任何的 下载链接 ,如 APP 的下载链接、 一般文件 的下载链接 等等。
H5 挪动端的下载还能怎么做?
因为这是 微信内置浏览器 环境对下载性能的屏蔽,因而 不必再思考()基于 微信内置浏览器 来实现下载性能,转而应该思考的是如何实现 间接下载: 想都不敢想
- 判断以后是否是属于 微信内置浏览器 ,若是则帮忙用户主动唤起 手机浏览器 实现下载,但并不是所有机型都反对 唤起 操作,因而最好是提醒应用用户间接通过 手机浏览器 实现下载,为了不便用户,能够实现 一键复制 的性能进行辅助
- 另一种就间接提醒只反对
PC
端下载,放弃对挪动端的下载操作
最初
综上所述,理论在实现 pdf
预览的过程中可能临时没有方法达到完满的形式,特地是针对相似 发票类 的 pdf
文件,仍存在如下的问题:
- 无奈保障
h5
挪动端都具备 下载 性能 - 无奈保障
pdf
预览 时,预览的字体和理论发票 字体 保持一致
现有大部分的预览形式都基于 pdf.js
的形式实现,而 pdf.js
外部通过 PDFJs.getDocument(url/buffer)
的形式基于 文件地址 或 数据流 来获取内容,再通过 canvas
解决渲染 pdf
文件,感兴趣能够去钻研 pdf.js
源码。
pdf.js
带来相干问题就是如果对应的 pdf
文件中蕴含了 pdf.js
中不存在的字体,那么就无奈残缺渲染,另外渲染进去的字体和本来的 pdf
文件字体会存在差别。
针对这两点,目前发现谷歌内置的 pdf
插件仿佛提供了很好的反对,意味着其余浏览器如果蕴含了谷歌相干的插件(如:Edge、QQ Browser),就能够间接基于 <iframe>
的形式实现预览,又或者为了更谨严字体一致性只能通过下载的形式来查看源文件。
实现不了产品的要求怎么办?
例如上述探讨的计划其实无奈满足文章结尾提到的局部要求。产品提出需要的目标也是为了提供更好的用户体验(
),然而这些要求依然要落实到技术上,而技术支持水平如何须要咱们及时反馈(失常状况下
),因而作为开发者你须要提供短缺的内容向产品证实,而后本人再给出一些间接实现的计划(除非你的产品是技术教训
又或者产品本人就给出新的计划
),看是否合乎 第二预期 ,外围就是 正当沟通 + 其余计划 (每个人的处境不同,理论状况兴许 ... 懂得都懂
)。
以上是集体的一些认识和了解,有不当之处,能够在评论区斧正!!!
心愿本文对你有所帮忙!!!