夜雨聆风学习资料网

ARTICLE · 1226859

前端 PDF 神器!停更 5 年,周下载仍是 4500 万!

前端 PDF 神器!停更 5 年,周下载仍是 4500 万!

大家好,我是 01。

前端和 PDF 的孽缘,比想象中深。

发票、合同、报告、证书,业务方总有各种"导出成 PDF"的需求。

而说到用 JavaScript 生成和修改 PDF,绕不开一个名字:pdf-lib。

它的 npm 周下载量是 4500 万,被 1800 多个包依赖。

但打开它的版本记录,会看到一个尴尬的事实:

最新版 1.17.1,发布于 5 年前。

停更 5 年,下载量不降反升。

今天就来聊聊:它凭什么,以及停更这 5 年发生了什么。

它是什么?

pdf-lib 的定位一句话讲完:

用 JavaScript 创建和修改 PDF 文档,零依赖。

支持的环境列出来很吓人:Node、浏览器、Deno、React Native 全覆盖。

一份代码,前端预览生成、服务端批量出票、边缘函数动态合成,都能跑。

TypeScript 类型内置,MIT 协议。

快速上手

import { PDFDocument, StandardFonts, rgb } from "pdf-lib";async function createPdf() {  const pdfDoc = await PDFDocument.create();  const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman);  const page = pdfDoc.addPage();  const { width, height } = page.getSize();  const fontSize = 30;  page.drawText("Creating PDFs in JavaScript is awesome!", {    x: 50,    y: height - 4 * fontSize,    size: fontSize,    font: timesRomanFont,    color: rgb(0, 0.53, 0.71),  });  const pdfBytes = await pdfDoc.save();}

除了 drawText,还有 drawRectangle、drawEllipse、drawLine、drawSvgPath,基本图形全覆盖。

embedFont 顺带解决了排版最头疼的问题:widthOfTextAtSize 可以精确测量文字宽度,居中、换行、省略号都能算。

常用高频场景

修改 PDF

import { degrees, PDFDocument, rgb, StandardFonts } from "pdf-lib";async function modifyPdf() {  const url = "https://pdf-lib.js.org/assets/with_update_sections.pdf";  const existingPdfBytes = await fetch(url).then((res) => res.arrayBuffer());  const pdfDoc = await PDFDocument.load(existingPdfBytes);  const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);  const pages = pdfDoc.getPages();  const firstPage = pages[0];  const { width, height } = firstPage.getSize();  firstPage.drawText("This text was added with JavaScript!", {    x: 5,    y: height / 2 + 300,    size: 50,    font: helveticaFont,    color: rgb(0.95, 0.1, 0.1),    rotate: degrees(-45),  });  const pdfBytes = await pdfDoc.save();}

修改 PDF 文档,添加文字、图片、表格等。

嵌入图片

import { PDFDocument } from "pdf-lib";async function embedImages() {  const jpgUrl = "https://pdf-lib.js.org/assets/cat_riding_unicorn.jpg";  const pngUrl = "https://pdf-lib.js.org/assets/minions_banana_alpha.png";  const jpgImageBytes = await fetch(jpgUrl).then((res) => res.arrayBuffer());  const pngImageBytes = await fetch(pngUrl).then((res) => res.arrayBuffer());  const pdfDoc = await PDFDocument.create();  const jpgImage = await pdfDoc.embedJpg(jpgImageBytes);  const pngImage = await pdfDoc.embedPng(pngImageBytes);  const jpgDims = jpgImage.scale(0.5);  const pngDims = pngImage.scale(0.5);  const page = pdfDoc.addPage();  page.drawImage(jpgImage, {    x: page.getWidth() / 2 - jpgDims.width / 2,    y: page.getHeight() / 2 - jpgDims.height / 2 + 250,    width: jpgDims.width,    height: jpgDims.height,  });  page.drawImage(pngImage, {    x: page.getWidth() / 2 - pngDims.width / 2 + 75,    y: page.getHeight() / 2 - pngDims.height + 250,    width: pngDims.width,    height: pngDims.height,  });  const pdfBytes = await pdfDoc.save();}

嵌入 PNG 和 JPEG 图像到 PDF 文档。

复制页面

