How to Automate Screenshots for PDF Reports
Capture reliable report screenshots and PDFs with Playwright or Puppeteer, handle charts and fonts, and avoid common layout and color problems.
Direct answer: render the report in a controlled Chromium session, wait for its data, charts, tables, and fonts to finish, then capture the required scope and export the same rendered page to PDF. Playwright and Puppeteer can both produce a viewport image, a full-page image, an element image, and a PDF. PDF output uses print CSS by default, so select screen media when visual fidelity to the browser matters.
1. Choose the capture scope
| Need | Use | Result |
|---|---|---|
| A dashboard card or chart | Element screenshot | Only the selected element |
| The visible browser area | Viewport screenshot | What fits in the current viewport |
| An entire report page | Full-page screenshot | The full scrollable document |
| A printable deliverable | PDF generation | Paginated output controlled by print CSS |
Playwright documents viewport, element, full-page, filename, image-type, and high-resolution screenshot options in its screenshot guide and Page API (screenshots, Page.screenshot). Its PDF API is documented at Page.pdf.
2. A complete Playwright workflow (Node.js)
Install Playwright and its browser once in the environment that will run the job:
npm install playwright
npx playwright install chromium
Save this as capture-report.mjs. Replace the URL and the application-specific readiness selector. The selector should appear only after the report has its final data.
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 2,
});
try {
await page.goto('https://example.com/report', { waitUntil: 'domcontentloaded' });
// Wait for the report's own completion signal.
await page.locator('[data-report-ready="true"]').waitFor({ state: 'visible', timeout: 30_000 });
// Ensure web fonts have loaded before rasterizing text.
await page.evaluate(() => document.fonts.ready);
// Optional: make screen colors print correctly in PDF.
await page.emulateMedia({ media: 'screen' });
await page.addStyleTag({
content: '* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }'
});
await page.screenshot({
path: 'report-full.webp',
fullPage: true,
type: 'webp',
quality: 90,
});
await page.locator('[data-report-chart="revenue"]').screenshot({
path: 'revenue-chart.png',
type: 'png',
});
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
margin: { top: '16mm', right: '14mm', bottom: '16mm', left: '14mm' },
});
} finally {
await browser.close();
}
The sequence matters: navigate, wait for application readiness, await fonts, choose the media type, then create both artifacts from the same page state. Puppeteer’s official PDF guide uses the same navigation-then-page.pdf() pattern and notes that fonts are awaited by default (Puppeteer PDF generation).
Useful Playwright options
| Option | When to use it |
|---|---|
fullPage: true |
Capture the complete scrollable report. |
locator.screenshot() |
Capture one chart, table, or card. |
type: 'png'|'jpeg'|'webp' |
Choose lossless PNG, compressed JPEG, or WebP. |
quality |
Set compression quality for JPEG or WebP. |
deviceScaleFactor |
Render high-resolution output for dense reports. |
omitBackground: true |
Use transparency when the page background permits it. |
clip |
Capture a precise rectangle when a locator is unsuitable. |
path |
Write a deterministic artifact filename. |
3. A complete Puppeteer workflow (Node.js)
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 2 });
try {
await page.goto('https://example.com/report', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-report-ready="true"]', { visible: true, timeout: 30_000 });
await page.evaluate(() => document.fonts.ready);
await page.emulateMediaType('screen');
await page.addStyleTag({
content: '* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }'
});
await page.screenshot({ path: 'report-full.png', fullPage: true });
const chart = await page.$('[data-report-chart="revenue"]');
if (!chart) throw new Error('Revenue chart was not found');
await chart.screenshot({ path: 'revenue-chart.png' });
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
margin: { top: '16mm', right: '14mm', bottom: '16mm', left: '14mm' },
});
} finally {
await browser.close();
}
Puppeteer exposes page.screenshot() and page.pdf(); its PDF documentation is at pptr.dev/guides/pdf-generation. Puppeteer is a JavaScript library that automates Chrome or Firefox through browser automation protocols (Chrome for Developers).
4. Python Playwright
from pathlib import Path
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 1440, "height": 1000}, device_scale_factor=2)
try:
page.goto("https://example.com/report", wait_until="domcontentloaded")
page.locator('[data-report-ready="true"]').wait_for(state="visible", timeout=30_000)
page.evaluate("document.fonts.ready")
page.emulate_media(media="screen")
page.add_style_tag(content="* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }")
page.screenshot(path="report-full.png", full_page=True)
page.locator('[data-report-chart="revenue"]').screenshot(path="revenue-chart.png")
page.pdf(path="report.pdf", format="A4", print_background=True,
margin={"top": "16mm", "right": "14mm", "bottom": "16mm", "left": "14mm"})
finally:
browser.close()
5. Make readiness deterministic
A navigation event only tells you that the document reached a browser loading milestone. Client-rendered charts, API responses, animations, and web fonts can still be incomplete. Prefer an explicit application signal:
// After your report finishes rendering:
window.dispatchEvent(new Event('report-ready'));
await page.evaluate(() => new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Report did not become ready')), 30_000);
window.addEventListener('report-ready', () => { clearTimeout(timer); resolve(); }, { once: true });
}));
Other reliable gates include a stable locator, a chart canvas with a known state, a “loading” element becoming hidden, and document.fonts.ready. Use a short delay only for a known animation; a generic sleep is less reliable than an application signal.
6. PDF layout and color fidelity
- Print CSS is the default. Print media rules can hide navigation, change spacing, or remove screen-only elements.
- Use screen media for screen matching. Call
page.emulateMedia({ media: 'screen' })in Playwright orpage.emulateMediaType('screen')in Puppeteer. - Preserve colors. Set
printBackground: trueand use-webkit-print-color-adjust: exactwhen the report depends on colored fills. - Control pagination. Add print CSS such as
break-inside: avoidto cards and tables, and define page size and margins inpage.pdf(). - Expect different dimensions. A full-page image is one tall bitmap; a PDF is paginated paper with printable margins.
7. Authentication, headers, and repeatability
For protected reports, create a browser context with the required cookies or headers, or perform the login flow before opening the report. Keep the capture inputs stable: fixed viewport, device scale factor, timezone, locale, seeded report data, and deterministic filenames. Avoid capturing while transitions are running. If the report contains timestamps, freeze or pass an explicit reporting period so reruns are comparable.
8. Performance, reliability, and cost
- Reuse a browser process for multiple URLs, but create a fresh context per tenant or authentication boundary.
- Capture an element instead of a full page when only one chart is needed; it reduces image dimensions and storage.
- Use WebP or JPEG for previews and PNG when exact pixels or transparency matter.
- Set navigation, readiness, and overall job timeouts. Record the URL, revision, viewport, browser version, and readiness duration with each artifact.
- Retry transient navigation or upstream API failures with a bounded retry count. Do not retry a deterministic selector or application error indefinitely.
- Fonts and remote images are external dependencies. Bundle them or wait for them explicitly when repeatability matters.
- Browser automation cost depends on your runtime, concurrency, storage, and upstream services. The cited Playwright and Puppeteer documentation provides capabilities, not universal speed or cost benchmarks.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Charts are blank | Capture ran before data or canvas drawing completed. | Wait for the chart’s ready signal or a stable locator; verify the API response in the page. |
| Text uses a fallback font | Web fonts were still loading. | Await document.fonts.ready; ensure font requests are reachable. |
| PDF colors are missing | Print CSS or background printing changed the output. | Use screen media and printBackground: true; apply print color adjustment. |
| Only the visible area is captured | Viewport capture was used. | Set fullPage: true or capture the required locator. |
| Element selector times out | The selector is wrong, hidden, or rendered conditionally. | Inspect the DOM, wait for visibility, and fail with a useful diagnostic. |
| PDF has unexpected page breaks | Content exceeds printable space or print CSS adds breaks. | Set paper size and margins; use break-inside: avoid and review print styles. |
| Navigation hangs | An image, font, analytics request, or API call never completes. | Use a bounded navigation timeout and an application readiness signal rather than waiting forever for network idle. |
| Artifacts differ between runs | Dynamic data, animations, timezone, or responsive layout changed. | Freeze inputs, disable animations, fix viewport and timezone, and record metadata. |
10. Or skip the browser setup
ScreenshotNeo provides a hosted screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF output. It accepts cookie banners and consent dialogs before capture, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and billing result.
See the ScreenshotNeo API documentation for the complete option list, including full-page capture with lazy images loaded, CSS-element capture, dark mode, device presets or custom viewports, retina scale, PDF paper size and page ranges, custom CSS and JavaScript, clicks, selector or network-idle waits, request blocking, headers, cookies, user agent, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous jobs and webhooks, bulk capture, usage, and the OpenAPI specification.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
It also includes an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots, and every feature is available on every plan.
Create a free ScreenshotNeo account and start with 1,000 screenshots a month without a card.
11. FAQ
Can one browser render both the screenshot and PDF?
Yes. Capture both before closing the page so they share the same loaded data, fonts, viewport, and application state.
Should I use Playwright or Puppeteer?
Both cover the core workflow. Choose based on your supported browsers, language/runtime, existing test stack, and the readiness controls your application needs; the documentation does not establish a universal performance winner.
When is an element screenshot better than a full-page image?
Use an element capture for a chart, table, or card that will be embedded elsewhere. Use full-page capture when the page’s vertical layout is itself the deliverable.
Why does my PDF not look like the screen?
PDF generation uses print media by default. Select screen media, print backgrounds, preserve colors, and review the report’s print CSS.
How do I prevent incomplete asynchronous data?
Expose a report-ready signal or wait for a stable, visible locator that represents completed data. Navigation completion alone is not sufficient for client-rendered reports.


