---
name: pdf
description: Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When the agent needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.
license: Proprietary. LICENSE.txt has complete terms
---

# PDF Processing Guide

## Overview

This guide covers essential PDF processing operations using Node.js libraries and command-line tools.
For PDF form filling, use `pdf-lib` which has full form-filling support (see the "Fill PDF Forms" section below).

### ⚠️ 中文支持

PDFKit 和 pdf-lib 默认只内置 Latin 字体，创建包含中文的 PDF 时**必须注册/嵌入中文字体（.ttf）**，否则会乱码。

**中文字体下载地址（Noto Sans CJK SC 思源黑体，开源免费）：**

```
https://dldir1v6.qq.com/invc/tt/QB/Public/ubuntu_qb/NotoSansCJKsc-Regular.ttf
```

> 只支持 `.ttf`/`.otf` 单体字体，**不支持 `.ttc`**。完整中文字体通常 > 5MB，下载后文件太小说明下载失败。

**嵌入方式：**

- **PDFKit**：`doc.registerFont('CJK', '<字体文件路径>')` → `doc.font('CJK')`
- **pdf-lib**：`npm install @pdf-lib/fontkit` → `pdfDoc.registerFontkit(fontkit)` → `pdfDoc.embedFont(fs.readFileSync('<字体文件路径>'))`
- **表单填写**：嵌入字体后调用 `field.updateAppearances(chineseFont)`

## Quick Start

```javascript
import { PDFDocument } from "pdf-lib";
import fs from "fs";

// Read a PDF
const pdfBytes = fs.readFileSync("document.pdf");
const pdfDoc = await PDFDocument.load(pdfBytes);
console.log(`Pages: ${pdfDoc.getPageCount()}`);

// Extract text (use pdf-parse for text extraction)
import pdf from "pdf-parse";
const dataBuffer = fs.readFileSync("document.pdf");
const data = await pdf(dataBuffer);
console.log(data.text);
```

## Node.js Libraries

### pdf-lib — Read, Merge, Split, Edit, Create

> `npm install pdf-lib`

#### Merge PDFs

```javascript
import { PDFDocument } from "pdf-lib";
import fs from "fs";

const mergedPdf = await PDFDocument.create();

for (const file of ["doc1.pdf", "doc2.pdf", "doc3.pdf"]) {
  const pdfBytes = fs.readFileSync(file);
  const pdf = await PDFDocument.load(pdfBytes);
  const pages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
  pages.forEach((page) => mergedPdf.addPage(page));
}

const mergedBytes = await mergedPdf.save();
fs.writeFileSync("merged.pdf", mergedBytes);
```

#### Split PDF

```javascript
import { PDFDocument } from "pdf-lib";
import fs from "fs";

const pdfBytes = fs.readFileSync("input.pdf");
const pdfDoc = await PDFDocument.load(pdfBytes);

for (let i = 0; i < pdfDoc.getPageCount(); i++) {
  const newPdf = await PDFDocument.create();
  const [page] = await newPdf.copyPages(pdfDoc, [i]);
  newPdf.addPage(page);
  const newPdfBytes = await newPdf.save();
  fs.writeFileSync(`page_${i + 1}.pdf`, newPdfBytes);
}
```

#### Extract Metadata

```javascript
import { PDFDocument } from "pdf-lib";
import fs from "fs";

const pdfBytes = fs.readFileSync("document.pdf");
const pdfDoc = await PDFDocument.load(pdfBytes);

console.log(`Title: ${pdfDoc.getTitle()}`);
console.log(`Author: ${pdfDoc.getAuthor()}`);
console.log(`Subject: ${pdfDoc.getSubject()}`);
console.log(`Creator: ${pdfDoc.getCreator()}`);
```

#### Rotate Pages

```javascript
import { PDFDocument, degrees } from "pdf-lib";
import fs from "fs";

const pdfBytes = fs.readFileSync("input.pdf");
const pdfDoc = await PDFDocument.load(pdfBytes);

const page = pdfDoc.getPage(0);
page.setRotation(degrees(90)); // Rotate 90 degrees clockwise

const rotatedBytes = await pdfDoc.save();
fs.writeFileSync("rotated.pdf", rotatedBytes);
```

