Skip to main content

PDF Examples / Sample Templates

Copy-paste, runnable examples for the Muonroi PDF engine. Every template here uses only supported HTML/CSS — paste one into the render call from the engine guide and it renders deterministically on any OS.

All examples assume the pipeline is registered once at startup:

builder.Services.AddPdf(builder.Configuration);

and that you inject IMPdfService pdf.

Runnable version: every scenario below is also a working console app in the building-block repo at samples/Muonroi.Pdf.Samplesdotnet run --project samples/Muonroi.Pdf.Samples renders them all to pdf-output/.


1. Minimal render

The smallest possible end-to-end render.

const string html = """
<!DOCTYPE html>
<html><head><style>
body { font-family: Arial, sans-serif; font-size: 12pt; color: #222; }
h1 { color: #0c6b6b; }
</style></head>
<body>
<h1>Hello, Muonroi.Pdf</h1>
<p>Pure-managed HTML → PDF. No browser, no native binary.</p>
</body></html>
""";

await using var output = File.OpenWrite("hello.pdf");
PdfRenderResult result = await pdf.RenderAsync(html, output, new PdfRenderOptions());
Console.WriteLine($"{result.PageCount} page(s), {result.ByteCount} bytes");

2. Invoice (tables, floats, totals)

The bread-and-butter business document: a header block, a line-item table with % column widths and table-layout: fixed, and a right-aligned totals box. No flex/grid needed.

<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; font-size: 11pt; color: #222; margin: 0; }
.head { overflow: hidden; margin-bottom: 16px; }
.head .brand { float: left; font-size: 18pt; font-weight: bold; color: #0c6b6b; }
.head .meta { float: right; text-align: right; font-size: 10pt; color: #555; }

table { width: 100%; border-collapse: collapse; table-layout: fixed; }
th, td { border: 1px solid #ccc; padding: 6px 8px; }
th { background-color: #0c6b6b; color: #fff; text-align: left; }
.num { text-align: right; }
.col-desc { width: 55%; }
.col-qty { width: 15%; }
.col-amt { width: 30%; }

.totals { float: right; width: 40%; margin-top: 12px; }
.totals td { border: none; padding: 2px 8px; }
.totals .grand { font-weight: bold; border-top: 2px solid #0c6b6b; }
</style>
</head>
<body>
<div class="head">
<div class="brand">ACME Corp</div>
<div class="meta">Invoice #INV-2026-0042<br/>Date: 2026-06-22</div>
</div>

<table>
<thead>
<tr><th class="col-desc">Description</th><th class="col-qty num">Qty</th><th class="col-amt num">Amount</th></tr>
</thead>
<tbody>
<tr><td>Consulting services</td><td class="num">10</td><td class="num">$1,500.00</td></tr>
<tr><td>Hosting (annual)</td><td class="num">1</td><td class="num">$600.00</td></tr>
<tr><td>Support plan</td><td class="num">1</td><td class="num">$300.00</td></tr>
</tbody>
</table>

<table class="totals">
<tr><td>Subtotal</td><td class="num">$2,400.00</td></tr>
<tr><td>Tax (10%)</td><td class="num">$240.00</td></tr>
<tr class="grand"><td>Total</td><td class="num">$2,640.00</td></tr>
</table>
</body>
</html>

Use the programmatic PdfHeaderFooter for a 3-column band with a logo, title, and page numbers via counter(page) / counter(pages).

var options = new PdfRenderOptions
{
PageSize = PdfPageSize.A4,
Orientation = PdfOrientation.Portrait,
Margins = PdfMargins.Uniform(15),
Header = new PdfHeaderFooter(
LeftHtml: "<img src=\"data:image/png;base64,iVBORw0K...\" style=\"width:64px;height:38px;\" />",
CenterHtml: "<b style=\"color:#0c6b6b;\">Quarterly Report</b>",
RightHtml: "Page counter(page)/counter(pages)",
HeightMm: 20,
ShowLine: true),
Footer = new PdfHeaderFooter(
CenterHtml: "Confidential — counter(page)/counter(pages)",
HeightMm: 12,
ShowLine: true),
TemplateId = "quarterly-report",
};

await pdf.RenderAsync(reportHtml, output, options);

Only counter(page) / counter(pages) are recognized as page tokens. wkhtmltopdf-style [page] / [topage] are not supported.

Pure-CSS alternative (@page margin boxes)

If you'd rather keep header/footer in the template, use @page margin boxes — no API call:

<style>
@page {
size: A4;
margin: 20mm 15mm;
@top-center { content: "Quarterly Report"; }
@bottom-center { content: "Page " counter(page) " of " counter(pages); }
}
</style>

A programmatic Header/Footer overrides the matching @page band (API wins per band).


4. Watermark & gradient header band

A diagonal watermark via transform: rotate(...) and a graded header band via linear-gradient (rendered as a PDF axial shading). A soft radial-gradient vignette also works.

<style>
.watermark {
position: absolute; top: 320pt; left: 120pt;
font-size: 64pt; color: #d0d0d0;
transform: rotate(-35deg); /* pivots about box center */
}
.banner {
height: 60px; color: #fff; padding: 16px;
background: linear-gradient(90deg, #0c6b6b 0%, #13a89e 100%);
}
.vignette {
background: radial-gradient(ellipse at center, #ffffff 0%, #eef3f3 100%);
padding: 24px;
}
</style>

<div class="banner"><h1>Certificate of Completion</h1></div>
<div class="vignette">
<div class="watermark">DRAFT</div>
<p>Awarded to …</p>
</div>

Supported transform functions: translate/scale/rotate/skew/matrix and chains. Supported gradients: linear-gradient and radial-gradient (circle/ellipse). conic-gradient/repeating-*, non-center transform-origin, and 3D transforms are rejected.


5. Modern layout — Flexbox (opt-in)

Flexbox renders for real only when PdfConfigs:Policy:AllowModernLayout = true (see Modern layout). Enable it once:

{
"PdfConfigs": {
"Policy": { "AllowModernLayout": true }
}
}

Then a row of equal-height cards is a few lines instead of a table:

<style>
.cards { display: flex; flex-direction: row; gap: 12px; align-items: stretch; }
.card { flex: 1 1 0; border: 1px solid #ccc; padding: 12px; }
.card h3 { margin: 0 0 6px; color: #0c6b6b; }
</style>

<div class="cards">
<div class="card"><h3>Revenue</h3><p>$2.64M</p></div>
<div class="card"><h3>Orders</h3><p>1,204</p></div>
<div class="card"><h3>Refunds</h3><p>1.8%</p></div>
</div>

Supported: flex-direction, flex-wrap, justify-content, align-items, align-content, gap/row-gap/column-gap; item flex-grow/flex-shrink/flex-basis/order/align-self.


6. Modern layout — CSS Grid (opt-in)

Also behind AllowModernLayout. A responsive product grid using repeat(auto-fill, minmax(...)):

<style>
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 10px;
}
.tile { border: 1px solid #ddd; padding: 10px; height: 80px; }
</style>

<div class="grid">
<div class="tile">SKU-001</div>
<div class="tile">SKU-002</div>
<div class="tile">SKU-003</div>
<div class="tile">SKU-004</div>
<div class="tile">SKU-005</div>
</div>

A named-area dashboard layout:

<style>
.dash {
display: grid;
grid-template-columns: 200px 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
"side header"
"side main";
gap: 8px;
}
.dash .h { grid-area: header; background: #0c6b6b; color: #fff; padding: 8px; }
.dash .side { grid-area: side; border: 1px solid #ccc; padding: 8px; }
.dash .main { grid-area: main; padding: 8px; }
</style>

<div class="dash">
<div class="h">Dashboard</div>
<div class="side">Filters</div>
<div class="main">Content…</div>
</div>

Supported track sizes: px/pt/%/fr/auto/minmax()/repeat() incl. repeat(auto-fill|auto-fit). Sparse grid-auto-flow only (dense is ignored); subgrid is not implemented.


7. Images (data-URI + resolver)

Inline images as data-URIs — no resolver needed. PNG color types supported: RGB, palette, RGBA (composited onto white), grayscale, and grayscale+alpha; plus JPEG.

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..." style="width:120px;height:60px;" />

To load images from blob storage or a URL, register an IResourceResolver before AddPdf:

public sealed class BlobResourceResolver(IBlobStore store) : IResourceResolver
{
public async Task<ResourceResult?> ResolveAsync(Uri uri, string? hint, CancellationToken ct)
{
byte[]? bytes = await store.TryGetAsync(uri.ToString(), ct);
return bytes is null ? null : new ResourceResult(bytes, hint ?? "image/png");
}
}

builder.Services.AddSingleton<IResourceResolver, BlobResourceResolver>();
builder.Services.AddPdf(builder.Configuration);

Then reference images by URL: <img src="https://cdn.example/logo.png" />. 16-bit and sub-8-bit grayscale PNG are rejected at decode (PNG-16BIT / PNG-GRAYSCALE-DEPTH) — convert to 8-bit.


8. Multi-page document

RenderMultiPageAsync renders several HTML fragments into one PDF; each fragment starts on a new page (page counters continue across the whole document).

var pages = new[]
{
"<h1>Cover</h1><p>Annual Report 2026</p>",
"<h2>Financials</h2><table>…</table>",
"<h2>Appendix</h2><p>…</p>",
};

await pdf.RenderMultiPageAsync(pages, output, new PdfRenderOptions
{
Footer = new PdfHeaderFooter(
CenterHtml: "counter(page)/counter(pages)", HeightMm: 10, ShowLine: false),
});

To force a page break within one HTML document instead, use CSS:

<div style="page-break-before: always;">…next section…</div>

9. Handling policy rejections

When a template uses a feature outside the subset, the engine throws PdfPolicyException before rendering — each violation names the rule, selector, rejected value, and a suggested fix.

try
{
await pdf.RenderAsync(html, output, options);
}
catch (PdfPolicyException ex)
{
foreach (var v in ex.Violations)
logger.LogWarning("PDF policy: {Rule} on {Selector}: {Value} → {Fix}",
v.RuleId, v.CssSelector, v.RejectedValue, v.SuggestedAlternative);
throw;
}

During migration, set PdfConfigs:Policy:SoftDegradeUnknownDisplay = true to downgrade display:flex/grid to block (warnings instead of errors), or AllowModernLayout = true to render them for real.


See also