【PDF】HTML通过dom节点生成pdf

1、简要描述

上一篇博客主要讲的是pdf文件转换成canvas,然后进行相关的画框截图操作。

【PDF】Canvas绘制PDF及截图

本篇博客主要讲html中dom如何生成pdf文件(前端生成pdf),后端生成pdf当然也可以,原理也是将html网页通过后端服务导出成pdf,然后css设置break-after:always;作为分页逻辑,但是我们不深入讲,这里着重讲前端生成pdf。

2、相关插件及知识

还是使用的老朋友jspdf插件和html2canvas

1、jspdf

"jspdf": "^2.5.1"

 使用方法:

import JsPDF from 'jspdf';const PDF = new jsPDF({unit: "mm", // 单位,本示例为mmformat: "a4", // 页面大小orientation: "portrait", // 页面方向,portrait: 纵向,landscape: 横向putOnlyUsedFonts: true, // 只包含使用的字体compress: true, // 压缩文档precision: 16, // 浮点数的精度
});// 或者const PDF = new JsPDF('p', 'mm', [210, 297]);<!-- 常用方法 -->
// 添加图片
PDF.addImage(imageData, // 此值可以为下面这些类型 string | HTMLImageElement | HTMLCanvasElement | Uint8Array | RGBAData'JPEG', // 转换后的格式x, // 被切割的imageData的横坐标y, // 被切割的imageData的纵坐标w, // 当前图片的宽度h, // 当前图片的高度
);
// 添加新的一页
PDF.addPage();
// 输出格式
PDF.output(type: "arraybuffer"): ArrayBuffer;
PDF.output(type: "blob"): Blob;
PDF.output(type: "bloburi" | "bloburl"): URL;
// 本地保存为pdf文件
PDF.save('lindadayo.pdf')

2、html2canvas

"html2canvas": "^1.4.1"
// 实例方法   
html2canvas(dom, config).then(function(canvas) {})

 config相关配置参考下图:

3、源码

1、dom结构

2、核心逻辑

