Guides · Content pipelines · Published 2026-09-12 · 2 min read

Generate a PDF or a Screenshot from HTML with Headless Chrome and No Puppeteer — Temporary Profile, Print CSS for A5 Books, Page Breaks, and Windows Path Escaping

Use the Chrome binary you already have to turn an HTML file into a print-ready PDF or a pixel-exact PNG, without installing Puppeteer: the flags, a throwaway user-data-dir so you never touch the real profile, @page CSS for A5 books, page-break rules, and the file URL escaping that bites on Windows.

Puppeteer is fine, but for a build script that just needs a PDF or a screenshot, spawning the Chrome you already have is smaller and has no dependency to keep updated. We use this to produce the print PDF of a book and to render title text onto cover images.

Find Chrome

const fs = require('fs');
const chrome = [
  'C:/Program Files/Google/Chrome/Application/chrome.exe',
  'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
  '/usr/bin/google-chrome',
].find((p) => fs.existsSync(p));

Always use a temporary profile

Running headless against your real profile can fail if Chrome is already open and leaves state behind. A throwaway --user-data-dir avoids both:

const os = require('os'), path = require('path');
const prof = fs.mkdtempSync(path.join(os.tmpdir(), 'chrome-build-'));
// ... run chrome ...
fs.rmSync(prof, { recursive: true, force: true });

HTML to PDF

const { spawnSync } = require('child_process');
const fileUrl = 'file:///' + htmlPath.split(path.sep).join('/');
spawnSync(chrome, [
  '--headless=new', '--disable-gpu', '--no-first-run', '--mute-audio',
  `--user-data-dir=${prof}`,
  '--no-pdf-header-footer',
  `--print-to-pdf=${pdfPath}`,
  fileUrl,
], { timeout: 120000, stdio: 'ignore' });

Building the file URL with split(path.sep).join('/') sidesteps the backslash-escaping problem that appears when the same code is written through a shell on Windows (a literal \\ in a regex arriving as \ and throwing a syntax error).

Print CSS for a book

@page { size: A5; margin: 16mm 14mm; }
body { font-family: Georgia, serif; font-size: 10.5pt; line-height: 1.6; }
h1 { page-break-before: always; }
table, figure { page-break-inside: avoid; }
.cover { page-break-after: always; text-align: center; }
.cover img { width: 100%; height: auto; }

Put the cover image as a data URI in the first page so the PDF is self-contained, then the title page, then a table of contents with in-document links, then chapters. Chrome honours page-break-* reliably; for exact page counts, check the output in a viewer rather than trusting the CSS.

HTML to PNG (screenshots and composed images)

The same binary takes pixel-exact screenshots, which is how we place Thai title text over a generated cover image without a font-rendering library:

spawnSync(chrome, [
  '--headless=new', '--disable-gpu', '--hide-scrollbars',
  `--user-data-dir=${prof}`,
  '--window-size=800,800', '--force-device-scale-factor=1',
  `--screenshot=${pngPath}`,
  fileUrl,
], { timeout: 60000, stdio: 'ignore' });

Set html, body { margin: 0; width: 800px; height: 800px; overflow: hidden } so the viewport equals the canvas. Then convert or compress the PNG with sharp if you need a JPEG.

Fonts

Headless Chrome uses the system fonts, so Thai, Japanese or any other script renders as long as the OS has the font. Specify a fallback stack anyway; on a server image with few fonts, install the ones you need before relying on this.

Common mistakes

Summary

Spawn the installed Chrome with a temporary profile, --print-to-pdf for documents and --screenshot for images, drive layout with @page and page-break CSS, and keep the HTML self-contained with data URIs. It is a dependency-free companion to building EPUBs from Markdown.

Related guides