web前端pdf导出

做一个项目需要实现浏览器端pdf的导出功能,在此记录一下整个实现过程以及遇到的一些坑:)

当然,解决这个问题有以下几个步骤:

  1. 确定要导出的dom元素
  2. 将dom元素转化成canvas( 使用html2canvas库)
  3. 将canvas转化成图片jpeg,png等都可以
  4. 将图片导出pdf (使用jspdf库)

确定要导出的dom元素

如果是原生写法可以直接使用document.getElementById来获取,如果是用vue或者react可以设置要ref, 此处我使用的是react框架

  const reportRef = useRef();
  // canvasEle 为需要获取的dom元素
  const canvasEle = reportRef.current

将dom元素转化成canvas

这里其实有几个坑:

  1. 如果导出的dom元素里存在svg,则需要首先把所有dom中的svg先转化成canvas,否则svg部分会变成空白,svg转canvas 可以使用canvg库来操作
// 此处封装了一个转化函数
import Canvg from 'canvg';
const svg2Canvas = (domEle: HTMLElement, cb: (node: any) => void) => {
  // 在临时div上将svg都转换成canvas,并删除原有的svg节点
  const svgElem = domEle.querySelectorAll("svg");

  svgElem.forEach(async (node: any) => {
    const { parentNode } = node;
    if (cb && typeof cb === 'function') {
      cb(node)
    }

    const svg = node.outerHTML.trim();
    const canvas = document.createElement('canvas');
    const width = parentNode.clientWidth
    const height = parentNode.clientHeight
    const ctx = canvas.getContext('2d');
    const v = Canvg.fromString(ctx, svg,
      {
        ignoreMouse: true,   // 不理会鼠标事件
        ignoreAnimation: true,  // 不理会动画
      });

    // 保持canvas大小和原来的svg一致
    v.resize(width, height);

    // 将svg替换成canvas
    v.render();

	// 拷贝位置的样式
    if (node.style.position) {
      canvas.style.position += node.style.position;
      canvas.style.left += node.style.left;
      canvas.style.top += node.style.top;
    }
    parentNode.removeChild(node);
    parentNode.appendChild(canvas);
  });
  return domEle
}
// svg2Canvas 使用方式
const tempEle = svg2Canvas(
  cloneEle,
  (node) => {
    const circleTrailColor = '#f5f5f5'
    const circleTrailPath = '#20486c'
    /* 
    此处注意!!! 
    我们这里是使用到了ant-design的Progress进度条组件,
    Progress中设置的颜色是通过class来设置的。
    这会导致颜色无法导出,
    因为canvas是不会带class的样式的,
    我们需要把颜色写到stroke才会正常显示。
    */
    if (node.className.baseVal === 'ant-progress-circle') {
      node.querySelector('.ant-progress-circle-trail').style.stroke = circleTrailColor
      node.querySelector('.ant-progress-circle-path').style.stroke = circleTrailPath
    } else {
      /*
      此处的处理是由于我们使用了阿里的字体图标,字体图标使用的是use标签如下所示:
      <use xlink:href="#iconfendianbu"></use>
      这个use标签下面的元素实际上是引用symbol模板的元素
      <symbol id="iconfendianbu" viewBox="0 0 1024 1024"></symbol>
      这会导致无法正常导出元素,所以我们需要把use标签转化成真正的svg元素
      注意在转化的时候一定要使用cloneNode()方法,否则symbol下的svg元素会被挂载上去,会影响到其他使用该图标的地方无法正常显示
	 */
      const use = node.querySelector('use')
      if (use) {
        node.style.fill = circleTrailPath
        node.setAttribute('viewBox', '0 0 1024 1024')
        const href = use.getAttribute('xlink:href')
        const id = href.replace('#', '')
        const svgSymbolEle = document.getElementById(id)
        node.removeChild(use)
        const path = svgSymbolEle.querySelectorAll('path')
        path.forEach(pathNode => {
          const clonePathNode = pathNode.cloneNode(true)
          node.appendChild(clonePathNode)
        })
      }
    }
  })
         // 设置放大倍数,处理画布导出图片模糊的问题
          const scale = 2.5
          const contentWidth = parseInt(tempEle.scrollWidth)
          const contentHeight = parseInt(tempEle.scrollHeight)
          // 获取到canvas元素
          const canvas = await html2canvas(tempEle, {
            dpi: window.devicePixelRatio * scale,
            scale, // 放大倍数
            width: contentWidth,
            heigth: contentHeight,
            useCORS: true // 【重要】开启跨域配置
          })

