Website-Screenshots mit PuppeteerSharp – sogar aus Azure!

Eine Website als Bild bzw. PDF zu exportieren ist mit .NET und dem Nuget-Paket von PuppeteerSharp und Chromium sehr einfach.

Headless Browser – eine kleine Geschichte

Der Schlüssel liegt in der Fähigkeit Browser in einem Headless Mode auszuführen. Chrome und Firefox können dies beispielsweise. D.h. es wird keine wirkliche GUI angezeigt, um die Browser-Engine zu starten. Der Headless Mode ist aber nicht beschränkt, es ist die gleichwertige Browser-Engine, wie sie auch im interaktiven Modus verwendet wird. Ein häufiger Einsatzzweck ist beispielsweise die Testautomatisierung. Bevor es out-of-the-box Headless-Browser gab, war PhantomJS sehr verbreitet. Das findet sich immer noch in alten JavaScript-Runnern für Unit Tests, aber seitdem die Browser-Hersteller ihr Commitment für den Headless-Mode offiziell gemacht haben (2017), hat der Entwickler von PhantomJS schnell angekündigt, dass eine Weiterentwicklung keinen Sinn mehr macht.

Mit Chrome kann man den Headless Mode sehr einfach über die Kommandozeile starten:

chrome --headless --disable-gpu --print-to-pdf https://www.chromestatus.com/

Das erzeugt z.B. ein PDF.

Aber Chrome bzw. Chromium bietet noch etwas viel schöneres an: die Puppeteer-API. Damit kann man sogar Remote Chrome bzw. Chromium steuern. Google hat dies zwar für nodeJS entwickelt, aber das OpenSource-Projekt PuppeteerSharp hat dies (soweit ich es überblicke 1:1) nach .NET portiert.

PuppeteerSharp in Web Application (.NET Core MVC)

Diese paar Zeilen Code sind als Basis alles, was man über PuppeteerSharp wissen muss:

using PuppeteerSharp;

public class ExportController : Controller
{
    public async Task<IActionResult> Cloud()
    {
        var browser = await Puppeteer.ConnectAsync(new ConnectOptions()
        {
            BrowserWSEndpoint = "wss://chrome.browserless.io",
            DefaultViewport = new ViewPortOptions() { Width = 2000, Height = 1000 }
        });

        var page = await browser.NewPageAsync();
        await page.GoToAsync("https://de.wikipedia.org/wiki/Chromium_(Browser)");

        return new FileStreamResult(await page.ScreenshotStreamAsync(), "image/png");
    }

    public async Task<IActionResult> Local()
    {
        await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);

        var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true,
            DefaultViewport = new ViewPortOptions() { Width = 2000, Height = 1000 }
        });


        var page = await browser.NewPageAsync();
        await page.GoToAsync("https://de.wikipedia.org/wiki/Chromium_(Browser)");

        return new FileStreamResult(await page.ScreenshotStreamAsync(), "image/png");
    }
}

Fangen wir mit der Variante „Local“ an. Hier wird ein Chromium heruntergeladen und ohne weitere Installation genutzt (XCOPY Installation). Dies funktioniert in „klassischen“ Server-Installationen, z.B. wenn die Webapplikation auf einem dedizierten Windows Server installiert wird. In einer „Azure WebApp“ oder gar in einer Serverless-Umgebung (Azure Functions) sind diese Infrastruktur-Voraussetzungen nicht erfüllt. Woran das jetzt ganz genau liegt, kann ich gar nicht sagen, ich würde vermuten, dass Puppeteer intern auch einfach nur Chrome.exe aufruft. Das das in einer so speziellen Cloud-Hosting-Umgebung nicht geht, ist einigermaßen klar.

Das ist aber kein großes Problem, da man eben von einer so beschränkten Cloud-Umgebung problemlos remote auf eine andere Maschine verbinden kann. Man könnte zwar Chromium oder Chrome auch selbst hosten, aber viel einfacher geht es mit einem Cloud-Angebot von Browserless.io. Man kann hier problemlos skalieren und bei den Preisen lohnt sich selber hosten kaum.

Die Testanwendung habe ich als Azure Web veröffentlicht: https://testapppuppeteer.azurewebsites.net/

Der Fall „Local“ funktioniert dort logischerweise nicht.

Fazit: Wenn man eine Software entwickelt, die sowohl On-Premise, als auch in der Cloud hostbar sein soll und eben eine Export-Funktionalität benötigt und man diese einfach auf Basis von HTML umsetzen will, kann man das mit PuppeteerSharp problemlos!