Click here to Skip to main content
13,794,235 members
Click here to Skip to main content
Add your own
alternative version

Stats

5.6K views
211 downloads
8 bookmarked
Posted 15 Nov 2018
Licenced CPOL

PDF Document Display and File Downloads with Angular, AngularJS, and Web API

, 15 Nov 2018
Rate this:
Please Sign up or sign in to vote.
A sample web application and discussions on creating, displaying, and downloading PDF documents with Web API data sources, client Angular CLI or AngularJS Components, and also presenting resolutions for web browser compatibilities to handle PDF documents.

Introduction

Displaying and downloading PDF documents are important features of web applications. What interests developers the most on this topic are the various processing scenarios and browser compatibilities. This article provides a sample application and detailed discussions on the topic in respect to legacy, evolved, and up-to-date technologies and web browsers.

The sample application uses these libraries and tools.

Web API for PDF Data Bytes

  • Web API 2.0
  • .NET Framework 4.6.1
  • PdfFileWriter library
  • Visual Studio 2015 or 2017

UI Web Projects

  • Angular 6 CLI with ASP.NET 5 and ASP.NET Core 2.1
  • For Angular 6, node.js installation on local machine is needed (version 8.11.x LTS or above is recommended)
  • AngularJS 1.5.8 with ASP.NET 5
  • Visual Studio 2017 (for ASP.NET Core and ASP.NET 5) or 2015 (for ASP.NET 5)
  • PDF.js Viewer
  • ngExDialog

Web Browsers

If you would like to get most of what the article and sample application delivers for practices, you may download and install more types of web browsers, either new or old, on your local machine. The installed browser types will automatically be shown in the IIS Express (browser) toolbar dropdown of the Visual Studio. You can then select a browser type before running the solution.

Build and Run Sample Application

The downloaded sources contain different Visual Studio solution/project types. Please pick up those you would like and do the setup on your local machine.

Pdf_AspNet5_Ng_Cli

  1. Double click the npm_install.bat and ng_build.bat files sequentially under the SM.WebApi.Pdf\ClientApp folder.

    Note: The ng build command may need to be executed every time after making any change in the TypeScript/JavaScript code. Whereas the execution of npm install is just needed whenever there is any update with the node module packages.

  2. Rebuild the solution with the Visual Studio.
  3. Since the project includes both server-side Web API and client-side Web UI, you can select the browser from the IIS Express tool bar dropdown and then click the IIS Express toolbar command (or press F5) to start the sample application.

Pdf_AspNetCore_Ng_Cli

  1. Do the same as the steps #1 and #2 of the Pdf_AspNet5_Ng_Cli project.
  2. The project has only the client Web UI. You also need to have the Pdf_AspNet5_Ng_Cli project on your local machine to provide the Web API data service. Run the Pdf_AspNet5_Ng_Cli project and keep it in the background. Or alternatively, start the IIS Express for the Web API data service by running the command in the console:
    C:\Program Files\IIS Express\iisexpress.exe" /site:SM.WebApi.Pdf  
    /config:"[your-vs-solution-path]\Pdf_AspNet5_Ng_Cli\.vs\config\applicationhost.config"

    Note: You can update the included WebApi_60100_Start.bat file with your solution path and then double click it to start the IIS Express for the Web API data service.

  3. Select the browser from the IIS Express tool bar dropdown and then click the IIS Express toolbar command (or press F5) to start the sample application.

Pdf_AspNet5_NgJS_1.5

  1. Re-build the solution with the Visual Studio.
  2. The solution also includes both server-side Web API and client-side Web UI, you can directly click the IIS Express toolbar command (or press F5) to start the sample application.

Throughout the article, I use the sample application projects in Angular for code demo and discussions. The project in AngularJS is for backward compatibility in case some developers need it. Here is the home page of the sample application in Angular.

When clicking a link, the Option-based Scenario for All Browsers under the View PDF in IFrame, for example, the PDF data document is shown on a popup dialog with an IFrame.

PDF Document Source

The sample application uses the server-side approach to provide the PDF document source, in which the PDF byte array is built with the requested data from the Web API. The byte array will then be sent to the client-side for processes. A client-side approach in JavaScript, such as PDF.js, can also be used to build the PDF documents with requested raw data. By comparisons, the server-side approach is much more powerful and has more features and flexibility. The server-provided byte array can also be used for various PDF document displaying and file downloading scenarios, either directly MIME type data transfer or client-side AJAX calls.

