HTML to PDF in Azure Functions with C#
An HTTP-triggered function converts HTML with SelectPdf.Universal and returns the PDF. It runs on a Dedicated (App Service) plan of Basic or above, or on Elastic Premium, on Linux or Windows.
Compiled and run: Azure Functions host image (.NET 10 isolated), x64
Build it step by step.
-
1Create the project
A function app is a .NET isolated worker project. The project file references the Functions worker packages and SelectPdf: the engine package matches the machine that builds the app, so it runs locally on Windows and is published for Linux with
-r linux-x64.local.settings.jsonis forfunc start(Azure Functions Core Tools) and is not published.<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <AzureFunctionsVersion>v4</AzureFunctionsVersion> <OutputType>Exe</OutputType> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <!-- Azure Functions v4, .NET isolated worker --> <ItemGroup> <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" /> <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.1.0" /> <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" /> </ItemGroup> <!-- the engine for the machine that builds the app; publishing with -r linux-x64 picks Linux, for a Linux function app. Fonts: a Linux function app has none for CJK and other scripts. --> <PropertyGroup> <_SelectPdfArch>$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant())</_SelectPdfArch> <SelectPdfRid Condition="$([MSBuild]::IsOSPlatform('Windows'))">win-$(_SelectPdfArch)</SelectPdfRid> <SelectPdfRid Condition="$([MSBuild]::IsOSPlatform('Linux'))">linux-$(_SelectPdfArch)</SelectPdfRid> <SelectPdfRid Condition="$([MSBuild]::IsOSPlatform('OSX'))">osx-$(_SelectPdfArch)</SelectPdfRid> <SelectPdfRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier)</SelectPdfRid> </PropertyGroup> <ItemGroup> <PackageReference Include="SelectPdf.Universal" Version="26.5.0" /> <PackageReference Include="SelectPdf.Universal.Native.$(SelectPdfRid)" Version="26.5.0" /> <PackageReference Include="SelectPdf.Universal.Fonts" Version="26.5.0" /> </ItemGroup> <ItemGroup> <None Update="host.json" CopyToOutputDirectory="PreserveNewest" /> <None Update="local.settings.json" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="Never" /> </ItemGroup> </Project>{ "version": "2.0" }{ "IsEncrypted": false, "Values": { "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", "AzureWebJobsStorage": "UseDevelopmentStorage=true" } } -
2Start the isolated worker
using System.Net; using System.Web; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Hosting; using SelectPdf.Universal; new HostBuilder() .ConfigureFunctionsWorkerDefaults() .Build() .Run();The .NET isolated worker hosts the functions. Nothing SelectPdf-specific happens here.
-
3Declare the HTTP trigger
public static class InvoiceFunction { // GET /api/Invoice?number=1042 returns the invoice as a PDF [Function("Invoice")] public static async Task<HttpResponseData> Invoice( [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData req)[Function("Invoice")]with anHttpTriggeranswersGET /api/Invoice.AuthorizationLevel.Functionrequires a function key in thecodequery parameter or thex-functions-keyheader. -
4Build the HTML
string number = HttpUtility.ParseQueryString(req.Url.Query)["number"] ?? "1"; string html = $""" <h1>Invoice #{HttpUtility.HtmlEncode(number)}</h1> <p>Billed to Contoso Ltd.</p> """;The invoice number comes from the query string.
HtmlUtility.HtmlEncodekeeps user input from becoming markup. -
5Convert and return the PDF
HtmlToPdf converter = new HtmlToPdf(); PdfDocument doc = await converter.ConvertHtmlStringAsync(html); byte[] pdf = doc.Save(); doc.Close(); HttpResponseData response = req.CreateResponse(HttpStatusCode.OK); response.Headers.Add("Content-Type", "application/pdf"); await response.Body.WriteAsync(pdf); return response; }ConvertHtmlStringAsyncrenders the HTML;Save()returns the bytes, which go into the response body with theapplication/pdfcontent type. -
6Run it
func start # in a second terminal (the local host does not ask for function keys): curl "http://localhost:7071/api/Invoice?number=1042" -o invoice.pdf
The request returns
invoice.pdf. Without a license key, every page carries a trial watermark; setGlobalProperties.LicenseKeybefore the first conversion to remove it.
The complete Program.cs.
This is the exact file our samples pipeline compiled and ran on Azure Functions host image (.NET 10 isolated), x64 to produce the output on this page.
using System.Net;
using System.Web;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Hosting;
using SelectPdf.Universal;
new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.Build()
.Run();
public static class InvoiceFunction
{
// GET /api/Invoice?number=1042 returns the invoice as a PDF
[Function("Invoice")]
public static async Task<HttpResponseData> Invoice(
[HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData req)
{
string number = HttpUtility.ParseQueryString(req.Url.Query)["number"] ?? "1";
string html = $"""
<h1>Invoice #{HttpUtility.HtmlEncode(number)}</h1>
<p>Billed to Contoso Ltd.</p>
""";
HtmlToPdf converter = new HtmlToPdf();
PdfDocument doc = await converter.ConvertHtmlStringAsync(html);
byte[] pdf = doc.Save();
doc.Close();
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
response.Headers.Add("Content-Type", "application/pdf");
await response.Body.WriteAsync(pdf);
return response;
}
}
Open the project on GitHub → this program and the variations below, ready to dotnet run
Deploy it.
Create a Linux function app on an Elastic Premium or Dedicated plan, then deploy the published app as a zip. Replace the names in angle brackets with your own.
az login
az group create --name selectpdf-rg --location westeurope
az storage account create --resource-group selectpdf-rg --name <storageaccount> --sku Standard_LRS
az functionapp plan create --resource-group selectpdf-rg --name selectpdf-fnplan --sku P1v3 --is-linux
az functionapp create --resource-group selectpdf-rg --name <function-app> --plan selectpdf-fnplan \
--storage-account <storageaccount> --functions-version 4 \
--runtime dotnet-isolated --runtime-version 10 --os-type Linux
az functionapp config set --resource-group selectpdf-rg --name <function-app> --always-on true
dotnet publish -c Release -r linux-x64 --self-contained false -o publish
cd publish && zip -r ../fn.zip . && cd ..
az functionapp deployment source config-zip --resource-group selectpdf-rg --name <function-app> --src fn.zip
# call it with a function key from the portal (Functions > Invoice > Function Keys)
curl "https://<function-app>.azurewebsites.net/api/Invoice?number=1042&code=<function-key>" -o invoice.pdf
These steps, with the Windows variants, are in the documentation: selectpdf.com/pdf-library/html/Azure-Functions.htm.
The PDF it produces.
Download invoice.pdf ↓ the file this sample produced, unedited
Common problems and fixes.
The function fails on a Consumption plan
Windows Consumption and Flex Consumption cannot run the engine: the package does not fit the plan's disk space or runs from a read-only mount. Use Elastic Premium (EP1 and up) or a Dedicated plan of Basic or above.
Chinese, Japanese or Korean text shows as boxes on Linux
The Linux image has no fonts for these scripts: reference SelectPdf.Universal.Fonts, as the project file above does.
The first call is slow
Turn on Always On (a Dedicated plan setting, in the deployment steps) so the app stays loaded between calls.
Good to know.
- Supported plans: Dedicated (App Service) Basic or above and Elastic Premium (EP1 and up), on Windows and Linux. Consumption and Flex Consumption are not supported. Source
- On Linux, reference SelectPdf.Universal.Native.linux-x64 and SelectPdf.Universal.Fonts; on Windows, SelectPdf.Universal.Native.win-x64. Source
- A function app on a Dedicated plan does not run from the package, so the engine starts from a writable folder. Do not set WEBSITE_RUN_FROM_PACKAGE. Source
Questions, answered.
Short answers about this guide. The SelectPdf.Universal documentation covers every class and option.