# pdf-to-png-converter

<p align="center">
  <strong>🎯 Convert PDF pages to PNG images with no native compilation required</strong>
</p>

<p align="center">
  <a href="https://www.npmjs.com/package/pdf-to-png-converter">
    <img src="https://img.shields.io/npm/v/pdf-to-png-converter.svg?style=flat-square" alt="npm version">
  </a>
  <a href="https://www.npmjs.com/package/pdf-to-png-converter">
    <img src="https://img.shields.io/npm/dm/pdf-to-png-converter.svg?style=flat-square" alt="npm downloads">
  </a>
  <a href="https://github.com/dichovsky/pdf-to-png-converter/actions/workflows/test.yml">
    <img src="https://github.com/dichovsky/pdf-to-png-converter/actions/workflows/test.yml/badge.svg?branch=main" alt="Tests">
  </a>
  <a href="https://github.com/dichovsky/pdf-to-png-converter/blob/main/LICENSE">
    <img src="https://img.shields.io/github/license/dichovsky/pdf-to-png-converter?style=flat-square" alt="License">
  </a>
</p>

---

A high-performance Node.js library for converting PDF files and buffers to PNG images. Perfect for web applications, document processing pipelines, and image generation workflows.

**Key Benefits:**

- ✨ **No Build-Time Compilation** - Pre-built native binaries included via `@napi-rs/canvas`, no `node-gyp` or compiler toolchain required
- 🚀 **High Performance** - Supports parallel page processing
- 🔐 **Encrypted PDFs** - Handle password-protected documents
- 📦 **Lightweight** - Minimal dependencies
- 💪 **TypeScript Support** - Full type definitions included
- 🎨 **Flexible Rendering** - Advanced font and rendering options

