World is now on Opti ID! Learn more

KennyG
Aug 8, 2024
  47
(0 votes)

Azure Function App for PDF Creation With Syncfusion .Net PDF Framework

We have a couple of use cases that require internal and external users to print content in PDF format. We've offloaded this functionality from the DXP Web App to an Azure Function App running in a Docker container. We make use of the Syncfusion .Net PDF Framework (note: subscription required). Our first use case allows you to provide a URL to the endpoint for printing. The second use case involves POSTing an array of XHTML content to the endpoint.

Create the Azure Function App

In Visual Studio create a new project and choose Azure Functions.

Give the project a name

I'm taking the defaults here:

  • .Net 8 Isolated for the version
  • Http trigger for the function because we will be using GET and POST
  • Enable container support because we will be utilizing Docker
  • Function for the Authorization level so that it will require a Function Key. (When hosted in Azure at this authorization level it will require a Function Key to access the endpoint.)

Click Create.

This gives us the basic scaffolding.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

namespace TestPdfEndpoint
{
    public class Function1
    {
        private readonly ILogger<Function1> _logger;

        public Function1(ILogger<Function1> logger)
        {
            _logger = logger;
        }

        [Function("Function1")]
        public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
        {
            _logger.LogInformation("C# HTTP trigger function processed a request.");
            return new OkObjectResult("Welcome to Azure Functions!");
        }
    }
}

Add the ConvertUrlToPdf Function

Rename Function1 to ConvertUrlToPdf. 

    public class ConvertUrlToPdf
    {
        private readonly ILogger<ConvertUrlToPdf> _logger;

        public ConvertUrlToPdf(ILogger<ConvertUrlToPdf> logger)
        {
            _logger = logger;
        }

        [Function("ConvertUrlToPdf")]
        public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
        {
            _logger.LogInformation("C# HTTP trigger function processed a request.");
            return new OkObjectResult("Welcome to Azure Functions!");
        }
    }

Change IActionResult to an async task and add a FunctionContext parameter.

public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req, FunctionContext functionContext)

Add the Syncfusion.HtmlToPdfConverter.Net.Linux nuget package. Our Function App will be hosted on Linux. This package will do the heavy lifting of converting the URL to a PDF file. The nuget includes the Blink headless Chrome browser.

We will pass a URL to the function as a querystring parameter so check the request for a "URL" parameter. Return an EmptyResult if not found.

string urlString = req.Query["url"];

 //return if no url
 if (string.IsNullOrEmpty(urlString))
{
     return new EmptyResult();
 }

Get the path to the Blink binaries. This will be needed later in a settings object:

var directory = Directory.GetCurrentDirectory();
var blinkBinariesPath = Path.Combine(directory, "runtimes/linux/native");

 Initialize the HTML to PDF Converter:

//Initialize the HTML to PDF converter with the Blink rendering engine.
var htmlConverter = new HtmlToPdfConverter(HtmlRenderingEngine.Blink)
 {
       ConverterSettings = null,
       ReuseBrowserProcess = false
};

Create a settings object and set the values. These command line argument settings come from the Syncfusion documentation.

var settings = new BlinkConverterSettings();

 //Set command line arguments to run without sandbox.
settings.CommandLineArguments.Add("--no-sandbox");
settings.CommandLineArguments.Add("--disable-setuid-sandbox");

settings.BlinkPath = blinkBinariesPath;

Assign the settings object.

//Assign WebKit settings to the HTML converter 
htmlConverter.ConverterSettings = settings;

Pass the URL to the converter. Save to an output stream.

var newDocument = htmlConverter.Convert(urlString);

var ms = new MemoryStream();

newDocument.Save(ms);
newDocument.Close();

ms.Position = 0;

Return a FileStreamResult from the MemoryStream. Giving it a content type and a file name.

return new FileStreamResult(ms, "application/pdf")
{
      FileDownloadName = "Test.pdf"
};

