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

以下是导出的方法:

// 通过截图、分页、处理文字截断后从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)
  }
  
}

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

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

最后的使用方法:

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
            })
          }
        }
      })

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

导出的文件示例:在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/658804.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Hive复杂数据类型之 Struct结构体

想写这篇文章蛮久了&#xff0c;但这个数据类型&#xff0c;确实很少用&#xff0c;翻遍了代码库的所有代码&#xff0c;也没有找到。 但&#xff0c;之前分享过的 Hive复杂数据类型之 array 数组&#xff0c; Hive复杂数据类型之array数组_hive 建表设置array类型-CSDN博客 …

Linux命令 jps(Java Process Status)解释

文章目录 1、第一种解释2、第二种解释3、第三种解释 1、第一种解释 jps 命令本身并不是一个标准的 Unix/Linux 命令&#xff0c;但您可能是想提到 jps 的一个变种或误写了 jps 为 jps&#xff0c;而实际上可能是想提及 jps&#xff08;Java Virtual Machine Process Status To…

linux下常用的终端命令

文章目录 1. MV移动文件、重命名文件1.1 移动文件&#xff1a;mv [选项] 源文件或目录 目标文件或目录1.2 文件重命名 2. 查找&#xff1a;文件&#xff0c;内容&#xff0c;统计文件2.1 find查找文件2.2 Linux查找文件内容 3. 查看当前用户4. linux修改文件所属用户和组5. 复制…

红酒配餐中的酒杯选择与使用技巧

在红酒配餐中&#xff0c;酒杯的选择与使用技巧是影响品鉴体验的重要因素。合适的酒杯不仅能展现出红酒的优雅和风味&#xff0c;还能提升用餐的仪式感和愉悦感。云仓酒庄雷盛红酒以其卓着的品质和丰富的口感&#xff0c;成为了红酒爱好者们的首要选择品牌。下面将介绍在红酒配…

菜鸟的JavaSE学习之旅7

这是一个目录 数组工具类Arrays数学工具类Math继承重写&#xff08;Override&#xff09;覆盖、覆写注意 构造方法super关键字用法this关键字super和this 抽象抽象方法和抽象类格式抽象方法和抽象类使用抽象方法和抽象类注意事项 数组工具类Arrays java.util.Arrays是一个与数…

飞书API(10):通过阿里云MaxCompute数仓入库 - 转为阿里云 DataFrame 再入库

一、引入 上一小节介绍了怎么入库到阿里云的 MaxCompute 数仓&#xff0c;其中涉及到 2 种入库方式&#xff0c;一种是转为阿里云的 DataFrame&#xff0c;然后类似 pandas 的 DataFrame 直接写入 MySQL 的方法&#xff0c;将数据写入表中&#xff1b;另外一种是转为列表&…

linux开发之设备树四、设备树中断节点

中断节点 这里是由原厂的BSP工程师写的一部分 在CPU的外部有一个GIC控制器&#xff0c;外设会连接在GIC控制器上 设备树是对硬件进行描述的&#xff0c;所以设备树会对CPU进行描述&#xff0c;也要对GIC 控制器进行描述&#xff0c;这部分的代码由原厂的BSP工程师进行编写&…

微软Build开发者大会速览,OpenAI CEO站台剧透新模型

5月22日凌晨&#xff0c;微软Build 2024开发者大会在美国西雅图召开。微软CEO萨蒂亚纳德拉在会上发表主题演讲&#xff0c;宣布了超过50项产品更新&#xff0c;涵盖AI基础设施、模型产品以及生产力工具等多个领域。纳德拉强调&#xff0c;微软一直致力于让人工智能理解人类并帮…

Python的解析网页

课前案例 通过requests模块爬取指定网站中的图片并保存到本地目录中。 上述案例采用的是同步方式下载图片&#xff0c;效率太低。异步方式如下&#xff08;线程&#xff09;&#xff1a; # target为目标函数&#xff1b;args中传入的是download函数的参数url threading.Threa…

TypeScript系列之-- 数组和元组类型

数组的定义&#xff1a; 第一种&#xff0c;可以在元素类型后面接上[] let list: number[] [1, 2, 3]; 第二种方式是使用数组泛型&#xff0c;Array<元素类型> let list: Array<number> [1, 2, 3]; 如果数组想每一项放入不同数据怎么办&#xff1f;用元组类型…