> **Note:** `@napi-rs/canvas` ships platform-specific pre-built native binaries (no compilation step). See the [@napi-rs/canvas repository](https://github.com/Brooooooklyn/canvas) for the full list of supported platforms.

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [CLI Usage](#cli-usage)
- [API Reference](#api-reference)
- [Examples](#examples)
- [Performance Notes](#performance-notes)
- [Output Format](#output-format)
- [Migration Guide](#migration-guide)
- [Project Links](#project-links)
- [License](#license)

---

## Installation

### npm

```sh
npm install pdf-to-png-converter
```

### Yarn

```sh
yarn add pdf-to-png-converter
```

> **Node.js Requirement:** Node.js 22.13 or higher is required.

---

## Quick Start

Convert a PDF file to PNG images in just a few lines:

```javascript
const { pdfToPng } = require('pdf-to-png-converter');

(async () => {
    const pngPages = await pdfToPng('document.pdf', {
        outputFolder: './output',
    });

    console.log(`Converted ${pngPages.length} pages`);
})();
```

Or with TypeScript:

```typescript
import { pdfToPng, VerbosityLevel, type PngPageOutput } from 'pdf-to-png-converter';

const pngPages: PngPageOutput[] = await pdfToPng('document.pdf', {
    outputFolder: './output',
    verbosityLevel: VerbosityLevel.ERRORS, // 0=ERRORS, 1=WARNINGS, 5=INFOS
});
```

> **Existing files are not overwritten.** Disk writes use exclusive-create mode. Re-running a conversion with the same output filenames throws `EEXIST`; clear the target directory or generate unique filenames between runs.

---

## CLI Usage

You can use the converter directly from the terminal without writing code:

```sh
npx pdf-to-png-converter my-document.pdf --output-folder ./output
```

**Options:**

- `--output-folder <dir>`: Directory to save PNG files. Required for image conversion. Existing files are not overwritten; duplicate output filenames throw `EEXIST`.
- `--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.
- `--enable-xfa <true|false>`: Process XFA form data.
- `--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. This prints JSON to stdout and does not require `--output-folder`.
- `--process-pages-in-parallel`: Process pages concurrently.
- `--concurrency-limit <number>`: Maximum number of pages rendered simultaneously.
- `--silent`: Suppress normal output messages unless there is an error.
- `--version`: Show package version.
- `--help`: Show help text.

The CLI has two output modes:

- image conversion: writes PNG files to `--output-folder`
- metadata inspection: prints JSON metadata to stdout with `--return-metadata-only`

If you need in-memory PNG buffers, use the library API (`returnPageContent`) rather than the CLI.

---

## API Reference

### `pdfToPng(input, options?)`

Converts PDF pages to PNG images.

**Parameters:**

| Parameter | Type                                      | Description                                      |
| --------- | ----------------------------------------- | ------------------------------------------------ |
| `input`   | `string \| ArrayBufferLike \| Uint8Array` | PDF file path, ArrayBuffer, or Uint8Array/Buffer |
| `options` | `PdfToPngOptions`                         | Optional configuration object                    |

**Returns:** `Promise<PngPageOutput[]>` - Array of converted PNG pages

### Options

```typescript
{
    // Font & Rendering Options
    disableFontFace?: boolean,       // Disable font face rendering (default: true)
    useSystemFonts?: boolean,        // Use system fonts as fallback (default: false)
    enableXfa?: boolean,             // Render XFA forms (default: true)

    // Output Options
    outputFolder?: string,           // Directory to save PNG files; existing files are not overwritten
    outputFileMaskFunc?: (pageNumber: number) => string, // Custom filename function
                                     // Must return a flat filename. "/" is rejected on all platforms;
                                     // "\" is also rejected on Windows.

    // Rendering Options
    viewportScale?: number,          // PNG scale/zoom level (default: 1.0, max: 100)
                                     // Note: large pages can still hit the 100-million-pixel canvas limit
                                     // at scales well below 100. Reduce viewportScale if you get an error.

    // Security
    pdfFilePassword?: string,        // Password for encrypted PDFs
    maxInputBytes?: number,          // Max input PDF size in bytes (default: 256 * 1024 * 1024)
                                     // Path inputs are stat()'d before reading and non-regular files
                                     // (FIFOs, sockets, /dev/zero) are rejected. Buffer / Uint8Array
                                     // inputs are validated against the same cap by byteLength.

    // Processing
    pagesToProcess?: number[],       // 1-indexed integer pages to convert (e.g., [1, 3, 5])
                                    // Non-integer and <= 0 values throw; pages beyond the PDF length are ignored
    processPagesInParallel?: boolean, // Enable parallel processing (default: false)
    concurrencyLimit?: number,       // Max concurrent pages (parallel) / worker-pool size (worker
                                     // threads): integer 1..16 (default: 4). The upper bound caps
                                     // peak in-flight canvas memory at ~6.4 GiB.
    renderInWorkerThreads?: boolean, // Rasterize pages in a pool of worker threads (default: false)
                                     // True multi-core parallelism (pool size = concurrencyLimit); each
                                     // worker loads its own document copy. Pays off on multi-page,
                                     // render-heavy PDFs; identical pixels, ordered results.

    // Output Control
    returnPageContent?: boolean,     // Include PNG buffer in output (default: true)
    returnMetadataOnly?: boolean,    // Return only page dimensions/rotation without rendering (default: false)

    // Logging
    verbosityLevel?: VerbosityLevel, // VerbosityLevel.ERRORS | WARNINGS | INFOS (default: ERRORS)
                                      // Use the VerbosityLevel enum for readable values:
                                      // import { VerbosityLevel } from 'pdf-to-png-converter'
}
```

---

## Examples

### Basic Usage

```javascript
const { pdfToPng } = require('pdf-to-png-converter');

(async () => {
    const pngPages = await pdfToPng('document.pdf', {
        outputFolder: './output',
    });
    console.log(`Successfully converted ${pngPages.length} pages`);
})();
```

### Advanced Configuration

```typescript
import { pdfToPng, VerbosityLevel } from 'pdf-to-png-converter';

const pngPages = await pdfToPng('document.pdf', {
    // Rendering
    viewportScale: 2.0, // 2x zoom for higher resolution
    disableFontFace: false, // Use font face rendering
    useSystemFonts: true, // Fallback to system fonts

    // Output
    outputFolder: './pdf-images',
    outputFileMaskFunc: (pageNumber) => `page-${String(pageNumber).padStart(3, '0')}.png`,
    returnPageContent: true,

    // Performance
    processPagesInParallel: true,
    concurrencyLimit: 8,

    // Logging
    verbosityLevel: VerbosityLevel.WARNINGS, // Log warnings
});
```

### Convert Specific Pages

```javascript
const pngPages = await pdfToPng('document.pdf', {
    outputFolder: './output',
    pagesToProcess: [1, 3, 5], // Only convert first, third, and fifth pages
});
```

### Handle Encrypted PDFs

```javascript
const pngPages = await pdfToPng('protected.pdf', {
    outputFolder: './output',
    pdfFilePassword: 'mypassword',
});
```

### Convert from Buffer

```javascript
const fs = require('fs');
const { pdfToPng } = require('pdf-to-png-converter');

const pdfBuffer = fs.readFileSync('document.pdf');
const pngPages = await pdfToPng(pdfBuffer, {
    outputFolder: './output',
    outputFileMaskFunc: (pageNumber) => `page_${pageNumber}.png`,
});
```

### Memory-Efficient Processing

```javascript
// Without returning page content (saves memory for large PDFs)
const pngPages = await pdfToPng('large-document.pdf', {
    outputFolder: './output',
    returnPageContent: false, // Don't keep PNG buffers in memory
    processPagesInParallel: true, // Process multiple pages concurrently
    concurrencyLimit: 4,
});

// Pages are written to disk, content property will be undefined
pngPages.forEach((page) => {
    if (page.kind === 'file') {
        console.log(`Saved: ${page.path}`);
    }
});
```

### Get Page Metadata Only

```javascript
// Inspect page dimensions and rotation without rendering any images
const pages = await pdfToPng('document.pdf', {
    returnMetadataOnly: true,
});

pages.forEach((page) => {
    console.log(`Page ${page.pageNumber}: ${page.width}x${page.height}px, rotation=${page.rotation}`);
});
```

This is significantly faster than full rendering and useful for checking page counts, dimensions, or orientation before deciding how to process a document.

---

## Performance Notes

- **Pipelined processing.** PNG encoding runs on the libuv threadpool and overlaps page rendering, so files may finish writing out of page order even in default (non-parallel) mode. Always consume results via the resolved, page-ordered array rather than directory-watch order.
- **Threadpool sizing.** PNG encodes and disk writes share Node's libuv threadpool (4 threads by default). For parallel file-output workloads on many-core machines, raising it can help: `UV_THREADPOOL_SIZE=8 node app.js`.
- **Strict serial processing.** If you need exactly one page in flight at a time (minimal memory, strict on-disk ordering), use `processPagesInParallel: true` with `concurrencyLimit: 1` — a sliding window of exactly one page.
- **Multi-core rendering.** `processPagesInParallel` interleaves pages on one thread — rasterization itself never runs in parallel. For CPU-bound documents (large embedded images, complex vector art), `renderInWorkerThreads: true` rasterizes pages in a pool of worker threads instead (measured ~3× end-to-end on a 12-page image-heavy document with the default pool of 4). Cost: one PDF copy + one pdf.js instance of memory per worker, plus one extra copy of the PDF retained on the main thread for the duration of the conversion, and a few hundred ms of pool startup per conversion — so prefer it for multi-page, render-heavy work rather than small documents.

---

## Output Format

The `pdfToPng` function returns an array of discriminated page objects. Branch on `kind` before using mode-specific fields:

| `kind`     | When returned                    | `path`    | `content`                                       |
| ---------- | -------------------------------- | --------- | ----------------------------------------------- |
| `metadata` | `returnMetadataOnly: true`       | `''`      | `undefined`                                     |
| `content`  | Rendering without `outputFolder` | `''`      | PNG `Buffer`, unless `returnPageContent: false` |
| `file`     | Rendering with `outputFolder`    | File path | PNG `Buffer`, unless `returnPageContent: false` |

All output objects also include `pageNumber`, `name`, `width`, `height`, and `rotation`. `width` and `height` are integer pixel dimensions of the rendered image: a fractional viewport (for example a 595×842 pt A4 page at `viewportScale: 1.5`, i.e. 892.5×1263) is floored to match the bitmap the canvas allocates (892×1263). `returnMetadataOnly` reports the same floored dimensions a render would produce — and, for the same reason, rejects the same unrenderable pages a render would: a `viewportScale` that floors a page to `0` px, or one whose rendered (floored) canvas area exceeds the internal canvas pixel limit, throws the identical error on both paths rather than returning dimensions for a page that cannot be rendered.

```javascript
[
    {
        kind: 'content',
        pageNumber: 1,                      // Page number in the PDF
        name: 'document_page_1.png',        // PNG filename
        content: Buffer<...>,               // PNG image data
                                            //   undefined if returnPageContent=false
        path: '',                           // Empty string for in-memory and metadata results
        width: 612,                         // Image width in pixels (integer; floored from viewportScale)
        height: 792,                        // Image height in pixels (integer; floored from viewportScale)
        rotation: 0                         // Page rotation in degrees: 0, 90, 180, or 270
    },
    // ... more pages
]
```

```javascript
pngPages.forEach((page) => {
    if (page.kind === 'file') {
        console.log(page.path);
    }

    if (page.kind === 'content' && page.content) {
        console.log(page.content.byteLength);
    }
});
```

---

## Migration Guide

Version **4.0.0** introduced public and behavioral changes that existing consumers may need to adopt:

1. **`PngPageOutput` is now discriminated.** Branch on `page.kind` before reading `page.path` or assuming `page.content` is present.
2. **`verbosityLevel` is now typed as `VerbosityLevel`.** Prefer `VerbosityLevel.ERRORS`, `VerbosityLevel.WARNINGS`, or `VerbosityLevel.INFOS` instead of raw numeric literals.
3. **Invalid `pagesToProcess` values now throw early.** `0`, negative numbers, and non-integers are rejected immediately; page numbers above the document length are still ignored.
4. **Disk writes are now exclusive-create (`'wx'`).** Re-running the same conversion into the same output filenames now throws `EEXIST`; clear the target directory or generate unique filenames between runs.

See the [changelog](CHANGELOG.md) for the full release history.

---

## Project Links

- [Changelog](CHANGELOG.md)
- [Contributing Guide](CONTRIBUTING.md)
- [Security Policy](SECURITY.md)
- [Issue Tracker](https://github.com/dichovsky/pdf-to-png-converter/issues)

---

## License

MIT © [dichovsky](https://github.com/dichovsky)

## Buy Me A Coffee

In case you want to support my work:

[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/dichovsky)
