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.
Peer Dependency (Recommended for Wrappers)
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:
| Feature | Impact |
|---|---|
| Basic parsing | Minimal |
| Text extraction | Moderate (font parsing) |
| Digital signatures | Larger (crypto, ASN.1) |
| Font embedding | Moderate (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
| Topic | Recommendation |
|---|---|
| Dependencies | Peer dependency for libraries, direct for apps |
| Types | Preserve generics, don't upcast |
| Errors | Re-export error types, add context |
| Async | Batch operations, consider memory |
| Low-level | Use sparingly, document version requirements |
| Testing | Test with real PDFs, cover error cases |
| Bundles | Import only what you need |
