前端导出PDF(适配ios Safari浏览器)

发布于:2025-06-13 ⋅ 阅读:(17) ⋅ 点赞:(0)

目前市面上常用的前端导出PDF库组合一般为:

1. html2canvas + js-pdf

2. html2canvas+pdf-lib

3. domtoimage+pdf-lib

因本人项目中导出pdf需求为导出30页及以上的多页pdf,考虑性能问题,选择了 html2canvas+pdf-lib 及domtoimage+pdf-lib两种方式尝试实现

html2canvas+pdf-lib(个人推荐,因为适配ios Safari浏览器)

     本人是先尝试使用的domtoimage+pdf-lib方案,但实测中发现H5在ios Safari浏览器端倒不出来故有个html2canvas+pdf-lib方案,经实战测试该方案能够适配ios Safari浏览器导出!!!

代码如下:

首先引入必要插件:

yarn add pdf-lib
yarn add html2canvas
yarn add file-saver 

file-saver 插件很重要,使用a.click方案导出的pdf在Safari中不是直接下载,而是打开一个类似预览页的页面查看pdf,需用户分享导出,比较麻烦。

async downloadPDF() {
      // 创建一个新的 PDF 文档
      const pdfDoc = await PDFDocument.create();
      // 处理需转pdf的dom的id数组
      const pdfDoms = await this.handlePDFPageDom();
      this.allNum = pdfDoms.length;

      for (let i = 0; i < pdfDoms.length; i++) {
        this.loadText = `文件生成中${i + 1}/${this.allNum}`;
        const doc = document.querySelector("#" + pdfDoms[i]);
        const canvas = await html2canvas(doc, {
          scale: 2, // 提高清晰度,控制内存
          useCORS: true,
        });
        const imgDataUrl = canvas.toDataURL("image/jpeg", 0.95); // 压缩图像
        const imgBytes = await fetch(imgDataUrl).then((res) =>
          res.arrayBuffer()
        );
        const img = await pdfDoc.embedJpg(imgBytes);
        // const { width, height } = img.scaleToFit(595.28, 841.89);
        const A4_WIDTH = 595.28; // A4 宽度
        const A4_HEIGHT = 841.89; // A4 高度
        const scale = Math.min(A4_WIDTH / img.width, A4_HEIGHT / img.height);
        const scaledWidth = img.width * scale;
        const scaledHeight = img.height * scale;
        const xOffset = (A4_WIDTH - scaledWidth) / 2;
        const yOffset = (A4_HEIGHT - scaledHeight) / 2;
        const page = pdfDoc.addPage([595.28, 841.89]);
        page.drawImage(img, {
          x: xOffset,
          y: yOffset,
          width: scaledWidth,
          height: scaledHeight,
        });
        canvas.remove();

        await new Promise((resolve) => setTimeout(resolve, 100)); // 防止卡死
      }
      const pdfBytes = await pdfDoc.save();
      const blob = new Blob([pdfBytes], {
        type: "application/octet-stream",
      });
      FileSaver.saveAs(blob, `导出的PDF.pdf`);
      uni.hideLoading();
      this.loadText = "文件生成成功!";
    },

domtoimage+pdf-lib

async downloadPDF() {
      this.loadText = "文件生成中...";
      // 创建一个新的 PDF 文档
      const pdfDoc = await PDFDocument.create();
      // 处理需转pdf的dom id
      const pdfDoms = await this.handlePDFPageDom();
      let pdfPage = [];
      let base64Arr = [];
      for (let i = 0; i < pdfDoms.length; i++) {
        const element = document.getElementById(pdfDoms[i]);
        const url = await domtoimage.toPng(element, {
          quality: 0.95,
          skipFonts: true,
        });
        base64Arr.push({ base64: url });
      }
      await base64Arr.map((item, index) => {
        pdfDoc.addPage([595.28, 841.89]);
        pdfPage.push(this.handleReportView(item.base64, index, pdfDoc));
      });

      await Promise.all(pdfPage)
        .then(async (res) => {
          // 将 PDF 文档保存为 Uint8Array
          const pdfBytes = await pdfDoc.save();

          // 生成下载链接并自动下载 PDF
          const blob = new Blob([pdfBytes], { type: "application/pdf" });
          const link = document.createElement("a");
          link.href = URL.createObjectURL(blob);
          link.download = `${this.studentName}.pdf`;
          link.click();
          URL.revokeObjectURL(link.href);
          uni.hideLoading();
          this.loadText = "文件生成成功!";
          setTimeout(() => {
            window.parent.postMessage({
              cmd: "success",
            });
          }, 1000);
        })
        .catch((err) => {
          // PDF = null;
          console.log("生成失败", err);
        });
    },
async handleReportView(imgBase64, index, pdfDoc) {
      const A4_WIDTH = 595.28; // A4 宽度
      const A4_HEIGHT = 841.89; // A4 高度
      // 获取所有页面
      const pages = pdfDoc.getPages();
      // 修改第index页(索引从0开始)
      const pageNow = pages[index];
      return await new Promise(async (resolve, reject) => {
        const pageData = imgBase64;
        // setTimeout(() => {
          let img = new Image();
          img.crossOrigin = "Anonymous";
          img.onload = async () => {
            const imgBytes = await fetch(pageData).then((res) =>
              res.arrayBuffer()
            );
            // 嵌入 PNG 图片
            const pngImage = await pdfDoc.embedPng(imgBytes);
            const { width: imgWidth, height: imgHeight } = img;
            // 计算缩放比例,确保图片适应 A4 页面并保持宽高比
            const scale = Math.min(A4_WIDTH / imgWidth, A4_HEIGHT / imgHeight);
            const scaledWidth = imgWidth * scale;
            const scaledHeight = imgHeight * scale;
            img.width = scaledWidth;
            img.height = scaledHeight;
            // 计算图片的偏移量,使其居中显示在页面上
            const xOffset = (A4_WIDTH - scaledWidth) / 2;
            const yOffset = (A4_HEIGHT - scaledHeight) / 2;
            // 将内容设置到第几页
            await pageNow.drawImage(pngImage, {
              x: xOffset,
              y: yOffset,
              width: scaledWidth,
              height: scaledHeight,
            });
            resolve();
          };
          img.onerror = () => {
            alert("资源加载失败");
            resolve();
          };
          img.src = pageData;
        // }, 500);
      }).catch((err) => {
        return Promise.resolve();
      });
    },