DriversRecommendedOutdated drivers can make a good PC feel brokenScan driver issues before chasing fixes manually.Scan NowOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix Now×
Skip to content
Headless Chrome

How to Screenshot a PDF in Headless Mode with Puppeteer

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Short answer: Puppeteer screenshots what Chromium has rendered, not PDF bytes. In headless shell, direct navigation to a PDF is explicitly unsupported, so page.goto('file.pdf') followed by page.screenshot() is not a reliable conversion. Render the selected PDF page into an HTML or canvas surface with a PDF renderer, wait for that surface to finish painting, and then capture it with Page.screenshot(). If you actually need to create a PDF from a web page, use Page.pdf() instead; it is the reverse operation.

What Puppeteer can—and cannot—capture

Page.screenshot() captures the current browser-rendered page and returns a Uint8Array by default. PNG is the default image type; you can also request JPEG or WebP, save to a path, capture the full page, or clip a rectangle. It does not rasterize an arbitrary PDF file by reading its bytes.

Page.pdf() generates a PDF from page content. Puppeteer’s own guide describes it as the API to use “For printing PDFs,” not as an input-PDF-to-image converter. It uses print CSS by default and waits for fonts by default.

The important runtime qualification is Puppeteer’s documented warning: headless shell mode does not support navigation to a PDF document. That statement is specific to headless shell; do not generalize it to every Chromium headless configuration or every embedded PDF viewer.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose the right workflow

Goal Correct workflow What Puppeteer does
Screenshot an existing PDF Load PDF bytes with a PDF renderer, paint one page into a DOM/canvas surface, wait for rendering, then call page.screenshot(). Captures the rendered surface.
Create a PDF from a web page Navigate to web content, optionally select screen media, then call page.pdf(). Produces the PDF.
Open a PDF URL directly in headless shell Do not rely on this path. Navigation to a PDF is unsupported in that mode.

Existing PDF to image: the reliable architecture

1. Fetch and validate the document

Fetch the PDF outside the page or through a controlled application endpoint. Check the HTTP response before attempting a render. A successful navigation response is not proof that a PDF viewer is available, and headless shell does not throw for every HTTP error status, so inspect status codes where your script performs navigation.

2. Render one page to a browser surface

Use a PDF rendering library that can decode the document and paint a chosen page into a canvas or other DOM element. The renderer is responsible for page selection, scale, rotation, encrypted-file handling, and malformed-PDF errors. The official Puppeteer material establishes the browser and screenshot APIs but does not validate a particular PDF.js initialization recipe, so treat your renderer’s own versioned documentation as authoritative for that integration.

3. Signal completion explicitly

Do not take the screenshot immediately after inserting a canvas. Have the rendering code set a marker such as window.__pdfPageReady = true or add a data-rendered="true" attribute. In Puppeteer, wait for that marker (and, if needed, for fonts or images) before capturing.

4. Capture the page or a clipped element

Capture the canvas or its wrapper with elementHandle.screenshot(), or use page.screenshot({fullPage:true}) when the rendered surface is intentionally the whole document. Use clip when you need a fixed region. Keep the browser viewport and device scale factor explicit so output dimensions are reproducible.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Puppeteer code for the capture stage

The following script is complete for the browser-capture portion. It expects an HTML rendering page at RENDER_PAGE_URL that accepts pdf and page query parameters, renders the selected PDF page, and sets data-rendered="true" on #pdf-page. That renderer-specific contract is deliberate: Puppeteer’s official documentation does not establish a tested PDF.js setup, and substituting an unverified initialization snippet would make the example misleading.

import puppeteer from 'puppeteer';

const pdfUrl = process.argv[2];
const pageNumber = Number(process.argv[3] || 1);
const output = process.argv[4] || 'page.png';

if (!pdfUrl) {
  throw new Error('Usage: node capture-pdf-page.mjs <pdf-url> [page] [output]');
}
if (!Number.isInteger(pageNumber) || pageNumber < 1) {
  throw new Error('Page number must be a positive integer');
}

const browser = await puppeteer.launch({headless: true});
try {
  const page = await browser.newPage();
  await page.setViewport({width: 1600, height: 1200, deviceScaleFactor: 2});

  const renderUrl = new URL(process.env.RENDER_PAGE_URL || 'http://127.0.0.1:3000/render.html');
  renderUrl.searchParams.set('pdf', pdfUrl);
  renderUrl.searchParams.set('page', String(pageNumber));

  const response = await page.goto(renderUrl, {waitUntil: 'domcontentloaded', timeout: 90000});
  if (!response || !response.ok()) {
    throw new Error(`Renderer returned HTTP ${response?.status() ?? 'no response'}`);
  }

  const surface = await page.waitForSelector('#pdf-page[data-rendered="true"]', {timeout: 90000});
  await page.evaluate(() => document.fonts?.ready);
  await surface.screenshot({path: output, type: 'png'});
  console.log(`Wrote ${output}`);
} finally {
  await browser.close();
}

