1.后端返回pdf文件流预览
2.pdf圈画(自由笔画)之后保存为新文件上传后端
3.获取pdf当前页的圈画截图
4.弹窗添加文字批注,录音和语音预览功能,选择用户发送
5.查看历史记录,包含批注截图,文字内容,语音信息
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,可能有很多兼容问题
let options = {
width: window.innerWidth,
// backgroundColor: '#eee',
isDrawingMode: true, // 开启绘图模式
originX: "left", //x起点
originY: "top",//y起点
};
this.canvas = new fabric.Canvas(this.$refs.canvasEl, options);
this.pdfh5 = new Pdfh5("#pdfContainer", {
pdfurl: fileURL,//文件流url
zoomEnable: true,
});
圈画时:
this.canvas.setZoom(1);
this.canvas.absolutePan({ x: 0, y: 0 });
let pdfCanvasEditor = document.querySelector(".pdfCanvasEditor");
let height = $(pdfCanvasEditor).height();
let width = $(pdfCanvasEditor).width();
this.canvas.setHeight(height);
this.canvas.setWidth(width);
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);
})
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)
}
}
}
},
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传给后台或者做其他操作
})
}
获取资源报错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
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);
});
}
前端使用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();
});
},