Thursday, December 27, 2012

Templated PDF Printing for Appcelerator based iOS Apps


Content

  1. Motivation
  2. Idea of a solution
  3. Detailed explanation
  • IOS Module
  • HTML-Template
  • Inline-Images
  • HTML Page-Formatting

Motivation

With Javascript being the language of choice within the appcelerator framework,
a huge variety of libraries and techniques for common problems, the catch being,
that many of these depend on the context of a web browser to funtion properly.
When it comes to creating PDF data within Appcelerator apps, so far the approach
had therefore been to small pure JS libs, such as jsPDF (http://jspdf.com/).
Yet, because of it's lowlevel approach (of drawing rectangles and lines) and
rendering text, it can be cumbersome to create a fully layouted document.

Idea of a solution

As an alternative, I propose to make use of Appcelerators module framework, i.e.,
making use of native code, to pass an html string to an IOS module that will
render the content in a UIWebview object and subsequently to a PDF file, that
is returned to the application layer.

IOS Module

The module used for this solution has a method "setHTMLString", that can be
called from the application layer with the HTML string to be rendered in
a UIWebview object. Upon loading, the method "webViewDidFinishLoad" is called,
where the page then is rendered to a PDF file, subsequently firing an event
"pdfready", including a path to the file. This is necessary, as events in
Appcelerator can only carry JSON serializable data. Because the UIWebkit library
is not thread safe, all the calls in "setHTMLString" and "webViewDidFinishLoad"
have to be queued on the main thread by using "dispatch_async". For details check
the source code from github, referenced below. For general information about
developing iOS modules, check https://wiki.appcelerator.org/display/guides/iOS+Module+Development+Guide

HTML Template

Since we are passing an HTML string from the application layer and basically
render it in a browser window, all known HTML traits apply and the document
can be layouted, as applicable. But, since there usually will be no webserver
running to pull resources from, you have to inline any CSS or JS code into the
HTML string, you pass to the IOS module. Based on such a template you might
create your final HTML string by replacing certain placeholders, such as
<!--PLACEHOLDER--> with content data. Or you choose to just construct your
HTML string, as needed. Consider the following Appcelerator code:
 var html2pdf = require('com.factisresearch.html2pdf');  
 Ti.API.info("module is => " + html2pdf);  
   
 html2pdf.addEventListener('pdfready', function(e) {  
   var emailDialog = Ti.UI.createEmailDialog();  
     emailDialog.addAttachment(Ti.Filesystem.getFile(e.pdf));  
     emailDialog.open();  
 });  
   
 var html = '<html><body><p>Hello World!</p></body></html>';  
   
 html2pdf.setHtmlString(html);  


Inline Images

Of course it should be possible to add images to the document. Because generally
it will not be possible to pull resources, the images also have to be embedded
into the HTML string. How this can be done is explained here:
http://www.techerator.com/2011/12/how-to-embed-images-directly-into-your-html/
and in countless other places. Consider the following code:
 var html2pdf = require('com.factisresearch.html2pdf');  
 Ti.API.info("module is => " + html2pdf);  
   
 html2pdf.addEventListener('pdfready', function(e) {  
   var emailDialog = Ti.UI.createEmailDialog();  
     emailDialog.addAttachment(Ti.Filesystem.getFile(e.pdf));  
     emailDialog.open();  
 });  
   
 var html = '<html><body>';  
   
 var imageData = Titanium.Filesystem.getFile(Ti.Filesystem.getResourcesDirectory(), 'Default.png').read();  
 var iv = Ti.UI.createImageView({  
     image: imageData  
 });  
 var image = iv.toImage();  
 var imageB64 = Ti.Utils.base64encode(image);  
       
 html += '<p><img src="data:image/png;base64,'+imageB64+'"></p></body></html>';  
   
 html2pdf.setHtmlString(html);  


HTML Page-Formatting

The IOS module described above uses static page sizes and bounds for the printable
area. You may however want to customize this for different pages. On this account
CSS offers a range of customization options, such as the "@page" attribute, where
margins can be defined or the "page-break-before" style, that can set on an html
element to enforce the subsequent content to be displayed on a new page in the
resulting pdf.
-------------------------------
 var html2pdf = require('com.factisresearch.html2pdf');  
 Ti.API.info("module is => " + html2pdf);  
   
 html2pdf.addEventListener('pdfready', function(e) {  
   var emailDialog = Ti.UI.createEmailDialog();  
     emailDialog.addAttachment(Ti.Filesystem.getFile(e.pdf));  
     emailDialog.open();  
 });  
   
 var html = '<html><body>';  
   
 var works = queryDBForWorks();  
 for (var i in works) {  
     var entry = {};  
     var rs = db.getWorkInfo(works[i]);  
       
     html += ('<h2 '+(i == 0 ? '>' : 'style="page-break-before:always;">')+''+rs.fieldByName('TITLE')+' - '+rs.fieldByName('ARTIST')+'</h2>');  
     html += ('<img src="data:image/jpg;base64,'  
             +Ti.Utils.base64encode(rs.fieldByName('THUMBNAIL'))+'">');  
     html += ('<p>'+''+rs.fieldByName('TEXT')+'</p></div>');  
     rs.close();  
 }   
   
   
 html2pdf.setHtmlString(html);  

You may pull an implementation, along with an example from
https://github.com/fscz/html2pdf