记录一个截图导出pdf的方法

以下是导出的方法:

javascript 复制代码
// 通过截图、分页、处理文字截断后从dom生成pdf并导出
import { nextTick } from 'vue'
import { BxmMessage } from 'bxm-ui3'
import html2Canvas from 'html2canvas'
import JsPDF from 'jspdf'

/**
 * 
 * @param {*} dom  导出的模块
 * @param {*} fileName  导出文件名称
 * @param {*} splitClassName   需要处理分页的类名(请将可能需要处理分页(即可能出现文字被分割)的元素都加上统一的类名)这里要保证整个dom中没有上下的margin,且每一行文字、图片所在的小容器都要设置统一的类名,把这个类名传入函数进行处理
 */
export async function exportFileByPDF(dom, fileName, splitClassName) {
  // 获取正确的a4纸转换成像素值宽高
  let { a4Width, a4Height, currentDPT, dptValue } = getA4Size()
  a4Width = a4Width / dptValue
  a4Height = a4Height / dptValue
  // dom所在为起点
  let startHeight = dom.getBoundingClientRect().top
 
  // 处理分页元素
  let questionTitleList = dom.querySelectorAll('.' + splitClassName)
  await checkBandary(questionTitleList, a4Width, a4Height, startHeight, fileName)
  
  // 开始截图
  nextTick(() => {
    html2Canvas(dom, {
      scale: 2, // 设置缩放(设置成1导出文件文本框右边有一块灰色部分)
      allowTaint: true, // 允许跨域渲染图片
      useCORS: true, // 使用CORS从服务器加载图像
      logging: false, // 是否打印日志
      bgcolor: '#ffffff', // 背景色
    }).then((canvas) => {
      
      // 比例采用计算的比例,不固定
      let scale = canvas.width / a4Width

      // 用px单位生成pdf
      let pdf = new JsPDF('p', 'px', 'a4')    //A4纸,纵向

      // 处理数据
      let ctx = canvas.getContext('2d')

      // 按A4显示比例换算一页图像的像素高度,截图为了清晰度设置了scale:2,所以这里是乘,在最后加到pdf中时在缩小
      // let height = Math.floor(((a4Height / scale) * canvas.width / (a4Width / scale)))
      let imgHeight = a4Height * scale

      let renderedHeight = 0

      // 分页
      while (renderedHeight < canvas.height) {
        let page = document.createElement('canvas')
        page.width = canvas.width
        let pageHeight = Math.min(imgHeight, canvas.height - renderedHeight)
        // 可能内容不足一页
        page.height = pageHeight

        // 用getImageData剪裁指定区域,并画到前面创建的canvas对象中
        // 这时等于是宽高放大两倍的
        page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, pageHeight), 0, 0)
        
        // 添加图像到页面
        // 加到pdf页面中的时候需要缩小,让a4纸放得下  
        // 0.2质量因子, jpeg格式压缩图片大小,质量因子控制图片质量,在0-1之间,值越低压缩得越小,质量越差
        // 两边各留25宽,让内容居中显示
        pdf.addImage(page.toDataURL('image/jpeg', 1), 'jpeg', 25, 25, a4Width / scale, Math.min(a4Height / scale, (a4Width / scale) * pageHeight / canvas.width))       
        // 这里不要用png,png下载文件太大了,页面会卡,下载出文件太大
        // pdf.addImage(page.toDataURL('image/png', 0.2), 'png', 0, 0, a4Width / scale, Math.min(a4Height / scale, (a4Width / scale) * pageHeight / canvas.width))       
        
        renderedHeight += imgHeight

        if (renderedHeight < canvas.height) {
          pdf.addPage() // 如果后面还有内容,添加一个空页
        }
      }
      pdf.save(fileName + '.pdf')
      // 下载完毕后隐藏分页,避免影响到预览效果
      let emptyDomList = document.getElementsByClassName('emptyDiv')
      Promise.all([...Array.from(emptyDomList).map(el => 
        new Promise((resolve, reject) => {
          try {
            el.remove()
            resolve()
          } catch {
            reject()
          }
        })
      )])
      BxmMessage({
        type: 'success',
        message: '试卷下载完成!'
      })
    })
  })
}

