html2canvas、pdf-lib、file-saver将html页面导出成pdf
项目背景
需要根据用户的账号信息,生成一个pdf报告发给客户,要求报告包含echart饼图、走势图等。
方案
使用html2canvas,将页面转成图片,再通过pdf-lib将图片转成pdf文件,最后通过file-saver保存到客户端。
需要注意:由于截长图放到pdf里面,会导致图片被截断,就是可能是某一行的文案或者某一个图被中间截断,所以方案是出一版ui设计稿,把页面、边框、背景都设计好,并且每一页的模块也是需要固定的
这里要求设计出的设计稿是595 * 842的,刚好的标准的A4格式,也就是pdf默认的大小格式,截图生成pdf采取的是分页截图,通过循环每次生成一页图片,放到pdf文件,最终导出文件
使用html2canvas,将页面转成图片
const canvas = await html2canvas(element, {scale: 3, // 提高分辨率height: element.scrollHeight,width: element.scrollWidth,useCORS: true,
})
- scale: 用于提高Canvas的分辨率。这个值通常大于1,以便在Canvas上渲染出更清晰的图像
- height和width: 分别设置为element.scrollHeight和element.scrollWidth,这确保了Canvas的大小与元素的可滚动区域的大小相匹配,即使元素的实际显示区域(即视口)较小
- useCORS: 设置为true,允许html2canvas处理跨域图像。这意味着如果元素中包含了来自不同源的图像,并且这些图像服务器支持CORS,那么这些图像也可以被正确地渲染到Canvas上
通过pdf-lib将图片转成pdf文件并保存
const canvas = await html2canvas(element, {scale: canvasConfig.scale, // 提高分辨率height: element.scrollHeight,width: element.scrollWidth,useCORS: true,
})
const imgData = canvas.toDataURL('image/png')
const pngImage = await pdfDoc.embedPng(imgData)
let page = pdfDoc.addPage([595, 842])
// 把整个页面塞到pdf
page.drawImage(pngImage, {x: 0,y: 0,width: canvas.width,height: canvas.height,
})
const pdfBytes = await pdfDoc.save()
const blob = new Blob([pdfBytes], { type: 'application/pdf' })
saveAs(blob, 'example.pdf')
- 通过pdfDoc.embedPng()将html2canvas生成的图片转成png图片
- pdfDoc.addPage新增一页pdf,宽高定义为标准的A4格式
- 将图片塞到pdf里面,需要注意:y值得从左下角开始计算得,并不是左上角
- 最后将pdf转成blob,通过saveAs保存为pdf文件
完整代码
const canvasConfig = {scale: 3,pageWidth: 595,pageHeight: 842,
}const processCreatePdf = async () => {try {const pdfDoc = await PDFDocument.create([canvasConfig.pageWidth, canvasConfig.pageHeight])for (let pageIndex = 1; pageIndex <= 10; pageIndex++) {const element = document.getElementById(`page-${pageIndex}`)if (!element) { continue }const canvas = await html2canvas(element, {scale: canvasConfig.scale, // 提高分辨率height: element.scrollHeight,width: element.scrollWidth,useCORS: true,})const imgData = canvas.toDataURL('image/png')const pngImage = await pdfDoc.embedPng(imgData)let page = pdfDoc.addPage([canvas.width, canvas.height])// 把整个页面塞到pdfpage.drawImage(pngImage, {x: 0,y: 0,width: canvas.width,height: canvas.height,})}const pdfBytes = await pdfDoc.save()const blob = new Blob([pdfBytes], { type: 'application/pdf' })saveAs(blob, 'example.pdf')} catch (e) {console.error(e)}
}
生成得pdf效果如下:
整体得下效果还行,但是生成得echart饼图比较模糊
试了很多方法都无法解决该问题,最终只能通过调用echart官方api,生成图片,再把图片覆盖到页面中得饼图位置
echart图片模糊解决方案
先贴上代码
// 饼图子组件中、生成组件方法
useImperativeHandle(ref, () => ({getCanvasImg: (pageHeight, scale) => {let { x, y, width, height } = getChartPosition(pageHeight, 'chart-id', scale)x = x * scaley = y * scalewidth = width * scaleheight = height * scaleconst url = getCanvasImg(echartRef, width, height)return { x, y, width, height, url }},
}))// 获取echart在pdf的位置
const getChartPosition = (pageHeight, chartId) => {const chartElement = document.getElementById(chartId) || {offsetLeft: 0,offsetTop: 0,clientHeight: 0,clientWidth: 0,}return {x: chartElement.offsetLeft,y: pageHeight - chartElement.offsetTop - chartElement.clientHeight,width: chartElement.clientWidth,height: chartElement.clientHeight,}
}// 调用官方api生成图片
const getCanvasImg = (ref, width, height) => {const chart = ref.current?.getEchartsInstance()return chart?.getDataURL({type: 'png',pixelRatio: 3,backgroundColor: '#FEFBF9',width: width,height,})
}
- 通过getChartPosition获取echart在pdf页面中的定位,包括x、y、width、height
- 通过echart官方api生成图片,这里width、height规定为echart元素的大小,避免图片变形
- 可以看到x、y、width、height都成以了scale,这个是放大的倍数,和上述html2canvas生成截图的参数一致
在生成页面中,调用方法getCanvasImg动态插入echart
const processCreatePdf = async () => {try {const pdfDoc = await PDFDocument.create([canvasConfig.pageWidth, canvasConfig.pageHeight])for (let pageIndex = 1; pageIndex <= 10; pageIndex++) {const element = document.getElementById(`page-${pageIndex}`)if (!element) { continue }const canvas = await html2canvas(element, {scale: canvasConfig.scale, // 提高分辨率height: element.scrollHeight,width: element.scrollWidth,useCORS: true,})const imgData = canvas.toDataURL('image/png')const pngImage = await pdfDoc.embedPng(imgData)let page = pdfDoc.addPage([canvas.width, canvas.height])// 把整个页面塞到pdfpage.drawImage(pngImage, {x: 0,y: 0,width: canvas.width,height: canvas.height,})await processInsertChartImg(pdfDoc, page, pageIndex)}const pdfBytes = await pdfDoc.save()const blob = new Blob([pdfBytes], { type: 'application/pdf' })saveAs(blob, 'example.pdf')} catch (e) {// eslint-disable-next-line no-consoleconsole.error(e)}
}/** 由于直接生成的canvas的echart不清晰,手动插入echart */
const processInsertChartImg = async (pdfDoc, page, pageIndex) => {const targetRef = pageRefMap[`${pageIndex}`]if (!targetRef) { return }for (let i = 1; i <= 5; i++) {const funcName = `getCanvasImg${i > 1 ? i : ''}`const func = targetRef.current[funcName]if (!func) { continue }const { x, y, url, width, height } = func(canvasConfig.pageHeight, canvasConfig.scale, canvasConfig.pageWidth)if (!url) { continue }const chartImg = await pdfDoc.embedPng(url)page.drawImage(chartImg, { x, y, width, height })}
}
- 多了processInsertChartImg方法用于动态插入echart
- 这里考虑了多张图的场景,不需要的可以简写
效果: