Skip to content

Repository files navigation

HtmlPdfPLus Logo Welcome to HtmlPdfPlus

Lightweight and scalable HTML to PDF converter in .NET.

GitHub license Build Publish .NET

The best tool to convert HTML to PDF in .NET with a modern engine

  • Client : NuGet Client NuGet Client
  • Server : NuGet Server NuGet Server

Table of Contents

Project Description

HtmlPdfPlus is a modern and lightweight library for .Net10, .Net9 and .Net8 that allows you to convert HTML or RAZOR pages to PDF with high fidelity.

It is a scalable and flexible solution that can be used in client-server mode or only server. It supports CSS and JavaScript, and it is easy to integrate with your application.

You can customize the PDF settings, such as page size and margins, and add headers and footers to your PDF files. HtmlPdfPlus is a powerful tool that can help you generate PDF files from HTML or RAZOR pages with ease.

This library was built using the Playwright (engine to automate Chromium, Firefox, and WebKit with a single API). Playwright is built to enable cross-browser web automation that is evergreen, capable, reliable, and fast.

As of the Playwright version this library currently targets (see the Microsoft.Playwright reference in HtmlPdfPlus.Server.csproj), its PDF generation API supports only the Chromium browser.

Features

Top

  • Convert HTML or RAZOR page to PDF with high fidelity
  • Support for CSS and JavaScript
  • Asynchronous API
  • Customizable PDF settings (e.g., page size, margins)
  • Support for headers and footers
  • Lightweight and easy to integrate
  • Flexible and scalable (Client-Server mode or only Server)
  • Support HTML5 and CSS3
  • Communicate with the server using REST API (with compressed request) or user custom protocol
  • Minify HTML and CSS
  • Client-side HTML parser with custom error action (optional)
  • Requests are sent as gzip-compressed raw bytes by default, no base64/JSON-string wrapping (see ADR-003)
  • A successful byte[] response is served as the raw PDF body, not wrapped in JSON
  • Extension on server side to customize the conversion process (before and after conversion)
    • BeforePDF : Normalize HTML, Replace tokens, etc
    • AfterPDF : Save file, Send to cloud, etc
  • Disable features to improve/balance performance (minify, compress and log)
  • Backpressure signaled via ErrorCode.PoolExhausted + a real Retry-After, automatic browser recovery, liveness/readiness endpoints, and System.Diagnostics.Metrics instrumentation - see the resilience guide

What's new

Current version: 2.0.0. Full version history has moved to CHANGELOG.md.

Prerequisites

Top

  • .NET 8, .NET 9 or .NET 10 (SDK to build from source or develop against the library; the runtime alone is enough to just run an app that already references the NuGet packages)
  • Visual Studio 2026 or later - optional, only if that's your editor of choice; any .NET-capable IDE/CLI works
  • Playwright browser binaries - only required on whatever machine actually runs HtmlPdfPlus.Server (your dev machine while testing the server, a VM, a container). HtmlPdfPlus.Client never launches a browser and does not need this at all. See Installation Steps for Playwright below, or the Docker guide if you're deploying in a container instead.

Installing

Top

Installation Steps for Playwright (Windows)

Only needed on a machine that runs HtmlPdfPlus.Server outside a container - skip this if you're only building against HtmlPdfPlus.Client, or deploying with Docker (see the Docker guide, which bundles the browser inside the image instead).

dotnet tool update --global PowerShell
dotnet tool install --global Microsoft.Playwright.CLI
playwright.exe install --with-deps

Note: Make sure that the path to the executable is mapped to: C:\Users\[login]\.dotnet\tools.

If it is not, run it directly via the path C:\Users\[login]\.dotnet\tools\playwright.exe install --with-deps

Installation Steps for HtmlPdfPlus

Client library can be installed via NuGet or line command.

Install-Package HtmlPdfPlus.Client [-pre]
dotnet add package HtmlPdfPlus.Client [--prerelease]

