Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

12 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

HtmlPdf β€” HTML to PDF as a Microservice

A production-ready, self-contained PDF generation service built on real browser rendering.
Drop in a Razor template. Call an API. Get a pixel-perfect PDF. That's it.

Docker Hub .NET License: MIT


Why This Exists

PDF generation is a solved problem β€” until you try to do it right.

Most libraries render HTML in a stripped-down engine with partial CSS support, missing fonts, broken layouts, and no Flexbox. The result: PDFs that look nothing like the design.

This project was born from that frustration. Instead of fighting with rendering quirks, HtmlPdf delegates the entire rendering pipeline to a real Chromium browser β€” the same engine your users see every day. Full CSS3, Flexbox, Grid, custom fonts, @page rules. If it renders in Chrome, it renders in your PDF.

Beyond that, the service is built to be plug-and-play for any system. You don't need to migrate your stack. Call the REST API from any language, any platform, any legacy system. Add custom templates at runtime without touching the code or restarting the container.

This project is part of my professional portfolio β€” a demonstration of how I approach real engineering problems: with clean architecture, thoughtful design decisions, and production-grade quality.


What You Get

Feature Details
πŸ–¨οΈ Real Browser Rendering Full Chromium engine β€” pixel-perfect PDFs with complete CSS3, Flexbox, Grid support
πŸ“‚ Dynamic Templates Drop a .cshtml file into a volume, restart, get a new endpoint. No code changes
⚑ Hot-Reload Config Change concurrency limits and template whitelists while the service runs
πŸ”’ Secure by Default Template whitelist prevents path traversal β€” only explicitly allowed templates are served
🐳 Docker-First One docker compose up and you're live. Chromium cached in a persistent volume
🌐 Language Agnostic Pure REST API β€” call it from Python, Java, PHP, Ruby, legacy .NET, anything
πŸ” Concurrent Processing Configurable semaphore-based concurrency β€” handles simultaneous requests safely

Architecture at a Glance

HTTP POST
    β”‚
    β–Ό
Endpoint Handler          ← auto-discovered via reflection
    β”‚
    β–Ό
Template Renderer         ← RazorLight compiles .cshtml β†’ HTML (cached)
    β”‚
    β–Ό
PDF Renderer              ← PuppeteerSharp sends HTML to headless Chromium
    β”‚
    β–Ό
PDF bytes β†’ Response

Two flavors of endpoints:

  • Static β€” strongly-typed DTO, defined in code, registered at build time
  • Dynamic β€” .cshtml file dropped in a volume folder, registered at startup automatically

Quick Start

Option A β€” Docker (recommended, zero setup)

docker pull svctech/svc-html-pdf

Or with docker compose:

# docker-compose.yml
name: htmlpdf-stack

services:
  htmlpdf:
    image: svctech/svc-html-pdf:latest
    ports:
      - "6000:8080"
    volumes:
      - chromium_data:/app/chromium
      - ./my-templates:/app/Templates/DynamicTemplates
    restart: unless-stopped

volumes:
  chromium_data:
docker compose up -d

The first start downloads Chromium into the chromium_data volume. Every subsequent start is instant β€” no re-download, no cold start.


Option B β€” Clone & Run (compatible with any .NET environment)

This option is ideal if you want to integrate the service directly into an existing solution, call it locally, or run it on a server without Docker.

git clone https://github.com/SvcGeek/HtmlPdf.git
cd HtmlPdf/HtmlPdf.Service
dotnet run

The service will be available at http://localhost:5008 (configured in appsettings.Development.json and launchSettings.json).

On first run, Chromium is downloaded automatically if not already present at the configured ChromiumPath.

You can then call it from any system or language via HTTP β€” no .NET dependency on the client side.


Try It β€” Your First PDF in 60 Seconds

The service ships with a working sample endpoint.

Port reference
Docker (docker compose up): http://localhost:6000
Clone & run (dotnet run): http://localhost:5008

# Docker
curl -X POST http://localhost:6000/pdf/sample-endpoint-render \
  -H "Content-Type: application/json" \
  -d '{
    "template": "sample-endpoint-render",
    "language": "en",
    "direction": "ltr",
    "data": {
      "orderId": "ORD-2025-001",
      "orderDate": "2025-01-15",
      "client": {
        "name": "Jane Smith",
        "email": "jane@example.com"
      },
      "items": []
    }
  }' \
  --output my-first.pdf

Open my-first.pdf. That's a real browser-rendered PDF β€” generated in under a second.


Dynamic Templates β€” No Code Required

The most powerful feature: add a PDF template to a running service without writing code or rebuilding the image.

1. Write your template

<!-- invoices/invoice.cshtml -->
@model dynamic
<!DOCTYPE html>
<html>
<body>
  <h1>Invoice</h1>
  <p>Client: @Model["client"]</p>
  <p>Amount: @Model["amount"]</p>
</body>
</html>

2. Drop it in the volume

cp invoice.cshtml ./my-templates/

3. Restart the container

docker compose restart

