Guides · reference

Every wkhtmltopdf flag and its 2026 equivalent — and the five defaults that flip on you silently

Published 2026-08-17 · the left column is taken verbatim from the shipped usage documentation; the right columns were checked against the shipped type definitions and the tools' own help output, and the behavioural claims were measured on the day of writing.

You have a command line with a dozen flags on it, it has worked since 2017, and now you have to write the same thing against something that is still maintained. Most of the flags have an equivalent. A handful do not. The ones that will actually cost you a Friday are neither: they are the flags you did not pass, because wkhtmltopdf's default and the replacement's default disagree.

Three things before the table, because they change how you read it:

  1. The shipped usage documentation lists 123 long options — seven of which only print help, licence or man-page text. Playwright's page.pdf() in 1.62.1 takes 15. That gap is not a feature deficit so much as a change of address: most of what wkhtmltopdf spelled as a flag is now CSS, or a call you make on the page before you print it.
  2. WeasyPrint's answer to most of the geometry flags is "that is CSS". It has no page-size, orientation or margin options at all — they live in @page, which is where the standard put them. If your templates are yours to edit, that is less work than it sounds.
  3. Five defaults flip. We measured them rather than listing them, because every one produces a PDF that renders, uploads and looks approximately right in a thumbnail — which is how they reach production.

The five that flip: measured, not remembered

Start here, because a literal port of the command line is what nearly everybody writes first, and it is wrong in five ways at once:

# What you have
wkhtmltopdf \
  --page-size A4 \
  --orientation Portrait \
  --margin-left 10mm --margin-right 10mm \
  --javascript-delay 500 \
  https://app.example.com/invoice/42  invoice.pdf

# The obvious three lines of Playwright
const page = await browser.newPage();
await page.goto('https://app.example.com/invoice/42', { waitUntil: 'networkidle' });
const bytes = await page.pdf({ format: 'A4' });

We ran one probe document through that, and through a corrected version, and read the results back out of the PDFs instead of eyeballing them. The document is deliberately obnoxious — dark background, one heading, and a pair of paragraphs that only appear under @media screen and @media print respectively:

