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])
// 把整个页面塞到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')
} 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 * scale
y = y * scale
width = width * scale
height = height * scale
const 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])
// 把整个页面塞到pdf
page.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-console
console.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
- 这里考虑了多张图的场景,不需要的可以简写
效果: