PDF Toolkit

GridStorm's PDF toolkit provides a headless PDF engine with a pluggable parser, a DOM-based renderer, and a rich plugin ecosystem for text extraction, form filling, document intelligence, and PII detection. The architecture mirrors the grid core -- commands for mutation, events for observation, and plugins for extension.

Installation

Install the packages you need. The core engine is required; the renderer and plugins are optional.

# Core engine (required)
pnpm add @gridstorm/pdf-core

# DOM renderer (optional -- for visual PDF viewing)
pnpm add @gridstorm/pdf-renderer

# Plugins (install only what you need)
pnpm add @gridstorm/pdf-plugin-text
pnpm add @gridstorm/pdf-plugin-form-fill
pnpm add @gridstorm/pdf-plugin-intelligence
pnpm add @gridstorm/pdf-plugin-pii

:::example{title="PDF Viewer Demo" href="/pdf-viewer/"} Interactive PDF viewer with canvas rendering, text search, annotations, and theme switching. Built on @gridstorm/pdf-core. :::

Architecture

The PDF toolkit follows the same headless architecture as the grid:

PdfParser (pluggable) ──► PdfEngine ──► Store + EventBus + CommandBus
                              │
                         PdfPluginManager
                              │
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
         Text Plugin    Form Fill Plugin   PII Plugin
              │
              ▼
         PdfRenderer (DOM)

The PdfParser interface allows you to bring your own PDF parsing backend (such as pdf.js). The engine manages document state, annotations, navigation, zoom, and undo/redo. Plugins extend functionality through the same command/event pattern used by the grid.

PdfParser Interface

The parser is a pluggable backend that handles raw PDF byte parsing. You must provide a parser implementation to load real PDF files.

interface PdfParser {
  loadDocument(source: ArrayBuffer | Uint8Array): Promise<ParsedDocument>;
  getPageText(pageIndex: number): Promise<PageTextContent>;
  getPageInfo(pageIndex: number): PageInfo;
  getPageCount(): number;
  destroy(): void;
}
Method Returns Description
loadDocument Promise<ParsedDocument> Parse raw PDF bytes into page metadata
getPageText Promise<PageTextContent> Extract text items from a specific page
getPageInfo PageInfo Get page dimensions and rotation
getPageCount number Total number of pages
destroy void Release parser resources

PdfEngine API

Create an engine instance with createPdfEngine. The returned object exposes the public API, store, event bus, command bus, and plugin manager.

import { createPdfEngine } from '@gridstorm/pdf-core';
import { createTextPlugin } from '@gridstorm/pdf-plugin-text';

const engine = createPdfEngine({
  initialZoom: 1.5,
  initialPage: 0,
  initialToolMode: 'select',
  maxHistorySize: 100,
  parser: myPdfJsParser, // your PdfParser implementation
  plugins: [createTextPlugin()],
});

Configuration

Option Type Default Description
container HTMLElement | string -- Container element or CSS selector
initialZoom number 1.0 Starting zoom level
initialPage number 0 Starting page index (0-based)
initialToolMode string 'select' Starting tool mode
maxHistorySize number 50 Maximum undo history entries
plugins PdfPlugin[] [] Plugins to install
enableTextLayer boolean true Enable text selection layer
enableAnnotationLayer boolean true Enable annotation overlay
parser PdfParser -- Pluggable PDF parsing backend

Public API Methods

Method Signature Description
loadDocument (source: ArrayBuffer | Uint8Array | string) => Promise<void> Load a PDF document
saveDocument () => Promise<Blob> Save the current document as a Blob
closeDocument () => void Close the current document and reset state
goToPage (pageIndex: number) => void Navigate to a specific page
getCurrentPage () => number Get the active page index
getPageCount () => number Get total page count
setZoom (zoom: number) => void Set zoom level (0.1 to 10)
getZoom () => number Get current zoom level
setToolMode (mode: string) => void Set the active tool mode
getToolMode () => string Get current tool mode
getAnnotations (pageIndex?: number) => PdfAnnotation[] Get annotations, optionally filtered by page
getAnnotation (id: string) => PdfAnnotation | undefined Get a single annotation by ID
undo () => void Undo the last undoable command
redo () => void Redo the last undone command
canUndo () => boolean Check if undo is available
canRedo () => boolean Check if redo is available
getState () => PdfDocumentState Get the full document state
addEventListener (event, listener) => () => void Subscribe to an event; returns unsubscribe function
getPluginApi (pluginId: string) => T | undefined Retrieve a plugin's public API
destroy () => void Destroy the engine and release resources

Tool Modes

The engine supports these tool modes for user interaction:

select, hand, text-select, annotation-highlight, annotation-underline, annotation-strikethrough, annotation-squiggle, annotation-circle, annotation-rectangle, annotation-polygon, annotation-ink, annotation-text, annotation-freetext, annotation-stamp, annotation-line, annotation-redaction, signature

PDF Renderer

The PdfRenderer class creates a scrollable, zoomable PDF viewer with text and annotation layers.

import { PdfRenderer } from '@gridstorm/pdf-renderer';

const renderer = new PdfRenderer({
  api: engine.api,
  container: '#pdf-viewer',
  enableToolbar: true,
  enableTextLayer: true,
  enableAnnotationLayer: true,
  devicePixelRatio: window.devicePixelRatio,
});

renderer.mount();

Renderer Configuration

Option Type Default Description
api PdfApi required PDF API instance from createPdfEngine
container HTMLElement | string required Container element or CSS selector
classPrefix string 'gs-pdf' CSS class prefix
devicePixelRatio number window.devicePixelRatio Pixel ratio for canvas rendering
enableToolbar boolean true Show the built-in toolbar
enableTextLayer boolean true Enable text selection layer
enableAnnotationLayer boolean true Enable annotation overlay
extensions PdfRendererExtension[] [] Additional renderer extensions

The renderer only renders pages that are currently visible in the viewport, recycling DOM nodes as you scroll -- the same virtual scrolling approach used by the grid.

Text Extraction Plugin

The text plugin provides text extraction and full-text search across all pages.

import { createTextPlugin } from '@gridstorm/pdf-plugin-text';

const engine = createPdfEngine({
  parser: myParser,
  plugins: [createTextPlugin()],
});

// After loading a document:
await engine.api.loadDocument(pdfBytes);

// Extract text from page 0
engine.commandBus.dispatch('text:extract', { pageIndex: 0 });

// Search across all extracted pages
engine.commandBus.dispatch('text:search', {
  query: 'invoice',
  options: { caseSensitive: false, wholeWord: true },
});

// Navigate through matches
engine.commandBus.dispatch('text:searchNext', {});
engine.commandBus.dispatch('text:searchPrev', {});

Text Commands

Command Payload Description
text:extract { pageIndex: number } Extract text from a single page
text:extractAll {} Extract text from all pages
text:search { query, caseSensitive?, wholeWord?, regex? } Search for text across pages
text:searchNext {} Navigate to the next search match
text:searchPrev {} Navigate to the previous search match
text:clearSearch {} Clear all search results

Form Fill Plugin

The form fill plugin detects form fields in PDFs and fills them with data. It depends on the text plugin.

import { createTextPlugin } from '@gridstorm/pdf-plugin-text';
import { createFormFillPlugin } from '@gridstorm/pdf-plugin-form-fill';

const engine = createPdfEngine({
  parser: myParser,
  plugins: [
    createTextPlugin(),
    createFormFillPlugin({ autoDetect: true }),
  ],
});

await engine.api.loadDocument(pdfBytes);

// Manually detect fields (or use autoDetect on text extraction)
engine.commandBus.dispatch('form:detectFields', { pageIndex: 0 });

// Fill detected fields with data
engine.commandBus.dispatch('form:fill', {
  data: {
    'Full Name': 'Jane Smith',
    'Email': 'jane@example.com',
    'Phone': '555-0123',
  },
});

// Validate filled values
engine.commandBus.dispatch('form:validate', {});

Form Fill Configuration

Option Type Default Description
autoDetect boolean false Automatically detect fields when text is extracted
validationRules Record<FieldType, RegExp> -- Custom validation patterns per field type

Field Types

text, date, email, phone, address, name, number, checkbox, signature, custom

Form Commands

Command Payload Description
form:detectFields { pageIndex?: number } Detect form fields on a page or all pages
form:fill { data: Record<string, string> } Fill detected fields with key-value data
form:validate {} Validate all field values
form:clear {} Clear all filled values

Intelligence Plugin

The intelligence plugin provides document classification, field extraction, summarization, and table detection. It depends on the text plugin.

import { createTextPlugin } from '@gridstorm/pdf-plugin-text';
import { createIntelligencePlugin } from '@gridstorm/pdf-plugin-intelligence';

const engine = createPdfEngine({
  parser: myParser,
  plugins: [
    createTextPlugin(),
    createIntelligencePlugin(),
  ],
});

await engine.api.loadDocument(pdfBytes);

// Classify the document type
engine.commandBus.dispatch('intel:classify', { topN: 3 });

// Extract structured fields
engine.commandBus.dispatch('intel:extract', {
  fields: ['Invoice Number', 'Total Amount', 'Due Date'],
});

// Generate a summary
engine.commandBus.dispatch('intel:summarize', { maxLength: 500 });

// Detect tables
engine.commandBus.dispatch('intel:detectTables', {});

Document Classes

invoice, contract, receipt, letter, report, form, legal, medical, financial, unknown

Intelligence Commands

Command Payload Description
intel:classify { topN?: number } Classify document type (default top 3)
intel:extract { fields?: string[] } Extract named fields from text
intel:summarize { maxLength?: number } Generate a document summary
intel:detectTables {} Detect tabular data across all pages

PII Detection Plugin

The PII plugin scans extracted text for personally identifiable information and can auto-create redaction annotations. It depends on the text plugin.

import { createTextPlugin } from '@gridstorm/pdf-plugin-text';
import { createPiiPlugin } from '@gridstorm/pdf-plugin-pii';

const engine = createPdfEngine({
  parser: myParser,
  plugins: [
    createTextPlugin(),
    createPiiPlugin({
      confidenceThreshold: 0.8,
      autoScan: true, // scan pages automatically after text extraction
      enabledTypes: ['email', 'ssn', 'phone', 'credit-card'],
    }),
  ],
});

await engine.api.loadDocument(pdfBytes);

// Manually scan a specific page
engine.commandBus.dispatch('pii:scan', { pageIndex: 0 });

// Scan all pages
engine.commandBus.dispatch('pii:scanAll', {});

// Auto-redact all detected PII
engine.commandBus.dispatch('pii:autoRedact', {
  types: ['ssn', 'credit-card'],
});

PII Configuration

Option Type Default Description
enabledTypes PiiType[] all types PII types to detect
confidenceThreshold number 0.7 Minimum confidence score (0-1)
customPatterns CustomPattern[] [] Custom regex patterns for detection
autoScan boolean false Scan automatically after text extraction

PII Types

email, phone, ssn, credit-card, date-of-birth, address, name, passport, ip-address, custom

PII Commands

Command Payload Description
pii:scan { pageIndex: number } Scan a single page for PII
pii:scanAll {} Scan all pages
pii:autoRedact { types?: string[] } Create redaction annotations for matches
pii:configure Partial<PiiConfig> Update PII configuration at runtime

Commands Reference

All core PDF commands available on the command bus:

Command Payload Description
page:goTo { pageIndex } Navigate to page
zoom:set { zoom } Set zoom level (0.1 - 10)
zoom:fitWidth { containerWidth? } Fit page width to container
zoom:fitPage { containerWidth?, containerHeight? } Fit full page to container
scroll:to { x, y } Set scroll position
tool:set { mode } Set tool mode
annotation:create { annotation } Create a new annotation
annotation:update { annotationId, changes } Update annotation properties
annotation:delete { annotationId } Delete an annotation
annotation:select { annotationIds } Select annotations
annotation:deselect {} Deselect all annotations
annotation:move { annotationId, deltaX, deltaY } Move an annotation
annotation:resize { annotationId, newRect } Resize an annotation
redaction:mark { pageIndex, rect, overlayText? } Mark a region for redaction
redaction:apply { annotationIds } Apply specific redactions
redaction:applyAll {} Apply all pending redactions
history:clear {} Clear undo/redo history

Events Reference

Event Payload Description
document:loaded { pageCount, metadata } Document loaded successfully
document:closed {} Document closed
document:saved { blob } Document saved
document:error { error, source } An error occurred
page:changed { pageIndex } Active page changed
page:rendered { pageIndex } Page rendered to canvas
zoom:changed { zoom } Zoom level changed
scroll:changed { x, y } Scroll position changed
tool:changed { mode } Tool mode changed
annotation:created { annotation } Annotation created
annotation:updated { annotation, changes } Annotation updated
annotation:deleted { annotationId } Annotation deleted
annotation:selected { annotationIds } Annotations selected
text:extracted { pageIndex, textContent } Text extracted from page
search:found { query, matches, total } Search results found
history:changed { canUndo, canRedo } History state changed

Full Working Example

import { createPdfEngine } from '@gridstorm/pdf-core';
import { PdfRenderer } from '@gridstorm/pdf-renderer';
import { createTextPlugin } from '@gridstorm/pdf-plugin-text';
import { createFormFillPlugin } from '@gridstorm/pdf-plugin-form-fill';
import { createIntelligencePlugin } from '@gridstorm/pdf-plugin-intelligence';
import { createPiiPlugin } from '@gridstorm/pdf-plugin-pii';

// 1. Create the engine with plugins
const engine = createPdfEngine({
  parser: myPdfJsParser,
  initialZoom: 1.0,
  maxHistorySize: 100,
  plugins: [
    createTextPlugin(),
    createFormFillPlugin({ autoDetect: true }),
    createIntelligencePlugin(),
    createPiiPlugin({
      confidenceThreshold: 0.8,
      autoScan: true,
    }),
  ],
});

// 2. Mount the renderer
const renderer = new PdfRenderer({
  api: engine.api,
  container: '#pdf-viewer',
  enableToolbar: true,
});
renderer.mount();

// 3. Load a document
const response = await fetch('/documents/sample.pdf');
const bytes = await response.arrayBuffer();
await engine.api.loadDocument(bytes);

// 4. Listen for events
engine.api.addEventListener('document:loaded', ({ pageCount }) => {
  console.log(`Loaded ${pageCount} pages`);
});

engine.api.addEventListener('pii:detected', (event) => {
  console.log(`Found ${event.total} PII matches`);
});

// 5. Interact with the document
engine.api.setZoom(1.5);
engine.api.goToPage(2);

engine.commandBus.dispatch('text:search', {
  query: 'confidential',
});

// 6. Save the modified document
const blob = await engine.api.saveDocument();
const url = URL.createObjectURL(blob);

// 7. Cleanup
renderer.destroy();
engine.destroy();

Next Steps