How to Generate a PDF File from HTML String in Xamarin Forms

Introduction 
 
In Xamarin Forms, there is no default support for either generating a PDF or viewing a PDF file. However, you can achieve these requirements with native support through renderers for both Android and iOS platforms.
 
The demo application in the repository is divided into three categories:
 
1. Create HTML from a URL.
2. Convert the HTML string to a PDF file.
3. View the PDF file.
 
Create HTML from a URL
 
You can easily get the HTML string from a URL through either the HTTPClient or WebClient class. Please refer the below code snippet:
  1. var client = new HttpClient();  
  2. htmlstring = await client.GetStringAsync(url);   
  3. //or
  4. using (WebClient client = new WebClient())
    {
    htmlstring = client.DownloadString(url);
    }

 Convert the HTML string to PDF file 

In recent days, I tried to convert the HTML string to PDF file in Xamarin Forms. But, unfortunately, there is no free library or default support in Xamarin platform. With the help of WebView and Xamarin Forums, I got ideas from colleagues and finally achieved the requirement through renderers.

For Android

With the support of Native WebView and WebViewClient, you can create your own print document adapter where you can customize your PDF file as required. Instead of printing the PDF file using device printer, create your own callbacks to write into the file and save the file in custom location.
  1. namespace XFPDF.Droid   
  2. {  
  3.     [Register("android/print/PdfLayoutResultCallback")]  
  4.     public class PdfLayoutResultCallback : PrintDocumentAdapter.LayoutResultCallback  
  5.     {  
  6.         public PrintDocumentAdapter Adapter { getset; }  
  7.   
  8.         public PDFToHtml PDFToHtml { getset; }  
  9.   
  10.         public PdfLayoutResultCallback(IntPtr javaReference, JniHandleOwnership transfer)  
  11.             : base(javaReference, transfer) { }  
  12.   
  13.         public PdfLayoutResultCallback() : base(IntPtr.Zero, JniHandleOwnership.DoNotTransfer)  
  14.         {  
  15.             if (!(Handle != IntPtr.Zero))  
  16.             {  
  17.                 unsafe  
  18.                 {  
  19.                     JniObjectReference val = JniPeerMembers.InstanceMethods.StartCreateInstance("()V", GetType(), null);  
  20.                     SetHandle(val.Handle, JniHandleOwnership.TransferLocalRef);  
  21.                     JniPeerMembers.InstanceMethods.FinishCreateInstance("()V"thisnull);  
  22.                 }  
  23.             }  
  24.   
  25.         }  
  26.   
  27.         public override void OnLayoutFinished(PrintDocumentInfo info, bool changed)  
  28.         {  
  29.             try  
  30.             {  
  31.                 var file = new Java.IO.File(PDFToHtml.FilePath);  
  32.                 var fileDescriptor = ParcelFileDescriptor.Open(file, ParcelFileMode.ReadWrite);  
  33.                 var writeResultCallback = new PdfWriteResultCallback(PDFToHtml);  
  34.                 Adapter.OnWrite(new PageRange[] { PageRange.AllPages }, fileDescriptor, new CancellationSignal(), writeResultCallback);  
  35.             }  
  36.             catch  
  37.             {  
  38.                 PDFToHtml.Status = PDFEnum.Failed;  
  39.             }  
  40.   
  41.             base.OnLayoutFinished(info, changed);  
  42.         }  
  43.           
  44.         public override void OnLayoutCancelled()  
  45.         {  
  46.             base.OnLayoutCancelled();  
  47.             PDFToHtml.Status = PDFEnum.Failed;  
  48.         }  
  49.   
  50.         public override void OnLayoutFailed(ICharSequence error)  
  51.         {  
  52.             base.OnLayoutFailed(error);  
  53.             PDFToHtml.Status = PDFEnum.Failed;  
  54.         }  
  55.     }  
  56.   
  57.     [Register("android/print/PdfWriteResult")]  
  58.     public class PdfWriteResultCallback : PrintDocumentAdapter.WriteResultCallback  
  59.     {  
  60.         readonly PDFToHtml pDFToHtml;  
  61.   
  62.         public PdfWriteResultCallback(PDFToHtml _pDFToHtml, IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)  
  63.         {  
  64.             pDFToHtml = _pDFToHtml;  
  65.         }  
  66.   
  67.         public PdfWriteResultCallback(PDFToHtml _pDFToHtml) : base(IntPtr.Zero, JniHandleOwnership.DoNotTransfer)  
  68.         {  
  69.             if (!(Handle != IntPtr.Zero))  
  70.             {  
  71.                 unsafe  
  72.                 {  
  73.                     JniObjectReference val = JniPeerMembers.InstanceMethods.StartCreateInstance("()V", GetType(), null);  
  74.                     SetHandle(val.Handle, JniHandleOwnership.TransferLocalRef);  
  75.                     JniPeerMembers.InstanceMethods.FinishCreateInstance("()V"thisnull);  
  76.                 }  
  77.             }  
  78.   
  79.             pDFToHtml = _pDFToHtml;  
  80.         }  
  81.   
  82.   
  83.         public override void OnWriteFinished(PageRange[] pages)  
  84.         {  
  85.             base.OnWriteFinished(pages);  
  86.             pDFToHtml.Status = PDFEnum.Completed;  
  87.         }  
  88.   
  89.         public override void OnWriteCancelled()  
  90.         {  
  91.             base.OnWriteCancelled();  
  92.             pDFToHtml.Status = PDFEnum.Failed;  
  93.         }  
  94.   
  95.         public override void OnWriteFailed(ICharSequence error)  
  96.         {  
  97.             base.OnWriteFailed(error);  
  98.             pDFToHtml.Status = PDFEnum.Failed;  
  99.         }  
  100.     }  

For iOS

With the support of WKWebView and WKNavigationDelegate, you can easily write the data in the file using UIPrintPageRenderer.
  1. namespace XFPDF.iOS  
  2. {  
  3.     class WebViewCallBack : WKNavigationDelegate  
  4.     {  
  5.         private PDFToHtml PDFToHtml { getset; }  
  6.   
  7.         public WebViewCallBack(PDFToHtml _pDFToHtml)  
  8.         {  
  9.             PDFToHtml = _pDFToHtml;  
  10.         }  
  11.   
  12.         public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)  
  13.         {  
  14.             try  
  15.             {  
  16.                 int padding = 10;  
  17.                 UIEdgeInsets pageMargins = new UIEdgeInsets(padding, padding, padding, padding);  
  18.                 webView.ViewPrintFormatter.ContentInsets = pageMargins;  
  19.                 UIPrintPageRenderer renderer = new UIPrintPageRenderer();  
  20.                 renderer.AddPrintFormatter(webView.ViewPrintFormatter, 0);  
  21.                 CGSize pageSize = new CGSize(PDFToHtml.PageWidth, PDFToHtml.PageHeight);  
  22.                 CGRect printableRect = new CGRect(padding, padding, pageSize.Width - (padding * 2), pageSize.Height - (padding * 2));  
  23.                 CGRect paperRect = new CGRect(0, 0, PDFToHtml.PageWidth, PDFToHtml.PageHeight);  
  24.   
  25.                 var nSString = new NSString("PaperRect");  
  26.                 var printableRectstring = new NSString("PrintableRect");  
  27.   
  28.                 renderer.SetValueForKey(NSValue.FromObject(paperRect), nSString);  
  29.                 renderer.SetValueForKey(NSValue.FromObject(printableRect), printableRectstring);  
  30.   
  31.                 NSData file = PrintToPDFWithRenderer(renderer, paperRect);  
  32.                 File.WriteAllBytes(PDFToHtml.FilePath + ".pdf", file.ToArray());  
  33.                 PDFToHtml.Status = PDFEnum.Completed;  
  34.             }  
  35.             catch  
  36.             {  
  37.                 PDFToHtml.Status = PDFEnum.Failed;  
  38.             }  
  39.         }  
  40.   
  41.         public override void DidFailNavigation(WKWebView webView, WKNavigation navigation, NSError error)  
  42.         {  
  43.             base.DidFailNavigation(webView, navigation, error);  
  44.             PDFToHtml.Status = PDFEnum.Failed;  
  45.         }  
  46.   
  47.         public override void DidFailProvisionalNavigation(WKWebView webView, WKNavigation navigation, NSError error)  
  48.         {  
  49.             base.DidFailProvisionalNavigation(webView, navigation, error);  
  50.             PDFToHtml.Status = PDFEnum.Failed;  
  51.         }  
  52.   
  53.         private NSData PrintToPDFWithRenderer(UIPrintPageRenderer renderer, CGRect paperRect)  
  54.         {  
  55.             NSMutableData pdfData = new NSMutableData();  
  56.             try  
  57.             {  
  58.                 UIGraphics.BeginPDFContext(pdfData, paperRect, null);  
  59.                 renderer.PrepareForDrawingPages(new NSRange(0, renderer.NumberOfPages));  
  60.                 for (int i = 0; i < renderer.NumberOfPages; i++)  
  61.                 {  
  62.                     UIGraphics.BeginPDFPage();  
  63.                     renderer.DrawPage(i, paperRect);  
  64.                 }  
  65.                 UIGraphics.EndPDFContent();  
  66.             }  
  67.             catch  
  68.             {  
  69.                 PDFToHtml.Status = PDFEnum.Failed;  
  70.             }  
  71.   
  72.             return pdfData;  
  73.         }  
  74.     }  
  75. }  

