DEV Community

sunshey
sunshey

Posted on • Originally published at smartontools.blogspot.com

How to Convert PDF Pages to Images in the Browser (No Server Uploads)

I needed to pull a chart from a PDF report and stick it into a slide deck last week. Screenshot looked terrible. Copy-paste from the PDF viewer? Worse.

The clean way is converting PDF pages to actual image files — PNG or JPEG. And doing it in the browser means the document never touches a server.

In this post, I'll show you how to render PDF pages to images client-side using Mozilla's PDF.js, with code I've been running in production for a while.


The Stack: pdfjs-dist

Mozilla's PDF.js is the standard for rendering PDFs in browsers. It parses PDF structure, renders pages to a <canvas> element, and lets you export that canvas as an image.

npm install pdfjs-dist
Enter fullscreen mode Exit fullscreen mode

The library uses a Web Worker for parsing (so the main thread stays responsive), which is great until you try to bundle it with Vite and the worker file 404s in production. More on that later.


Basic Conversion: One Page to PNG

Here's the simplest possible flow: load a PDF, render page 1 to a canvas, export as PNG.

import * as pdfjsLib from 'pdfjs-dist';
import PdfWorker from 'pdfjs-dist/build/pdf.worker?worker';

// Initialize worker
pdfjsLib.GlobalWorkerOptions.workerPort = new PdfWorker();

async function pdfPageToImage(file, pageNumber = 1, scale = 2) {
  const arrayBuffer = await file.arrayBuffer();
  const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;

  const page = await pdf.getPage(pageNumber);

  // Set up canvas
  const viewport = page.getViewport({ scale });
  const canvas = document.createElement('canvas');
  const context = canvas.getContext('2d');

  canvas.width = viewport.width;
  canvas.height = viewport.height;

  // Render PDF page to canvas
  await page.render({
    canvasContext: context,
    viewport: viewport
  }).promise;

  // Export as PNG
  const blob = await new Promise(resolve => 
    canvas.toBlob(resolve, 'image/png')
  );

  return blob;
}

// Usage
const imageBlob = await pdfPageToImage(file, 1, 2);
downloadBlob(imageBlob, 'page-1.png');
Enter fullscreen mode Exit fullscreen mode

What's happening:

  1. Load the PDF into pdfjs-dist
  2. Get the specific page
  3. Create a canvas sized to the page dimensions (multiplied by scale)
  4. Render the PDF page onto the canvas
  5. Convert the canvas to a PNG blob

The scale parameter is the key quality lever. scale: 1 gives you 72 DPI (screen resolution). scale: 2 doubles both dimensions, effectively giving you 144 DPI. For print-quality output, you'd use scale: 4 (288 DPI).


DPI Control: Matching Output to Use Case

PDF.js doesn't have a direct "DPI" setting. It works in CSS pixels, where 1 unit = 1/96 inch. To hit specific DPI targets:

function getScaleForDPI(targetDPI) {
  // PDF default is 72 DPI
  // scale = targetDPI / 72
  return targetDPI / 72;
}

const scales = {
  screen: getScaleForDPI(96),   // 1.33
  standard: getScaleForDPI(150), // 2.08
  print: getScaleForDPI(300),    // 4.17
};
Enter fullscreen mode Exit fullscreen mode

Practical impact on file size:

Scale Effective DPI 8.5x11" Page Size Typical PNG Size
1.33 96 816x1056 px ~200KB
2.08 150 1275x1650 px ~500KB
4.17 300 2550x3300 px ~2MB

I learned the hard way that rendering every page at 300 DPI produces massive files. Most of my users just need 150 DPI for slides and documents. 300 DPI is overkill unless you're sending something to a print shop.


Format Choice: PNG vs JPEG

Once you have the canvas, you can export to either format:

// PNG — lossless, sharp text, larger files
const pngBlob = await new Promise(resolve => 
  canvas.toBlob(resolve, 'image/png')
);

// JPEG — compressed, smaller files, configurable quality
const jpegBlob = await new Promise(resolve => 
  canvas.toBlob(resolve, 'image/jpeg', 0.85) // 85% quality
);
Enter fullscreen mode Exit fullscreen mode

