html2canvas + padMake + Canvas2Image将html页面指定内容导出成pdf

前言:事情是这样的,在一个并不愉快的周一部门会议上,产品经理突然给我需求,想要**页面,按勾选导出指定模块,当时不明所以,觉得so easy,就接下了需求,结果一调研,我**,对于html导出pdf,网上一片哀嚎,不是这个样式不行,就是清晰度不够,还没有什么明确的按模块导出的方案,心想完了,不能拿来即用了,没得时间摸鱼了,最后经过一番调研确定使用html2canvas + padMake + Canvas2Image导出pdf。

原理:先将html页面使用html2canvas转化成canvas,再使用Canvas2Image将canvas转换成一张高清图片,最后将图片用pdfMake导出成pdf。

按模块导出的思路:在每个模块前端增加一个勾选框,勾选中的模块是需要导出的模块,没有勾选中的模块,在点击按钮的时候改变模块绑定的v-show的值,使其在html页面不可见,实现按需导出的功能,具体实现看代码吧

示例图:

 

前置依赖插件:pdfmake,html2canvas,dayjs,Canvas2Image(由于安装不成功问题,直接复制文件吧)

npm i pdfmake,html2canvas,dayjs
/**
 * covert canvas to image
 * and save the image file
 */
 
var Canvas2Image = function () {
 
	// check if support sth.
	var $support = function () {
		var canvas = document.createElement('canvas'),
			ctx = canvas.getContext('2d');
 
		return {
			canvas: !!ctx,
			imageData: !!ctx.getImageData,
			dataURL: !!canvas.toDataURL,
			btoa: !!window.btoa
		};
	}();
 
	var downloadMime = 'image/octet-stream';
 
	function scaleCanvas (canvas, width, height) {
		var w = canvas.width,
			h = canvas.height;
		if (width == undefined) {
			width = w;
		}
		if (height == undefined) {
			height = h;
		}
 
		var retCanvas = document.createElement('canvas');
		var retCtx = retCanvas.getContext('2d');
		retCanvas.width = width;
		retCanvas.height = height;
		retCtx.drawImage(canvas, 0, 0, w, h, 0, 0, width, height);
		return retCanvas;
	}
 
	function getDataURL (canvas, type, width, height) {
		canvas = scaleCanvas(canvas, width, height);
		return canvas.toDataURL(type);
	}
 
	function saveFile (strData) {
		document.location.href = strData;
	}
 
	function genImage(strData) {
		var img = document.createElement('img');
		img.src = strData;
		return img;
	}
	function fixType (type) {
		type = type.toLowerCase().replace(/jpg/i, 'jpeg');
		var r = type.match(/png|jpeg|bmp|gif/)[0];
		return 'image/' + r;
	}
	function encodeData (data) {
		if (!window.btoa) { throw 'btoa undefined' }
		var str = '';
		if (typeof data == 'string') {
			str = data;
		} else {
			for (var i = 0; i < data.length; i ++) {
				str += String.fromCharCode(data[i]);
			}
		}
 
		return btoa(str);
	}
	function getImageData (canvas) {
		var w = canvas.width,
			h = canvas.height;
		return canvas.getContext('2d').getImageData(0, 0, w, h);
	}
	function makeURI (strData, type) {
		return 'data:' + type + ';base64,' + strData;
	}
 
 
	/**
	 * create bitmap image
	 * 按照规则生成图片响应头和响应体
	 */
	var genBitmapImage = function (oData) {
 
		//
		// BITMAPFILEHEADER: http://msdn.microsoft.com/en-us/library/windows/desktop/dd183374(v=vs.85).aspx
		// BITMAPINFOHEADER: http://msdn.microsoft.com/en-us/library/dd183376.aspx
		//
 
		var biWidth  = oData.width;
		var biHeight	= oData.height;
		var biSizeImage = biWidth * biHeight * 3;
		var bfSize  = biSizeImage + 54; // total header size = 54 bytes
 
		//
		//  typedef struct tagBITMAPFILEHEADER {
		//  	WORD bfType;
		//  	DWORD bfSize;
		//  	WORD bfReserved1;
		//  	WORD bfReserved2;
		//  	DWORD bfOffBits;
		//  } BITMAPFILEHEADER;
		//
		var BITMAPFILEHEADER = [
			// WORD bfType -- The file type signature; must be "BM"
			0x42, 0x4D,
			// DWORD bfSize -- The size, in bytes, of the bitmap file
			bfSize & 0xff, bfSize >> 8 & 0xff, bfSize >> 16 & 0xff, bfSize >> 24 & 0xff,
			// WORD bfReserved1 -- Reserved; must be zero
			0, 0,
			// WORD bfReserved2 -- Reserved; must be zero
			0, 0,
			// DWORD bfOffBits -- The offset, in bytes, from the beginning of the BITMAPFILEHEADER structure to the bitmap bits.
			54, 0, 0, 0
		];
 
		//
		//  typedef struct tagBITMAPINFOHEADER {
		//  	DWORD biSize;
		//  	LONG  biWidth;
		//  	LONG  biHeight;
		//  	WORD  biPlanes;
		//  	WORD  biBitCount;
		//  	DWORD biCompression;
		//  	DWORD biSizeImage;
		//  	LONG  biXPelsPerMeter;
		//  	LONG  biYPelsPerMeter;
		//  	DWORD biClrUsed;
		//  	DWORD biClrImportant;
		//  } BITMAPINFOHEADER, *PBITMAPINFOHEADER;
		//
		var BITMAPINFOHEADER = [
			// DWORD biSize -- The number of bytes required by the structure
			40, 0, 0, 0,
			// LONG biWidth -- The width of the bitmap, in pixels
			biWidth & 0xff, biWidth >> 8 & 0xff, biWidth >> 16 & 0xff, biWidth >> 24 & 0xff,
			// LONG biHeight -- The height of the bitmap, in pixels
			biHeight & 0xff, biHeight >> 8  & 0xff, biHeight >> 16 & 0xff, biHeight >> 24 & 0xff,
			// WORD biPlanes -- The number of planes for the target device. This value must be set to 1
			1, 0,
			// WORD biBitCount -- The number of bits-per-pixel, 24 bits-per-pixel -- the bitmap
			// has a maximum of 2^24 colors (16777216, Truecolor)
			24, 0,
			// DWORD biCompression -- The type of compression, BI_RGB (code 0) -- uncompressed
			0, 0, 0, 0,
			// DWORD biSizeImage -- The size, in bytes, of the image. This may be set to zero for BI_RGB bitmaps
			biSizeImage & 0xff, biSizeImage >> 8 & 0xff, biSizeImage >> 16 & 0xff, biSizeImage >> 24 & 0xff,
			// LONG biXPelsPerMeter, unused
			0,0,0,0,
			// LONG biYPelsPerMeter, unused
			0,0,0,0,
			// DWORD biClrUsed, the number of color indexes of palette, unused
			0,0,0,0,
			// DWORD biClrImportant, unused
			0,0,0,0
		];
 
		var iPadding = (4 - ((biWidth * 3) % 4)) % 4;
 
		var aImgData = oData.data;
 
		var strPixelData = '';
		var biWidth4 = biWidth<<2;
		var y = biHeight;
		var fromCharCode = String.fromCharCode;
 
		do {
			var iOffsetY = biWidth4*(y-1);
			var strPixelRow = '';
			for (var x = 0; x < biWidth; x++) {
				var iOffsetX = x<<2;
				strPixelRow += fromCharCode(aImgData[iOffsetY+iOffsetX+2]) +
							   fromCharCode(aImgData[iOffsetY+iOffsetX+1]) +
							   fromCharCode(aImgData[iOffsetY+iOffsetX]);
			}
 
			for (var c = 0; c < iPadding; c++) {
				strPixelRow += String.fromCharCode(0);
			}
 
			strPixelData += strPixelRow;
		} while (--y);
 
		var strEncoded = encodeData(BITMAPFILEHEADER.concat(BITMAPINFOHEADER)) + encodeData(strPixelData);
 
		return strEncoded;
	};
 
	/**
	 * saveAsImage
	 * @param canvasElement
	 * @param {String} image type
	 * @param {Number} [optional] png width
	 * @param {Number} [optional] png height
	 */
	var saveAsImage = function (canvas, width, height, type) {
		if ($support.canvas && $support.dataURL) {
			if (typeof canvas == "string") { canvas = document.getElementById(canvas); }
			if (type == undefined) { type = 'png'; }
			type = fixType(type);
			if (/bmp/.test(type)) {
				var data = getImageData(scaleCanvas(canvas, width, height));
				var strData = genBitmapImage(data);
				saveFile(makeURI(strData, downloadMime));
			} else {
				var strData = getDataURL(canvas, type, width, height);
				saveFile(strData.replace(type, downloadMime));
			}
		}
	};
 
	var convertToImage = function (canvas, width, height, type) {
		if ($support.canvas && $support.dataURL) {
			if (typeof canvas == "string") { canvas = document.getElementById(canvas); }
			if (type == undefined) { type = 'png'; }
			type = fixType(type);
 
			if (/bmp/.test(type)) {
				var data = getImageData(scaleCanvas(canvas, width, height));
				var strData = genBitmapImage(data);
				return genImage(makeURI(strData, 'image/bmp'));
			} else {
				var strData = getDataURL(canvas, type, width, height);
				return genImage(strData);
			}
		}
	};
 
 
 
	return {
		saveAsImage: saveAsImage,
		saveAsPNG: function (canvas, width, height) {
			return saveAsImage(canvas, width, height, 'png');
		},
		saveAsJPEG: function (canvas, width, height) {
			return saveAsImage(canvas, width, height, 'jpeg');
		},
		saveAsGIF: function (canvas, width, height) {
			return saveAsImage(canvas, width, height, 'gif');
		},
		saveAsBMP: function (canvas, width, height) {
			return saveAsImage(canvas, width, height, 'bmp');
		},
 
		convertToImage: convertToImage,
		convertToPNG: function (canvas, width, height) {
			return convertToImage(canvas, width, height, 'png');
		},
		convertToJPEG: function (canvas, width, height) {
			return convertToImage(canvas, width, height, 'jpeg');
		},
		convertToGIF: function (canvas, width, height) {
			return convertToImage(canvas, width, height, 'gif');
		},
		convertToBMP: function (canvas, width, height) {
			return convertToImage(canvas, width, height, 'bmp');
		}
	};
 
}();
export default Canvas2Image;

1.上面是Canvas2Image的文件,直接复制到自己建立的本地文件里,然后引入需要调用的文件中

2.创建一个pdf.js文件,在其中创建一个downloadPDF方法

import html2canvas from "html2canvas";
import  pdfMake  from "pdfmake/build/pdfmake"
import Canvas2Image from '@/assets/utils/canvas2image'
import dayjs from 'dayjs'

export const downloadPDF = (page, name) => {
  const time = new Date()
  const timeName = dayjs(time).format('YYYY-MM-DD HH:mm:ss')
  const fileName = name + timeName
  const width = page.offsetWidth
  const height = page.offsetHeight
  const scale = 1
  const  canvas = document.createElement("canvas")
  canvas.width = width * scale
  canvas.height = height * scale
  canvas.getContext('2d').scale(scale, scale)
  const opts = {
    backgroundColor:'#ffffff',
    scale: scale, // 添加的scale 参数
    canvas: canvas, // 自定义 canvas
    // logging: true, // 日志开关,便于查看html2canvas的内部执行流程
    width: width, // dom 原始宽度
    height: height,
    useCORS: true //[重要]开启跨域配置
  }
  html2canvas(page, opts).then((canvas) => {
    const context = canvas.getContext('2d')
    // [重要]关闭抗锯齿
    context.mozImageSmoothingEnabled = false
    context.webkitImageSmoothingEnabled = false
    context.msImageSmoothingEnabled = false
    context.imageSmoothingEnabled = false
    const img = Canvas2Image.convertToJPEG(canvas, canvas.width, canvas.height)
    const docDefinition = {
      content: [
        {
          image: img.src,
          width: 500,
          height: canvas.height/4,
        }
      ]
      }
      pdfMake.createPdf(docDefinition).download(fileName)
  })
}

