赞
踩
- const puppeteer = require('puppeteer');
-
- const useProxy = require('puppeteer-page-proxy');
-
- const {delay} = require("bluebird");
-
- const Promise = require("bluebird");
-
- const ms = require("ms");
-
- const fs = require('fs');
-
-
-
- (async () => {
-
- const browser = await puppeteer.launch();
-
- // const browser = await puppeteer.launch({headless:false});
-
- // const page = await browser.newPage();
-
- const page = await browser.newPage();
-
- await page.goto('http://127.0.0.1:5173/',
-
- {waitUntil:'networkidle2'}
-
- );
-
- // await delay(ms("5s"));
-
- await page.pdf({path: './test123.pdf' , format: 'A4',printBackground:true});
-
- await browser.close();
-
- })();
实现原理很简单,就是通过puppeteer自动打开对于网页,然后调用他的pdf方法保存,值得注意的是这样printBackground:true这个参数控制的是背景颜色的显示,如果不选true,背景色是不会渲染出来的,这样做的好处是生成简单,图片非常清晰,即使放大后依旧清晰,并且不会有太大的内存,我这三张图片的原图就3m多,生成文件是3.56m可见比较符合预期的。缺点:①生成的pdf是默认两页,我尝试很多手段解决,可能是学术不精,我在puppeteer的API文档为找到对应的参数设置,百度搜索等方式也未找到好的解决方案,至今不清楚原因,我试了其他网站,有些一页就能显示完,具体原因我不详做叙述了,反正笔者可能是陷入死胡同,尝试很多无果后选择了vue去解决。②由于这样需要打开新的窗口,就比如会有跳转操作,虽然有无头模式,但是我未去尝试,因为已经被①劝退了。
方法参考来源:
Vue页面生成PDF的方法_vue生成pdf_愤怒小绵羊的博客-CSDN博客
- ①npm install --save html2canvas
-
- ②npm install jspdf --save
- <template>
-
- <button @click="handleExport">导出</button>
-
- <div ref="pdf" class="spec">
-
- 需要转换成pdf的结构或者图片
-
- </div>
-
- </template>
然后定义点击函数,并且拿到对应页面的pdf传递给downloadPDF函数,由于downloadPDF结构比价多,将其抽离出来pdf.js里面保存
- const handleExport =()=>{
-
- console.log(proxy.$refs.pdf)
-
- downloadPDF(proxy.$refs.pdf)
-
- }
然后导入js,由于pdf.js内容较多就将其抽离了
- import {downloadPDF} from "./pdf.js"
-
- pdf.js内容如下:
-
- import html2canvas from "html2canvas";
-
- import jsPDF from "jspdf";
-
- import compress from './compress.js';
-
-
-
- function base64ToFile(dataURL) {
-
- var arr = dataURL?.split?.(',')
-
- let mime = arr[0].match(/:(.*?);/)[1]
-
- let bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
-
- while (n--) {
-
- u8arr[n] = bstr.charCodeAt(n);
-
- }
-
- let filename = new Date().getTime() + "" + Math.ceil(Math.random() * 100) + "." + mime.split("/")[1]
-
- return (new File([u8arr], filename, { type: mime }))
-
- }
-
-
-
-
- export const downloadPDF = page => {
-
- html2canvas(page,{
-
- allowTaint: true, //开启跨域
-
- useCORS: true,
-
- scale: 2,
-
- }).then(function(canvas) {
-
- canvas2PDF(canvas);
-
- });
-
- };
-
- const canvas2PDF = canvas => {
-
- let contentWidth = canvas.width*0.2;
-
- let contentHeight = canvas.height*0.2;
-
- let imgHeight = contentHeight;
-
- let imgWidth = contentWidth;
-
- let pdf = new jsPDF("p", "pt");
-
- let sharePic
-
- sharePic = canvas.toDataURL("image/jpeg", 1)
-
- let fileba = base64ToFile(sharePic)
-
- compress(fileba)
-
- .then(res => {
-
- pdf.addImage(
-
- res.compressBase64,
-
- "JPEG",
-
- 0,
-
- 0,
-
- imgWidth,
-
- imgHeight
-
- );
-
- // console.log(pdf,999)
-
- pdf.save("导出.pdf");
-
- })
-
- .catch(err => {
-
- // error(err);
-
- });
-
- };
到这里就已经可以将对应的html代码转换成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的方法贴出来:
- const downloadImage = (imgsrc) => {//下载图片地址和图片名(下载部分代码已经被我删除了,这是上一个需求用到了,我在此基础魔改了一下)
-
- var image = new Image();
-
- // 解决跨域 Canvas 污染问题,
-
- image.setAttribute("crossorigin", "anonymous");
-
- image.onload = function () {
-
- var canvas = document.createElement("canvas");
-
- canvas.width = image.width;
-
- canvas.height = image.height;
-
- var context = canvas.getContext("2d");
-
- context.drawImage(image, 0, 0, image.width, image.height);
-
- var url = canvas.toDataURL("image/png"); //将图片格式转为base64
-
- // console.log(url,"base64")
-
- };
-
- image.src = imgsrc + '?time=' + Date.now(); //注意,这里是灵魂,否则依旧会产生跨域问题
-
- console.log(image.src,"image.src")
-
- return image.src
-
- }
这个函数的作用就是传入一个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清晰度太低了,像马赛克一样,于是我又去查询问题解决,有两种解决方案,一个增加scale(其实我们在css里面经常看到就是加大比例一样),我尝试了一下,确实可以,但是同时带来的是图片变得很大很大,由于需求是在一个A4纸大小,增进scale:4后字体等等清晰度是高了很多,但是纸张只能够展示出四分之一不到的内容(生成PDF的左上角),这样整个PDF不能全部展示在A4中,显然不如这样做,于是我又查到了一个参数dpi,整个参数的描述非常符合我的预期,但是就是没用,我也不知道是什么问题,可能是版本,因为我查到有一个html2canvas的参数中压根没有这一个参数,总之这条路(最简单)走不通了,于是在带我的大佬的点拨下,有了一个新的思路,就是在html转换成canvas的时候设置scale:2将转换的canvas画板变大,然后在转换成图片后缩小图片的大小,达到增加清晰度(其实和dpi实现思路一样,更麻烦了,因为dpi参数失效了)
- html2canvas(page,{
-
- allowTaint: true, //开启跨域
-
- useCORS: true,
-
- scale: 2,
-
- }).then(function(canvas) {
-
- canvas2PDF(canvas);
-
- });
然后在此代码片段,缩小图片的大小,这个方法俗称先放大再缩小,先放大canvas(画板),在将其转换成图片后缩小图片的大小以增加清晰,这个步骤就是可以通俗理解成,你在一个巨大的画板下先画出需要的画面,由于画板很大,即使画的不是很清晰,在绘画完成后,将图片缩小到一个A4纸大小,那么在同样面积内像素就增加了很大,因为从一个巨大的画板都压缩在一个小小A4纸张上了,像素自然就高了(这是我个人理解)
- const canvas2PDF = canvas => {
-
- 下面位置就是width*0.2
-
- let contentWidth = canvas.width*0.2;
-
- let contentHeight = canvas.height*0.2;
-
- let imgHeight = contentHeight;
-
- let imgWidth = contentWidth;
然后依旧发现清晰度偏低,然后又有了第二个思路,就html代码结构放大,比如原本放在600px的盒子里面,将盒子改成1200px,其实和前面思路一样,一个是增大画板以提升精度,现在这个就是在绘画时候花大一点,那么画板自热而然也会加大,也可以理解成html缩到原来600px的时候像素肯定也会加大,这两个思路在我看来很相似。好的目前生成的图片清晰度已经很高了,基本可以满足公司的需求了,接下来又有一个问题。
由于之前使用的方法中,修改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代码如下:
- // 将File(Blob)对象转变为一个dataURL字符串, 即base64格式
- const fileToDataURL = file => new Promise((resolve) => {
- const reader = new FileReader();
- reader.onloadend = e => resolve(e.target.result);
- reader.readAsDataURL(file);
- });
-
- // 将dataURL字符串转变为image对象,即base64转img对象
- const dataURLToImage = dataURL => new Promise((resolve) => {
- const img = new Image();
- img.onload = () => resolve(img);
- img.src = dataURL;
- });
-
- // 将一个canvas对象转变为一个File(Blob)对象
- const canvastoFile = (canvas, type, quality) => new Promise(resolve => canvas.toBlob(blob => resolve(blob), type, quality));
-
- const compress = (originfile, maxSize) => new Promise(async (resolve, reject) => {
- const originSize = originfile.size / 1024; // 单位为kb
- console.log('图片指定最大尺寸为', maxSize, '原始尺寸为:', originSize);
-
- // 将原图片转换成base64
- const base64 = await fileToDataURL(originfile);
-
- // 缩放图片需要的canvas
- const canvas = document.createElement('canvas');
- const context = canvas.getContext('2d');
-
- // 小于maxSize,则不需要压缩,直接返回
- if (originSize < maxSize) {
- resolve({ compressBase64: base64, compressFile: originfile });
- console.log(`图片小于指定大小:${maxSize}KB,不用压缩`);
- return;
- }
-
-
- const img = await dataURLToImage(base64);
-
- const scale = 1;
- const originWidth = img.width;
- const originHeight = img.height;
- const targetWidth = originWidth * scale;
- const targetHeight = originHeight * scale;
-
- canvas.width = targetWidth;
- canvas.height = targetHeight;
- context.clearRect(0, 0, targetWidth, targetHeight);
- context.drawImage(img, 0, 0, targetWidth, targetHeight);
-
- // 将Canvas对象转变为dataURL字符串,即压缩后图片的base64格式
- // const compressedBase64 = canvas.toDataURL('image/jpeg', 0.1);
- // 经过我的对比,通过scale控制图片的拉伸来压缩图片,能够压缩jpg,png等格式的图片
- // 通过canvastoFile方法传递quality来压缩图片,只能压缩jpeg类型的图片,png等格式不支持
- // scale的压缩效果没有canvastoFile好
- // 在压缩到指定大小时,通过scale压缩的图片比通过quality压缩的图片模糊的多
- // 压缩的思路,用二分法找最佳的压缩点
- // 这里为了规避浮点数计算的弊端,将quality转为整数再计算;
- // const preQuality = 100;
- const maxQualitySize = { quality: 100, size: Number.MAX_SAFE_INTEGER };
- const minQualitySize = { quality: 0, size: 0 };
- let quality = 100;
- let count = 0; // 压缩次数
- let compressFinish = false; // 压缩完成
- let invalidDesc = '';
- let compressBlob = null;
-
- // 二分法最多尝试8次即可覆盖全部可能
- while (!compressFinish && count < 12) {
- compressBlob = await canvastoFile(canvas, 'image/jpeg', quality / 100);
- const compressSize = compressBlob.size / 1024;
- count++;
- if (compressSize === maxSize) {
- console.log(`压缩完成,总共压缩了${count}次`);
- compressFinish = true;
- return;
- }
- if (compressSize > maxSize) {
- maxQualitySize.quality = quality;
- maxQualitySize.size = compressSize;
- }
- if (compressSize < maxSize) {
- minQualitySize.quality = quality;
- minQualitySize.size = compressSize;
- }
- console.log(`第${count}次压缩,压缩后大小${compressSize},quality参数:${quality}`);
-
- quality = Math.ceil((maxQualitySize.quality + minQualitySize.quality) / 2);
-
- if (maxQualitySize.quality - minQualitySize.quality < 2) {
- if (!minQualitySize.size && quality) {
- quality = minQualitySize.quality;
- } else if (!minQualitySize.size && !quality) {
- compressFinish = true;
- invalidDesc = '压缩失败,无法压缩到指定大小';
- console.log(`压缩完成,总共压缩了${count}次`);
- } else if (minQualitySize.size > maxSize) {
- compressFinish = true;
- invalidDesc = '压缩失败,无法压缩到指定大小';
- console.log(`压缩完成,总共压缩了${count}次`);
- } else {
- console.log(`压缩完成,总共压缩了${count}次`);
- compressFinish = true;
- quality = minQualitySize.quality;
- }
- }
- }
-
- if (invalidDesc) {
- // 压缩失败,则返回原始图片的信息
- console.log(`压缩失败,无法压缩到指定大小:${maxSize}KB`);
- reject({ msg: invalidDesc, compressBase64: base64, compressFile: originfile });
- return;
- }
-
- compressBlob = await canvastoFile(canvas, 'image/jpeg', quality / 100);
- const compressSize = compressBlob.size / 1024;
- console.log(`最后一次压缩(即第${count + 1}次),quality为:${quality},大小:${compressSize}`);
- const compressedBase64 = await fileToDataURL(compressBlob);
-
- const compressedFile = new File([compressBlob], originfile.name, { type: 'image/jpeg' });
-
- resolve({ compressFile: compressedFile, compressBase64: compressedBase64 });
- });
-
- export default compress;
-
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。