h5页面pdf预览、canvas自由笔画、截图、文字/recorder语音批注项目总结

一、需求:

1.后端返回pdf文件流预览
2.pdf圈画(自由笔画)之后保存为新文件上传后端
3.获取pdf当前页的圈画截图
4.弹窗添加文字批注,录音和语音预览功能,选择用户发送
5.查看历史记录,包含批注截图,文字内容,语音信息

二、使用的工具/插件总结

1.Pdfh5,用于pdf移动端的pdf文件流预览
2.fabric.js 基于canvas封装的JS工具库
3. pdf-lib 用于获取pdf对象,PDFDocument.load(blob)方法读取pdf,回调参数pdfDoc可进行获取当前页数,当页内容,添加水印,保存等操作
4.html2canvas 纯JS(一般是根据页面容器DOM)对页面进行截图
5.js-audio-recorder 基于navigator.mediaDevices.getUserMedia获取麦克风权限进行录音的插件 (navigator包含有关浏览器的信息)
6.lamejs 前端音频解码工具,一般配合js-audio-recorder使用
7.benz-amr-recorder 纯前端解码、播放、录音、编码 AMR 音频,无须服务器支持,基于 amr.js 和 RecorderJs(AMR格式无法直接绑定audio或者video播放,必须插件播放)

三、思路

1.进入页面获取后端返回的文件流,window.URL.createObjectURL方法创建一个url,作为pdfh5的参数,在页面预览展示
2.fabric.js操作自由笔画结合pdf-lib保存圈画的内容,fabric在预览区域获取当前pdf的页数和位置,在上面覆盖一个大小一模一样的canvas,当在canvs上面进行笔画后,保存时要保证两个画面的大小和位置一模一样(整个项目的难点,首先pdf的翻页是停留在随机位置,做成一次翻一页的话,不符合用户的习惯,获取当前页的时候,这一页不可能正好在预览区域的正中间,为了保证canvas是覆盖在当前这一页的,就需要进行x,y坐标的补偿,其次保存的时候,插件的方法似乎不是从左上角算原点的,预览的时候pdf经过了缩放,所以要获取到pdf的原始尺寸和现在的展示尺寸,计算缩放比例,这样圈画保存的时候才会在对应位置)
3.截图的实现,同样是难点,正常页面的截图,只需要找到要截图的dom,然后就会截图dom里面的内容,这个项目是带了批注的,意思是预览的容器是一个,canvas圈画的是另一个,必须把圈画之后保存为新的内容,才能截图保存(类似于用打印机打印文件,我想把a文件和b文件的内容融合打印出来,你单独打印a,或者b,都只能得到一部分,但是又没法把两份一起放进去打印,只能是把a,b整合到一张纸上,再打印)
4.录音使用js-audio-recorder插件结合lamejs,可能有很多兼容问题

四、问题总结

1.fabric.js在dom挂载后,即mounted中开启绘图模式(也可以后面canvas.isDrawingMode = true来开启)
let options = {
  width: window.innerWidth,
  // backgroundColor: '#eee',
  isDrawingMode: true, // 开启绘图模式
  originX: "left",  //x起点
  originY: "top",//y起点
};
this.canvas = new fabric.Canvas(this.$refs.canvasEl, options);   
2.初始化pdfh5可以允许用户缩放,但是圈画时最好禁止,并且设置缩放级别为1,否则计算比例截图保存的时候,位置偏移,坐标补正,一车bug!!!
this.pdfh5 = new Pdfh5("#pdfContainer", {
    pdfurl: fileURL,//文件流url
    zoomEnable: true,
  });
圈画时:
this.canvas.setZoom(1);
this.canvas.absolutePan({ x: 0, y: 0 });
3.canvas的宽高必须和预览容器的宽高一致
 let pdfCanvasEditor = document.querySelector(".pdfCanvasEditor");
 let height = $(pdfCanvasEditor).height();
 let width = $(pdfCanvasEditor).width();
 this.canvas.setHeight(height);
 this.canvas.setWidth(width);
