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

news2024/9/20 1:00:40

前端导出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 = font
  ctx.fillStyle = fillStyle
  ctx.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.parentElement
  const parentElement = el
  // 获取对应的 canvas 画布相关的 base64 url
  const 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.coloradmin.cn/o/2103993.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

我的大模型岗位面试总结!太卷了!!!—我面试了24家大模型岗位 只拿了9个offer!

这段时间面试了很多家&#xff08;共24家&#xff0c;9个offer&#xff0c;简历拒了4家&#xff0c;剩下是面试后拒的&#xff09;&#xff0c;也学到了超级多东西。 大模型这方向真的卷&#xff0c;面试时好多新模型&#xff0c;新paper疯狂出&#xff0c;东西出的比我读的快…

传统CV算法——基于opencv的答题卡识别判卷系统

基于OpenCV的答题卡识别系统&#xff0c;其主要功能是自动读取并评分答题卡上的选择题答案。系统通过图像处理和计算机视觉技术&#xff0c;自动化地完成了从读取图像到输出成绩的整个流程。下面是该系统的主要步骤和实现细节的概述&#xff1a; 1. 导入必要的库 系统首先导入…

误删的PPT怎么恢复回来?

在日常工作和学习中&#xff0c;PPT已成为我们不可或缺的工具。然而&#xff0c;有时不小心误删重要的PPT文件&#xff0c;可能会让人倍感焦虑。别担心&#xff0c;本文将为你提供几种实用的方法&#xff0c;帮助你轻松恢复误删的PPT文件。 一、从回收站恢复 当你误删文件时&…

【Grafana】Prometheus结合Grafana打造智能监控可视化平台

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

香港一带一路研究院国际事务研究中心副主任陈景才阐述香港在一带一路建设及区块链金融领域的关键作用

2024年8月28日&#xff0c;香港金管局举行Ensemble项目沙盒&#xff08;以下简称沙盒&#xff09;启动仪式&#xff0c;并宣布首阶段试验将涵盖四大代币化资产用例主题&#xff0c;标志着金融业在代币化技术的实际应用进程中迈出重要一步。香港一带一路研究院国际事务研究中心副…

解剖学上合理的分割:通过先验变形显式保持拓扑结构|文献速递--基于深度学习的医学影像病灶分割

Title 题目 Anatomically plausible segmentations: Explicitly preserving topology through prior deformations 解剖学上合理的分割&#xff1a;通过先验变形显式保持拓扑结构 01 文献速递介绍 进行环向应变或壁厚度的计算&#xff0c;这些测量通常用于诊断肥厚性心肌病…

IDEA 安装lombok插件不兼容的问题及解决方法

解决&#xff1a;IDEA 安装lombok插件不兼容问题&#xff0c;plugin xxxx is incompatible 一、去官网下载最新的2024版本 地址传送通道&#xff1a; lombok插件官网地址https://plugins.jetbrains.com/plugin/6317-lombok/versions/stable 二、修改参数的配置 在压缩包路径…

理解C++的【内部链接】和【外部链接】

一、前言 最近在看《大规模C程序设计》一书&#xff0c;看第一章关于内部链接和外部链接这部分时&#xff0c;有点不太明白。通过书本理解和网上查阅文献&#xff0c;在此记录一下自己对这部分知识点的理解。 首先&#xff0c;提几个问题&#xff1a; 什么是内部链接&#x…

全域运营公司哪家做得好?全域运营系统综合评测结果揭晓!

作为当前火爆的风口项目&#xff0c;一直以来&#xff0c;全域运营都以其广阔的业务范围和巨大的收益潜力吸引着一批又一批的创业者入局分羹&#xff0c;使得全域运营公司哪家做得好等问题一度成为了相关创业者交流群内的讨论重点。 从目前的市场情况来看&#xff0c;由于进入…

定期加强医疗器械维修技能学习重要性

医学影像技术是现代医疗的重要支撑,是辅助临床诊断和治疗不可或缺的技术手段。影像医疗设备成像质量的优劣程度在一定程度上决定了疾病诊断结果的准确性,而术中使用的影像设备的优劣甚至可能影响手术的成功率。因此保证设备正常使用是重中之重&#xff0c;设备售后维修保养也就…

Langchain-Chatchat+Qwen实现本地知识库

1.基础介绍 Langchain-Chatchat一种利用 langchain 思想实现的基于本地知识库的问答应用&#xff0c;目标期望建立一套对中文场景与开源模型支持友好、可离线运行的知识库问答解决方案。大致过程包括加载文件 -> 读取文本 -> 文本分割 -> 文本向量化 -> 问句向量化…

《OpenCV计算机视觉》—— 对图片的各种操作

文章目录 1、安装OpenCV库2、读取、显示、查看图片3、对图片进行切割4、改变图像的大小5、图片打码6、图片组合7、图像运算8、图像加权运算 1、安装OpenCV库 使用pip是最简单、最快捷的安装方式 pip install opencv-python3.4.2还需要安装一个包含了其他一些图像处理算法函数的…

vector中的push_back()和emplace_back()的区别、以及使用场景

目录 前言 1. 基本区别 2. 性能差异 3. 构造参数传递 4. 使用场景总结 前言 push_back() 更适合在已经有对象实例的情况下使用。emplace_back() 则更适合需要在容器内部直接构造对象的场景&#xff0c;特别是在性能敏感的情况下。 1. 基本区别 push_back(): 作用&#xff…

酒店智能触摸开关在酒店管理中的作用

在众多智能化设备中&#xff0c;酒店智能触摸开关以其便捷性、高效性和节能环保的特性&#xff0c;正逐步成为提升住客体验、优化酒店运营管理的关键元素。本文将深入探讨酒店智能触摸开关在酒店管理中的多重作用。 一、提升住客体验&#xff0c;增强服务品质 便捷操作&#xf…

护眼灯真的可以保护眼睛吗?曝光劣质护眼台灯常见的三个特征

护眼灯真的可以保护眼睛吗&#xff1f;随着时代的发展&#xff0c;我们注意到越来越多的孩子开始佩戴眼镜。这一趋势引起了许多细心家长的关注&#xff0c;他们认识到这不仅是个别情况&#xff0c;而是现代生活方式和环境对孩子视力健康的挑战。自然而然地&#xff0c;“儿童是…

【淘宝采集项目经验分享】商品评论采集 |商品详情采集 |关键词搜索商品信息采集

商品评论采集 1、输入商品ID 2、筛选要抓取评论类型 3、填写要抓取的页数 4、立刻提交-启动测试 5、等爬虫结束后就可以到“爬取结果”里面下载数据 商品详情采集 1、输入商品ID 2、立刻提交-启动爬虫 3、等爬虫结束后就可以到“爬取结果”里面下载数据 taobao.item_…

报名开启!IDEA研究院编程语言MoonBit全球编程创新挑战赛启动

"懂语言者得天下"。探索编程之革新&#xff0c;参与AI时代编程语言之构建。2024年MoonBit全球编程创新挑战赛&#xff0c;为你开启&#xff01; 我们向每一位怀揣才华与创意的编程爱好者发出邀请&#xff0c;一起在这场创新与挑战的盛会中&#xff0c;将理想照进现实…

针对IP专用https证书的详细申请教程

IP&#xff08;Internet Protocol&#xff09;地址是网络中最基本的标识之一&#xff0c;它是互联网上设备之间通信的基础。 IP SSL作为一种强大的技术&#xff0c;不仅可以保护网站和应用程序的数据传输安全&#xff0c;还能为企业和个人提供一个更加安全的网络环境。本文将探…

佰朔资本:热热热!关于这项业务,上市公司扎堆回复

跟着折叠屏手机概念火爆&#xff0c;与折叠屏手机相关的上市公司股价走出独立行情。 近期&#xff0c;包含科森科技、凯盛科技、国风新材、鼎龙科技、金银河在内的多家上市公司股价出现显着上涨。其间&#xff0c;科森科技结束7连板&#xff0c;国风新材结束3连板&#xff0c;…

iphone怎么设置自定义铃声?手把手教你3个方法解决问题

iPhone手机铃声如何换成自己喜欢的音乐呢&#xff1f;今天&#xff0c;小编就为你带来了3个超好用的方法&#xff0c;手把手教你iPhone怎么设置自定义铃声&#xff0c;为你打造一个手机响铃。一起来看一下这3个方法具体是怎么操作的吧。 操作环境&#xff1a; 演示机型&#xf…