PDF-Dateien unter Android mit Xamarin.Forms anzeigen

Manchmal ist es notwendig, dass man eine PDF-Datei in seiner eigenen App anzeigen muss bzw. möchte. In diesem Beitrag möchte ich euch jetzt drei Varianten aufzeigen, wie ihr dieses Ziel erreichen könnt. Ich konzentriere mich hierbei hauptsächlich auf Android, da man unter iOS einfach eine normale Xamarin.Forms.WebView nutzen kann, welche in der Lage ist die PDF-Datei richtig zu rendern. Ich habe diesen Beitrag vor kurzem auch auf Englisch auf medium veröffentlicht.

Xamarin.Essentials

Die erste Variante, welche ich vorstellen möchte, ist gleichzeitig wohl auch die einfachste. Denn wir wollen die Launcher-API aus dem Xamarin.Essentials NuGet-Package verwenden. In jedem neuen Xamarin.Forms Projekt ist Xamarin.Essentials bereits vorinstalliert und kann daher direkt verwendet werden. Um nun diesen Ansatz für die PDF-Anzeige zu nutzen, müssen wir die Datei lokal zur Verfügung haben und können anschließend Launcher.OpenAsync() aufrufen und ein OpenFileRequest-Objekt übergeben. Sobald wir diesen Aufruf durchführen, wir die Standard-App zur Anzeige von PDF-Dateien auf eurem Gerät gestartet und die PDF-Datei angezeigt.

Nun wollt ihr sicherlich auch ein bisschen Code sehen, welche die Launcher-API in Aktion zeigt. Zunächst erstellen wir eine kleine Hilfsmethode mit dem Namen DownloadPdfFileAsync(), welche uns die PDF-Datei aus dem Internet herunterlädt und im lokalen Speicher der App ablegt. Anschließend liefert die Methode den vollständigen Pfad zur PDF-Datei zurück, welchen wir dann im weiteren Verlauf verwenden wollen.

private async Task<string> DownloadPdfFileAsync()
{
    var filePath = Path.Combine(FileSystem.AppDataDirectory, "test.pdf");

    if (File.Exists(filePath))
        return filePath;

    var httpClient = new HttpClient();
    var pdfBytes = await httpClient.GetByteArrayAsync("URL TO THE PDF FILE");

    try
    {
        File.WriteAllBytes(filePath, pdfBytes);

        return filePath;
    }
    catch (Exception ex)
    {
        await Application.Current.MainPage.DisplayAlert("Error", ex.Message, "Ok");
    }

    return null;
}

Nun habe ich einen einfachen Button in meiner Applikation erstellt und das Clicked-Event registriert. In dieser Methode nutze ich zunächst die DownloadPdfFileAsync-Methode und anschließend rufe ich die OpenAsync-Methode der Launcher-Klasse auf, wobei ich hier den zurückgegebenen Pfad übergebe.

private async void LauncherButtonOnClicked(object sender, EventArgs e)
{
    var filePath = await DownloadPdfFileAsync();

    if (filePath != null)
    {
        await Launcher.OpenAsync(new OpenFileRequest
        {
            File = new ReadOnlyFile(filePath)
        });
    }
}

Das ist auch schon die gesamte Magie. Wenn ich jetzt die Demo-Applikation einmal starte, so sieht man, dass die PDF-Datei angezeigt wird, sobald ich den Button drücke. Man sollte an dieser Stelle jedoch beachten, dass eine weitere App geöffnet wird, welche die PDF-Anzeige übernimmt, auf die man keinen Einfluss hat.


Google Drive Viewer

Im zweiten Ansatz wollen wir den Google Drive Viewer anschauen, welcher eigentlich nur eine Webseite ist, welche in der Lage ist eine PDF-Datei anzuzeigen. Hierfür brauchen wir die Datei nicht mehr lokal vorher herunterladen, sondern brauchen nur eine URL zur PDF-Datei. Dadurch benötigen wir beim Aufrufen aber auch eine aktive Internet-Verbindung, denn offline funktioniert dies nicht.

