如何用html2canvas +jsPDF更简单实现pdf防截断导出(含算法优化)?

5,016 阅读4分钟

前言

最近的需求是将一个低码报表(里面分布了很多图形,比如表格,饼图等等)防截断导出,翻了很多的文章,要么实现复杂,要么就是有漏洞?什么漏洞,往下看 其实一个页面导出,是不是如果碰上一个图形,在分页的时候被截断,那就得把这个图形相关的元素(比如在它旁边的图形以及它自己本身)往下挪,放去下一页?

思考

  1. 先考虑我们用什么当作截断的工具呢?是否可以通过getImageData()获取页面像素数据,将像素跟raga(255,255,255,1)做对比,如果存在10px高度跟宽度为100%页面宽度,且颜色为raga(255,255,255,1)的间隔条就视为一个分割线,就是一个分页的标志?
  2. 我们怎么把要截断的元素往下挪去另一页再打印呢? 其实我们可以这么做:将整个页面全部转成canvas以后,先截取一个A4纸高度页面信息,从下往上判断是否存在一个分界线,如果存在,那就将分界线以下的页面导出成一张pdf。整个操作递归,直到打出到最后一页

实现

/**
 *
 * @param {Object} ref 导出页面的节点
 * @param {*} pageName 导出pdf名称
 * @param {*} margin 截断分界线高度
 * @param {*} colors 对比颜色数组
 */
 // 设计思路:position:位置指针,leftHeight:整个canvas剩余高度,每次获取一页a4纸对应的canvas的高度 a4HeightCanvas,
// 从下往上开始检测,如果碰到margin*2的匹配色高度,则认为这是一条可分割线,
// 于是可以生成一张高度为分割线以上部分height的pdf,此时的position = position + height ,leftHeight = leftHeight-height,
// 如果canvas剩下的高度 leftHeight 大于0,则继续上面的步骤
// 注意:如果两条分割线中间的部分超过a4HeightCanvas,生成的height 将为a4HeightCanvas ,图像会被截断!
// 此时,只能由报表用户调整图表位置重新导出pdf
function toChangeRealPdf (ref, pageName, margin = 10) {
  const color1 = 255
  const color2 = 255
  const color3 = 255
  
  // grid-stack
  html2canvas(ref,
    scale: window.devicePixelRatio * 2 // 增加清晰度
  }).then(canvas => {
    // 未生成pdf的canvas页面高度
    let leftHeight = canvas.height

    const a4Width = 190
    const a4Height = 277 // A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
    // 一页pdf显示html页面生成的canvas高度;
    const a4HeightCanvas = Math.floor(canvas.width / a4Width * a4Height)

    const context = canvas.getContext('2d')
    const imageData = context.getImageData(0, 0, canvas.width, canvas.height)
    // pdf页面偏移
    let position = 0

    const pdf = new jsPDF('p', 'mm', 'a4') // A4纸,纵向
    let index = 1
    const canvas1 = document.createElement('canvas')
    let height
    pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen')
    const createImgPdf = (canvas) => {
      if (leftHeight > 0) {
        index++
        let checkCount = 0
        if (leftHeight > a4HeightCanvas) {
          let i = position + a4HeightCanvas
          // 获取图像数据
          // const context = canvas.getContext('2d')
          const canvasW = canvas.width
          for (i = position + a4HeightCanvas; i >= position; i--) {
            let isWrite = true
            for (let j = 0; j < canvasW; j++) {
               // 每四个数组元素代表了一个像素点的RGBA信息,每个元素数值介于0~255,进行颜色数据匹配,找出分界线
              const c = imageData.data
              const index = i * canvasW + j
              const lastIndex = i * canvasW - j - 1
              const index4 = 4 * index
              if (!(c[index4] == color1 && c[index4 + 1] == color2 && c[index4 + 2] == color3 && c[index4 + 3] == 255)) {
                isWrite = false
                break
              }
            }
            if (isWrite) {
              checkCount++
              if (checkCount >= margin * 2) {
                break
              }
            } else {
              checkCount = 0
            }
          }
          height = Math.round(i - position) || Math.min(leftHeight, a4HeightCanvas)
          if (height <= 0) {
            height = a4HeightCanvas
          }
        } else {
          height = leftHeight
        }

        canvas1.width = canvas.width
        canvas1.height = height

        const ctx = canvas1.getContext('2d')
        ctx.drawImage(canvas, 0, position, canvas.width, height, 0, 0, canvas.width, height)

        if (position != 0) {
          pdf.addPage()
        }
        pdf.addImage(canvas1, 'JPEG', 10, 10, a4Width, a4Width / canvas1.width * height)
        leftHeight -= height
        position += height
      }
    }

    // 当内容未超过pdf一页显示的范围,无需分页
    if (leftHeight < a4HeightCanvas) {
      pdf.addImage(canvas, 'JPEG', 10, 10, a4Width, a4Width / canvas.width * leftHeight)
    } else {
      while (leftHeight > 0) {
        createImgPdf(canvas)
      }
    }
    pdf.save(pageName + '.pdf')
  })
}

