UNPKG

n8n-nodes-extract-pdf

Version:

n8n node to extract text, images and tables from PDF with multilingual support, language detection and comprehensive test suite

425 lines 18.4 kB
"use strict"; /** * Module xử lý OCR cho ExtractPdfNode * @author AI Assistant * @version 1.0.0 */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.enhanceImageForOcr = exports.generateLanguageStatistics = exports.enhanceOcrWithAI = exports.performMultilingualOCR = exports.mapScriptToLanguage = exports.extractScriptFromOSD = exports.performOcrOnPdf = void 0; const fs = __importStar(require("fs")); const os = __importStar(require("os")); const path = __importStar(require("path")); const worker_threads = __importStar(require("worker_threads")); const crypto_1 = __importDefault(require("crypto")); const node_cache_1 = __importDefault(require("node-cache")); const tesseract_js_1 = require("tesseract.js"); const pdf_img_convert_1 = require("pdf-img-convert"); const __1 = require("../"); // Cache cho kết quả OCR const cacheOptions = { stdTTL: 3600, checkperiod: 600, useClones: false }; const ocrCache = new node_cache_1.default(cacheOptions); /** * Tạo cache key từ nội dung hình ảnh và tùy chọn OCR */ function generateCacheKey(imageBuffer, options) { const imageHash = crypto_1.default .createHash('md5') .update(imageBuffer) .digest('hex'); return `${imageHash}_${JSON.stringify(options)}`; } /** * Xử lý OCR cho một trang trong worker thread */ async function processPageInWorker(imageBuffer, options) { if (worker_threads.isMainThread) { return new Promise((resolve, reject) => { const worker = new worker_threads.Worker(__filename, { workerData: { imageBuffer, options, type: 'ocrWorker' } }); worker.on('message', resolve); worker.on('error', reject); worker.on('exit', (code) => { if (code !== 0) { reject(new Error(`Worker stopped with exit code ${code}`)); } }); }); } else { // Code chạy trong worker thread try { const { imageBuffer, options } = worker_threads.workerData; const result = await (0, tesseract_js_1.recognize)(imageBuffer, options.language || 'eng', { logger: (_) => { } // Tắt logging }); if (worker_threads.parentPort) { worker_threads.parentPort.postMessage(result.data.text); return result.data.text; } throw new Error('Worker thread parent port is null'); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`OCR processing failed in worker: ${errorMessage}`); } } } /** * Thực hiện OCR trên tệp PDF với cải tiến hiệu suất */ async function performOcrOnPdf(filePath, pageNumbers, options = {}) { const startTime = Date.now(); try { // Tự động phát hiện ngôn ngữ nếu được yêu cầu if (options.autoDetect) { // Đọc một phần nhỏ của PDF để phát hiện ngôn ngữ const sampleImage = await (0, pdf_img_convert_1.convert)(filePath, { page_numbers: [0], scale: 1.0 }); if (sampleImage && sampleImage.length > 0) { const sampleResult = await (0, tesseract_js_1.recognize)(sampleImage[0], 'eng', // Sử dụng tiếng Anh để lấy mẫu văn bản { logger: (_) => { } }); if (sampleResult.data.text) { const detectedLang = await __1.languageDetection.detectLanguage(sampleResult.data.text); options.language = detectedLang; console.log(`Detected language: ${detectedLang}`); } } } // Chuyển đổi PDF thành hình ảnh const outputImages = await (0, pdf_img_convert_1.convert)(filePath, { page_numbers: pageNumbers.map(p => p - 1), scale: options.scale || 2.0, dpi: options.dpi || 300 }); let allText = ''; const processedPages = new Set(); const maxWorkers = options.maxWorkers || os.cpus().length; // Xử lý từng trang const processPage = async (imageBuffer, pageIndex) => { // Kiểm tra cache nếu được bật if (options.useCache) { const cacheKey = generateCacheKey(imageBuffer, options); const cachedResult = ocrCache.get(cacheKey); if (cachedResult) { return cachedResult; } } // Nâng cao chất lượng hình ảnh nếu được yêu cầu let processableImage = imageBuffer; if (options.enhanceImage) { const tempImagePath = path.join(os.tmpdir(), `ocr_page_${pageIndex}.png`); await fs.promises.writeFile(tempImagePath, imageBuffer); const enhancedImagePath = await __1.imageExtraction.enhanceImage(tempImagePath, { contrast: 50, brightness: 10, sharpness: 50, denoise: true, format: 'png' }); processableImage = await fs.promises.readFile(enhancedImagePath); // Dọn dẹp file tạm try { await fs.promises.unlink(tempImagePath); if (enhancedImagePath !== tempImagePath) { await fs.promises.unlink(enhancedImagePath); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.warn(`Warning: Could not delete temporary files: ${errorMessage}`); } } // Xử lý OCR let pageText; if (options.useParallel) { pageText = await processPageInWorker(processableImage, options); } else { const result = await (0, tesseract_js_1.recognize)(processableImage, options.language || 'eng', { logger: options.verbose ? console.log : (_) => { } }); pageText = result.data.text || ''; } // Lưu vào cache nếu được bật if (options.useCache) { const cacheKey = generateCacheKey(imageBuffer, options); ocrCache.set(cacheKey, pageText); } return pageText; }; // Xử lý song song hoặc tuần tự tùy thuộc vào cấu hình if (options.useParallel && outputImages.length > 1) { // Xử lý song song với giới hạn số worker const batches = []; for (let i = 0; i < outputImages.length; i += maxWorkers) { const batchImages = outputImages.slice(i, i + maxWorkers); const batchIndices = pageNumbers.slice(i, i + maxWorkers); const batchPromises = batchImages.map((img, j) => processPage(img, batchIndices[j])); const batchResults = await Promise.all(batchPromises); batches.push(...batchResults); // Cập nhật các trang đã xử lý batchIndices.forEach(pageNum => processedPages.add(pageNum)); } allText = batches.join('\n\n--- Page Break ---\n\n'); } else { // Xử lý tuần tự const results = []; for (let i = 0; i < outputImages.length; i++) { const pageText = await processPage(outputImages[i], pageNumbers[i]); results.push(pageText); processedPages.add(pageNumbers[i]); } allText = results.join('\n\n--- Page Break ---\n\n'); } // Phân tích thống kê ngôn ngữ const languageStats = await generateLanguageStatistics(allText); const endTime = Date.now(); const processingTime = (endTime - startTime) / 1000; // Trả về kết quả return { text: allText, pageCount: processedPages.size, processedPages: Array.from(processedPages), language: options.language || 'eng', hasMultipleLanguages: Object.keys(languageStats).length > 1, languageStats: Object.entries(languageStats).map(([language, confidence]) => ({ language, confidence: parseFloat(confidence.toString()) })), performance: { totalProcessingTime: processingTime, pagesPerSecond: processedPages.size / processingTime, startTime, endTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`OCR processing failed: ${errorMessage}`); } } exports.performOcrOnPdf = performOcrOnPdf; /** * Trích xuất script từ dữ liệu OSD (Orientation and Script Detection) * @param osdData Dữ liệu OSD từ Tesseract * @returns Mã script đã trích xuất hoặc undefined nếu không tìm thấy */ function extractScriptFromOSD(osdData) { if (!osdData || typeof osdData !== 'object') { return undefined; } // OSD data có thể có cấu trúc khác nhau tùy theo phiên bản Tesseract if (osdData.script_name) { return osdData.script_name.toLowerCase(); } if (osdData.scripts && Array.isArray(osdData.scripts) && osdData.scripts.length > 0) { // Lấy script có điểm cao nhất const bestScript = osdData.scripts.reduce((best, current) => { if (!best || (current.score && current.score > best.score)) { return current; } return best; }, null); if (bestScript && bestScript.name) { return bestScript.name.toLowerCase(); } } return undefined; } exports.extractScriptFromOSD = extractScriptFromOSD; /** * Ánh xạ script tới ngôn ngữ Tesseract phù hợp * @param script Mã script * @returns Mã ngôn ngữ Tesseract */ function mapScriptToLanguage(script) { if (!script) return 'eng'; // Mặc định là tiếng Anh // Ánh xạ từ script sang mã ngôn ngữ Tesseract const scriptToLang = { 'latin': 'eng', 'cyrillic': 'rus', 'arabic': 'ara', 'devanagari': 'hin', 'han': 'chi_sim', 'hangul': 'kor', 'greek': 'grc', 'hebrew': 'heb', 'thai': 'tha', 'japanese': 'jpn', // Thêm các script khác khi cần }; return scriptToLang[script.toLowerCase()] || 'eng'; } exports.mapScriptToLanguage = mapScriptToLanguage; /** * Thực hiện OCR đa ngôn ngữ trên một hình ảnh * @param imagePath Đường dẫn đến hình ảnh * @param languages Mảng mã ngôn ngữ cần nhận dạng * @returns Văn bản đã trích xuất */ async function performMultilingualOCR(imagePath, languages = ['eng']) { try { // Sắp xếp ngôn ngữ theo thứ tự ưu tiên (eng luôn được ưu tiên cao nhất) const sortedLangs = Array.from(new Set(['eng', ...languages])); // Chuyển đổi mảng ngôn ngữ thành chuỗi Tesseract (e.g., "eng+fra+deu") const langString = sortedLangs.join('+'); // Thực hiện nhận dạng với đa ngôn ngữ const result = await (0, tesseract_js_1.recognize)(imagePath, langString, { logger: (_) => { } // Tắt logging }); return result.data.text || ''; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.error(`Multilingual OCR failed: ${errorMessage}`); // Thử lại với chỉ tiếng Anh try { const fallbackResult = await (0, tesseract_js_1.recognize)(imagePath, 'eng', { logger: (_) => { } }); return fallbackResult.data.text || ''; } catch (innerError) { console.error('Fallback OCR also failed:', innerError); return ''; // Trả về chuỗi rỗng trong trường hợp lỗi } } } exports.performMultilingualOCR = performMultilingualOCR; /** * Nâng cao kết quả OCR bằng AI * @param text Văn bản cần nâng cao * @param language Mã ngôn ngữ * @param correctionLevel Mức độ sửa lỗi ('low', 'medium', 'high') * @returns Văn bản đã nâng cao */ async function enhanceOcrWithAI(text, language = 'eng', correctionLevel = 'medium') { try { // Trong phiên bản thực, đây sẽ gọi API NLP hoặc mô hình AI // Đây là mô phỏng đơn giản của quá trình sửa lỗi if (!text || text.trim().length === 0) { return text; } // Sửa một số lỗi OCR phổ biến let enhancedText = text // Sửa số 0 và chữ O .replace(/0(?=[a-zA-Z])/g, 'O') .replace(/(?<=[a-zA-Z])0/g, 'o') // Sửa số 1, chữ l và chữ I .replace(/1(?=[a-zA-Z])/g, 'I') .replace(/(?<=[a-zA-Z])1/g, 'l') // Sửa số 5 và chữ S .replace(/5(?=[a-zA-Z])/g, 'S') .replace(/(?<=[a-zA-Z])5/g, 's') // Sửa dấu | và chữ l, I .replace(/\|(?=[a-zA-Z])/g, 'I') .replace(/(?<=[a-zA-Z])\|/g, 'l') // Sửa dấu cách thừa .replace(/\s{2,}/g, ' ') .replace(/\s+\./g, '.') .replace(/\s+,/g, ',') // Sửa lỗi xuống dòng .replace(/(\w)-\s+(\w)/g, '$1$2'); // Áp dụng sửa lỗi nâng cao dựa trên mức độ if (correctionLevel === 'high') { // Trong thực tế, đây sẽ gọi mô hình ngôn ngữ nâng cao // Mô phỏng bằng cách sửa thêm một số lỗi enhancedText = enhancedText // Viết hoa đầu câu .replace(/(\.\s+)([a-z])/g, (_, p1, p2) => p1 + p2.toUpperCase()); } return enhancedText; } catch (error) { console.error('Error enhancing OCR with AI:', error); return text; // Trả về văn bản gốc nếu có lỗi } } exports.enhanceOcrWithAI = enhanceOcrWithAI; /** * Tạo thống kê ngôn ngữ từ văn bản * @param text Văn bản cần phân tích * @returns Thống kê tần suất ngôn ngữ */ async function generateLanguageStatistics(text) { try { // Nếu văn bản quá ngắn, không thể tạo thống kê chính xác if (!text || text.length < 100) { return { 'und': 1 }; // undefined language } // Chia văn bản thành các đoạn để phát hiện ngôn ngữ const paragraphs = text.split(/\n\s*\n/); const languageCounts = {}; // Đếm ngôn ngữ trong từng đoạn for (const paragraph of paragraphs) { if (paragraph.trim().length < 20) continue; // Bỏ qua đoạn quá ngắn // Phát hiện ngôn ngữ const detectedLang = await __1.languageDetection.detectLanguage(paragraph); // Chuyển đổi sang mã Tesseract const tesseractCode = __1.languageDetection.mapLanguageCodeToTesseract(detectedLang); // Tính tần suất languageCounts[tesseractCode] = (languageCounts[tesseractCode] || 0) + 1; } // Mặc định là tiếng Anh nếu không phát hiện được ngôn ngữ nào if (Object.keys(languageCounts).length === 0) { languageCounts['eng'] = 1; } return languageCounts; } catch (error) { console.error('Error generating language statistics:', error); return { 'eng': 1 }; // Mặc định là tiếng Anh nếu có lỗi } } exports.generateLanguageStatistics = generateLanguageStatistics; /** * Nâng cao chất lượng hình ảnh để cải thiện OCR * @param imagePath Đường dẫn đến hình ảnh * @returns Đường dẫn đến hình ảnh đã cải thiện */ async function enhanceImageForOcr(imagePath) { return __1.imageExtraction.enhanceImage(imagePath); } exports.enhanceImageForOcr = enhanceImageForOcr; //# sourceMappingURL=index.js.map