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.
281 lines (279 loc) • 11 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HELP_TEXT = void 0;
exports.parseBoolean = parseBoolean;
exports.parseNumberList = parseNumberList;
exports.buildPdfToPngOptions = buildPdfToPngOptions;
exports.executeConversion = executeConversion;
exports.getVersion = getVersion;
exports.run = run;
const node_util_1 = require("node:util");
const node_path_1 = __importDefault(require("node:path"));
const node_fs_1 = __importDefault(require("node:fs"));
const pdfToPngCore_js_1 = require("./pdfToPngCore.js");
const normalizePdfToPngOptions_js_1 = require("./normalizePdfToPngOptions.js");
/**
* Help text shown for `--help` and on invalid usage.
* Exported so it can be asserted in tests without running the full CLI pipeline.
*/
exports.HELP_TEXT = `Usage: pdf-to-png-converter <pdf-file-path> [options]
Options:
--output-folder <dir> Folder path where PNG files will be written (required unless --return-metadata-only)
--viewport-scale <number> Scale factor applied to each page viewport
--use-system-fonts Attempt to use fonts installed on the host system
--disable-font-face <true|false> Do not load embedded fonts (true/false)
--enable-xfa <true|false> Process XFA form data (true/false)
--pdf-file-password <pwd> Password for encrypted PDFs
--pages-to-process <n,m,...> Comma-separated list of 1-based page numbers
--verbosity-level <number> pdfjs verbosity level (0=errors, 1=warnings, 5=infos)
--return-metadata-only Return page metadata without rendering images
--process-pages-in-parallel Process pages concurrently
--concurrency-limit <number> Max concurrent pages (parallel) / worker-pool size (worker threads)
--render-in-worker-threads Rasterize pages in a pool of worker threads (multi-core)
--silent Suppress output unless there is an error
--version Show version
--help Show this help message`;
/**
* Shared `parseArgs` option schema.
* Declared `as const` so TypeScript infers precise `'string'` / `'boolean'` literal types
* for every flag and propagates them through the `parseArgs` return type.
*/
const CLI_OPTIONS = {
'output-folder': { type: 'string' },
'viewport-scale': { type: 'string' },
'use-system-fonts': { type: 'boolean' },
'disable-font-face': { type: 'string' },
'enable-xfa': { type: 'string' },
'pdf-file-password': { type: 'string' },
'pages-to-process': { type: 'string' },
'verbosity-level': { type: 'string' },
'return-metadata-only': { type: 'boolean' },
'return-page-content': { type: 'boolean' },
'process-pages-in-parallel': { type: 'boolean' },
'concurrency-limit': { type: 'string' },
'render-in-worker-threads': { type: 'boolean' },
silent: { type: 'boolean' },
version: { type: 'boolean' },
help: { type: 'boolean' },
};
/**
* Parses a CLI string value as a boolean.
*
* - `'true'` / `'1'` → `true`
* - `'false'` / `'0'` → `false`
* - `undefined` → `undefined` (flag not provided)
*
* @throws {Error} When the value is not a recognised boolean string.
*/
function parseBoolean(val) {
if (val === undefined)
return undefined;
if (val === 'true' || val === '1')
return true;
if (val === 'false' || val === '0')
return false;
throw new Error(`Invalid boolean value: "${val}". Expected true|false|1|0.`);
}
/**
* Parses a comma-separated string of integers into a `number[]`.
*
* Returns `undefined` when `val` is `undefined` (flag not provided).
*
* @throws {Error} When any token in the list is not a valid integer.
*/
function parseNumberList(val) {
if (val === undefined)
return undefined;
return val.split(',').map((token) => {
const trimmed = token.trim();
if (trimmed === '')
throw new Error('Invalid integer in list: empty value.');
const parsed = Number(trimmed);
if (!Number.isInteger(parsed))
throw new Error(`Invalid integer in list: "${trimmed}".`);
return parsed;
});
}
function parseNumericOption(value, errorMessage) {
if (value === undefined) {
return undefined;
}
if (value.trim() === '') {
throw new Error(errorMessage);
}
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
throw new Error(errorMessage);
}
return parsed;
}
function parseIntegerOption(value, errorMessage) {
if (value === undefined) {
return undefined;
}
if (value.trim() === '') {
throw new Error(errorMessage);
}
const parsed = Number(value);
if (!Number.isInteger(parsed)) {
throw new Error(errorMessage);
}
return parsed;
}
function safeParseArgs() {
try {
return (0, node_util_1.parseArgs)({ options: CLI_OPTIONS, allowPositionals: true });
}
catch (err) {
/* v8 ignore next */
console.error(err instanceof Error ? err.message : String(err));
console.error(exports.HELP_TEXT);
process.exit(1);
return null;
}
}
/**
* Parses raw CLI flags into a validated `NormalizedPdfToPngOptions` plus the positional
* `pdfFilePath`. Single source of normalization — downstream {@link executeConversion}
* passes the result straight to {@link pdfToPngCore} so the library does NOT re-normalize.
*
* The CLI only supports stdout-friendly metadata mode (`--return-metadata-only`) or
* file-writing image conversion (`--output-folder`). In-memory PNG buffers remain a
* library-only capability because the CLI does not serialize page results.
*/
function buildPdfToPngOptions(values, positionals) {
const pdfFilePath = positionals[0];
if (!pdfFilePath) {
throw new Error('<pdf-file-path> is required.');
}
const rawOptions = {
outputFolder: values['output-folder'],
viewportScale: parseNumericOption(values['viewport-scale'], '--viewport-scale must be a valid number.'),
useSystemFonts: values['use-system-fonts'],
disableFontFace: parseBoolean(values['disable-font-face']),
enableXfa: parseBoolean(values['enable-xfa']),
pdfFilePassword: values['pdf-file-password'],
pagesToProcess: parseNumberList(values['pages-to-process']),
verbosityLevel: parseIntegerOption(values['verbosity-level'], '--verbosity-level must be a valid integer.'),
returnMetadataOnly: values['return-metadata-only'],
returnPageContent: values['return-page-content'] ?? false,
processPagesInParallel: values['process-pages-in-parallel'],
concurrencyLimit: parseIntegerOption(values['concurrency-limit'], '--concurrency-limit must be a valid integer.'),
renderInWorkerThreads: values['render-in-worker-threads'],
};
const options = (0, normalizePdfToPngOptions_js_1.normalizePdfToPngOptions)(rawOptions);
if (values['return-page-content']) {
throw new Error('--return-page-content is not supported by the CLI. Use the library API if you need in-memory PNG buffers.');
}
if (!options.returnMetadataOnly && options.outputFolder === undefined) {
throw new Error('The CLI requires --output-folder for image conversion. Use --return-metadata-only for stdout-friendly page metadata.');
}
return { pdfFilePath, options };
}
async function executeConversion(pdfFilePath, options, logInfo, writeOutput = console.log) {
try {
const results = await (0, pdfToPngCore_js_1.pdfToPngCore)(pdfFilePath, options);
if (options.returnMetadataOnly) {
writeOutput(JSON.stringify(results, null, 2));
return;
}
logInfo(`Successfully processed ${results.length} page(s).`);
}
catch (err) {
throw new Error(err instanceof Error ? err.message : String(err), {
cause: err,
});
}
}
function createLogger(silent) {
return (...msgs) => {
if (!silent)
console.log(...msgs);
};
}
function handleRunError(err) {
if (err instanceof Error && err.cause !== undefined) {
console.error('Error:');
console.error(err.message);
}
else {
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
}
if (err instanceof Error && err.message === '<pdf-file-path> is required.') {
console.error(exports.HELP_TEXT);
}
process.exit(1);
}
/**
* Reads the package version from the adjacent `package.json`.
* Throws when `package.json` is missing or malformed so packaging defects surface immediately.
*/
function getVersion() {
try {
const pkgPath = node_path_1.default.resolve(__dirname, '../package.json');
const pkgInfo = JSON.parse(node_fs_1.default.readFileSync(pkgPath, 'utf-8'));
if (typeof pkgInfo.version !== 'string' || pkgInfo.version.length === 0) {
throw new Error('Cannot determine package version: package.json missing or malformed');
}
return pkgInfo.version;
}
catch {
throw new Error('Cannot determine package version: package.json missing or malformed');
}
}
/**
* Main CLI entry point.
*
* Parses `process.argv`, validates all options up-front through
* {@link normalizePdfToPngOptions} via {@link buildPdfToPngOptions}, and delegates to
* {@link pdfToPngCore}. Exported so it can be unit-tested without spawning a child process.
*/
async function run() {
const parseResult = safeParseArgs();
if (!parseResult)
return;
const { values, positionals } = parseResult;
if (values.help) {
console.log(exports.HELP_TEXT);
process.exit(0);
return;
}
if (values.version) {
try {
console.log(`v${getVersion()}`);
process.exit(0);
}
catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
return;
}
try {
const { pdfFilePath, options } = buildPdfToPngOptions(values, positionals);
const logInfo = createLogger(values.silent);
if (!options.returnMetadataOnly) {
logInfo(`Processing PDF: ${pdfFilePath}`);
if (options.outputFolder) {
logInfo(`Output folder: ${options.outputFolder}`);
}
}
await executeConversion(pdfFilePath, options, logInfo);
}
catch (err) {
handleRunError(err);
}
}
/* v8 ignore next 6 */
try {
if (node_fs_1.default.realpathSync(process.argv[1]) === node_fs_1.default.realpathSync(__filename)) {
void run();
}
}
catch {
// realpathSync can fail (e.g. path does not exist) — skip auto-execution
}