Recently I was facing an integration challenge where I would have to convert an SVG file to PNG in order to store it in a Dynamics 365 image field. The difficulty hereby was that the SVG contained a custom font and the integration was powered by Azure Functions on Windows whose execution environment was missing the needed font and AFAIK there is no way of easily changing that. A common solution to this is usually to shovel the logic into a Docker container where a custom font can be installed and provided. But I was hoping for a solution which didn’t involve changing or managing Azure resources.
So I got creative and came up with an idea on solving this from all within the existing Azure Function.
Basically, my idea was
- to embed the SVG as well as the needed web font within an HTML document.
- to upload the document to blob storage and generate a blob SAS URI.
- to navigate to the URL within a headless browser instance.
- to take a screenshot of the rendered SVG in memory.
For the headless browser part I used Microsoft Playwright for .NET
. Actually its intended use is for E2E-testing, but I found it to be a perfect fit for my scenario, being well documented and able to easily install, run and orchestrate a headless chromium browser instance.
Talk is cheap. Show me the code. - Linus Torvalds
Here’s my working PoC.
public async Task<byte[]> ConvertSvgToPngAsync(byte[] svg, BlobServiceClient blobServiceClient)
{
// 1. Construct the html document with the svg and font embedded.
string html =
$@"
<!DOCTYPE html>
<html>
<head>
<meta charset=""UTF-8"">
<link href=""https://fonts.googleapis.com/css?family=Roboto"" rel=""stylesheet"">
</head>
<body>
{Encoding.UTF8.GetString(svg)}
</body>
";
// 2. Upload the document to blob storage and generate a sas uri.
BlobClient blobClient = blobServiceClient
.GetBlobContainerClient("tmp")
.GetBlobClient("tmp.html");
using MemoryStream stream = new(Encoding.UTF8.GetBytes(html));
await blobClient.UploadAsync(stream, new BlobUploadOptions
{
HttpHeaders = new()
{
ContentType = "text/html"
}
});
Uri uri = blobClient.GenerateSasUri(
BlobSasPermissions.Read,
DateTimeOffset.UtcNow.AddHours(1));
// 3. Initialize a headless (chromium) browser instance and open the document online.
Environment.SetEnvironmentVariable(
"PLAYWRIGHT_BROWSERS_PATH",
Environment.GetEnvironmentVariable("HOME_EXPANDED"));
Microsoft.Playwright.Program.Main(new string[] { "install", "chromium", "--with-deps", });
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
await page.SetViewportSizeAsync(1920, 1080);
await page.GotoAsync(uri.ToString());
// 4. Take an in-memory screenshot of the svg.
await page.WaitForTimeoutAsync(5000);
var element = await page.QuerySelectorAsync("svg");
byte[] png = await element!.ScreenshotAsync(new()
{
OmitBackground = true,
});
return png;
}
Let me know what you think about this approach!