位置: IT常识 - 正文

jsPDF + html2canvas A4分页截断 完美解决方案(含代码 + 案例)

编辑:rootadmin
jsPDF + html2canvas A4分页截断 完美解决方案(含代码 + 案例) 业务需求

推荐整理分享jsPDF + html2canvas A4分页截断 完美解决方案(含代码 + 案例),希望有所帮助,仅作参考,欢迎阅读内容。

文章相关热门搜索词:,内容如对您有帮助,希望把文章链接给更多的朋友!

网页html生成A4大小分页的pdf,翻遍了整个互联网发现没有很系统的整理与分析,甚至对jsPDF的解析也没有几篇。遇到过几次,用的比较多,完成代码编写后特此整理分析,自我记录。

业务难点

1.存在图片/组件/文字被分割的现象,即分页处理 2.包括页头、页脚、上下安全间隔的情况 3.富文本分页情况

处理思路

通过深度搜索优先遍历,从顶部遍历需要转换的HTML节点, 并将节点分为三种情况进行处理(1. 普通节点。2. 需要进行分页处理并且内部可能包含也需要分页处理子节点的节点。3. 需要进行分页内部不包含需要分页处理的节点,即深度搜索的终点节点),通过从高到低遍历维护一个分页数组pages,该数组记录每一页的起始位置,如:pages[0] 对应 第一页起始位置, pages[1] 对应 第二页起始位置

图解如下:

通过深度遍历后得出每页起始位置的数组,遍历数组,通过jspdf的addImage接口对canvas进行画面截取,由于addImage只能固定位置的左上角起始点,不能进行非常精确的上下定位截取(下一节会详解addImage),会造成截取多余的内容(如上图页面1中pages[1] 下方的内容会和 页面2 中 pages[1] 下方的内容会一样(除长度外),而页面1中pages[1] 下方的内容是多余的(是属于页面2的内容))因此需要对页面不需要的内容 使用jspdf的addBlank进行空白遮挡处理。

jsPDF.addImage详解

官方文档链接addImage - Documentation (artskydj.github.io)

需要注意的点是坐标(x,y) 的取值, (x,y)对应的是添加图片的左上角取值,宽高则是根据转化成canvas的宽高取值,图解如下

因此在对一个长图片进行截取时,往往将y值设未负数,让需要截取图片的起始位置落于当前的pdf页面内,在当前案例下,每一页的图片摆放坐标y = -pages[i]

jsPDF.rect详解

文档链接 context2d - Documentation (artskydj.github.io)

该接口的参数 (x,y)坐标、宽高 与addImage接口的一致 当前pdf页需要的内容的高度为 pages[i] - pages[i-1], 除去顶部这个高度外以下的内容都是不需要的,因此得到每一页添加空白的y坐标值为- pages[i] - pages[i-1],高度h为一页pdf的高度(此处为A4页的高度) - pages[i] - pages[i-1],宽度为A4宽度,x为0, 图解如下:

深度优先遍历三种类型的节点

通过深度优先遍历操作,可以从高到低去遍历需要进行跨页判断的元素,检测是否跨页,并记录分页点,从而避免跨页问题。

1. 普通节点

当遍历到普通节点,即不需要进行分页判断的节点时,只需要进行 2步操作:

jsPDF + html2canvas A4分页截断 完美解决方案(含代码 + 案例)

1.当前节点距离顶部的高度 - pages最后一位元素的值(即上一页的分界点)得出的差值是否 大于 页面的高度 , 如果大于,则证明当前节点已经跨页,进行操作**pages.push(pages[pages.length - 1] + 一页PDF的高度)**2.对子节点进行深度遍历### 2. 需要进行跨页判断,且内部也含有 可能跨页/需要进行跨页判断 的节点

当元素进行到该类型的节点时, 需要进行3步操作:

