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,//文件流urlzoomEnable: 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:0width: canvalWidth, //pdf在画布上面显示的宽度height: canvalHeight, //pdf在画布上面显示的高度//缩放因子,page.getSize()获取到原始pdf实际宽高为595*842,//放在容器里的宽高为当前pdf获取的宽高,计算的缩放比例为595/canvalWidthmultiplier: 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 为声音源,连接到处理节点 recorderthis.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();});
},

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


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部