wkhtmltopdf → EvoPdf Next

Replacing wkhtmltopdf in a .NET application

wkhtmltopdf is a command line tool and a C library built on the WebKit engine of Qt 4, frozen around 2012. The .NET libraries built on it, such as DinkToPdf, Rotativa, TuesPechkin and Pechkin, render with the same engine. Its last release, 0.12.6, dates from 2020 and the project has announced that it is no longer maintained. Flexbox and grid layouts, ECMAScript 6 code, WOFF 2 fonts and current CSS render incorrectly or not at all. EvoPdf Next renders with a current Chromium engine inside your .NET process: no external executable, no output file, the PDF comes back as bytes. This page maps every wkhtmltopdf option to its EvoPdf Next equivalent, including the options of the libraries built on it, and shows where the behavior differs, so the documents you produce today come out the same, or better, after the move.

Step 1

From a process to a method call

A wkhtmltopdf integration starts a process, waits for it, reads the output file and deletes it. In EvoPdf Next the same conversion is a method call with the options set on the converter object. The two examples produce the same A4 document with 10 mm margins from a URL.

wkhtmltopdf
wkhtmltopdf --page-size A4 \
  --margin-top 10mm --margin-bottom 10mm \
  --margin-left 10mm --margin-right 10mm \
  https://www.example.com out.pdf
var converter = new HtmlToPdfConverter();
converter.PdfDocumentOptions.LeftMargin = 28;    // 10 mm in points
converter.PdfDocumentOptions.RightMargin = 28;
converter.PdfDocumentOptions.TopMargin = 28;
converter.PdfDocumentOptions.BottomMargin = 28;   // the page is A4 by default
byte[] pdf = converter.ConvertUrl("https://www.example.com");
Sizes in EvoPdf Next are in points, 72 to the inch: 10 mm is 28 points, 1 inch is 72 points. An HTML string is converted with ConvertHtml(html, baseUrl), where the base URL resolves relative image and CSS paths, the equivalent of running wkhtmltopdf on a file in that folder. A local file is converted with ConvertUrl and a file:// URL. Several pages in one document, the wkhtmltopdf call with more than one input, are covered in Merge Multiple HTML to PDF.
Libraries built on wkhtmltopdf

From DinkToPdf, Rotativa or TuesPechkin

DinkToPdf, TuesPechkin and Pechkin call the wkhtmltopdf library from .NET; Rotativa runs wkhtmltopdf.exe on a Razor view. Their settings are the wkhtmltopdf options under other names, so the option map on this page applies to them: PaperSize and Orientation are the page layout, the margins are the page margins in points and the header and footer settings become HTML with page number variables.

DinkToPdf
var converter = new SynchronizedConverter(
    new PdfTools());
var doc = new HtmlToPdfDocument
{
    GlobalSettings = {
        PaperSize = PaperKind.A4,
        Orientation = Orientation.Portrait
    },
    Objects = {
        new ObjectSettings {
            HtmlContent = html,
            HeaderSettings = {
                Right = "Page [page] of [toPage]"
            }
        }
    }
};
byte[] pdf = converter.Convert(doc);
var converter = new HtmlToPdfConverter();
// A4 portrait is the default layout
converter.FitBrowserWindowToPage(PdfPageSize.A4);

var header = converter.PdfDocumentOptions.PdfHtmlHeader;
header.Html = "<div style=\\"text-align:right\\">" +
    "Page {page_number} of {total_pages}</div>";

byte[] pdf = converter.ConvertHtml(html, baseUrl);
Rotativa
public IActionResult Invoice(int id)
{
    var model = LoadInvoice(id);
    return new ViewAsPdf("Invoice", model)
    {
        PageSize = Size.A4,
        PageOrientation = Orientation.Portrait,
        CustomSwitches =
            "--footer-center \\"[page] / [toPage]\\""
    };
}
public IActionResult Invoice(int id)
{
    // the view, converted from its URL
    string url = Url.Action("InvoiceView", "Orders",
        new { id }, Request.Scheme);

    var converter = new HtmlToPdfConverter();
    var footer = converter.PdfDocumentOptions.PdfHtmlFooter;
    footer.Html = "<div style=\\"text-align:center\\">" +
        "{page_number} / {total_pages}</div>";

    byte[] pdf = converter.ConvertUrl(url);
    return File(pdf, "application/pdf");
}
A DinkToPdf converter has to be a singleton that runs the conversions one at a time on a single thread; an EvoPdf Next converter is created for each conversion and the conversions can run in parallel. When the Razor view needs the session of the user, forward the authentication cookie with HttpRequestCookies, as shown in the demo page Convert the Current Page.
Step 2

Page size, margins and smart shrinking

wkhtmltopdf always produces a fixed page size, A4 by default, with margins of 10 mm and smart shrinking on. The EvoPdf Next default is the same shape: an A4 page with the 1024 pixel browser window scaled to fit it, margins 0. Set the margins and, for another paper size, call FitBrowserWindowToPage with it.

What smart shrinking did

