日常业务-PDF常用导出方案

日常-PDF常用导出方案

在前端的日常业务中,经常会有需要提供下载 pdf 的功能,可能是下载数据报表、发票、合同凭证等等。一般来说,pdf 文件是由后端服务进行生成的。后端服务能够获取足够的信息用于生成 pdf 文件,后端能够存储 pdf 文件缓存已供多次下载,后端生成 pdf 可以以异步的方式发送至多端比如浏览器直接下载、邮件、短信等。 不过后端生成 pdf 也有一定的局限性,后端生成 pdf 一般是先设置文件的模板,然后根据用户的请求填充对应的数据,然后提供给用户。在这种场景下,文件模板一般由产品经理和业务方确认后提供给后端进行生成,模板的样式一般较为简陋,比较适用于简单文档、数据表格等内容。但是偶尔也会有需要由前端开发进行生成 pdf 文件的场景,比如说文档样式比较复杂、或者样式允许用户自定义的情况。

React + Python

React:主要使用了1. 前端使用html-to-image库生成base64数据,调用接口传给服务端

html-to-image库:html-to-image是一个JavaScript库,用于将HTML元素转换为图像。它可以在浏览器中将HTML内容渲染为图像,并将其导出为PNG或JPEG格式的图像文件。这对于需要在网页上动态生成图像的应用程序非常有用,比如生成图表、截图、预览等。 可以使用html-to-image库来捕获特定的HTML元素,然后将其转换为图像

ini 复制代码
import { toBlob } from 'html-to-image';

const exportPdfData = (data, filename) => {
  const url = window.URL.createObjectURL(new Blob([data], { type: '.pdf' }));
  let a = document.createElement('a');
  a.style.display = 'none';
  a.href = url;
  a.setAttribute('download', `${filename}.pdf`);
  document.body.appendChild(a);
  a.click();
  window.URL.revokeObjectURL(url);
  document.body.removeChild(a);
};

toBlob(element).then(blob => {
  const blobData = new Blob([blob], { type: 'image/png' });
  const reader = new FileReader();

  reader.onload = function (event: any) {
    // 1. 获取 base64 图片数据
    const base64String = event.target.result;
    // 2. 传给后端,后端来裁剪和拼接

    // 3. 获取后端返回的数据流data,前端进行下载
    exportPdfData(data, 'xxx.pdf')

  }
  reader.readAsDataURL(blobData);
})

Python:主要使用了reportlab拿到base64数据后,进行裁剪、拼接

ReportLab:是一个Python库,用于生成PDF文档。它提供了丰富的功能和工具,使用户能够通过编程方式创建高质量的PDF文件。您可以使用ReportLab库来添加文本、图像、表格、图形等内容到PDF文档中,并控制页面布局、样式等方面。

python 复制代码
import base64
from io import BytesIO

import arrow
from PIL import Image as PImage
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import SimpleDocTemplate, Image

PAGE_HEIGHT = A4[1]
PAGE_WIDTH = A4[0]
song = "simsun"
pdfmetrics.registerFont(TTFont(song, "SimSun.ttf"))

def base64_to_image(base64_string, output_path):
    try:
        base64_data = base64_string.split(',')[1]
        # 解码Base64字符串为字节数据
        image_data = base64.b64decode(base64_data)
        # file = open(output_path, 'wb')
        # file.write(image_data)
        # file.close()
        # 创建PIL图像对象
        image = PImage.open(BytesIO(image_data))

        # 保存图像到指定路径
        image.save(output_path)
        print(f"图像已保存到 {output_path}")
        return output_path
    except Exception as e:
        print(f"出现错误:{e}")

def image_cv(path, img_height_config):
    image = PImage.open(path)
    print(image.size)
    width = image.size[0]
    result = []
    cur_height = 0
    i = 0
    for img_height in img_height_config:
        cur_height += 1
        crop = image.crop((0, cur_height, width, cur_height + img_height))
        save_path = f"temp/temp{i}.png"
        i += 1
        crop.save(save_path)
        result.append({
            "width": width,
            "height": img_height,
            "path": save_path
        })
        cur_height = cur_height + img_height
    return result

def image_to_pdf(images_info, pdf_path):
    # 打开图片
    images = list()
    for i in images_info:
        # A4标准宽高为 21cm * 29.7cm
        image = Image(i["path"])
        image.drawWidth = 21 * cm  # 设置图片的宽度
        image.drawHeight = i["height"]/i["width"] * 21 * cm  # 设置图片的高度
        images.append(image)

    # 设置页面大小
    doc = SimpleDocTemplate(pdf_path, pagesize=A4)
    doc.topMargin = 0
    doc.bottomMargin = 0
    doc.leftMargin = 0
    doc.rightMargin = 0

    # 添加图片到PDF
    doc.build(images)
# doc.build(images, onFirstPage=my_first_pages, onLaterPages=my_later_pages)