Run it with npm install puppeteer, then node capture-pdf-page.mjs https://example.com/file.pdf 2 second-page.png. The URL must be permitted by your renderer’s network and security policy. For a local file, prefer a controlled server rather than enabling broad file access in Chromium.

Controlling quality, size, and page selection

Scale and dimensions

Set deviceScaleFactor for higher-density output, and set the viewport to the dimensions your renderer uses. A larger scale increases pixel dimensions and memory use. Keep the same values across runs when image diffs matter.

Element, full-page, or clip

  • Element screenshot: best when the renderer places exactly one page in a canvas wrapper.
  • fullPage:true: useful for a document surface whose height is already the rendered page height.
  • clip: useful for a known rectangle, but coordinates are CSS pixels and must match the current layout.

Output type

PNG is lossless and the default. JPEG can reduce file size when a solid background and photographic content make compression acceptable. WebP is available where your Puppeteer/Chromium version supports it; verify downstream consumers before choosing it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Multiple pages

Loop over page numbers and render one page at a time, writing distinct files. Reusing one browser and page is normally cheaper than launching a browser per page, but reset renderer state between documents. For very large PDFs, process in bounded batches to avoid retaining every canvas in memory.

When the direction is web page to PDF

If your source is HTML and the desired result is a PDF, use Page.pdf(); do not render a PDF and screenshot it. Puppeteer generates PDFs with print CSS by default. Call page.emulateMediaType('screen') first when screen styles are required. Printing can alter colors; -webkit-print-color-adjust can force exact colors in the page’s CSS.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({headless: true});
try {
  const page = await browser.newPage();
  await page.goto('https://example.com', {waitUntil: 'networkidle0', timeout: 90000});
  await page.emulateMediaType('screen');
  await page.pdf({path: 'page.pdf', format: 'A4', printBackground: true, margin: {
    top: '12mm', right: '12mm', bottom: '12mm', left: '12mm'
  }});
} finally {
  await browser.close();
}

Common failures and fixes

“Navigation to PDF failed” or a blank viewer

Cause: direct PDF navigation in headless shell, or a viewer that requires unavailable browser UI. Fix: fetch the bytes and use a PDF renderer that paints into your own page, then screenshot that surface.

The screenshot is blank or only partly painted

Cause: capture happened before asynchronous page rendering completed. Fix: wait for an explicit renderer marker, canvas dimensions, fonts, and any required images. Avoid using a short arbitrary delay as the only readiness test.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

HTTP 404/403 is saved as an image

Cause: navigation status was not checked, or the server returned an HTML error page. Fix: inspect the response status and content type before handing data to the PDF renderer.

Wrong page or off-by-one result

Cause: your UI is one-based while the renderer API is zero-based (or the reverse). Fix: define one convention at the boundary and validate it with a known two-page document.

Fonts or colors differ from the source

Cause: fonts were not loaded, device scale changed, or print CSS was used accidentally. Fix: await document.fonts.ready, set the viewport and scale explicitly, and use screen media only when that matches your goal.

Out-of-memory or timeouts on large files

Cause: high-resolution canvases, many simultaneous pages, or a slow/remote PDF. Fix: lower scale, render one page at a time, cap concurrency, set a realistic timeout, and close pages promptly.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Security and reliability checklist

  • Allow only expected PDF origins; a PDF URL is untrusted input.
  • Set navigation and rendering timeouts, and abort work that exceeds them.
  • Keep Chromium and the renderer library updated through your normal dependency process.
  • Do not expose a renderer endpoint that lets arbitrary users access internal network addresses.
  • Record source URL, page number, scale, output type, status, and failure reason for reproducibility.

Or skip the browser setup

ScreenshotNeo provides a single HTTP request for a website URL, returning PNG, JPEG, WebP, or PDF. It is useful when your input is a web-rendered document or PDF viewer URL rather than raw PDF-byte decoding. Before capture it accepts cookie/consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing result in X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo documentation for options and authentication. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots, and yearly billing gives two months free. Create a free ScreenshotNeo account.

FAQ

Can Page.pdf() turn an existing PDF into a PNG?

No. It creates a PDF from rendered page content. Existing PDFs need a PDF renderer before Puppeteer can capture pixels.

Is headless shell the same as every headless mode?

No. The documented limitation is specifically for headless shell. Test the exact Chromium/Puppeteer mode you deploy, but keep the render-to-surface architecture for portability.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Which screenshot format should I archive?

Use PNG when exact text and line-art fidelity matter. Choose JPEG or WebP only when your consumers support them and the smaller files justify lossy or format-specific handling.

Frequently Asked Questions

Can Page.pdf() turn an existing PDF into a PNG?

No. It creates a PDF from rendered page content. Existing PDFs need a PDF renderer before Puppeteer can capture pixels.

Is headless shell the same as every headless mode?

No. The documented limitation is specifically for headless shell. Test the exact Chromium/Puppeteer mode you deploy, but keep the render-to-surface architecture for portability.

Which screenshot format should I archive?

Use PNG when exact text and line-art fidelity matter. Choose JPEG or WebP only when your consumers support them and the smaller files justify lossy or format-specific handling.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read next

Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.