4.圈画内容的保存,移动端不同设备的像素不同,展示不同,需要动态获取宽高,然后计算缩放比例,还有偏移量的补正,整个项目最难的部分,贴方法就不贴参数了
PDFDocument.load(this.localFileBlob).then(async (pdfDoc) => {
	  //获取当前页的内容,老问题,不确定圈画的部分是当前页还是上一页,实际要做很多判断
	  let page = pdfDoc.getPage(this.pdfh5.currentNum) 
      let embedPng = await pdfDoc.embedPng(
         this.canvas.toDataURL({
           format: "png",
           left: 0, //x方向补充的偏移量
           top:0
           width: canvalWidth, //pdf在画布上面显示的宽度
           height: canvalHeight, //pdf在画布上面显示的高度
           //缩放因子,page.getSize()获取到原始pdf实际宽高为595*842,
           //放在容器里的宽高为当前pdf获取的宽高,计算的缩放比例为595/canvalWidth
           multiplier: 595 / canvalWidth,
         })
       );
       //立即更新画布时,应该使用 renderAll() 方法,
       //需要在一段时间内多次更新画布时,使用 requestRenderAll() 方法来避免重复绘制画布
       this.canvas.requestRenderAll();
       page.drawImage(embedPng);
})
5.pdfh5插件的goto方法有bug,从第二页开始会产生叠加的8px偏移,即第二页偏移8,第三页偏移16…需要在pdfh5.js的依赖包里重写goto方法
goto: function (num) {
      var self = this;
      if (!isNaN(num)) {
        if (self.viewerContainer) {
          self.pages = self.viewerContainer.find('.pageContainer');
          console.log('self.pages=', self.pages)
          if (self.pages) {
            var h = 0;
            var signHeight = 0;
            if (num - 1 > 0) {
              signHeight = self.pages[0].getBoundingClientRect().height;
            }
            self.viewerContainer.animate({
              scrollTop: signHeight * (num - 1)
            }, 300)
          }
        }
      }
    },
6.html2canvas截图,必须是添加圈画内容后,重新渲染成功再开始截,可以用pdfh5的on事件去监听success状态
this.pdfh5.on("success", () => {
    this.getHtml2Canvas();
});
getHtml2Canvas() {
    let pdfContainer = document.querySelector(".pdfContainer");
   	html2canvas(pdfContainer, {
    useCORS: true, //允许跨域
    backgroundColor: "#ececec", //画布背景色,设置null为透明
    width: canvalWidth, //画布宽
    height: canvalHeight, //画布高
    scale: 2, // 处理模糊问题
    dpi: 300, // 处理模糊问题
  }).then((canvas) => {
  	let url = canvas.toDataURL("image/png");//转为url,可绑定img直接查看截图,也可以转为blob传给后台或者做其他操作
  })
}
7.录音插件js-audio-recorder兼容性问题

获取资源报错Uncaught TypeError: Cannot read property ‘getUserMedia’ of undefined,必须https环境才会暴露getUserMedia方法,google上的配置:
第一步:输入地址 chrome://flags/#unsafely-treat-insecure-origin-as-secure
第二步:Insecure origins treated as secure 设置为Enabled

8.每次开始录音都会申请获取录音权限的问题

js-audio-recorder底层还是navigator对象(获取浏览器信息)获取媒体权限的方法navigator.mediaDevices.getUserMedia,
使用navigator对象获取浏览器麦克风权限navigator.mediaDevices.getUserMedia({audio: true}),由于是app嵌入h5页面,内部浏览器实际是webview,不会像google,firefox等浏览器存储用户的权限,所以每次开始录音调用getUserMedia方法都会弹窗提示用户需要麦克风权限(配置项中的audio的值传true,每次都会询问,传false不会询问,但是不能在回调中获取到流数据,因此只能是true),体验不太好,目前还没有解决的办法,贴下插件开始录音的源码 (PC端是不会有弹窗这个问题的)

startRecord(): Promise<{}> {
    if (this.context) {
        // 关闭先前的录音实例,因为前次的实例会缓存少量前次的录音数据
        this.destroyRecord();
    }
    // 初始化
    this.initRecorder();

    return navigator.mediaDevices.getUserMedia({    //这里就是弹窗的原因!!!!!!!!!
        audio: true
    }).then(stream => {
        // audioInput表示音频源节点
        // stream是通过navigator.getUserMedia获取的外部(如麦克风)stream音频输出,对于这就是输入
        this.audioInput = this.context.createMediaStreamSource(stream);
        this.stream = stream;
    }/* 报错丢给外部使用者catch,后期可在此处增加建议性提示
        , error => {
        // 抛出异常
        Recorder.throwError(error.name + " : " + error.message);
    } */).then(() => {
        // audioInput 为声音源,连接到处理节点 recorder
        this.audioInput.connect(this.analyser);
        this.analyser.connect(this.recorder);
        // this.audioInput.connect(this.recorder);
        // 处理节点 recorder 连接到扬声器
        this.recorder.connect(this.context.destination);
    });
}
9.amr格式音频无法直接绑定在audio标签播放

前端使用js-audio-recorder录音,取的格式是wav,取不到amr格式,但是发送消息需要调别人提供的接口,必须amr格式,所以传给后端去转成amr。录完预览的时候,可以直接window.URL.createObjectURL创建url绑定在audio标签播放,但是在历史列表页的预览,是后端取的amr格式文件流,没法直接audio播放,必须要借助插件benz-amr-recorder播放amr格式音频

import BenzAMRRecorder from "benz-amr-recorder";

initBenzAMRRecorder() {
  this.amr = new BenzAMRRecorder();
  this.amr.initWithBlob(this.amrBlob).then(() => {
    this.duration = Math.ceil(this.amr.getDuration());  //获取音频文件时长
    this.amr.play();
  });
},

暂时就这些,后续的问题再补充

你可能感兴趣的:(pdf,javascript,前端,html5)