ScreenshotNeo

BlogHTML to image & PDF

How to Print a Full-Screen Page with jsPDF and HTML2Canvas

Render a complete HTML page with html2canvas, paginate it with jsPDF, and fix clipping, CORS, memory, and page-break problems.

By the ScreenshotNeo team30 September 20265 min read

How to Print a Full-Screen Page with jsPDF and HTML2Canvas

Direct answer: render the page or a print container with html2canvas, create a jsPDF document, then place the canvas on one or more pages with addImage. For a long page, render at the element’s full scroll dimensions and slice the canvas at each PDF page boundary. Placing one very tall image on one PDF page is why output gets cut off.

html2canvas reconstructs the DOM in a canvas, so it creates a visual snapshot rather than native browser print output. Unsupported CSS, cross-origin images, and cross-origin iframes may differ or be omitted.

1. Prepare a stable print container

A dedicated container avoids sticky headers, live animations, and unrelated application chrome.

const root = document.querySelector('#invoice');
if (!root) throw new Error('Missing #invoice');
const freeze = document.createElement('style');
freeze.textContent = '#invoice *, #invoice *::before, #invoice *::after { animation: none !important; transition: none !important; }';
document.head.appendChild(freeze);

2. Install and import

npm install html2canvas jspdf
import html2canvas from 'html2canvas';
import { jsPDF } from 'jspdf';

See the html2canvas documentation and jsPDF project for the APIs.

3. Render the complete element

const canvas = await html2canvas(root, {
  windowWidth: root.scrollWidth,
  windowHeight: root.scrollHeight,
  scrollX: 0,
  scrollY: 0,
  scale: Math.min(window.devicePixelRatio || 1, 2),
  useCORS: true,
  backgroundColor: '#ffffff'
});
  • windowWidth and windowHeight prevent viewport clipping.
  • scale controls output pixels. A value of 2 is often a useful quality and memory compromise.
  • useCORS requires CORS headers from the image host.
  • Use backgroundColor: null for transparency, or an explicit color for predictable PDFs.

4. Paginate with jsPDF

export async function saveFullPagePdf(root, filename = 'page.pdf') {
  const canvas = await html2canvas(root, {
    windowWidth: root.scrollWidth,
    windowHeight: root.scrollHeight,
    scrollX: 0, scrollY: 0,
    scale: Math.min(window.devicePixelRatio || 1, 2),
    useCORS: true, backgroundColor: '#fff'
  });
  const pdf = new jsPDF({ orientation: 'p', unit: 'pt', format: 'a4', compress: true });
  const pageWidth = pdf.internal.pageSize.getWidth();
  const pageHeight = pdf.internal.pageSize.getHeight();
  const sourcePageHeight = pageHeight * canvas.width / pageWidth;
  let y = 0, page = 0;
  while (y < canvas.height) {
    if (page++) pdf.addPage();
    const h = Math.min(sourcePageHeight, canvas.height - y);
    const slice = document.createElement('canvas');
    slice.width = canvas.width; slice.height = Math.ceil(h);
    const ctx = slice.getContext('2d');
    ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, slice.width, slice.height);
    ctx.drawImage(canvas, 0, y, canvas.width, h, 0, 0, canvas.width, h);
    pdf.addImage(slice, 'PNG', 0, 0, pageWidth, slice.height * pageWidth / canvas.width);
    y += h;
  }
  pdf.save(filename);
}

await saveFullPagePdf(document.querySelector('#invoice'), 'invoice.pdf');

addImage accepts a canvas, image format, coordinates, and dimensions. The function preserves aspect ratio and creates pages until the source canvas is exhausted.

The pipeline renders one tall canvas, then places slices on separate PDF pages.
The pipeline renders one tall canvas, then places slices on separate PDF pages.

5. Margins, orientation, and page ranges

const margin = 36;
const pdf = new jsPDF({ orientation: 'l', unit: 'pt', format: 'letter' });
const width = pdf.internal.pageSize.getWidth() - margin * 2;
const height = pdf.internal.pageSize.getHeight() - margin * 2;
// Use width and height when calculating the source slice height.
// Place each image at (margin, margin).

