DEV Community

Cover image for PDF to JPG/PNG in the Browser: Rendering Pages with PDF.js Without Uploading the Document
Muhaymin Bin Mehmood
Muhaymin Bin Mehmood

Posted on

PDF to JPG/PNG in the Browser: Rendering Pages with PDF.js Without Uploading the Document

"Convert this PDF to PNG" sounds like an image conversion task.

But a PDF page is not necessarily an image.

It can contain:

  • vector text
  • fonts
  • shapes
  • embedded images
  • clipping paths
  • transparency
  • annotations
  • multiple layers of drawing instructions

So a PDF → PNG workflow usually has to render the page, not simply extract an image from it.

That difference explains almost everything about how browser-side PDF-to-image tools work.


Mental model: render first, encode second

The pipeline is:

PDF bytes
   ↓
Parse document
   ↓
Load page
   ↓
Create viewport
   ↓
Render page to Canvas
   ↓
Encode Canvas as JPG / PNG
   ↓
Download
Enter fullscreen mode Exit fullscreen mode

Mozilla's PDF.js is a web-standards-based PDF parsing and rendering platform.

Its examples show exactly this kind of browser rendering workflow.


Load a PDF

Assume the user selects a local PDF:

<input
  id="pdfInput"
  type="file"
  accept="application/pdf"
/>
Enter fullscreen mode Exit fullscreen mode

Read the bytes:

const file =
  pdfInput.files[0];

const arrayBuffer =
  await file.arrayBuffer();
Enter fullscreen mode Exit fullscreen mode

Then load the document with PDF.js:

const loadingTask =
  pdfjsLib.getDocument({
    data: arrayBuffer
  });

const pdf =
  await loadingTask.promise;

console.log(
  pdf.numPages
);
Enter fullscreen mode Exit fullscreen mode

Nothing has been uploaded.

The PDF bytes can stay inside the browser.


Render one page

Let's render page 1.

const page =
  await pdf.getPage(1);
Enter fullscreen mode Exit fullscreen mode

PDF.js uses a viewport to determine rendering dimensions.

const viewport =
  page.getViewport({
    scale: 2
  });
Enter fullscreen mode Exit fullscreen mode

Create a Canvas:

const canvas =
  document.createElement(
    "canvas"
  );

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

canvas.width =
  Math.ceil(viewport.width);

canvas.height =
  Math.ceil(viewport.height);
Enter fullscreen mode Exit fullscreen mode

Then render:

await page.render({
  canvasContext: ctx,
  viewport
}).promise;
Enter fullscreen mode Exit fullscreen mode

At this point, the composed PDF page has become pixels in a Canvas.


Why scale matters

Suppose the PDF page's base viewport is approximately:

612 × 792
Enter fullscreen mode Exit fullscreen mode

At:

scale: 1
Enter fullscreen mode Exit fullscreen mode

you render around:

612 × 792
Enter fullscreen mode Exit fullscreen mode

At:

scale: 2
Enter fullscreen mode Exit fullscreen mode

you render around:

1224 × 1584
Enter fullscreen mode Exit fullscreen mode

At:

scale: 3
Enter fullscreen mode Exit fullscreen mode

you render around:

1836 × 2376
Enter fullscreen mode Exit fullscreen mode

Higher scale means:

  • more pixels
  • potentially sharper output
  • larger files
  • more memory usage
  • more rendering time

So "quality" is not only an encoder setting.

For a PDF page, render resolution itself is a major quality decision.


Export as PNG

Once the page is on Canvas:

const blob =
  await new Promise(
    (resolve, reject) => {
      canvas.toBlob(
        (result) => {
          result
            ? resolve(result)
            : reject(
                new Error(
                  "PNG encoding failed"
                )
              );
        },
        "image/png"
      );
    }
  );
Enter fullscreen mode Exit fullscreen mode

PNG is useful when:

  • text must remain crisp
  • screenshots/diagrams dominate
  • transparency matters
  • lossless output is preferred

Export as JPEG

For JPEG:

const blob =
  await new Promise(
    (resolve, reject) => {
      canvas.toBlob(
        (result) => {
          result
            ? resolve(result)
            : reject(
                new Error(
                  "JPEG encoding failed"
                )
              );
        },
        "image/jpeg",
        0.9
      );
    }
  );
Enter fullscreen mode Exit fullscreen mode

The third argument controls JPEG quality.

Example:

0.6 → smaller / more artifacts
0.8 → useful balance
0.9 → higher quality / larger
Enter fullscreen mode Exit fullscreen mode

JPEG is often better for photo-heavy PDF pages.


Download the page

const url =
  URL.createObjectURL(blob);

const link =
  document.createElement("a");

link.href = url;
link.download = "page-1.png";
link.click();

URL.revokeObjectURL(url);
Enter fullscreen mode Exit fullscreen mode

One page is easy.

The interesting engineering begins when the PDF has 100 pages.


Render a page range

Users rarely want to process every page blindly.

A simple range parser can start with input like:

1-3,7,10-12
Enter fullscreen mode Exit fullscreen mode

Conceptually:

