pdf.js-extract
Version:
super-simple async PDF reader that extracts text with x,y page positions based on pdf.js
143 lines (129 loc) • 3.93 kB
JavaScript
import { readFile } from "node:fs";
import { fileURLToPath } from "node:url";
import { join, sep, dirname } from "node:path";
import { createRequire } from "node:module";
import * as utils from "./utils.mjs";
// Polyfill for process.getBuiltinModule, added in Node 22.3.0 / 20.16.0.
// Node 21 is EOL and never received this backport; without it the bundled
// pdf.js fails to load CMaps (node_utils_fetchData throws) and CMap-encoded
// text (e.g. Chinese characters) is silently dropped from extraction results.
if (!process.getBuiltinModule) {
const _require = createRequire(import.meta.url);
process.getBuiltinModule = name => _require(name);
}
import { getDocument, GlobalWorkerOptions } from "./pdfjs/pdf.mjs";
import { getPageImages } from "./extraction/images.mjs";
import { getAttachments } from "./extraction/attachments.mjs";
import { getPageAnnotations } from "./extraction/annotations.mjs";
import { getPageContent } from "./extraction/content.mjs";
import { getMetadata } from "./extraction/metadata.mjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
GlobalWorkerOptions.workerSrc = new URL("./pdfjs/pdf.worker.mjs", import.meta.url).href;
class PDFExtract {
constructor() {
}
extract(filename, options, cb) {
if (!cb) {
return new Promise((resolve, reject) => {
this.extract(filename, options, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
readFile(filename, (err, buffer) => {
if (err) {
return cb(err);
}
return this.extractBuffer(buffer, options, (err, pdf) => {
if (err) {
return cb(err);
} else {
return cb(null, pdf);
}
});
});
}
extractBuffer(buffer, options = {}, cb) {
if (!cb) {
return this.extractBufferAsync(buffer, options);
}
this.extractBufferAsync(buffer, options)
.then(
pdf => {
cb(null, pdf);
},
err => cb(err)
);
}
async extractBufferAsync(buffer, options) {
const opts = {
verbosity: -1,
cMapUrl: join(__dirname, "cmaps") + sep, // trailing path separator is important
cMapPacked: true,
...options,
data: new Uint8Array(buffer)
};
const textExtractOptions = {
normalizeWhitespace: opts.normalizeWhitespace === true,
disableCombineTextItems: opts.disableCombineTextItems === true
};
const doc = await getDocument(opts).promise;
const pdf = {
meta: {},
pages: [],
info: {
numPages: doc.numPages,
fingerprints: doc.fingerprints.filter(fp => fp !== null)
}
};
const firstPage = Math.max(1, opts?.firstPage ?? 1);
const lastPage = Math.min(opts?.lastPage ?? doc.numPages, doc.numPages);
const getPageInfo = (pageNum, page) => {
const viewport = page.getViewport({ scale: 1.0 });
return {
num: pageNum,
scale: viewport.scale,
rotation: viewport.rotation,
offsetX: viewport.offsetX,
offsetY: viewport.offsetY,
width: viewport.width,
height: viewport.height,
view: { minX: page.view[0], minY: page.view[1], maxX: page.view[2], maxY: page.view[3] }
};
};
const getPage = async pageNum => {
const page = await doc.getPage(pageNum);
await page.getOperatorList();
const resultPage = {
info: getPageInfo(pageNum, page),
content: await getPageContent(page, textExtractOptions, opts.includeColors === true)
};
const annotations = await getPageAnnotations(page);
if (annotations) {
resultPage.annotations = annotations;
}
if (opts.includeImages) {
const images = await getPageImages(page);
if (images) {
resultPage.images = images;
}
}
pdf.pages.push(resultPage);
};
pdf.meta = await getMetadata(doc);
if (opts.includeAttachments) {
pdf.attachments = await getAttachments(doc);
}
for (let i = firstPage; i <= lastPage; i++) {
await getPage(i);
}
return pdf;
}
}
PDFExtract.utils = utils;
export { PDFExtract };