当前位置:   article > 正文

Vue 如何将网页转换成PDF实现步骤以及问题解决:_vue页面导出pdf

vue页面导出pdf

实现步骤:(只想了解vue实现的不用看方法一,直接看二)

方法1:

使用node.js和puppeteer(谷歌自动检测工具),由于第一种尝试结果不太理想,所以我直接粗略讲解:(原代码实现如下,依赖包并非所有都有用,puppeteer是必需要npm按照并且引入)

  1. const puppeteer = require('puppeteer');
  2. const useProxy = require('puppeteer-page-proxy');
  3. const {delay} = require("bluebird");
  4. const Promise = require("bluebird");
  5. const ms = require("ms");
  6. const fs = require('fs');
  7. (async () => {
  8.     const browser = await puppeteer.launch();
  9.     // const browser = await puppeteer.launch({headless:false});
  10.     // const page = await browser.newPage();
  11.     const page = await browser.newPage();
  12.     await page.goto('http://127.0.0.1:5173/',
  13.     {waitUntil:'networkidle2'}
  14.     );
  15.     // await delay(ms("5s"));
  16.     await page.pdf({path: './test123.pdf' , format: 'A4',printBackground:true});
  17.     await browser.close();
  18. })();

实现原理很简单,就是通过puppeteer自动打开对于网页,然后调用他的pdf方法保存,值得注意的是这样printBackground:true这个参数控制的是背景颜色的显示,如果不选true,背景色是不会渲染出来的,这样做的好处是生成简单,图片非常清晰,即使放大后依旧清晰,并且不会有太大的内存,我这三张图片的原图就3m多,生成文件是3.56m可见比较符合预期的。缺点:①生成的pdf是默认两页,我尝试很多手段解决,可能是学术不精,我在puppeteer的API文档为找到对应的参数设置,百度搜索等方式也未找到好的解决方案,至今不清楚原因,我试了其他网站,有些一页就能显示完,具体原因我不详做叙述了,反正笔者可能是陷入死胡同,尝试很多无果后选择了vue去解决。②由于这样需要打开新的窗口,就比如会有跳转操作,虽然有无头模式,但是我未去尝试,因为已经被①劝退了。

方法2:

方法参考来源:

Vue页面生成PDF的方法_vue生成pdf_愤怒小绵羊的博客-CSDN博客

在vue的项目中,先安装需要用到的两个依赖分别是html2canvas和jspdf

  1. ①npm install --save html2canvas
  2. ②npm install jspdf --save

①的作用是将我们需要转换成PDF的html页页面先转换成canvas(canvas是html的一个标签,在画图、特效甚至小游戏方面非常重要,了解可以去看相关系列知识,不详解)转换成canvas之后我们可以通过一系列参数去调整我们需要转换的pdf,这里是非常重要的点,因为在默认情况下,生成的canva是非常模糊的,待会细讲问题及解决

②的作用就是将canvas转换成的图片转换成需要的pdf并且导出,这里能做的事情不多,也相对较为简单

接下来讲的是具体实现步骤:(参考博主愤怒的小绵羊,非常感谢分享,可以先根据小绵羊的代码来做,由于我的代码场景和他不同,我的并不是一个示例代码。可能会存在无法执行,但是绵羊的是可以的 我试过了。)

  1. <template>
  2. <button @click="handleExport">导出</button>
  3. <div ref="pdf" class="spec">
  4. 需要转换成pdf的结构或者图片
  5. </div>
  6. </template>

然后定义点击函数,并且拿到对应页面的pdf传递给downloadPDF函数,由于downloadPDF结构比价多,将其抽离出来pdf.js里面保存

  1. const handleExport =()=>{
  2.             console.log(proxy.$refs.pdf)
  3.             downloadPDF(proxy.$refs.pdf)
  4.         }