For demo purposes, a PDF data report, Product Order Activity (shown in the screenshot above), is generated from a static data source (simulating the data from a database) using the PdfDataReport tool I previously posted. The tool uses the PDF rendering library, PdfFileWriter, and dynamically builds the PDF byte array from a generic List of data with an XML descriptor for the report schema and styles. The PDF data document creation is not the focus of this article. Audiences can look into the source code and article A Generic and Advanced PDF Data List Reporting Tool for details if interested.

The PdfFileWriter library is not working with the ASP.NET Core project since it references the System.Drawing, System.Windows.Forms, and System.Windows.Forms.DataVisualization namespaces which are not fully supported by the .NET Framework Core, .NET standard library, and Microsoft.Windows.Compatibility pack. Thus, the sample application of ASP.NET Core project type doesn’t include the built-in Core MVC RESTful data service. Instead, it calls the Web API data services from other project types for obtaining the PDF byte array data.

Request PDF Bytes from Web API

When the request is sent to the Get_OrderActivityPdf() method of the Web API, the GetOrderActivityPdfBytes() method and then, in turn, the generic GetDataPdfBytes() method is called to obtain the PDF byte array. No physical PDF file is created on Web API server drives. The resulted byte array is assigned to the content of the HttpResponseMessage using the ByteArrayContent object. Other items are also set for the response header before the response is returned to the caller.

[Route("~/api/orderactivitypdf")]
[Route("~/api/orderactivitypdf/{requestId:int}")]
public HttpResponseMessage Get_OrderActivityPdf(int requestId = 0)
{
    //Call to generate PDF byte array.
    var pdfBytes = GetOrderActivityPdfBytes();

    //Create a new response.
    var response = new HttpResponseMessage(HttpStatusCode.OK);

    //Assign byte array to response content.
    response.Content = new ByteArrayContent(pdfBytes);

    //Set "Content-Disposition: attachment" for downloading file through direct MIME transfer. 
    if (requestId == 1)
    {
        //Explicitly specified as file downloading.
        response.Content.Headers.ContentDisposition = 
                  new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");

        //This FileName in Content-Disposition won't be taken by Edge 
        //if using Blob for downloading file.
        response.Content.Headers.ContentDisposition.FileName = "OrderActivity.pdf";
    }

    //Add default file name that can be used by client code for both MIME transfer and AJAX Blob.
    response.Content.Headers.Add("x-filename", "OrderActivity.pdf");

    //Set MIME type.
    response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");

    //Returning base HttpResponseMessage type.
    return response;
}
        
//Get PDF byte array for a specific report. 
private byte[] GetOrderActivityPdfBytes()
{
    //Get data list fabricated for breaking page in the end of a group.
    var dataList = TestData.GetOrderDataList(50, 7, 9);

    //Define XML descriptor node.
    var descriptorFile = "report_desc_sm.xml";
    var descriptorNode = "reports/report[@id='SMStore302']";

    //Call to generate PDF byte arrays.
    return GetDataPdfBytes(dataList, descriptorFile, descriptorNode);
}

//Generic function for generating PDF byte array.
private byte[] GetDataPdfBytes<T>(List<T> dataList, string descriptorFile, string descriptorNode)
{
    string xmlDescriptor = string.Empty;
    XmlDocument objXml = new XmlDocument();
                        
    //Load XML report descriptor.            
    xmlDescriptor = File.ReadAllText(System.IO.Path.Combine
                    (System.Web.HttpRuntime.AppDomainAppPath, descriptorFile));
    objXml.LoadXml(xmlDescriptor);
            
    //Get node for a designated report.
    XmlNode elem = objXml.SelectSingleNode(descriptorNode);            

    //Call library tool to get PDF bytes.
    ReportBuilder builder = new ReportBuilder();
    var pdfBytes = builder.GetPdfBytes(dataList, elem.OuterXml);

    return pdfBytes;
}

