Skip to main content

PDF Extractor Plugin Deep Dive

Overview​

The PDF Extractor plugin (@ever-works/plugins/pdf-extractor) is a content extraction plugin that extracts text content from PDF files. It uses a hybrid approach: first attempting text-layer extraction via the unpdf library, then falling back to OCR via the Mistral AI API when the text layer is insufficient.

This plugin is marked as supplementary: true, meaning it extends the content extraction capabilities of the platform rather than serving as a primary content source. It integrates with the ContentExtractorFacade to handle PDF URLs encountered during work generation.

  • Plugin ID: pdf-extractor
  • Category: content-extraction
  • Capabilities: content-extraction
  • Configuration Mode: hybrid
  • Source: packages/plugins/pdf-extractor/src/

Architecture​

Extraction Strategy​

The plugin follows a two-tier extraction approach:

URL with .pdf extension
│
├─ 1. Text-Layer Extraction (unpdf)
│ ├─ Parse PDF binary
│ ├─ Extract text from all pages
│ ├─ Calculate text density
│ └─ If density >= threshold → Return text content
│
└─ 2. OCR Fallback (Mistral AI)
├─ Only if text density < threshold
├─ Only if Mistral API key is configured
├─ Send PDF to Mistral OCR API
└─ Return markdown content from OCR

Text Density Calculation​

The calculateTextDensity() method determines whether the text-layer extraction produced meaningful content:

textDensity = meaningfulCharacters / numberOfPages

Where "meaningful characters" are non-whitespace characters in the extracted text. If the density falls below the textDensityThreshold setting (default: 100 chars/page), the plugin considers the text layer insufficient and falls back to OCR.

Component Structure​

  • PdfExtractorPlugin - Main plugin class implementing IPlugin and IContentExtractorPlugin
  • PdfTextExtractor - Handles text-layer extraction using unpdf library
  • MistralOcrService - Handles OCR extraction using Mistral AI's OCR API

Configuration​

Settings Schema​

FieldTypeDefaultDescription
mistralApiKeystring-Mistral AI API key for OCR fallback (x-secret, x-envVar: PLUGIN_PDF_EXTRACTOR_MISTRAL_API_KEY)
ocrModelstring'mistral-ocr-latest'Mistral OCR model name
textDensityThresholdnumber100Minimum chars/page for text extraction to be considered sufficient
maxPagesnumber50Maximum pages to process
timeoutnumber60000Request timeout in milliseconds

Environment Variables​

VariableMaps To
PLUGIN_PDF_EXTRACTOR_MISTRAL_API_KEYmistralApiKey

Capabilities​

Content Extraction Interface​

The plugin implements IContentExtractorPlugin with two key methods:

  • canExtract(url) - Returns true if the URL ends with .pdf (case-insensitive)
  • extract(url, options) - Extracts text content from the PDF at the given URL

Supported Content​

  • PDF files accessible via HTTP/HTTPS URLs
  • Text-layer PDFs (native digital documents)
  • Image-based PDFs (scanned documents) via OCR fallback
  • Multi-page documents up to maxPages limit

API Reference​

Plugin Class​

class PdfExtractorPlugin implements IPlugin, IContentExtractorPlugin {
readonly id = 'pdf-extractor';
readonly name = 'PDF Extractor';
readonly version = '1.0.0';
readonly category: PluginCategory = 'content-extraction';
readonly capabilities = ['content-extraction'];

canExtract(url: string): boolean;

async extract(url: string, options?: ContentExtractionOptions): Promise<ContentExtractionResult>;

async onLoad(context: PluginContext): Promise<void>;
async onUnload(): Promise<void>;
async healthCheck(): Promise<PluginHealthCheck>;
getManifest(): PluginManifest;
}

PdfTextExtractor​

class PdfTextExtractor {
async extractText(pdfBuffer: Buffer): Promise<PdfTextResult>;
}

interface PdfTextResult {
text: string;
numPages: number;
metadata?: Record<string, unknown>;
}

MistralOcrService​

