DEV Community

zepubo-code
zepubo-code

Posted on

How to Build a Client-Side PDF to JPG Converter (No Server Required)

Most online PDF converters work the same way: you upload your file, their server processes it, and you download the result. Simple — but it means your document passes through someone else's infrastructure. For sensitive files like contracts, invoices, or ID scans, that's a real concern.

In this post, I'll show you how to build a PDF to JPG converter that runs entirely in the browser using PDF.js. No server, no uploads, no backend — just JavaScript.

Why Client-Side?

When you process files in the browser:

  • The file never leaves the user's device
  • No storage costs or server infrastructure needed
  • Works offline once the page loads
  • Faster for the user (no upload/download round trip)

The tradeoff is that client-side processing is limited by the user's device CPU and RAM. For most use cases — converting a few PDF pages to images — this is completely fine.

What We'll Use

  • PDF.js — Mozilla's open-source PDF rendering library
  • Canvas API — built into every modern browser
  • File API — for reading the user's local file

No npm, no build tools. Just vanilla JS.

The Core Concept

PDF.js renders PDF pages onto an HTML <canvas> element. Once a page is on a canvas, you can export it as a JPG using canvas.toDataURL('image/jpeg'). That's it — the whole trick.

PDF File → PDF.js renders to Canvas → Canvas exports as JPG → User downloads
Enter fullscreen mode Exit fullscreen mode

Step 1: Load PDF.js

<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script>
  pdfjsLib.GlobalWorkerOptions.workerSrc =
    'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
</script>
Enter fullscreen mode Exit fullscreen mode

Step 2: Read the PDF File

const fileInput = document.getElementById('fileInput');

fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file || file.type !== 'application/pdf') return;

  const arrayBuffer = await file.arrayBuffer();
  const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;

  console.log(`PDF loaded: ${pdf.numPages} pages`);
  convertAllPages(pdf);
});
Enter fullscreen mode Exit fullscreen mode

Step 3: Render Each Page to Canvas

async function convertAllPages(pdf) {
  for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
    const page = await pdf.getPage(pageNum);
    const jpgDataUrl = await renderPageToJPG(page, 2.0); // scale = quality
    downloadImage(jpgDataUrl, `page-${pageNum}.jpg`);
  }
}

async function renderPageToJPG(page, scale = 1.5) {
  const viewport = page.getViewport({ scale });

  const canvas = document.createElement('canvas');
  canvas.width = viewport.width;
  canvas.height = viewport.height;

  const ctx = canvas.getContext('2d');

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

  // White background (JPG doesn't support transparency)
  const finalCanvas = document.createElement('canvas');
  finalCanvas.width = canvas.width;
  finalCanvas.height = canvas.height;
  const finalCtx = finalCanvas.getContext('2d');
  finalCtx.fillStyle = '#ffffff';
  finalCtx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
  finalCtx.drawImage(canvas, 0, 0);

  return finalCanvas.toDataURL('image/jpeg', 0.92); // 0.92 = quality
}
Enter fullscreen mode Exit fullscreen mode

Note the white background step. Canvas is transparent by default. If you export directly to JPG without filling the background first, transparent areas become black. Always fill white before exporting as JPG.

Step 4: Trigger the Download

function downloadImage(dataUrl, filename) {
  const link = document.createElement('a');
  link.href = dataUrl;
  link.download = filename;
  link.click();
}
Enter fullscreen mode Exit fullscreen mode

Putting It All Together

Here's the minimal full implementation:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>PDF to JPG Converter</title>
</head>
<body>
  <h1>PDF to JPG Converter</h1>
  <input type="file" id="fileInput" accept=".pdf" />
  <p id="status"></p>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
  <script>
    pdfjsLib.GlobalWorkerOptions.workerSrc =
      'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';

    document.getElementById('fileInput').addEventListener('change', async (e) => {
      const file = e.target.files[0];
      if (!file) return;

      const status = document.getElementById('status');
      status.textContent = 'Loading PDF...';

      const arrayBuffer = await file.arrayBuffer();
      const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;

      status.textContent = `Converting ${pdf.numPages} page(s)...`;

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

        const canvas = document.createElement('canvas');
        canvas.width = viewport.width;
        canvas.height = viewport.height;
        await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;

        // Add white background
        const out = document.createElement('canvas');
        out.width = canvas.width;
        out.height = canvas.height;
        const ctx = out.getContext('2d');
        ctx.fillStyle = '#fff';
        ctx.fillRect(0, 0, out.width, out.height);
        ctx.drawImage(canvas, 0, 0);

        const link = document.createElement('a');
        link.href = out.toDataURL('image/jpeg', 0.92);
        link.download = `page-${i}.jpg`;
        link.click();
      }

      status.textContent = 'Done!';
    });
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Quality vs File Size

The scale parameter controls output resolution:

Scale Output Quality Use Case
1.0 Screen quality Quick preview
1.5 Good quality Web use
2.0 High quality Print/archive
3.0 Very high Large format print

The second argument to toDataURL('image/jpeg', quality) controls JPEG compression, from 0.0 (lowest) to 1.0 (highest). 0.92 is a good balance.

Limitations to Be Aware Of

Large PDFs: Converting a 100-page PDF will trigger 100 downloads. You'd want to zip them first using something like JSZip.

Password-protected PDFs: PDF.js supports these — pass the password in getDocument({ data, password }).

Complex PDFs: Some PDFs with unusual fonts or vector graphics may not render perfectly. PDF.js handles the vast majority correctly but it's not 100% identical to Acrobat.

Memory: Very large pages at high scale can consume significant RAM. Consider adding a max scale cap for large documents.

Real-World Implementation

If you want to see this in action without building it yourself, I built a free browser-based PDF to JPG converter at OneWeeb that handles all the edge cases above — multiple page download as ZIP, quality selection, and mobile support. Your files never leave your device.

Wrapping Up

Client-side file conversion is underused. Most developers reach for a backend or a third-party API because that's the familiar pattern — but for many conversion tasks, the browser has everything you need.

PDF.js is mature, well-maintained by Mozilla, and handles the hard parts of PDF parsing. The Canvas API handles rendering. You just need to wire them together.

Give it a try — your users will appreciate not having their files uploaded to a random server.


Have you built any other client-side file processing tools? I'd love to hear what you've made in the comments.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.