这里为什么要将dom进行分区处理呢?请看第四点疑难解答中1、为什么要对dom进行分区操作?

    /*** 生成pdf* @param CommonPage 需要转换的dom节点* @param i 分区索引* @returns*/async generatePdf(CommonPage?: Element, childLen?: number) {PDF = new JsPDF('p', 'mm', [210, 297]); // pdf实例for (let i = 0; i < childLen; i++) {await asyncSingleAreaControl(CommonPage, i)}generateUploadPdf();},/*** 上传pdf文件*/async generateUploadPdf() {// 文件重命名,修改生成pdf后的文件名const pdfName = pdfNameHandle()const uri = PDF.output('blob')const file = await blobUriToFile(uri, pdfName)// 此时的file是File类对象,你可以选择上传到服务器噢~当然你也可以选择直接导出到前端// PDF.output('lindadayo.pdf');},/*** 单个分区生成pdf操作* @param CommonPage 父节点dom* @param i 分区索引* @returns*/async asyncSingleAreaControl(CommonPage, i) {const canvas = await singleHandle(CommonPage, i)await areaPage(canvas, i)},/*** 分区pdf处理* @param canvas 各个分区dom转换后的canvas* @param areaNo 分区索引*/areaPage(canvas, areaNo) {// 是否是第一个分区(作用于是否开始就addPage)const isFirstArea = areaNo === 0return new Promise((resolve, _reject) => {// a4纸宽高const A4Origin = {width: PDF.internal.pageSize.getWidth(),height: PDF.internal.pageSize.getHeight()}const contentWidth = canvas.width;/*** html2canvas放大3.125倍时精度丢失导致多了2像素* 3368: 高度285mm纸张html2canvas放大300dpi后像素* 3366:正常实际高度*/const contentHeight = canvas.height <= 3368 ? 3366 : canvas.height;const pageHeight = Math.round(contentWidth / A4Origin.width * A4Origin.height);let leftHeight = contentHeight;let position = 0;const imgWidth = A4Origin.width;const imgHeight = Math.ceil(A4Origin.width / contentWidth * contentHeight);const pageData = canvas.toDataURL('image/jpeg', 1);// 非首个分区,得先addPage,因为不然会少一页 && 大于某个范围才新增一页,避免因为浮点数计算精度造成多增一页if (!isFirstArea && leftHeight > 0) {PDF.addPage()}while (leftHeight > 0) {PDF.addImage(pageData, 'JPEG', 0, position, imgWidth + (isBrower() ? 0.62 : 0), imgHeight + (isBrower() ? 0.32 : 0));position -= A4Origin.height;leftHeight -= pageHeight// 大于某个范围才新增一页,避免因为浮点数计算精度造成多增一页if (leftHeight > 0) {PDF.addPage()}}resolve(true)})},/*** 单页pdf处理//  * @param root 总节点* @param index 分区索引*/async singleHandle(CommonPage, index) {// 报错Unable to find element in cloned iframe解决方法// getDiv在外部声明, 内部赋值try {getDiv = CommonPage.querySelector(`#CommonPageItemArea-${index}`)const res = await html2canvas(getDiv, {useCORS: true,allowTaint: true,scale: 3.125}).then(function(canvas) {return canvas})return res} catch (e) {console.log(e)}}

4、疑难解答

1、为什么要对dom进行分区操作?

其实如果你不使用html2canvas的参数scale,就没必要进行分区,但是在很多时候,你不放大canvas的话,会导致pdf中的图片很模糊,还有锯齿,所以要对canvas进行方法,但是放大后,会导致一些问题:生成pdf后,超过15000px以后的dom会有样式丢失,所以得对dom进行分区操作,让每个分区的dom高度 * 放大倍数不超过15000px。我们一般都会导出a4纸大小,a4纸宽高是210mm*297mm,换算成像素是793.29px * 1122.52px,如果你选择放大两倍,那么,单页高度就是2245px,结论为一个分区能够放六个a4纸高度的dom,所以你在开发页面时,就要做好这种页面结构噢~

2、html2canvas仍然报图片出错/跨域的问题,即使后端oss已经解决跨域了

报错Error loading image

这个涉及知识点:img标签实例化获取属于非跨域操作,Image类实例化属于跨域操作,所以得再html2canvas依赖中打补丁,当图片是你本地的静态图片,那不需要转,还是按照Image实例化来做,当图片已经是base64格式的话,也不需要转,赋值给img标签,否则的话加上随机数。

/dist/html2canvas.js 第5759行

3、报错Unable to find element in cloned iframe解决方法

在分区中循环处理dom生成canvas时会报出这种错误,原因是html2canvas第一参数的变量应该设置为全局变量而不应该是局部变量

    try {getDiv = CommonPage.querySelector(`#CommonPageItemArea-${index}`)const res = await html2canvas(getDiv, {useCORS: true,allowTaint: true,scale: 3.125}).then(function(canvas) {return canvas})return res} catch (e) {console.log(e)}

4、dom-to-image和html2canvas相比,哪个更优?

dom-to-image是一个js库,可以将任意dom节点转换为矢量(SVG)或光栅(PNG或JPEG)图像。和html2canvas相比的话,算是一个新起之秀,更轻巧,相同点就是都会先将dom转成canvas进行操作,所以在dom层级深和多的情况下,还是建议使用html2canvas这种老牌插件

5、生成pdf里图片缺失

那是因为图片转换及获取是异步的,需要时间渲染,所以生成pdf的步骤应该在图片完全加载完之后,由此我们可以加个定时器来循环判断全部图片是否加载完成,加载完成再进行生成操

    /*** CommonPage生成dom渲染完成* @param callback*/commonPageLoadFinish(callback) {nextTick(() => {// 生成节点const CommonPage = document.querySelector('#CommonPage')const childLen = CommonPage.querySelectorAll('.CommonPageItemArea').lengthconsole.log('分区数量', childLen)if (childLen > 0) {let timer = null;// 监听页面中所有转base64图片是否已生成完毕,如果已生成完毕,则进入下一步与dom相关的操作timer = setInterval(() => {// isImageAllCompleted.sum => 图片总数量, isImageAllCompleted.loadSum => 目前图片已经加载完的数量if (isImageAllCompleted.sum === isImageAllCompleted.loadSum) {clearInterval(timer)timer = nullcallback(CommonPage, childLen)}}, 1000)}})}

将生成pdf步骤作为回调函数放在上述函数里

commonPageLoadFinish(generatePdf)

 那在组件中如何监听图片是否加载完成呢?按照以下代码来写

    onMounted(() => {nextTick(() => {// commonRef.value 为某dom的refs// 被动检测是否有图片, 无则直接进入主逻辑const img = commonRef.value.querySelectorAll('img');if (!img.length) return methods.successCallback();let imgSum = 0;asyncImgCompLoad(img).then((res) => {res.forEach(() => imgSum++);// 设置图片总数量和加载数量setImageAllCompleted({ sum: isImageAllCompleted.sum + img.length, loadSum: imgSum + isImageAllCompleted.loadSum });})})})async asyncImgCompLoad(imgList) {const promiseList = []for await (const item of imgList) {promiseList.push(new Promise((res, rej) => {// 参数没有被赋值if (!item.src) {rej(false)}if (item.complate) {res(true)} else {item.addEventListener('load', () => {res(true)})// 图片被赋值,但是赋的是错误的值item.addEventListener('error', () => {rej(false)})}}))}return Promise.allSettled(promiseList)}

 6、生成的pdf里,单页底部有白边?

在使用PDFjs插件时候,加入需要导出a4纸大小,那么很多童鞋就会将宽高固定设置为210mm, 297mm,但是实际上不是整数,是小数,所以获取时按照下述方法获取

        // a4纸宽高const A4Origin = {width: PDF.internal.pageSize.getWidth(),height: PDF.internal.pageSize.getHeight()}

PDF分页核心源码

        // a4纸宽高const A4Origin = {width: PDF.internal.pageSize.getWidth(),height: PDF.internal.pageSize.getHeight()}const contentWidth = canvas.width;/*** html2canvas放大3.125倍时精度丢失导致多了2像素* 3368: 高度285mm纸张html2canvas放大300dpi后像素* 3366:正常实际高度*/const contentHeight = canvas.height <= 3368 ? 3366 : canvas.height;const pageHeight = Math.round(contentWidth / A4Origin.width * A4Origin.height);let leftHeight = contentHeight;let position = 0;const imgWidth = A4Origin.width;const imgHeight = Math.ceil(A4Origin.width / contentWidth * contentHeight);const pageData = canvas.toDataURL('image/jpeg', 1);// 非首个分区,得先addPage,因为不然会少一页 && 大于某个范围才新增一页,避免因为浮点数计算精度造成多增一页if (!isFirstArea && leftHeight > 0) {PDF.addPage()}while (leftHeight > 0) {PDF.addImage(pageData, 'JPEG', 0, position, imgWidth + (isBrower() ? 0.62 : 0), imgHeight + (isBrower() ? 0.32 : 0));position -= A4Origin.height;leftHeight -= pageHeight// 大于某个范围才新增一页,避免因为浮点数计算精度造成多增一页if (leftHeight > 0) {PDF.addPage()}}

 有两个地方可能童鞋们没看懂,1、首先为啥非首个分区,得先addPage呢?因为PDF默认就有一页,所以你能够直接addImage而不出错,然后后续PDF想要新增一页,都得先addPage,这时候默认背景颜色是白色的,然后再将canvas转成图片,贴到这白板上的,所以你看到PDF文档里有白边,那毫无疑问,就是贴的图片没占完那一页,并且火狐浏览器和谷歌浏览器还有一些细微的差别所以你这就得一点一点微调来达到最佳显示效果。2、为啥addIMage时,里面传的参数不同呢?

PDF.addImage(pageData, 'JPEG', 0, position, imgWidth + (isBrower() ? 0.62 : 0), imgHeight + (isBrower() ? 0.32 : 0));

 这就是浏览器差异问题,火狐浏览器不仅底部有白边,侧面也有白边,相比之下谷歌要更兼容一些。

 7、pdf生成的File文件对象,想要先传入oss,再通过服务端下载怎么实现?

这其实就涉及到大文件上传技术了,因为pdf稍微大点可能都上百M,一般都不会一次性上传完的,所以得做切片上传,然后在服务端合并上传到oss,最后将oss路径地址返回给前端,前端通过这地址去下载。当然具体的大文件上传我就不写在这篇博客了,下一篇博客我将着重讲大文件上传如何写噢~

--- 有问题可以随时评论噢~喜欢的请点赞收藏啦 ---

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IntelliJ IDEA 版本控制

IntelliJ IDEA 版本控制&#xff08;VCS&#xff09;日常使用方法备忘 1、搁置更改 2、移至另一个更改列表 对于工程项目中的配置文件&#xff0c;已经在本地修改但是不能提交&#xff0c;如果在提交项目代码时全选变更的文件&#xff0c;可能会误提交配置文件&#xff0c;此…

Monocular 3D Object Detection with Depth from Motion 论文学习

论文链接&#xff1a;Monocular 3D Object Detection with Depth from Motion 1. 解决了什么问题&#xff1f; 从单目输入感知 3D 目标对于自动驾驶非常重要&#xff0c;因为单目 3D 的成本要比多传感器的方案低许多。但单目方法很难取得令人满意的效果&#xff0c;因为单张图…

Stable Diffusion + EbSynth + ControlNet 解决生成视频闪烁

一、安装 1.1、安装ffmpeg 下载地址&#xff1a; 解压&#xff0c;配置环境变量 E:\AI\ffmpeg\bin 检查是否安装成功 1.2、安装SD的 EbSynth 插件 插件地址 https://github.com/s9roll7/ebsynth_utility 报错&#xff1a;ModuleNotFoundError: No module named extension…

13.postgresql--函数

文章目录 标量示例复合示例有返回值函数返回voidRETURN NEXT ,RETURN QUERYRETURN EXECUTEIF THEN END IFFOREACH,LOOPSLICE &#xff08;1&#xff09;如果函数返回一个标量类型&#xff0c;表达式结果将自动转行成函数的返回类型。但要返回一个复合&#xff08;行&#xff09…

OpenCVForUnity(六)图像的对比度和亮度

文章目录 前言公式讲解Unity嵌套循环实现使用convertTo实现亮度和对比度调整:伽马矫正 前言 图片处理中这也是非常常用的功能,下面我们一起来学习一下如何在OpenCVForUnity中修改图像的对比度亮度 图像处理中的常见算子可以将一个或多个输入图像转换为输出图像。这些变换包括点…

RISCV - 1 RV32/64G指令集清单

RISCV - 1 RV32/64G指令集清单 1 RV32/64G指令类型2 RV32I 基本指令集3 RV64I基础指令集&#xff08;除了RV32I)4 RV32/RV64 Zifencei标准扩展5 RV32/RV64 Zicsr标准扩展6 RV32M标准扩展7 RV64M标准扩展&#xff08;除了RV32M)8 RV32A标准扩展9 RV64A标准扩展&#xff08;除了R…

(栈队列堆) 剑指 Offer 09. 用两个栈实现队列 ——【Leetcode每日一题】

❓ 剑指 Offer 09. 用两个栈实现队列 难度&#xff1a;简单 用两个栈实现一个队列。队列的声明如下&#xff0c;请实现它的两个函数 appendTail 和 deleteHead &#xff0c;分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素&#xff0c;deleteHead …

机器学习32:《推荐系统-V》再谈召回、打分和重排

在《机器学习28&#xff1a;推荐系统-概述》一文中&#xff0c;笔者概述了推荐系统的基本术语和一般架构&#xff0c;通过【推荐系统 I&#xff5e;IV】系列课程的学习&#xff0c;相信读者对推荐系统已经有了一定的理解。本节&#xff0c;我们再来回顾一下推荐系统的核心环节—…

泛在电力物联网、能源互联网与虚拟电厂

导读&#xff1a;从能源互联网推进受阻&#xff0c;到泛在电力物联网名噪一时&#xff0c;到虚拟电厂再次走向火爆&#xff0c;能源领域亟需更进一步的数智化发展。如今&#xff0c;随着新型电力系统建设推进&#xff0c;虚拟电厂有望迎来快速发展。除了国网和南网公司下属的电…

浅谈自动化测试工具 Appium

目录 前言&#xff1a; 一、简单介绍 &#xff08;一&#xff09;测试对象 &#xff08;二&#xff09;支持平台及语言 &#xff08;三&#xff09;工作原理 &#xff08;四&#xff09;安装工具 二、环境搭建 &#xff08;一&#xff09;安装 Android SDK &#xff0…

欧姆龙以太网模块如何设置ip连接 Kepware opc步骤

在数字化和自动化的今天&#xff0c;PLC在工业控制领域的作用日益重要。然而&#xff0c;PLC通讯口的有限资源成为了困扰工程师们的问题。为了解决这一问题&#xff0c;捷米特推出了JM-ETH-CP转以太网模块&#xff0c;让即插即用的以太网通讯成为可能&#xff0c;不仅有效利用了…

Unity自定义后处理——Vignette暗角

大家好&#xff0c;我是阿赵。   继续说一下屏幕后处理的做法&#xff0c;这一期讲的是Vignette暗角效果。 一、Vignette效果介绍 Vignette暗角的效果可以给画面提供一个氛围&#xff0c;或者模拟一些特殊的效果。 还是拿这个角色作为底图 添加了Vignette效果后&#xff0…