// 处理边界元素
export async function checkBandary(domList, a4Width, a4Height, startHeight, fileName) {
  return new Promise((resolve, reject) => {
    if (!domList.length) resolve(false)
    try {
      // 进行分割操作,当dom内容已超出a4的高度,则将该dom前插入一个空盒子,把他挤下去,分割
      for (let i = 0; i < domList.length; i++) { 
        // 获取实时高度
        let topHeight =  domList[i].getBoundingClientRect().top
        let clientHeight = domList[i].getBoundingClientRect().height
        // let clientHeight = domList[i].clientHeight
        // 计算页数
        let multiple = Math.ceil((topHeight + clientHeight - startHeight) / a4Height)
        let height = startHeight + (multiple - 1) * a4Height
        // 判断元素是否跨页
        if (isSplit(domList, i, a4Height, height)) {
          // 获取该div的父节点
          let divParent = domList[i].parentNode
          // 创建页脚
          let newNode = document.createElement('div')
          newNode.className = 'emptyDiv'
          // newNode.innerHTML = `第${multiple}页`
          newNode.style.cssText = `display: flex; align-items: center; justify-content: center; width: calc(${a4Width}px - 70px); background: transparent; font-size: 10px;`
          
          // 高度为当前元素距离当前页底部的距离
          let _H = a4Height - (topHeight + clientHeight - height)
          // newNode.style.height = _H < 120 ? 120 + 'px' : _H + 'px'
          // newNode.style.height = 120 + 'px
          newNode.style.height = _H + 'px'
          // 获取兄弟节点
          let next = getNextNode(domList[i])
          // 判断兄弟节点是否存在
          if (next) {
            // 存在则将新节点插入到div的下一个兄弟节点之前,即div之后
            divParent.insertBefore(newNode, next)
          } else {
            // 不存在则直接添加到最后,appendChild默认添加到divParent的最后
            divParent.append(newNode)
          }
        }
      }
      resolve()
    } catch {
      reject()
    }
  })
}

// 判断当前元素是否跨页
/**
 * 
 * @param {*} nodes 
 * @param {*} index 
 * @param {*} a4Height 
 * @param {*} startHeight 
 * @param {*} preFooterH  前一页的底部空白高度
 * @returns 
 */
export function isSplit(nodes, index, a4Height, startHeight) {
  let topHeight = nodes[index].getBoundingClientRect().top
  let clientHeight = nodes[index].getBoundingClientRect().height
  if (index < nodes.length - 1) {
    let topHeightNext = nodes[index + 1].getBoundingClientRect().top
    let clientHeightNext = nodes[index + 1].getBoundingClientRect().height
    // 当前元素不跨页,下一个元素要跨页,说明在当前元素之后要分页
    if (nodes[index + 1] 
        && topHeight + clientHeight - startHeight + 60 <= a4Height 
        && (topHeightNext + clientHeightNext - startHeight + 60 > a4Height
        || (a4Height - (topHeight + clientHeight - startHeight) < clientHeightNext))) {
      return true
    }
    return false
  }
  return false
}

// 根据不同分辨率获取到a4纸不同的宽高
export function getA4Size() {
  // A4纸的标准尺寸为210 mm x 297 mm,换算成英寸大约是8.27" x 11.69"。
  // 使用公式 像素 = 实际尺寸(英寸)× DPI 可以得到不同DPI下的像素值
  // 大多浏览器
  let defaultDPI = 96; // 默认DPI值
  let currentDPT = 0
  let dptValue = 1
  // IE浏览器
  if (window.screen.deviceXDPI !== undefined) {
    currentDPT = window.screen.deviceXDPI
  } else if (window.devicePixelRatio) { // 一般浏览器
    currentDPT = defaultDPI * window.devicePixelRatio
    dptValue = window.devicePixelRatio
  } else {
    currentDPT = defaultDPI
  }
  return {
    a4Width: 8.27 * currentDPT,
    a4Height: 11.69 * currentDPT,
    currentDPT,
    dptValue
  }
}

