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.
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
-
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.
-
Download and unzip the source code file to your local workspace.
-
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.
-
Open the solution with the Visual Studio 2019, and rebuild the solution with the Visual Studio.
-
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
-
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).
- Open and rebuild the solution with the Visual Studio 2019 or 2017.
-
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
-
Open and rebuild the solution with the Visual Studio 2019 or 2017.
-
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.
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 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.
[Route("~/api/orderactivitypdf")]
[Route("~/api/orderactivitypdf/{requestId:int}")]
public HttpResponseMessage Get_OrderActivityPdf(int requestId = 0)
{
var pdfBytes = GetOrderActivityPdfBytes();
var response = new HttpResponseMessage(HttpStatusCode.OK);
response.Content = new ByteArrayContent(pdfBytes);
if (requestId == 1)
{
response.Content.Headers.ContentDisposition =
new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
response.Content.Headers.ContentDisposition.FileName = "OrderActivity.pdf";
}
response.Content.Headers.Add("x-filename", "OrderActivity.pdf");
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
return response;
}
private byte[] GetOrderActivityPdfBytes()
{
var dataList = TestData.GetOrderDataList(50, 7, 9);
var descriptorFile = "report_desc_sm.xml";
var descriptorNode = "reports/report[@id='SMStore302']";
return GetDataPdfBytes(dataList, descriptorFile, descriptorNode);
}
private byte[] GetDataPdfBytes<T>(List<T> dataList, string descriptorFile, string descriptorNode)
{
string xmlDescriptor = string.Empty;
XmlDocument objXml = new XmlDocument();
xmlDescriptor = File.ReadAllText(System.IO.Path.Combine
(System.Web.HttpRuntime.AppDomainAppPath, descriptorFile));
objXml.LoadXml(xmlDescriptor);
XmlNode elem = objXml.SelectSingleNode(descriptorNode);
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.
- 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)
{
return ResponseMessage(response);
}
- 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.
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).
public IActionResult Get_OrderActivityPdf(int requestId = 0)
{
- - -
var response = new HttpResponseMessage(HttpStatusCode.OK);
- - -
this.HttpContext.Response.RegisterForDispose(response);
return new HttpResponseMessageResult(response);
}
The custom HttpResponseMessageResult
class transforms the response header and streams the response content that are returned to the caller.
public class HttpResponseMessageResult : IActionResult
{
private readonly HttpResponseMessage _responseMessage;
public HttpResponseMessageResult(HttpResponseMessage responseMessage)
{
_responseMessage = responseMessage;
}
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:
showPdfMimeType() {
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) {
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:
let blob: any = new Blob([(response.data)], { type: 'application/pdf' });
let blobUrl: string = window.URL.createObjectURL(blob);
this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(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.
let blob: any = new Blob([(response.data)], { type: 'application/pdf' });
let blobUrl: string = window.URL.createObjectURL(blob);
let link: any = window.document.createElement('a');
if ('download' in link) {
link.setAttribute('href', blobUrl);
link.setAttribute("download", fileName);
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.
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.
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.
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.