漏洞

思考:这会不会出现问题?比如,页面背景色刚好就是rgba(255,255,255,1),这样的话,如果一个图表特别长,刚好图表的内部有很多空白部分,高度超过10,这时候,会不会就会导致这个图表被截断了?

处于截断页面下部的图表表现: 1689662486136.png 导出以后,图表被截断: 1689662583990.png

这种情况应该如何优化呢?

我们可以将页面转成canvas,然后将页面拷贝一份,并设置一个对比色,这个颜色可以自定义,然后将这两份数据进行对比,再打印

function toChangeRealPdf (result, ref, pageName, loading, margin = 10, colors = [0, 153, 217]) {
  const [color1, color2, color3] = colors
  // grid-stack
  if (result) {
    html2canvas(ref, {
      onclone: (document) => {
        document.getElementsByClassName('grid-stack')[0].style.backgroundColor = `rgba(${color1},${color2},${color3},1)`
      },
      scale: window.devicePixelRatio * 2 // 增加清晰度
    }).then(canvas => {
      // 未生成pdf的canvas页面高度
      let leftHeight = canvas.height

      const a4Width = 190
      const a4Height = 277 // A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
      // 一页pdf显示html页面生成的canvas高度;
      const a4HeightCanvas = Math.floor(canvas.width / a4Width * a4Height)

      const context = canvas.getContext('2d')
      const imageData = context.getImageData(0, 0, canvas.width, canvas.height)

      // pdf页面偏移
      let position = 0

      const pdf = new jsPDF('p', 'mm', 'a4') // A4纸,纵向
      let index = 1
      const canvas1 = document.createElement('canvas')
      let height
      pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen')
      const createImgPdf = (canvas) => {
        if (leftHeight > 0) {
          index++
          let checkCount = 0
          if (leftHeight > a4HeightCanvas) {
            let i = position + a4HeightCanvas
            // 获取图像数据
            // const context = canvas.getContext('2d')
            const canvasW = canvas.width
            for (i = position + a4HeightCanvas; i >= position; i--) {
              let isWrite = true
              for (let j = 0; j < canvasW; j++) {
                // 每四个数组元素代表了一个像素点的RGBA信息,每个元素数值介于0~255,进行颜色数据匹配,找出分界线
                const c = imageData.data
                const index = i * canvasW + j
                const lastIndex = i * canvasW - j - 1
                const index4 = 4 * index
                if (!(c[index4] == color1 && c[index4 + 1] == color2 && c[index4 + 2] == color3 && c[index4 + 3] == 255)) {
                  isWrite = false
                  break
                }
              }
              if (isWrite) {
                checkCount++
                if (checkCount >= margin * 2) {
                  break
                }
              } else {
                checkCount = 0
              }
            }
            height = Math.round(i - position) || Math.min(leftHeight, a4HeightCanvas)
            if (height <= 0) {
              height = a4HeightCanvas
            }
          } else {
            height = leftHeight
          }

          canvas1.width = canvas.width
          canvas1.height = height

          console.log(index, 'height:', height, 'pos', position)

          const ctx = canvas1.getContext('2d')
          ctx.drawImage(result, 0, position, canvas.width, height, 0, 0, canvas.width, height)

          if (position != 0) {
            pdf.addPage()
          }
          pdf.addImage(canvas1, 'JPEG', 10, 10, a4Width, a4Width / canvas1.width * height)
          leftHeight -= height
          position += height
        }
      }

      // 当内容未超过pdf一页显示的范围,无需分页
      if (leftHeight < a4HeightCanvas) {
        // const pageData = result.toDataURL('image/jpeg', 1.0)
        pdf.addImage(result, 'JPEG', 10, 10, a4Width, a4Width / result.width * leftHeight)
      } else {
        while (leftHeight > 0) {
          createImgPdf(canvas)
        }
      }
      pdf.save(pageName + '.pdf')
      // console.log(that.$createDate())
      loading.close()
    })
  }
}
export function downloadPdf (ref, pageName, margin, colors) {
  const loading = Loading.service({
    fullscreen: true,
    lock: true,
    text: 'Loading',
    spinner: 'icon-loading',
    background: 'rgba(255, 255, 255, 0.7)'
  })
  // console.log(that.$createDate())
  html2canvas(ref, {
    scale: window.devicePixelRatio * 2 // 增加清晰度
  }).then(canvas => {
    toChangeRealPdf(canvas, ref, pageName, loading, margin, colors)
  })
}

效果图:

1689663397565.png 嘿嘿,是不是就好了?? 1689663492853.png