将canvas转化成图片jpeg

这一步骤比较简单,只是在之前生成的canvas上调用toDataURL()方法即可

 const pageData = canvas.toDataURL('image/jpeg')

将图片导出pdf

此处需要使用jspdf库来操作

// 封装了单页导出的方法,直接调用即可
 exportPdfOnePage({
   pageData,
   contentWidth,
   contentHeight,
   fileName,
   scale
 })
// 这里使用单例模式异步导入jspdf库,直接导入会导致项目启动报错
const importJsPDF = () => {
  let jspdf: any = null
  return async () => {
    if (!jspdf) {
      const { default: JsPDF } = await import('jspdf')
      jspdf = JsPDF
    }
    return jspdf
  }
}
export const getJsPDF = importJsPDF()
// 单页导出psf
export const exportPdfOnePage = async (options: any) => {
  const {
    pageData,
    contentWidth,
    contentHeight,
    fileName,
    scale
  } = options

  // 设置pdf的尺寸,pdf要使用pt单位 已知 1pt/1px = 0.75   pt = (px/scale)* 0.75
  const pdfX = (contentWidth) * 0.75
  const pdfY = (contentHeight + 100) * 0.75// 为底部留白

  // 设置内容图片的尺寸,img是pt单位
  const imgX = pdfX;
  const imgY = (contentHeight) * 0.75; // 内容图片这里不需要留白的距离

  // 初始化jspdf 第一个参数方向:默认''时为纵向,第二个参数设置pdf内容图片使用的长度单位为pt,第三个参数为PDF的大小,单位是pt
  const JsPDF = await getJsPDF()
  const pdf = new JsPDF('', 'pt', [pdfX, pdfY])
  pdf.addImage(pageData, 'jpeg', 0, 0, imgX, imgY)
  pdf.save(`${fileName}.pdf`)
}

// 多页导出pdf
export const exportPdfMutiPage = async (options: any) => {
  const {
    canvas,
    contentWidth,
    contentHeight,
    fileName,
  } = options

  const a4Width = 592.28
  const a4Height = 841.89

  // 一页pdf显示html页面生成的canvas高度;
  const pageHeight = contentWidth / a4Width * a4Height
  // 未生成pdf的html页面高度
  let leftHeight = contentHeight
  // pdf页面偏移
  let position = 0
  // a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
  const imgWidth = a4Width
  const imgHeight = a4Width / contentWidth * contentHeight
  const pageData = canvas.toDataURL('image/jpeg', 1.0)
  const JsPDF = await getJsPDF()
  const pdf = new JsPDF('', 'pt', 'a4')
  // 有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
  // 当内容未超过pdf一页显示的范围,无需分页
  if (leftHeight < pageHeight) {
    pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth + 20, imgHeight)
  } else {
    while (leftHeight > 0) {
      pdf.addImage(pageData, 'JPEG', 0, position, imgWidth + 20, imgHeight)
      leftHeight -= pageHeight
      position -= 841.89
      // 避免添加空白页
      if (leftHeight > 0) {
        pdf.addPage()
      }
    }
  }
  pdf.save(`${fileName}.pdf`)
}

其他的坑

  1. 在所有的代码都写完之后发现一个问题,就是偶尔导出的pdf上半部分会留空白,截图不完整,而且很多时候留的空白距离还不一样,后来才发现,其实是滚动条的问题,因为html2canvas截图的时候,是根据body从上往下截图的,所以后来做了一个处理,把滚动条跳动到最顶端
 // 解决 canvas 截图顶部可能留空白的问题
 window.pageYoffset = 0;
 document.documentElement.scrollTop = 0;
 document.body.scrollTop = 0;
 canvasEle.scrollLeft = 0;

完整功能代码

