How to build a Node.js PDF editor with pdf-lib
Table of contents
Build a Node.js PDF editor with pdf-lib to read and write PDFs, add text and images, and copy pages between documents. This guide also covers Nutrient API for teams that need OCR, document conversion, or compliance features.
In this tutorial, you’ll use the pdf-lib(opens in a new tab) library in Node.js to edit PDFs, including reading and writing files, adding text and images, and copying pages between documents.
The second part covers Nutrient API, which provides similar operations plus OCR, document conversion, and other tools through HTTP requests.
Comparing Nutrient API and pdf-lib
Both Nutrient API and pdf-lib handle PDF editing in Node.js, but they differ in scope, features, and deployment. Here’s how they compare.
pdf-lib
pdf-lib is an open source library for basic PDF operations:
- Add text, embed images, and copy pages between documents
- Runs in browsers, Node.js, Deno, and React Native
- MIT license, free to use
- No native dependencies
pdf-lib doesn’t include OCR, document conversion, digital signatures, or form creation. You’ll need to handle PDF spec edge cases yourself, and there are no compliance certifications for regulated industries.
Nutrient API
Nutrient API is a hosted service with more than 30 document processing tools:
- Merge, split, OCR, convert Office files, add watermarks, flatten annotations
- SOC 2-compliant with encrypted connections
- No document storage — files are processed and discarded
- Pricing based on documents processed
Nutrient API requires a subscription after the free tier (100 documents/month). It’s a good fit when you need features pdf-lib doesn’t offer or want to avoid maintaining PDF processing code.
Prerequisites for building a pdf-lib and Node.js PDF editor
Before you dive into building your Node.js PDF editor, ensure you have the following prerequisites:
- Basic knowledge of JavaScript and Node.js.
- Node.js(opens in a new tab) installed on your system.
- A text editor of your choice.
- A package manager for installing packages. You can use npm(opens in a new tab) or Yarn(opens in a new tab).
Setting up the pdf-lib project
You’ll begin by setting up a new Node.js project. Open your terminal and follow the steps outlined below.
Create a new directory for your project:
Terminal window mkdir pdf-editorcd pdf-editorInitialize a new Node.js project and install pdf-lib and
axios(opens in a new tab):Terminal window npm init -ynpm install pdf-lib axiosCreate a new JavaScript file, e.g.
index.js, and open it in your preferred code editor.Import the necessary dependencies:
const { PDFDocument, rgb, StandardFonts, degrees } = require("pdf-lib");const fs = require("fs");const axios = require("axios");
With the project set up, you can now implement the PDF editing functionality.
Reading and writing PDF files with pdf-lib
Before you can edit a PDF, you need the ability to read existing PDF files and write modified PDFs to disk. You’ll create two functions — readPDF and writePDF — to handle these tasks:
async function readPDF(path) { const file = await fs.promises.readFile(path); return await PDFDocument.load(file);}
async function writePDF(pdf, path) { const pdfBytes = await pdf.save(); await fs.promises.writeFile(path, pdfBytes); console.log("PDF saved successfully!");}The readPDF function loads a PDF document from a file path and returns a PDFDocument object. Meanwhile, the writePDF function takes a PDFDocument and a file path as parameters, saves the modified PDF, and writes it to the specified path.
To demonstrate the use, you’ll load an existing PDF document and save it as a new file:
(async () => { // Load an existing PDF. const pdf = await readPDF("input.pdf");
// Access document information. const pageCount = pdf.getPageCount(); console.log(`PDF has ${pageCount} pages`);
// Save the PDF to a new file. await writePDF(pdf, "output.pdf");})();Modifying PDFs with pdf-lib: Adding text and images
The pdf-lib library allows you to modify existing PDF documents by adding various elements such as text, images, and more. In this section, you’ll see an example of how to add text to a PDF document using pdf-lib.
Adding text to a PDF
To add text to a PDF, use pdf-lib library’s PDFDocument(opens in a new tab) API:
async function modifyPdf() { const filePath = "input.pdf"; const existingPdfBytes = fs.readFileSync(filePath);
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();
fs.writeFileSync("modified.pdf", pdfBytes); console.log("Modified PDF saved successfully!");}
// Usage.modifyPdf();In this example, you start by loading an existing PDF document from a local file (input.pdf). You then embed the Helvetica font using pdfDoc.embedFont to ensure it’s available for use. Next, you access the first page of the PDF using pdfDoc.getPages()[0] and obtain its width and height. Using the drawText method, you add a text annotation to the page, specifying the position, font, size, color, and rotation.
Once the modifications are complete, you save the modified PDF as modified.pdf using pdfDoc.save(), and a success message is logged to the console.
Embedding images into PDFs
In addition to text, you can also add images to a PDF using 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 jpgResponse = await axios.get(jpgUrl, { responseType: "arraybuffer", }); const pngResponse = await axios.get(pngUrl, { responseType: "arraybuffer", });
const jpgImageBytes = jpgResponse.data; const pngImageBytes = pngResponse.data;
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(); fs.writeFileSync("output3.pdf", pdfBytes); console.log("PDF saved successfully!");}
embedImages();In this example, you use axios to fetch two image files: a JPEG image (cat_riding_unicorn.jpg), and a PNG image (minions_banana_alpha.png). You create a new PDF document and embed the JPEG and PNG images into it. The images are scaled down by 50 percent and placed on a new page. The modified PDF is saved as output3.pdf.
Copying pages from multiple PDFs with pdf-lib
Another powerful feature of the pdf-lib library is the ability to extract pages from different PDF documents and combine them into a new document:
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 firstDonorPdfResponse = await axios.get(url1, { responseType: "arraybuffer", }); const secondDonorPdfResponse = await axios.get(url2, { responseType: "arraybuffer", });
const firstDonorPdfBytes = firstDonorPdfResponse.data; const secondDonorPdfBytes = secondDonorPdfResponse.data;
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();
fs.writeFileSync("output2.pdf", pdfBytes); console.log("PDF saved successfully!");}
copyPages();In this example, you fetch two PDF documents (with_update_sections.pdf and with_large_page_count.pdf) from remote URLs using axios. You load the PDF data using the PDFDocument.load method from pdf-lib to create PDFDocument objects for each donor PDF. Then, using the copyPages method, you combine specific pages from the donor PDFs into a new PDF. The modified PDF is saved as output2.pdf.
Integrating with Nutrient API
Nutrient API is an HTTP API that provides powerful features for manipulating PDF files. With the PDF Editor API, you can perform various actions, such as merging, splitting, deleting, flattening, and duplicating PDF documents. Here are some key functionalities:
- Merge — Combine multiple PDF files into a single document using the merge PDF API. This allows you to merge large files efficiently.
- Split — Select specific pages from an existing PDF file and create a new document using the split PDF API.
- Delete — Remove one or multiple pages from a PDF using the PDF page deletion API.
- Flatten — Flatten annotations in single or multiple PDF files with the flatten PDF API, ensuring a simplified viewing experience.
- Duplicate — Create a copy of a page within a PDF document using the duplicate PDF page API.
Requirements
To get started, you’ll need:
- A Nutrient API key(opens in a new tab).
- Node.js(opens in a new tab).
- A package manager for installing packages. You can use npm(opens in a new tab) or Yarn(opens in a new tab).
You also need to install the following dependencies for all the examples:
axios(opens in a new tab) — This package is used for making REST API calls.- Form-Data(opens in a new tab) — This package is used for creating form data.
npm install form-data axiosTo access your Nutrient API key, sign up for a free account(opens in a new tab). Your account lets you generate 100 documents for free every month. Once you’ve signed up, you can find your API key in the Dashboard > API Keys section(opens in a new tab).
Merging PDFs using Nutrient API
In this example, learn how to merge multiple PDF files using the Merge PDF API.
- Create a new folder named
merge_pdfand open it in a code editor. - Create a new file named
processor.jsin the root of the folder. Copy the following code and paste it into the file:
const axios = require("axios");const FormData = require("form-data");const fs = require("fs");
const formData = new FormData();formData.append( "instructions", JSON.stringify({ parts: [ { file: "first_half", }, { file: "second_half", }, ], }),);formData.append("first_half", fs.createReadStream("first_half.pdf"));formData.append("second_half", fs.createReadStream("second_half.pdf"));
(async () => { try { const response = await axios.post( "https://api.nutrient.io/build", formData, { headers: formData.getHeaders({ Authorization: "Bearer YOUR_API_KEY", // Replace `YOUR_API_KEY` with your API key. }), responseType: "stream", }, );
response.data.pipe(fs.createWriteStream("result.pdf")); } catch (e) { const errorString = await streamToString(e.response.data); console.log(errorString); }})();
function streamToString(stream) { const chunks = []; return new Promise((resolve, reject) => { stream.on("data", (chunk) => chunks.push(Buffer.from(chunk))); stream.on("error", (err) => reject(err)); stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); });}Replace
YOUR_API_KEYwith your API key.
Add your PDF files to the merge_pdf folder. In this example, you’re using first_half.pdf and second_half.pdf as your input files.
This code snippet demonstrates the process of merging two PDF files into a single PDF document. It utilizes the axios library for making the API request, FormData for handling form data, and fs for file system operations. The code creates a new FormData instance, appends the merging instructions and the PDF files to be merged, and then performs an API call to Nutrient API. The resulting merged PDF document is streamed and saved as result.pdf.
Splitting PDFs using Nutrient API
In this example, explore the process of splitting PDF files using the Split PDF API.
- Create a new folder named
split_pdfand open it in a code editor. - Inside the
split_pdffolder, create a new file namedprocessor.js. Copy the following code and paste it into theprocessor.jsfile:
const axios = require("axios");const FormData = require("form-data");const fs = require("fs");
(async () => { const firstHalf = new FormData(); firstHalf.append( "instructions", JSON.stringify({ parts: [ { file: "document", pages: { end: -2, }, }, ], }), ); firstHalf.append("document", fs.createReadStream("input.pdf")); // Add your document here.
const secondHalf = new FormData(); secondHalf.append( "instructions", JSON.stringify({ parts: [ { file: "document", pages: { start: -1, }, }, ], }), ); secondHalf.append("document", fs.createReadStream("input.pdf"));
await executeRequest(firstHalf, "first_half.pdf"); await executeRequest(secondHalf, "second_half.pdf");})();
async function executeRequest(formData, outputFile) { try { const response = await axios.post( "https://api.nutrient.io/build", formData, { headers: formData.getHeaders({ Authorization: "Bearer YOUR_API_KEY_HERE", // Replace with your API key. }), responseType: "stream", }, );
response.data.pipe(fs.createWriteStream(outputFile)); } catch (e) { const errorString = await streamToString(e.response.data); console.log(errorString); }}
function streamToString(stream) { const chunks = []; return new Promise((resolve, reject) => { stream.on("data", (chunk) => chunks.push(Buffer.from(chunk))); stream.on("error", (err) => reject(err)); stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); });}Replace
YOUR_API_KEY_HEREwith your API key.
Add the PDF file you want to split and name it input.pdf. Ensure the file is in the same folder as the processor.js file.
The code snippet uses an immediately invoked async function expression (IIFE(opens in a new tab)) to execute the PDF splitting process. It leverages the FormData object to configure splitting instructions and attach the PDF file. With specified page ranges, you can precisely define the parts you need.
The executeRequest function handles the API request to Nutrient API, ensuring a secure and reliable connection. It streams the response containing the split PDF content and saves it to separate output files using fs.createWriteStream.
Deleting pages using Nutrient API
In this example, learn how to delete one or more pages from a PDF file using the delete PDF page API.
- Create a new folder named
delete_pagesand open it in a code editor. - Add the PDF file you want to delete pages from and name it
input.pdf. - Create a new file named
processor.jsand copy the following code into it:
const axios = require("axios");const FormData = require("form-data");const fs = require("fs");
const formData = new FormData();formData.append( "instructions", JSON.stringify({ parts: [ { file: "document", pages: { end: 2, }, }, { file: "document", pages: { start: 4, }, }, ], }),);formData.append("document", fs.createReadStream("input.pdf")); // Add your document here.
(async () => { try { const response = await axios.post( "https://api.nutrient.io/build", formData, { headers: formData.getHeaders({ Authorization: "Bearer YOUR_API_KEY_HERE", // Replace with your API key. }), responseType: "stream", }, );
response.data.pipe(fs.createWriteStream("result.pdf")); } catch (e) { const errorString = await streamToString(e.response.data); console.log(errorString); }})();
function streamToString(stream) { const chunks = []; return new Promise((resolve, reject) => { stream.on("data", (chunk) => chunks.push(Buffer.from(chunk))); stream.on("error", (err) => reject(err)); stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); });}Replace
YOUR_API_KEY_HEREwith your API key.
After importing the necessary packages, a FormData object was created to hold the instructions for the API processing. In this case, the code demonstrates the removal of page number 4, but you can modify it to target a different page. The input document was read using the fs.createReadStream function. The API call was made using axios, and the resulting response was stored in a file named result.pdf.
Flattening PDFs using Nutrient API
Learn how to programmatically flatten PDFs using the flatten PDF API. Streamline your workflow by automating the flattening process, ensuring accurate printing and compatibility with PDF viewers, and protecting form field data.
In this example, you’ll flatten all the annotations present in your provided document, effectively rendering them non-editable. This process guarantees that the annotations become a permanent part of the PDF and can no longer be modified.
- Create a new folder named
flattenand open it in a code editor. - Add the PDF file you want to flatten and name it
input.pdf. - Create a new file named
processor.jsand copy the following code into it:
const axios = require("axios");const FormData = require("form-data");const fs = require("fs");
const formData = new FormData();formData.append( "instructions", JSON.stringify({ parts: [ { file: "document", }, ], actions: [ { type: "flatten", }, ], }),);formData.append("document", fs.createReadStream("input.pdf")); // Add your document here.
(async () => { try { const response = await axios.post( "https://api.nutrient.io/build", formData, { headers: formData.getHeaders({ Authorization: "Bearer YOUR_API_KEY_HERE", // Replace with your API key. }), responseType: "stream", }, );
response.data.pipe(fs.createWriteStream("result.pdf")); } catch (e) { const errorString = await streamToString(e.response.data); console.log(errorString); }})();
function streamToString(stream) { const chunks = []; return new Promise((resolve, reject) => { stream.on("data", (chunk) => chunks.push(Buffer.from(chunk))); stream.on("error", (err) => reject(err)); stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); });}Replace
YOUR_API_KEY_HEREwith your API key.
This code snippet creates a FormData object with instructions to flatten the specified document. The API request is made using axios.post(), and the flattened PDF response is saved as result.pdf using a writable stream.
Duplicating pages using Nutrient API
In this example, you’ll explore how to duplicate specific PDF pages using the duplicate PDF page API provided by Nutrient. By duplicating PDF pages using the duplicate PDF page API, you can perform operations such as watermarking, merging, and preserving backups, allowing for enhanced document workflows with ease and automation.
- Create a new folder named
duplicate_pagesand open it in a code editor. - Add a PDF file you want to duplicate pages from and name it
input.pdf. - Create a new file named
processor.jsand copy the following code into it:
const axios = require("axios");const FormData = require("form-data");const fs = require("fs");
const formData = new FormData();formData.append( "instructions", JSON.stringify({ parts: [ { file: "document", pages: { start: 0, end: 0, }, }, { file: "document", }, { file: "document", pages: { start: -1, end: -1, }, }, ], }),);formData.append("document", fs.createReadStream("input.pdf"));(async () => { try { const response = await axios.post( "https://api.nutrient.io/build", formData, { headers: formData.getHeaders({ Authorization: "Bearer YOUR_API_KEY_HERE", // Replace with your API key. }), responseType: "stream", }, );
response.data.pipe(fs.createWriteStream("result.pdf")); } catch (e) { const errorString = await streamToString(e.response.data); console.log(errorString); }})();
function streamToString(stream) { const chunks = []; return new Promise((resolve, reject) => { stream.on("data", (chunk) => chunks.push(Buffer.from(chunk))); stream.on("error", (err) => reject(err)); stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); });}Replace
YOUR_API_KEY_HEREwith your API key.
In this code example, the first and last pages of a document are duplicated. By providing the specific pages to be duplicated, the code makes a request to Nutrient API. The API returns the duplicated PDF as a response, which is then saved as result.pdf for further use.
Conclusion
This tutorial covered PDF editing in Node.js with pdf-lib — reading and writing files, adding text and images, and copying pages between documents. pdf-lib handles these operations well and works in any JavaScript environment.
If you need OCR, document conversion, digital signatures, or SOC 2 compliance, Nutrient API provides those features through HTTP endpoints. You can create a free account(opens in a new tab) to try it with 100 documents per month.
Related Node.js guides
- Generate PDF invoices with Node.js
- Convert images to PDF in Node.js
- Fill PDF forms in Node.js
- HTML to PDF with Node.js
Other server-side solutions
FAQ
pdf-lib works in both environments. It runs in Node.js, all modern browsers, Deno, and React Native. No native dependencies are required, making it suitable for client-side PDF generation and editing.
pdf-lib can load encrypted PDFs if you provide the password using the password option in PDFDocument.load(). However, it cannot crack or bypass PDF encryption.
pdf-lib supports the 14 standard PDF fonts (like Helvetica, Times Roman, and Courier) without embedding. For custom fonts, you can embed TrueType (.ttf) and OpenType (.otf) fonts using pdfDoc.embedFont().
Nutrient API processes files up to 100 MB per document. For large batch operations, you can make parallel API requests. The API uses streaming responses, so you receive processed files as they complete rather than waiting for all operations to finish.
Use pdf-lib for client-side operations, simple edits, or when you need a free, open source solution. Choose Nutrient API for server-side automation, advanced features like OCR and document conversion, SOC 2 compliance requirements, or when processing high volumes of documents.