pdf-to-png-converter
Version:
Node.js utility to convert PDF file/buffer pages to PNG files/buffers. No build-time compilation required — pre-built native binaries included for all major platforms.
101 lines (100 loc) • 6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPdfFileBuffer = getPdfFileBuffer;
const node_fs_1 = require("node:fs");
function rejectOversized(byteLength, maxInputBytes) {
if (byteLength > maxInputBytes) {
throw new Error(`Input PDF exceeds maxInputBytes (${byteLength} > ${maxInputBytes} bytes)`);
}
}
/** An object exposing a finite numeric `length` — the shape pdf.js itself accepts as byte data. */
function isByteArrayLike(value) {
return typeof value === 'object' && value !== null && Number.isFinite(value.length);
}
/**
* Normalizes every supported input shape to a `Uint8Array` that pdfjs may safely transfer
* (detach). Closing the return type here means this module is the single owner of "what shape we
* hand pdfjs" — downstream seams (`getPdfDocument`, the worker dispatch) take a `Uint8Array` and
* re-derive nothing.
*/
async function getPdfFileBuffer(pdfFile, maxInputBytes) {
if (typeof pdfFile === 'string') {
const stats = await node_fs_1.promises.stat(pdfFile);
if (!stats.isFile()) {
throw new Error(`Input PDF path is not a regular file: ${pdfFile}`);
}
rejectOversized(stats.size, maxInputBytes);
const buffer = await node_fs_1.promises.readFile(pdfFile);
// Post-read re-check: closes the TOCTOU window between stat() and readFile().
// If the file was replaced or grew between the two calls, the buffer may exceed
// maxInputBytes — reject it before it propagates further into pdfjs parsing.
rejectOversized(buffer.byteLength, maxInputBytes);
if (buffer instanceof ArrayBuffer) {
// Fresh allocation owned by this call — a full-span view is safe to hand over.
return new Uint8Array(buffer);
}
if (Buffer.isBuffer(buffer)) {
// Zero-copy handoff. This Buffer was freshly allocated by readFile and is never
// exposed to the caller, so pdfjs may safely transfer (detach) its underlying
// ArrayBuffer — unlike the caller-owned branches below, no defensive copy is needed.
// readFile allocates non-pooled memory today; the full-span guard protects against
// any future pooled allocation whose ArrayBuffer is shared with unrelated data.
// Empty (or detached, byteLength 0) buffers take the copy path so pdfjs raises its
// clear "empty PDF" error instead of an opaque constructor TypeError.
if (buffer.byteLength > 0 && buffer.byteOffset === 0 && buffer.byteLength === buffer.buffer.byteLength) {
return new Uint8Array(buffer.buffer);
}
return new Uint8Array(buffer);
}
throw new Error(`Unsupported buffer type: ${Object.prototype.toString.call(buffer)}`);
}
if (Buffer.isBuffer(pdfFile)) {
rejectOversized(pdfFile.byteLength, maxInputBytes);
return new Uint8Array(pdfFile);
}
rejectOversized(pdfFile.byteLength, maxInputBytes);
// Defensive copy. pdfjs `getDocument()` lists the input's underlying ArrayBuffer as a
// transferable, which DETACHES it (byteLength → 0) when the data is a full-span Uint8Array.
// Returning the caller's buffer by reference would therefore neuter their input and break
// reuse across calls. The string-path and Node-Buffer branches above already allocate fresh
// memory; copy the Uint8Array / ArrayBuffer branch too so every supported input shape leaves
// the caller's buffer intact.
if (pdfFile instanceof Uint8Array) {
return Uint8Array.from(pdfFile);
}
if (pdfFile instanceof ArrayBuffer) {
return new Uint8Array(pdfFile.slice(0));
}
if (pdfFile instanceof SharedArrayBuffer) {
// `slice()` would yield another SharedArrayBuffer (non-transferable, shared) — copy into a
// fresh, regular-ArrayBuffer-backed Uint8Array so downstream pdfjs always receives
// unshared memory. `new Uint8Array(sab)` only views the SAB, so copy via `Uint8Array.from`.
return Uint8Array.from(new Uint8Array(pdfFile));
}
// The branches below are unreachable for TypeScript callers — the parameter type is exhausted
// above — but `instanceof` is realm-bound, so genuine byte containers created in another realm
// land here. pdf.js accepted all of these before this seam closed the union (`getDataProp`
// takes anything satisfying `ArrayBuffer.isView` or having a numeric `length`, and the old
// loader coerced the rest with `new Uint8Array(value)`), so they must keep converting.
const candidate = pdfFile;
// Cross-realm typed arrays and `DataView`s — e.g. built inside `node:vm` or `isolated-vm`,
// where a real `Uint8Array` fails `instanceof` while still satisfying the declared type.
if (ArrayBuffer.isView(candidate)) {
return Uint8Array.from(new Uint8Array(candidate.buffer, candidate.byteOffset, candidate.byteLength));
}
// Cross-realm `ArrayBuffer`, for the same reason.
if (Object.prototype.toString.call(candidate) === '[object ArrayBuffer]') {
return Uint8Array.from(new Uint8Array(candidate));
}
// Array-likes of byte values — most commonly a Node `Buffer` that round-tripped through JSON
// as `{ type: 'Buffer', data: number[] }` and reaches us as that `data` array. `byteLength` is
// `undefined` on these, so the size cap above skipped them; apply it to `length` here.
if (isByteArrayLike(candidate)) {
rejectOversized(candidate.length, maxInputBytes);
return Uint8Array.from(candidate);
}
// Genuinely unsupported. Reject here with the same message the file-path branch uses, rather
// than passing the value through for pdfjs to reject: this module owns the input contract, so
// an unsupported shape must not escape it.
throw new Error(`Unsupported buffer type: ${Object.prototype.toString.call(pdfFile)}`);
}