UNPKG

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
#!/usr/bin/env node "use strict"; 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 }