如何从PDF中提取页面并用JavaScript渲染它们

162 阅读10分钟

PDF是便携式文档格式的缩写。PDF是由Adobe在90年代为Windows设计的。它们是独立的文件,支持几乎所有主要的操作系统。

但有时你需要修改PDF以满足你的需要,而不仅仅是查看它。不幸的是,用于PDF的现有软件往往不能满足你的专门要求。

但你是一个程序员,对吗?为什么不做一些软件来帮助PDF按照你的要求来工作呢?嗯,这就是本文的灵感所在。

在这篇文章中,我们将探讨所有流行的PDF相关的JavaScript库。为什么是JavaScript?因为它有一些相当不错的PDF包可用,而且人们喜欢它。尤其是我自己。

我们将在本教程中做什么?

  1. 首先,我们将探索一些流行的PDF包,用于JavaScript中的PDF相关工作。然后,我们将对它们进行比较,找到适合我们要求的最佳软件包。
  2. 接下来,我们将加载一个现有的PDF,并从其中提取一些页面。提取的页面将组成一个新的PDF文档。
  3. 然后,我们将在浏览器中渲染这个新的PDF(我们在第二步中制作的)。
  4. 最后,我们将下载新的PDF供以后使用。

所以这些就是我们在这里要经历的所有步骤。我希望你能兴奋地看到结果。让我们深入了解一下。

用于JavaScript的PDF库

我发现JavaScript中的PDF库主要有两种类型。一种是用于PDF渲染,另一种是用于PDF操作(或修改)。我在搜索了一个多小时后发现了一堆PDF库,这些是我最好的选择。

这里列出的所有软件包都是免费的、开源的软件包。你可以在npm注册表中找到所有这些包。

pdfjs

这个包是由火狐网络浏览器背后的公司Mozilla制作的。pdfjs是一个基于网络标准的平台,用于解析和渲染PDF。
当你在Firefox中查看PDF时,PDF查看器就是用这个pdfjs包制作的。

这个包的核心优势是在网页上进行PDF渲染。其他的PDF修改功能在这个包里是非常有限的。如果你想为你的网站制作一个自定义的PDF浏览器,这可能是你正在寻找的软件包。

pdfjs有一个非常简单的API。他们有很多教程可以让你开始使用这个库。如果你还不相信,那就用这个库玩上一段时间,你一定会爱上它的。

pdf-lib

与之前的pdfjs包不同,pdf-lib主要用于PDF的创建和操作。你可以根据你的需要用这个包动态地生成一个新的PDF文档。

这个包对修改现有文档有强大的支持。你可以用这个库做很多PDF的修改。例如,你可以做PDF的分割和合并,你可以提取一个页面,注释一个PDF文档,添加一个大纲,以及更多你能想象的事情。

它只有JavaScript作为一个依赖项。因此,它可以运行在任何具有JavaScript运行时间的设备上。浏览器、Nodejs、Deno和React Native都得到良好的支持。如果你能设法在设备上安装JavaScript,那么这个库就肯定能工作。

pdf-lib的主要缺点是,它没有强大的渲染支持。如果你想用这个库做一个漂亮的PDF浏览用户界面,那么pdf-lib不是你的正确选择。在这种情况下,你应该使用pdfjs来代替。

pdfjs#2

如果你认为我在重复自己的话,那么我没有。这是一个用于创建PDF文档的JavaScript库。它有一个非常简单的API可以使用。

我们讨论过的前一个pdfjs库在用户界面上有非常强大的渲染支持,但它缺乏PDF创建和修改功能。

但这个库是以创建PDF为目的而建立的。它有一个非常简单的API,对初学者很友好。你可以将它与pdf-lib包进行比较。

这个pdfjs库的主要缺点是,对现有文档的修改支持仍处于测试阶段。它并不是一直在工作,而且仍在进行中。

如果你的主要关注点是PDF修改(例如,页面提取、合并、拆分、注释等),那么这个库可能不适合你。

如果贡献者能够使修改功能发挥作用,那么这可能是最好的JavaScript的PDF包。

js-pdf

与上面列出的所有PDF包不同,这个库是一个完整的野兽。你可以用这个库做任何与PDF有关的工作。这就像一个万能的库。如果你想要一些复杂的PDF相关的东西,那么这个库可以做到。

但在JavaScript中还有更好的包,对个别任务非常好。例如,pdfjs是一个比js-pdf更好的PDF渲染器,而pdf-lib比js-pdf有更好的修改支持。