def draw_page_info(c: Canvas):
    """绘制页眉"""
    c.setFillColor(colors.aliceblue)
    c.rect(0, PAGE_HEIGHT-50, PAGE_WIDTH, 50, stroke=0, fill=True)
    c.setFont(song, 8)
    c.setFillColor(colors.black)
    c.drawString(PAGE_WIDTH - 105, PAGE_HEIGHT - 30, f"长安UNI-V")
    """绘制页脚"""
    timestamp = arrow.now()
    formatted_time = timestamp.format("YYYY-MM-DD")
    # 设置边框颜色
    c.setStrokeColor(colors.dimgrey)
    # 绘制线条
    c.line(30, PAGE_HEIGHT - 790, 570, PAGE_HEIGHT - 790)
    # 绘制页脚文字
    c.setFont(song, 8)
    c.setFillColor(colors.black)
    c.drawString(PAGE_WIDTH - 105, PAGE_HEIGHT - 810, f"长安UNI-V {formatted_time}")

def my_first_pages(c: Canvas, doc):
    c.saveState()
    # 绘制页眉页脚
    draw_page_info(c)
    c.restoreState()

def my_later_pages(c: Canvas, doc):
    c.saveState()
    # 绘制页眉页脚
    draw_page_info(c)
    c.restoreState()

if __name__ == '__main__':
    with open("a.txt", "r") as f:
        content = f.read()

    img_path = base64_to_image(content, 'img2.png')
    img_config = [2532, 1660, 2287, 1742, 2514, 1468, 2293, 1888]
    images_info = image_cv(img_path, img_config)
    pdf_path = 'output.pdf'  # 替换成想要保存的PDF文件路径
    image_to_pdf(images_info, pdf_path)

适用场景

  1. 页面复杂但是页面数量不多,由前端生成图片,后端生成pdf
  2. 需要动态添加数据的场景,由后端动态添加后生成pdf

其他文档参考

Pkmage

主要是通过Pkmage 来进行PDF导出(支持服务端(node) 客户端)

pdfmake 是一个方便的 JavaScript 库,是免费且开源的,可用于简化在 Web 应用程序中创建 PDF 文档的过程。您可以通过定义文本、图像、表格等结构化数据来声明 PDF 文档结构,并应用样式,pdfmake 将处理其余工作,以创建具有所需视觉样式的 PDF。

pdfmake 的主要特点包括:

  • 创建 PDF 文档:通过结构化数据,轻松生成报告、发票、表单等类型的文档。
  • 添加表格:轻松设计和插入表格,使显示表格数据和保持结构化布局变得简单。
  • 添加图像:允许在 PDF 文档中包含图像,可以增强视觉吸引力。
  • 添加密码:支持向 PDF 添加密码保护,有助于通过需要密码访问文档来保护敏感信息

服务端(node)

因为pdfmake是由纯JavaScript写的 所以可以在node中使用

createPdfKitDocumentcreatePdf 区别

createPdfKitDocument 方法用于创建一个 PDF 文档实例,该实例可以进一步处理、修改或导出为文件。这个方法返回一个 PDF 文档对象,您可以对其进行进一步的操作。

  1. pipe:将 PDF 文档对象连接到可写流,用于将 PDF 数据写入文件或其他目标。
  2. end:结束 PDF 文档的写入,完成文档的生成。 因为pdfDoc.pipe(res)方法将PDF文档的数据流式传输到响应对象res中,而pdfDoc.end()用于标记数据流的结束。因此,当数据流传输完成后,Node.js会自动关闭响应
  3. addPage:向文档添加新页面。
  4. text:在当前页面上添加文本内容。
  5. image:在当前页面上添加图像。
  6. fontSize:设置文本的字体大小。
  7. moveTolineTo:用于绘制线条或路径。

createPdf 方法用于将文档定义对象转换为 PDF 文档。它接受文档定义对象作为参数,并返回一个包含 PDF 数据的 Promise 对象。这个方法可以用于将文档定义转换为 PDF 数据,以便在浏览器中预览或下载。

因此,createPdfKitDocument 用于创建 PDF 文档实例,而 createPdf 用于将文档定义转换为 PDF 数据。

import 复制代码
function generatePdf(req, res) {
    var fonts = {
        // ...字体定义...
        Roboto: {
            normal: 'fonts/alibaba.ttf',
            bold: 'fonts/alibaba-bold.ttf',
            italics: 'fonts/alibaba.ttf',
            bolditalics: 'fonts/alibaba-bold.ttf'
        }
    };
    const documentData = req.body;
    // 使用Zod schema验证请求体数据
    const validationResult = documentSchema.safeParse(documentData);
    if (!validationResult.success) {
        res.status(400).json({ error: '数据格式不正确', details: validationResult.error });
        return
    }

    var printer = new PdfPrinter(fonts);

    var pdfDoc = printer.createPdfKitDocument(documentData);
    res.setHeader('Content-Disposition', 'attachment; filename="generated.pdf"');
    // 将生成的 PDF 发送给客户端
    pdfDoc.pipe(res);
    pdfDoc.end();
}