With smart shrinking on, the wkhtmltopdf default, the page is laid out wider than the paper and scaled down to fit, so text comes out smaller than in the browser. The usual remedies were --zoom 1.3 or --disable-smart-shrinking, which lays the HTML out at the paper width at 96 pixels per inch, 1:1.

What the zoom does in EvoPdf Next

It works like the print zoom of a browser: the page is laid out at the paper width divided by the zoom and drawn scaled by the zoom, so a lower zoom gives a wider layout and smaller text. Content wider than the layout is shrunk by at most 1.5 times and cut at the right edge beyond that, where wkhtmltopdf shrank without limit. The values for other pages and margins are in HTML to PDF Page Setup and Scaling.

wkhtmltopdfEvoPdf NextResult on A4
--disable-smart-shrinkingLayoutAtPageWidth(PdfPageSize.A4)The HTML is laid out at 793 pixels (the paper width) and drawn 1:1
smart shrinking on (default)FitBrowserWindowToPage(PdfPageSize.A4)A 1024 pixel desktop layout drawn at 77.47 percent
--zoom 1.3 with smart shrinking onLayoutAtPageWidth(PdfPageSize.A4)The two settings cancel each other in wkhtmltopdf; the 1:1 layout is the equivalent
--zoom 1.5 without smart shrinkingLayoutAtPageWidth(PdfPageSize.A4), then HtmlViewerZoom = 150 and HtmlViewerWidth = 529Laid out at 529 pixels and drawn at 150 percent
--viewport-size 1024x768HtmlViewerWidth = 1024, HtmlViewerHeight = 768The browser window in which the page loads and runs its scripts
--page-width 200mm --page-height 300mmPdfPageSize = new PdfPageSize(567, 850)A custom page size, in points
--orientation LandscapePdfPageOrientation = PdfPageOrientation.Landscape
--dpi, --image-dpi, --image-quality, --lowqualitynoneText and vector graphics are always exact and images keep their original data, so there is nothing to resample
Reference

Option map

The remaining wkhtmltopdf options and their EvoPdf Next equivalents. Options not listed either have no meaning in a library (--copies, --collate, --quiet, --read-args-from-stdin) or are covered by the Chromium engine without a setting.

wkhtmltopdfEvoPdf NextNotes
--print-media-typeMediaType = "print"Screen is the default in both. Select Media Type for Screen or Print
--no-backgroundPdfDocumentOptions.PrintBackgrounds = falseBackgrounds are printed by default in both
--grayscalenoneApply a CSS filter to the page or convert the colors in the PDF afterwards
--javascript-delay 2000ConversionDelay = 2Seconds in EvoPdf Next, milliseconds in wkhtmltopdf
--window-status doneTriggeringMode = TriggeringMode.Manual and evoPdfConverter.startConversion() called by the pageThe page decides when it is ready. Select Conversion Triggering Mode
--run-scriptScriptToExecuteAfterLoadJavaScript run in the page after it loads and before it is printed
--disable-javascript, -nJavaScriptEnabled = false
--no-stop-slow-scriptsNavigationTimeoutThe only limit is the navigation timeout, 120 seconds by default
--cookie name valueHttpRequestCookies.Add(name, value)Add Cookies to HTML Page Request
--custom-header name valueHttpRequestHeaders.Add(name, value)Add HTTP Headers to HTML Page Request
--post name valueHttpPostFields.Add(name, value)Access HTML Pages with GET and POST
--username, --passwordAuthenticationOptions.Username, AuthenticationOptions.PasswordConvert HTML Pages with Authentication
--enable-local-file-access, --allowLocalFilesEnabled = trueLocal files referenced by the page are blocked by default in both
--encoding utf-8noneHTML strings are .NET strings; a page loaded from a URL takes its encoding from the headers and meta tags
--user-style-sheet file.cssScriptToExecuteAfterLoad adding a <style> or <link> elementAdd it at the end of the document head so that it wins over the page CSS
--minimum-font-sizenoneFont sizes come from the CSS; a zoom above 100 enlarges the whole page
--titlePdfDocumentInfo.TitleThe HTML title is used when the property is not set
--outline, --outline-depthPdfDocumentOptions.GenerateDocumentOutline = trueAuto Create Hierarchical Bookmarks
toc object, --xsl-style-sheetPdfDocumentOptions.TableOfContents with Title, Style and CreateInlineThe style is CSS instead of XSL. Auto Create Table of Contents
cover page.htmlthe cover converted first and mergedMerge Multiple HTML to PDF
--enable-formsPdfDocumentOptions.GeneratePdfFormFields = trueCreate PDF Forms from HTML Forms
--enable-internal-links, --enable-external-linksalways onConvert Internal Links from HTML to PDF
--page-offsetPdfHtmlHeader.PageNumberOffsetSee headers and footers below
--proxy, --ssl-*, --load-error-handlingsystem proxy settings; TLS validation is the Chromium oneCertificate errors are reported as conversion errors
page-break-* rules in a user style sheetthe same CSS in the pageInsert Page Breaks in PDF Using CSS
thead repeated on every pagePdfDocumentOptions.RepeatTableHeaderFooter = trueRepeat HTML Table Header and Footer in PDF
Step 3