1.需要进行与普通节点第一步相同的判断2.**(检测当前节点距离顶部的距离 + 节点自身的高度) 是否大于 (pages 最后一位元素(即当前页 顶部位置) + 一页PDF的高度(当前指A4的高度))**如果条件为真,则证明该节点属于跨页元素,距离页面顶部距离的值top 是分页点,往pages中 push top

3.且由于内部还存在需要进行跨页检测的节点,因此需要对子节点进行深度遍历

3. 需要进行跨页判断,但内部不含有可能跨页/需要进行跨页判断 的节点, 即深度终点

该节点只需要进行 内部含有可能跨页/需要进行跨页判断 的节点 的第一第二步操作, 由于内部不再含有,因此不需要遍历子节点,为搜索的叶子节点。

html2Canvas生成图片模糊导致导出的PDF也模糊的问题

通过 scale 参数, 对canvas进行等比放大,可以使canvas生成的图片更清晰。

代码如下

// 将元素转化为canvas元素// 通过 放大 提高清晰度// width为内容宽度async function toCanvas(element, width) {// canvas元素const canvas = await html2canvas(element, { // allowTaint: true, // 允许渲染跨域图片scale: window.devicePixelRatio * 3// 增加清晰度});// 获取canavs转化后的宽度const canvasWidth = canvas.width;// 获取canvas转化后的高度const canvasHeight = canvas.height;// 高度转化为PDF的高度const height = (width / canvasWidth) * canvasHeight;// 转化成图片Dataconst canvasData = canvas.toDataURL('image/jpeg', 1.0);//console.log(canvasData)return { width, height, data: canvasData };} 样例及代码

gitee仓库: output_pdf_demo: jsPDF + html2canvas , 网页HTML导出A4格式PDF 处理分页切割问题 (gitee.com)

npm install & npm run serve 即可运行

分页效果:

富文本分页:

table行分页:

组件分页:

样例注意事项

样例比上述讲的情况内,引入了页眉、页脚、还有上下左右间距的情况,图解如下:

需要做的额外处理:

1.图片摆放的Y坐标由原来的-pages[i] 变成了 baseY + 页头元素高度 - pages[i] 2.中间实际内容部分与页眉/页脚之间的边距也需要进行遮白处理 3.内容的高度才为PDF页面的实际高度,判断分页的依据应该以内容高度为准 4.富文本文字的分页处理

