UNPKG

pdf-decomposer

Version:

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

202 lines (175 loc) • 5.03 kB
import os from 'os'; import path from 'path'; import fs from 'fs'; import { execFile } from 'child_process'; import { promisify } from 'util'; import { randomUUID } from 'crypto'; const execFileAsync = promisify(execFile); export type PageDecompose = { page: number; text: string; images: Array<Buffer>; }; export type DecomposeOptions = { ocr?: boolean; }; // Interface pour les images retournées (buffers) export type Decompose = Array<PageDecompose>; /** * Extract text / images from pdf * @param bufferOrPath buffer or path of file * @returns TODO */ export async function decompose( bufferOrPath: Buffer | string, options?: DecomposeOptions ): Promise<Decompose> { const buffer = typeof bufferOrPath === 'string' ? getFileBuffer(bufferOrPath) : bufferOrPath; isFileValid(buffer); // Work in temp folder const packageDir = path.join(os.tmpdir(), 'pdf-decomposer'); // Clean old parsing if (fs.existsSync(packageDir)) fs.rmSync(packageDir, { recursive: true }); const tmpDir = path.join(packageDir, randomUUID()); fs.mkdirSync(tmpDir, { recursive: true }); const tmpFilePath = path.join(tmpDir, 'original.pdf'); fs.writeFileSync(tmpFilePath, buffer); const nbPages = await getPdfPageCount(tmpFilePath); if (options?.ocr) { // Convert pdf to png if using OCR const pdfToPngDir = path.join(tmpDir, 'pdf-to-png'); if (!fs.existsSync(pdfToPngDir)) fs.mkdirSync(pdfToPngDir); await execFileAsync('pdftoppm', [ '-png', tmpFilePath, path.join(pdfToPngDir, 'page'), ]); } const pages: Array<PageDecompose> = []; for (let i = 1; i <= nbPages; i++) { const [text, images] = await Promise.all([ extractPageText(tmpFilePath, i, options), extractPageImages(tmpFilePath, i), ]); pages.push({ page: i, text, images, }); } // 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: string): Buffer { if (!fs.existsSync(path)) throw new Error(`File ${path} does not exist`); if (!fs.lstatSync(path).isFile()) throw new Error(`${path} is not a file`); return fs.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: Buffer): void { 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: Buffer): boolean { 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: string): Promise<number> { const { stdout } = await execFileAsync('pdfinfo', [pdfPath]); const match = stdout.match(/Pages:\s+(\d+)/); return match ? parseInt(match[1], 10) : 0; } /** * 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: string, pageIndex: number, options?: DecomposeOptions ): Promise<string> { if (!options?.ocr) { const { stdout } = await execFileAsync('pdftotext', [ '-f', `${pageIndex}`, '-l', `${pageIndex}`, '-layout', pdfPath, '-', // <--- important : "-" = use stdout ]); return stdout; } const imagePath = path.join( path.dirname(pdfPath), 'pdf-to-png', `page-${pageIndex}.png` ); if (!fs.existsSync(imagePath)) throw new Error(`Page ${pageIndex} image not found`); try { const { stdout } = await execFileAsync('tesseract', [imagePath, 'stdout']); // 10 Mo max buffer return stdout.trim(); } catch { throw new Error(`Error OCR on page ${pageIndex}`); } } /** * 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: string, pageIndex: number ): Promise<Array<Buffer>> { const tmpDir = path.join(path.dirname(pdfPath), 'img-extract'); const outputPrefix = path.join(tmpDir, `page-${pageIndex}-img`); if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir); await execFileAsync('pdfimages', [ '-f', `${pageIndex}`, '-l', `${pageIndex}`, '-png', pdfPath, outputPrefix, ]); const imageFiles = fs .readdirSync(tmpDir) .filter((name) => name.startsWith(`page-${pageIndex}-img`)) .map((name) => path.join(tmpDir, name)); return imageFiles.map((file) => fs.readFileSync(file)); }