cdpfleet Docs GitHub Dashboard

Cases / #44 · 2026-10-14 · Easy

Print to PDF across browsers: which engines can, and the fallback for the rest

`page.pdf()` on seven remote browsers: Chromium, Chrome and Edge print — headless and headful — while Firefox, Camoufox and WebKit refuse. Print CSS plus a full-page screenshot works everywhere.

Chromium Google Chrome Microsoft Edge Firefox Camoufox WebKit

Run on production on 2026-10-14: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

The problem

Invoices, reports and archived pages often have to end up as PDFs. Playwright's page.pdf() is documented as "headless Chromium only", which raises two questions on a fleet of real browsers: does a headful Chrome or Edge print, and what do you do on Firefox, Camoufox or WebKit when the site only works there?

What we used, and why

WhatWhy
page.setContent()The same test page everywhere, with a coloured background, a screen-only banner and enough text for several printed pages — no proxy traffic.
page.pdf({ format: "A4" })Asks the browser to print the page; the PDF bytes come back over the connection.
printBackground: trueBackgrounds are left out of printed pages by default, as in a desktop print dialog.
@media print + page.emulateMedia({ media: "print" })Applies the page's print stylesheet for screenshots — the part of printing every engine supports.
page.screenshot({ fullPage: true })The fallback capture when page.pdf() isn't available.

How it works

  1. Launch seven setups one after another: Chromium headless, Chrome headless and headful, Edge headful, Firefox, Camoufox and WebKit (headful, their default).
  2. Load the same test page and call page.pdf() twice — without and with backgrounds — counting pages and bytes, or record the error.
  3. Switch to print media, check that the print stylesheet hid the banner, and take a full-page screenshot.

The code

The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:

// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium, firefox, webkit } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;
const TYPES = { chromium, firefox, webkit };

// Which browsers can print a page to PDF over a remote connection, headless and headful.
const SETUPS = [
  { label: 'Chromium, headless', engine: 'chromium', type: 'chromium', headless: true },
  { label: 'Chrome, headless', engine: 'chrome', type: 'chromium', headless: 'new' },
  { label: 'Chrome, headful', engine: 'chrome', type: 'chromium', headless: false },
  { label: 'Edge, headful', engine: 'edge', type: 'chromium', headless: false },
  { label: 'Firefox, headful', engine: 'firefox', type: 'firefox', headless: false },
  { label: 'Camoufox, headful', engine: 'camoufox', type: 'firefox', headless: false },
  { label: 'WebKit, headful', engine: 'webkit', type: 'webkit', headless: false },
];

// A page with a coloured background, a screen-only banner hidden by print CSS and enough text
// for several printed pages. setContent: no proxy traffic, the same page everywhere.
const HTML = `<html><head><style>
  body { font: 14px sans-serif; background: #e8f0ff; }
  .screen-only { background: #c00; color: #fff; padding: 8px; }
  @media print { .screen-only { display: none; } }
</style></head><body>
  <div class="screen-only">Cookie banner (screen only)</div>
  <h1>Quarterly report</h1>
  ${Array.from({ length: 120 }, (_, i) => `<p>Paragraph ${i + 1}: lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>`).join('')}
</body></html>`;

const pagesIn = (buf) => (buf.toString('latin1').match(/\/Type\s*\/Page(?!s)/g) || []).length;

// 503 means the fleet is momentarily busy: wait Retry-After and try again.
async function launch(engine, body) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(`https://starter.cdpfleet.com/${engine}/session`, {
      method: 'POST',
      headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
      body: JSON.stringify(body),
    });
    if (res.ok) return res.json();
    if (res.status !== 503 || attempt === 5) throw new Error(`launch ${engine} ${res.status} ${await res.text()}`);
    await new Promise((r) => setTimeout(r, (Number(res.headers.get('retry-after')) || 5) * 1000));
  }
}

