Click here to Skip to main content
15,860,861 members
Articles / Web Development / HTML

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

Rate me:
Please Sign up or sign in to vote.
4.70/5 (5 votes)
22 Feb 2020CPOL16 min read 69.9K   1.4K   17  
A sample web application and discussions on creating, displaying, and downloading PDF documents with Web API data sources (including ASP.NET Core), client Angular CLI or AngularJS Components, and resolutions for web browser compatibility to handle PDF documents.
The article and the sample application provide consolidated and practical resolutions on the online displaying and downloading server-sourced PDF data documents, particularly with latest technologies of Angular and Web API RESTful data services (both ASP.NET 5 and ASP.NET Core 3.1). The browser compatibility issues and supported features for processing and displaying byte array data are also discussed in details. The approaches described in the article can also be extended to process similar items with different types, such as displaying document content, or downloading files, of the CSV text, spreadsheets, and images.

Introduction

Viewing and getting PDF documents are important features of web applications. A PDF document can be server-sourced with a PDF rendering tool for the data or directly retrieved from a physical file, or client-sourced with a PDF rendering tool in JavaScript for the data or markup page content. This article provides a sample application and detailed discussions on how to display and download the server-sourced PDF documents in respect to legacy, evolved, and up-to-date technologies and web browsers.

Libraries and Tools

The sample application with different project types uses these libraries and tools.

  • .NET Core 3.1
  • ASP.NET Core 3.1 Data Services
  • .NET Framework 4.6.1
  • Web API 2.0
  • PdfFileWriter library
  • Visual Studio 2019 or 2017
  • Angular 8 CLI
  • AngularJS 1.5.8
  • 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.

Image 1

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. The server-side data service project is embedded in each solution for easy data access.

You may check the available versions of the TypeScript for Visual Studio in the C:\Program Files (x86)\Microsoft SDKs\TypeScript folder. All downloaded project types of the sample application set the version of TypeScript for Visual Studio to 3.7 in the TypeScriptToolsVersion node of the *.csproj file. I have tested that all versions from 3.5 to 3.8 are compatible for the *.ts file compilations with the Visual Studio. If you need version 3.7 for Visual Studio, you can download the installation package from the Microsoft site.

For setting up and running the project types with Angular CLI, you need the node.js (recommended version 10.16.x LTS or above) and Angular CLI (recommended version 8.1.2 or above) installed globally on the local machine. Please check the node.js and Angular CLI documents for details.

Pdf_AspNetCore_Ng_Cli

  1. You need to use Visual Studio 2019 (version 16.4.x) on the local machine. The .NET Core 3.1 SDK is included in the Visual Studio installation and update.

  2. Download and unzip the source code file to your local workspace.

  3. Go to physical location of your local work space, double click the npm_install.bat and ng_build.bat files sequentially under the SM.Ng.Pdf.Web\AppDev 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.

  4. Open the solution with the Visual Studio 2019, and rebuild the solution with the Visual Studio.

  5. 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_Ng_Cli

  1. Double click the npm_install.bat and ng_build.bat files sequentially under the SM.WebApi.Pdf\ClientApp folder (also see the same NOTE for setting up the Pdf_AspNetCore_Ng_Cli project).

  2. Open and rebuild the solution with the Visual Studio 2019 or 2017.
  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. Open and rebuild the solution with the Visual Studio 2019 or 2017.

  2. 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.

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 still need it. Here is the home page of the sample application in Angular.

Image 2

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.

Image 3

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 services. The byte array will then be sent to the client-side for processes. A client-side approach in JavaScript, such as jsPDF.js, can also be used to build the PDF documents with requested raw data or page content. 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 with 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 built with the .NET Framework 4.x works only for the ASP.NET 5 projects. The .NET Core 3.x and ASP.NET Core 3.x are now fully supporting the Windows desktop development with the namespaces of System.Drawing, System.Windows.Forms, System.Windows.Forms.DataVisualization, PresentationCore, etc. This makes it possible to process and provide the PDF document byte array data through the ASP.NET Core API data services. You can see how the ASP.NET 5 Web API data services sends the PDF data to the client in the Pdf_AspNet5_Ng_Cli and Pdf_AspNet5_NgJS_1.5 sample applications. You can also see how the ASP.NET Core 3.1 data services outputs the PDF data to the client in the Pdf_AspNetCore_Ng_Cli application.

Request PDF Bytes from ASP.NET 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 are 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.

C#
[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:
    C#
    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.

    Image 4

ASP.NET Core 3.1 Related Changes

The Web API data service code and workflow with the ASP.NET Core 3.1 are mostly the same as those with the ASP.NET 5 except returning the custom response messages. The ASP.NET 5 Web API can return the HttpResponseMessage or IHttpActionResult type of object with the custom headers. When the custom headers and contents are required, the ASP.NET Core data services need to add these into an HttpResponseMessage object instance and then return it with the custom IActionResult wrapper.

The Get_OrderActivityPdf method in the ASP.NET Core API looks like below (most same code lines as in the ASP.NET 5 method are omitted).

C#
public IActionResult Get_OrderActivityPdf(int requestId = 0)        
{
    - - -

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

    - - -
    
    //Register HttpResponseMessage for disposing later.
    this.HttpContext.Response.RegisterForDispose(response);

    //Return IActionResult wrapper.
    return new HttpResponseMessageResult(response);
}

The custom HttpResponseMessageResult class transforms the response header and streams the response content that are returned to the caller.

C#
public class HttpResponseMessageResult : IActionResult
{
    private readonly HttpResponseMessage _responseMessage;

    public HttpResponseMessageResult(HttpResponseMessage responseMessage)
    {
        _responseMessage = responseMessage; // could add throw if null
    }

    public async Task ExecuteResultAsync(ActionContext context)
    {
        context.HttpContext.Response.StatusCode = (int)_responseMessage.StatusCode;

        foreach (var header in _responseMessage.Content.Headers)
        {
            context.HttpContext.Response.Headers.TryAdd
                    (header.Key, new StringValues(header.Value.ToArray()));
        }

        using (var stream = await _responseMessage.Content.ReadAsStreamAsync())
        {
            await stream.CopyToAsync(context.HttpContext.Response.Body);
            await context.HttpContext.Response.Body.FlushAsync();
        }
    }
}

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's default PDF viewer.

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

JavaScript
//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:

XML
<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:

JavaScript
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 whether 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 on which the Adobe Reader is installed but still cannot load the PDF viewer, you may need to uncheck two checkboxes in the Adobe Reader’s Edit > Preferences > Security (Enhanced) panel:

  • Enable Protected Mode at startup
  • Enable Enhanced Security

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 a lot of confusion 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:

JavaScript
//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).

Image 5

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.

JavaScript
//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 and Edge (EdgeHTML) 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. Thus, these browsers will render the error if using the Blob URL.

Fortunately, both Internet Explorer and Edge (EdgeHTML) 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.

JavaScript
//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 (EdgeHTML) to download PDF file when using the option-based scenario for browser compatibilities. See the Browser Compatibility Solutions Using Option-based Scenarios section later.

The new Microsoft Edge (Chromium) behaves as the same as the Chrome browser, which supports the Blob URL.

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.

Image 6

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.

JavaScript
//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.

History

  • 11/15/2018: Original post
  • 2/22/2020: Updated sample application with the ASP.NET Core 3.1 Web API data services and Angular 8 CLI. Added new sections and edited most existing sections in the article. The previous source code with the Angular 6 CLI can still be downloaded here.

License

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


Written By
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.

Comments and Discussions

 
-- There are no messages in this forum --