效果展示pdfmake.org/playground....

适用场景

  1. 动态数据展示,如分页

puppeteer(是一个基于 Chrome/Chromium 的浏览器自动化工具,node.js)

简介:Puppeteer 是一个 Node.js 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chrome/Chromium。 Puppeteer 默认以无头模式(headless mode)运行,但可以配置为在完整("有头"headful mode)Chrome/Chromium 中运行

无头模式(headless mode) :即在没有界面的情况下运行 Chrome/Chromium 浏览器。无头模式可以在后台执行浏览器操作,无需显示浏览器窗口,这在许多自动化任务中非常有用,例如生成页面截图、进行性能测试等

完整("有头"headful mode)模式:是指在具有界面的 Chrome/Chromium 浏览器中运行 Puppeteer。这意味着你可以看到浏览器窗口,并且可以与浏览器进行交互,例如手动操作页面、调试代码等。
DevTools Protocol:开发工具协议 (DevTools Protocol) 是一种用于与浏览器进行通信的接口规范。它定义了一组命令和事件,允许开发人员通过发送命令和监听事件的方式与浏览器进行交互。开发工具协议提供了一种标准化的方式来控制和监视浏览器的行为,从而实现自动化测试、性能分析、调试等功能
Puppeteer 是一个 Node.js 库,它提供了一个高级 API 来通过开发工具协议 (DevTools Protocol) 控制 Chrome/Chromium 浏览器 1。你可以使用 Puppeteer 来进行各种操作,例如生成页面截图和 PDF、抓取 SPA (单页应用) 并生成预渲染内容、模拟键盘输入和表单提交等 2。Puppeteer 还支持调试功能,你可以将浏览器的 headless 模式关闭,添加 slowMo 参数来观察浏览器的操作,并且可以打开 Chrome DevTools 进行调试 5

用户传入页面 url等参数进行生成pdf

javascript 复制代码
import puppeteer, { Browser } from "puppeteer";
import { Request, Response } from "express";
import { printSchema } from "../zod";

let browserInstance: Browser;

async function getBrowserInstance() {
  if (!browserInstance) {
    browserInstance = await puppeteer.launch({
      headless: "new",
      args: [
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--disable-features=site-per-process",
      ],
    });
  }
  return browserInstance;
}

async function printPage(req: Request, res: Response) {
  const browser = await getBrowserInstance(); // 启动浏览器
  const page = await browser.newPage(); // 打开新页面
  try {
    // 获取参数
    const { url, selector, localStorageData, sessionStorageData, cookies } =
      req.body;

    // 验证参数
    const validationResult = printSchema.safeParse(req.body);
    if (!validationResult.success) {
      res
        .status(400)
        .json({ error: "数据格式不正确", details: validationResult.error });
      return;
    }

    // Inject localStorage data
    if (localStorageData) {
      await page.evaluateOnNewDocument((localStorageData) => {
        console.log(localStorageData);
        for (const key in localStorageData) {
          localStorage.setItem(key, JSON.stringify(localStorageData[key]));
        }
      }, localStorageData);
    }

    // Inject sessionStorage data
    if (sessionStorageData) {
      await page.evaluateOnNewDocument((sessionStorageData) => {
        for (const key in sessionStorageData) {
          sessionStorage.setItem(key, sessionStorageData[key]);
        }
      }, sessionStorageData);
    }

    // Set cookies
    if (cookies) {
      await page.setCookie(...cookies);
    }

    // Set page configuration and perform actions
    await page.setViewport({ width: 1920, height: 1080 });
    // const waitForNavigationPromise = page.waitForNavigation({ waitUntil: 'networkidle0' })
    await page.goto(url, { waitUntil: "networkidle0" }); // 跳转到指定页面

    // Wait for a specific element to load
    if (selector) {
      await page.waitForSelector(selector);
    }
    // else {
    //     await waitForNavigationPromise
    // }

    // Generate PDF
    const pdfBuffer = await page.pdf({
      path: "./temp.pdf",
      printBackground: true,
      scale: 0.5,
    });

    // Close the page but keep the browser open
    await page.close();

    // Send the generated PDF to the client
    res.contentType("application/pdf");
    res.send(pdfBuffer);
  } catch (error) {
    await page.close();
    res.status(500).send(JSON.stringify({ error: error.message }));
  }
}

export const puppeteerController = { printPage };

适用场景

  1. 性能要求不高

代码:github

相关推荐
2401_857622661 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
正小安1 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
2402_857589361 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
哎呦没3 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch3 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光3 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   3 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   3 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web3 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常3 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式