使用 html2canvas 和 pdf-lib 生成和下载指南

520 阅读6分钟

介绍

在许多应用中,将网页内容转换为 PDF 文档是一项常见的需求。本文将展示如何使用 html2canvaspdf-lib 两个库,将 HTML 元素转换为图像并嵌入到 PDF 文件中,最后实现文件的下载。这个解决方案非常适用于生成报告、发票或任何其他需要以 PDF 格式保存的内容。

环境准备

在开始之前,确保开发环境中已安装以下库:

  • html2canvas:用于将 HTML 元素转换为 Canvas 图像。
  • pdf-lib:用于创建和处理 PDF 文档。

可以通过 npm 安装这两个库:

npm install html2canvas pdf-lib

代码解析

下面的代码展示了一个类方法,包含了生成报告的完整过程。我们将逐行解释每个部分的功能。

1. 下载报告的方法

async downloadReports() {
  this.disDownloadReport = true; // 禁用下载按钮
  document.documentElement.scrollLeft = 0;
  document.documentElement.scrollTop = 0;
  
  const docs = document.querySelectorAll('.reportPage:not(.hidden)');
  const totalDocs = docs.length; // 计算报告页面总数
  const pdfDoc = await PDFDocument.create(); // 创建新的 PDF 文档
  this.pageDataArr = []; // 存储每页的图像数据

解释

  • 当用户开始下载报告时,首先禁用下载按钮,避免重复点击。
  • 然后重置页面的滚动位置,确保 PDF 的内容从顶部开始。
  • 使用 querySelectorAll 获取所有可见的报告页面,并计算页面的总数。
  • 创建一个新的 PDF 文档,并初始化一个数组以存储图像数据。

2. 处理批次

  const batchSize = 5; // 每批处理 5 页
  let batches = Math.ceil(totalDocs / batchSize); // 计算需要的总批次

  for (let batch = 0; batch < batches; batch++) {
    const start = batch * batchSize; // 当前批次开始的索引
    const end = Math.min(start + batchSize, totalDocs); // 当前批次结束的索引
    const batchPromises = []; // 存储当前批次的 Promise

解释

  • 设置每批处理的页面数为 5 页。
  • 计算出需要的总批次数。
  • 遍历每个批次,计算每个批次的开始和结束索引,并初始化一个数组用于存储 Promise。

3. 转换 HTML 为 Canvas

    for (let i = start; i < end; i++) {
      batchPromises.push(this.htmlToCanvas(docs[i], i)); // 将每页转换为 canvas
    }

    await Promise.all(batchPromises); // 等待所有页面转换完成

解释

  • 在每个批次中,将当前页面的 HTML 转换为 Canvas,存储 Promise 以便后续处理。
  • 使用 Promise.all 确保所有页面都已成功转换后再进行下一步。

4. 嵌入图像到 PDF

    for (let i = start; i < end; i++) {
      const imgBytes = await fetch(this.pageDataArr[i]).then((res) => res.arrayBuffer());
      const img = await pdfDoc.embedJpg(imgBytes); // 嵌入 JPEG 图像

      const page = pdfDoc.addPage([595.28, 841.89]); // A4 页面
      const { width, height } = img.scaleToFit(595.28, 841.89); // 计算图像自适应宽高
      page.drawImage(img, {
        x: (page.getWidth() - width) / 2,
        y: (page.getHeight() - height) / 2,
        width,
        height
      });
    }

解释

  • 将每个页面的图像字节数据读取为 ArrayBuffer,并嵌入到 PDF 文档中。
  • 为每个页面添加一个新的 A4 页面,并计算图像的适应大小,使其居中显示。

5. 生成并下载 PDF

  const pdfBytes = await pdfDoc.save(); // 保存 PDF 文档
  const blob = new Blob([pdfBytes], { type: 'application/pdf' }); // 创建 Blob 对象
  const url = URL.createObjectURL(blob); // 生成下载链接
  const a = document.createElement('a'); // 创建一个临时链接
  a.href = url; // 设置链接地址
  a.download = (this.reportInfo.name || this.reportInfo.name) + '报告.pdf'; 
  document.body.appendChild(a); // 将链接添加到文档
  a.click(); // 触发下载
  document.body.removeChild(a); // 下载后移除链接

解释

  • 生成 PDF 字节并创建 Blob 对象,形成可下载的链接。
  • 创建一个临时的链接元素并触发下载。
  • 下载完成后,恢复下载按钮的状态。

6. HTML 转 Canvas 的辅助函数

htmlToCanvas(doc, index) {
  return new Promise((resolve, reject) => {
    const box = window.getComputedStyle(doc);
    const width = box.width.replace('px', '');
    const height = box.height.replace('px', '');
    const scaleBy = window.devicePixelRatio > 1 ? window.devicePixelRatio : 1;

    html2canvas(doc, {
      scale: window.devicePixelRatio * 2,
      width,
      height,
      scrollX: 0,
      scrollY: 0
    })
    .then((canvas) => {
      let pageData = canvas.toDataURL('image/jpeg', 1.0);
      this.pageDataArr[index] = pageData; // 存储图像数据
      resolve(canvas);
    })
    .catch((error) => {
      console.error('Error generating canvas:', error);
      reject(error);
    });
  });
}

解释

  • 将 HTML 元素转换为 Canvas,并返回一个 Promise 以便异步处理。
  • 使用 html2canvas 进行转换,并处理可能出现的错误。

使用方法

要使用上述代码,只需执行以下步骤:

  1. 确保已引入 html2canvas 和 pdf-lib:在 HTML 文件中或通过 JavaScript 模块引入这两个库。

  2. 创建报告页面:在 HTML 中创建具有类名为 reportPage 的元素。确保这些元素是可见的,且符合报告格式。

  3. 调用 downloadReports 方法:在用户点击下载按钮时调用此方法,开始生成并下载报告。

示例按钮代码:


<template>
  <div>
    <div class="reportPage" ref="reportPage">
      <h1>信息数据转换成报告形式</h1>
      <p>这是数据转换报告的内容。</p>
      <!-- 假设这里还有更多内容,通过遍出来 -->
      <div v-for="n in 15" :key="n">
        <h2>内容块 {{ n }}</h2>
        <p>这是内容块 {{ n }} 的文本。</p>
      </div>
    </div>
    <button :disabled="disDownloadReport" @click="downloadReports">下载报告</button>
  </div>
</template>

最后展示整体代码


<template>
  <div>
    <div class="reportPage" ref="reportPage">
      <h1>信息数据转换成报告形式</h1>
      <p>这是数据转换报告的内容。</p>
      <!-- 假设这里还有更多内容,通过遍出来 -->
      <div v-for="n in 15" :key="n">
        <h2>内容块 {{ n }}</h2>
        <p>这是内容块 {{ n }} 的文本。</p>
      </div>
    </div>
    <button :disabled="disDownloadReport" @click="downloadReports">下载报告</button>
  </div>
</template>
<script>
import html2canvas from 'html2canvas';
import { PDFDocument } from 'pdf-lib'; // 导入 pdf-lib
export default {
	data(){
		return{
	 		pageDataArr: [], // 存储每一页的图像数据
      		totalPages: 15, // 总页数
      		pagesPerBatch: 5, // 每批处理的页数
      		disDownloadReport: false, // 按钮是否禁用
		}
	},
	methods:{
		async downloadReports() {
  			// 开始下载报告时禁用下载按钮
  			this.disDownloadReport = true;
  			// 将页面滚动位置重置到顶部
  			document.documentElement.scrollLeft = 0;
  			document.documentElement.scrollTop = 0;
  
  			// 获取所有非隐藏的报告页面
  			const docs = document.querySelectorAll('.reportPage:not(.hidden)');
  			const totalDocs = docs.length; // 计算报告页面总数
  			const pdfDoc = await PDFDocument.create(); // 创建新的 PDF 文档
  			this.pageDataArr = []; // 用于存储每页的图像数据

  			const batchSize = 5; // 每批处理 5 页
  			let batches = Math.ceil(totalDocs / batchSize); // 计算需要的总批次

  			// 遍历每个批次
  			for (let batch = 0; batch < batches; batch++) {
    			const start = batch * batchSize; // 当前批次开始的索引
    			const end = Math.min(start + batchSize, totalDocs); // 当前批次结束的索引
    			const batchPromises = []; // 用于存储当前批次的 Promise

    			// 遍历当前批次的页面
    			for (let i = start; i < end; i++) {
      				// 将每页转换为 canvas,并将 Promise 添加到数组中
      				batchPromises.push(this.htmlToCanvas(docs[i], i));
    			}

    			// 等待当前批次的所有页面转换完成
   		 		await Promise.all(batchPromises);

    			// 将当前批次的图像嵌入到 PDF 中
    			for (let i = start; i < end; i++) {
      			// 获取当前页面图像的字节数据
      			const imgBytes = await fetch(this.pageDataArr[i]).then((res) => res.arrayBuffer());
      			// 嵌入 JPEG 图像
      			const img = await pdfDoc.embedJpg(imgBytes); 

      			// 创建新的一页,设置页面尺寸为 A4
      			const page = pdfDoc.addPage([595.28, 841.89]);
      			// 计算图像自适应宽高
      			const { width, height } = img.scaleToFit(595.28, 841.89); 
     			// 将图像绘制到页面中心
      			page.drawImage(img, {
       			 	x: (page.getWidth() - width) / 2,
        		 	y: (page.getHeight() - height) / 2,
        			width,
        			height
      		  	});
    		}
  		}

  		// 生成 PDF 文件并触发下载
  		const pdfBytes = await pdfDoc.save(); // 保存 PDF 文档
  		const blob = new Blob([pdfBytes], { type: 'application/pdf' }); // 创建 Blob 对象
  		const url = URL.createObjectURL(blob); // 生成下载链接
  		const a = document.createElement('a'); // 创建一个临时链接
  		a.href = url; // 设置链接地址
  		// 下载文件的名称
  		a.download = (this.reportInfo.name || this.reportInfo.name) + '报告.pdf'; 
  		document.body.appendChild(a); // 将链接添加到文档
  		a.click(); // 触发下载
  		document.body.removeChild(a); // 下载后移除链接
  
  		// 下载完成后恢复下载按钮状态
  		this.disDownloadReport = false; 
	},

	htmlToCanvas(doc, index) {
  		// 将 HTML 元素转换为 Canvas 的函数
  		return new Promise((resolve, reject) => {
    		// 获取元素的计算样式
    		const box = window.getComputedStyle(doc);
    		const width = box.width.replace('px', ''); // 获取元素宽度
    		const height = box.height.replace('px', ''); // 获取元素高度
    		// 根据设备像素比进行缩放
    		const scaleBy = window.devicePixelRatio > 1 ? window.devicePixelRatio : 1;

    		// 使用 html2canvas 将 HTML 元素转换为 Canvas
    		html2canvas(doc, {
      			scale: window.devicePixelRatio * 2, // 设置缩放比例
      			width,
      			height,
      			scrollX: 0,
      			scrollY: 0
    		}).then((canvas) => {
      			// 将 Canvas 转换为 JPEG 格式的数据 URL
      			let pageData = canvas.toDataURL('image/jpeg', 1.0);
      			this.pageDataArr[index] = pageData; // 存储图像数据
		    	resolve(canvas); // 解析 Promise
   			}).catch((error) => {
      			// 捕获并记录转换错误
      			console.error('Error generating canvas:', error);
      			reject(error); // 拒绝 Promise
    		});
  		});
	},
  }
}
</script>

结论

通过上述步骤,将网页内容转换为 PDF 文档,并实现下载功能。此方法不仅高效,而且易于实现,适合于各种需要生成 PDF 报告的场景。 如果在实现过程中遇到任何问题,请随时参考库的文档或寻求社区的帮助。 可以根据具体的项目需求和受众调整文章的内容和深度。如果有特定的格式要求或风格。