async function one(s) {
  const { wsUrl } = await launch(s.engine, { proxy: process.env.PROXY_URL, headless: s.headless });
  const browser = await TYPES[s.type].connect(wsUrl, { headers: { 'x-api-key': KEY } });
  const out = { label: s.label };
  try {
    const page = await browser.newPage();
    await page.setContent(HTML);
    const t = Date.now();
    try {
      const pdf = await page.pdf({ format: 'A4' });
      const withBg = await page.pdf({ format: 'A4', printBackground: true });
      out.pdf = 'yes';
      out.pdf_bytes = pdf.length;
      out.pdf_pages = pagesIn(pdf);
      out.with_background_bytes = withBg.length;
      out.pdf_seconds = Math.round((Date.now() - t) / 10) / 100;
    } catch (err) {
      out.pdf = 'no';
      out.error = err.message.split('\n')[0].slice(0, 120);
    }
    // The fallback that works everywhere: print CSS + a full-page screenshot.
    await page.emulateMedia({ media: 'print' });
    out.print_css_applied = await page.evaluate(() => getComputedStyle(document.querySelector('.screen-only')).display === 'none');
    out.print_screenshot_kb = Math.round((await page.screenshot({ fullPage: true })).length / 1024);
  } finally {
    await browser.close();
  }
  return out;
}

const results = [];
for (const s of SETUPS) results.push(await one(s));
console.log(JSON.stringify({ results }, null, 2));

What we got

Setuppage.pdfPagesKB (with background)Print CSSPrint screenshot KB
Chromium, headlessyes, 0.03 s419.6 (19.7)applied595
Chrome, headlessyes, 0.17 s419.8 (19.9)applied595
Chrome, headfulyes, 0.16 s419.8 (19.9)applied595
Edge, headfulyes, 0.05 s419.8 (19.9)applied595
Firefox, headfulno—page.pdf: PDF generation is only supported for Headless Chromiumapplied850
Camoufox, headfulno—page.pdf: PDF generation is only supported for Headless Chromiumapplied312
WebKit, headfulno—page.pdf: PDF generation is only supported for Headless Chromiumapplied759

From the Node.js run on 2026-10-14. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)
{
  "results": [
    {
      "label": "Chromium, headless",
      "pdf": "yes",
      "pdf_bytes": 20044,
      "pdf_pages": 4,
      "with_background_bytes": 20170,
      "pdf_seconds": 0.03,
      "print_css_applied": true,
      "print_screenshot_kb": 595
    },
    {
      "label": "Chrome, headless",
      "pdf": "yes",
      "pdf_bytes": 20240,
      "pdf_pages": 4,
      "with_background_bytes": 20366,
      "pdf_seconds": 0.17,
      "print_css_applied": true,
      "print_screenshot_kb": 595
    },
    {
      "label": "Chrome, headful",
      "pdf": "yes",
      "pdf_bytes": 20232,
      "pdf_pages": 4,
      "with_background_bytes": 20358,
      "pdf_seconds": 0.16,
      "print_css_applied": true,
      "print_screenshot_kb": 595
    },
    {
      "label": "Edge, headful",
      "pdf": "yes",
      "pdf_bytes": 20246,
      "pdf_pages": 4,
      "with_background_bytes": 20372,
      "pdf_seconds": 0.05,
      "print_css_applied": true,
      "print_screenshot_kb": 595
    },
    {
      "label": "Firefox, headful",
      "pdf": "no",
      "error": "page.pdf: PDF generation is only supported for Headless Chromium",
      "print_css_applied": true,
      "print_screenshot_kb": 850
    },
    {
      "label": "Camoufox, headful",
      "pdf": "no",
      "error": "page.pdf: PDF generation is only supported for Headless Chromium",
      "print_css_applied": true,
      "print_screenshot_kb": 312
    },
    {
      "label": "WebKit, headful",
      "pdf": "no",
      "error": "page.pdf: PDF generation is only supported for Headless Chromium",
      "print_css_applied": true,
      "print_screenshot_kb": 759
    }
  ]
}

Takeaways