Zunächst erstellen wir uns ein eigenes Control. In meinem Fall nenne ich dieses GoogleDriveViewerWebView und leite von der Xamarin.Forms.WebView ab. Hier fügen wir nun noch eine BindableProperty mit dem Namen Uri ein. Diese Property beinhaltet später die URL für unsere PDF-Datei.


public class GoogleDriveViewerWebView : WebView
{
    public static readonly BindableProperty UriProperty =
        BindableProperty.Create(nameof(Uri), typeof(string), 
            typeof(GoogleDriveViewerWebView), default(string));

    public string Uri
    {
        get => (string)GetValue(UriProperty);
        set => SetValue(UriProperty, value);
    }
}

Nun benötigen wir noch einen CustomRenderer in unserem Android-Projekt. Wir erstellen daher eine Klasse GoogleDriveViewerWebViewRenderer, welcher von der Klasse WebViewRenderer erbt. Hier überschreiben wir die beiden Methoden OnElementChanged und OnElementPropertyChanged. In diesen Methoden nutzen wir jetzt eine eigene Logik, um die PDF-URL in eine URL für Google Drive umzuwandeln.

using Android.Content;
using PDFViewerSample.Controls;
using PDFViewerSample.Droid.CustomRenderer;
using System.ComponentModel;
using System.Net;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(GoogleDriveViewerWebView), 
    typeof(GoogleDriveViewerWebViewRenderer))]
namespace PDFViewerSample.Droid.CustomRenderer
{
    public class GoogleDriveViewerWebViewRenderer : WebViewRenderer
    {
        private GoogleDriveViewerWebView _googleDriveViewerWebView;

        public GoogleDriveViewerWebViewRenderer(Context context) : base(context)
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
        {
            base.OnElementChanged(e);

            if (Control == null)
                return;

            _googleDriveViewerWebView = Element as GoogleDriveViewerWebView;

            Control.Settings.JavaScriptEnabled = true;
            Control.Settings.AllowUniversalAccessFromFileURLs = true;

            if (_googleDriveViewerWebView.Uri != null)
            {
                Control.LoadUrl(
                    string.Format("https://drive.google.com/viewerng/viewer?url={0}",
                    WebUtility.UrlEncode(_googleDriveViewerWebView.Uri)));
            }
        }

        protected override void OnElementPropertyChanged(object sender, 
            PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == nameof(GoogleDriveViewerWebView.Uri)
                && _googleDriveViewerWebView.Uri != null)
            {
                Control.LoadUrl(
                    string.Format("https://drive.google.com/viewerng/viewer?url={0}",
                    WebUtility.UrlEncode(_googleDriveViewerWebView.Uri)));
            }
        }
    }
}

Jetzt können wir unser neues Control GoogleDriveViewerWebView nutzen und die Uri-Property mit Inhalt füllen. Wenn ich jetzt die Demo-Applikation einmal starte, so sieht man, dass die PDF-Datei angezeigt wird, sobald ich den Button drücke. Der Vorteil gegenüber dem Xamarin.Essentials Ansatz ist, dass man innerhalb der eigenen App bleibt. Jedoch hat man auch keine Option das Design zu verändern.


PDF.js

Ich möchte noch eine dritte Variante im Rahmen dieses Beitrags vorstellen. Dabei handelt es sich um eine JavaScript-Bibliothek mit dem Namen PDF.js, welche von Mozilla entwickelt wird. Auf der Webseite müssen wir nun die aktuellste Version herunterladen und wir speichern die ZIP-Datei entsprechend auf unserem Entwicklungsrechner. Anschließend können wir die Datei entpacken.

Im Android-Projekt öffnen wir den Assets-Ordner und erstellen darin einen neuen Ordner mit dem Namen pdfjs. Nun fügen wir diesem Ordner den gesamten Inhalt von der ZIP-Datei (die Ordner build und web) hinzu. Wir müssen jetzt noch sicherstellen, dass die BuildAction für alle Dateien auf AndroidAsset steht, welches aber eigentlich automatisch erfolgen sollte.