然后导入js,由于pdf.js内容较多就将其抽离了

  1. import {downloadPDF} from "./pdf.js"
  2. pdf.js内容如下:
  3. import html2canvas from "html2canvas";
  4. import jsPDF from "jspdf";
  5. import compress from './compress.js';
  6. function base64ToFile(dataURL) {
  7.   var arr = dataURL?.split?.(',')
  8.   let mime = arr[0].match(/:(.*?);/)[1]
  9.   let bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
  10.   while (n--) {
  11.     u8arr[n] = bstr.charCodeAt(n);
  12.   }
  13.   let filename = new Date().getTime() + "" + Math.ceil(Math.random() * 100) + "." + mime.split("/")[1]
  14.   return (new File([u8arr], filename, { type: mime }))
  15. }
  16. export const downloadPDF = page => {
  17.   html2canvas(page,{
  18.     allowTaint: true, //开启跨域
  19.     useCORS: true,
  20.     scale: 2,
  21.   }).then(function(canvas) {
  22.     canvas2PDF(canvas);
  23.   });
  24. };
  25. const canvas2PDF = canvas => {
  26.   let contentWidth = canvas.width*0.2;
  27.   let contentHeight = canvas.height*0.2;
  28.   let imgHeight = contentHeight;
  29.   let imgWidth = contentWidth;
  30.   let pdf = new jsPDF("p", "pt");
  31.   let sharePic
  32.   sharePic = canvas.toDataURL("image/jpeg", 1)
  33.   let fileba = base64ToFile(sharePic)
  34.   compress(fileba)
  35.     .then(res => {
  36.       pdf.addImage(
  37.         res.compressBase64,
  38.         "JPEG",
  39.         0,
  40.         0,
  41.         imgWidth,
  42.         imgHeight
  43.       );
  44.       // console.log(pdf,999)
  45.       pdf.save("导出.pdf");
  46.     })
  47.     .catch(err => {
  48.     // error(err);
  49.     });
  50. };

到这里就已经可以将对应的html代码转换成pdf了,接下来我将讲解我遇到的问题已经解决方案:

问题及解决:

①打印出的PDF没有图片部分

我遇到的第一个问题就是打印出的pdf没有图片,我试了在nodejs+puppeteer情况下是可以直接打印,我猜测可能是转换成pdf的方式不同,可能puppeteer类似于截图(猜测观点),而vue这种先转换成canvas的形式是需要下载图片资源的,所有下载资源就存在跨域问题,一开始我是想通过配置代理的方式实现(后续会详细讲解跨域问题以及配置代理方法,帮助自己复习总结并且发出来),不过结果并不是很顺利,也可能是我操作的问题,于是我通过搜索发现,可以将图片转换base64的格式就能避开跨域问题了,http://nomad-public.oss-cn-shanghai.aliyuncs.com/size_chart/34e4debd-a8ea-4730-acd2-8a58cc7c6b5d.jpg?time=1677729826618,我大致观察了一下好像就是加了个时间戳,然后在图片前加上了image.setAttribute("crossorigin", "anonymous");据说是解决跨域策略的方法,接下来我就把将图片转换成base64的方法贴出来:

  1. const downloadImage = (imgsrc) => {//下载图片地址和图片名(下载部分代码已经被我删除了,这是上一个需求用到了,我在此基础魔改了一下)
  2.         var image = new Image();
  3.         // 解决跨域 Canvas 污染问题,
  4.         image.setAttribute("crossorigin", "anonymous");
  5.         image.onload = function () {
  6.         var canvas = document.createElement("canvas");
  7.         canvas.width = image.width;
  8.         canvas.height = image.height;
  9.         var context = canvas.getContext("2d");
  10.         context.drawImage(image, 0, 0, image.width, image.height);
  11.         var url = canvas.toDataURL("image/png"); //将图片格式转为base64
  12.         // console.log(url,"base64")
  13.         };
  14.         image.src = imgsrc + '?time=' + Date.now();  //注意,这里是灵魂,否则依旧会产生跨域问题
  15.         console.log(image.src,"image.src")
  16.         return image.src
  17.         }

这个函数的作用就是传入一个url,函数就会将转换base64格式的图片url返回了(可能概念会有错,因为我base64和canvas理解较浅),这样再去转换成pdf就会发现可以看到图片了,顺便小提一嘴,如果是背景图片的格式可以在template中用模板字符串以动态形式添加:style="{'margin-top':'0','background-image':`url(${downloadImage('https://nomad-public.oss-cn-shanghai.aliyuncs.com/size_chart/929c3c47-e0b6-4765-bda9-5fb8c1c05191.jpg')})`}",这样就可以避开设置动态css样式了,这样就解决了第一个问题,pdf中无法显示图片。

②生成的PDF清晰度太低

当我打印出PDF之后,我又发现了一个问题,就是我的PDF清晰度太低了,像马赛克一样,于是我又去查询问题解决,有两种解决方案,一个增加scale(其实我们在css里面经常看到就是加大比例一样),我尝试了一下,确实可以,但是同时带来的是图片变得很大很大,由于需求是在一个A4纸大小,增进scale:4后字体等等清晰度是高了很多,但是纸张只能够展示出四分之一不到的内容(生成PDF的左上角),这样整个PDF不能全部展示在A4中,显然不如这样做,于是我又查到了一个参数dpi,整个参数的描述非常符合我的预期,但是就是没用,我也不知道是什么问题,可能是版本,因为我查到有一个html2canvas的参数中压根没有这一个参数,总之这条路(最简单)走不通了,于是在带我的大佬的点拨下,有了一个新的思路,就是在html转换成canvas的时候设置scale:2将转换的canvas画板变大,然后在转换成图片后缩小图片的大小,达到增加清晰度(其实和dpi实现思路一样,更麻烦了,因为dpi参数失效了)

  1. html2canvas(page,{
  2.     allowTaint: true, //开启跨域
  3.     useCORS: true,
  4.     scale: 2,
  5.   }).then(function(canvas) {
  6.     canvas2PDF(canvas);
  7.   });

