随着网络的发展,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;
    }

如果你有更好的方法,欢迎留言互相探讨学习!

Logo

前往低代码交流专区

更多推荐