Track the page counter in the slicing loop to export only a requested range. For a section that must start on a fresh page, end the current slice and call addPage().

6. Keep blocks together

Canvas slicing cuts pixels, so a heading or table row can cross a boundary. Capture sections separately, or add print-only break markers and start a new slice at those markers. CSS break-inside rules do not automatically control a bitmap canvas.

7. html2pdf.js alternative

html2pdf.js combines both libraries and exposes margins, image quality, html2canvas settings, jsPDF settings, and page-break modes.

import html2pdf from 'html2pdf.js';
const node = document.querySelector('#invoice');
html2pdf().set({
  margin: [24, 24, 24, 24], filename: 'page.pdf',
  image: { type: 'jpeg', quality: 0.95 },
  html2canvas: { scale: 2, useCORS: true, windowWidth: node.scrollWidth, windowHeight: node.scrollHeight },
  jsPDF: { unit: 'pt', format: 'a4', orientation: 'p' },
  pagebreak: { mode: ['css', 'legacy'] }
}).from(node).save();

Use the wrapper for conventional breaks; use direct APIs for deterministic slicing, page ranges, or custom placement.

8. Images, fonts, iframes, and lazy content

  • Cross-origin images: configure CORS on the image host or use a same-origin proxy. useCORS cannot grant permission.
  • Cross-origin iframes: browser security blocks access to their document; capture their content separately.
  • Fonts: await document.fonts.ready before capture to avoid changed line wrapping.
  • Lazy images: scroll or otherwise load them before rendering.
  • Video: the output is one frame; pause media for repeatable results.
  • Huge pages: lower scale, capture sections, or use a server-side browser when canvas limits are reached.

9. Troubleshooting

Symptom Cause Fix
Only viewport captured Viewport dimensions used Set window dimensions to scroll dimensions.
Bottom cut off One image placed on one page Slice the canvas and call addPage().
SecurityError or failed export Tainted canvas from cross-origin image Enable CORS or proxy the image.
Blank iframe Cross-origin frame Capture it from its own origin or replace it server-side.
Different wrapping Fonts or data not ready Await fonts, images, and application data.
Tab freezes Canvas too large Lower scale or capture sections.
Black transparent areas Viewer/background handling Set a white background and fill slices.
Thin seams Fractional slice boundary Ceil slice height and advance by the drawn height.

10. Performance, reliability, and cost

  • Canvas memory grows with width × height × scale²; cap scale and avoid oversized hidden nodes.
  • Disable animation, wait for resources once, and capture the smallest required subtree.
  • Disable the download button during capture and restore it in finally; treat the operation as asynchronous.
  • PNG preserves UI text; JPEG is smaller for photos but can soften text.
  • Do not put secrets in page HTML. A proxy should allow-list destinations and sanitize forwarded headers.
  • Client-side capture has no API request charge but uses the user's CPU and memory; a hosted browser API trades setup for per-capture billing.

11. Or skip the browser setup

ScreenshotNeo returns a screenshot or PDF from one GET request. It supports full-page capture with lazy images loaded, PDF paper size, margins, landscape, and page ranges. See the ScreenshotNeo API docs.

A clean capture removes common overlays before rendering the page.
A clean capture removes common overlays before rendering the page.
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)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
require('node:fs').writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));

Before capture, ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses identify the result with X-Page-Verdict and X-Billed. Its MCP server gives Claude, Cursor, and other MCP clients take_screenshot, get_page_info, and capture_pdf. Free includes 1,000 shots/month with no card; paid plans start at $5 for 3,000.

Create a free ScreenshotNeo account and start with 1,000 monthly screenshots.

12. FAQ

Can I capture the whole document?

Yes. A dedicated container is usually more predictable; set its full scroll dimensions when rendering.

Why is the PDF blurry?

Increase scale within memory limits and map the image to page width without stretching.

Can the PDF keep selectable text?

No. This method embeds a bitmap. Use native print CSS or structured PDF generation when text selection matters.

Should I choose PNG or JPEG?

PNG suits UI text and line art; JPEG is smaller for photographic pages.

How do I capture an authenticated page?

Capture after the browser session is authenticated. For a server API, use documented headers or cookies and keep credentials server-side.