Wir erstellen nun ein weiteres Control, welches wir dieses Mal PdfJsWebView nennen wollen und wieder von der Xamarin.Forms.WebView ableitet. Auch hier fügen wir wieder die BindableProperty mit dem Namen Uri hinzu.

public class PdfJsWebView : WebView
{
    public static readonly BindableProperty UriProperty =
       BindableProperty.Create(nameof(Uri), typeof(string), 
           typeof(PdfJsWebView), default(string));

    public string Uri
    {
        get => (string)GetValue(UriProperty);
        set => SetValue(UriProperty, value);
    }
}

Nun benötigen wir auch hier wieder einen CustomRenderer in unserem Android-Projekt. Dazu erstellen wir eine Klasse PdfJsWebViewRenderer. Der Aufbau ist dabei ähnlich zu dem Renderer der GoogleDriveViewerWebView. Auch hier überschreiben wir wieder die beiden Methoden OnElementChanged und OnElementPropertyChanged. In diesen Methoden referenzieren wir nun die lokale Datei viewer.html von PDF.js und übergeben den lokalen Pfad zu unserer PDF-Datei.

[assembly: ExportRenderer(typeof(PdfJsWebView), 
    typeof(PdfJsWebViewRenderer))]
namespace PDFViewerSample.Droid.CustomRenderer
{
    public class PdfJsWebViewRenderer : WebViewRenderer
    {
        private PdfJsWebView _pdfJsWebView;

        public PdfJsWebViewRenderer(Context context) : base(context)
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
        {
            base.OnElementChanged(e);

            if (Control == null)
                return;

            _pdfJsWebView = Element as PdfJsWebView;

            Control.Settings.JavaScriptEnabled = true;
            Control.Settings.BuiltInZoomControls = true;
            Control.Settings.AllowContentAccess = true;
            Control.Settings.AllowFileAccess = true;
            Control.Settings.AllowFileAccessFromFileURLs = true;
            Control.Settings.AllowUniversalAccessFromFileURLs = true;

            if (_pdfJsWebView.Uri != null)
            {
                Control.LoadUrl(
                    $"file:///android_asset/pdfjs/web/viewer.html?" +
                    $"file={WebUtility.UrlEncode(_pdfJsWebView.Uri)}");
            }
        }

        protected override void OnElementPropertyChanged(object sender, 
            PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == nameof(PdfJsWebView.Uri)
                && _pdfJsWebView.Uri != null)
            {
                Control.LoadUrl(
                    $"file:///android_asset/pdfjs/web/viewer.html?" +
                    $"file={WebUtility.UrlEncode(_pdfJsWebView.Uri)}");
            }
        }
    }
}

Nun können wir unser PdfJsWebView-Control verwenden. Bitte beachtet, dass wir die PDF-Datei zunächst herunterladen müssen und lokal abspeichern müssen. Sobald dies geschehen ist, sind wir jetzt jedoch in der Lage die PDF-Datei auch ohne eine Internetverbindung anzeigen zu können.

An dieser Stelle noch ein kleiner Hinweis von mir. Dieser Ansatz funktioniert leider nicht im Emulator, aber dafür auf einem echten Gerät. Dies solltet ihr bei euren Tests beachten.


In diesem Beitrag habe ich euch nun drei verschiedene Varianten vorgestellt, wie ihr eine PDF-Datei unter Android mit der Hilfe von Xamarin.Forms anzeigen lassen könnt. Jede Variante hat hierbei ihre Vor- und Nachteile. Ihr müsst also nun selbst entscheiden, welcher Ansatz für eure App am besten passt. Den gesamten Code findet ihr auch in diesem GitHub-Repository.

Cognitive Services: Alter einer Person ermitteln Hot Restart: iOS-Apps unter Windows deployen Pokédex – Kleines Xamarin.Forms Projekt – Teil 7