核心代码import jsPDF from 'jspdf';import html2canvas from 'html2canvas';import { message } from 'ant-design-vue';const A4_WIDTH = 592.28;const A4_HEIGHT = 841.89;// 将元素转化为canvas元素// 通过 放大 提高清晰度// width为内容宽度async function toCanvas(element, width) {// canvas元素const canvas = await html2canvas(element, { // allowTaint: true, // 允许渲染跨域图片scale: window.devicePixelRatio * 2// 增加清晰度});// 获取canavs转化后的宽度const canvasWidth = canvas.width;// 获取canvas转化后的高度const canvasHeight = canvas.height;// 高度转化为PDF的高度const height = (width / canvasWidth) * canvasHeight;// 转化成图片Dataconst canvasData = canvas.toDataURL('image/jpeg', 1.0);//console.log(canvasData)return { width, height, data: canvasData };}/** * 生成pdf(A4多页pdf截断问题, 包括页眉、页脚 和 上下左右留空的护理) * @param {Object} param * @param {HTMLElement} param.element - 需要转换的dom根节点 * @param {number} [param.contentWidth=550] - 一页pdf的内容宽度,0-592.28 * @param {string} [param.filename='document.pdf'] - pdf文件名 * @param {HTMLElement} param.header - 页眉dom元素 * @param {HTMLElement} param.footer - 页脚dom元素 */export async function outputPDF({ element, contentWidth = 550,footer, header, filename = "测试A4分页.pdf" }) {if (!(element instanceof HTMLElement)) {return;}// jsPDFs实例const pdf = new jsPDF({unit: 'pt',format: 'a4',orientation: 'p',});// 一页的高度, 转换宽度为一页元素的宽度const { width, height, data } = await toCanvas(element, contentWidth);// 添加页脚async function addHeader(header, pdf, contentWidth) {const { height: headerHeight, data: headerData, width: hWidth } = await toCanvas(header, contentWidth);pdf.addImage(headerData, 'JPEG', 0, 0, contentWidth, headerHeight);}// 添加页眉async function addFooter(pageNum, now, footer, pdf, contentWidth) {const newFooter = footer.cloneNode(true);newFooter.querySelector('.pdf-footer-page').innerText = now;newFooter.querySelector('.pdf-footer-page-count').innerText = pageNum;document.documentElement.append(newFooter);const { height: footerHeight, data: footerData, width: fWidth } = await toCanvas(newFooter, contentWidth);pdf.addImage(footerData, 'JPEG', 0, A4_HEIGHT - footerHeight, contentWidth, footerHeight)}// 添加function addImage(_x, _y, pdf, data, width, height) {pdf.addImage(data, 'JPEG', _x, _y, width, height);}// 增加空白遮挡function addBlank(x, y, width, height, pdf) {pdf.setFillColor(255, 255, 255);pdf.rect(x, y, Math.ceil(width), Math.ceil(height), 'F');};// 页脚元素 经过转换后在PDF页面的高度const { height: tfooterHeight } = await toCanvas(footer, contentWidth)// 页眉元素 经过转换后在PDF的高度const { height: theaderHeight } = await toCanvas(header, contentWidth);// 距离PDF左边的距离,/ 2 表示居中 const baseX = (A4_WIDTH - contentWidth) / 2;// 预留空间给左边// 距离PDF 页眉和页脚的间距, 留白留空const baseY = 15;// 出去页头、页眉、还有内容与两者之间的间距后 每页内容的实际高度const originalPageHeight = (A4_HEIGHT - tfooterHeight - theaderHeight - 2 * baseY);// 元素在网页页面的宽度const elementWidth = element.offsetWidth;// PDF内容宽度 和 在HTML中宽度 的比, 用于将 元素在网页的高度 转化为 PDF内容内的高度, 将 元素距离网页顶部的高度转化为 距离Canvas顶部的高度const rate = contentWidth / elementWidth// 每一页的分页坐标, PDF高度, 初始值为根元素距离顶部的距离const pages = [rate * getElementTop(element)];// 获取元素距离网页顶部的距离// 通过遍历offsetParant获取距离顶端元素的高度值function getElementTop(element) {let actualTop = element.offsetTop;let current = element.offsetParent;while (current && current !== null) {actualTop += current.offsetTop;current = current.offsetParent;}return actualTop;}// 遍历正常的元素节点function traversingNodes(nodes) {for (let i = 0; i < nodes.length; ++i) {const one = nodes[i];// 需要判断跨页且内部存在跨页的元素const isDivideInside = one.classList && one.classList.contains('divide-inside');// 图片元素不需要继续深入,作为深度终点const isIMG = one.tagName === 'IMG';// table的每一行元素也是深度终点const isTableCol = one.classList && ((one.classList.contains('ant-table-row')));// 特殊的富文本元素const isEditor = one.classList && (one.classList.contains('editor'));// 对需要处理分页的元素,计算是否跨界,若跨界,则直接将顶部位置作为分页位置,进行分页,且子元素不需要再进行判断let { offsetHeight } = one;// 计算出最终高度let offsetTop = getElementTop(one);// dom转换后距离顶部的高度// 转换成canvas高度const top = rate * (offsetTop)// 对于需要进行分页且内部存在需要分页(即不属于深度终点)的元素进行处理if (isDivideInside) {// 执行位置更新操作updatePos(rate * offsetHeight, top, one);// 执行深度遍历操作traversingNodes(one.childNodes);}// 对于深度终点元素进行处理else if (isTableCol || isIMG) {// dom高度转换成生成pdf的实际高度// 代码不考虑dom定位、边距、边框等因素,需在dom里自行考虑,如将box-sizing设置为border-boxupdatePos(rate * offsetHeight, top, one);}else if (isEditor) {// 执行位置更新操作updatePos(rate * offsetHeight, top, one);// 遍历富文本节点traversingEditor(one.childNodes)}// 对于普通元素,则判断是否高度超过分页值,并且深入else {// 执行位置更新操作updateNomalElPos(top)// 遍历子节点traversingNodes(one.childNodes);}}return;}// 对于富文本元素,观察所得段落之间都是以<p> / <img> 元素相隔,因此不需要进行深度遍历 (仅针对个人遇到的情况)function traversingEditor(nodes) {// 遍历子节点for (let i = 0; i < nodes.length; ++i) {const one = nodes[i];let { offsetHeight } = one;let offsetTop = getElementTop(one);const top = contentWidth / elementWidth * (offsetTop)updatePos(contentWidth / elementWidth * offsetHeight, top, one);}}// 普通元素更新位置的方法// 普通元素只需要考虑到是否到达了分页点,即当前距离顶部高度 - 上一个分页点的高度 大于 正常一页的高度,则需要载入分页点 function updateNomalElPos(top) {if (top - (pages.length > 0 ? pages[pages.length - 1] : 0) > originalPageHeight) {pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight);}}// 可能跨页元素位置更新的方法// 需要考虑分页元素,则需要考虑两种情况// 1. 普通达顶情况,如上// 2. 当前距离顶部高度加上元素自身高度 大于 整页高度,则需要载入一个分页点function updatePos(eheight, top) {// 如果高度已经超过当前页,则证明可以分页了if (top - (pages.length > 0 ? pages[pages.length - 1] : 0) >= originalPageHeight) {pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight);}// 若 距离当前页顶部的高度 加上元素自身的高度 大于 一页内容的高度, 则证明元素跨页,将当前高度作为分页位置else if ((top + eheight - (pages.length > 0 ? pages[pages.length - 1] : 0) > originalPageHeight) && (top != (pages.length > 0 ? pages[pages.length - 1] : 0))) {pages.push(top);}}// 深度遍历节点的方法traversingNodes(element.childNodes);// 可能会存在遍历到底部元素为深度节点,可能存在最后一页位置未截取到的情况if (pages[pages.length - 1] + originalPageHeight < height) {pages.push(pages[pages.length - 1] + originalPageHeight);}//console.log({ pages, contentWidth, width,height })// 根据分页位置 开始分页for (let i = 0; i < pages.length; ++i) {message.success(`共${pages.length}页, 生成第${i + 1}页`)// 根据分页位置新增图片addImage(baseX, baseY + theaderHeight - pages[i], pdf, data, width, height);// 将 内容 与 页眉之间留空留白的部分进行遮白处理addBlank(0, theaderHeight, A4_WIDTH, baseY, pdf);// 将 内容 与 页脚之间留空留白的部分进行遮白处理addBlank(0, A4_HEIGHT - baseY - tfooterHeight, A4_WIDTH, baseY, pdf);// 对于除最后一页外,对 内容 的多余部分进行遮白处理if (i < pages.length - 1) {// 获取当前页面需要的内容部分高度const imageHeight = pages[i + 1] - pages[i];// 对多余的内容部分进行遮白addBlank(0, baseY + imageHeight + theaderHeight, A4_WIDTH, A4_HEIGHT - (imageHeight), pdf);}// 添加页眉await addHeader(header, pdf, A4_WIDTH)// 添加页脚await addFooter(pages.length, i + 1, footer, pdf, A4_WIDTH);// 若不是最后一页,则分页if (i !== pages.length - 1) {// 增加分页pdf.addPage();}}return pdf.save(filename)} 最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。