现在又发现一个问题,toChangeRealPdf方法中,找出分界线的代码这是双层遍历,整个页面的imageData.data是一个非常庞大的数据,目前我的测试页面组件很少,可能没多大影响,但是如果一张报表组件很多,数据很多的时候,就会导致页面导出很慢

for (i = position + a4HeightCanvas; i >= position; i--) {
  let isWrite = true
  for (let j = 0; j < canvasW; j++) {
    // 每四个数组元素代表了一个像素点的RGBA信息,每个元素数值介于0~255,进行颜色数据匹配,找出分界线
    const c = imageData.data
    const index = i * canvasW + j
    const lastIndex = i * canvasW - j - 1
    const index4 = 4 * index
    if (!(c[index4] == color1 && c[index4 + 1] == color2 && c[index4 + 2] == color3 && c[index4 + 3] == 255)) {
      isWrite = false
      break
    }
  }

算法又如何优化

如何优化呢? 我们可以将页面分成两边,比对的时候,从左右双头进行

优化前,toChangeRealPdf函数性能分析 1689667633732.jpg 优化后,toChangeRealPdf执行时间明显减少 1689667820762.jpg

function toChangeRealPdf (result, ref, pageName, loading, margin = 10, colors = [0, 153, 217]) {
  const [color1, color2, color3] = colors
  // grid-stack
  if (result) {
    html2canvas(ref, {
      onclone: (document) => {
        document.getElementsByClassName('grid-stack')[0].style.backgroundColor = `rgba(${color1},${color2},${color3},1)`
      },
      scale: window.devicePixelRatio * 2 // 增加清晰度
    }).then(canvas => {
      // 未生成pdf的canvas页面高度
      let leftHeight = canvas.height

      const a4Width = 190
      const a4Height = 277 // A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
      // 一页pdf显示html页面生成的canvas高度;
      const a4HeightCanvas = Math.floor(canvas.width / a4Width * a4Height)

      const context = canvas.getContext('2d')
      const imageData = context.getImageData(0, 0, canvas.width, canvas.height)

      // pdf页面偏移
      let position = 0

      const pdf = new jsPDF('p', 'mm', 'a4') // A4纸,纵向
      let index = 1
      const canvas1 = document.createElement('canvas')
      let height
      pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen')
      const createImgPdf = (canvas) => {
        if (leftHeight > 0) {
          index++
          let checkCount = 0
          if (leftHeight > a4HeightCanvas) {
            let i = position + a4HeightCanvas
            // 获取图像数据
            // const context = canvas.getContext('2d')
            const canvasW = canvas.width
            const mid = parseInt(canvasW / 2)
            for (i = position + a4HeightCanvas; i >= position; i--) {
              let isWrite = true
              for (let j = 0; j < mid; j++) {
                // 每四个数组元素代表了一个像素点的RGBA信息,每个元素数值介于0~255
                const c = imageData.data
                const index = i * canvasW + j
                const lastIndex = i * canvasW - j - 1
                const index4 = 4 * index
                const lastIndex4 = 4 * lastIndex
                if (!(c[index4] == color1 && c[index4 + 1] == color2 && c[index4 + 2] == color3 && c[index4 + 3] == 255) ||
              !(c[lastIndex4] == color1 && c[lastIndex4 + 1] == color2 && c[lastIndex4 + 2] == color3 && c[lastIndex4 + 3] == 255)) {
                  isWrite = false
                  break
                }
              }
              if (isWrite) {
                checkCount++
                if (checkCount >= margin * 2) {
                  break
                }
              } else {
                checkCount = 0
              }
            }
            height = Math.round(i - position) || Math.min(leftHeight, a4HeightCanvas)
            if (height <= 0) {
              height = a4HeightCanvas
            }
          } else {
            height = leftHeight
          }

          canvas1.width = canvas.width
          canvas1.height = height

          console.log(index, 'height:', height, 'pos', position)

          const ctx = canvas1.getContext('2d')
          ctx.drawImage(result, 0, position, canvas.width, height, 0, 0, canvas.width, height)

          if (position != 0) {
            pdf.addPage()
          }
          pdf.addImage(canvas1, 'JPEG', 10, 10, a4Width, a4Width / canvas1.width * height)
          leftHeight -= height
          position += height
        }
      }

      // 当内容未超过pdf一页显示的范围,无需分页
      if (leftHeight < a4HeightCanvas) {
        // const pageData = result.toDataURL('image/jpeg', 1.0)
        pdf.addImage(result, 'JPEG', 10, 10, a4Width, a4Width / result.width * leftHeight)
      } else {
        while (leftHeight > 0) {
          createImgPdf(canvas)
        }
      }
      pdf.save(pageName + '.pdf')
      // console.log(that.$createDate())
      loading.close()
    })
  }
}

嘿嘿,大功告成!!!

最后,感谢大家阅读我的小文章,请帮我点亮一下我的小心心吧!!!

1689577155763(1).png 1689577224795.png 1689577345238(1).png