这里我说的不是实际性能或其他类型的指标,我说的是开发者的体验。我发现它的API不是很直观。对于一个初学者来说,第一眼就会感到不知所措。不过,这是我的看法,也是我使用它时的体验。

PDF生成是这个库的主要优势。你可以用你的任何设计来生成任何类型的PDF。这个软件包将为你完成所有繁重的工作。如果你有经验,那么这可能是你的最佳选择。

react-pdf

顾名思义,这个库是专门用于React生态系统的。它的用法很像React。你可以用它类似JSX的语法轻松创建一个文档。

你可以用简单的React组件创建和显示一个PDF文档。但功能非常有限。这个库主要用于生成PDF。

如果你的目标是向用户显示一个PDF,那么你可以使用这个包。作为一个React爱好者,你会喜欢这个库。看看他们的游乐场,花些时间来使用这个包。这样你就会知道你是否需要这个库。

为什么我们要在本教程中使用pdf-lib?

在上面提到的所有这些PDF库中,我将在本文中使用pdf-lib。因为我们要分割和合并PDF页面,并在浏览器中渲染它们,pdf-lib似乎是这种情况下的最佳选择。

而且,pdf-lib有相当简单的API可以使用,所有这些API都有很好的文档。如果你使用TypeScript,你还可以获得类型推理,这非常有帮助。

最后但同样重要的是,他们的例子非常好。你可以在几分钟内启动并运行。所以我喜欢这个库,适合我的使用情况。

如何用JavaScript读取本地PDF文件

在对我们的PDF文件做任何操作之前,我们必须从用户那里获得该文件。在浏览器中读取任何文件都可以通过FileReader web API来处理。

首先,我们要做和文件输入按钮,然后用FileReader web API处理上传的文件。

<input type="file" id="file-selector" accept=".pdf" onChange={onFileSelected} />

由于Filereader API使用回调工作,我发现async/await要干净得多,也更容易操作。所以让我们做一个辅助函数,把Filereader的回调修改成async/await。

function readFileAsync(file) {
    return new Promise((resolve, reject) => {
      let reader = new FileReader();
      reader.onload = () => {
        resolve(reader.result);
      };
      reader.onerror = reject;
      reader.readAsArrayBuffer(file);
    });
  }

现在,当用户使用之前的文件输入上传文件时,我们监听文件输入事件,然后使用这个readFileAsync 函数读取文件。

这个逻辑的实现在代码中看起来像这样:

const onFileSelected = async (e) => {
    const fileList = e.target.files;
    if (fileList?.length > 0) {
      const pdfArrayBuffer = await readFileAsync(fileList[0]);
    }
  };

如何提取PDF页面

到此为止,我们的PDF被上传并转换为JavaScriptArrayBuffer 。由于我们要从PDF中提取一系列的页面,我们希望有一个包含PDF的这些页码的数组。

在JavaScript中生成一个自然数的数组并不难。所以我们做了一个名为range() 的函数来生成我们想要的所有索引。

我们必须提供开始页数和结束页数,然后这个range() 函数可以生成一个带有适当页数的数组:

function range(start, end) {
	let length = end - start + 1;
	return Array.from({ length }, (_, i) => start + i - 1);
}

这里我们在最后加上-1。你知道其中的原因吗?是的--在编程中,索引是从0开始的,而不是1。所以我们必须从每一个页码中扣除-1,以获得我们想要的行为。

现在让我们开始本文的主要部分:提取。在做任何工作之前,先导入pdf-lib库:

import { PDFDocument } from "pdf-lib";

首先,我们加载我们从之前的onFileSelected 函数中得到的PDFArrayBuffer 。然后我们将ArrayBuffer 加载到PDFDocument.load(arraybuffer) 函数中。这就是我们的用户提供的PDF。为了方便起见,我们把它叫做pdfSrcDoc

现在我们将创建一个新的PDF。所有从用户提供的文件中提取的PDF页面都被合并到新文件中。我们使用PDFDocument.create() 函数来做这件事。为了便于使用,我们称它为pdfNewDoc

之后,我们通过使用copyPages() 函数将我们想要的页面从pdfSrcDoc 复制到pdfNewDoc 。然后,我们将复制的页面添加到pdfNewDoc