Two coding scenarios are worth being further discussed for the Get_OrderActivityPdf() method.

  1. Choices of using IHttpActionResult or HttpResponseMessage type to return the response. The IHttpActionResult in the Web API 2.0 is an extended wrapper of the HttpResponseMessage. It offers several benefits over the HttpResponseMessage, such as better implementing structures, chaining action results, simplified unit testing for controllers, using Async and Await by default, easy to create own ActionResult, and so on. Since the sample application is demonstrated for single PDF byte array downloading process without taking considerations of entire Web API ActionResult structures, the response here is returned in a straightforward manner as the base HttpResponseMessage object. If you would like to use the IhttpActionResult as the return type, just change two code lines, the method definition and return code, in the method:
    public IHttpActionResult Get_OrderActivityPdf(int requestId = 0)
    {
        //Same code as shown previously...
    
        //Returning base HttpResponseMessage type.
        //return response;
       
        //Convert to IHttpActionResult type and return it.
        return ResponseMessage(response);
    }
  2. About the Content-Disposition response header item. The method accepts an optional int type argument requestId. This is used for conditionally setting the Content-Disposition: attachment in the response header by assigning hard-coded “attachment” to the constructor of ContentDispositionHeaderValue class. If the requestId value 1 is passed, the code adds this Content-Disposition header item, which specifies the byte array content to be downloaded as a file when using traditional MIME data transfer. Otherwise, the response content should be displayed on the browser based on the content type. This is useful for old browsers or browsers that do not support JavaScript Blob object and its derived structures. For file downloading with AJAX data transfer and JavaScript Blob related processing logic, the Content-Disposition: attachment in the response header is ignored. You can check if the Content-Disposition item is included or not in the response header using the tools, such as Fiddler2 or Postman, when calling the Web API method from any browser.

Traditional MIME PDF Data Transfer

The MIME type “application/pdf” defined in the RFC 3778 is for the standard PDF data transfer and supported by all major browsers with even very old versions. Although it’s not the pure Angular way, using the traditional MIME type data transfer to get the PDF documents is the easiest and straightforward for any web application, especially serving as the last resort for applications that need to support vast variety and older versions of browsers.

In Angular, it’s easy for the code to obtain the PDF documents from the Web API method to browsers by assigning the Web API URL to the target source, such as iframe tag, embed tag, or another window. In the sample application, the iframe is used to display the PDF data report, Product Order Activity, using any browser default PDF viewer.

The code in Angular component also uses the bypassSecurityTrustResourceUrl method of the DomSanitizer API to wrap the source URL:

//Default viewers with direct MIME transfer.
showPdfMimeType() {     
    //Request and assign MIME data to element src.
    this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl
                     (WebApiRootUrl + this.callerData.apiMethod);;
};

The iframe tag and its settings in HTML are also quite standard:

<iframe id="pdfViewer" [src]="iframeSrc" style="width: 100%; height: 450px;" zindex="100" ></iframe>

To download the PDF file directly with the MIME type data transfer, just specify one line of code in the method:

downloadMimePdfFile(apiMethod) {
    //Assign MIME type data source to browser page.
    window.location.href = WebApiRootUrl + apiMethod;
}

Clicking the Default Viewer with Direct MIME Type Transfer or Download File with Direct MIME Type Transfer links on the demo home page will execute the above code lines and render the expected results.

Since the Chrome, Firefox, Opera, and Edge browsers have their own proprietary PDF viewers to display the documents delivered as the MIME type, no special consideration is needed on weather or not the Adobe Reader exists in the client devices. The Internet Explorer, however, embeds the Adobe Reader as the default PDF viewer so that you need to install the Adobe Reader on local machine or set it as add-ons to the browser (please see this link for the support). If you use Internet Explorer 11 with previous versions of Windows, or previous builds of the Windows 10, on which the Adobe Reader is installed but still cannot load the PDF viewer, you need to uncheck two checkboxes in the Adobe Reader’s Preferences > Security (Enhanced) panel:

  • Enable Protected Mode at startup
  • Enable Enhanced Security

You don’t have to disable these security features in the Adobe Reader if using Internet Explorer 11 with newer builds of the Windows 10 and the default application for PDF is set to Adobe Reader on the Windows 10.

Using JavaScript Blob Object and Blob URL

