【背景】
昨天财务的同学过来核对新的退款流程,顺便提了下之前开发的电子发票的项目,说上传电子发票PDF文件后,打开无法预览。起初以为是网络的原因,因为项目在当时开发完测试的时候是出现过这种问题的,由于这个问题是偶发性的,当时公司的网络经常出现问题,因此测试同学也就当成网络问题处理了。现在跟财务同学沟通以后,才知道原来已经不能用好几个月了。说只是公司内部的系统不能查看,在用户那边是正常的,所以就没有通知我们修复了。那怎么行,程序员的眼里怎么能容下bug。其实当时这块由于时间关系当时开发的并不理想,后面也一直没有抽出时间来改。
【需求】
先大致讲一下当时的需求吧。原需求是,财务人员导入电子发票后,可通过点击已经导入的电子发票,像图片一样展示 pdf 文件。而用户在B端查看电子发票时是会下载那个PDF文件的,所以也是财务同学说的公司内部系统无法查看,用户那边不影响的结果。
【解决方案】
后台去腾讯云拿到 pdf 文件然后经过 base64 处理后返回给前端,前端通过使用 pdfjs 将 pdf 文件显示出来。这个方案当时在做这个项目的时候就在网上找到了,后面因为 pdfjs 这个插件用起来有点麻烦,当时找到一个 vue-show-pdf 的插件可以直接用,但其实效果不怎么好,只是时间赶,而且是公司内部系统,只有财务人员能够使用,所以就粗糙的上了。
说了一大堆废话,下面说下具体实现的过程以及过程中遇到的一些问题。
首先,pdfjs 在网上找到的其实大多数都是使用 url 去显示的。通过 url 显示 pdf 的话这个比较简单,网上也很多。但是因为我们文件资源是存放在腾讯云的,涉及到前端跨域的问题。因此是由后端直接去腾讯云拿到 pdf 文件通过 base64 处理后返回给前端。前端拿到 base64 字符串后。是不能直接放到 pdfjs 中使用的,但是在pdfjs 的官方文档中,有提到使用 base64 的方法。
因此,pdfjs,实际上是支持传入 经过 base64 处理的字符串,重点就是这个 as an array。
/**
* 函数名:getUint8Array
* 简介:将base64 格式的字符串转换成 uint8Array (pdf.js 无法直接接受base64 格式的参数)
* 参数:base64_string(pdf格式的电子发票经过base64处理的字符串)
* return:Array
*/
getUint8Array(base64Str){
let data = base64Str.replace(/[\n\r]/g, ''); // 替换多余的空格和换行
var raw = window.atob(data);
var rawLength = raw.length;
var array = new Uint8Array(new ArrayBuffer(rawLength));
for (var i = 0; i < rawLength; i++) {
array[i] = raw.charCodeAt(i)
}
return array
},
这里涉及到使用 Uint8Array 和 ArrayBuffer,Uint8Array 类型数组表示的8位无符号整数数组。内容初始化为0。一旦建立,您可以使用对象的方法或使用标准数组索引语法(即使用括号表示法)引用数组中的元素。详细的可以参考 官方文档
ArrayBuffer 类型化数组,类型化数组是JavaScript操作二进制数据的一个接口。最初为了满足JavaScript与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式的背景下诞生的。
charCodeAt() 方法可返回指定位置的字符的 Unicode 编码。这个返回值是 0 - 65535 之间的整数。
将返回的base64 字符串,传入getUint8Array 方法中,返回一个 array ,将这个 array 交由 pdfjs 调用。
/*将解码后的值传给PDFJS.getDocument(),交给pdf.js处理*/
showPdfFile(data) {
let pdfView = this.$refs.pdf;
pdfView.innerHTML = "";
const CMAP_URL = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.0.943/cmaps/';
PDFJS.getDocument({data: data ,cMapUrl: CMAP_URL,cMapPacked: true}).then(pdf => {
pdf.getPage(1).then(page => {
let scale = 1.5; // 默认1.5倍缩放
let viewport = page.getViewport(scale);
if(viewport.height > window.screen.height){
scale = (window.screen.height / viewport.height).toFixed(1);
viewport = page.getViewport(scale);
}
let canvas = document.createElement('canvas');
let canvasContext = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
pdfView.appendChild(canvas);
// 将页面呈现到画布上
let renderContext = {
canvasContext: canvasContext,
viewport: viewport
}
page.render(renderContext);
this.isShowPDF = true;
},err => {
// PDF loading error
console.error(err);
});
});
},
这里有几个地方需要注意一下:
- 电子发票上传后,点击预览发现 pdf 中的中文字符不现实了。网上查询了一下,是因为中文的编码问题,引入pdfjs 的编码文件。在使用的时候传入即可
const CMAP_URL = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.0.943/cmaps/';
PDFJS.getDocument({data: data ,cMapUrl: CMAP_URL,cMapPacked: true}).then()
- 电子发票字体有些模糊不清。尝试调整了缩放比例,可以满足需求。网上有些提到使用了canvas 导致的字体很不清晰的问题,在电子发票上能够清晰显示,之前在做电子合同项目的时候,尝试了相同的方法,文字比较多还涉及了表格,不过效果还行。
let scale = 1.5; // 默认1.5倍缩放
let viewport = page.getViewport(scale);
if(viewport.height > window.screen.height) {
scale = (window.screen.height / viewport.height).toFixed(1);
viewport = page.getViewport(scale);
}
在这里做了一些兼容处理,因为之前的测试数据上传的是一个随便找的 pdf 文件,是高大于宽的,在1.5倍缩放的情况下,超出了屏幕的显示范围,且无法滚动的情况,所以在这稍微做了下简单的处理,改变了一下缩放的比例,保证不会超出屏幕的显示,因为这里的功能主要是预览导入的电子发票,尺寸一般都是差不多的,所以没有做比较复杂的处理了。
另外,因为跟财务同学确认过,发票都是单张的,所以没有做分页的处理了。之前在做电子合同的项目的时候,做过分页的处理,直接贴个代码参考下吧
showPdf(){
let pdfView = document.getElementById('pdf-view');
const CMAP_URL = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.0.943/cmaps/';
pdfjsLib.getDocument({data: this.getUint8Array,cMapUrl: CMAP_URL,cMapPacked: true,})
.then(pdf => {
that.pageCount = pdf.numPages;
for(var i=1;i<pdf.numPages;i++){
pdf.getPage(i).then(page => {
let scale = 1.0;
let viewport = page.getViewport(scale);
let canvas = document.createElement('canvas');
let canvasContext = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.width * 841.229/ 592.28;
pdfView.appendChild(canvas);
// 将页面呈现到画布上
let renderContext = {
canvasContext: canvasContext,
viewport: viewport
}
page.render(renderContext);
},err => {
// PDF loading error
console.error(err);
});
}
});
},
合同这里因为是按A4纸规格设计的,所以做了尺寸的处理。
canvas.width = viewport.width;
canvas.height = viewport.width * 841.229 / 592.28;
最后因为各种原因,合同这块涉及打印的一些问题,并没有采用 pdfjs 的方案。说实话电子合同这块的坑蛮多的!后面会抽空把合同这块踩过的一些坑写一下。