3.在vue文件中调用

   // 点击导出按钮,   
    onExportPdf() {
      const isChose = this.activeChexkbox.every(item => {
        return !item.key
      })
  // 判断是否勾选需要导出的模块
      if (isChose) return this.$message.warning('请先勾选需要导出的模块')
  // 有导出模块,先调用onHiden方法,将那些没有勾选的模块隐藏起来
      this.onHiden('batch').then(() => {
  // 然后再调用导出这个方法,导出勾选的模块
        this.onExport()
      }).then(() => {
  // 最后将那些隐藏的模块复现,因为代码执行速度极快,用户无法感知到隐藏了模块的
        this.onShow('batch')
      })
    },
    async onHiden(type) {
      if (type === 'batch') {
        await this.activeChexkbox.forEach(item => {
          item.key ? item.value = true : item.value = false
        })
      }
      this.isHandle = false
    },
    async onExport() {
      await downloadPDF(this.$refs.pdf, '活动分析')
    },
    onShow(type) {
      if (type === 'batch') {
        this.activeChexkbox.forEach(item => {
          item.key = false
          item.value = true
        })
      }
      this.isHandle = true
    },

this.activeChexkbox

data() {
  return {
   activeChexkbox: [
        { key: false, value: true },
        { key: false, value: true },
        { key: false, value: true },
        { key: false, value: true },
        { key: false, value: true },
        { key: false, value: true },
        { key: false, value: true },
        { key: false, value: true },
        { key: false, value: true },
        { key: false, value: true }
      ],
  }
}

具体在template中的实现,这里没有写全,解释一下,在一个模块最大的div下面绑定this.activechexkbox[4].value的,通过看下面的勾选框,是否勾选,来控制在导出的时候控制显示隐藏

注意:这里要用v-show,不要用v-if来控制显示隐藏,因为v-if需要重新渲染dom,会导致导出的时候没有数据

<div v-show="activeChexkbox[4].value" class="activeAnalysis-content">
      <div class="activeAnalysis-content__header">
        <div class="title">
          <el-checkbox v-model="activeChexkbox[4].key" class="mr10"></el-checkbox>次数/人数
        </div>

以上就是大体思路了

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值