With the AJAX data transfer modal for the Web applications, the Blob object is becoming popular to process file related operations in client JavaScript code. However, the usability and API availability are quite different among browser types, which causes many confusing and inconvenience for developing web applications regarding file content display or file downloads. Let’s see what we can do with the Blob in the Angular code for the PDF documents.

Display PDF Documents with Blob

It seems simple to initiate a Blob object with the AJAX arraybuffer data source and MIME type, create a Blob URL in the memory, and then assign the Blob URL to the HTML target source as indicated in these lines of code:

//Initiate blob object with byte array and MIME type.
let blob: any = new Blob([(response.data)], { type: 'application/pdf' });

//Create blobUrl from blob object.
let blobUrl: string = window.URL.createObjectURL(blob);

//Bind trustedUrl to element src.
this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(blobUrl);          

//Revoking blobUrl.
window.URL.revokeObjectURL(blobUrl);

This works well for the Chrome, Firefox, and Opera with versions supporting the Blob object starting many years ago. Although the Internet Explorer has supported Blob object since version 10, the browser, and even its successor, the Edge, doesn’t load the PDF document to the target source even the Blob URL is generated. The most possible reason could be the Blob URL handling logic and security enforcement by the browsers.

The Default Viewer with Blob from AJAX Call link on the home page of the sample application demonstrates the display of the PDF data report in an IFrame. A message dialog is shown for unsupported browsers, such as Internet Explorer, Edge, and Safari (for Windows).

Download PDF Files with Blob

Downloading PDF files with the AJAX data source and Blob URL also works for Chrome, Firefox, and Opera browsers, in which a dynamic <a> element having the download attribute and simulating click event are needed to mediate the operation.

//Initiate blob object with byte array and MIME type.
let blob: any = new Blob([(response.data)], { type: 'application/pdf' });

//Create blobUrl from blob object.
let blobUrl: string = window.URL.createObjectURL(blob); 