Server library can be installed via NuGet or line command.

Install-Package HtmlPdfPlus.Server [-pre]
dotnet add package HtmlPdfPlus.Server [--prerelease]

Note: [-pre]/[--prerelease] usage for pre-release versions

Getting Started

Top

Follow these steps to get started with HtmlPdfPlus:

  1. Install the necessary packages using NuGet.
  2. Configure the services in your application.
  3. Use the provided API to convert HTML to PDF.

Usage

Top

It is possible to generate a PDF in two ways:

1) Using client-server mode

1.1) Via http

sequenceDiagram
    participant AppClient as App Client
    participant HtmlPdfClient
    participant AppServer as App Server
    participant HtmlPdfServer

    HtmlPdfServer->>AppServer: AddHtmlPdfService
    AppServer-->>AppServer: Warmup HtmlPdfService

    Note over AppClient,HtmlPdfClient: Minify, Compress and Logging can be disabled (via DisableOptionsHtmlToPdf)

    AppClient->>HtmlPdfClient: FromHtml
    HtmlPdfClient-->>HtmlPdfClient: Minify HTML
    AppClient->>HtmlPdfClient: FromRazor
    HtmlPdfClient-->>HtmlPdfClient: Execute Razor engine, minify HTML
    AppClient->>HtmlPdfClient: FromUrl
    AppClient->>HtmlPdfClient: PageConfig / Timeout
    AppClient->>HtmlPdfClient: Run (optional input param)
    HtmlPdfClient-->>HtmlPdfClient: Build RequestHtmlPdf, gzip it
    HtmlPdfClient->>AppServer: HTTP POST (gzip bytes or plain JSON as the raw body)

    AppServer->>HtmlPdfServer: BeforePDF hook (optional)
    AppServer->>HtmlPdfServer: AfterPDF hook (optional)
    AppServer->>HtmlPdfServer: Run
    HtmlPdfServer-->>HtmlPdfServer: Decompress to RequestHtmlPdf
    HtmlPdfServer-->>HtmlPdfServer: Exec BeforePDF(input param)
    HtmlPdfServer-->>HtmlPdfServer: Generate PDF
    HtmlPdfServer-->>HtmlPdfServer: Exec AfterPDF(input param, transform output)
    HtmlPdfServer->>AppServer: HtmlPdfResult

    Note over AppServer,HtmlPdfClient: byte[] success returns the raw PDF body (application/pdf), any other outcome returns ErrorInfo/HtmlPdfResult as JSON
    AppServer->>HtmlPdfClient: HTTP response
    HtmlPdfClient->>AppClient: HtmlPdfResult
Loading

basic usage client side

using HtmlPdfPlus;

...
Host.CreateDefaultBuilder(args).ConfigureServices((hostContext, services) =>
{ 
   services.AddHttpClient("HtmlPdfServer", httpClient =>
   {
      httpClient.BaseAddress = new Uri("https://localhost:7212/GeneratePdf");
   });
});
...

//client http to endpoint    
var clienthttp = HostApp!.Services
	.GetRequiredService<IHttpClientFactory>()
	.CreateClient("HtmlPdfServer");

//create client instance and send to HtmlPdfPlus server endpoint    
var pdfresult = await HtmlPdfClient
    .Create("HtmlPdfPlusClient")
    .PageConfig((cfg) =>
    {
       cfg.Margins(10)
          .Footer("'<span style=\"text-align: center;width: 100%;font-size: 10px\"> <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></span>")
          .Header("'<span style=\"text-align: center;width: 100%;font-size: 10px\" class=\"title\"></span>")
          .Orientation(PageOrientation.Landscape)
          .DisplayHeaderFooter(true);
     })
     .Logger(HostApp.Services.GetService<ILogger<Program>>())
     .FromHtml(HtmlSample())
     .Timeout(5000)
     .Run(clienthttp, applifetime.ApplicationStopping);

//performs writing to file after performing conversion
if (pdfresult.IsSuccess)
{
    await File.WriteAllBytesAsync("html2pdfsample.pdf", pdfresult.OutputData!);
}
else
{
    //show error via pdfresult.Error
}

basic usage Server side

using HtmlPdfPlus;

...
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
builder.Services.AddHtmlPdfService((cfg) =>
{
    cfg.Logger(LogLevel.Debug, "MyPDFServer");
});
var app = builder.Build();
app.MapOpenApi();
...

// The request/response contract (raw PDF on success, structured ErrorInfo on failure) comes
// straight from the library, so the OpenAPI document generated above actually describes it.
app.MapHtmlPdfEndpoints("/GeneratePdf");

1.2) Via any process

sequenceDiagram
    participant AppClient as App Client
    participant HtmlPdfClient
    participant Submit as Func.Submit (custom transport)
    participant AppServer as App Server
    participant HtmlPdfServer

    HtmlPdfServer->>AppServer: AddHtmlPdfService
    AppServer-->>AppServer: Warmup HtmlPdfService

    Note over AppClient,HtmlPdfClient: Minify, Compress and Logging can be disabled (via DisableOptionsHtmlToPdf)

    AppClient->>HtmlPdfClient: FromHtml / FromRazor / FromUrl
    HtmlPdfClient-->>HtmlPdfClient: Minify HTML (and execute Razor engine, if FromRazor)
    AppClient->>HtmlPdfClient: PageConfig / Timeout
    AppClient->>HtmlPdfClient: Run(Submit, optional input param)
    HtmlPdfClient-->>HtmlPdfClient: Build RequestHtmlPdf, gzip it
    HtmlPdfClient->>Submit: Execute Submit(bytes)
    Submit->>AppServer: caller-defined transport (TCP, queue, gRPC, ...)

    AppServer->>HtmlPdfServer: BeforePDF hook (optional)
    AppServer->>HtmlPdfServer: AfterPDF hook (optional)
    AppServer->>HtmlPdfServer: Run
    HtmlPdfServer-->>HtmlPdfServer: Decompress to RequestHtmlPdf
    HtmlPdfServer-->>HtmlPdfServer: Exec BeforePDF(input param)
    HtmlPdfServer-->>HtmlPdfServer: Generate PDF
    HtmlPdfServer-->>HtmlPdfServer: Exec AfterPDF(input param, transform output)
    HtmlPdfServer->>AppServer: HtmlPdfResult<TOut>
    AppServer->>Submit: caller-defined transport response
    Submit-->>HtmlPdfClient: HtmlPdfResult<TOut>
    HtmlPdfClient->>AppClient: HtmlPdfResult<TOut>
Loading

Unlike the HTTP path above, the wire format between Submit and App Server is entirely up to the caller's own Submit delegate - the library only hands it request bytes and expects an HtmlPdfResult<TOut> back, so there is no built-in compress/decompress step to describe on the response side (see ClientSendTcp for a working example over raw TCP).

basic usage client side

using HtmlPdfPlus;

// Generic suggestion for writing a file to a cloud like gcp/azure
// Suggested return would be the full path "repo/filename"
var paramTosave = new DataSavePDF("Filename.pdf","MyRepo","MyConnectionstring");

var pdfresult = await HtmlPdfClient.Create("HtmlPdfPlusClient")
      .PageConfig((cfg) =>
      {
         cfg.Margins(10);
      })
      .Logger(HostApp.Services.GetService<ILogger<Program>>())
      .FromRazor(TemplateRazor(), order1)
      .Timeout(50000)
      .Run<DataSavePDF,string>(SendToServer,paramTosave, applifetime.ApplicationStopping);

//Shwo result
if (pdfresult.IsSuccess)
{
   Console.WriteLine($"File PDF generate at {pdfresult.OutputData}");
}
else
{
    Console.WriteLine($"HtmlPdfClient error: {pdfresult.Error!}");
}

