Use Mozilla PDF.js to render the PDF page into an HTML canvas, then export that canvas as PNG or JPEG. The reliable sequence is: load the document, call pdf.getPage(pageNumber), create a viewport with page.getViewport({ scale }), size the canvas, await page.render(), and finally call canvas.toDataURL() or canvas.toBlob(). This captures the page content—not the browser’s built-in PDF plug-in controls—and works in a normal HTTP(S) application.
The complete browser workflow
PDF.js has a low-level display layer intended for custom rendering. It converts PDF coordinates (whose origin is at the bottom-left) into canvas coordinates (top-left) through the viewport and render transform. Render only the page or pages you need, wait for the render promise, and export after drawing has finished.
1. Load PDF.js and its worker
Use matching versions of the PDF.js main library and worker. The worker performs parsing away from the main UI thread. Pin both files to the same release in your package manager or deployment bundle. Run the app from HTTP or HTTPS; the normal worker setup is not enabled for file:// pages.
2. Render and download page one
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>PDF page screenshot</title>
<style>#pdf-canvas { max-width: 100%; height: auto; }</style>
</head>
<body>
<canvas id="pdf-canvas"></canvas>
<button id="save" disabled>Save PNG</button>
<script type="module">
import * as pdfjsLib from '/pdfjs/pdf.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdfjs/pdf.worker.mjs';
const canvas = document.querySelector('#pdf-canvas');
const context = canvas.getContext('2d', { alpha: false });
const save = document.querySelector('#save');
const loadingTask = pdfjsLib.getDocument('/files/example.pdf');
const pdf = await loadingTask.promise;
const pageNumber = 1;
const page = await pdf.getPage(pageNumber);
const scale = 2;
const viewport = page.getViewport({ scale });
const outputScale = window.devicePixelRatio || 1;
canvas.width = Math.floor(viewport.width * outputScale);
canvas.height = Math.floor(viewport.height * outputScale);
canvas.style.width = `${Math.floor(viewport.width)}px`;
canvas.style.height = `${Math.floor(viewport.height)}px`;
const transform = outputScale !== 1
? [outputScale, 0, 0, outputScale, 0, 0]
: null;
const renderTask = page.render({
canvasContext: context,
viewport,
transform
});
await renderTask.promise;
save.disabled = false;
save.addEventListener('click', () => {
canvas.toBlob((blob) => {
if (!blob) return;
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `page-${pageNumber}.png`;
link.click();
URL.revokeObjectURL(link.href);
}, 'image/png');
});
</script>
</body>
</html>
In a bundler, import PDF.js from your installed package instead of a local /pdfjs path, and set GlobalWorkerOptions.workerSrc to the matching worker URL. The important ordering is unchanged: do not export until renderTask.promise resolves.
Recommended Free Tools
#1 Best Overall
Control resolution, dimensions and orientation
Viewport scale
scale multiplies the page’s logical dimensions. A larger value creates more pixels and a sharper image, but increases rendering time, memory use and file size. For a thumbnail, use a value near 1; for a document image intended for zooming or printing, use a larger value and test the resulting dimensions.
Device-pixel ratio
The example separates the CSS display size from the canvas backing-store size. On a high-density display, window.devicePixelRatio multiplies the backing store while the CSS size remains the viewport size. Without this step, a canvas can look soft even when its logical dimensions are correct. If you are generating a fixed-size server asset, choose an explicit output scale rather than inheriting a user’s screen density.
Page rotation and crop
PDF.js includes a page’s rotation in the viewport returned by getViewport. You can pass an explicit rotation when you need to override it. A canvas screenshot is the full rendered page; cropping to a region is a separate canvas operation performed after rendering.
Export PNG or JPEG
PNG: lossless default
Use canvas.toDataURL('image/png') when text and diagrams must remain lossless. A data URL is convenient for an <img> or small result, but it temporarily keeps the entire encoded image in memory.
Rank #2
const pngDataUrl = canvas.toDataURL('image/png');
JPEG: smaller photographic files
JPEG can be substantially smaller when a PDF page contains photographs and some quality loss is acceptable. It is not lossless and can create ringing around small text. Prefer toBlob for uploads or downloads:
canvas.toBlob((blob) => {
if (!blob) throw new Error('Canvas export failed');
// Upload blob or create an object URL for download.
}, 'image/jpeg', 0.92);
The quality argument is used for lossy formats such as JPEG; PNG ignores it. Always check for a null blob and revoke object URLs after use.
Remote PDFs, CORS and deployment
A PDF loaded from another origin is subject to the browser’s same-origin policy. PDF.js does not bypass that policy. The PDF host must send suitable CORS headers, or your application must fetch the file through a server-side endpoint on an origin your page is allowed to access. A typical failure is a successful URL in a new browser tab but a blocked request inside JavaScript; that is a CORS configuration problem, not a rendering-scale problem.
Keep the PDF.js library and worker on compatible, matching versions. Serve the page, worker and PDF over HTTP(S), configure correct MIME types, and avoid opening the HTML directly from the filesystem. If documents require authorization, fetch them through an authenticated endpoint and pass the resulting data to getDocument rather than exposing private tokens in a public PDF URL.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Rendering multiple pages safely
For a page picker, render one selected page into a reusable canvas. For a batch export, process pages sequentially and release intermediate canvases or blobs as soon as each result is stored. Rendering every page at a large scale at once can exhaust memory.
async function renderPageToBlob(pdf, pageNumber, scale = 1.5, type = 'image/png') {
const page = await pdf.getPage(pageNumber);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d', { alpha: false });
const outputScale = window.devicePixelRatio || 1;
canvas.width = Math.floor(viewport.width * outputScale);
canvas.height = Math.floor(viewport.height * outputScale);
const transform = outputScale !== 1
? [outputScale, 0, 0, outputScale, 0, 0]
: null;
await page.render({ canvasContext: context, viewport, transform }).promise;
return await new Promise((resolve, reject) =>
canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error('Export failed')), type)
);
}
const loadingTask = pdfjsLib.getDocument('/files/report.pdf');
const pdf = await loadingTask.promise;
for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber++) {
const blob = await renderPageToBlob(pdf, pageNumber);
// Upload or download blob here, then let the loop continue.
}
Render only visible pages in a scrolling viewer. A letter page at 96 DPI is about 816×1056 pixels and uses approximately 3.5 MB for one canvas before high-DPI multiplication; dozens of large canvases can therefore consume hundreds of megabytes. The canvas backend is the stable default. PDF.js describes its SVG backend as experimental and not feature complete, so do not select it for a production screenshot path without validating every document type.
Common failures and fixes
“Setting up fake worker” or worker-load errors
The worker URL is missing, incorrect or from a different PDF.js version. Set GlobalWorkerOptions.workerSrc to the deployed matching worker file and verify it returns JavaScript rather than an HTML error page.
Nothing appears on the canvas
Check that you awaited both getDocument(...).promise and page.render(...).promise. Confirm the canvas has non-zero width and height; setting only CSS dimensions does not create drawing pixels. Inspect network requests for a missing PDF or worker.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Rank #4
“Failed to fetch” or CORS errors
Move the PDF to the same origin, configure CORS on the PDF host, or proxy the request through your backend. Browser JavaScript cannot override a server’s cross-origin policy.
Blurry output
Increase scale, size the backing store with devicePixelRatio, and avoid enlarging a low-resolution canvas with CSS. Remember that increasing both scale and device-pixel ratio multiplies pixel count.
Tab freezes or the browser runs out of memory
Lower the scale, render sequentially, reuse one canvas, and discard finished blobs or canvases. Do not render all pages concurrently unless you have a measured memory budget.
Fonts, annotations or unusual PDFs look different
Ensure PDF.js can load its auxiliary resources and that the document is not damaged or encrypted without a password. If your feature requires interactive annotations rather than a visual screenshot, add the appropriate PDF.js annotation-layer UI; the canvas alone is a bitmap of the page.
Or skip the browser setup
If you need a screenshot or PDF conversion in a backend, CI job or AI workflow, ScreenshotNeo can capture a URL with one request. It accepts cookie and consent banners before capture, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and lets you turn each cleanup step off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed; each response reports the result in X-Page-Verdict and X-Billed headers. It also provides an MCP server for Claude, Cursor and other MCP clients, with take_screenshot, get_page_info and capture_pdf tools.
One-call cURL example
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', data);
See the ScreenshotNeo documentation for the 63 capture options, including full-page lazy-image loading, CSS-selector element capture, dark mode, device presets, retina scale, PDF paper and margin controls, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting and the OpenAPI specification. Existing parameter names used by other screenshot APIs are accepted to ease migration.
The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; Growth is $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000 and Business $249 for 1,000,000. Yearly billing provides two months free, and every feature is available on every plan. Create a free ScreenshotNeo account to start without a card.
Choosing the right implementation
| Need | Recommended approach | Reason |
|---|---|---|
| One page inside an existing web app | PDF.js canvas | No upload service; immediate control over scale and export. |
| Many pages in a browser viewer | PDF.js with sequential rendering | Virtualize visible pages to control memory. |
| Server, CI or automated URL capture | ScreenshotNeo | One HTTP call, cleanup of consent UI and explicit billing verdicts. |
| Lossless diagrams and text | PNG | No JPEG compression artifacts. |
| Photographic pages where size matters | JPEG | Smaller output at a chosen quality, with some loss. |
Frequently Asked Questions
Can I screenshot the browser’s native PDF viewer with JavaScript?
Use PDF.js instead. The native plug-in UI is outside your page’s DOM and is not a dependable target for custom capture.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Does PDF.js bypass authentication or CORS?
No. Your server must make the PDF available under normal browser security rules, or your backend must fetch it and serve an allowed response.
Should I use PNG or JPEG for text-heavy PDFs?
PNG is the safer default for crisp text and line art; choose JPEG only when smaller files outweigh compression artifacts.
How do I capture only one page from a multi-page PDF?
Call pdf.getPage(desiredPageNumber), render that page into the canvas, and export after its render promise resolves.
Quick Recap
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.