View the PDF file

Showing a PDF file seems a very easy task, and depending on what platform you are targeting, it is. Through native support, one can view the PDF file with the help of pdfjs library.

Android

The first thing we need to do is download the pdfjs library and paste the pdfjs folder in Android Assets. Make sure the build action for all files is set to AndroidAsset.
  1. [assembly: ExportRenderer(typeof(PdfWebView), typeof(PdfWebViewRenderer))]  
  2. namespace XFPDF.Droid  
  3. {  
  4.     public class PdfWebViewRenderer : WebViewRenderer  
  5.     {  
  6.         private PdfWebView PdfWebView { get { return this.Element as PdfWebView; } }  
  7.   
  8.         private string PdfJsViewerUri => PDFUtils.PdfJsViewerUri;  
  9.   
  10.         public PdfWebViewRenderer(Android.Content.Context context) : base(context)  
  11.         {  
  12.   
  13.         }  
  14.   
  15.         protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)  
  16.         {  
  17.             base.OnElementChanged(e);  
  18.   
  19.             if (e.NewElement != null)  
  20.             {  
  21.                 Control.Settings.AllowFileAccess = true;  
  22.                 Control.Settings.AllowFileAccessFromFileURLs = true;  
  23.                 Control.Settings.AllowUniversalAccessFromFileURLs = true;  
  24.                 Control.Settings.UseWideViewPort = true;  
  25.                 Control.Settings.LoadWithOverviewMode = true;  
  26.                 this.UpdateDisplayZoomControls();  
  27.                 this.UpdateEnableZoomControls();  
  28.                 this.Control.SetBackgroundColor(Android.Graphics.Color.Transparent);  
  29.                 this.LoadPdfFile(this.PdfWebView.Uri);  
  30.             }  
  31.         }  
  32.   
  33.         protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)  
  34.         {  
  35.             base.OnElementPropertyChanged(sender, e);  
  36.   
  37.             if (e.PropertyName == PdfWebView.UriProperty.PropertyName)  
  38.                 this.LoadPdfFile(this.PdfWebView.Uri);  
  39.         }  
  40.   
  41.         void UpdateEnableZoomControls()  
  42.         {  
  43.             // BuiltInZoomControls supported as of API level 3  
  44.             if (Control != null && ((int)Build.VERSION.SdkInt >= 3))  
  45.             {  
  46.                 var value = Element.OnThisPlatform().ZoomControlsEnabled();  
  47.                 Control.Settings.SetSupportZoom(value);  
  48.                 Control.Settings.BuiltInZoomControls = value;  
  49.             }  
  50.         }  
  51.   
  52.         void UpdateDisplayZoomControls()  
  53.         {  
  54.             // DisplayZoomControls supported as of API level 11  
  55.             if (Control != null && ((int)Build.VERSION.SdkInt >= 11))  
  56.             {  
  57.                 var value = Element.OnThisPlatform().ZoomControlsDisplayed();  
  58.                 Control.Settings.DisplayZoomControls = value;  
  59.             }  
  60.         }  
  61.   
  62.         void LoadPdfFile(string uri)  
  63.         {  
  64.             if (string.IsNullOrWhiteSpace(uri))  
  65.                 return;  
  66.   
  67.             string url = $"?file={WebUtility.UrlEncode(uri)}";  
  68.             Control.LoadUrl(PdfJsViewerUri + url);  
  69.         }  
  70.     }  
  71. }  
 
