How To Create And Display A PDF File In Your Ionic App

In this tutorial we're going to create and display PDF files using the JavaScript libraries pdfmake and PDF.js. I'll explain why I chose these libraries and then we'll create a simple PDF with some text and tables in it.

The source code can be found on GitHub.


Generate PDF

I had a look at 3 different libraries for generating PDF files in JavaScript:

All 3 libraries allow you to programmatically create a PDF file by adding pages, text and images.

Here is a short overview of what each library has to offer.

jsPDF

  • Basic text styling (fonts, colors, font size)
  • Draw basic shapes (circle, rectangle, triangle, line)
  • Render HTML to PDF, but no support for CSS

See the jsPDF website for examples.

pdfkit

  • Draw vector graphics
  • Advanced text layout (columns, page breaking, custom fonts and more)

For an example of the capabilities of pdfkit, see the documentation as a generated PDF Guide.

pdfmake

  • Built on top of pdfkit
  • Define document layout in JSON
  • Support for tables out of the box

See the pdfmake playground for examples.

Based on the supported features, I chose pdfmake and after trying it out I was happy enough to proceed with it.

Display PDF

For displaying the PDF, I thought it would be as simple as just opening the PDF using the InAppBrowser, but unfortunately that only works on iOS and not on Android. And it would be nice if the file just opens inside the app instead of using a 3rd party app.

So, after hitting Google, I came across pdf.js, which is made by Mozilla and is used as the default PDF viewer in FireFox.

The cool thing about pdf.js is that you can completely customize what you want the viewer to look like and there is also a default viewer included.

OK, so that was enough for the intro, right? Let's write some code!

What are we going to build?

Let's build an app that has a button and when you tap on it, an invoice pdf will be generated and displayed in a modal dialog.

App Screenshot

For the sake of keeping this tutorial short and to the point, we'll just use some random data for the invoice using chance.js. In a real-life app, you'll get that data from user input or a service.

Setting up the project

Start by creating a new Ionic app.

$ ionic start ionic-tutorial-pdf blank

Next, we need to install the libraries we're going to use.

$ bower install pdfmake angular-pdf chance --save

As you can see, we'll be using the angular-pdf directive. This directive uses pdf.js to display the pdf, so we don't have to write all that code ourselves.

Make sure you inject the directive in app.js.

angular.module('starter', ['ionic', 'pdf'])  

Add the following lines into index.html:

<script src="lib/pdfmake/build/pdfmake.js"></script>  
<script src="lib/pdfmake/build/vfs_fonts.js"></script>  
<script src="lib/pdfjs-dist/build/pdf.js"></script>  
<script src="lib/angular-pdf/dist/angular-pdf.js"></script>

<!-- Chance.js is only used to generate random data -->  
<script src="lib/chance/chance.js"></script>

<!-- your app's js -->  
<script src="js/app.js"></script>  
<script src="js/invoice.service.js"></script>  
<script src="js/document.controller.js"></script>  

Create the UI

We'll start by adding a Create Invoice button to the first view that is displayed when the app is started.

<body ng-app="starter">  
    <ion-pane>
      <ion-header-bar class="bar-stable">
        <h1 class="title">PDF Tutorial</h1>
      </ion-header-bar>
      <ion-content ng-controller="DocumentController as vm" class="padding">
        <p><button ng-click="vm.createInvoice()" class="button icon-left ion-play button-assertive">Create Invoice</button></p> 
      </ion-content>
    </ion-pane>
</body>  

And we'll also add the template for the modal dialog.

<script id="pdf-viewer.html" type="text/ng-template">  
    <ion-modal-view>
    <ion-header-bar>
        <h1 class="title">Invoice</h1> 
        <button ng-click="vm.modal.hide()" class="button button-icon icon ion-android-close"></button>
    </ion-header-bar>
    <ion-content>
        <ng-pdf ng-if="pdfUrl" template-url="partials/viewer.html" canvasid="pdf" scale="page-fit"></ng-pdf>  
    </ion-content>
    </ion-modal-view>
</script>  

Now let's have a look at the invoice.service.js and document.controller.js we're going to build.

Create the Invoice Service

The InvoiceService will be responsible for creating our invoice PDF and returning it in a Uint8Array format. This array will be passed on later in the controller so it can be displayed with pdf.js.

angular.module('starter').factory('InvoiceService', ['$q', InvoiceService]);

function InvoiceService($q) {  
    function createPdf(invoice) {
        return $q(function(resolve, reject) {
            var dd = createDocumentDefinition(invoice);
            var pdf = pdfMake.createPdf(dd);

            pdf.getBase64(function (output) {
                resolve(base64ToUint8Array(output));
            });
        });
    }

    return {
        createPdf: createPdf
    };    
}

function base64ToUint8Array(base64) {  
    var raw = atob(base64);
    var uint8Array = new Uint8Array(raw.length);
    for (var i = 0; i < raw.length; i++) {
    uint8Array[i] = raw.charCodeAt(i);
    }
    return uint8Array;
}

Create the Document Definition

The document definition is just a big JSON object that defines the content and styles for the PDF.

Here is the implementation for the createDocumentDefinition function:

function createDocumentDefinition(invoice) {

    var items = invoice.Items.map(function(item) {
        return [item.Description, item.Quantity, item.Price];
    });

    var dd = {
        content: [
            { text: 'INVOICE', style: 'header'},
            { text: invoice.Date, alignment: 'right'},

            { text: 'From', style: 'subheader'},
            invoice.AddressFrom.Name,
            invoice.AddressFrom.Address,
            invoice.AddressFrom.Country,        

            { text: 'To', style: 'subheader'},
            invoice.AddressTo.Name,
            invoice.AddressTo.Address,
            invoice.AddressTo.Country,  

            { text: 'Items', style: 'subheader'},
            {
                style: 'itemsTable',
                table: {
                    widths: ['*', 75, 75],
                    body: [
                        [ 
                            { text: 'Description', style: 'itemsTableHeader' },
                            { text: 'Quantity', style: 'itemsTableHeader' },
                            { text: 'Price', style: 'itemsTableHeader' },
                        ]
                    ].concat(items)
                }
            },
            {
                style: 'totalsTable',
                table: {
                    widths: ['*', 75, 75],
                    body: [
                        [
                            '',
                            'Subtotal',
                            invoice.Subtotal,
                        ],
                        [
                            '',
                            'Shipping',
                            invoice.Shipping,
                        ],
                        [
                            '',
                            'Total',
                            invoice.Total,
                        ]
                    ]
                },
                layout: 'noBorders'
            },
        ],
        styles: {
            header: {
                fontSize: 20,
                bold: true,
                margin: [0, 0, 0, 10],
                alignment: 'right'
            },
            subheader: {
                fontSize: 16,
                bold: true,
                margin: [0, 20, 0, 5]
            },
            itemsTable: {
                margin: [0, 5, 0, 15]
            },
            itemsTableHeader: {
                bold: true,
                fontSize: 13,
                color: 'black'
            },
            totalsTable: {
                bold: true,
                margin: [0, 30, 0, 0]
            }
        },
        defaultStyle: {
        }
    }

    return dd;
}

As you can see, we're just adding all the lines of text to the document definition content. We have full control over the style of the text when we add it and we can define table rows and columns.

We can also define styles to reference in our content.

I won't go into much detail on how to format the PDF, you can use the pdfmake playground to learn how to add different layouts and you also just copy in your layout and it will display the generated PDF.

Create the Document Controller

The controller is responsible for getting the dummy data and sending that off to the invoice service. The service will return the pdf and then we'll update the $scope.pdfUrl value so the ng-pdf directive knows what to display.

angular.module('starter').controller('DocumentController', ['$scope', '$ionicModal', 'InvoiceService', DocumentController]);

function DocumentController($scope, $ionicModal, InvoiceService) {  
    var vm = this;

    setDefaultsForPdfViewer($scope);

    // Initialize the modal view.
    $ionicModal.fromTemplateUrl('pdf-viewer.html', {
        scope: $scope,
        animation: 'slide-in-up'
    }).then(function (modal) {
        vm.modal = modal;
    });

    vm.createInvoice = function () {
        var invoice = getDummyData();

        InvoiceService.createPdf(invoice)
                        .then(function(pdf) {
                            var blob = new Blob([pdf], {type: 'application/pdf'});
                            $scope.pdfUrl = URL.createObjectURL(blob);

                            // Display the modal view
                            vm.modal.show();
                        });
    };

    // Clean up the modal view.
    $scope.$on('$destroy', function () {
        vm.modal.remove();
    });

    return vm;
}    

There are a couple of defaults you can set for the ng-pdf directive, like the text that should be displayed when the pdf is loading. Below we are also handling errors by writing them to the console and we also log the progress.

function setDefaultsForPdfViewer($scope) {  
    $scope.scroll = 0;
    $scope.loading = 'loading';

    $scope.onError = function (error) {
        console.error(error);
    };

    $scope.onLoad = function () {
        $scope.loading = '';
    };

    $scope.onProgress = function (progress) {
        console.log(progress);
    };
}

And the last function in the controller will provide us with the dummy data to populate the PDF with.

function getDummyData() {  
    return {
        Date: new Date().toLocaleDateString("en-IE", { year: "numeric", month: "long", day: "numeric" }),
        AddressFrom: {
            Name: chance.name(),
            Address: chance.address(),
            Country: chance.country({ full: true })
        },
        AddressTo: {
            Name: chance.name(),
            Address: chance.address(),
            Country: chance.country({ full: true })
        },
        Items: [
            { Description: 'iPhone 6S', Quantity: '1', Price: '€700' },
            { Description: 'Samsung Galaxy S6', Quantity: '2', Price: '€655' }
        ],
        Subtotal: '€2010',
        Shipping: '€6',
        Total: '€2016'
    };
}

Displaying the PDF

The only thing left to do now is writing the template for the pdf viewer.

Add the file viewer.html in the www/partials directory.

{{loading}}
<canvas id="pdf" class="rotate0"></canvas>  

The pdf will be displayed on the <canvas>. I'm keeping it very simple for now, it's only displaying the pdf but you can add buttons to zoom in/out, rotate, go to the next/previous page and jump to a page number. For an example of how to hook up these buttons, have a look a the documentation.

We're done!

This should now work on both the desktop browser and mobile devices.

To test on the desktop browser:

$ ionic serve

To test on the mobile devices:

$ ionic run android
$ ionic run ios

If it's not working on older Android devices, you should use Crosswalk.

The source code can be found on GitHub.


Follow me on Twitter @ashteya and sign up for my weekly emails to get new tutorials.

If you found this article useful, could you hit the share buttons so that others can benefit from it, too? Thanks!

comments powered by Disqus