C# yolov8 TensorRT +ByteTrack Demo

C# yolov8 TensorRT ByteTrack Demo 目录 效果 说明 项目 代码 Form2.cs YoloV8.cs ByteTracker.cs 下载 参考 效果 说明 环境 NVIDIA GeForce RTX 4060 Laptop GPU cuda12.1cudnn 8.8.1TensorRT-8.6.1.6 版本和我不一致的需要重新编译TensorRtExtern.dll&…

深入分析 Android Activity (十)

文章目录 深入分析 Android Activity (十)1. Activity 的资源管理1.1 使用资源 ID 访问资源1.2 Drawable 资源1.3 使用 TypedArray 管理资源1.4 使用资源配置 2. Activity 的数据存储2.1 SharedPreferences2.2 文件存储2.3 SQLite 数据库2.4 ContentProvider 3. Activity 的性能…

倪老师是教我们如何去读书

之前一篇我们了解了倪老师&#xff0c;告诉我们如何去学习一些东西&#xff0c;今天这一篇&#xff0c;我把倪老师视频里面总结的几点&#xff0c;倪老师教我们如何去读书&#xff0c;我把一些小细节做了一个简单的整理&#xff0c;我们共同地去看&#xff0c;倪老师是教我们如…

JVS物联网、逻辑引擎、智能BI(重构优化)5.21功能新增说明

项目介绍 JVS是企业级数字化服务构建的基础脚手架&#xff0c;主要解决企业信息化项目交付难、实施效率低、开发成本高的问题&#xff0c;采用微服务配置化的方式&#xff0c;提供了 低代码数据分析物联网的核心能力产品&#xff0c;并构建了协同办公、企业常用的管理工具等&am…

SqliSniper:针对HTTP Header的基于时间SQL盲注模糊测试工具

关于SqliSniper SqliSniper是一款基于Python开发的强大工具&#xff0c;该工具旨在检测HTTP请求Header中潜在的基于时间的SQL盲注问题。 该工具支持通过多线程形式快速扫描和识别目标应用程序中的潜在漏洞&#xff0c;可以大幅增强安全评估过程&#xff0c;同时确保了速度和效…

使用 Django 连接 MySQL 数据库

文章目录 步骤一&#xff1a;安装必要的库和驱动步骤二&#xff1a;配置数据库连接步骤三&#xff1a;执行数据库迁移步骤四&#xff1a;开始使用 MySQL 数据库创建一个模型迁移模型到数据库使用模型进行数据操作创建新记录&#xff1a;查询记录&#xff1a;更新记录&#xff1…

Innodb Buffer Pool缓存机制(一)一条sql的执行过程

思维导图 石墨文档&#xff1a;https://shimo.im/mindmaps/NJkbnZV0ePINXzkR 一、SQL的执行 执行过程&#xff1a; 加载缓存数据&#xff0c;加载id为1的记录所在的整页数据&#xff08;相当于索引树的一个结点&#xff0c;16KB&#xff09;&#xff1b;写入更新数据的旧值到…

重生奇迹mu卡智力的方法

1、准备3个号A打手,B智力MM,C随意。 2、使用C匹配组队,但是不能选择自动进入队伍。 3、用A申请C的队伍,但是C不做通过处理。 4、用A组B,用快捷键D的方式。 5、所谓的卡智力就是智力MM可以给打手加属性加血&#xff0c;但是并不在一个队伍里享受经验&#xff0c;适用于MM不是…

如何提升网站运营效率

企业网站作为品牌展示、客户获取和商业目标实现的关键平台&#xff0c;其运营效率的提升对增强用户体验、搜索引擎排名和转化率至关重要。以下是一些有效技术和策略的介绍&#xff0c;旨在助力您提高网站运营的效率。 一、网站速度的优化 1.1 利用内容分发网络&#xff08;CD…

基于Docker的ROS开发

本文主要介绍如何使用Docker在Windows和Linux环境中部署并使用ROS&#xff0c;通过Docker Container运行ROS&#xff0c;可以方便我们在一个本地环境中运行多个ROS版本。 更多内容&#xff0c;访问专栏目录获取实时更新。 关于ROS的版本 参考ROS1 Distribution Wiki和ROS2 Dis…