---
title: "Convert HTML to PDF"
url: https://recursiveai.org/onerender/cookbook/html-to-pdf/
description: "Turn an HTML page into a tagged PDF in C# with OneRender, with no browser: set the page size and margins, write metadata, stream the file to a response…"
version: "1.0.0"
---

# Convert HTML to PDF

Turn an HTML page into a tagged PDF in C# with OneRender, with no browser: set the page size and margins, write metadata, stream the file to a response, and keep links working.

```csharp
using OneRender.Pdf;

string html = """
<!DOCTYPE html>
<html lang="en">
<head>
<title>Packing slip 5521</title>
<style>
  body { font: 11pt/1.4 sans-serif; margin: 0 }
  h1 { font-size: 16pt; margin: 0 0 8px }
  ul { padding-left: 18px }
</style>
</head>
<body>
  <h1>Packing slip 5521</h1>
  <p>Ship to: Harbor Street Books, 14 Harbor Street</p>
  <ul><li>Shelf labels × 400</li><li>Reading lamps × 6</li></ul>
  <p><a href="https://example.com/track/5521">Track this order</a> · <a href="#returns">Returns</a></p>
  <h2 id="returns" style="break-before: page">Returns</h2>
  <p>Return anything within 30 days.</p>
</body>
</html>
""";

// 4 by 6 inches, a common label size, with quarter-inch margins. Units are CSS px: 96 per inch.
var label = new PdfPageSetup(width: 4 * 96, height: 6 * 96, marginTop: 24, marginRight: 24, marginBottom: 24, marginLeft: 24);

var options = new HtmlPdfOptions
{
    Page = label,
    Output = new PdfOutputOptions
    {
        Author = "Harbor Street Books",
        Subject = "Order 5521",
        CreationDate = "D:20261027090000Z",   // a PDF date string; the default is none
    },
};

byte[] bytes = HtmlPdf.ToPdf(html, options);
File.WriteAllBytes("slip.pdf", bytes);
Console.WriteLine($"slip.pdf: {bytes.Length:N0} bytes");

// In a web endpoint, write straight to the response body instead: pages stream out as they are finished.
var response = new MemoryStream();
HtmlPdfResult result = HtmlPdf.ToPdf(html, response, options);
Console.WriteLine($"{result.PageCount} pages, same bytes: {response.ToArray().AsSpan().SequenceEqual(bytes)}");
```

```text
slip.pdf: 17,149 bytes
2 pages, same bytes: True
```

The file is 4 by 6 inches, two pages long, titled “Packing slip 5521” in English, with the author and subject set. The link to the tracking page is a link annotation, and “Returns” jumps to the heading on page 2.

## What goes into the file

- **Structure.** The PDF is tagged from the HTML: headings, paragraphs, lists, tables with header scopes, links, and images with their `alt` text. Turn it off with `Tagged = false` in `PdfOutputOptions`.
- **Title and language** come from `<title>` and `<html lang>`, unless `Title` or `Language` is set in the options.
- **Text** is real text in subsetted fonts, so it can be selected, searched and extracted, including ligatures and right-to-left runs.
- **Vector output.** Backgrounds, borders, gradients, shadows without blur, transforms and clips are written as PDF drawing operations. Effects PDF can’t express, such as blurred shadows and filters, are rasterized at `RasterDpi` (192 by default) for just that area.
- **The same bytes every time.** Nothing reads the clock: with no `CreationDate`, the same input gives the same file on every platform, which makes PDFs easy to compare in tests.

## Good to know

- Pass a stream to write page by page. A `PdfProfile` other than `None` (see [Accessible PDF](https://recursiveai.org/onerender/cookbook/accessible-pdf/)) builds the file in memory first, then writes it.
- `PdfPageSetup.A4` and `PdfPageSetup.Letter` have 48 px (half-inch) margins. A `@page` rule in the document wins over the options: see [Headers, footers and page breaks](https://recursiveai.org/onerender/cookbook/paged-media/).
- Style sheets, images and fonts come through `BaseUrl` and `Resources`, as in [Getting started](https://recursiveai.org/onerender/docs/getting-started/#supply-style-sheets-and-images). Scripts don’t run, so render the final HTML on the server, for example from a [Razor component](https://recursiveai.org/onerender/cookbook/razor-to-pdf/).
- `MaxPages` (100,000 by default) stops a runaway document with a `PdfRenderException`.

Every rule of the writer is in [PDF output](https://recursiveai.org/onerender/docs/pdf/).
