vue通过html2canvas+jspdf生成PDF问题全解(水印,分页,截断,多页,黑屏,空白,附源码)

前端导出PDF的方法不多,常见的就是利用canvas画布渲染,再结合jspdf导出PDF文件,代码也不复杂,网上的代码基本都可以拿来即用。 如果不是特别追求完美的情况下,或者导出PDF内容单页的话,那么基本上也就满足业务需求了。但是,如果你需要导出PDF的内容又多又复杂呢?

目录

1.PDF常规导出 

2.问题1:水印

3.问题2:导出黑屏与空白

4.问题3:分页与截断

5.结束语


1.PDF常规导出 

        刚接触这个需求的时候,lz想的是能实现将当前网页内容导出PDF下载即可。因为这也符合常规业务需求逻辑,也没有考虑其他的,因此参考了几篇网上的博文,很快就选定了vue项目中利用html2canvas+jspdf来实现导出PDF。

        所以,很快就实现了。在此基础上,还满足了可以动态进行PDF分页dom节点的划分 ,并且觉得就这?就这?蜜汁自信(不自量力)的lz还马上写了一篇博客,下面附上博客地址,内附源码。

vue2利用html2canvas+jspdf动态生成多页PDF_html2pdf 多页-CSDN博客

 如果诸位的导出pdf内容很简单,那上面的博文应该大概或许能助尔等一臂之力。导出过程中有其他疑难杂症的咱们接着往下看。

2.问题1:水印

        给导出的PDF加上水印,这个需求很合理吧?毕竟版权和文件安全意识还是要有的。对于这个需求,想要解决也很简单。对应的依赖如watermark-dom,但是lz在用的时候发现这个水印一旦加上就甩都甩不掉,全局都附带上了,而且在导出的时候,PDF文件上竟然也没带上水印?wtf?

不信邪的可以试试,也有可能是项目的差异性呢,呵呵哒...

依赖安装

npm install watermark-dom --save

在你要用到水印的页面引入:

import watermark from "watermark-dom";export default {name:'PDF',data() {return {compony:"多页PDF导出"}},mounted() {//该水印依赖可用,但是导出文件后不会带上this.$nextTick(() => { var waterdom = document.getElementById('pdfinsurancepdf'); var height = waterdom.offsetHeight;console.log(waterdom,height)watermark.load({watermark_id: 'wm_div_id',watermark_parent_node:'pdfinsurancepdf',   //水印插件挂载的父元素element,不输入则默认挂在body上watermark_txt: "PDF导出",                  //水印的内容watermark_fontsize:'24px',                  //水印字体大小watermark_x_space:100,              //水印x轴间隔watermark_y_space:100,      })})},destroyed() {watermark.remove();},
}

 如果上面的方法不好用,你可以试试这个推荐方法:

通过js操作dom的方式,创建vue自定义指令,来动态的给dom元素加上水印。

优点创建后全局可用,精准性高,指哪打哪,而且导出的PDF精准的附带了水印,由于是通过js来实现的,可塑性强,可通过直接改js文件来二次调整适应需求变化

在untils目录下创建watermark.js文件:

const globalCanvas = null
const globalWaterMark = null// watermark 样式
let style = `
display: block;
overflow: hidden;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-repeat: repeat;
pointer-events: none;`const getDataUrl = ({ font, fillStyle, textAlign, textBaseline, text, rotate = -20 }) => {font = font || '25px normal'fillStyle = fillStyle || 'rgba(180, 180, 180, 0.2)'text = text || ''const canvas = globalCanvas || document.createElement('canvas')const ctx = canvas.getContext('2d') // 获取画布上下文ctx.rotate((rotate * Math.PI) / 180)ctx.font = fontctx.fillStyle = fillStylectx.textAlign = textAlign || 'left'ctx.textBaseline = textBaseline || 'middle'ctx.fillText(text, canvas.width / 10, canvas.height / 2)return canvas.toDataURL('image/png', 1) // 第二个参数为质量
}const setWaterMark = (el, binding) => {//const parentElement = el.parentElementconst parentElement = el// 获取对应的 canvas 画布相关的 base64 urlconst url = getDataUrl(binding)// 创建 waterMark 父元素const waterMark = globalWaterMark || document.createElement('div')waterMark.className = 'water-mark' // 方便自定义展示结果style = `${style}background-image: url(${url});`waterMark.setAttribute('style', style)// 将对应图片的父容器作为定位元素parentElement.setAttribute('style', 'position: relative;')// 将图片元素移动到 waterMark 中parentElement.appendChild(waterMark)
}// 监听 DOM 变化
const createObserver = (el, binding) => {const waterMarkEl = el.parentElement.querySelector('.water-mark')const observer = new MutationObserver((mutationsList) => {if (mutationsList.length) {const { removedNodes, type, target } = mutationsList[0]const currStyle = waterMarkEl?waterMarkEl.getAttribute('style'):'';// 证明被删除了if (removedNodes[0] === waterMarkEl) {observer.disconnect()// 重新添加水印,dom监听init(el, { value: binding })} else if (type === 'attributes' && target === waterMarkEl && currStyle !== style) {waterMarkEl.setAttribute('style', style)}}})observer.observe(el.parentElement, {childList: true,attributes: true,subtree: true})
}// 初始化
const init = (el, binding) => {// 设置水印setWaterMark(el, binding.value)// 启动监控createObserver(el, binding.value)
}// 定义指令配置项
const directives = {inserted(el, binding) {init(el, binding)}
}export default {name: 'watermark',directives
}

main.js进行全局指令注入:

import waterMark from '@/utils/watermark.js'
Vue.directive('watermark', waterMark.directives)

 后面就可以在你需要导出PDF的dom上附带指令添加水印即可:

<div id="page1" v-watermark="{ text: '这是水印' }"><img src="@/assets/pdfhead.png" style="width:100%;height:auto;" alt="封面" />
</div>

3.问题2:导出黑屏与空白

        原因分析:关于这个问题,lz是在苹果移动端遇到的,谷歌浏览器和安卓环境下都能正常导出,但是在ios移动端导出时就出现了黑屏的情况,而且出现黑屏的这一段pdf刚好涉及到多页pdf,单页的pdf却是正常的。网上说各种原因的都有,看的很头痛。直到lz无意中看到了一个说法:

tips:这里PDF大小指常规a4纸大小,A4大小,210mm x 297mm

不同主流浏览器以及移动终端针对canvas画布的大小有对应的限制,而导出PDF原理恰好就是通过将当前网页内容通过canva渲染成图片再导出pdf的。

安卓端绘制canvas大小转化成PDF,大概是10几页

苹果端绘制canvas大小转化成PDF,大概5~6页

而lz黑屏的地方,正好是要一次性生成9页pdf,按照这个思路一测,果然真相大白

解决办法:就是分页节点细化,避免绘制canvas画布大小超出限制。

4.问题3:分页与截断

        对于将网页内容导出pdf,出现截断问题算是老生常谈的问题了。对于固定的内容,我们可以指定分页节点,再合并导出一个PDF文件,不用考虑分页出现截取的情况。但是对于动态的dom内容,无法指定分页节点,我们在一股脑导出,让其自动分页的情况下,就很容易出现文字被截取,表格被截取的情况。

网上的解决办法也五花八门,什么限制高度啊,动态计算文字高度啊,动态计算分页节点啊。一看就头大。直到lz看到了一篇这样的示例:

vue-pdf2: 纯前端导出版本2(回炉重制版)已解决分页截断,页眉,页脚,页码,页边距,模糊等情况

根据封装的参数来看,也算是能满足很多常规业务需求了。 

/*** 生成pdf(处理多页pdf截断问题)* @param {Object} param* @param {HTMLElement} param.element - 需要转换的dom根节点* @param {number} [param.contentWidth=550] - 一页pdf的内容宽度,0-595* @param {number} [param.contentHeight=800] - 一页pdf的内容高度,0-842* @param {string} [param.outputType='save'] - 生成pdf的数据类型,添加了'file'类型,其他支持的类型见http://raw.githack.com/MrRio/jsPDF/master/docs/jsPDF.html#output* @param {number} [param.scale=window.devicePixelRatio * 2] - 清晰度控制,canvas放大倍数,默认像素比*2* @param {string} [param.direction='p'] - 纸张方向,l横向,p竖向,默认A4纸张* @param {string} [param.fileName='document.pdf'] - pdf文件名,当outputType='file'时候,需要加上.pdf后缀* @param {number} param.baseX - pdf页内容距页面左边的高度,默认居中显示,为(A4宽度 - contentWidth) / 2)* @param {number} param.baseY - pdf页内容距页面上边的高度,默认 15px* @param {HTMLElement} param.header - 页眉dom元素* @param {HTMLElement} param.footer - 页脚dom元素* @param {HTMLElement} param.headerFirst - 第一页的页眉dom元素(如果需要指定第一页不同页眉时候再传这个,高度可以和其他页眉不一样)* @param {HTMLElement} param.footerFirst - 第一页页脚dom元素* @param {string} [param.groupName='pdf-group'] - 给dom添加组标识的名字,分组代表要进行分页判断,当前组大于一页则新起一页,否则接着上一页* @param {string} [param.itemName='pdf-group-item'] - 给dom添加元素标识的名字,设置了itemName代表此元素内容小于一页并且不希望被拆分,子元素也不需要遍历,即手动指定深度终点,优化性能* @param {string} [param.editorName='pdf-editor'] - 富文本标识类* @param {string} [param.tableSplitName='el-table__row'] - 表格组件内部的深度节点* @param {string} [param.splitName='pdf-split-page'] - 强制分页,某些情况下可能想不同元素单独起一页,可以设置这个类名* @param {string} [param.isPageMessage=false] - 是否显示当前生成页数状态* @param {string} [param.isTransformBaseY=false] - 是否将baseY按照比例缩小(一般固定A4页边距时候可以用上)* @param {Array} [param.potionGroup=[]] - 需要计算位置的元素属性,格式是 data-position='xxx',需要同时在节点上加上param.itemName,如<p data-position='p-position' class='pdf-group-item'></p>* @returns {Promise} 根据outputType返回不同的数据类型,是一个对象*/export class PdfLoader {...
}

测试用例效果:

5.结束语

        如果你要问最终lz选择了什么方法来解决?我会告诉你,我选择了交给后端。不可否认,前端是万能的,确实能实现将网页内容导出PDF,但是话又说回来,经过和产品沟通发现,后端Java通过依赖工具包生成的PDF竟然效果更好?

        所以,需要复杂的PDF导出就让后端来吧!别问,问就是前端和后端的相爱相杀~

        别遇到什么事都自己抗,沟通最重要~

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

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

相关文章

ChatGPT+Simple Mind Map生成思维导图:快速提升学习效率

一、告别杂乱笔记&#xff0c;一键生成清晰思维导图&#xff01; 最近开始学习网络安全&#xff0c;一头扎进了各种协议、漏洞、防御机制的海洋中。信息量巨大&#xff0c;知识点零散&#xff0c;让我很快便陷入了“知识焦虑”——笔记越记越多&#xff0c;却越来越混乱&#…

第50课 Scratch入门篇:放烟花

放烟花 故事背景: 水在一个宁静的小镇上,生活着一位充满好奇心和创造力的小朋友。   有一天晚上,小镇的天空格外黑暗,星星也躲在了云层后面。小朋友望着黑漆漆的夜空,心想:要是能有一场绚丽的烟花表演,那该多好啊!于是,他决定用自己所学的 Scratch 编程知识来创造一…

通过域名无法访问不到网站,IP可正常访问(DNS污染)

一 DNS被污染 就在刚刚突然访问不到csdn&#xff0c;域名无法访问如下图&#xff1a; 确认DNS是否解析有问题 1 ping 域名 先ping一下域名&#xff0c;ping 域名后得到ip, ping通了如下图&#xff1a; 2 使用IP访问测试 通过ip再访问网站&#xff0c;ip可以正常访问如下图&…

.NetCore+vue3上传图片 Multipart body length limit 16384 exceeded.

实现目标。点击图片上传头像 效果图 前端部分图片上传关键代码 <div class"avatar-wrap"><el-imagestyle"width: 154px; height: 154px":src"form.headPic":fit"fit"/></div><div class"upload-box"…

vllm使用BitAndBytes量化模型失败

ValueError: BitAndBytes quantization with TP or PP is not supported yet 使用加载hf模型时&#xff0c;使用load_in_8bit来量化模型&#xff08;底层其实是调用bitsandbytes来量化&#xff09;&#xff1a; import argparse import os import torchdef parse_arguments()…

RP2040 C SDK RTC功能使用

RP2040 C SDK RTC功能使用 &#x1f4cd;《RP2040 C SDK串口功能使用》&#x1f955;RP2040 RTC API官方文档说明&#xff1a;https://www.raspberrypi.com/documentation/pico-sdk/hardware.html#group_hardware_rtc&#x1f955;官方例程参考&#xff1a;https://github.com/…

【MySQL】MySQL中表的增删改查——(基础篇)(超详解)

前言&#xff1a; &#x1f31f;&#x1f31f;本期讲解关于MySQL中CDUD的基础操作&#xff0c;希望能帮到屏幕前的你。 &#x1f308;上期博客在这里&#xff1a;http://t.csdnimg.cn/fNldO &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 目录 …

【Python篇】详细学习 pandas 和 xlrd:从零开始

文章目录 详细学习 pandas 和 xlrd&#xff1a;从零开始前言一、环境准备和安装1.1 安装 pandas 和 xlrd1.2 验证安装 二、pandas 和 xlrd 的基础概念2.1 什么是 pandas&#xff1f;2.2 什么是 xlrd&#xff1f; 三、使用 pandas 读取 Excel 文件3.1 读取 Excel 文件的基础方法…

Git常用命令备忘

Git常用命令备忘 Git已经成为程序员日常工具之一&#xff0c;那些Git基本的命令&#xff0c;每天都要用得命令你都记住了吗&#xff1f;如果还没的话&#xff0c;笔者整理了一份清单&#xff0c;以备不时之需所用。 ####三个基本概念 工作区(Workspace)是计算机中项目的根目…

熬夜后补救措施

人体的肝功能问题 直接体现在体态和容颜上 伤肝 三大坏行为 熬夜后补救 *补充养b族、口、、锌、硒 加强代谢 能力 (1)另外熬夜后一定要多喝水 提升身体代谢能力 (2)谷肤甘肽清肝 肝脏排毒&#xff0c;减轻负拒 (3)水飞前含量高点 &#xff08;4)熬夜出更多油 容易长痘 需要清…

springboot项目--后端问题记录

springboot项目后端记录 前言一、包1. lombok--自动生成勾子方法作用依赖使用 2. Validated--自动校验作用依赖使用一般参数校验实体参数校验 结论 3. JWT(json web taken) 令牌生成什么是takenJWT包依赖使用获取taken校验 封装的工具类使用 二、处理技巧1. 全局异常处理作用代…

JDBC详细知识点和操作

javaweb的作用&#xff0c;属于中间者&#xff0c;负责逻辑处理 这三部分互相协作组成了网页 javaweb也就是这三部分 一.数据库部分&#xff08;略&#xff09; 二.javaweb程序 1.JDBC 概念&#xff1a;通过java代码操作数据库 数据库种类有很多&#xff0c;比如Oracle&a…

C高级编程 第十六天(树 二叉树)

1.树 1.1结构特点 非线性结构&#xff0c;有一个直接前驱&#xff0c;但可能有多个直接后继有递归性&#xff0c;树中还有树可以为空&#xff0c;即节点个数为零 1.2相关术语 根&#xff1a;即根结点&#xff0c;没有前驱叶子&#xff1a;即终端结点&#xff0c;没有后继森…

6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)

目录 一.堆(Heap)的基本介绍 二.堆的常用操作&#xff08;以小根堆为例&#xff09; 三.实现代码 3.1 堆结构定义 3.2 向下调整算法* 3.3 初始化堆* 3.4 销毁堆 3.4 向上调整算法* 3.5 插入数据 3.6 删除数据 3.7 返回堆顶数据 四.下篇内容 1.堆排序 2.TopK问题 一…

LeetCode第414场周赛(第一题)

目录 一&#xff1a;题目&#xff1a;3280. 将日期转换为二进制表示 一&#xff1a;题目&#xff1a;3280. 将日期转换为二进制表示 给你一个字符串 date&#xff0c;它的格式为 yyyy-mm-dd&#xff0c;表示一个公历日期。 date 可以重写为二进制表示&#xff0c;只需要将年…

一款免费开源功能丰富的看图软件NeeView

NeeView 是一款功能丰富的图像查看软件&#xff0c;它以其独特的浏览体验和广泛的支持格式受到用户的欢迎。NeeView 不仅可以浏览普通的图像文件&#xff0c;还能够查看压缩包内的图片、预览PDF文档甚至播放视频文件。 NeeView 的主要特点&#xff1a; 多格式支持&#xff1a…

高频知识总结 | 算法题如何刷?我的高效刷题方法

1. 前言 所以本文章主要就是详细的告诉大家我的刷题方法论&#xff0c;可以做一个参考&#xff0c;如果你觉得我的分享对你有帮助&#xff0c;希望多多点赞收藏评论转发支持&#xff01; 2. 算法题到底该怎么刷&#xff1f; 回答这个问题只需要两个点&#xff1a;一是刷什么…

JavaWeb笔记整理13——Mybatis

目录 Mybatis介绍 删除 预编译SQL SQL注入 新增 更新 查询 数据封装 条件查询 XML映射文件 动态SQL 更新案例 foreach Mybatis介绍 删除 预编译SQL SQL注入 新增 更新 查询 数据封装 条件查询 XML映射文件 动态SQL <if> 更新案例<set> foreach &l…

AIGC简化文件管理:Python自动重命名Word和PDF文件

1.背景 大家应该也有遇到&#xff0c;自己电脑有很多文件命名不合理的文件&#xff0c;比如&#xff1a;文件1、想法3 &#xff0c;当你长时间再看到这个文件的时候&#xff0c;已经很难知道文件内容。 今天我们将借助AIGC的编码能力&#xff0c;帮我们生成一个批量改文件名的…

Linux内核编程(十五)网络设备驱动

本文目录 一、常见的网络协议二、网络模型二、网络数据的封装和解封装二、抓包工具wireshark三、传输介质四、RJ-45接口1. 百兆网口2. 千兆网口 五、PHY芯片1. 网络变压器的作用2. PHY芯片类型判断 六、MAC控制器七、MAC控制器与PHY芯片连接方式1. MII接口方式&#xff08;百兆…