#### Fill PDF Forms

```javascript
import { PDFDocument } from "pdf-lib";
import fs from "fs";

const pdfBytes = fs.readFileSync("form.pdf");
const pdfDoc = await PDFDocument.load(pdfBytes);
const form = pdfDoc.getForm();

// Fill text fields
form.getTextField("name").setText("John Doe");
form.getTextField("email").setText("john@example.com");

// Check a checkbox
form.getCheckBox("agree").check();

// Select a dropdown option
form.getDropdown("country").select("US");

// Flatten form (make fields non-editable)
form.flatten();

const filledBytes = await pdfDoc.save();
fs.writeFileSync("filled_form.pdf", filledBytes);
```

### pdf-parse — Text Extraction

> `npm install pdf-parse`

#### Extract Text

```javascript
import pdf from "pdf-parse";
import fs from "fs";

const dataBuffer = fs.readFileSync("document.pdf");
const data = await pdf(dataBuffer);

console.log(`Pages: ${data.numpages}`);
console.log(`Text: ${data.text}`);
console.log(`Metadata: ${JSON.stringify(data.info)}`);
```

#### Extract Text from Specific Pages

```javascript
import pdf from "pdf-parse";
import fs from "fs";

const dataBuffer = fs.readFileSync("document.pdf");

// Custom page render to extract specific pages
const options = {
  max: 5, // Extract only first 5 pages
};
const data = await pdf(dataBuffer, options);
console.log(data.text);
```

### PDFKit — Create PDFs from Scratch

> `npm install pdfkit`

#### Basic PDF Creation

```javascript
import PDFDocument from "pdfkit";
import fs from "fs";

const doc = new PDFDocument();
doc.pipe(fs.createWriteStream("hello.pdf"));

// Add text
doc.fontSize(25).text("Hello World!", 100, 100);
doc.fontSize(12).text("This is a PDF created with PDFKit", 100, 130);

// Add a line
doc.moveTo(100, 160).lineTo(400, 160).stroke();

// Save
doc.end();
```

#### Create PDF with Multiple Pages

```javascript
import PDFDocument from "pdfkit";
import fs from "fs";

const doc = new PDFDocument();
doc.pipe(fs.createWriteStream("report.pdf"));

// Page 1
doc.fontSize(24).text("Report Title", { align: "center" });
doc.moveDown();
doc.fontSize(12).text("This is the body of the report. ".repeat(20), {
  align: "justify",
  lineGap: 5,
});

// Page 2
doc.addPage();
doc.fontSize(18).text("Page 2", { underline: true });
doc.moveDown();
doc.fontSize(12).text("Content for page 2");

// Add a table-like structure
doc.moveDown();
const tableTop = doc.y;
doc.text("Name", 50, tableTop, { width: 150, continued: false });
doc.text("Value", 200, tableTop, { width: 150, continued: false });
doc
  .moveTo(50, tableTop + 15)
  .lineTo(350, tableTop + 15)
  .stroke();
doc.text("Item 1", 50, tableTop + 20);
doc.text("100", 200, tableTop + 20);

doc.end();
```

### pdf-table-extractor — Table Extraction

> `npm install pdf-table-extractor`

#### Extract Tables

```javascript
import pdf_table_extractor from "pdf-table-extractor";

pdf_table_extractor(
  "document.pdf",
  (result) => {
    // result.pageTables is an array of pages
    result.pageTables.forEach((page, i) => {
      console.log(`Tables on page ${i + 1}:`);
      page.tables.forEach((table, j) => {
        console.log(`  Table ${j + 1}:`);
        table.forEach((row) => console.log(`    ${row.join(" | ")}`));
      });
    });
  },
  (err) => {
    console.error("Error:", err);
  },
);
```

## Command-Line Tools

### pdftotext (poppler-utils)

```bash
# Extract text
pdftotext input.pdf output.txt

# Extract text preserving layout
pdftotext -layout input.pdf output.txt

# Extract specific pages
pdftotext -f 1 -l 5 input.pdf output.txt  # Pages 1-5
```

### qpdf

```bash
# Merge PDFs
qpdf --empty --pages file1.pdf file2.pdf -- merged.pdf

# Split pages
qpdf input.pdf --pages . 1-5 -- pages1-5.pdf
qpdf input.pdf --pages . 6-10 -- pages6-10.pdf

# Rotate pages
qpdf input.pdf output.pdf --rotate=+90:1  # Rotate page 1 by 90 degrees

# Remove password
qpdf --password=mypassword --decrypt encrypted.pdf decrypted.pdf
```

### pdftk (if available)

```bash
# Merge
pdftk file1.pdf file2.pdf cat output merged.pdf

# Split
pdftk input.pdf burst

# Rotate
pdftk input.pdf rotate 1east output rotated.pdf
```

## Common Tasks

### Extract Text from Scanned PDFs (OCR)

```javascript
import Tesseract from "tesseract.js";
import { execSync } from "child_process";
import fs from "fs";
import path from "path";

// Step 1: Convert PDF pages to images using command-line tools
// Requires: poppler-utils (pdftoppm)
const outputDir = "./pdf_images";
fs.mkdirSync(outputDir, { recursive: true });
execSync(`pdftoppm -png input.pdf ${outputDir}/page`);

// Step 2: OCR each page image
const imageFiles = fs
  .readdirSync(outputDir)
  .filter((f) => f.endsWith(".png"))
  .sort();

let fullText = "";
for (const imageFile of imageFiles) {
  const imagePath = path.join(outputDir, imageFile);
  const {
    data: { text },
  } = await Tesseract.recognize(imagePath, "eng");
  fullText += `${imageFile}:\n${text}\n\n`;
}
console.log(fullText);
```

### Add Watermark

```javascript
import { PDFDocument, rgb, StandardFonts } from "pdf-lib";
import fs from "fs";

const pdfBytes = fs.readFileSync("document.pdf");
const pdfDoc = await PDFDocument.load(pdfBytes);
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);

for (let i = 0; i < pdfDoc.getPageCount(); i++) {
  const page = pdfDoc.getPage(i);
  const { width, height } = page.getSize();

  page.drawText("WATERMARK", {
    x: width / 4,
    y: height / 2,
    size: 60,
    font: helveticaFont,
    color: rgb(0.75, 0.75, 0.75),
    opacity: 0.3,
    rotate: { type: "degrees", angle: 45 },
  });
}

const watermarkedBytes = await pdfDoc.save();
fs.writeFileSync("watermarked.pdf", watermarkedBytes);
```

### Extract Images

```bash
# Using pdfimages (poppler-utils)
pdfimages -j input.pdf output_prefix

# This extracts all images as output_prefix-000.jpg, output_prefix-001.jpg, etc.
```

### Password Protection

```javascript
import { execSync } from "child_process";

// Using qpdf for encryption (most reliable cross-platform approach)
execSync("qpdf --encrypt userpassword ownerpassword 256 -- input.pdf encrypted.pdf");
```

> Note: `pdf-lib` does not natively support encryption. Use `qpdf` command-line tool for password protection.

## Quick Reference

| Task               | Best Tool               | Command/Code                          |
| ------------------ | ----------------------- | ------------------------------------- |
| Merge PDFs         | pdf-lib                 | `mergedPdf.copyPages()` + `addPage()` |
| Split PDFs         | pdf-lib                 | Copy single page to new doc           |
| Extract text       | pdf-parse               | `pdf(dataBuffer)`                     |
| Extract tables     | pdf-table-extractor     | `pdf_table_extractor(file, cb)`       |
| Create PDFs        | PDFKit                  | `new PDFDocument()` + pipe            |
| Fill PDF forms     | pdf-lib                 | `form.getTextField().setText()`       |
| Command line merge | qpdf                    | `qpdf --empty --pages ...`            |
| OCR scanned PDFs   | tesseract.js + pdftoppm | Convert to image first                |
| Add watermark      | pdf-lib                 | `page.drawText()` with opacity        |
| Password protect   | qpdf                    | `qpdf --encrypt ...`                  |