private static async Task<HtmlPdfResult<string>> SendToServer(byte[] requestdata, CancellationToken token)
{
   //send requestdata to server and return result
}

basic usage Server side

using HtmlPdfPlus;

...
var builder = WebApplication.CreateBuilder(args);  
builder.Services.AddHtmlPdfService<DataSavePDF,string>((cfg) =>
{
    cfg.Logger(LogLevel.Debug, "MyPDFServer");
});
...
var PDFserver = HostApp.Services.GetHtmlPdfService();

var result = await PDFserver
        .ScopeRequest(data)
        .BeforePDF( (html,inputparam, _) =>
        {
            if (inputparam is null)
            {
                return Task.FromResult(html);
            }
            //performs replacement token substitution in the HTML source before performing the conversion
            var aux = html.Replace("[{FileName}]", inputparam.Filename);
            return Task.FromResult(aux);
        })
        .AfterPDF( (pdfbyte, inputparam, token) =>
        {
            if (inputparam is null)
            {
                return Task.FromResult(string.Empty);
            }
            //TODO : performs writing to file  after performing conversion
            return Task.FromResult(inputparam.Filename);
        })
        .Run(token);

//send result to client

2) Using ony-server

sequenceDiagram
    participant AppServer as App Server
    participant HtmlPdfServer

    HtmlPdfServer->>AppServer: AddHtmlPdfService
    AppServer-->>AppServer: Warmup HtmlPdfService

    Note over AppServer,HtmlPdfServer: Minify and Logging can be disabled (via DisableFeatures on the builder) - there is no network hop here, so there is nothing to compress/decompress

    AppServer->>HtmlPdfServer: FromHtml / FromRazor / FromUrl
    HtmlPdfServer-->>HtmlPdfServer: Minify HTML (and execute Razor engine, if FromRazor)
    AppServer->>HtmlPdfServer: Input param / Timeout / PageConfig (all optional)
    AppServer->>HtmlPdfServer: BeforePDF / AfterPDF hooks (optional)
    AppServer->>HtmlPdfServer: Run

    HtmlPdfServer-->>HtmlPdfServer: Exec BeforePDF(input param)
    HtmlPdfServer-->>HtmlPdfServer: Generate PDF
    HtmlPdfServer-->>HtmlPdfServer: Exec AfterPDF(input param, transform output)
    HtmlPdfServer->>AppServer: HtmlPdfResult
Loading

basic usage

using HtmlPdfPlus;

...
Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) =>
    {
        services.AddHtmlPdfService((cfg) =>
        {
            cfg.Logger(LogLevel.Debug, "MyPDFServer")
               .DefaultConfig((page) =>
               {
                   page.DisplayHeaderFooter(true)
                       .Margins(10, 10, 10, 10);
               });
        });
    });
...

//instance of Html to Pdf Engine and Warmup HtmlPdfServerPlus
var PDFserver = HostApp!.Services.GetHtmlPdfService();

//Performs conversion and custom operations on the server
var pdfresult = await PDFserver
       .ScopeData()
       .FromHtml(HtmlSample(),5000)
       .Run(applifetime.ApplicationStopping);

//performs writing to file after performing conversion
if (pdfresult.IsSuccess)
{
    await File.WriteAllBytesAsync( "html2pdf.pdf", pdfresult.OutputData!);
}
else
{
    //show error via pdfresult.Error
}

Docker Usage

Top

The use of Playwright works very well for local testing on Windows machines following the standard installation instructions. For containerization scenarios, image size is worth a closer look: the working Dockerfile keeps only the one browser variant the code actually launches, cutting the image from 763MB to 370MB. See the Docker guide for the full before/after numbers, what changed, and why.

Examples

Top