class MistralOcrService {
constructor(apiKey: string, model?: string);

async extractFromUrl(url: string, maxPages?: number): Promise<string>;
}

Types​

interface MistralOcrRequest {
model: string;
document: {
type: 'document_url';
document_url: string;
};
}

interface MistralOcrPage {
index: number;
markdown: string;
images: unknown[];
dimensions: { width: number; height: number };
}

interface MistralOcrResponse {
pages: MistralOcrPage[];
}

Implementation Details​

Text-Layer Extraction (PdfTextExtractor)​

Uses the unpdf library which provides:

  • getDocumentProxy() - Parses PDF binary into a document object
  • extractText() - Extracts text content from all pages
  • getMeta() - Retrieves PDF metadata (title, author, etc.)

The extracted text is returned along with page count and metadata.

OCR Extraction (MistralOcrService)​

Calls the Mistral AI OCR API at https://api.mistral.ai/v1/ocr:

  1. Sends the PDF URL directly (no download needed)
  2. Receives per-page markdown content with OCR results
  3. Combines pages using --- (horizontal rule) separators
  4. Returns concatenated markdown string

The service handles specific HTTP error codes:

StatusHandling
401Throws "Invalid Mistral API key"
400Throws "Bad request" with detail message
429Throws "Rate limit exceeded"
OtherThrows generic error with status code

Extraction Flow​

async extract(url: string, options?: ContentExtractionOptions) {
// 1. Fetch PDF binary
const response = await fetch(url, { signal: AbortSignal.timeout(timeout) });
const buffer = Buffer.from(await response.arrayBuffer());

// 2. Try text-layer extraction
const textResult = await this.textExtractor.extractText(buffer);

// 3. Check text density
const density = this.calculateTextDensity(textResult);

if (density >= settings.textDensityThreshold) {
return { rawContent: textResult.text, metadata: textResult.metadata };
}

// 4. Fall back to OCR if available
if (settings.mistralApiKey) {
const ocrService = new MistralOcrService(settings.mistralApiKey, settings.ocrModel);
const ocrText = await ocrService.extractFromUrl(url, settings.maxPages);
return { rawContent: ocrText };
}

// 5. Return whatever text was extracted
return { rawContent: textResult.text, metadata: textResult.metadata };
}

Usage Examples​

Automatic Content Extraction​

The PDF Extractor is typically invoked automatically by the ContentExtractorFacade when a PDF URL is encountered during work generation:

// ContentExtractorFacade routes to PDF Extractor when URL ends in .pdf
const result = await contentExtractorFacade.extractContent(
'https://example.com/whitepaper.pdf',
undefined,
facadeOptions
);
// result.rawContent contains the extracted text

Direct Usage​

const plugin = new PdfExtractorPlugin();
await plugin.onLoad(context);

if (plugin.canExtract('https://example.com/report.pdf')) {
const result = await plugin.extract('https://example.com/report.pdf');
console.log(result.rawContent); // Extracted text content
}

Error Handling​

Extraction Errors​

ErrorCauseHandling
Fetch timeoutPDF download exceeds timeoutAbortSignal.timeout triggers, error propagated
Invalid PDFCorrupted or non-PDF binaryunpdf parse error, logged and propagated
Empty text layerPDF has no text (image-only)Falls back to OCR if configured
OCR API failureMistral API errorSpecific error messages for 401/400/429, falls back to text layer
OCR rate limitToo many requestsError propagated, text layer used as fallback
Max pages exceededPDF has more pages than limitOnly first maxPages pages processed
Buffer too largeVery large PDFMay cause memory issues, mitigated by maxPages

Graceful Degradation​

The plugin degrades gracefully through multiple fallback layers:

  1. Best case: Text-layer extraction succeeds with sufficient density
  2. Fallback 1: Text density low, OCR available - use OCR
  3. Fallback 2: Text density low, no OCR configured - return sparse text
  4. Fallback 3: Text extraction fails entirely - error propagated to caller

Health Check​

The healthCheck() returns healthy status regardless of Mistral API key configuration, since text-layer extraction works without any external API. The manifest notes that OCR is optional.