LibPDF

For Library Authors

Best practices for building libraries and applications on top of LibPDF.

For Library Authors

Building a library or application on top of LibPDF? This guide covers integration patterns, best practices, and common pitfalls.

Dependency Strategy

Choose the right dependency type based on your use case.

If you're building a wrapper or extension library, use a peer dependency:

{
  "name": "my-pdf-wrapper",
  "peerDependencies": {
    "@libpdf/core": "^1.0.0"
  },
  "devDependencies": {
    "@libpdf/core": "^1.0.0"
  }
}

Benefits:

  • Users control which version they use
  • Avoids duplicate copies in the bundle
  • Version conflicts are caught at install time

Direct Dependency (For Internal Use)

If LibPDF is an implementation detail not exposed to users:

{
  "name": "my-pdf-service",
  "dependencies": {
    "@libpdf/core": "^1.0.0"
  }
}

Benefits:

  • You control the exact version
  • Simpler for end users
  • Good for applications (not libraries)

Type Preservation

When wrapping LibPDF types, preserve type information.

Do: Use Generics

import { PDF, PDFPage } from "@libpdf/core";

// Preserves the exact type
function processPages<T extends PDFPage>(pages: T[]): T[] {
  return pages.filter(p => p.width > 100);
}

// Works with subclasses too
async function loadAndProcess(bytes: Uint8Array): Promise<PDF> {
  const pdf = await PDF.load(bytes);
  // Type is preserved through the call chain
  return pdf;
}

Don't: Lose Type Information

// Bad: Returns base type, loses subclass info
function processPdf(pdf: PDF): PDF {
  return pdf;
}

// Bad: Upcasts to unknown
function getPage(pdf: PDF): unknown {
  return pdf.getPage(0);
}

Error Handling Patterns

Re-export Errors

Let users catch specific error types:

// my-library/index.ts
export { 
  SecurityError, 
  PermissionDeniedError,
  SignatureError,
  SignerError 
} from "@libpdf/core";

// Or re-export everything
export * from "@libpdf/core";

Wrap with Context

Add context when wrapping errors:

import { PDF, SecurityError } from "@libpdf/core";

export class MyLibraryError extends Error {
  cause?: Error;
  
  constructor(message: string, cause?: Error) {
    super(message);
    this.cause = cause;
  }
}

export async function processSecureDocument(bytes: Uint8Array) {
  try {
    const pdf = await PDF.load(bytes);
    // ... process
  } catch (error) {
    if (error instanceof SecurityError) {
      throw new MyLibraryError(
        `Failed to process secure document: ${error.message}`,
        error
      );
    }
    throw error;
  }
}

Preserve Error Types

Don't swallow important error information:

// Bad: loses error type
try {
  await pdf.sign({ signer });
} catch (error) {
  throw new Error("Signing failed");
}

// Good: preserves error type
try {
  await pdf.sign({ signer });
} catch (error) {
  if (error instanceof SignerError) {
    // Handle signer-specific issues
  }
  throw error; // Re-throw unknown errors
}

Async Patterns

All I/O is Async

LibPDF uses async/await for all I/O operations:

// Loading is async
const pdf = await PDF.load(bytes);

// Page access is async
const page = await pdf.getPage(0);

// Text extraction is async
const text = await page.extractText();

// Saving is async
const savedBytes = await pdf.save();

Batch Operations

For performance, batch related operations:

// Inefficient: sequential page loading
const pages: PDFPage[] = [];
for (let i = 0; i < pdf.getPageCount(); i++) {
  pages.push(await pdf.getPage(i));
}

// Better: parallel loading
const pagePromises = [];
for (let i = 0; i < pdf.getPageCount(); i++) {
  pagePromises.push(pdf.getPage(i));
}
const pages = await Promise.all(pagePromises);

// Best: use getPages() which is optimized
const pages = await pdf.getPages();

Streaming Considerations

For large files, consider memory usage:

// Process pages one at a time for large documents
async function extractAllText(pdf: PDF): Promise<string[]> {
  const results: string[] = [];
  
  for (let i = 0; i < pdf.getPageCount(); i++) {
    const page = await pdf.getPage(i);
    if (page) {
      const text = await page.extractText();
      results.push(text.text);
    }
    // Page can be garbage collected after processing
  }
  
  return results;
}

Low-Level Access

For advanced features, access internal APIs.

Object Registry

Access the raw object storage:

import { PDF, PdfRef, PdfDict } from "@libpdf/core";

const pdf = await PDF.load(bytes);

// Get the registry
const registry = pdf.context.registry;

// Resolve any reference
const obj = await registry.resolve(PdfRef.of(5, 0));

// Register new objects
const newDict = PdfDict.of({ Type: PdfName.of("Custom") });
const ref = registry.register(newDict);

Document Context

Access document-wide state:

const ctx = pdf.context;

// Access the catalog
const catalog = ctx.catalog.getDict();

// Get trailer info
const trailer = ctx.info.trailer;

// Check encryption state
const isEncrypted = ctx.info.isEncrypted;