function parsePageRange(
  input,
  maxPages
) {
  const pages = new Set();

  for (
    const part
    of input.split(",")
  ) {
    const token =
      part.trim();

    if (!token) continue;

    if (token.includes("-")) {
      const [a, b] =
        token
          .split("-")
          .map(Number);

      for (
        let page = a;
        page <= b;
        page++
      ) {
        if (
          page >= 1 &&
          page <= maxPages
        ) {
          pages.add(page);
        }
      }
    } else {
      const page =
        Number(token);

      if (
        page >= 1 &&
        page <= maxPages
      ) {
        pages.add(page);
      }
    }
  }

  return [...pages]
    .sort((a, b) => a - b);
}
Enter fullscreen mode Exit fullscreen mode

Then:

const selectedPages =
  parsePageRange(
    "1-3,7",
    pdf.numPages
  );
Enter fullscreen mode Exit fullscreen mode

Do not render 100 pages simultaneously

This is the easiest way to turn a browser tab into a memory problem.

Imagine each rendered page becomes:

2000 × 2800
Enter fullscreen mode Exit fullscreen mode

That is:

5.6 million pixels
Enter fullscreen mode Exit fullscreen mode

Raw RGBA memory alone can be roughly:

5.6M × 4 bytes
≈ 22.4 MB
Enter fullscreen mode Exit fullscreen mode

per canvas before counting PDF.js internals, encoded Blobs, JavaScript objects, and browser overhead.

Ten pages at once can become expensive quickly.

A better strategy is:

page
→ render
→ encode
→ store Blob / ZIP entry
→ release Canvas
→ next page
Enter fullscreen mode Exit fullscreen mode

That limits peak memory.


Sequential renderer

async function renderPages(
  pdf,
  pages,
  scale = 2
) {
  const results = [];

  for (const pageNumber of pages) {
    const page =
      await pdf.getPage(
        pageNumber
      );

    const viewport =
      page.getViewport({
        scale
      });

    const canvas =
      document.createElement(
        "canvas"
      );

    canvas.width =
      Math.ceil(
        viewport.width
      );

    canvas.height =
      Math.ceil(
        viewport.height
      );

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

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

    const blob =
      await new Promise(
        (resolve, reject) => {
          canvas.toBlob(
            (result) => {
              result
                ? resolve(result)
                : reject(
                    new Error(
                      "Encoding failed"
                    )
                  );
            },
            "image/png"
          );
        }
      );

    results.push({
      pageNumber,
      blob
    });

    canvas.width = 1;
    canvas.height = 1;

    page.cleanup?.();
  }

  return results;
}
Enter fullscreen mode Exit fullscreen mode

This is not necessarily the fastest possible implementation.

It is deliberately conservative.

For a browser tool, predictable memory behavior is often more valuable than maximum concurrency.


PDF → Image is not image extraction

This distinction is worth repeating.

Suppose a PDF page contains:

logo.svg-like vectors
+ selectable text
+ a photograph
+ a table
Enter fullscreen mode Exit fullscreen mode

Extracting the embedded photograph would give you only one asset.

Rendering the page gives you the complete visual composition.

That is why "PDF to PNG" normally means:

render the page into raster pixels.


Handling passwords and invalid PDFs

Production UX needs to account for:

  • encrypted PDFs
  • malformed files
  • zero-page edge cases
  • very large files
  • unsupported structures
  • rendering errors

Do not collapse all failures into:

Something went wrong
Enter fullscreen mode Exit fullscreen mode

Useful errors are part of the product.

For example:

This PDF requires a password.

Page 17 could not be rendered.

The requested render size is too large for this browser.
Enter fullscreen mode Exit fullscreen mode

Progress matters

A 70-page conversion should not feel frozen.

Useful progress:

Rendering page 23 of 70
Enter fullscreen mode Exit fullscreen mode

or:

33%
Enter fullscreen mode Exit fullscreen mode

Internally, the operation can report:

onProgress?.({
  completed: index + 1,
  total: pages.length
});
Enter fullscreen mode Exit fullscreen mode

If the user understands that work is happening, they are much less likely to refresh the page.


ZIP download is the natural multi-page output

If you render:

page-001.png
page-002.png
page-003.png
...
Enter fullscreen mode Exit fullscreen mode

asking the user to click 30 downloads is terrible UX.

The natural result is:

document-pages.zip
Enter fullscreen mode Exit fullscreen mode

containing all outputs.

This is one reason a finished PDF-to-image tool needs more than a 20-line PDF.js demo.

The rendering API is only the engine.

The product still needs:

  • page selection
  • quality controls
  • format controls
  • naming
  • progress
  • ZIP packaging
  • memory management
  • download UX

Why local processing is useful for PDFs

PDFs are often more sensitive than ordinary images.

They may contain:

  • invoices
  • internal documents
  • contracts
  • presentations
  • reports
  • customer records

When a PDF-to-image workflow can run locally, the document does not need to be uploaded merely to render its pages.

That is one of the reasons I like browser-side PDF rendering.

For the straightforward "I need these PDF pages as JPG/PNG" use case, I added a dedicated local workflow to BatchSet.

It supports page selection, JPG/PNG output, quality controls, and ZIP download.

If you need the rendered pages rather than the rendering code:

Convert PDF to JPG/PNG with BatchSet


Final takeaway

A PDF page is a document surface, not simply an image file.

The practical browser pipeline is:

parse
→ load page
→ choose render scale
→ render to Canvas
→ encode JPG/PNG
→ package outputs
Enter fullscreen mode Exit fullscreen mode

Once you understand that, the quality, memory, and performance trade-offs become much easier to reason about.

And if your goal is just to get the images, you can let the browser do the rendering without building the surrounding workflow yourself.

Top comments (0)