At this point, your function should look like this:

    public class ConvertUrlToPdf
    {
        private readonly ILogger<ConvertUrlToPdf> _logger;

        public ConvertUrlToPdf(ILogger<ConvertUrlToPdf> logger)
        {
            _logger = logger;
        }

        [Function("ConvertUrlToPdf")]
        public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req, FunctionContext functionContext)
        {
            string urlString = req.Query["url"];

            //return if no url
            if (string.IsNullOrEmpty(urlString))
            {
                return new EmptyResult();
            }

            var directory = Directory.GetCurrentDirectory();

            var blinkBinariesPath = Path.Combine(directory, "runtimes/linux/native");

            //Initialize the HTML to PDF converter with the Blink rendering engine.
            var htmlConverter = new HtmlToPdfConverter(HtmlRenderingEngine.Blink)
            {
                ConverterSettings = null,
                ReuseBrowserProcess = false
            };

            var settings = new BlinkConverterSettings();

            //Set command line arguments to run without sandbox.
            settings.CommandLineArguments.Add("--no-sandbox");
            settings.CommandLineArguments.Add("--disable-setuid-sandbox");

            settings.BlinkPath = blinkBinariesPath;

            //Assign WebKit settings to the HTML converter 
            htmlConverter.ConverterSettings = settings;

            var newDocument = htmlConverter.Convert(urlString);

            var ms = new MemoryStream();

            newDocument.Save(ms);
            newDocument.Close();

            ms.Position = 0;

            return new FileStreamResult(ms, "application/pdf")
            {
                FileDownloadName = "Test.pdf"
            };

        }
    }

You can add additional querystring parameters to control all aspects of the PDF output such as orientation, height, width, output file name, etc. 

string orientation = req.Query["orientation"];
string widthString = req.Query["width"];
string heightString = req.Query["height"];
string fileName = req.Query["filename"]; 

int.TryParse(widthString, out int width);
int.TryParse(heightString, out var height);

if (width == 0)
{
      width = 817;
}

if (!string.IsNullOrEmpty(orientation) && orientation.ToLower().Equals("landscape"))
       settings.Orientation = PdfPageOrientation.Landscape;

settings.BlinkPath = blinkBinariesPath;
settings.Margin.All = 0;
settings.ViewPortSize = new Size(width, height);

Updating the Dockerfile

There are some underlying requirements that Syncfusion needs in place so you'll need to add the following to your Dockerfile (refer to Syncfusion documentation):

RUN apt-get update && \
apt-get install -yq --no-install-recommends \ 
libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \ 
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \ 
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \ 
libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \ 
libnss3 libgbm1

So my complete Dockerfile looks as follows:

#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0 AS base
RUN apt-get update && \
apt-get install -yq --no-install-recommends \ 
libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \ 
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \ 
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \ 
libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \ 
libnss3 libgbm1
WORKDIR /home/site/wwwroot
EXPOSE 8080

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["TestPdfEndpoint.csproj", "."]
RUN dotnet restore "./TestPdfEndpoint.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "./TestPdfEndpoint.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./TestPdfEndpoint.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /home/site/wwwroot
COPY --from=publish /app/publish .
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
    AzureFunctionsJobHost__Logging__Console__IsEnabled=true

Testing the PrintUrlToPdf Endpoint

Spin up the container using Docker Desktop. You can use your browser or Postman to test the PDF generation.  Append the URL of the site you want to print to the querystring.

Add the ConvertToPdf Function

The second function is mostly a copy/paste of the first with some tweaks.

For this function, we'll look for an array of Xhtmlstrings that are POSTed.

req.Form.TryGetValue("html", out var strings);

Then we'll create a stream for each one.

var streams = new Stream[strings.Count];

for (var i = 0; i < strings.Count; i++)
{
        streams[i] = GetMemoryStream(strings[i], htmlConverter);
}

GetMemoryStream looks like the following:

private static MemoryStream GetMemoryStream(string url, HtmlToPdfConverter htmlConverter)
{
    try
    {
        PdfDocument newDocument = htmlConverter.Convert(url, string.Empty);

        var ms = new MemoryStream();

        newDocument.Save(ms);
        newDocument.Close();

        ms.Position = 0;

        return ms;
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
        throw;
    }
}

Create a new PdfDocument that will hold the merged content.

var mergedDocument = new PdfDocument
{
    EnableMemoryOptimization = true
};

Add the streams into the PdfDocument.

PdfDocumentBase.Merge(mergedDocument, streams);

//Save the document into stream.
var stream = new MemoryStream();
mergedDocument.Save(stream);

//Close the document.
mergedDocument.Close(true);

//Disposes the streams.
foreach (var memStream in streams)
{
    await memStream.DisposeAsync();
}

stream.Position = 0;

Finally, return the FileStreamResult.