The service logs:

πŸ“„ Dynamic template discovered β†’ POST /pdf/dynamic/invoice

4. Call the new endpoint

POST http://localhost:6000/pdf/dynamic/invoice
Content-Type: application/json

{
  "data": {
    "client": "Acme Corp",
    "amount": "€ 4,200.00"
  }
}

No code. No build. No deployment pipeline.


Configuration

// appsettings.json
{
  "Browser": {
    "ChromiumPath": "/app/chromium"
  },
  "PdfRendering": {
    "MaxConcurrentRenderings": 10,
    "AllowedTemplates": [
      "sample-endpoint-render"
    ],
    "PagePdfOptions": [
      {
        "NameOption": "default",
        "PrintBackground": true,
        "Landscape": false,
        "MarginOptions": {
          "Top": "1cm",
          "Bottom": "1cm",
          "Left": "1cm",
          "Right": "1cm"
        }
      }
    ]
  }
}

Hot-reload supported: MaxConcurrentRenderings and AllowedTemplates update instantly when you save the file β€” no restart needed.


Adding a Static Endpoint (code-defined)

When you need a strongly-typed, validated endpoint with a specific DTO:

1. Create the DTO (Pdf.Abstractions/DTO/InvoiceDTO.cs)

public class InvoiceDTO
{
    public string? Language { get; set; }
    public string? Direction { get; set; }
    public string? ClientName { get; set; }
    public decimal Total { get; set; }
}

2. Create the handler (PdfHandlerEndpoints/InvoicePdfHandler.cs)

public class InvoicePdfHandler : IPdfEndpoint
{
    public string Pattern => "invoice";
    private readonly IPdfRenderer _renderer;

    public InvoicePdfHandler(IPdfRenderer renderer) => _renderer = renderer;

    public async Task<IResult> ProcessHandle(RenderPdfRequestBase request,
        IOptionsMonitor<PdfRenderingOptions> options)
    {
        var model = IPdfEndpoint.BuildModel<InvoiceDTO>(request);
        var pdf = await _renderer.RenderAsync(request.Template, model);
        return Results.File(pdf, "application/pdf");
    }
}

3. Create the template (Templates/invoice.cshtml)

@model Pdf.Abstractions.DTO.InvoiceDTO
<h1>Invoice for @Model.ClientName</h1>
<p>Total: @Model.Total.ToString("C")</p>

4. Add to whitelist in appsettings.json β€” no restart needed.

That's it. Auto-discovery registers the endpoint at startup.


Project Structure

HtmlPdf/
β”œβ”€β”€ HtmlPdf.Service/               # Main web API
β”‚   β”œβ”€β”€ DependencyInjection/       # DI wiring + auto-discovery
β”‚   β”œβ”€β”€ Helpers/                   # BrowserProvider, PDF option helpers
β”‚   β”œβ”€β”€ Options/                   # Configuration models
β”‚   β”œβ”€β”€ PdfEndpoints/              # IPdfEndpoint interface
β”‚   β”œβ”€β”€ PdfHandlerEndpoints/
β”‚   β”‚   β”œβ”€β”€ DynamicEndpoints/      # Dynamic template handler
β”‚   β”‚   └── SampleEndpointPdfHandler.cs
β”‚   β”œβ”€β”€ Renderer/                  # TemplateRenderer, PuppeteerPdfRenderer
β”‚   β”œβ”€β”€ Templates/
β”‚   β”‚   β”œβ”€β”€ DynamicTemplates/      # ← mount your volume here
β”‚   β”‚   └── sample-endpoint-render.cshtml
β”‚   └── Program.cs
β”‚
β”œβ”€β”€ Pdf.Abstractions/              # Shared DTOs and request models
β”‚
└── docs/
    └── adr/                       # Architecture Decision Records
        β”œβ”€β”€ ADR-001-chromium-rendering.md
        β”œβ”€β”€ ADR-002-razorlight-templating.md
        β”œβ”€β”€ ADR-003-dynamic-endpoints.md
        └── ADR-004-singleton-lifetime.md

Architecture Decisions

Key technical choices are documented as Architecture Decision Records:

ADR Decision
ADR-001 Why Chromium (PuppeteerSharp) over iTextSharp, WkHtmlToPdf, and others
ADR-002 Why RazorLight for runtime template compilation
ADR-003 How and why Dynamic Endpoints work via file system volume
ADR-004 Why all core services are singletons

Tech Stack

Layer Technology
Runtime .NET 10, ASP.NET Core Minimal APIs
PDF Engine PuppeteerSharp 24 (Chromium)
Templating RazorLight 2.3
Containerization Docker, Docker Compose

About

Built by Silviu Cătălin Valcu — a .NET developer who got tired of bad PDF libraries and decided to build the right tool.

If you find this useful, a ⭐ on the repo goes a long way.
Questions, issues, or ideas? Open an issue or reach out on LinkedIn.


License

MIT β€” see LICENSE for details.

About

A simple and compatibile pdf generator base on html templates

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages