UNPKG

pdf-decomposer

Version:

📚 Extract text and images (as buffers) from PDF files using native parsing and OCR with Tesseract.

173 lines (172 loc) • 5.57 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.decompose = decompose; const os_1 = __importDefault(require("os")); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const child_process_1 = require("child_process"); const util_1 = require("util"); const crypto_1 = require("crypto"); const execFileAsync = (0, util_1.promisify)(child_process_1.execFile); /** * Extract text / images from pdf * @param bufferOrPath buffer or path of file * @returns TODO */ async function decompose(bufferOrPath) { const buffer = typeof bufferOrPath === 'string' ? getFileBuffer(bufferOrPath) : bufferOrPath; isFileValid(buffer); // Work in temp folder const packageDir = path_1.default.join(os_1.default.tmpdir(), 'pdf-decomposer'); // Clean old parsing if (fs_1.default.existsSync(packageDir)) fs_1.default.rmSync(packageDir, { recursive: true }); const tmpDir = path_1.default.join(packageDir, (0, crypto_1.randomUUID)()); fs_1.default.mkdirSync(tmpDir, { recursive: true }); const tmpFilePath = path_1.default.join(tmpDir, 'original.pdf'); fs_1.default.writeFileSync(tmpFilePath, buffer); const nbPages = await getPdfPageCount(tmpFilePath); // const pages = await pdfToPng(tmpFilePath); const promises = []; for (let i = 1; i <= nbPages; i++) { promises.push(new Promise(async (resolve) => { const [text, images] = await Promise.all([ extractPageText(tmpFilePath, i), extractPageImages(tmpFilePath, i), ]); resolve({ page: i, text, images, }); })); } const pages = await Promise.all(promises); // Sort pages by page number return pages.sort((a, b) => a.page - b.page); } /** * Check if file is valid throw error if not * @param path File path * @returns Buffer * @throws Error if file does not exist or is not a file */ function getFileBuffer(path) { if (!fs_1.default.existsSync(path)) throw new Error(`File ${path} does not exist`); if (!fs_1.default.lstatSync(path).isFile()) throw new Error(`${path} is not a file`); return fs_1.default.readFileSync(path); } /** * Check if file is valid throw error if not * @param buffer File buffer * @returns void * @throws Error if file is not a pdf */ function isFileValid(buffer) { if (!isMimeValid(buffer)) { throw new Error('File is not a pdf'); } } /** * Check if file is a pdf * @param buffer File buffer * @returns boolean true if pdf * @throws Error if not a pdf */ function isMimeValid(buffer) { if (!buffer || buffer.length < 4) return false; // PDF: always start with "%PDF" return buffer.subarray(0, 4).toString() === '%PDF'; } /** * Get pdf pages count * @param pdfPath path to pdf file * @returns number of pages */ async function getPdfPageCount(pdfPath) { const { stdout } = await execFileAsync('pdfinfo', [pdfPath]); const match = stdout.match(/Pages:\s+(\d+)/); return match ? parseInt(match[1], 10) : 0; } // /** // * Convert each pdf page into png // * @param pdfPath path to pdf file // * @returns png buffers // * @throws Error if pdftoppm fails // */ // function pdfToPng(pdfPath: string): Promise<Array<Buffer>> { // const tmpDir = path.dirname(pdfPath); // return new Promise((resolve, reject) => { // const proc = spawn('pdftoppm', [ // '-png', // pdfPath, // path.join(tmpDir, 'page'), // ]); // proc.on('close', (code) => { // if (code === 0) { // resolve( // fs.readdirSync(tmpDir).map((f) => fs.readFileSync(`${tmpDir}/${f}`)) // ); // } else { // reject(new Error(`pdftoppm exited with code ${code}`)); // } // }); // proc.stderr.on('data', (err) => { // throw new Error(`Fail to parse pdf ${err.toString()}`); // }); // }); // } /** * Get one pdf page text * @param pdfPath path to pdf file * @param pageIndex number of the page * @returns string text * @throws Error if pdftotext fails */ async function extractPageText(pdfPath, pageIndex) { const { stdout } = await execFileAsync('pdftotext', [ '-f', `${pageIndex}`, '-l', `${pageIndex}`, '-layout', pdfPath, '-', // <--- important : "-" = use stdout ]); return stdout; } /** * Get one pdf page images * @param pdfPath path to pdf file * @param pageIndex number of the page * @returns image buffers * @throws Error if pdfimages fails */ async function extractPageImages(pdfPath, pageIndex) { const tmpDir = path_1.default.join(path_1.default.dirname(pdfPath), 'img-extract'); const outputPrefix = path_1.default.join(tmpDir, `page-${pageIndex}-img`); if (!fs_1.default.existsSync(tmpDir)) fs_1.default.mkdirSync(tmpDir, { recursive: true }); await execFileAsync('pdfimages', [ '-f', `${pageIndex}`, '-l', `${pageIndex}`, '-png', pdfPath, outputPrefix, ]); const imageFiles = fs_1.default .readdirSync(tmpDir) .filter((name) => name.startsWith(`page-${pageIndex}-img`)) .map((name) => path_1.default.join(tmpDir, name)); return imageFiles.map((file) => fs_1.default.readFileSync(file)); }