return new FileStreamResult(stream, "application/pdf")
{
    FileDownloadName = "TestMergedPdf.pdf"
};

The final version of ConvertToPdf is as follows:

    public class ConvertToPdf
    {
        private readonly ILogger<ConvertToPdf> _logger;

        public ConvertToPdf(ILogger<ConvertToPdf> logger)
        {
            _logger = logger;
        }

        [Function("ConvertToPdf")]
        public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
        {
            req.Form.TryGetValue("html", out var strings);
            
            var directory = Directory.GetCurrentDirectory();

            var blinkBinariesPath = Path.Combine(directory, "runtimes/linux/native");

            //Initialize the HTML to PDF converter with the Blink rendering engine.
            var htmlConverter = new HtmlToPdfConverter(HtmlRenderingEngine.Blink)
            {
                ConverterSettings = null,
                ReuseBrowserProcess = false
            };

            var settings = new BlinkConverterSettings();

            //Set command line arguments to run without sandbox.
            settings.CommandLineArguments.Add("--no-sandbox");
            settings.CommandLineArguments.Add("--disable-setuid-sandbox");

            settings.BlinkPath = blinkBinariesPath;

            //Assign WebKit settings to the HTML converter 
            htmlConverter.ConverterSettings = settings;

            var streams = new Stream[strings.Count];

            for (var i = 0; i < strings.Count; i++)
            {
                streams[i] = GetMemoryStream(strings[i], htmlConverter);
            }


            var mergedDocument = new PdfDocument
            {
                EnableMemoryOptimization = true
            };

            try
            {
                PdfDocumentBase.Merge(mergedDocument, streams);

                //Save the document into stream.
                var stream = new MemoryStream();
                mergedDocument.Save(stream);

                //Close the document.
                mergedDocument.Close(true);

                //Disposes the streams.
                foreach (var memStream in streams)
                {
                    await memStream.DisposeAsync();
                }

                stream.Position = 0;

                return new FileStreamResult(stream, "application/pdf")
                {
                    FileDownloadName = "TestMergedPdf.pdf"
                };
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }
        private static MemoryStream GetMemoryStream(string url, HtmlToPdfConverter htmlConverter)
        {
            try
            {
                PdfDocument newDocument = htmlConverter.Convert(url, string.Empty);

                var ms = new MemoryStream();

                newDocument.Save(ms);
                newDocument.Close();

                ms.Position = 0;

                return ms;
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }
    }

Testing the ConvertToPdf Endpoint

Spin up the container using Docker Desktop. Use Postman to test. POST to the endpoint, providing multiple "HTML" Key-Value pairs. One per page of the PDF

Tying it Back to your Optimizely Web App

Now that you have the endpoints, you can provide links to print specific URLs or POST content.

Notes

For production use, you will need to purchase a license and provide it to the app:

Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense("XXX...");

Setting up the Function in Azure is outside of the scope of this article but I might cover it in a future post.

Aug 08, 2024

Comments

Please login to comment.
Latest blogs
Make Global Assets Site- and Language-Aware at Indexing Time

I had a support case the other day with a question around search on global assets on a multisite. This is the result of that investigation. This co...

dada | Jun 26, 2025

The remote server returned an error: (400) Bad Request – when configuring Azure Storage for an older Optimizely CMS site

How to fix a strange issue that occurred when I moved editor-uploaded files for some old Optimizely CMS 11 solutions to Azure Storage.

Tomas Hensrud Gulla | Jun 26, 2025 |

Enable Opal AI for your Optimizely products

Learn how to enable Opal AI, and meet your infinite workforce.

Tomas Hensrud Gulla | Jun 25, 2025 |

Deploying to Optimizely Frontend Hosting: A Practical Guide

Optimizely Frontend Hosting is a cloud-based solution for deploying headless frontend applications - currently supporting only Next.js projects. It...

Szymon Uryga | Jun 25, 2025

World on Opti ID

We're excited to announce that world.optimizely.com is now integrated with Opti ID! What does this mean for you? New Users:  You can now log in wit...

Patrick Lam | Jun 22, 2025

Avoid Scandinavian Letters in File Names in Optimizely CMS

Discover how Scandinavian letters in file names can break media in Optimizely CMS—and learn a simple code fix to automatically sanitize uploads for...

Henning Sjørbotten | Jun 19, 2025 |