Each sample is scoped to one clear lesson. For more examples, please refer to the Samples directory:

  • Server Only - no client, no network, HTML/URL converted in the same process
  • Client-Server (HTTP) - client and server as separate processes over HttpClient
    • ClientSendHttp - one client walking through all three content sources (FromHtml, FromRazor with a typed model, FromUrl) against the same generic server
    • WebHtmlToPdf.GenericServer - the server side: MapHtmlPdfEndpoints(), AddOpenApi(), and the health endpoints, in as few lines as the library allows
  • Client-Server Custom - customizing what the server returns instead of raw PDF bytes
    • ClientCustomSendHttp - typed input/output (DataSavePDF) standing in for "save the PDF to cloud storage, return its path"
    • WebHtmlToPdf.CustomSaveFileServer - the matching server: token substitution via BeforePDF, then AfterPDF turning the PDF bytes into a saved-file result
  • Client-Server TCP - swapping the transport for a non-HTTP one (⚠ demonstrates shipping flexibility only, not production-ready)
  • Cross-language - consuming the server from outside .NET
    • JavaClientSendHttp - a single dependency-free .java file (JDK's own HttpClient + GZIPOutputStream, no build tool) showing the exact wire format any non-.NET client must produce: JSON → gzip → POST as application/octet-stream - see the file header for the javac/java commands and which server profile to run
  • Production readiness - the resilience/observability features covered in the resilience guide, each with a deliberately tiny page pool (PagesBuffer(1)) so the behavior being demonstrated is easy to reproduce on any machine instead of depending on real render timing
    • RetryAfterBackpressure - firing concurrent requests, detecting ErrorCode.PoolExhausted, and backing off using ErrorInfo.RetryAfterSeconds before retrying
    • MetricsObserver - attaching a MeterListener (no OTel/exporter package needed) to observe the instruments a healthy run produces (htmlpdfplus.pool.available_pages, .request.duration, .errors, .pool.acquire_wait), including how a validation failure increments htmlpdfplus.errors without touching htmlpdfplus.request.duration - htmlpdfplus.browser.restarts only appears after an unexpected disconnect, so it stays silent here

/healthz and /readyz are mapped by the two web server samples above (via MapHtmlPdfHealthEndpoints()), but no sample calls them from a client or shows what a real orchestrator would do with the response. See the resilience guide for how they work until a dedicated sample exists.

Architecture

Top

Package layout (Client/Server/Shared), the one distinction that decides whether a request gets compressed (ScopeData() vs ScopeRequest(bytes)), the page pool and browser lifecycle, and where each piece of configuration is supposed to live: see the Architecture guide.

How-To

Top

Task-oriented recipes, one page per use case - rendering content, running client and server as separate processes, customizing the pipeline, handling failures: see the How-To index.

API Reference

Top

The library has a main namespace HtmlPdfPlus for client and server, and all methods use a fluent interface. The full generated reference for every public type is in the Docs directory.

Architecture Decision Records (ADR)

Top

HtmlPdfPlus documents its significant architectural and design decisions as Architecture Decision Records (ADR), following the adrplus convention. Each record captures the context, the decision, the alternatives considered, and the consequences - so the reasoning behind the library's design stays traceable over time.

👉 See the ADR index for the full list of decisions.

Documentation

Top

Deeper operational guides live under docs/guide: resilience and observability, Docker. Version history is in CHANGELOG.md.

Code of Conduct

Top

This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community. For more information see the Code of Conduct.

Contributing

Top

Please read Contributing for details on our code of conduct, and the process for submitting pull requests to us.

Credits

Top

API documentation generated by

License

Top

This project is licensed under the MIT License - see the License file for details.

Disclaimer : HtmlPdfPlus includes PackageReference from other software released under other licences:

FAQ

Top

Common questions, answered briefly: see the FAQ page.

About

Lightweight and scalable HTML to PDF converter in .NET

Topics

Resources

Code of conduct

Contributing

Stars

14 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages