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
JavaScript
;
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));
}