Creating PDF Docs in Ionic using pdfMake.org

To start with, kudos to the Ionic team and the framework you have put together. I was working on a project last spring that started off as a native iOS app. But to reach a larger audience, I looked into hybrid options and stumbled across Ionic. And am I ever glad I did! Your framework is nothing short of amazing. There are times that I get frustrated with the learning curve as I try to work w/ the HTML5/CSS to get my app looking the way I want. But the fundamental base upon which things are built is great.

So, as I get more comfortable I am trying to answer more question an participate in the forum. This post is a demo of the work I did to incorporate generating pdf’s into my app. I had a design requirement to provide output of user data in PDF reports that could be previewed and then emailed as attachments. I found the javascript pdfMake.org library that is a wrapper for pdfKit.js and provides declarative pdf docs as JSON objects. I wrote a builderSvc to generate the JSON objects and a reportSvc to generate and save the pdf. I pulled out this part of my app and published it to a github repo:

GitHub - jeffleus/ionic-pdf: sample use of pdfMake.org library to generate a pdf and display in iFrame or save as local file

that also has an online demo shared as a github.io project page:

https://jeffleus.github.io/ionic-pdf/www/#/

I hope it will help anyone looking to incorporate a feature like this. And, I would appreciate a look and any comments from folks that are interested and could help me to clean things up even more. My todo list includes switching all plugin calls to ngCordova and publishing as a module/package that can be easily added to ionic projects. This would free developers up to focus on defining their pdf document declarations w/out spending time on the generation/saving.

4 Likes

Good Job on this library.

How about you generate PDF file if get data from localstorage or read data from any $scope?

Missed this when it was first posted, this will come in very useful for me.
THanks