Page Tree

Manipulate the page structure:

// Get page references
const pageRefs = pdf.context.pages.getPages();

// Direct page manipulation
pdf.context.pages.insertPage(0, newPageRef, newPageDict);
pdf.context.pages.removePage(index);

Warning: Low-level APIs may change between minor versions. Use high-level APIs when possible.

Testing Your Integration

Mock PDF Data

Create test PDFs programmatically:

import { PDF } from "@libpdf/core";

function createTestPdf(): Promise<Uint8Array> {
  const pdf = PDF.create();
  const page = pdf.addPage();
  page.drawText("Test content", { x: 50, y: 700, size: 12 });
  return pdf.save();
}

describe("MyPdfWrapper", () => {
  it("processes PDF correctly", async () => {
    const bytes = await createTestPdf();
    const result = await myWrapper.process(bytes);
    expect(result).toBeDefined();
  });
});

Test with Real PDFs

Use a fixtures directory:

import { readFile } from "fs/promises";
import { join } from "path";

async function loadFixture(name: string): Promise<Uint8Array> {
  const path = join(__dirname, "fixtures", name);
  return new Uint8Array(await readFile(path));
}

describe("Complex PDFs", () => {
  it("handles encrypted PDF", async () => {
    const bytes = await loadFixture("encrypted.pdf");
    const result = await myWrapper.process(bytes, { password: "test" });
    expect(result).toBeDefined();
  });
});

Test Error Conditions

import { SecurityError } from "@libpdf/core";

describe("Error handling", () => {
  it("handles missing password", async () => {
    const encryptedBytes = await loadFixture("encrypted.pdf");
    
    // Documents requiring a password throw SecurityError when no password provided
    await expect(myWrapper.process(encryptedBytes))
      .rejects
      .toThrow(SecurityError);
  });
  
  it("handles wrong password", async () => {
    const encryptedBytes = await loadFixture("encrypted.pdf");
    
    await expect(myWrapper.process(encryptedBytes, { password: "wrong" }))
      .rejects
      .toThrow(SecurityError);
  });
});

Bundle Size Considerations

Tree Shaking

LibPDF is ESM-only and tree-shakeable:

// Only imports what you use
import { PDF } from "@libpdf/core";

// Signature features not included if not imported
import { P12Signer } from "@libpdf/core";

Optional Features

Some features have larger dependencies:

FeatureImpact
Basic parsingMinimal
Text extractionModerate (font parsing)
Digital signaturesLarger (crypto, ASN.1)
Font embeddingModerate (font parsing)

Structure your code to allow tree-shaking:

// Good: conditional import
async function signIfNeeded(pdf: PDF, shouldSign: boolean) {
  if (shouldSign) {
    const { P12Signer } = await import("@libpdf/core");
    // ...
  }
}

Version Compatibility

Semantic Versioning

LibPDF follows semver:

  • Patch (1.0.x): Bug fixes, no API changes
  • Minor (1.x.0): New features, backwards compatible
  • Major (x.0.0): Breaking changes

Handling Updates

// Check version at runtime if needed
import { version } from "@libpdf/core";

if (version.startsWith("1.")) {
  // Use v1 API
} else {
  // Use v2 API
}

Changelog Monitoring

Subscribe to releases:

  • Watch the GitHub repository
  • Check the changelog before updating
  • Test your integration after updates

Example: Building a PDF Redaction Library

Here's a complete example of building on LibPDF:

// pdf-redactor/index.ts
import { PDF, PDFPage, rgb, type Color } from "@libpdf/core";

export interface RedactionOptions {
  color?: Color;
}

export interface TextRedaction {
  page: number;
  text: string;
}

export async function redactText(
  bytes: Uint8Array,
  redactions: TextRedaction[],
  options: RedactionOptions = {}
): Promise<Uint8Array> {
  const pdf = await PDF.load(bytes);
  const color = options.color ?? rgb(0, 0, 0);
  
  // Group redactions by page
  const byPage = new Map<number, string[]>();
  for (const r of redactions) {
    const texts = byPage.get(r.page) ?? [];
    texts.push(r.text);
    byPage.set(r.page, texts);
  }
  
  // Process each page with redactions
  for (const [pageIndex, texts] of byPage) {
    const page = await pdf.getPage(pageIndex);
    if (!page) continue;
    
    for (const text of texts) {
      const matches = await page.findText(text);
      for (const match of matches) {
        // Draw a filled rectangle over each match
        page.drawRectangle({
          x: match.bbox.x,
          y: match.bbox.y,
          width: match.bbox.width,
          height: match.bbox.height,
          color,
        });
      }
    }
  }
  
  return pdf.save();
}

Summary

TopicRecommendation
DependenciesPeer dependency for libraries, direct for apps
TypesPreserve generics, don't upcast
ErrorsRe-export error types, add context
AsyncBatch operations, consider memory
Low-levelUse sparingly, document version requirements
TestingTest with real PDFs, cover error cases
BundlesImport only what you need

On this page