//Use a download link.
let link: any = window.document.createElement('a'); 
if ('download' in link) {
    link.setAttribute('href', blobUrl);

    //Set the download attribute.
    //Edge doesn’t take filename here.
    link.setAttribute("download", fileName);

    //Simulate clicking download link.
    let event: any = window.document.createEvent('MouseEvents');
    event.initMouseEvent
        ('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
    link.dispatchEvent(event);    
}

Internet Explorer 11 still cannot use the Blob URL for downloading the file based on the above code. The browser also does not support the download attribute in the dynamic link. From the Edge, the file can be downloaded using the above code but it ignores the default fileName value and, instead, constructs file name as GUID string and pushes the file to a pre-defined path, such as [current-user-folder]\AppData\Local\Packages\Microsoft.MicrosoftEdge_8wekyb3d8bbwe\AC\MicrosoftEdge\Cache\AST7S3US, without any available customization. Thus, it seems no practical significance for the Edge browser to use the Blob URL to download a PDF document file.

Fortunately, both Internet Explorer and Edge provide the navigator.msSaveBlob method that fulfills the file downloading task directly with the Blob object. The default fileName value is also picked up by the process.

//Use msSaveBlob if supported.
let blob: any = new Blob([response.body], { type: "application/pdf" });
if (navigator.msSaveBlob) {
    navigator.msSaveBlob(blob, fileName);
}

The msSaveBlob is an important feature for Internet Explorer 11 and Edge to download PDF file when using the option-based scenario for browser compatibilities. See the Browser Compatibility Solutions Using Option-based Scenarios section later.

Show PDF Documents with PDF.js Viewer

The PDF.js is a PDF rendering tool in JavaScript owned by the mozilla.org. Its Viewer API provides the UI to display the PDF documents on browsers based on the PDF.js. The PDF.js Viewer is actually the default PDF viewer of the Firefox browser. Developers can build their own PDF viewer by using the PDF.js renderer or modifying existing PDF.js Viewer. For easy demonstration, the sample application uses the basically unmodified version of the PDF.js Viewer (only removed the default PDF file URL by resetting it to empty string: var DEFAULT_URL = ''; in the web/viewer.js file). All PDF.js and Viewer library files are located in the ClientApp/PdfViewer or wwwroot/ClientApp/PdfViewer folder of the SM.WebApi.Pdf project.

As the time of writing this article, the latest stable version of the PDF.js is 2.0.943. This version works fine for all latest versions of major browsers except for Internet Explorer 11 in which a runtime error is thrown when closing the viewer in an IFrame. The sample application that comes with the PDF.js 1.8.188, the latest release version that supports all major browsers being used in the market including Internet Explorer 11. In your real applications, you may replace the files in the …/ClientApp/PdfViewer with the latest stable version if your applications would not tend to support Internet Explorer 11. You can find all release versions of the PDF.js from the site here.

To open the PDF document in an IFrame, the PDFViewerApplication.open method in the viewer.js should be called from the Angular component. In the below line, the iframe is the DOM object and the response.data is the PDF byte array object.

//Call PDFJS Viewer open method and pass byte array data.
iframe.contentWindow.PDFViewerApplication.open(response.data);

The PDFJS Viewer with Byte Array from AJAX Call link on the home page of the sample application can display the Product Order Activity report from all major browsers except the Safari (for Windows) due to inability to support the PDF.js as the same as for the JavaScript Blob object. The Apple stopped to release the Safari for Windows after the version 5.1.7. I don’t use any Macintosh machine but I think that the later versions of Safari for Macintosh should work well for both the Blob object and PDF.js.

Although the PDF.js Viewer provides decent options and features for displaying the PDF content, it’s bulky and has the performance impact in some instances, especially when using the older versions of either PDF.js or browsers. I do notice that the later versions of PDF.js Viewer loads the data with a large byte array much faster when using the latest versions of major browsers including the IE 11.

Browser Compatibility Solutions Using Option-based Scenarios

There is usually no issue for all major browsers to display PDF documents and download PDF files with the traditional MIME type data transfer, even for the Safari (for Windows) and older versions of Internet Explorer. However, when switching to using the AJAX calls and JavaScript Blob object, browsers behave differently due to the supporting status of JavaScript objects and APIs. In the past, Web developers commonly use the code to explicitly check the browser types and versions for conditionally directing to executions of particular code sections. The better practice now is to conduct the available option-based scenarios to resolve possible browser compatibility issues. The sample application presents such scenarios for displaying PDF documents and downloading PDF files as shown with the link Option-based Scenario for All Browsers on the demo home page. Since the functionality and code pieces for each option-based approach have been detailed in the previous sections of the article, below are listed only option selections and execution sequences. Audiences can practice with the code and make any change to meet their needs.

View PDF in IFrame

  • Use the JavaScript Blob URL with default PDF viewer as the first choice.
  • If it fails, render the PDF.js Viewer and then load the PDF with the byte array object.
  • If it still fails, the browser is unable to show the PDF document with the AJAX data and JavaScript. The direct MIME type PDF data transfer is then used.

Download PDF File

  • Using the JavaScript Blob URL as the first choice.
  • If it fails, trying to call one of the save-blob methods.
  • If it still fails, switching to the direct MIME type PDF file download.

Note that there is a downside when using the MIME type data transfer as the last resort in the option-based scenario. Since the Blob-object approach has called the server to load the AJAX data already, the browser will call the server again to directly transfer the MIME type data if it doesn’t support the Blob object. This may pose a noticeable additional delay if the data size is large.

Summary

Many discussions and code examples for online displaying or downloading PDF data documents can be seen across the Internet. But this article and the sample application provide consolidated and practical resolutions on this area, particularly with latest technologies of Angular and Web API RESTful data service. The approaches described in the article can also be extended to process similar items with different types, such as displaying content, or downloading files, of the CSV text, spreadsheets, and images.

History

  • 11/15/2018: Original post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Shenwei Liu
United States United States
Shenwei is a software developer and architect, and has been working on business applications using Microsoft and Oracle technologies since 1996. He obtained Microsoft Certified Systems Engineer (MCSE) in 1998 and Microsoft Certified Solution Developer (MCSD) in 1999. He has experience in ASP.NET, C#, Visual Basic, Windows and Web Services, Silverlight, WPF, JavaScript/AJAX, HTML, SQL Server, and Oracle.

You may also be interested in...

Pro

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web02 | 2.8.181207.3 | Last Updated 15 Nov 2018
Article Copyright 2018 by Shenwei Liu
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid