2025-10-01 16:51:03 -07:00
/ * *
* PDF Format Handler
* Handles PDF files with :
* - Text extraction with layout preservation
* - Table detection and extraction
* - Metadata extraction ( author , dates , etc . )
* - Page - by - page processing
* /
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs'
import { BaseFormatHandler } from './base.js'
import { FormatHandlerOptions , ProcessedData } from '../types.js'
// Use built-in worker for Node.js environments
// In production, this can be customized via options
const initializeWorker = ( ) = > {
if ( typeof pdfjsLib . GlobalWorkerOptions . workerSrc === 'undefined' ||
pdfjsLib . GlobalWorkerOptions . workerSrc === '' ) {
// Use a data URL to avoid file system dependencies
// This tells pdfjs to use the built-in fallback worker
try {
pdfjsLib . GlobalWorkerOptions . workerSrc = 'data:,'
} catch {
// Ignore if already set or in incompatible environment
}
}
}
initializeWorker ( )
export class PDFHandler extends BaseFormatHandler {
readonly format = 'pdf'
canHandle ( data : Buffer | string | { filename? : string , ext? : string } ) : boolean {
const ext = this . detectExtension ( data )
if ( ext === 'pdf' ) return true
// Check for PDF magic bytes
if ( Buffer . isBuffer ( data ) ) {
const header = data . slice ( 0 , 5 ) . toString ( 'ascii' )
return header === '%PDF-'
}
return false
}
async process ( data : Buffer | string , options : FormatHandlerOptions ) : Promise < ProcessedData > {
const startTime = Date . now ( )
feat: comprehensive import progress tracking for all 7 formats
Add real-time progress reporting throughout the entire import pipeline
with a standardized API that works across all supported formats.
Workshop Team Feature Request:
- Eliminates "0% complete" hangs during AI extraction
- Shows continuous progress with entities/sec, throughput, ETA
- Reports contextual messages ("Processing page 5 of 23")
- Standardized progress API for CSV, PDF, Excel, JSON, Markdown, YAML, DOCX
Core Changes:
- Add FormatHandlerProgressHooks interface for extensible progress
- Wire up all 3 binary format handlers (CSV, PDF, Excel) with 7+ progress points
- Wire up all 4 text format importers (JSON, Markdown, YAML, DOCX)
- Add ImportProgress interface with stage, message, counts, throughput, ETA
- ImportCoordinator normalizes all format progress to standard interface
CLI Improvements:
- Import command now uses brain.import() directly with full progress
- Add --include-vfs flag to find command (v4.4.0 compatibility)
- Add --confidence and --weight options to add command
Documentation:
- docs/guides/standard-import-progress.md - Universal API guide
- docs/guides/import-progress-implementation.md - Developer guide
- docs/guides/import-progress-examples.md - Practical examples
- JSDoc on brain.import() with universal handler examples
Result: ONE progress handler works for ALL 7 formats with zero format-specific code!
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 14:45:46 -07:00
const progressHooks = options . progressHooks
2025-10-01 16:51:03 -07:00
// Convert to buffer
const buffer = Buffer . isBuffer ( data ) ? data : Buffer.from ( data , 'binary' )
feat: comprehensive import progress tracking for all 7 formats
Add real-time progress reporting throughout the entire import pipeline
with a standardized API that works across all supported formats.
Workshop Team Feature Request:
- Eliminates "0% complete" hangs during AI extraction
- Shows continuous progress with entities/sec, throughput, ETA
- Reports contextual messages ("Processing page 5 of 23")
- Standardized progress API for CSV, PDF, Excel, JSON, Markdown, YAML, DOCX
Core Changes:
- Add FormatHandlerProgressHooks interface for extensible progress
- Wire up all 3 binary format handlers (CSV, PDF, Excel) with 7+ progress points
- Wire up all 4 text format importers (JSON, Markdown, YAML, DOCX)
- Add ImportProgress interface with stage, message, counts, throughput, ETA
- ImportCoordinator normalizes all format progress to standard interface
CLI Improvements:
- Import command now uses brain.import() directly with full progress
- Add --include-vfs flag to find command (v4.4.0 compatibility)
- Add --confidence and --weight options to add command
Documentation:
- docs/guides/standard-import-progress.md - Universal API guide
- docs/guides/import-progress-implementation.md - Developer guide
- docs/guides/import-progress-examples.md - Practical examples
- JSDoc on brain.import() with universal handler examples
Result: ONE progress handler works for ALL 7 formats with zero format-specific code!
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 14:45:46 -07:00
const totalBytes = buffer . length
// v4.5.0: Report start
if ( progressHooks ? . onBytesProcessed ) {
progressHooks . onBytesProcessed ( 0 )
}
if ( progressHooks ? . onCurrentItem ) {
progressHooks . onCurrentItem ( 'Loading PDF document...' )
}
2025-10-01 16:51:03 -07:00
try {
// Load PDF document
const loadingTask = pdfjsLib . getDocument ( {
data : new Uint8Array ( buffer ) ,
useSystemFonts : true ,
standardFontDataUrl : undefined
} )
const pdfDoc = await loadingTask . promise
// Extract metadata
const metadata = await pdfDoc . getMetadata ( )
const numPages = pdfDoc . numPages
feat: comprehensive import progress tracking for all 7 formats
Add real-time progress reporting throughout the entire import pipeline
with a standardized API that works across all supported formats.
Workshop Team Feature Request:
- Eliminates "0% complete" hangs during AI extraction
- Shows continuous progress with entities/sec, throughput, ETA
- Reports contextual messages ("Processing page 5 of 23")
- Standardized progress API for CSV, PDF, Excel, JSON, Markdown, YAML, DOCX
Core Changes:
- Add FormatHandlerProgressHooks interface for extensible progress
- Wire up all 3 binary format handlers (CSV, PDF, Excel) with 7+ progress points
- Wire up all 4 text format importers (JSON, Markdown, YAML, DOCX)
- Add ImportProgress interface with stage, message, counts, throughput, ETA
- ImportCoordinator normalizes all format progress to standard interface
CLI Improvements:
- Import command now uses brain.import() directly with full progress
- Add --include-vfs flag to find command (v4.4.0 compatibility)
- Add --confidence and --weight options to add command
Documentation:
- docs/guides/standard-import-progress.md - Universal API guide
- docs/guides/import-progress-implementation.md - Developer guide
- docs/guides/import-progress-examples.md - Practical examples
- JSDoc on brain.import() with universal handler examples
Result: ONE progress handler works for ALL 7 formats with zero format-specific code!
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 14:45:46 -07:00
// v4.5.0: Report document loaded
if ( progressHooks ? . onCurrentItem ) {
progressHooks . onCurrentItem ( ` Processing ${ numPages } pages... ` )
}
2025-10-01 16:51:03 -07:00
// Extract text and structure from all pages
const allData : Array < Record < string , any > > = [ ]
let totalTextLength = 0
let detectedTables = 0
for ( let pageNum = 1 ; pageNum <= numPages ; pageNum ++ ) {
feat: comprehensive import progress tracking for all 7 formats
Add real-time progress reporting throughout the entire import pipeline
with a standardized API that works across all supported formats.
Workshop Team Feature Request:
- Eliminates "0% complete" hangs during AI extraction
- Shows continuous progress with entities/sec, throughput, ETA
- Reports contextual messages ("Processing page 5 of 23")
- Standardized progress API for CSV, PDF, Excel, JSON, Markdown, YAML, DOCX
Core Changes:
- Add FormatHandlerProgressHooks interface for extensible progress
- Wire up all 3 binary format handlers (CSV, PDF, Excel) with 7+ progress points
- Wire up all 4 text format importers (JSON, Markdown, YAML, DOCX)
- Add ImportProgress interface with stage, message, counts, throughput, ETA
- ImportCoordinator normalizes all format progress to standard interface
CLI Improvements:
- Import command now uses brain.import() directly with full progress
- Add --include-vfs flag to find command (v4.4.0 compatibility)
- Add --confidence and --weight options to add command
Documentation:
- docs/guides/standard-import-progress.md - Universal API guide
- docs/guides/import-progress-implementation.md - Developer guide
- docs/guides/import-progress-examples.md - Practical examples
- JSDoc on brain.import() with universal handler examples
Result: ONE progress handler works for ALL 7 formats with zero format-specific code!
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 14:45:46 -07:00
// v4.5.0: Report current page
if ( progressHooks ? . onCurrentItem ) {
progressHooks . onCurrentItem ( ` Processing page ${ pageNum } of ${ numPages } ` )
}
2025-10-01 16:51:03 -07:00
const page = await pdfDoc . getPage ( pageNum )
const textContent = await page . getTextContent ( )
// Extract text items with positions
const textItems = textContent . items . map ( ( item : any ) = > ( {
text : item.str ,
x : item.transform [ 4 ] ,
y : item.transform [ 5 ] ,
width : item.width ,
height : item.height
} ) )
// Combine text items into lines (group by similar Y position)
const lines = this . groupIntoLines ( textItems )
// Detect tables if requested
if ( options . pdfExtractTables !== false ) {
const tables = this . detectTables ( lines )
if ( tables . length > 0 ) {
detectedTables += tables . length
for ( const table of tables ) {
allData . push ( . . . table . rows )
}
}
}
// Extract paragraphs from non-table lines
const paragraphs = this . extractParagraphs ( lines )
for ( let i = 0 ; i < paragraphs . length ; i ++ ) {
const text = paragraphs [ i ] . trim ( )
if ( text . length > 0 ) {
totalTextLength += text . length
allData . push ( {
_page : pageNum ,
_type : 'paragraph' ,
_index : i ,
text
} )
}
}
feat: comprehensive import progress tracking for all 7 formats
Add real-time progress reporting throughout the entire import pipeline
with a standardized API that works across all supported formats.
Workshop Team Feature Request:
- Eliminates "0% complete" hangs during AI extraction
- Shows continuous progress with entities/sec, throughput, ETA
- Reports contextual messages ("Processing page 5 of 23")
- Standardized progress API for CSV, PDF, Excel, JSON, Markdown, YAML, DOCX
Core Changes:
- Add FormatHandlerProgressHooks interface for extensible progress
- Wire up all 3 binary format handlers (CSV, PDF, Excel) with 7+ progress points
- Wire up all 4 text format importers (JSON, Markdown, YAML, DOCX)
- Add ImportProgress interface with stage, message, counts, throughput, ETA
- ImportCoordinator normalizes all format progress to standard interface
CLI Improvements:
- Import command now uses brain.import() directly with full progress
- Add --include-vfs flag to find command (v4.4.0 compatibility)
- Add --confidence and --weight options to add command
Documentation:
- docs/guides/standard-import-progress.md - Universal API guide
- docs/guides/import-progress-implementation.md - Developer guide
- docs/guides/import-progress-examples.md - Practical examples
- JSDoc on brain.import() with universal handler examples
Result: ONE progress handler works for ALL 7 formats with zero format-specific code!
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 14:45:46 -07:00
// v4.5.0: Estimate bytes processed (pages are sequential)
const bytesProcessed = Math . floor ( ( pageNum / numPages ) * totalBytes )
if ( progressHooks ? . onBytesProcessed ) {
progressHooks . onBytesProcessed ( bytesProcessed )
}
// v4.5.0: Report extraction progress
if ( progressHooks ? . onDataExtracted ) {
progressHooks . onDataExtracted ( allData . length , undefined ) // Total unknown until complete
}
}
// v4.5.0: Final progress - all bytes processed
if ( progressHooks ? . onBytesProcessed ) {
progressHooks . onBytesProcessed ( totalBytes )
}
if ( progressHooks ? . onDataExtracted ) {
progressHooks . onDataExtracted ( allData . length , allData . length )
2025-10-01 16:51:03 -07:00
}
const processingTime = Date . now ( ) - startTime
feat: comprehensive import progress tracking for all 7 formats
Add real-time progress reporting throughout the entire import pipeline
with a standardized API that works across all supported formats.
Workshop Team Feature Request:
- Eliminates "0% complete" hangs during AI extraction
- Shows continuous progress with entities/sec, throughput, ETA
- Reports contextual messages ("Processing page 5 of 23")
- Standardized progress API for CSV, PDF, Excel, JSON, Markdown, YAML, DOCX
Core Changes:
- Add FormatHandlerProgressHooks interface for extensible progress
- Wire up all 3 binary format handlers (CSV, PDF, Excel) with 7+ progress points
- Wire up all 4 text format importers (JSON, Markdown, YAML, DOCX)
- Add ImportProgress interface with stage, message, counts, throughput, ETA
- ImportCoordinator normalizes all format progress to standard interface
CLI Improvements:
- Import command now uses brain.import() directly with full progress
- Add --include-vfs flag to find command (v4.4.0 compatibility)
- Add --confidence and --weight options to add command
Documentation:
- docs/guides/standard-import-progress.md - Universal API guide
- docs/guides/import-progress-implementation.md - Developer guide
- docs/guides/import-progress-examples.md - Practical examples
- JSDoc on brain.import() with universal handler examples
Result: ONE progress handler works for ALL 7 formats with zero format-specific code!
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 14:45:46 -07:00
// v4.5.0: Report completion
if ( progressHooks ? . onCurrentItem ) {
progressHooks . onCurrentItem (
` PDF complete: ${ numPages } pages, ${ allData . length } items extracted `
)
}
2025-10-01 16:51:03 -07:00
// Get all unique fields (excluding metadata fields)
const fields = allData . length > 0
? Object . keys ( allData [ 0 ] ) . filter ( k = > ! k . startsWith ( '_' ) )
: [ ]
return {
format : this.format ,
data : allData ,
metadata : this.createMetadata (
allData . length ,
fields ,
processingTime ,
{
pageCount : numPages ,
textLength : totalTextLength ,
tableCount : detectedTables ,
pdfMetadata : {
title : ( metadata . info as any ) ? . Title || null ,
author : ( metadata . info as any ) ? . Author || null ,
subject : ( metadata . info as any ) ? . Subject || null ,
creator : ( metadata . info as any ) ? . Creator || null ,
producer : ( metadata . info as any ) ? . Producer || null ,
creationDate : ( metadata . info as any ) ? . CreationDate || null ,
modificationDate : ( metadata . info as any ) ? . ModDate || null
}
}
) ,
filename : options.filename
}
} catch ( error ) {
throw new Error ( ` PDF parsing failed: ${ error instanceof Error ? error.message : String ( error ) } ` )
}
}
/ * *
* Group text items into lines based on Y position
* /
private groupIntoLines ( items : Array < { text : string , x : number , y : number , width : number , height : number } > ) : Array < Array < { text : string , x : number } > > {
if ( items . length === 0 ) return [ ]
// Sort by Y position (descending, since PDF coordinates go bottom-up)
const sorted = [ . . . items ] . sort ( ( a , b ) = > b . y - a . y )
const lines : Array < Array < { text : string , x : number } > > = [ ]
let currentLine : Array < { text : string , x : number } > = [ ]
let currentY = sorted [ 0 ] . y
for ( const item of sorted ) {
// If Y position differs by more than half the height, it's a new line
if ( Math . abs ( item . y - currentY ) > ( item . height / 2 ) ) {
if ( currentLine . length > 0 ) {
// Sort line items by X position
currentLine . sort ( ( a , b ) = > a . x - b . x )
lines . push ( currentLine )
}
currentLine = [ ]
currentY = item . y
}
if ( item . text . trim ( ) ) {
currentLine . push ( { text : item.text , x : item.x } )
}
}
// Add last line
if ( currentLine . length > 0 ) {
currentLine . sort ( ( a , b ) = > a . x - b . x )
lines . push ( currentLine )
}
return lines
}
/ * *
* Detect tables from lines
* Tables are detected when multiple consecutive lines have similar structure
* /
private detectTables ( lines : Array < Array < { text : string , x : number } > > ) : Array < { rows : Array < Record < string , any > > } > {
const tables : Array < { rows : Array < Record < string , any > > } > = [ ]
let potentialTable : Array < Array < { text : string , x : number } > > = [ ]
for ( let i = 0 ; i < lines . length ; i ++ ) {
const line = lines [ i ]
// A line with multiple items could be part of a table
if ( line . length >= 2 ) {
potentialTable . push ( line )
} else {
// End of potential table
if ( potentialTable . length >= 3 ) { // Need at least header + 2 rows
const table = this . parseTable ( potentialTable )
if ( table ) {
tables . push ( table )
}
}
potentialTable = [ ]
}
}
// Check last potential table
if ( potentialTable . length >= 3 ) {
const table = this . parseTable ( potentialTable )
if ( table ) {
tables . push ( table )
}
}
return tables
}
/ * *
* Parse a potential table into structured rows
* /
private parseTable ( lines : Array < Array < { text : string , x : number } > > ) : { rows : Array < Record < string , any > > } | null {
if ( lines . length < 2 ) return null
// First line is headers
const headerLine = lines [ 0 ]
const headers = headerLine . map ( item = > this . sanitizeFieldName ( item . text ) )
// Remaining lines are data
const rows : Array < Record < string , any > > = [ ]
for ( let i = 1 ; i < lines . length ; i ++ ) {
const line = lines [ i ]
const row : Record < string , any > = { _type : 'table_row' }
// Match each item to closest header by X position
for ( let j = 0 ; j < line . length && j < headers . length ; j ++ ) {
const header = headers [ j ]
const value = line [ j ] . text . trim ( )
row [ header ] = value || null
}
if ( Object . keys ( row ) . length > 1 ) { // More than just _type
rows . push ( row )
}
}
return rows . length > 0 ? { rows } : null
}
/ * *
* Extract paragraphs from lines
* /
private extractParagraphs ( lines : Array < Array < { text : string , x : number } > > ) : string [ ] {
const paragraphs : string [ ] = [ ]
let currentParagraph : string [ ] = [ ]
for ( const line of lines ) {
const lineText = line . map ( item = > item . text ) . join ( ' ' ) . trim ( )
if ( lineText . length === 0 ) {
// Empty line - end paragraph
if ( currentParagraph . length > 0 ) {
paragraphs . push ( currentParagraph . join ( ' ' ) )
currentParagraph = [ ]
}
} else {
currentParagraph . push ( lineText )
}
}
// Add last paragraph
if ( currentParagraph . length > 0 ) {
paragraphs . push ( currentParagraph . join ( ' ' ) )
}
return paragraphs
}
}