// 递归查找节点,因为有的节点v-if为false被视为注释节点
export function getNextNode(node, nextNode = null) {
  if (!node.nextSibling) return null
  let next = node.nextSibling
  if (next.nodeType !== Node.COMMENT_NODE) {
    nextNode = next
    return nextNode
  } else {
    getNextNode(next)
  }
  
}

在导出之前需要保证图片被完全加载,否则会影响到高度计算

javascript 复制代码
// 保证图片加载完毕
const loadImage = (imgElement) => {
  return new Promise((resolve, reject) => {
    if (imgElement.complete) {
      resolve(imgElement)
    } else {
      imgElement.onload = () => resolve(imgElement)
      imgElement.onerror = () => reject(`图片加载失败: ${imgElement.src}`)
    }
  })
}

最后的使用方法:

javascript 复制代码
nextTick(async () => {
        let dom = document.getElementById('pdfDom')
        let imgs = dom.getElementsByTagName('img')
        // 有图片时,要先将图片加载完整,否则导出时图片高度计算错误导致页面文字分割
        if (imgs.length) {
          exportLoading.value = true
          BxmMessage({
            type: 'warning',
            message: '文件数据加载中,该试卷==文件中图片较多,加载缓慢,为避免影响导出文件排版,请不要滚动页面和关闭弹窗,耐心等待!',
            showClose: true,
            duration: imgs.length * 1500
          })
          const promises = Array.from(imgs).map(async img => { await loadImage(img) })
          Promise.all(promises).then(() => {
              // 等待浏览器重绘
              let timer = setTimeout(() => {
                clearTimeout(timer)
                BxmMessage({
                  type: 'warning',
                  message: '文件加载完毕,正在导出文件,文件中图片较多,下载缓慢,请耐心等待,不要关闭弹窗!',
                  showClose: true
                })
                try {
                  exportFileByPDF(dom, '文件名', 'question-content-text')
                } catch {
                  BxmMessage({
                    type: 'warning',
                    message: '导出失败!',
                    showClose: true
                  })
                }
              }, 1500)
          }).catch(() => {
            BxmMessageBox.confirm('部分图片加载失败,导出文件展示可能不全,是否继续导出?', '提示', {
              confirmButtonText: '确定',
              cancelButtonText: '取消',
              type: 'warning'
            }).then(() => {
              exportLoading.value = false
              try {
                exportFileByPDF(dom, '文件名', 'question-content-text')
              } catch {
                BxmMessage({
                  type: 'warning',
                  message: '导出失败!',
                  showClose: true
                })
              }
            }).catch(() => {
              BxmMessage({
                type: 'info',
                message: '已取消导出!',
                showClose: true
              })
              exportLoading.value = false
            })
          })
        } else {
          BxmMessage({
            message: '文件加载完毕,正在导出文件,请不要关闭弹窗!',
            type: 'warning',
            showClose: true
          })
          try {
            exportFileByPDF(dom, '文件名', 'question-content-text')
          } catch {
            BxmMessage({
              type: 'warning',
              message: '导出失败!',
              showClose: true
            })
          }
        }
      })

在图片加载完毕之后确实能够导出完整的文件,其中文字、图片不会被分割,但这个方法有一个缺点是图片太多了,页面等待时间太长,效率不高的问题,还没找到如何解决。

导出的文件示例:

相关推荐
Dread_lxy5 分钟前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
网络安全指导员1 小时前
恶意PDF文档分析记录
网络·安全·web安全·pdf
羡与1 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
前端郭德纲1 小时前
浏览器是加载ES6模块的?
javascript·算法
JerryXZR1 小时前
JavaScript核心编程 - 原型链 作用域 与 执行上下文
开发语言·javascript·原型模式
帅帅哥的兜兜1 小时前
CSS:导航栏三角箭头
javascript·css3
渗透测试老鸟-九青2 小时前
通过投毒Bingbot索引挖掘必应中的存储型XSS
服务器·前端·javascript·安全·web安全·缓存·xss
龙猫蓝图2 小时前
vue el-date-picker 日期选择器禁用失效问题
前端·javascript·vue.js
夜色呦2 小时前
掌握ECMAScript模块化:构建高效JavaScript应用
前端·javascript·ecmascript