How to Convert a Puppeteer Screenshot to a PDF Page
Choose between a print-ready PDF and a PDF page containing the exact screenshot pixels, with runnable Puppeteer examples for both.
There are two ways to turn a Puppeteer page into a PDF, depending on what you mean by “convert a screenshot.” For a document-style PDF rendered with the page’s print styles, use page.pdf(). For a PDF page containing the exact pixels captured by page.screenshot(), capture an image and place it onto a PDF page with a PDF-writing library. page.screenshot() itself returns image bytes; it does not create a PDF.
This distinction determines whether the result follows print CSS and can paginate content, or preserves one captured view as an image. The examples below use Puppeteer and PDFKit for the image-to-PDF case.
1. Set up a runnable Puppeteer project
Use a current Node.js version and install Puppeteer. The package downloads a compatible browser for its standard installation.
mkdir puppeteer-pdf
cd puppeteer-pdf
npm init -y
npm install puppeteer pdfkit
Save the examples below as print-page.mjs and screenshot-to-pdf.mjs. Run either with node print-page.mjs https://example.com or node screenshot-to-pdf.mjs https://example.com.
These examples accept an optional URL argument and use https://example.com when none is given. Only pass URLs you are authorized to access.
2. Make a document-style PDF with page.pdf()
Use this when you want a PDF rendering of the page, with print CSS, pagination, and configurable paper settings. Puppeteer’s PDF method uses the print CSS media type by default.
import puppeteer from 'puppeteer';
const url = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
await page.locator('body').wait();
await page.pdf({
path: 'page.pdf',
format: 'A4',
printBackground: true,
margin: { top: '12mm', right: '12mm', bottom: '12mm', left: '12mm' },
preferCSSPageSize: true,
waitForFonts: true,
});
} finally {
await browser.close();
}
domcontentloaded only means the initial document has been parsed. It does not guarantee that a client-rendered app, remote images, or late-loaded data is ready. Replace the readiness step with an application-specific condition when needed, as shown in the section on waiting for content.
Choose print or screen styling
Print CSS is the default for page.pdf(). If the PDF should use screen styles instead, emulate screen media before generating it:
await page.emulateMediaType('screen');
await page.pdf({ path: 'page.pdf', printBackground: true });
To keep print media, omit that call. If colors or backgrounds matter, use printBackground: true and consider the CSS rule -webkit-print-color-adjust: exact on the relevant elements.
Configure the page size and layout
The PDFOptions interface supports these main controls:
| Option | What it controls | When to set it |
|---|---|---|
format |
Named paper size; Letter is the default. | Use a standard such as A4 or Letter. |
width, height |
Custom paper dimensions. | Set both when the output needs a nonstandard page size. |
landscape |
Landscape orientation. | Use for wide tables, dashboards, or diagrams. |
margin |
Top, right, bottom, and left margins. | Set explicitly when printable area or alignment matters. |
printBackground |
Whether background graphics are included; default is false. | Set true if colors, fills, or background images are part of the design. |
preferCSSPageSize |
Whether CSS @page dimensions take priority; default is false. |
Set true when the page’s declared print size should control the PDF. |
scale |
Scales page rendering. | Adjust when content needs to fit or occupy more of the page; inspect the result for clipping. |
pageRanges |
Selects PDF page ranges. | Use when only specified pages are required. |
displayHeaderFooter, headerTemplate, footerTemplate |
Header and footer output. | Enable and supply templates when page labels or repeated metadata are needed. |
waitForFonts |
Waits for fonts to be ready before PDF generation. | Keep enabled when font rendering affects line breaks or layout. |
When preferCSSPageSize is false, content is scaled to fit the configured paper size. When true, CSS @page dimensions take priority. Avoid relying on both an unrelated CSS page size and a conflicting paper format; choose one page-size authority deliberately.
3. Put the exact screenshot on a PDF page
When visual pixel fidelity to the capture matters, make a screenshot and embed the resulting bytes in a PDF. The following uses PDFKit and creates a PDF page whose dimensions match the screenshot’s pixel dimensions, with one PDF point per image pixel. It does not rerender the web page into print layout.
import puppeteer from 'puppeteer';
import PDFDocument from 'pdfkit';
import { createWriteStream } from 'node:fs';
import { once } from 'node:events';
const url = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
await page.locator('body').wait();
const image = await page.screenshot({ type: 'png', fullPage: false });
const viewport = page.viewport();
if (!viewport) throw new Error('Page viewport is unavailable');
const pdf = new PDFDocument({
autoFirstPage: false,
size: [viewport.width, viewport.height],
margin: 0,
});
const output = createWriteStream('screenshot.pdf');
pdf.pipe(output);
pdf.addPage({ size: [viewport.width, viewport.height], margin: 0 });
pdf.image(image, 0, 0, { width: viewport.width, height: viewport.height });
pdf.end();
await once(output, 'finish');
} finally {
await browser.close();
}
The image is embedded at its captured dimensions. PDF page dimensions use points, while screenshot dimensions are pixels; using the same numeric dimensions makes the page large in physical print units. To target a standard paper size, create the PDF page at that size and fit the image deliberately, accepting that it may be scaled or surrounded by whitespace.
Capture a full page or a region
For an image-based PDF page that contains the full scrollable document, change the capture to:
const image = await page.screenshot({ type: 'png', fullPage: true });
For only a particular region, use a screenshot clip:
const image = await page.screenshot({
type: 'png',
clip: { x: 0, y: 0, width: 900, height: 600 },
});
fullPage and clip control the screenshot extent. They do not make the screenshot method return a PDF. With fullPage: true, the output can be much taller and use more memory; check that the PDF dimensions and downstream viewers can handle the resulting image.
4. Wait for the content you need
Navigation completion is not the same as application readiness. A site may render content after an API call, lazy-load images while scrolling, or show a consent dialog before the page is usable. Wait for a concrete signal tied to the content you need.
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
await page.waitForSelector('[data-report-ready="true"]', { timeout: 15_000 });
await page.evaluate(() => document.fonts.ready);
For a specific image, wait for it to load rather than assuming the document event covers it:
await page.waitForFunction(() => {
const image = document.querySelector('#report-chart');
return image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0;
});
For full-page screenshots, lazy images may not load until their sections enter the viewport. If every image must appear, scroll through the page in steps before capture, then return to the desired scroll position or use full-page capture. The exact scrolling strategy depends on how the site loads content.
The Puppeteer screenshot guide shows networkidle2 as an example navigation wait, but it is not a universal readiness rule. Analytics, long-polling, and other persistent requests may prevent network idle, while a quiet network can occur before the content you need appears. Prefer a selector, application state, or bounded delay tied to your page.
5. Select the right conversion path
| Need | Use | Trade-off |
|---|---|---|
| Text and layout that follow print styles and paginate | page.pdf() |
It is a browser PDF rendering, not a copy of screenshot pixels; print CSS applies by default. |
| One viewport preserved as a captured image | page.screenshot() plus an image-to-PDF writer |
The PDF page contains a raster image; text is not preserved as selectable page text. |
| A tall capture of the whole page | fullPage: true, then embed the image |
Large image dimensions can increase memory, file size, and viewer strain. |
| Only a specific visual area | clip, then embed the image |
Clip coordinates and dimensions must fit the rendered page. |
6. Other screenshot and PDF options
ScreenshotOptions lets you choose the capture path and output: fullPage for the page beyond the viewport, clip for a region, type for PNG, JPEG, or WebP, encoding for binary or base64 output, and path to save directly to a file. Screenshot output defaults to PNG. For embedding in a PDF, binary bytes are convenient; if you request base64, decode it before passing it to the PDF writer.
Use PNG when crisp text and lossless edges matter. JPEG can reduce image size for photographic pages but is lossy. WebP can be useful where the chosen PDF library accepts it; check that library’s supported image formats. If a library cannot embed the screenshot format, capture PNG or convert the image to a supported format first.
For a PDF generated by page.pdf(), set page size, margins, orientation, scaling, page ranges, headers or footers, and background printing to match the intended document. For a screenshot-based PDF, those PDF layout choices belong to the PDF-writing library: Puppeteer’s screenshot settings govern the source image, not the PDF page.
7. cURL, Python, and Node.js alternatives
Puppeteer is a Node.js browser automation library. cURL and Python do not invoke Puppeteer directly; they can call a service that performs the capture. If you need a local browser controlled from Python, use a browser automation library with its own PDF and screenshot APIs rather than treating a Puppeteer image as a PDF.
cURL: download a PDF from a screenshot API
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-d format=pdf \
-o page.pdf
See the ScreenshotNeo API documentation for request parameters and response behavior.
Python: request a PDF
import requests
response = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://example.com",
"format": "pdf",
},
timeout=90,
)
response.raise_for_status()
with open("page.pdf", "wb") as output:
output.write(response.content)
Node.js: request a PDF
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com',
format: 'pdf',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) =>
writeFile('page.pdf', Buffer.from(await res.arrayBuffer()))
);
These API examples return a PDF rendering from the service; they do not embed the exact bytes produced by a local Puppeteer screenshot. Use the local image-to-PDF method when that exact capture is the requirement.
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. One request can return a PNG, JPEG, WebP, or PDF. For a PDF response:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-d format=pdf \
-o page.pdf
Cookie banners are accepted like a visitor and more than 60 known consent platforms, newsletter popups, and chat widgets are removed before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server gives Claude, Cursor, and other MCP clients the take_screenshot, get_page_info, and capture_pdf tools. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan.
Read the API docs, then sign up for 1,000 free screenshots a month, with no card.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The PDF has different layout or colors from the screenshot. | page.pdf() uses print media and may apply print CSS or print color handling. |
For screen styling, call emulateMediaType('screen') before pdf(). For exact captured pixels, embed screenshot bytes instead. |
| Background colors or images are missing. | PDF background printing is off by default. | Set printBackground: true for page.pdf(). |
| Text wraps differently or a page spills onto another sheet. | Paper size, margins, CSS @page, or scaling do not match the intended layout. |
Set format or custom dimensions and margins explicitly. Decide whether CSS page size or the PDF format should take priority with preferCSSPageSize. |
| Content, charts, or images are missing. | The capture ran before client rendering, image loading, or lazy loading finished. | Wait for a page-specific selector or image state. For lazy content, scroll it into view before capture. |
| The script hangs waiting for navigation or network idle. | A persistent request can prevent a network-idle condition. | Use a bounded navigation timeout and wait for the specific content state needed rather than relying on network idle alone. |
| The screenshot is clipped. | Default screenshot captures the viewport, or the clip is outside page bounds. | Set fullPage: true for a full-page image or correct the clip coordinates and dimensions. |
| The screenshot PDF is huge or slow to open. | A tall full-page raster or high-resolution image is embedded. | Capture only the required area, consider JPEG for photographic content, or use page.pdf() when a print-rendered document is acceptable. |
| The PDF writer rejects the screenshot image. | The writer may not support the selected image format or received base64 text instead of bytes. | Capture PNG, pass binary screenshot bytes, or decode base64 and convert to a supported format. |
| Browser launch fails in a container. | The environment may lack browser dependencies or required runtime configuration. | Install Puppeteer’s documented browser dependencies for that environment and use its supported installation setup. Avoid disabling browser security flags without understanding the environment. |
10. Performance, reliability, and cost
Both approaches incur the cost of opening a browser page and waiting for the target content. For a screenshot-based PDF, memory and output size also grow with image dimensions; full-page capture can be substantially larger than a viewport capture. Reduce the captured area and use an appropriate image format when size matters. For page.pdf(), complex print CSS and long documents can also affect render time and output size.
For reliable jobs, set navigation and content wait timeouts, close the browser in a finally block, and wait for the output stream to finish before reporting success. Treat remote pages as variable: they may be slow, change layout, fail to load, or require authentication. For repeatable results, use a stable target state and explicit viewport, media type, paper size, margins, and readiness condition. No performance benchmark is available in the cited Puppeteer documentation.
Local Puppeteer has no per-capture API charge, but the browser uses compute, memory, and storage that you operate. A hosted screenshot service trades browser operations for service pricing. ScreenshotNeo’s stated plans are Free: 1,000 shots/month; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; and Business: $249 for 1,000,000. Yearly billing gives two months free. Only clean shots are billed, according to the product’s stated policy.
11. FAQ
Can a PDF made from a screenshot contain selectable text?
Not as text from the page: the screenshot is a raster image. Use page.pdf() when text and document layout should be rendered as a PDF page.
Does fullPage: true create multiple PDF pages?
No. It changes the image capture extent. The image-to-PDF writer then places that tall image on a page unless you add your own slicing or pagination logic.
Should I use networkidle2?
Only when that condition matches the target site’s behavior. For dynamic pages, waiting for the specific data or element you need is more reliable than assuming network quiet means the page is ready.
Which approach should I choose for archiving a page?
Choose page.pdf() for a paginated, print-oriented document. Choose an image embedded in a PDF when the visual appearance of one captured view must be preserved.