Headers and footers without the query string script

wkhtmltopdf has text headers (--header-left, --header-center, --header-right) and HTML headers loaded from a file with --header-html. The variables [page], [topage], [date] and [title] are replaced in text headers and passed to HTML headers as query string parameters, which a script in the header reads and writes into the elements. EvoPdf Next has one kind of header, an HTML template, in which {page_number} and {total_pages} are written directly. Lines and spacing are CSS; a text header becomes a table with three cells.

wkhtmltopdf header.html
<html><head><script>
function subst() {
  var vars = {};
  var query = document.location.search.substring(1).split('&');
  for (var i in query) {
    var pair = query[i].split('=');
    vars[pair[0]] = decodeURIComponent(pair[1]);
  }
  document.getElementById('page').textContent = vars['page'];
  document.getElementById('topage').textContent = vars['topage'];
}
</script></head>
<body onload="subst()">
  <table style="width:100%;font:10pt Arial;border-bottom:1px solid #999"><tr>
    <td>Quarterly report</td>
    <td style="text-align:right">Page <span id="page"></span>
        of <span id="topage"></span></td>
  </tr></table>
</body></html>
converter.PdfDocumentOptions.PdfHtmlHeader.Html = """
  <table style="width:100%;font:10pt Arial;border-bottom:1px solid #999"><tr>
    <td>Quarterly report</td>
    <td style="text-align:right">Page {page_number} of {total_pages}</td>
  </tr></table>
  """;
converter.PdfDocumentOptions.PdfHtmlHeader.HtmlBaseUrl = "https://www.example.com";
converter.PdfDocumentOptions.PdfHtmlHeader.Margins.Bottom = 14;   // --header-spacing 5
wkhtmltopdfEvoPdf Next
--header-html header.htmlPdfDocumentOptions.PdfHtmlHeader.HtmlSourceUrl, or PdfHtmlHeader.Html with HtmlBaseUrl; setting one of them enables the header
[page], [topage] read from the query string by a script{page_number}, {total_pages} written in the header HTML
[date], [time], [title]the value written in the header HTML by the .NET code that builds it
[section], [subsection]none; a different header per section is done by converting the sections separately and merging them
--header-left, --header-center, --header-right, --header-font-size, --header-font-namean HTML table with three cells and its CSS
--header-spacing 5 (mm)PdfHtmlHeader.Margins.Bottom = 14; the header height is measured from its content or fixed with PdfHtmlHeader.Height
--header-linea border-bottom rule in the header CSS
--page-offset 3PdfHtmlHeader.PageNumberOffset
header on every pagethe same, with ShowInFirstPage, ShowInOddPages and ShowInEvenPages to hide it on some pages
The header and footer properties are described in Add HTML Header and Footer with Page Numbers.
Engine behavior

Rendering differences to expect

Most pages look better after the move without any change, because the engine is a current Chromium. A few differences are worth knowing before comparing the documents.

Current CSS and JavaScript

  • Flexbox, grid, position: sticky, CSS variables, ECMAScript 6 and later, WOFF 2 fonts and SVG filters work as in Chrome
  • Workarounds added for wkhtmltopdf, such as tables in place of flexbox, -webkit- prefixed properties or polyfills, can be removed

Responsive pages

  • Pages that read window.innerWidth or use media queries see the browser window width, 1024 pixels by default
  • With smart shrinking, wkhtmltopdf laid such pages out at a different width, so a responsive page can change layout after the move; the zoom settings above give the desktop layout on a standard page

Page size and fonts

  • The default page in EvoPdf Next is A4 with margins 0; set the margins for every conversion moved from wkhtmltopdf
  • Fonts are the ones installed on the server or loaded by the page; a missing font shows as a substitution in both, but the substitute can differ

Images and process model

  • Images loaded lazily, with loading="lazy" or by script, are loaded before printing when LoadLazyImages is true, the default; wkhtmltopdf left them empty
  • The rendering engine runs in its own process tree, started on first use and stopped when idle; nothing to install and no zombie processes to clean up
Checklist

Migration checklist

Code

  • Replace the process start with a converter object and ConvertUrl or ConvertHtml; the PDF comes back as bytes, no output file is needed
  • Set the margins in points (10 mm is 28 points) and call FitBrowserWindowToPage or LayoutAtPageWidth with the page size and the orientation
  • Decide the text size: LayoutAtPageWidth for the --disable-smart-shrinking look, FitBrowserWindowToPage for the smart shrinking look

Options and templates

  • Move --print-media-type, --no-background, --javascript-delay, cookies, headers and credentials to the properties in the option map
  • Rewrite the header and footer HTML with {page_number} and {total_pages} in place of the query string script; replace --header-spacing with the template margins
  • Remove the workarounds added for the old engine and compare a few documents side by side

Convert your wkhtmltopdf pages on the Next engine

The demo application runs without a license key, with a stamp on the output, so you can compare the two results before changing anything in production.