你们好,我是后端妹。
序言
在开发过程中要求对PDF类型的收据提供预览和下载功能,PDF类型文件的来源又包括H5联通端和PC端,而针对这两个不同端的处理会有些许不同,下文会有所提到。
针对PDF预览的文章不在少数,但其实都没有提到可能碰到的问题linux命令行和shell脚本编程宝典,或是提供对应的具体需求场景下怎么选择,为此,本文的核心就是结合实际需求场景下,瞧瞧目前各类实现方案究竟哪一个更适宜,其实希望你们可以在评论区对文中的内容进行斧正,或是提供更优质的方案。
基本要求:
产品要求:
PDF预览
先抛掉里面的各类要求,俺们先总结下目前实现PDF预览的几种常用方法:
接出来分别瞧瞧以上方案怎样实现,以及是否符合上述提供的要求!
/实现预览标签
元素将外部内容嵌入文档中的指定位置,此内容由外部应用程序或其他交互式内容源(如浏览器插件)提供。
说简单点,就是使用来展示的资源是完全交由它所在的环境提供的展示功能,即若果当前的应用环境支持这个资源的展示这么就可以正常展示,假若不支持那就没法展示。陌陌搜索公众号:构架师手册,回复:构架师发放资料。
使用上去也是十分简单:
<embed
type="application/pdf"
:src="pdfUrl"
width="800"
height="600" />
多数现代浏览器早已弃用并取消了对浏览器插件的支持,如今已然不建议使用标签,但可以使用
、、、等标签取代。
标签
基于的方法和以上差不多,整体疗效也一致,这儿这就不在额外展示:
<iframe
:src="pdfUrl"
width="800"
height="600" />
值得注意的是,虽然使用的是但实际展开其外层结构后你会发觉:
其内部还是标签?这是如何回事,不是说最好不建议使用吗?
首先来在caniuse[2]查看兼容情况,如下:
我们再找一个不支持的浏览器,例如IElinux 删除文件夹,来试试疗效:
换成试试,如下:
其实,在不兼容的环境直接未能显示,而是才能正常辨识的,只不过加载的资源难以被IE浏览器处理,即本质缘由是IE浏览器根本就不支持对类似PDF等文件的预览,例如当尝试直接在地址栏中输入:3000/src/assets/2.pdf时会得到:
为此,一般情况下当浏览器不支持内联PDF时,应当提供一个PDF的回退链接,即以下载的形式来实现,而这就是pdfobject[3]做的事情linux怎么安装字体库,实际上它的源码内容比较简单,核心就是PDFObject会检查浏览器对内联/嵌入PDF的支持,假如支持嵌入,则嵌入PDF,假如浏览器不支持嵌入,则不会嵌入PDF,并提供一个指向PDF的回退链接,比如在IE中的表现:
事实上,这似乎只是帮我们少写了一些兼容性的代码而已,也不一定符合大部份人的场景,在这儿提及只是由于其与之间存在的联系。
vue3-pdfjs实现预览为何不直接使用pdfjs-dist?
pdf.js[4]几个显著的可吐槽的点:
为此,既然早已有基于vue/react封装好的包,这儿就直接拿来作为演示。
具体使用
安装和使用过程可参考vue3-pdfjs[5],具体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>
<VuePdf v-for="page in numOfPages" :key="page" :src="pdfSrc" :page="page" />
</template>
@import '@/assets/base.css';
疗效如下:
存在问题
看起来加载正常的pdf文档虽然没啥大问题,来试试加载pdf收据瞧瞧,但因为实际收据敏感信息较多,这儿就不贴出先前的收据内容,直接来看预览后的收据内容:
【注意】无法显示完整的内容是由于pdf.js是须要一些字体库的支持,假如原PDF文件中部份字体没有匹配到字体库将难以在pdf.js中显示,而字体库储存在cmaps文件夹下
常见的解决方案:解决pdf.js未能完全显示pdf文件内容的问题[6],实际上还是依据执行环境的错误信息进行剖析,须要强行更改源码内容。
MozillaFirefox(傲游浏览器)
MozillaFirefox外置的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[7]这个包,但在实际转换时发生较多的异常错误,沿着错误查看源码后发觉其内部须要依赖一些额外的工具,由于其中须要使用pdfinfoxxx相关命令,但是其对应的issue[8]上也存在着一些类似问题,但都试了试最后还是没有成功!另外,搜索公众号Linux就该这样学后台回复“猴子”,获取一份惊喜礼包。
为此,更推荐使用pdf-poppler[9]其中附送了一个pdftocairo的程序可以实现pdf到图片的转换能力,不过它目前版本支持Windows和MacOS,如下:
坑二:path.basenamenotafunction
在上述的代码内容中须要获取文件的名称,实际上我们可以简单直接的使用NodeApi中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启动模拟业务服务,因为业务服务(:4000)和应用服务(:3000)间的端口不一致,因而会形成跨域,此时可以通过koa-cors来解决,值得注意的是有时侯的那种业务服务器重启时koa-cors可能不起作用。
因为响应的内容直接在koa通用中间件中返回,因而,假如你须要支持业务服务提供静态资源的访问能力,就可以通过koa-static来实现,值得注意的是,当你通过koa-static指定静态文件资源后,如**app.use(koaStatic('./static'))**,此时若果你直接通过:4000/static/pdf/xxx.png时,这么会得到404NotFound的错误,缘由在于koa-static是直接把/static/设置成了根路径,因而正确的访问路径为::4000/pdf/xxx.png。
疗效演示
收据内容不便捷展示这儿就不直接展示了,只须要关注生成的图片和路径即可:
PDF下载
这儿的下载实际除了指pdf的下载,而是顾客端方面所能支持的下载形式,最常见的如下几种:
实现下载
的download属性用于指示浏览器下载href指定的URLlinux怎么安装字体库,而不是导航到该资源,一般会提示用户将其保存为本地文件,假如download属性有指定内容,这个值都会在下载保存过程中作为预填充的文件名,主要是由于如下诱因:
这些应当是你们最熟悉的形式了,但熟悉归熟悉,还有一些值得注意的点:
若HTTP响应头中的Content-Disposition[12]属性手指定了一个不同的文件名,这么会优先使用Content-Disposition中的内容
HTTP若HTTP响应头中的Content-Disposition被设置为Content-Disposition='inline',这么在Firefox中会优先使用Content-Disposition的download属性
静态方法:
<a style='color:#0000CC;font-size:16px;' 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响应头指示回复的内容该以何种方式展示,是以内联的方式(即网页或页面的一部份)展示,还是以附件的方式下载并保存到本地,如下:
为此,基于location.href='xxx'和window.open(xxx)的形式能实现下载就是基于Content-Disposition:attachment;filename="filename.jpg"的方式,又或则说是触发了浏览器本身的下载行为,满足了这个条件,无论是通过a标签跳转、location.href导航、window.open打开新页面、直接在地址栏上输入URL等都可以实现下载。
H5联通端的下载
H5联通端针对于预览操作而言基于以上的方法都是可以实现,而且下载操作可就不同了,由于这是要分辨场景:
基于手机浏览器的下载形式和上述提及的内容大致上也是一致的,本质上只要所在的顾客端支持下载那就没有问题,但是在陌陌外置浏览器中你使用常规的下载形式可能达不到预期:
本质缘由是在陌陌外置浏览器中屏蔽任何的下载链接,如APP的下载链接、普通文件的下载链接等等。
H5联通端的下载能够怎样做?
因为这是陌陌外置浏览器环境对下载功能的屏蔽,因而不用再考虑(~~想都不敢想~~)基于陌陌外置浏览器来实现下载功能,转而应当考虑的是怎样实现间接下载:
BD272F44.gif最后
综上所述,实际在实现pdf预览的过程中可能暂时没有办法达到完美的形式,非常是针对类似收据类的pdf文件,仍存在如下的问题:
现有大部份的预览方法都基于pdf.js的方法实现,而pdf.js内部通过PDFJs.getDocument(url/buffer)的方法基于文件地址或数据流来获取内容,再通过canvas处理渲染pdf文件,感兴趣可以去研究pdf.js源码。
pdf.js带来相关问题就是假如对应的pdf文件中包含了pdf.js中不存在的字体,这么就难以完整渲染,另外渲染下来的字体和先前的pdf文件字感受存在差别。
针对这两点,目前发觉微软外置的pdf插件虽然提供了挺好的支持,意味着其他浏览器假如包含了微软相关的插件(如:Edge、Browser),就可以直接基于的形式实现预览,又或则为了更严谨字体一致性只能通过下载的形式来查看源文件。
实现不了产品的要求如何办?
比如上述阐述的方案虽然未能满足文章开头提及的部份要求。产品提出需求的目的也是为了提供更好的用户体验(~~正常情况下),并且这种要求依然要落实到技术上,而技术支持程度怎样须要我们及时反馈(除非你的产品是技术经验~~),因而作为开发者你须要提供充足的内容向产品证明,之后自己再给出一些间接实现的方案(又或则产品自己就给出新的方案),看是否符合第二预期,核心就是合理沟通+其他方案(每位人的境况不同,实际情况似乎...懂得都懂)。
参考资料
[1]
pdfjs-dist:
[2]
caniuse:
[3]
pdfobject:
[4]
:%3A%2F%%2Fmozilla%2Fpdf.js
[5]
:%3A%2F%2F%2Fpackage%2Fvue3-pdfjs
[6]
解决pdf.js未能完全显示pdf文件内容的问题:
[7]
pdf-image:
[8]
issue:
[9]
:%3A%2F%2F%2Fpackage%2Fpdf-poppler
[10]
blob:URL:
[11]
data:URL:
[12]
Content-Disposition:
假如觉得有帮助的话,别忘了给后端妹点个
。感谢你们哦!!!
假如认为这篇文章还不错,来个【分享、点赞、在看】三连吧,让更多的人也听到~
后端妹拍了拍你说:
记得戳小花花哦~
近来笔试BAT,整理一份笔试资料《前端笔试BAT通关指南》,覆盖了后端技术、CSS、JavaScript、框架、数据库、数据结构等等。