PDF 生成(3)— 封面、尾页

当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注点赞收藏评论

新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn

文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。

回顾

PDF 生成(2)--- 生成 PDF 文件 我们以百度新闻页为例为大家展示了 puppeteer 的基本使用:

  • 通过短短的 10行 代码将百度新闻页打印成一份 PDF 文件
  • 通过 puppeteer 的 page.evaluate 方法为浏览器注入一段 JS 代码,用代码来模拟页面滚动,以解决懒加载的问题,从而保证 PDF 文件内容的完整性
  • 通过自定义页眉、页脚的方式讲解了 puppeteer 中关于页眉、页脚相关选项的基本使用和其中的

文章最后也提到了 puppeteer 在 PDF 文件生成场景下的能力基本到头了 ,但现有内容在我们的技术架构中只是九牛一毛,所以,接下来的内容就全是基于 puppeteer 的增量开发了,也是整套架构的核心难点

问题

一份专业的 PDF 文件都会有自己的封面尾页。在本文开始之前,大家先想想,基于现状如何为我们之前生成的 PDF 文件增加封面和尾页呢?比如

所以,本文的内容就是为我们在上文中生成的 PDF 文件增加封面和尾页。

分析

不知道大家是否还记得在 PDF 生成(1)--- 开篇 中的技术架构图,为什么架构图中的 PDF 生成服务会产出 3份 PDF 文件?带着问题接着往下看。

假设前文中我们用的百度新闻页就是我们自己开发的一个页面,那在页面的开始和结束位置分别加上封面和尾页的 DOM,然后直接生成 PDF 文件,是不是就可以了?想想,这样做最简单了,一个页面搞定所有内容,比如:

但稍微一分析,就发现不行,因为我们我们在 page.pdf 方法中设置的 margin 属性和页眉、页脚是针对整个 PDF 文件的,但封面和尾页不需要边距和页眉、页脚。

一个页面(URL)对应一份 PDF 文件,这是大方向,是由技术方案本身的特性所决定的,因此封面和尾页不能和内容页放一起。

经过分析,结合架构图的指引,我们的实现思路是一份完整的 PDF 文件至少应该包括三个页面 ------ 封面页、内容页、尾页,每个页面对应一份 PDF 文件,最后将三份 PDF 合并成一份 PDF,接下来就进入实战。

实战

前端页面的开发不是重点,所以这里我们就简单写了。

封面页 --- /fe/cover.html

xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    body {
      width: 100%;
      height: 1123px;
      background: linear-gradient(173deg, #F5F8FF 20%, #E1E9FC 80%, rgba(225, 233, 252, 0) 86%);
      display: flex;
      justify-content: center;
      align-items: center;
    }
  </style>
</head>
<body>
  <h1>我是封面</h1>
</body>
</html>

尾页 --- /fe/last-page.html

xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    body {
      width: 100%;
      height: 1123px;
      background: linear-gradient(173deg, #F5F8FF 20%, #E1E9FC 80%, rgba(225, 233, 252, 0) 86%);
      display: flex;
      justify-content: center;
      align-items: center;
    }
  </style>
</head>
<body>
  <h1>我是尾页</h1>
</body>
</html>

PDF 生成服务 --- /server/index.mjs

/server/index.mjs 中增加如下代码,用来生成封面和尾页的 PDF 文件

截图中对应的代码如下:

php 复制代码
/* 省略之前的代码... */
  // 封面
  await page.goto('file:///Users/liyongning/studyspace/generate-pdf/fe/cover.html')
  await page.pdf({
    path: './cover.pdf',
    format: 'A4',
    printBackground: true
  })
  // 尾页
  await page.goto('file:///Users/liyongning/studyspace/generate-pdf/fe/last-page.html')
  await page.pdf({
    path: './last-page.pdf',
    format: 'A4',
    printBackground: true
  })
/* 省略之后的代码... */

生成的 PDF 效果如下:

解析来就是本文的重点了 --- PDF 文件合并,因为我们最终交付的是一份 PDF 文件,而不是三份。

PDF 文件合并

我们借助第三方库 pdf-lib 来完成 PDF 文件的合并。

  • 首先安装 pdf-lib ------ npm i pdf-lib
  • 新建 /server/merge-pdf.mjs 文件来编写文件合并的代码

实现如下:

/server/index.mjs:

/server/merge-pdf.mjs:

这里大家可能会有两个疑问点:

  • 为什么不直接通过 Buffer.concat 合并内容,然后直接写盘,而是要通过 第三方库 先合并再写盘(page.pdf 的返回值是一个 Buffer 类型的数据)
  • 为什么不新创建一份 PDF 文件,然后将三个文件合并到一起,或者是将内容页 PDF 的各个页面和尾页 PDF 的页面添加到封面 PDF 中,而是分别将封面 PDF 的页面和尾页 PDF 的页面插到内容 PDF 的对应位置

第一个问题的答案是:数据格式问题 ,虽然都是保存在内存中的二进制内容,但是 PDF 文件的二进制内容格式有点特殊,如果直接通过 Buffer.concat 将内容拼接,会发现拼接的内容就丢了,所以这里需要借助专门操作 PDF 文件的第三方库。当然了,如果是一个普通的文本文件,通过 Buffer.concat 完全没问题,有兴趣的话大家可以自己写个简单的 Demo。

至于第二个问题,答案是:不行,简单解释就是 ------ 在当前的技术架构下,会导致目录页中目录项的页面跳转能力失效,目录页会用到 HTML 锚点,这些锚点被 pdf-lib 处理之后就失效了。具体内容在后面 PDF 生成(4)--- 目录页 详细讲解。

最终效果图如下:

PDF 文件合并(/server/merge-pdf.mjs)的完整代码如下:

javascript 复制代码
import { PDFDocument } from 'pdf-lib'
​
/**
 * 将三份 PDF 文件合并为一份
 *    另外三个参数的类型都是 Buffer,是表示 PDF 文件加载到内存后二进制内容
 * @param { Buffer } coverBuffer 封面 PDF
 * @param { Buffer } contentBuffer 内容页 PDF
 * @param { Buffer } lastPageBuffer 尾页 PDF
 * @returns 合并后的 PDF 文件的二进制内容
 */
export default async function mergePDF(coverBuffer, contentBuffer, lastPageBuffer) {
  // 通过 pdf-lib 加载现有的 3份 PDF 文档
  const { load } = PDFDocument
  const [coverPdfDoc, contentPdfDoc, lastPagePdfDoc] = await Promise.all([load(coverBuffer), load(contentBuffer), load(lastPageBuffer)])
  // 分别将封面文档和尾页文档的第一页拷贝到内容文档
  const [[coverPage], [lastPagePage]] = await Promise.all([contentPdfDoc.copyPages(coverPdfDoc, [0]), contentPdfDoc.copyPages(lastPagePdfDoc, [0])])
  // 将封面页插入到 内容文档 的第 0 页,即最开始的位置
  contentPdfDoc.insertPage(0, coverPage)
  // 将尾页添加到 内容文档 的最后一页
  contentPdfDoc.addPage(lastPagePage)
  // 将合并后的 内容文档 序列化为字节数组(Uint8Array),并以二进制的格式返回
  return Buffer.from(await contentPdfDoc.save())
}

总结

本文介绍了如何为通过 Puppeteer 生成的 PDF 文件添加封面和尾页,现在再来整体回顾一下:

  • 首先,技术方案决定了一个页面对应一份 PDF 文件,这是大前提,因为 page.xx 方法的所有配置都是针对当前页的
  • 在大前提下,我们通过 PDF 文件合并方案(pdf-lib),分别将封面 PDF、内容页 PDF 和尾页 PDF 三份文件合并为一份报告包含封面、内容页和尾页的完整 PDF

到这里,PDF 文件的整体框架已经基本形成(包括封面、内容页、尾页),但还有一点不完整,比如缺少目录页 ,一份完整的文件或文章怎么能没有目录呢?所以,接下来我们就讲 PDF 生成(4)--- 目录页

链接

  • PDF 生成(1)--- 开篇 中讲解了 PDF 生成的技术背景、方案选型和决策,以及整个方案的技术架构图,所以后面的几篇一直都是在实现整套技术架构
  • PDF 生成(2)--- 生成 PDF 文件 中我们通过 puppeteer 来生成 PDF 文件,并讲了自定义页眉、页脚的使用和其中的 。本文结束之后 puppeteer 在 PDF 文件生成场景下的能力也基本到头了,所以,接下来的内容就全是基于 puppeteer 的增量开发了,也是整套架构的核心难点
  • PDF 生成(3)--- 封面、尾页 通过 PDF 文件合并技术让一份 PDF 文件包含封面、内容页和尾页三部分。
  • PDF 生成(4)--- 目录页 通过在内容页的开始位置动态插入 HTML 锚点、页面缩放、锚点元素高度计算、换页高度补偿等技术让 PDF 文件拥有了包含准确页码 + 页面跳转能力的目录页
  • PDF 生成(5)--- 内容页支持由多页面组成 通过多页面合并技术 + 样式沙箱解决了用户在复杂 PDF 场景下前端代码维护问题,让用户的开发更自由、更符合业务逻辑
  • PDF 生成(6)--- 服务化、配置化 就是本文了,本系列的最后一篇,以服务化的方式对外提供 PDF 生成能力,通过配置服务来维护接入方的信息,通过队列来做并发控制和任务分类
  • 代码仓库 欢迎 Star

当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注点赞收藏评论

新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn

文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。

相关推荐
hong_zc3 分钟前
初始 html
前端·html
小小吱9 分钟前
HTML动画
前端·html
Bio Coder25 分钟前
学习用 Javascript、HTML、CSS 以及 Node.js 开发一个 uTools 插件,学习计划及其周期
javascript·学习·html·开发·utools
糊涂涂是个小盆友30 分钟前
前端 - 使用uniapp+vue搭建前端项目(app端)
前端·vue.js·uni-app
浮华似水1 小时前
Javascirpt时区——脱坑指南
前端
王二端茶倒水1 小时前
大龄程序员兼职跑外卖第五周之亲身感悟
前端·后端·程序员
_oP_i1 小时前
Web 与 Unity 之间的交互
前端·unity·交互
钢铁小狗侠1 小时前
前端(1)——快速入门HTML
前端·html
凹凸曼打不赢小怪兽1 小时前
react 受控组件和非受控组件
前端·javascript·react.js
狂奔solar2 小时前
分享个好玩的,在k8s上部署web版macos
前端·macos·kubernetes