有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文链接地址:https://www.jiuchutong.com/zhishi/284144.html 转载请保留说明!

上一篇:尿急、尿频、尿痛怎么办(图文)(尿急尿频尿不尽吃什么药效果好)

下一篇:uniapp中单选按钮的实现(uniapp单选功能)

  • 小米智能门锁怎么换电池(小米智能门锁怎么连接wifi)

    小米智能门锁怎么换电池(小米智能门锁怎么连接wifi)

  • 腾讯微云怎么备份照片(腾讯微云怎么备份通讯录)

    腾讯微云怎么备份照片(腾讯微云怎么备份通讯录)

  • 微博上铁粉怎么获得(微博铁粉怎么恢复)

    微博上铁粉怎么获得(微博铁粉怎么恢复)

  • 腾讯课堂切屏出去老师知道吗(腾讯课堂切屏出去老师能看到我的屏幕吗?)

    腾讯课堂切屏出去老师知道吗(腾讯课堂切屏出去老师能看到我的屏幕吗?)

  • 苹果11pro max怎么删除软件(苹果11pro max怎么样)

    苹果11pro max怎么删除软件(苹果11pro max怎么样)

  • 安卓手机微信分身在哪里设置(安卓手机微信分身怎么弄)

    安卓手机微信分身在哪里设置(安卓手机微信分身怎么弄)

  • k30变焦版和标准版有什么区别(变焦版 标准版 k30pro)

    k30变焦版和标准版有什么区别(变焦版 标准版 k30pro)

  • jpeg和jpeg2000的区别(jpeg2000和jpeg哪种清晰)

    jpeg和jpeg2000的区别(jpeg2000和jpeg哪种清晰)

  • mic是什么接口(主板mic是什么接口)

    mic是什么接口(主板mic是什么接口)

  • 苹果x的返回键在哪里(苹果x的返回键在屏幕哪里)

    苹果x的返回键在哪里(苹果x的返回键在屏幕哪里)

  • 苹果6为什么抖音没有影集(苹果为什么抖音关掉屏幕还有声音)

    苹果6为什么抖音没有影集(苹果为什么抖音关掉屏幕还有声音)

  • oppoa33为什么总显示空间不足(oppo手机为什么老)

    oppoa33为什么总显示空间不足(oppo手机为什么老)

  • pchm30是oppo什么型号(pchm30是什么手机)

    pchm30是oppo什么型号(pchm30是什么手机)

  • 苹果imei在哪里看(苹果imei在哪查询)

    苹果imei在哪里看(苹果imei在哪查询)

  • 苹果店换屏是原装吗(苹果换屏原装屏拿走)

    苹果店换屏是原装吗(苹果换屏原装屏拿走)

  • iphonex无面容能修吗(iphonex无面容能拍照吗)

    iphonex无面容能修吗(iphonex无面容能拍照吗)

  • 华为备忘录如何取消时间(华为备忘录如何导入小米手机)

    华为备忘录如何取消时间(华为备忘录如何导入小米手机)

  • 红米note8怎么设置屏幕色温(红米Note8怎么设置往上划关闭)

    红米note8怎么设置屏幕色温(红米Note8怎么设置往上划关闭)

  • 数据的四种基本存储结构(数据的四种基本结构分别是什么)

    数据的四种基本存储结构(数据的四种基本结构分别是什么)

  • iphone11几寸(iphone11几寸屏幕)

    iphone11几寸(iphone11几寸屏幕)

  • oppoa9有息屏显示吗

    oppoa9有息屏显示吗

  • 笔记本电脑不开机(笔记本电脑不开机一直插着电源好吗)

    笔记本电脑不开机(笔记本电脑不开机一直插着电源好吗)

  • 苹果耳机3代上市时间(苹果耳机3代叫什么名字)

    苹果耳机3代上市时间(苹果耳机3代叫什么名字)

  • 华为麦芒8上市时间(华为麦芒8上市价格)

    华为麦芒8上市时间(华为麦芒8上市价格)

  • ppt图表编辑数据没反应(ppt图表编辑数据图表不变)

    ppt图表编辑数据没反应(ppt图表编辑数据图表不变)

  • python生成器的三种构建方法(python生成器的应用场景)

    python生成器的三种构建方法(python生成器的应用场景)

  • 差额征税的税率怎么计算
  • 纳税人离线开票时间的文件
  • 解除劳动合同支付违约金合法吗
  • 股东权益是资产吗
  • 利息收入交所得税吗
  • 固定成本和变动成本之和称作
  • 半成品怎么结转到成品
  • 房租押金没退款怎么起诉
  • 自来水税率是9%还是3%
  • 季度申报利润表本月数怎么填
  • 收到返利款怎么做账
  • 电梯合同属于什么合同
  • 一般纳税人增值税申报操作流程
  • 电商刷单返现如何做账务处理合适?
  • 不征收耕地占用税的情形
  • 支付的劳务派遣服务费现金流量
  • 钱汇错需要退回应该怎么做分录?
  • 没有发票合法吗
  • 支付境外咨询费代扣代缴企业所得税
  • 增值税普通发票税率
  • 有发票的福利费可以不交个税吗
  • 个人独资企业是法人吗
  • 增值税普通发票可以抵扣吗
  • 小规模纳税人月超10万季度不超30万
  • 免税和不征税货一样吗
  • 上月已认证的发票发现错误怎么办
  • 资产负债表在途物资属于存货吗
  • 盈余公积为0说明什么问题
  • 以库存抵债的账务处理
  • 建筑行业没有库存要做暂估成本怎么做?
  • 企业购入存货分录
  • 增值税各科目账务处理
  • 生产成本中的电费计入制造费用吗
  • 什么叫冲减进项税额
  • 蛋糕店盈亏平衡分析
  • PHP:mb_substitute_character()的用法_mbstring函数
  • php常用设计模式(大总结)
  • 进价金额核算法的账务处理特点
  • kavsvc.exe - kavsvc是什么进程 有什么作用
  • 简易计税方法的适用主体有
  • 关于政府的话
  • three.js入门指南
  • 普通发票的开票规定
  • linux rm 命令
  • 利息收入交所得税吗
  • 关闭php报错
  • mybatis入门菜鸟教程
  • 财政拨款方式有哪几种
  • 附加税税率是指
  • 怎么盘存货
  • 一般纳税人改成小规模可以吗
  • 公司买车可以少多少钱
  • 免税申请需要什么材料
  • 跨年的管理费用怎么处理
  • 企业发票冲红的风险
  • 建账选用什么会计制度
  • Win7/Win8.1/Win10的UAC对话框“是”点不了的原因及解决方法
  • centos怎么设置
  • windows电源图标消失
  • fedora i3wm
  • ubuntu pdf编辑器
  • alg.exe是什么程序
  • mscorsvw.exe是病毒吗
  • win7点开始一直跳闪
  • win7旗舰版升级win10教程
  • 安卓框架app
  • 安卓手机键盘怎么调出来
  • css条件语句
  • javascript数据结构与算法第三版
  • AngularJS2 与 D3.js集成实现自定义可视化的方法
  • unityai寻路
  • 前端头像裁剪
  • Unite Beijing 2015大型活动
  • 山东省国税地税体制改革
  • 江西省税务局发票查询平台官网
  • 广东共青团如何解绑微信
  • 营业税发票现在可以冲红么
  • 车船税与船舶吨税法律制度ppt
  • 代理记账公司有什么风险吗?
  • 营改增后如何纳税
  • 免责声明:网站部分图片文字素材来源于网络,如有侵权,请及时告知,我们会第一时间删除,谢谢! 邮箱:opceo@qq.com

    鄂ICP备2023003026号

    网站地图: 企业信息 工商信息 财税知识 网络常识 编程技术

    友情链接: 武汉网站建设