<!doctype html><html><head>
<meta charset="utf-8"><title>Flag map probe document</title>
<style>
  html,body{margin:0;padding:0}
  body{background:#101010;color:#ffffff;font:14px/1.4 Helvetica,Arial,sans-serif}
  .screen-only{display:none}
  .print-only{display:none}
  @media screen { .screen-only{display:block} }
  @media print  { .print-only{display:block} }
</style></head><body>
<h1>PROBE-HEADING</h1>
<p class="screen-only">MEDIA-SCREEN</p>
<p class="print-only">MEDIA-PRINT</p>
<p>BODYTEXT-ANCHOR</p>
</body></html>

and the four readers are all command-line tools, so you can repeat this on your own template in about five minutes:

# background: is the page centre still dark?
pdftoppm -gray -r 12 -singlefile out.pdf ras   # then read the centre pixel of ras.pgm

# margins: where does the first glyph actually start, in points?
pdftotext -bbox out.pdf -                      # <word xMin=... yMin=...>

# media type: which of the two paragraphs survived?
pdftotext out.pdf - | grep -o 'MEDIA-[A-Z]*'

# outline: is there a /Outlines entry in the catalog?
node -e "…PDFDocument.load(b).catalog.get(PDFName.of('Outlines'))"

Chromium 151.0.7922.34 driven by Playwright 1.62.1, WeasyPrint 69.0, on 2026-08-17:

Behaviourwkhtmltopdf 0.12.6Playwright page.pdf()What we measured on the naive port
Paper sizeA4 (documented default)Letter page.pdf({}) → 612 × 792 pt. If you did not pass format, your European invoices are now US Letter.
Backgroundsprinted — --background is the default printBackground: false Centre pixel grey 255 (white) instead of 16. The dark panel, the zebra striping and the coloured status chip are simply gone.
Left / right margin10 mm (documented default)0 First glyph at x = 0.0 pt instead of 27.7 pt. Text runs into the paper edge and printers clip it.
Media typescreen--no-print-media-type is the defaultprint, always The @media print paragraph appeared and the @media screen one vanished. Every @media print { display:none } rule you ever wrote for the browser now fires.
PDF outline / bookmarkson — --outline is the default, depth 4outline: false /Outlines absent. Nobody notices until a reader opens the sidebar on a 90-page document.
The media-type flip is the one that surprises people twice. wkhtmltopdf rendered screen media unless you asked otherwise, so a decade of templates were written against screen CSS and print rules were never exercised. Chromium's page.pdf() uses print media and offers no option to change it — you call page.emulateMedia({ media: 'screen' }) first. WeasyPrint takes -m screen and also defaults to print.

The corrected port, which is what the four lines should have been:

const bytes = await page.pdf({
  format: 'A4',                 // wkhtmltopdf defaults to A4; page.pdf() defaults to Letter
  printBackground: true,        // --background was on by default
  margin: { top: '10mm', right: '10mm', bottom: '10mm', left: '10mm' },
  outline: true,                // and see below: this one needs `tagged` too
  tagged: true,
});

// Screen media, which is what wkhtmltopdf gave you unless you passed
// --print-media-type. page.pdf() uses print media whatever you do,
// so this has to be said out loud, before the render:
await page.emulateMedia({ media: 'screen' });

Two behaviours the type definitions do not tell you

outline: true on its own does nothing

Playwright documents the option as "whether or not to embed the document outline into the PDF. Defaults to false." Setting it to true did not produce an outline. Setting it together with tagged: true did. Same page, same run, four combinations:

page.pdf({ format: 'A4' })                        -> /Outlines absent
page.pdf({ format: 'A4', outline: true })         -> /Outlines absent   <-- surprise
page.pdf({ format: 'A4', tagged: true })          -> /Outlines absent
page.pdf({ format: 'A4', outline: true,
           tagged: true })                        -> /Outlines PRESENT

So if you are replacing wkhtmltopdf's default-on --outline, budget for the tagged-PDF pass as well. That is not a bad trade — a tagged PDF is the accessible one, and wkhtmltopdf could not produce it at all — but it is a second flag, and the failure mode of forgetting it is silence.

A stray @page rule beats the landscape option

preferCSSPageSize is documented as the switch that gives CSS priority over format. It governs the size. It does not govern the orientation, which the CSS wins regardless, in both directions:

page.pdf({ format: 'A4' })                                      595.9 x 842.9  portrait
  + <style>@page{size:A5 landscape}</style>                     842.9 x 595.9  LANDSCAPE A4
  + <style>@page{size:landscape}</style>                        842.9 x 595.9  LANDSCAPE A4
  + <style>@page{size:A5}</style>                               595.9 x 842.9  portrait

page.pdf({ format: 'A4', landscape: true })
  + <style>@page{size:portrait}</style>                         595.9 x 842.9  PORTRAIT

page.pdf({ format: 'A4', preferCSSPageSize: true })
  + <style>@page{size:A5 landscape}</style>                     595.0 x 420.0  A5 landscape

Read the third line: @page { size: landscape } turned an A4 portrait render landscape with preferCSSPageSize off, and the fifth line shows @page { size: portrait } overriding an explicit landscape: true. wkhtmltopdf's -O was authoritative; here the stylesheet has a vote. If a page comes out rotated after migration, grep the CSS for @page before you touch the render options.

The map, by what you were trying to do

Page geometry

wkhtmltopdfPlaywright / PuppeteerWeasyPrint
-s, --page-size A4format: 'A4' (default Letter) @page { size: A4 }
--page-width / --page-height width / height, units in the string @page { size: 210mm 297mm }
-O, --orientation Landscapelandscape: true — but see above, CSS wins@page { size: A4 landscape }
-T -B -L -R, --margin-*margin: { top, right, bottom, left }, default 0@page { margin: 20mm 10mm }
--zoom <float>, default 1, unbounded scale, clamped to 0.1–2. Outside that Chromium rejects the call: Protocol error (Page.printToPDF): scale is outside of [0.1 - 2] range CSS zoom / rem-based sizing; the CLI has no scale option
-d, --dpi, default 96 No equivalent. CSS pixels are the unit; use scale or real CSS lengths. No page DPI; -D caps image resolution
--disable-smart-shrinking Nothing to disable — the shrink-to-fit behaviour that made wkhtmltopdf's pixel/DPI ratio non-constant does not exist here. preferCSSPageSize and scale are the levers.Same: no such behaviour
--viewport-sizepage.setViewportSize() before the renderNo viewport concept; layout is the page box

What gets painted

wkhtmltopdfPlaywright / PuppeteerWeasyPrint
--background (default) / --no-background printBackground, default false Painted; suppress with CSS if you want it gone
--print-media-type (off by default) Print media always; page.emulateMedia({ media: 'screen' }) to go back -m, --media-type, defaults to print
--images / --no-images page.route() aborting image requests Custom URL fetcher, or CSS img { display: none }
--image-dpi 600 / --image-quality 94 No equivalent — images are embedded as fetched -D, --dpi and -j, --jpeg-quality, plus --optimize-images
-g, --grayscale No equivalent in either. Post-process: gs -sDEVICE=pdfwrite -sColorConversionStrategy=Gray -dProcessColorModel=/DeviceGray, or a CSS filter: grayscale(1) on html if approximate is enough.
-l, --lowquality, --no-pdf-compression No equivalent--uncompressed-pdf is the debug direction only
--user-style-sheet <path> page.addStyleTag({ path })-s, --stylesheet
--minimum-font-sizeNo page.pdf option; a Chromium launch argument exists but we did not test it, so treat this as unconfirmed No equivalent
-e, --encodingServe a correct Content-Type: text/html; charset=…, or use page.setContent() -e, --encoding — a direct match

JavaScript and waiting

wkhtmltopdfPlaywright / PuppeteerWeasyPrint
-n, --disable-javascriptcontext({ javaScriptEnabled: false }) Does not run JavaScript at all — that is the engine, not an option
--javascript-delay <msec>, default 200 waitUntil: 'networkidle', page.waitForSelector(), or page.waitForFunction() — wait for the thing, not for a guess at the clock n/a
--window-status <s> page.waitForFunction(() => window.status === 'ready') — the same trick, and it still worksn/a
--run-script <js>page.evaluate() after load, or page.addInitScript() before itn/a
--stop-slow-scripts (default)Navigation and render timeouts n/a
--load-error-handling abort|ignore|skip Check the Response object page.goto() returns and decide yourself — no policy switch, which is more code and less surprise --fail-on-http-errors
--debug-javascriptpage.on('console', …) and page.on('pageerror', …)n/a

Headers and footers — the family that costs the most

wkhtmltopdf gave you nineteen options here, including --header-html, which loads a whole HTML document. Chromium gives you two template strings, and they come with two restrictions Playwright states plainly and everybody discovers anyway: script tags inside templates are not evaluated, and page styles are not visible inside templates.

That second one has a specific, measurable failure mode. A template that relies on a class from the page's own stylesheet is not "ignored" — it renders, at Chromium's fallback size, which is far too small to see:

displayHeaderFooter: true, margin: { top: '20mm', bottom: '20mm' }

headerTemplate: '<div class="brandbar">HDRTEXT</div>'
        page stylesheet says .brandbar{font-size:22px}
        -> text IS in the PDF, glyph box height 0.8pt          (unreadable)

headerTemplate: '<div style="font-size:12px">HDRTEXT</div>'
        -> glyph box height 9.0pt                              (fine)

headerTemplate: '<div style="font-size:22px">HDRTEXT</div>'
        -> glyph box height 16.5pt

A 0.8 pt glyph box is ink on the page, so it will not show up as missing in a diff of the text layer; it looks like a hairline smudge in the margin. The fix is to inline every style the template needs, and to remember that displayHeaderFooter turns on both slots — Chromium's stock header is a date line and its stock footer is the URL, so pass an empty <span></span> for the one you did not want.

wkhtmltopdfPlaywright / PuppeteerWeasyPrint
--header-html <url> headerTemplate — a string, not a URL; inline all styling @top-left / @top-center / @top-right margin boxes in @page
--header-left/-center/-right <text> One template; place the three parts yourself Three separate margin boxes with content:
--footer-* (same six)footerTemplate @bottom-left / @bottom-center / @bottom-right
[page] / [topage] <span class="pageNumber"> / <span class="totalPages"> counter(page) / counter(pages)
[date], [title], [webpage] <span class="date">, <span class="title">, <span class="url"> string-set + content: string(…)
[section] / [subsection] No equivalent — compute it and inject it per page range, or give up and use a static header string-set: chapter content() on the heading, then content: string(chapter) in the margin box. This is the one place WeasyPrint is clearly ahead of Chromium.
--header-spacing, --header-line Margin size plus a border in the template Margin box padding and border-bottom
--header-font-name/-size, default Arial 12 Inline CSS in the template — required, see the measurement above Normal CSS on the margin box
--replace <name> <value> Build the template string in your own code — it is a string Same, or string-set
One more Chromium quirk worth knowing before you debug it: the header and footer are painted in the page margins. If the top margin is smaller than the header, you get a sliver. Reserve the space explicitly — this is why hosted renderers that accept header templates tend to force a margin on the side that carries one.

Document structure — where the object model went

wkhtmltopdf's command line was not only flags: it had objects. You could write wkhtmltopdf cover c.html toc page a.html page b.html out.pdf and get a cover sheet, a generated table of contents and two documents in one file. Nothing in the replacement set has that shape.

wkhtmltopdf2026
toc object, --toc-header-text, --toc-level-indentation, --toc-text-size-shrink, --disable-dotted-lines, --xsl-style-sheet No equivalent in Playwright. WeasyPrint builds one from CSS generated content with target-counter() for the page numbers, which is a real answer but a rewrite. In Chromium you generate the TOC markup yourself, which means you need the page numbers, which means rendering twice.
cover object Render the cover separately and concatenate — pdf-lib's copyPages(), pdftk, or qpdf --empty --pages.
Multiple page objects in one call Same: render each, then merge.
--outline (default), --outline-depth 4 outline: true plus tagged: true; no depth control. WeasyPrint emits an outline by default — we measured it present with no flags at all.
--page-offset, --exclude-from-outline No equivalent. Post-process, or restructure the document.
--title <text> Comes from <title>; in our probe the PDF's /Title was "Flag map probe document" in every renderer. To force a different one, set it with pdf-lib afterwards.
--copies, --collate Not a rendering concern any more — that is the print job.

Network, auth and file access

wkhtmltopdfPlaywright / PuppeteerWeasyPrint
--cookie <n> <v>, --cookie-jar context.addCookies(); storageState is the jar Custom URL fetcher
--custom-header, --custom-header-propagation extraHTTPHeaders on the context, documented as sent "with every request" — so the propagation switch has no counterpart Custom URL fetcher
--username / --password httpCredentials on the contextCustom URL fetcher
-p, --proxy, --bypass-proxy-for, --proxy-hostname-lookup chromium.launch({ proxy: { server, bypass, username, password } }) Standard HTTP_PROXY environment handling
--ssl-crt-path, --ssl-key-path, --ssl-key-password clientCertificates on the browser context Custom URL fetcher
--post, --post-file No equivalent. Do the POST with your HTTP client and feed the response HTML to page.setContent(), or make the endpoint accept GET. Feed it stdin
--cache-dirPersistent browser context, or an HTTP cache in front of the renderer-c, --cache-folder
--disable-local-file-access (default), --enable-local-file-access, --allow <path> Covered at length in the CVE-2022-35583 page, because this is where the security ticket lives. Short version: the restrictive one was already the default, so a wrapper passing --enable-local-file-access is a regression rather than a configuration. On the replacement side, page.route() is the in-process lever and WeasyPrint has --allowed-protocols, which restricts schemes and not hosts — neither one substitutes for an egress boundary.
--disable-external-links, --disable-internal-links, --keep-relative-links, --resolve-relative-links No equivalent in either. Link annotations are emitted from the markup; strip or rewrite the hrefs before rendering if you need them gone.

Form fields

--enable-forms is the flag that does not map cleanly, and it needs more than a table row, so it has its own page: what replaces wkhtmltopdf --enable-forms, where the same form goes through each engine and the AcroForm fields are counted. The two-line summary: Chromium produces zero form fields no matter what you pass it, and WeasyPrint produces them with --pdf-forms. The related SVG flags map to CSS:

wkhtmltopdf2026
--enable-forms / --disable-forms (default) Chromium: none. WeasyPrint: --pdf-forms / pdf_forms=True / CSS appearance: auto. Hosted APIs: a request flag, named differently by each.
--checkbox-svg, --checkbox-checked-svg, --radiobutton-svg, --radiobutton-checked-svg CSS. These existed because QtWebKit drew form controls badly; every current engine styles input[type=checkbox] with ordinary rules, or you swap in a background image.

Things you gain, which no flag map will show you

A migration table only lists losses, which makes the move look worse than it is. Four capabilities on the other side have no wkhtmltopdf flag because wkhtmltopdf could not do them:

If your templates are yours: the WeasyPrint shape

Two-thirds of the flag list above is page geometry and page furniture, and WeasyPrint's position is that all of it belongs in the stylesheet. That is either a rewrite or a relief, depending on whether you own the templates:

/* WeasyPrint has no --page-size, no --orientation, no --margin-*.
   All four live in CSS, which is also where the browser wanted them. */
@page {
  size: Letter landscape;      /* --page-size Letter  +  --orientation Landscape */
  margin: 25mm 5mm;            /* --margin-top/-bottom  --margin-left/-right     */

  @top-center   { content: 'Quarterly report'; }        /* --header-center */
  @bottom-right { content: counter(page) '/' counter(pages); }  /* --footer-right [page]/[toPage] */
}

Measured on 2026-08-17 with WeasyPrint 69.0 and no command-line options at all:

$ weasyprint page.html out.pdf     # the @page block above, nothing else
$ pdfinfo out.pdf | grep 'Page size'
Page size:       792 x 612 pts (letter)      <- landscape honoured
$ pdftotext -bbox out.pdf - | head
<word xMin="20.17" yMin="82.87" …>           <- 25mm top margin honoured

In the same run its defaults matched wkhtmltopdf's more closely than Chromium's did: the background was painted, the outline was present, and the page carried a margin without being asked. What you give up is the browser — no JavaScript, and a CSS subset rather than Chromium's. For a server-rendered invoice that is often no loss at all. For a dashboard built in React it is the end of the conversation.

Where a hosted renderer fits, and what it costs

The third option is not running a renderer. Every hosted API in this space, ours included, exposes a small fraction of these 123 flags — that is the deal, and it is worth stating precisely rather than in marketing terms.

snapdok.io takes a JSON body and returns bytes. It renders with real Chromium via Playwright, prints backgrounds by default, accepts header and footer templates (reserving the margin for them automatically, because of the sliver problem above), and can emit AcroForm fields with pdf_forms:

curl -X POST https://snapdok.io/v1/render \
  -H "Authorization: Bearer $SNAPDOK_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://app.example.com/invoice/42",
       "format":"pdf",
       "wait_until":"networkidle",
       "pdf_header":"<div style=\"font-size:10px;width:100%;text-align:center\">Invoice 42</div>",
       "pdf_forms":true}' \
  -o invoice.pdf
