随着网络的发展,PC端的网站已不能满足人们的需求,人们更喜欢采用移动端进行业务操作。最近公司要求把PC端网站的订单合同签署功能移植到微信端,而不再局限于PC端操作。
对于这样的要求,我们需要了解的是订单合同,协议书之类的一般都属于不可以任意修改的文件(PDF),这样的文件,现在的浏览器基本都支持直接访问的。但是遗憾的是,移动端并不支持直接访问,这样我们需要对PDF文件进行解析处理。首先我们考虑到通过服务器访问到PDF文件,传递到前端,再由前端进行解析处理。
这里前端框架采用vue,vue中有整合到第三方pdf解析库pdfjs-dist
1、安装 pdfjs-dist
npm install pdfjs-dist --save
2、创建pdf组件 pdf-component.vue
<template>
<div>
<canvas v-for="page in pages" :id="'the-canvas'+page" :key="page"></canvas>
</div>
</template>
<script>
import PDFJS from 'pdfjs-dist'
const Base64 = require('js-base64').Base64
export default {
name: 'pdf-component',
data () {
return {
title: '查看合同',
pdfDoc: null,
pages: 0
}
},
methods: {
_renderPage (num) {
this.pdfDoc.getPage(num).then((page) => {
let canvas = document.getElementById('the-canvas' + num)
let ctx = canvas.getContext('2d')
let dpr = window.devicePixelRatio || 1
let bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1
let ratio = dpr / bsr
let viewport = page.getViewport(screen.availWidth / page.getViewport(1).width)
canvas.width = viewport.width * ratio
canvas.height = viewport.height * ratio
canvas.style.width = viewport.width + 'px'
canvas.style.height = viewport.height + 'px'
ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
let renderContext = {
canvasContext: ctx,
viewport: viewport
}
page.render(renderContext)
if (this.pages > num) {
this._renderPage(num + 1)
}
})
},
_loadFile (url) {
PDFJS.getDocument(url).then((pdf) => {
this.pdfDoc = pdf
this.pages = this.pdfDoc.numPages
this.$nextTick(() => {
this._renderPage(1)
})
})
}
},
mounted () {
document.title = this.title
let url = Base64.decode(this.$route.query.url)
this._loadFile(url)
}
}
</script>
<style scoped>
canvas {
display: block;
border-bottom: 1px solid black;
}
</style>
3、路由配置
{
path: '/pdf',
name: 'pdf',
component: () => import('@/components/public/pdf-component'),
meta: {
}
}
4、点击跳转事件
_loadingPdf (id) {
let url = '/api/orders/findPdfById/' + id
this.$router.push({ name: 'pdf', query: { url: Base64.encode(url) } })
}
整个前端加载pdf功能已全部做完。代码量不多,在测试中一个1M左右的pdf文件在页面渲染时间大约在3秒左右,如果有多页的则需要更多时间,效率上并不是很理想。
在后续的测试中发现了该pdf文件预览功能有个致命的bug,第三方的签证合同印章竟然没有被渲染出来,对于一般的pdf文件来说,该组件完全可以胜任了,但是对于合同、协议书之类的需要第三方电子签证的pdf来说就不能满足使用了。
分析:电子印章属于授权类签证,应该是有特定的属性的?后经过跟第三方签证公司技术人员沟通得知,该公司电子印章需要支持电子签证授权的库才能解析
也就是说我们之前做的组件完全没用处了?pdfjs-dist不支持第三方签证授权解析?秉着有问题就要求索的精神,在网上遍历了一番,发现早有人提出了这样的疑问,也有相应的解析说明。原来是该开发者默认屏蔽了电子签证,按照网上所说,打开了pdfjs-dist框架文件,在build文件夹下有3个文件:pdf.js、pdf.worker.entry.js、pdf.worker.js。在pdf.worker.js这个pdf业务解析库中搜索到了以下代码段:
if (data.fieldType === 'Sig') {
_this2.setFlags(_util.AnnotationFlag.HIDDEN);
}
通过对该段代码进行注释,发现原来不能渲染的电子章被成功渲染出来,问题得到了初步解决。但是这种方法很明显不现实,我们不可能去更改第三方库的,而且就算可以修改,也不能这样做,不能每次发布项目的时候都要先去修改然后再rebuild,这样太繁琐了,特别是要发布的项目比较多的时候,很容易就忘记了,还有就是使用容器技术的时候,采用自动化构建发布,不可能发布完之后再进去容器进行修改,这样不现实。
so,第一种方案就paas了,那么前端还有什么库可以解析电子签章呢?很遗憾,目前好像真没有。那么我们需要考虑是否需要在服务器端进行pdf文件处理了。
思路: 通过获取云端pdf文件,解析成图片,然后返回给前端,前端只需要展现图片即可,不需要做任何操作。
这里采用的是java语言,对pdf转图片的库有很多,我们采用比较多人使用,而且性能比较好的进行测试,这里我们使用一个叫pdf-renderer的jar包
1、引入pdf-renderer.jar
<!-- https://mvnrepository.com/artifact/org.swinglabs/pdf-renderer -->
<dependency>
<groupId>org.swinglabs</groupId>
<artifactId>pdf-renderer</artifactId>
<version>1.0.5</version>
</dependency>
2、编写pdf转图片方法
/**
* Created by hou on 21/8/18.
*/
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.OutputStream;
import com.sun.pdfview.*;
/**
*附件
*/
@Controller
@RequestMapping(value = "api/orders")
public class AttachmentController {
private static final String DefaultFileType = "image/jpeg";
private static final String PDF = "application/pdf";
/**
* PDF转图片,注意content-type
*
* @param id
* @return
* @throws Exception
*/
@RequestMapping(value = "findPdf2ImgById/{id}", method = RequestMethod.GET)
public void findPdf2ImgById(@PathVariable(value = "id") String id, HttpServletResponse response)
throws Exception {
response.setContentType(DefaultFileType);
OutputStream ouputStream = response.getOutputStream();
//TODO 数据来源(可以从数据库、云端等读取文件流)
File file = new File("d://xxxxx.pdf" );
RandomAccessFile raf = new RandomAccessFile(file, "r" );
FileChannel channel = raf.getChannel();
ByteBuffer buf = channel.map(FileChannel.MapMode.READ_ONLY, 0 , channel.size());
PDFFile pdffile = new PDFFile(buff);
System.out.println("页数: " + pdffile.getNumPages());
for ( int i = 1 ; i <= pdffile.getNumPages(); i++) {
// draw the first page to an image
PDFPage page = pdffile.getPage(i);
// get the width and height for the doc at the default zoom
Rectangle rect = new Rectangle( 0 , 0 , ( int ) page.getBBox()
.getWidth(), (int ) page.getBBox().getHeight());
// generate the image
Image img = page.getImage(rect.width, rect.height, // width &
// height
rect, // clip rect
null , // null for the ImageObserver
true , // fill background with white
true // block until drawing is done
);
BufferedImage tag = new BufferedImage(rect.width, rect.height, BufferedImage.TYPE_INT_RGB);
tag.getGraphics().drawImage(img, 0 , 0 , rect.width, rect.height, null );
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(ouputStream);
encoder.encode(tag); // JPEG编码
ouputStream.close();
}
}
}
代码初步编写完成,在本地通过编译,但是使用ant编译时却报找不到com.sun.image.codec.jpeg,明明在本地编译没有报错,也能找到对应的包,但是用ant却没有。原来是在java7以后oracle公司就不允许调用sun的相应jar包了,com.sun.image.codec.jpeg.*成为了sun的内部包了。那么就不能使用该包下的类了,对代码做如下修改:
// JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(ouputStream);
// encoder.encode(tag); // JPEG编码
ImageIO.write(tag, "png", ouputStream);
2、前端新建pdf-img.vue组件
<template>
<div>
<img v-if="imgSrc" :src="imgSrc">
</div>
</template>
<script>
const Base64 = require('js-base64').Base64
export default {
data () {
return {
title: '查看合同',
imgSrc: '',
pages: 0
}
},
methods: {
},
mounted () {
document.title = this.title
this.imgSrc = Base64.decode(this.$route.query.url)
}
}
</script>
<style scoped>
img {
display: block;
border-bottom: 1px solid black;
}
</style>
3、路由指向新的组件
{
path: '/pdf',
name: 'pdf',
component: () => import('@/components/public/pdf-img.vue'),
meta: {
}
}
再次测试,发现还是没有电子印章,说明该库也不支持电子章解析,那么还有其他库支持吗?有,那就是采用pdfbox.jar
1、引入pdfbox.jar
<!-- https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.11</version>
</dependency>
2、修改方法为
/**
* PDF转图片,注意content-type
*
* @param id
* @return
* @throws Exception
*/
@RequestMapping(value = "findPdf2ImgById/{id}", method = RequestMethod.GET)
public void findPdf2ImgById(@PathVariable(value = "id") String id, HttpServletResponse response)
throws Exception {
response.setContentType(DefaultFileType);
OutputStream ouputStream = response.getOutputStream();
//TODO 数据来源(可以从数据库、云端等读取文件流)
File file = new File("d://xxxxx.pdf" );
// PDFFile pdffile = new PDFFile(buff);
PDDocument pdf = PDDocument.load(file); //加载方式有多种,可以是File、byte[]、inputStream、ByteBuffer等等
PDFRenderer pdfRenderer = new PDFRenderer(pdf);
PDPageTree pageTree = pdf.getPages();
int pageCounter = 0;
for(PDPage page : pageTree){
float width = page.getCropBox().getWidth();
float scale = 1.0f; // 提高图片质量
if(width >720) scale = 720/width;
BufferedImage bim = pdfRenderer.renderImage(pageCounter,scale, ImageType.RGB);
ImageIO.write(bim, "png", ouputStream);
ouputStream.close();
}
}
再次测试发现电子印章已经正常显示,同时页面渲染速度也大大提高了,只是渲染出来的图片在Android显示比较模糊,ios上没有影响。
转成高质量图片? 则使用DPI方式,代码修改如下:
// BufferedImage bim = pdfRenderer.renderImage(pageCounter,scale, ImageType.RGB);
BufferedImage bim = pdfRenderer.renderImageWithDPI(pageCounter,144, ImageType.RGB);
注:dpi值越高,则图片越清晰,转换所需要的时间也就越长
待优化的地方:
1. 前端:由于图片没有设置长宽,是有多大就显示多大,对于高清晰图片来说,手机查看时体验是非常差的,所以可以设置为双指滑动操作放大缩小(代码后续补上)
2. 服务端:该方法只支持只有一页的pdf文件,如果多于1页,则最终只展现最后一页,可以把方法修改成如下:
/**
* PDF转图片,注意content-type
* @param id
* @return
* @throws Exception
*/
@RequestMapping(value = "findPdf2ImgById/{id}", method = RequestMethod.GET)
public void findPdf2ImgById(@PathVariable(value = "id") String id, HttpServletResponse response)
throws Exception {
response.setContentType(DefaultFileType);
OutputStream ouputStream = response.getOutputStream();
//TODO 数据来源(可以从数据库、云端等读取文件流)
File file = new File("d://xxxxx.pdf" );
// PDFFile pdffile = new PDFFile(buff);
PDDocument pdf = PDDocument.load(file); //加载方式有多种,可以是File、byte[]、inputStream、ByteBuffer等等
PDFRenderer pdfRenderer = new PDFRenderer(pdf);
PDPageTree pageTree = pdf.getPages();
BufferedImage bi= null;
for(int i = 0; i < pdf.getNumberOfPages(); i++){
float width = pageTree.get(i).getCropBox().getWidth();
float scale = 1.0f; // 提高图片质量
if(width >720) scale = 720/width;
BufferedImage bim = pdfRenderer.renderImage(i,scale, ImageType.RGB);
if(i == 0) {
bi = bim;
}else{
bi = mergeImg(bi,bim);
}
}
ImageIO.write(bi, "png", ouputStream);
ouputStream.close();
}
/**
* 合并图片方法
* @param priorImg
* @param afterImg
* @return combined
*/
private static BufferedImage mergeImg(BufferedImage priorImg, BufferedImage afterImg) {
BufferedImage combined = new BufferedImage(
priorImg.getWidth(),
priorImg.getHeight() + afterImg.getHeight(),
BufferedImage.TYPE_INT_RGB);
Graphics g = combined.getGraphics();
g.drawImage(priorImg, 0, 0, null);
g.drawImage(afterImg, 0, priorImg.getHeight(), null);
g.dispose();
return combined;
}