JsPDF + html2Canvas 网页导出pdf 解决页眉页脚问题 解决分页截断问题 解决图片跨域问题

6,192 阅读4分钟

简介

之前有个要求前端导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"
    }
  }
}