然后在此代码片段,缩小图片的大小,这个方法俗称先放大再缩小,先放大canvas(画板),在将其转换成图片后缩小图片的大小以增加清晰,这个步骤就是可以通俗理解成,你在一个巨大的画板下先画出需要的画面,由于画板很大,即使画的不是很清晰,在绘画完成后,将图片缩小到一个A4纸大小,那么在同样面积内像素就增加了很大,因为从一个巨大的画板都压缩在一个小小A4纸张上了,像素自然就高了(这是我个人理解)

  1. const canvas2PDF = canvas => {
  2. 下面位置就是width*0.2
  3.   let contentWidth = canvas.width*0.2;
  4.   let contentHeight = canvas.height*0.2;
  5.   let imgHeight = contentHeight;
  6.   let imgWidth = contentWidth;

然后依旧发现清晰度偏低,然后又有了第二个思路,就html代码结构放大,比如原本放在600px的盒子里面,将盒子改成1200px,其实和前面思路一样,一个是增大画板以提升精度,现在这个就是在绘画时候花大一点,那么画板自热而然也会加大,也可以理解成html缩到原来600px的时候像素肯定也会加大,这两个思路在我看来很相似。好的目前生成的图片清晰度已经很高了,基本可以满足公司的需求了,接下来又有一个问题。

③生成的PDF文件过于庞大

由于之前使用的方法中,修改html代码css样式以加大清晰度,那么就会带来一个大小,原来的图片也被放大了,那么如果只是简单再去缩小,图片会非常大,我试过再没被处理的情况下,转换成jpeg的情况下生成pdf大小在13m左右,原图的大小才3m,可见问题之大,这样pdf在文件传输的过程会非常浪费资源,于是我就研究起来了压缩,首先我优化了代码结构,对文件大小改动很小,于是从图片着手,传入图片是20kb左右的时候生成的PDF都依旧有3.8m左右,于是在代码canvas.toDataURL("image/jpeg", 1)处我将参数1修改成0.92,1就是百分百还原图片,0.92就是牺牲清晰度换来文件大小,效果很显著,从3.8m降到了1m,很符合预期,但是我改动了图片清晰度,显然是拆东墙补西墙,我的领导也跟我说这个清晰度较低,希望维持清晰度的情况下尽可能讲到1m,在开始我的认知里面,是越清晰就越大,虽然我也在市面上见过压缩pdf的软件,但是免费情况下都是牺牲清晰度换来文件大小的降低,除了付费情况,所以我开始不知道怎么办,我又去请教带我的那个大佬,大佬直接跟我说,可以啊,随便像压到多小都可以,还不降低清晰度,于是我大佬给了一个.js压缩方法给我,我试了一下直接把3m的压缩到400kb,由于代码是大佬给我的,没经过允许前我就不公开展示了,我写的代码和参考文章代码都放上了,如果有类似需求可以私信我,我可以私发,感谢大佬的分享。方法大概是传入文件,然后将图片转换成base64,然后用canvas处理,然后经过一些我还没研究清楚的方法缩小,如果后续我在网上搜索到公开代码我会在文章中分享出来补充。

最终效果展示:

特别鸣谢:感谢好兄弟某可提出的宝贵意见,已经修改代码处为代码块,经验不足,还请见谅,还有啥不便阅读的欢迎指出

compress.js代码如下:

  1. // 将File(Blob)对象转变为一个dataURL字符串, 即base64格式
  2. const fileToDataURL = file => new Promise((resolve) => {
  3. const reader = new FileReader();
  4. reader.onloadend = e => resolve(e.target.result);
  5. reader.readAsDataURL(file);
  6. });
  7. // 将dataURL字符串转变为image对象,即base64转img对象
  8. const dataURLToImage = dataURL => new Promise((resolve) => {
  9. const img = new Image();
  10. img.onload = () => resolve(img);
  11. img.src = dataURL;
  12. });
  13. // 将一个canvas对象转变为一个File(Blob)对象
  14. const canvastoFile = (canvas, type, quality) => new Promise(resolve => canvas.toBlob(blob => resolve(blob), type, quality));
  15. const compress = (originfile, maxSize) => new Promise(async (resolve, reject) => {
  16. const originSize = originfile.size / 1024; // 单位为kb
  17. console.log('图片指定最大尺寸为', maxSize, '原始尺寸为:', originSize);
  18. // 将原图片转换成base64
  19. const base64 = await fileToDataURL(originfile);
  20. // 缩放图片需要的canvas
  21. const canvas = document.createElement('canvas');
  22. const context = canvas.getContext('2d');
  23. // 小于maxSize,则不需要压缩,直接返回
  24. if (originSize < maxSize) {
  25. resolve({ compressBase64: base64, compressFile: originfile });
  26. console.log(`图片小于指定大小:${maxSize}KB,不用压缩`);
  27. return;
  28. }
  29. const img = await dataURLToImage(base64);
  30. const scale = 1;
  31. const originWidth = img.width;
  32. const originHeight = img.height;
  33. const targetWidth = originWidth * scale;
  34. const targetHeight = originHeight * scale;
  35. canvas.width = targetWidth;
  36. canvas.height = targetHeight;
  37. context.clearRect(0, 0, targetWidth, targetHeight);
  38. context.drawImage(img, 0, 0, targetWidth, targetHeight);
  39. // 将Canvas对象转变为dataURL字符串,即压缩后图片的base64格式
  40. // const compressedBase64 = canvas.toDataURL('image/jpeg', 0.1);
  41. // 经过我的对比,通过scale控制图片的拉伸来压缩图片,能够压缩jpg,png等格式的图片
  42. // 通过canvastoFile方法传递quality来压缩图片,只能压缩jpeg类型的图片,png等格式不支持
  43. // scale的压缩效果没有canvastoFile好
  44. // 在压缩到指定大小时,通过scale压缩的图片比通过quality压缩的图片模糊的多
  45. // 压缩的思路,用二分法找最佳的压缩点
  46. // 这里为了规避浮点数计算的弊端,将quality转为整数再计算;
  47. // const preQuality = 100;
  48. const maxQualitySize = { quality: 100, size: Number.MAX_SAFE_INTEGER };
  49. const minQualitySize = { quality: 0, size: 0 };
  50. let quality = 100;
  51. let count = 0; // 压缩次数
  52. let compressFinish = false; // 压缩完成
  53. let invalidDesc = '';
  54. let compressBlob = null;
  55. // 二分法最多尝试8次即可覆盖全部可能
  56. while (!compressFinish && count < 12) {
  57. compressBlob = await canvastoFile(canvas, 'image/jpeg', quality / 100);
  58. const compressSize = compressBlob.size / 1024;
  59. count++;
  60. if (compressSize === maxSize) {
  61. console.log(`压缩完成,总共压缩了${count}次`);
  62. compressFinish = true;
  63. return;
  64. }
  65. if (compressSize > maxSize) {
  66. maxQualitySize.quality = quality;
  67. maxQualitySize.size = compressSize;
  68. }
  69. if (compressSize < maxSize) {
  70. minQualitySize.quality = quality;
  71. minQualitySize.size = compressSize;
  72. }
  73. console.log(`第${count}次压缩,压缩后大小${compressSize},quality参数:${quality}`);
  74. quality = Math.ceil((maxQualitySize.quality + minQualitySize.quality) / 2);
  75. if (maxQualitySize.quality - minQualitySize.quality < 2) {
  76. if (!minQualitySize.size && quality) {
  77. quality = minQualitySize.quality;
  78. } else if (!minQualitySize.size && !quality) {
  79. compressFinish = true;
  80. invalidDesc = '压缩失败,无法压缩到指定大小';
  81. console.log(`压缩完成,总共压缩了${count}次`);
  82. } else if (minQualitySize.size > maxSize) {
  83. compressFinish = true;
  84. invalidDesc = '压缩失败,无法压缩到指定大小';
  85. console.log(`压缩完成,总共压缩了${count}次`);
  86. } else {
  87. console.log(`压缩完成,总共压缩了${count}次`);
  88. compressFinish = true;
  89. quality = minQualitySize.quality;
  90. }
  91. }
  92. }
  93. if (invalidDesc) {
  94. // 压缩失败,则返回原始图片的信息
  95. console.log(`压缩失败,无法压缩到指定大小:${maxSize}KB`);
  96. reject({ msg: invalidDesc, compressBase64: base64, compressFile: originfile });
  97. return;
  98. }
  99. compressBlob = await canvastoFile(canvas, 'image/jpeg', quality / 100);
  100. const compressSize = compressBlob.size / 1024;
  101. console.log(`最后一次压缩(即第${count + 1}次),quality为:${quality},大小:${compressSize}`);
  102. const compressedBase64 = await fileToDataURL(compressBlob);
  103. const compressedFile = new File([compressBlob], originfile.name, { type: 'image/jpeg' });
  104. resolve({ compressFile: compressedFile, compressBase64: compressedBase64 });
  105. });
  106. export default compress;

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/码创造者/article/detail/880316
推荐阅读
相关标签
  

闽ICP备14008679号