简介
之前有个要求前端导PDF的需求,当时稍微谷歌了一下,用了JSPDF + html2Canvas导出PDF的方案,遇到一堆问题,就写在这里, 希望可以帮到同样遇到坑的小伙伴~
遇到的问题
- 导出图片报错,百度说是跨域问题
- 导的PDF背景为黑
- PDF模糊
- 导出的PDF没有页眉页脚
- 导出的PDF的表格在分页的地方被截断了
1.导出的图片有跨域问题
百度的解决方案有很多,但我试了对我都没用... 因为需要导出的图片不多,最终我选择把图片下载到本地然后转乘base64编码 传到前端,虽然麻烦了点,其他解决方案都无效我也很绝望啊... php代码如下
php代码: 图片链接转base64
function downImgToBase64($images_root_dir) {
// 设置图片存储目录
$image_append_dir = 'templates/tempImg/' . rand(1, 999999). '/'; // 图片存储目录的前半部分
defined("ROOT_PATH") || define("ROOT_PATH", preg_replace('/webapp(.*)/', '', str_replace('\\', '/', __FILE__)));
$images_root_dir = ROOT_PATH . $image_append_dir; //图片要存储的绝对路径 这部分每个人不一定相同
if (!file_exists($images_root_dir)) {
mkdir($images_root_dir, 0777, true);
chmod($images_root_dir, 0777);
}
$imgUrl = 'https://www.baidu.com/img/bd_logo1.png'; // 测试用图
$imgPath = substr($imgUrl, -13, 8); // 图片下载下来的文件名
$path = curl_file_get_contents($imgUrl, $images_root_dir . $imgPath); // 图片下载到本地 返回存储路径
$base64Img = $gg->base64EncodeImage($path); // 将图片转成base64编码
unlink($path);
return $base64Img;
}
// 下载图片导本地 $url: 图片链接 $path: 图片存储本地路径
function curl_file_get_contents($url,$path)
{
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_TIMEOUT, 60); //允许执行的最长秒数
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30); //在发起连接前等待的时间,如果设置为0,则无限等待
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);// 将curl_exec()获取的信息以字符串返回,而不是直接输出。
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); //这个是重点,规避ssl的证书检查。
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); // 跳过host验证
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727)");
$res = curl_exec($ch);
$error = curl_error($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if ($info['http_code'] != 200) {
throw new \Exception($url . ' http code: ' . $info['http_code'] . $error);
}
$fp = fopen($path,'wb');
fwrite($fp, $res);
fclose($fp);
return $path;
}
// 图片转base64 $file为图片存储本地路径
function base64EncodeImage ($file) {
$type = getimagesize( $file ); //取得图片的大小,类型等
if ($type === false) {
$error = error_get_last();
throw new \Exception($error['message']);
}
$img_type = $type['mime'];
$file_content = base64_encode( file_get_contents( $file ) );
$img = 'data:image/' . $img_type . ';base64,' . $file_content; //合成图片的base64编码
return $img;
}
导的PDF背景为黑
- html2Canvas那里可以设置参数
background: '#FFFFFF';backgroundColor: null;
,详见我最后的htmlToPdf.js函数 - 将导出的dom背景设置为白 我的vue代码:
<tbody id="pdfDomId" style="background: #FFFFFF"></tbody>
PDF模糊
这是另一个小伙伴遇到的问题,我的需求对清晰度要求不高。
解决方案是将htmlToPdf.js里的 scale
设置为2,scale
越大PDF越清晰,但同时PDF的文件大小也会变大
导出的PDF没有页眉页脚
主要参考的文档是 zhuanlan.zhihu.com/p/35753622
基本逻辑就是通过计算在分页处新增一个div元素,样式内容自定义。详见htmlToPdf.js啦
有个坑是因为我的PDF含图片, 随着图片的加载,元素的高度会变化,总是不能正确分页 而且vue的this.$nextTick(() => { getPdf(id, title) }
也没用,坑了我好久,最终把导出PDF函数放到window.onload里解决。window.onload = () => { getPdf(id, title) }
导出的PDF的表格在分页的地方被截断了
也是参考 zhuanlan.zhihu.com/p/35753622
node对象下的每一个一级子元素都被当作一个不可被分割的整体,遍历这些子元素,如果某个元素距离body的高度是pageH的倍数x,当加上该元素自身的高度时是pageH的倍数x+1,那么说明x这一页放不下该元素,该元素需要被放在x+1页上,利用marginTop设置一个留白区域即可。
我的页面布局vue代码为:
<tbody id="goodsCraftPdfDom" style="background: #FFFFFF">
<el-card :body-style="{ padding: '0px' }">
{{ childNodes1... }}
</el-card>
<el-card :body-style="{ padding: '0px' }">
{{ childNodes2... }}
</el-card>
</tbody>
代码 htmlToPdf.js
// 下面两个package要单独安装
import html2Canvas from 'html2canvas'
import JsPDF from 'jspdf'
/**
* 这个函数导出的pdf可以基本完美分页,并解决文字/表格被分页截断的问题
* note1: 每个表格要各自作为一个childNodes,所以整个页面不能用el-col包起来,那样的话 整个页面就是一个整体了
* note2:下载有图片的pdf的函数请在window.onload()中执行,否则图片大小变化会导致分页不准确. (页面有多个window.onload()时只能执行一个,所以不能把window.onload()写在本函数里)
* @param id: 下载dom的ID
* @param title: 下载pdf文件名
*/
export default {
install(Vue, options) {
Vue.prototype.getPdf = function (id, title) {
const WGUTTER = 15 // 横向页边距
let deleteNullPage = 0 // 这完全是我加的特殊逻辑了 不知道为啥生成canvas后的高度会比现在的高度高一些 就会多一个空页 用这个筛选掉 可能最后一页的页尾没了 不过还好吧...
const SIZE = [595.28, 841.89] // a4宽高
let node = document.querySelector(`#${id}`)
let nodeH = node.clientHeight
let nodeW = node.clientWidth
const pageH = nodeW / (SIZE[0] - 2 * WGUTTER) * SIZE[1]
let modules = node.childNodes
let pageFooterH = 10 // 10为页尾的高度
this.addPageHeader(node, node.childNodes[0]); //添加页头
// console.log(node.clientHeight, node.clientWidth, pageH)
for (let i = 0, len = modules.length; i < len; i++) {
len = modules.length // 因为加了页头页尾后modules.length会变 这里更新一下len
let item = modules[i]
if (typeof item.clientHeight === "undefined") { // 过滤空元素
continue
}
// div距离body的高度是pageH的倍数x,但是加上自身高度之后是pageH的倍数x+1
let beforeH = item.offsetTop + pageFooterH
let afterH = item.offsetTop + item.clientHeight + pageFooterH
let currentPage = parseInt(beforeH / pageH)
// console.log(pageH, item.offsetTop, item.clientHeight, currentPage, parseInt(afterH / pageH), item)
if (currentPage !== parseInt(afterH / pageH)) {
let diff = pageH - item.offsetTop % pageH - pageFooterH
// console.log(pageH, item.offsetTop, item.clientHeight, lastItemAftarH, diff)
// console.log(modules[j - 1].offsetTop, modules[j - 1].clientHeight)
// 加页尾
this.addPageFooter(node, item, currentPage + 1, diff)
// 加页头
this.addPageHeader(node, item)
}
if (i === modules.length - 1) { // 加了页头页尾后modules.length会变
let diff = pageH - afterH % pageH
deleteNullPage = diff + pageFooterH // 这完全是我加的特殊逻辑了 不知道为啥生成canvas后的高度会比现在的高度高一些 就会多一个空页 我把这里加上之后
// console.log(pageH, afterH, diff)
// 加页尾
this.addPageFooter(node, item, currentPage + 1, diff, true)
}
}
let obj = document.querySelector(`#${id}`)
let width = obj.clientWidth
let height = obj.clientHeight
let canvasBox = document.createElement('canvas')
let scale = window.devicePixelRatio
let rect = obj.getBoundingClientRect()
canvasBox.width = width * scale
canvasBox.height = height * scale
canvasBox.style.width = width + 'px'
canvasBox.style.height = height + 'px'
canvasBox.getContext('2d').scale(scale, scale)
canvasBox.getContext('2d').translate(-rect.left, -rect.top)
// const WGUTTER = 10 // 横向页边距
html2Canvas(document.querySelector(`#${id}`), {
backgroundColor: null,
background: '#FFFFFF',
useCORS: true, // 看情况选用上面还是下面的,
scale: scale,
canvas: canvasBox,
crossOrigin: 'Anonymous'
}).then(function (canvas) {
let pdf = new JsPDF('', 'pt', 'a4', true) // A4纸,纵向
let ctx = canvas.getContext('2d')
let a4w = SIZE[0] - 2 * WGUTTER
let a4h = SIZE[1] // A4大小,210mm x 297mm,两边各保留10mm的边距,显示区域190x297
let imgHeight = pageH // 按A4显示比例换算一页图像的像素高度
let renderedHeight = 0
let page
while (renderedHeight < canvas.height + 1) { // 这个-1时因为有时
page = document.createElement('canvas')
page.width = canvas.width
page.height = Math.min(imgHeight, canvas.height - renderedHeight - deleteNullPage) // 可能内容不足一页
// 用getImageData剪裁指定区域,并画到前面创建的canvas对象中
page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, Math.min(imgHeight, canvas.height - renderedHeight)), 0, 0)
pdf.addImage(page.toDataURL('image/jpeg', 1.0), 'JPEG', WGUTTER, 0, a4w, a4w * page.height / page.width) // 添加图像到页面
// console.log(page.height, page.width, Math.min(a4h, a4w * page.height / page.width))
renderedHeight += imgHeight
console.log(renderedHeight, imgHeight, canvas.height, deleteNullPage)
if (renderedHeight < canvas.height - deleteNullPage) {
pdf.addPage()// 如果后面还有内容,添加一个空页
} else {
break
}
}
pdf.save(title + '.pdf')
window.close()
)
},
Vue.prototype.addPageHeader = (node, item) => {
let pageHeader = document.createElement("div")
pageHeader.className = "c-page-head"
pageHeader.innerHTML = "页头内容"
node.insertBefore(pageHeader,item)
},
Vue.prototype.addPageFooter = (node, item, currentPage, diff, isLastest) => {
console.log(item.offsetTop, diff)
let pageFooter = document.createElement("div")
pageFooter.className = "c-page-foot"
pageFooter.innerHTML = "第" + currentPage + " 页"
isLastest?node.insertBefore(pageFooter,null):node.insertBefore(pageFooter,item)
pageFooter.style.marginTop = diff+"px"
pageFooter.style.marginBottom = "10px"
}
}
}