import { PDFDocument } from "pdf-lib";async function copyPages() {  const url1 = "https://pdf-lib.js.org/assets/with_update_sections.pdf";  const url2 = "https://pdf-lib.js.org/assets/with_large_page_count.pdf";  const firstDonorPdfBytes = await fetch(url1).then((res) => res.arrayBuffer());  const secondDonorPdfBytes = await fetch(url2).then((res) =>    res.arrayBuffer(),  );  const firstDonorPdfDoc = await PDFDocument.load(firstDonorPdfBytes);  const secondDonorPdfDoc = await PDFDocument.load(secondDonorPdfBytes);  const pdfDoc = await PDFDocument.create();  const [firstDonorPage] = await pdfDoc.copyPages(firstDonorPdfDoc, [0]);  const [secondDonorPage] = await pdfDoc.copyPages(secondDonorPdfDoc, [742]);  pdfDoc.addPage(firstDonorPage);  pdfDoc.insertPage(0, secondDonorPage);  const pdfBytes = await pdfDoc.save();}

合并多个 PDF 文档,每个文档的第一页作为新文档的第一页,第 743 页作为新文档的第 2 页,以此类推。

中文用户必看

pdf-lib 内置的 StandardFonts(Helvetica 那一族)不包含中文字形。

写中文,必须嵌入自定义字体。

import fontkit from "@pdf-lib/fontkit";pdfDoc.registerFontkit(fontkit);const fontBytes = await fetch("/fonts/SourceHanSansSC.otf").then((r) =>  r.arrayBuffer(),);const font = await pdfDoc.embedFont(fontBytes, { subset: true });page.drawText("你好,PDF", { font, size: 24 });

subset: true 会只嵌入用到的字形,体积从十几 MB 压到几十 KB。

但注意:原版 pdf-lib 的子集化在部分中日韩字体上存在字符损坏的 bug(已知 issue)。

踩到这个坑,有两个出路,往下看。

停更 5 年,谁在接棒

先回答大家最关心的问题:为什么停更?

作者 Hopding 在维护者文档里写得很坦诚:他不拿一分钱维护这个项目,每周只有约 5 小时,还有全职工作。

4500 万周下载的库,一个人用爱发电。

不是跑路,是精力见顶了。

于是社区出现了两个活跃的 fork。

@cantoo/pdf-lib:功能增强派

法国公司 Cantoo 为自家产品维护,保持原版 API 和文档不变,2 天前刚发了新版本。

增强清单相当扎实:

  • • 完整 SVG 绘制:drawSvg 直接嵌整个 SVG,不只是 path
  • • 加密:PDFDocument.encrypt(),AES-256
  • • PDF/A 转换:convertToPDFA(),归档场景刚需
  • • Factur-X / ZUGFeRD:欧洲电子发票标准直接嵌
  • • 增量更新:电子签名工作流的关键能力
  • • PDF 图层、附件增删、截断文件恢复……

选它的情况:需要 SVG、加密、PDF/A 这些原版没有的能力。

@pdfme/pdf-lib:修坑实用派

pdfme(PDF 模板引擎)生态的 fork,同样是 9 天前还在发版。

它修掉了原版的 CJK 字体子集化 bug,还顺手加了圆角矩形。

用 pdf-lib 写中文遇到字符损坏,换成它最省事。

怎么选

  • • 只做基础生成/修改:原版 pdf-lib 照用,停更不等于不能用,PDF 格式本身也很稳定
  • • 要 SVG、加密、PDF/A、电子签:@cantoo/pdf-lib,API 无缝切换
  • • 中文项目踩到字体 bug:@pdfme/pdf-lib
  • • 要模板化设计 PDF:直接看 pdfme 本体

另外划清边界,pdf-lib 不做这些事:

  • • 渲染/预览 PDF:那是 pdf.js、react-pdf 的活
  • • 提取 PDF 文本:看 pdf-parse、unpdf
  • • HTML 转 PDF:无头浏览器方案更合适

写在最后

开源世界的一个经典悖论:最有用的库,往往维护得最少。

pdf-lib 用 5 年没更新的 1.17.1,撑起了每周 4500 万次下载。

而它的接棒者们,正让这个故事继续下去。

下次和 PDF 搏斗之前,先看看这条工具链。

官网:https://pdf-lib.js.org/

原版 GitHub:https://github.com/Hopding/pdf-lib

相关学习资料