For iOS platform, just copy the downloaded pdfjs folder and paste it into the Resources folder. Make sure the build action for all files is set to BundleResource.
  1. [assembly: ExportRenderer(typeof(PdfWebView), typeof(PdfWebViewRenderer))]  
  2. namespace XFPDF.iOS  
  3. {  
  4.     class PdfWebViewRenderer : ViewRenderer<PdfWebView, UIWebView>  
  5.     {  
  6.         private string PdfJsViewerUri => PDFUtils.PdfJsViewerUri;  
  7.   
  8.         public PdfWebViewRenderer() : base()  
  9.         {  
  10.   
  11.         }  
  12.   
  13.         protected override void OnElementChanged(ElementChangedEventArgs<PdfWebView> e)  
  14.         {  
  15.             base.OnElementChanged(e);  
  16.   
  17.             if (e.NewElement != null)  
  18.             {  
  19.                 this.SetNativeControl(new UIWebView());  
  20.                 this.Control.ScrollView.Bounces = false;  
  21.                 this.Control.ScrollView.BouncesZoom = false;  
  22.                 this.Control.ScrollView.AlwaysBounceHorizontal = false;  
  23.                 this.Control.ScrollView.AlwaysBounceVertical = false;  
  24.                 this.LoadPdfFile(this.Element?.Uri);  
  25.                 this.Control.BackgroundColor = UIColor.Clear;  
  26.                 //Control.ScrollView.ScrollEnabled = false;  
  27.                 //Control.ScalesPageToFit = false;  
  28.                 //Control.MultipleTouchEnabled = false;  
  29.             }  
  30.         }  
  31.   
  32.         protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)  
  33.         {  
  34.             base.OnElementPropertyChanged(sender, e);  
  35.   
  36.             if (e.PropertyName == PdfWebView.UriProperty.PropertyName)  
  37.                 this.LoadPdfFile(this.Element.Uri);  
  38.         }  
  39.   
  40.         private void LoadPdfFile(string path)  
  41.         {  
  42.             if (string.IsNullOrWhiteSpace(path))  
  43.                 return;  
  44.   
  45.             Control.LoadRequest(new NSUrlRequest(new NSUrl(PdfJsViewerUri, false)));  
  46.             Control.LoadFinished += Control_LoadFinished;  
  47.         }  
  48.   
  49.         private void Control_LoadFinished(object sender, System.EventArgs e)  
  50.         {  
  51.             Control.LoadFinished -= Control_LoadFinished;  
  52.             Control.EvaluateJavascript($"DEFAULT_URL='{Element?.Uri}'; window.location.href='{PdfJsViewerUri}?file=file://{Element?.Uri}'; ");  
  53.         }  
  54.     }  
  55. }  

Display the PDF file

All set, now its time to use the pdfjs to show the PDF file. Write the custom renderers for each platform for your custom WebView as shared in above code snippet and in Uri bindable property set the path of PDF file.
 
Additionally, to give a connection between the pdfjs and this project, you need to bind them as like below code example.

Android

  1. string url = $"?file={WebUtility.UrlEncode(uri)}";  
  2. Control.LoadUrl(PdfJsViewerUri + url);   

iOS

  1. Control.EvaluateJavascript($"DEFAULT_URL='{Element?.Uri}'; window.location.href='{PdfJsViewerUri}?file=file://{Element?.Uri}'; ");  
Where PdfJsViewerUri is nothing but the path of pdfjs folder.
  1. namespace XFPDF  
  2. {  
  3.     public static class PDFUtils  
  4.     {  
  5.         private static string GetBaseUrl()  
  6.         {  
  7.             var fileHelper = DependencyService.Get<IFileHelper>();  
  8.             return fileHelper.ResourcesBaseUrl + "pdfjs/";  
  9.         }  
  10.   
  11.         public static string PdfJsViewerUri => GetBaseUrl() + "web/viewer.html";  
  12.     }  
  13. }  
You can find the complete source code from the following link.