为了保存这些变化,运行pdfNewDoc.save() 。让我们创建一个名为extractPdfPage() 的函数来重复使用这个逻辑。该函数内部的代码将看起来像这样:

async function extractPdfPage(arrayBuff) {
    const pdfSrcDoc = await PDFDocument.load(arrayBuff);
    const pdfNewDoc = await PDFDocument.create();
    const pages = await pdfNewDoc.copyPages(pdfSrcDoc,range(2,3));
    pages.forEach(page=>pdfNewDoc.addPage(page));
    const newpdf= await pdfNewDoc.save();
    return newpdf;
  }

我们将从extractPdfPage() 函数中返回一个Uint8Array

如何在浏览器中渲染PDF

到现在为止,我们有一个修改过的PDF的Uint8Array 。为了在浏览器中呈现它,我们必须将其转换为Blob。

然后我们把它做成一个URL,在一个iframe中呈现。

你也可以使用我上面提到的pdfjs库制作你的自定义PDF浏览器。但如果你不需要这样的品牌和定制,浏览器的默认PDF浏览器就可以达到这个目的:

function renderPdf(uint8array) {
    const tempblob = new Blob([uint8array], {
      type: "application/pdf",
    });
    const docUrl = URL.createObjectURL(tempblob);
    setPdfFileData(docUrl);
  }

iframe现在你可以很容易地在一个renderPdf() 函数中渲染这个docUrl返回。

完整的代码示例

我在本教程中使用Next.js。如果你使用的是其他框架或vanilla JavaScript,结果也会类似。下面是这个项目的所有代码:

import { useState } from "react";
import { PDFDocument } from "pdf-lib";

export default function Home() {
  const [pdfFileData, setPdfFileData] = useState();

  function readFileAsync(file) {
    return new Promise((resolve, reject) => {
      let reader = new FileReader();
      reader.onload = () => {
        resolve(reader.result);
      };
      reader.onerror = reject;
      reader.readAsArrayBuffer(file);
    });
  }

  function renderPdf(uint8array) {
    const tempblob = new Blob([uint8array], {
      type: "application/pdf",
    });
    const docUrl = URL.createObjectURL(tempblob);
    setPdfFileData(docUrl);
  }

  function range(start, end) {
    let length = end - start + 1;
    return Array.from({ length }, (_, i) => start + i - 1);
  }

  async function extractPdfPage(arrayBuff) {
    const pdfSrcDoc = await PDFDocument.load(arrayBuff);
    const pdfNewDoc = await PDFDocument.create();
    const pages = await pdfNewDoc.copyPages(pdfSrcDoc, range(2, 3));
    pages.forEach((page) => pdfNewDoc.addPage(page));
    const newpdf = await pdfNewDoc.save();
    return newpdf;
  }

  // Execute when user select a file
  const onFileSelected = async (e) => {
    const fileList = e.target.files;
    if (fileList?.length > 0) {
      const pdfArrayBuffer = await readFileAsync(fileList[0]);
      const newPdfDoc = await extractPdfPage(pdfArrayBuffer);
      renderPdf(newPdfDoc);
    }
  };

  return (
    <>
      <h1>Hello world</h1>
      <input
        type="file"
        id="file-selector"
        accept=".pdf"
        onChange={onFileSelected}
      />
      <iframe
        style={{ display: "block", width: "100vw", height: "90vh" }}
        title="PdfFrame"
        src={pdfFileData}
        frameborder="0"
        type="application/pdf"
      ></iframe>
    </>
  );
}

现在你可以使用PDF浏览器上的下载按钮保存生成的PDF。

今后的发展方向

在这篇文章中,我只是触及了冰山一角。如果你想处理PDF,并想从中获得一些东西,那么pdf-lib是一个非常强大的库,可以达到这个目的。

你可以将两个PDF合并成一个,你可以旋转页面,或者从一个PDF中删除一些页面。这些只是一些例子,可能性是无穷的。

如果你想将你的Next.js应用程序部署到Cloudflare页面,这是你应该查看的文章

做点什么出来。做一些创造性的东西,并在Twitter上向我展示。

结论

如果你读到现在,我非常感激。感觉我做的内容,世界上另一个地方的人都会读到。请与你的编码朋友分享。

你想在你的PDF文档中添加一个大纲吗?我知道这是一个非常难实现的任务。我已经经历了很多痛苦,用JavaScript在PDF文档中添加这个功能。你有兴趣吗? 那是未来的一个故事。

祝你有个愉快的一天。