其中使用的svg2Canvas, exportPdfOnePage这两个函数,可以在上文中找到代码,此处不重复粘贴。

    try {
      // 增加页面渲染所用的时间
      setTimeout(async () => {
        message.info('正在导出,请稍后。')
        const timestamp = (new Date()).getTime();
        const canvasEle: HTMLElement = reportRef.current
        if (!canvasEle) return

        // 解决 canvas 截图顶部可能留空白的问题
        window.pageYoffset = 0;
        document.documentElement.scrollTop = 0;
        document.body.scrollTop = 0;
        canvasEle.scrollLeft = 0;
        const cloneEle = canvasEle.cloneNode(true)
        cloneEle.style.position = 'absolute'
        cloneEle.style.top = '2000px'
        cloneEle.style['max-width'] = '1600px'

        try {
          const fileName = `导出pdf_${timestamp}`
          document.body.appendChild(cloneEle);

          const tempEle = svg2Canvas(
            cloneEle,
            (node) => {
              const circleTrailColor = '#f5f5f5'
              const circleTrailPath = '#20486c'
              if (node.className.baseVal === 'ant-progress-circle') {
                // eslint-disable-next-line no-param-reassign
                node.querySelector('.ant-progress-circle-trail').style.stroke = circleTrailColor
                // eslint-disable-next-line no-param-reassign
                node.querySelector('.ant-progress-circle-path').style.stroke = circleTrailPath
              } else {
                const use = node.querySelector('use')
                if (use) {
                  node.style.fill = circleTrailPath
                  node.setAttribute('viewBox', '0 0 1024 1024')
                  const href = use.getAttribute('xlink:href')
                  const id = href.replace('#', '')
                  const svgSymbolEle = document.getElementById(id)
                  node.removeChild(use)
                  const path = svgSymbolEle.querySelectorAll('path')
                  path.forEach(pathNode => {
                    const clonePathNode = pathNode.cloneNode(true)
                    node.appendChild(clonePathNode)
                  })
                }
              }
            })
            
          // 设置放大倍数,处理画布导出图片模糊的问题
          const scale = 2.5
          const contentWidth = parseInt(tempEle.scrollWidth)
          const contentHeight = parseInt(tempEle.scrollHeight)
          const canvas: HTMLCanvasElement = await html2canvas(tempEle, {
            dpi: window.devicePixelRatio * scale,
            scale, // 放大倍数
            width: contentWidth,
            heigth: contentHeight,
            useCORS: true // 【重要】开启跨域配置
          })

          document.body.removeChild(cloneEle);
          const pageData = canvas.toDataURL('image/jpeg')
          // 导出pdf文件
          exportPdfOnePage({
            pageData,
            contentWidth,
            contentHeight,
            fileName,
            scale
          })
        } catch (err) {
          document.body.removeChild(cloneEle);
          console.log(err)
        }
        setExportLoading(false)
      }, 500)
    } catch (err) {
      console.log(err)
      setExportLoading(false)
    }
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
前端解析PDF文件并将其转换成PDF图像是一种常见的需求。为了实现这个目标,我们可以借助一些现有的前端库和技术。 首先,我们需要选择一个适合的前端库,例如pdf.js。pdf.js是一个流行的开源JavaScript库,用于在Web页面上渲染PDF文档。它可以将PDF文件解析为可供前端使用的数据格式。 使用pdf.js,我们可以通过以下步骤来实现将PDF文件转换为PDF图像: 1. 引入pdf.js库文件,并在页面上创建一个用于显示PDF图像的元素,例如一个<canvas>元素。 2. 通过调用pdf.js的API,我们可以将PDF文件加载到内存中。可以使用XMLHttpRequest对象或Fetch API来获取PDF文件。 3. 一旦PDF文件加载完毕,我们可以通过调用pdf.js提供的函数来解析PDF文件,并获得每个页面的数据。 4. 对于每个页面,我们可以使用Canvas API在<canvas>元素上绘制PDF图像。pdf.js提供了一些函数来帮助我们将PDF页面渲染为图像。 5. 最后,我们可以通过将<canvas>元素转换为图像,或者使用其他方法来导出图像,以将PDF文件转换为PDF图像。 需要注意的是,由于PDF文件可能包含多个页面,并且每个页面可能包含大量的内容,因此在前端进行PDF解析和图像渲染可能会占用较多的系统资源。我们应该注意性能优化,以确保页面加载和渲染的效率和速度。 综上所述,我们可以利用pdf.js等前端库和技术,以及Canvas API等工具,将PDF文件解析为PDF图像,并在前端显示出来。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值