Take a look at the github repo. It is setup in an Ionic sample app. I use the pdfmake library by splitting things into (20 major components. I create a report builder as an angular service. This is used to generate the desired JSON report format. Then, I use the output (JSON) of the report builder as the input to my report generator, which is another angular service. The generator passes the JSON of the report to the pdfmake library, which generates a binary blob of the report that can be output to a dataURL for display or output as a local file that can be attached to an email or saved.

For your question, I imagine that you can customize the report builder to receive input from localstorage or from the $scope and use this to output the report JSON. Keep in mind that the $scope probably shouldn’t be referenced in the actual builder. Instead, the builder should receive the data in the scope as a parameter. Your controller can pass the $scope values to the service and receive the JSON output back.

Let me know if you need to more specific help w/ this. I am far from an expert, but I am more than happy to share what I have learned so far. I love the Ionic framework and would like to give back what I can to the community.

-Jeff

@jeffleus How I can make a table layout from my concept with your guide?

I am testing your example.
This is working on chrome but it is not working on device.

the pdf file is created, but it is not opened
probably because of access rights

@jeffleus What’s your experience with adding images to your PDFs? I found that adding even 2-3 medium-quality images increases the report generation time significantly (from 2-3 seconds to 10+ seconds)…

@jeffleus
I could you your help on adding a jpeg to the .pdf.
I am starting with a camera app and I am trying to add the picture to the .pdf.

For my test I take the picture and then call the processes in the demo to create the .pdf.
That process is working.

This is the section of the main controller that takes the picture and runs the remainder of the demo.
$scope.takePicture = function() {
var options = {
quality : 75,
destinationType : Camera.DestinationType.DATA_URL,
sourceType : Camera.PictureSourceType.CAMERA,
allowEdit : true,
encodingType: Camera.EncodingType.JPEG,
targetWidth: 300,
targetHeight: 300,
popoverOptions: CameraPopoverOptions,
saveToPhotoAlbum: false
};
console.log(‘main controller b’);

  $cordovaCamera.getPicture(options).then(function(imageData) {
      $scope.imgURI = "data:image/jpeg;base64," + imageData;
      $scope.imgData = imageData;

  }, function(err) {
      // An error occured. Show a message to the user
  });
console.log('main controller c');

$scope.runReport($scope.imgData);

}

I am trying to pass the image and insert to the reportBuilderSvc.js.
All attempts I have made at this have failed.

Thank you in advance.

It looks like your .runReport call happens outside of the .getPicture promise execution. Code flow probably continues to the .runReport with an empty or non-existent .imgData attribute on the $scope. Then, the image is returned and imgData is set. Can you either set a breakpoint on the .runReport line and check the $scope.imgData? Or, can you console.log the $scope.imgData in the .runReport method to see what is in there?

My guess is that you need to call the .runReport after setting the .imgData inside of the .then() block. Or, you could broadcast an event on the $rootscope, like ‘camera:image_received’, and wire a listener in the controller. I prefer using events like this in cases of aync operation.

@jeffleus Thank you for your quick response.
looks like you are correct in the scope issue.

      console.log('image data 1: ' + $scope.imgData);

returns: [INFO:CONSOLE(45)] "image data 1: /9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGB …

console.log('imgOut: 2: ’ + imgOut);
returns: [INFO:CONSOLE(73)] “imgOut: 2: undefined”

I will research your two suggestions as a fix. Note: I am new to AngularJS.

Thank you!

The async stuff took me a while to figure out when I first started in Angular as well. It is most pronounced when you are interfacing w/ the phone’s hdwr because of the delays involved. I would recommend reading up on a couple of techniques that can be used. The first is promises and the second is broadcasting messages.

Promises
These are a very useful technique for handling async code in an orderly manner. They make your code significantly more readable and easier to digest when you review. You basically return a ‘handle’ to the results of a delayed function. Then, w/in the function you have code to complete and return to that promise. In your case you are using the promise returned from the getPicture() method of the cordovaCamera service. The thing you need to get comfortable with is that the .then() method where you process the imageData does not happen immediately. Instead, that method sits and waits for completion of the promise, while the execution continues down to the next line

console.log('main ctrl c');

As mentioned before, you could move your runReport method to execute inside the .then() function. You can also write your own promises to handle async processing of long running operations. Below is an example of code from one of my projects where I run a query against a Parse.com database that needs to be handled async:

// getPlayer returns a single player from Parse using id
getPlayer: function(playerId) { 
	var defer = $q.defer();

	var query = new Parse.Query(this);
	query.get(playerId, function(player) {
		defer.resolve(player);
	}, function(error) {
		defer.reject(error);
	});

	return defer.promise;
}, 

...

Player.getPlayer(pid).then(function(player) {
  //this executes after the getPlayer code completes and passes the player data
  doSomething(player.lastName);
});

Broadcast
The $rootScope has a broadcast method that can be used to dispatch a named event down to all child scopes. Then, you can wire a listener w/ the $on method of the scope to listen for the event. In your case, you could insert

$rootScope.broadcast('Camera::picture_taken', imageData);

instead of attaching the data to the $scope. This passes the imageData in an event that can be consumed in your reportProcessor as follows:

$scope.$on('Camera::picture_taken, function(event, imageData) {
  $scope.runReport(imageData);
});

This event listener would likely be setup in the initialization code for your controller. I really like to use this pattern in my code and could probably find an example to post in a Gist if you are interested. I know I struggled w/ this stuff for a while and noticed a big productivity gain once I grasped it a little better (although I am far from an expert…) Good luck.

@jeffleus
I was able to get it to work!
Thank you for all you help and instruction.

As mentioned by others, this is a great demonstration of the process.

I will follow-up with your suggestions as I believe that IONIC is going to be a product that I will be using frequently in the future.

Thanks again.

@jeffleus

Thanks for this. I downloaded the code and it worked first off.

Can you please give me any guidelines as to how to port this code to Ionic 2?

Thanks

1 Like

I haven’t really started any Ionic2 or Angular2 work yet. I will post some updates once I do. But that is still a few months off for me.

-Jeff

1 Like

Hello! Any update on this for Ionic 2? It worked well with Ionic 1, and was looking to use the same library for my new Ionic 2 project. Thanks!

@dbertels hey man if you got the code for ionic 2 so can you please share it here, m struggling for this from last 2 weeks

Thanks in advance…

@roshandhabekar Sorry - I ended up creating the pdf on a php server instead, since I was interacting with the server anyway.

@dbertels thanks man !