What that costs you, stated plainly. Paper size is A4 and is not configurable today. There is no orientation option, no margin control, no scale, no grayscale, no page ranges. If your wkhtmltopdf line carries --page-size Legal --orientation Landscape --margin-top 15mm, the Snapdok API does not currently express it and you want Playwright or WeasyPrint, both of which do. And your HTML leaves your network, which is a decision to make deliberately for documents assembled from customer data — the trade-off is discussed on the security page. What you get instead is that nobody on your team maintains a browser in a container.

The short version

Most flags map. The geometry family moves into CSS, the waiting family becomes real waits instead of a 200 ms guess, and the network family becomes browser-context options. The table of contents and cover objects have no successor and turn into code you write. --enable-forms and --grayscale have no equivalent in a Chromium renderer at all.

But the flags that break production are the ones you never typed. Port the command line literally and you get Letter paper with no backgrounds, no margins, no outline, and print media applied to templates that were written against screen media. Set printBackground, margin, format, outline with tagged, and decide about emulateMedia deliberately — then diff a real document, page by page, against one your current stack produced. Five minutes of pdftotext -bbox is cheaper than one reprint run.

For the wider decision — which replacement, and what the security ticket actually says — see the migration guide and CVE-2022-35583 explained.

Two of the defaults above have a page of their own, because they cost people whole afternoons once the port is finished: why the PDF header does not show (five causes, including the 1 px default font size inside header templates) and why landscape: true is ignored (a CSS @page rule outranks it, and preferCSSPageSize is not the fix).

Sources, all checked 2026-08-17: wkhtmltopdf 0.12.6 usage documentation (the left column, verbatim) · Playwright's page.pdf() reference, cross-checked against the type definitions shipped in playwright-core@1.62.1 · WeasyPrint API reference and the weasyprint --help output of 69.0 · MDN on @page. Every measurement above is ours, taken with Chromium 151.0.7922.34 via Playwright 1.62.1 and WeasyPrint 69.0, and reproducible with the probe document and the four command lines given near the top. We did not install wkhtmltopdf to measure it: an archived binary carrying an unpatched 9.8 does not go on a working machine to make a table symmetrical, so every claim in its column comes from its own shipped documentation.