My heuristic:

  • Documents with fine text, diagrams, or line art → PNG. JPEG compression artifacts ruin crisp edges.
  • Photo-heavy PDFs (brochures, magazines) → JPEG at 85-90% quality. File size is 3-5x smaller with no visible loss.
  • Mixed content → PNG if under 10 pages, JPEG if larger (email-friendly).

Batch Conversion: Multiple Pages

Rendering one page is easy. Rendering all 50 pages without freezing the UI is harder.

async function convertAllPages(file, format = 'png', scale = 2) {
  const arrayBuffer = await file.arrayBuffer();
  const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;

  const images = [];

  for (let i = 1; i <= pdf.numPages; i++) {
    const page = await pdf.getPage(i);
    const viewport = page.getViewport({ scale });

    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    canvas.width = viewport.width;
    canvas.height = viewport.height;

    await page.render({ canvasContext: context, viewport }).promise;

    const mimeType = format === 'png' ? 'image/png' : 'image/jpeg';
    const blob = await new Promise(resolve => 
      canvas.toBlob(resolve, mimeType, 0.9)
    );

    images.push({
      name: `page-${i}.${format}`,
      blob
    });

    // Yield to UI thread between pages
    await new Promise(r => setTimeout(r, 0));
  }

  return images;
}
Enter fullscreen mode Exit fullscreen mode

The setTimeout(..., 0) is critical. Without it, the browser locks up for the entire duration. With 50 pages at high DPI, that's 5-10 seconds of frozen UI. Yielding between pages keeps the tab responsive and lets you show a progress bar.

For very large documents (200+ pages), I use requestIdleCallback to render only when the browser isn't busy:

function renderWithIdleCallback(pdf, pageNumber, scale) {
  return new Promise((resolve) => {
    requestIdleCallback(async () => {
      const page = await pdf.getPage(pageNumber);
      // ... render logic
      resolve(blob);
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Memory Management: The Canvas Problem

Here's a trap I fell into: creating 50 canvases and holding them all in memory.

Each canvas at scale: 4 for a letter-size page is about 2MB of pixel data. 50 pages = 100MB of canvas buffers. On a mobile browser, that's an out-of-memory crash waiting to happen.

Fix: Convert to blob and immediately discard the canvas.

// BAD: keeps all canvases in memory
const canvases = [];
for (const page of pages) {
  const canvas = await renderPage(page);
  canvases.push(canvas); // Memory hoarding
}

// GOOD: render, export, release
for (const page of pages) {
  const canvas = await renderPage(page);
  const blob = await canvasToBlob(canvas);

  // Release canvas memory immediately
  canvas.width = 0;
  canvas.height = 0;

  saveBlob(blob);
}
Enter fullscreen mode Exit fullscreen mode

Setting canvas.width = 0 forces the browser to release the pixel buffer. Without this, the garbage collector might not run until much later — or until the tab crashes.


The Worker File 404 (Vite Users)

I mentioned this earlier. In development, pdfjs-dist's worker loads fine. In production, Vite's bundler moves or renames the worker file.

Fix for Vite:

import PdfWorker from 'pdfjs-dist/build/pdf.worker?worker';
pdfjsLib.GlobalWorkerOptions.workerPort = new PdfWorker();
Enter fullscreen mode Exit fullscreen mode

If that fails in production, pin the worker path explicitly:

import workerSrc from 'pdfjs-dist/build/pdf.worker.js?url';
pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
Enter fullscreen mode Exit fullscreen mode

Performance: Real Numbers

I tested batch conversion on a 20-page, 12MB PDF:

Scale Format Time Output Size
1.33 (96 DPI) PNG 2.1s 4.2MB total
2.08 (150 DPI) PNG 3.8s 11MB total
2.08 (150 DPI) JPEG 85% 3.5s 3.1MB total
4.17 (300 DPI) JPEG 90% 8.2s 12MB total

For most real-world use (slides, documents, web), 150 DPI JPEG at 85% quality is the sweet spot.


Try the Live Tool

If you want to convert PDF pages to images without writing code:

👉 en.sotool.top/pdf-to-image

Free, no signup, browser-based. Pick your pages, choose format and DPI, download as ZIP.

The source code is open source if you want to see the full Vue 3 + PDF.js integration.


Have you built browser-based document tools? How do you handle large batch conversions or memory limits?

Top comments (0)