xinvoice/ts/formats/pdf/extractors/text.extractor.ts

162 lines
5.5 KiB
TypeScript

import { BaseXMLExtractor } from './base.extractor.js';
/**
* Text-based XML extractor for PDF documents
* Extracts XML by searching for XML patterns in the PDF text
* Used as a fallback when other extraction methods fail
*/
export class TextXMLExtractor extends BaseXMLExtractor {
// Maximum chunk size to process at once (4MB)
private readonly CHUNK_SIZE = 4 * 1024 * 1024;
// Maximum number of chunks to check (effective 20MB search limit)
private readonly MAX_CHUNKS = 5;
// Common XML patterns to look for
private readonly XML_PATTERNS = [
'<?xml',
'<CrossIndustryInvoice',
'<CrossIndustryDocument',
'<Invoice',
'<CreditNote',
'<rsm:CrossIndustryInvoice',
'<rsm:CrossIndustryDocument',
'<ram:CrossIndustryDocument',
'<ubl:Invoice',
'<ubl:CreditNote',
'<FatturaElettronica'
];
/**
* Extract XML from a PDF buffer by searching for XML patterns in the text
* Uses a chunked approach to handle large files efficiently
* @param pdfBuffer PDF buffer
* @returns XML content or null if not found
*/
public async extractXml(pdfBuffer: Uint8Array | Buffer): Promise<string | null> {
try {
console.log('Attempting text-based XML extraction from PDF...');
// Convert Buffer to Uint8Array if needed
const buffer = Buffer.isBuffer(pdfBuffer) ? new Uint8Array(pdfBuffer) : pdfBuffer;
// Try extracting XML using the chunked approach
return this.extractXmlFromBufferChunked(buffer);
} catch (error) {
console.error('Error in text-based extraction:', error);
return null;
}
}
/**
* Extract XML from buffer using a chunked approach
* This helps avoid memory issues with large PDFs
* @param buffer Buffer to search in
* @returns XML content or null if not found
*/
private extractXmlFromBufferChunked(buffer: Uint8Array): string | null {
// Process the PDF in chunks
for (let chunkIndex = 0; chunkIndex < this.MAX_CHUNKS; chunkIndex++) {
const startPos = chunkIndex * this.CHUNK_SIZE;
if (startPos >= buffer.length) break;
const endPos = Math.min(startPos + this.CHUNK_SIZE, buffer.length);
const chunk = buffer.slice(startPos, endPos);
// Try to extract XML from this chunk
const chunkResult = this.processChunk(chunk, startPos);
if (chunkResult) {
return chunkResult;
}
}
console.warn('No valid XML found in any chunk of the PDF');
return null;
}
/**
* Process a single chunk of the PDF buffer
* @param chunk Chunk buffer to process
* @param chunkOffset Offset position of the chunk in the original buffer
* @returns XML content or null if not found
*/
private processChunk(chunk: Uint8Array, chunkOffset: number): string | null {
try {
// First try UTF-8 encoding for this chunk
const utf8String = this.decodeBufferToString(chunk, 'utf-8');
let xmlContent = this.searchForXmlInString(utf8String);
if (xmlContent) {
console.log(`Found XML content in chunk at offset ${chunkOffset} using UTF-8 encoding`);
return xmlContent;
}
// If UTF-8 fails, try Latin-1 (ISO-8859-1) which can handle binary better
const latin1String = this.decodeBufferToString(chunk, 'latin1');
xmlContent = this.searchForXmlInString(latin1String);
if (xmlContent) {
console.log(`Found XML content in chunk at offset ${chunkOffset} using Latin-1 encoding`);
return xmlContent;
}
// No XML found in this chunk
return null;
} catch (error) {
console.warn(`Error processing chunk at offset ${chunkOffset}:`, error);
return null;
}
}
/**
* Safely decode a buffer to string using the specified encoding
* @param buffer Buffer to decode
* @param encoding Encoding to use ('utf-8' or 'latin1')
* @returns Decoded string
*/
private decodeBufferToString(buffer: Uint8Array, encoding: 'utf-8' | 'latin1'): string {
try {
if (encoding === 'utf-8') {
return new TextDecoder('utf-8', { fatal: false }).decode(buffer);
} else {
// For Latin-1 we can use a direct mapping (bytes 0-255 map directly to code points 0-255)
// This is more reliable for binary data than TextDecoder for legacy encodings
return Array.from(buffer)
.map(byte => String.fromCharCode(byte))
.join('');
}
} catch (error) {
console.warn(`Error decoding buffer using ${encoding}:`, error);
// Return empty string on error to allow processing to continue
return '';
}
}
/**
* Search for XML patterns in a string
* @param content String to search in
* @returns XML content or null if not found
*/
private searchForXmlInString(content: string): string | null {
if (!content) return null;
// Search for each XML pattern
for (const pattern of this.XML_PATTERNS) {
const patternIndex = content.indexOf(pattern);
if (patternIndex !== -1) {
console.log(`Found XML pattern "${pattern}" at position ${patternIndex}`);
// Try to extract the XML content starting from the pattern position
const xmlContent = this.extractXmlFromString(content, patternIndex);
// Validate the extracted content
if (xmlContent && this.isValidXml(xmlContent)) {
console.log('Successfully extracted and validated XML from text');
return xmlContent;
}
}
}
return null;
}
}