package at.modalog.cordova.plugin.html2pdf; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.PluginResult; import org.json.JSONArray; import org.json.JSONException; import com.itextpdf.text.Document; import com.itextpdf.text.DocumentException; import com.itextpdf.text.Element; import com.itextpdf.text.Image; import com.itextpdf.text.PageSize; import com.itextpdf.text.pdf.PdfWriter; import android.R.bool; import android.annotation.TargetApi; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Canvas; import android.media.MediaScannerConnection; import android.media.MediaScannerConnection.OnScanCompletedListener; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.os.Handler; import android.print.PrintAttributes; import android.print.PrintDocumentAdapter; import android.print.PrintManager; import android.printservice.PrintJob; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.util.Log; @TargetApi(19) public class Html2pdf extends CordovaPlugin { private static final String LOG_TAG = "Html2Pdf"; private CallbackContext callbackContext; // change your path on the sdcard here private String publicTmpDir = ".at.modalog.cordova.plugin.html2pdf"; // prepending a dot "." would make it hidden private String tmpPdfName = "print.pdf"; // set to true to see the webview (useful for debugging) private final boolean showWebViewForDebugging = false; /** * Constructor. */ public Html2pdf() { } @Override public boolean execute (String action, JSONArray args, CallbackContext callbackContext) throws JSONException { try { if( action.equals("create") ) { if( showWebViewForDebugging ) { Log.v(LOG_TAG,"java create pdf from html called"); Log.v(LOG_TAG, "File: " + args.getString(1)); // Log.v(LOG_TAG, "Html: " + args.getString(0)); Log.v(LOG_TAG, "Html start:" + args.getString(0).substring(0, 30)); Log.v(LOG_TAG, "Html end:" + args.getString(0).substring(args.getString(0).length() - 30)); } if( args.getString(1) != null && args.getString(1) != "null" ) this.tmpPdfName = args.getString(1); final Html2pdf self = this; final String content = args.optString(0, "<html></html>"); this.callbackContext = callbackContext; cordova.getActivity().runOnUiThread( new Runnable() { public void run() { if( Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT ) // Android 4.4 { /* * None-Kitkat pdf creation (Android < 4.4) */ self.loadContentIntoWebView(content); } else { /* * Kitkat pdf creation by using the android print framework (Android >= 4.4) */ // Create a WebView object specifically for printing WebView page = new WebView(cordova.getActivity()); page.getSettings().setJavaScriptEnabled(false); page.setDrawingCacheEnabled(true); // Auto-scale the content to the webview's width. page.getSettings().setLoadWithOverviewMode(true); page.getSettings().setUseWideViewPort(true); page.setInitialScale(0); // Disable android text auto fit behaviour page.getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL); if( showWebViewForDebugging ) { page.setVisibility(View.VISIBLE); } else { page.setVisibility(View.INVISIBLE); } // self.cordova.getActivity().addContentView(webView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); page.setWebViewClient( new WebViewClient() { public boolean shouldOverrideUrlLoading(WebView view, String url) { return false; } @Override public void onPageFinished(WebView view, String url) { // Get a PrintManager instance PrintManager printManager = (PrintManager) self.cordova.getActivity() .getSystemService(Context.PRINT_SERVICE); // Get a print adapter instance PrintDocumentAdapter printAdapter = view.createPrintDocumentAdapter(); // Get a print builder attributes instance PrintAttributes.Builder builder = new PrintAttributes.Builder(); builder.setMinMargins(PrintAttributes.Margins.NO_MARGINS); // builder.setMediaSize(PrintAttributes.MediaSize.ISO_A4); // builder.setColorMode(PrintAttributes.COLOR_MODE_COLOR); // builder.setResolution(new PrintAttributes.Resolution("default", self.tmpPdfName, 600, 600)); // send success result to cordova PluginResult result = new PluginResult(PluginResult.Status.OK); result.setKeepCallback(false); self.callbackContext.sendPluginResult(result); // Create & send a print job File filePdf = new File(self.tmpPdfName); printManager.print(filePdf.getName(), printAdapter, builder.build()); } }); // Reverse engineer base url (assets/www) from the cordova webView url String baseURL = self.webView.getUrl(); baseURL = baseURL.substring(0, baseURL.lastIndexOf('/') + 1); // Load content into the print webview if( showWebViewForDebugging ) { cordova.getActivity().addContentView(page, new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); } page.loadDataWithBaseURL(baseURL, content, "text/html", "utf-8", null); } } }); // send "no-result" result to delay result handling PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT); pluginResult.setKeepCallback(true); callbackContext.sendPluginResult(pluginResult); return true; } return false; } catch (JSONException e) { // TODO: signal JSON problem to JS //callbackContext.error("Problem with JSON"); return false; } } /** * * Clean up and close all open files. * */ @Override public void onDestroy() { // ToDo: clean up. } // -------------------------------------------------------------------------- // LOCAL METHODS // -------------------------------------------------------------------------- /** * Loads the html content into a WebView, saves it as a single multi page pdf file and * calls startPdfApp() once it´s done. */ private void loadContentIntoWebView (String content) { Activity ctx = cordova.getActivity(); final WebView page = new Html2PdfWebView(ctx); final Html2pdf self = this; if( showWebViewForDebugging ) { page.setVisibility(View.VISIBLE); } else { page.setVisibility(View.INVISIBLE); } page.getSettings().setJavaScriptEnabled(false); page.setDrawingCacheEnabled(true); // Don´t auto-scale the content to the webview's width. page.getSettings().setLoadWithOverviewMode(false); page.getSettings().setUseWideViewPort(false); page.setInitialScale(100); // Disable android text auto fit behaviour page.getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL); page.setWebViewClient( new WebViewClient() { @Override public void onPageFinished(final WebView page, String url) { new Handler().postDelayed( new Runnable() { @Override public void run() { // slice the web screenshot into pages and save as pdf Bitmap b = getWebViewAsBitmap(page); if( b != null ) { File tmpFile = self.saveWebViewAsPdf(b); b.recycle(); // add pdf as stream to the print intent Intent pdfViewIntent = new Intent(Intent.ACTION_VIEW); pdfViewIntent.setDataAndNormalize(Uri.fromFile(tmpFile)); pdfViewIntent.setType("application/pdf"); // remove the webview if( !self.showWebViewForDebugging ) { ViewGroup vg = (ViewGroup)(page.getParent()); vg.removeView(page); } // add file to media scanner MediaScannerConnection.scanFile( self.cordova.getActivity(), new String[]{tmpFile.getAbsolutePath()}, null, new OnScanCompletedListener() { @Override public void onScanCompleted(String path, Uri uri) { Log.v(LOG_TAG, "file '" + path + "' was scanned seccessfully: " + uri); } } ); // start the pdf viewer app (trigger the pdf view intent) PluginResult result; boolean success = false; if( self.canHandleIntent(self.cordova.getActivity(), pdfViewIntent) ) { try { self.cordova.startActivityForResult(self, pdfViewIntent, 0); success = true; } catch( ActivityNotFoundException e ) { success = false; } } if( success ) { // send success result to cordova result = new PluginResult(PluginResult.Status.OK); result.setKeepCallback(false); self.callbackContext.sendPluginResult(result); } else { // send error result = new PluginResult(PluginResult.Status.ERROR, "activity_not_found"); result.setKeepCallback(false); self.callbackContext.sendPluginResult(result); } } } }, 500); } }); // Set base URI to the assets/www folder String baseURL = webView.getUrl(); baseURL = baseURL.substring(0, baseURL.lastIndexOf('/') + 1); /** We make it this small on purpose (is resized after page load has finished). * Making it small in the beginning has some effects on the html <body> (body * width will always remain 100 if not set explicitly). */ if( !showWebViewForDebugging ) { ctx.addContentView(page, new ViewGroup.LayoutParams(100, 100)); } else { ctx.addContentView(page, new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } page.loadDataWithBaseURL(baseURL, content, "text/html", "utf-8", null); } public static final String MIME_TYPE_PDF = "application/pdf"; /** * Check if the supplied context can handle the given intent. * * @param context * @param intent * @return boolean */ public boolean canHandleIntent(Context context, Intent intent) { PackageManager packageManager = context.getPackageManager(); return (packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0); } /** * Takes a WebView and returns a Bitmap representation of it (takes a "screenshot"). * @param WebView * @return Bitmap */ Bitmap getWebViewAsBitmap(WebView view) { Bitmap b; // prepare drawing cache view.setDrawingCacheEnabled(true); view.buildDrawingCache(); //Get the dimensions of the view so we can re-layout the view at its current size //and create a bitmap of the same size int width = ((Html2PdfWebView) view).getContentWidth(); int height = view.getContentHeight(); if( width == 0 || height == 0 ) { // return error answer to cordova String msg = "Width or height of webview content is 0. Webview to bitmap conversion failed."; Log.e(LOG_TAG, msg ); PluginResult result = new PluginResult(PluginResult.Status.ERROR, msg); result.setKeepCallback(false); callbackContext.sendPluginResult(result); return null; } Log.v(LOG_TAG, "Html2Pdf.getWebViewAsBitmap -> Content width: " + width + ", height: " + height ); //Cause the view to re-layout view.measure(width, height); view.layout(0, 0, width, height); //Create a bitmap backed Canvas to draw the view into b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(b); // draw the view into the canvas view.draw(c); return b; } /** * Slices the screenshot into pages, merges those into a single pdf * and saves it in the public accessible /sdcard dir. */ private File saveWebViewAsPdf(Bitmap screenshot) { try { File sdCard = Environment.getExternalStorageDirectory(); File dir = new File (sdCard.getAbsolutePath() + "/" + this.publicTmpDir + "/"); dir.mkdirs(); File file; FileOutputStream stream; // creat nomedia file to avoid indexing tmp files File noMediaFile = new File(dir.getAbsolutePath() + "/", ".nomedia"); if( !noMediaFile.exists() ) { noMediaFile.createNewFile(); } double pageWidth = PageSize.A4.getWidth() * 0.85; // width of the image is 85% of the page double pageHeight = PageSize.A4.getHeight() * 0.80; // max height of the image is 80% of the page double pageHeightToWithRelation = pageHeight / pageWidth; // e.g.: 1.33 (4/3) Bitmap currPage; int totalSize = screenshot.getHeight(); int currPos = 0; int currPageCount = 0; int sliceWidth = screenshot.getWidth(); int sliceHeight = (int) Math.round(sliceWidth * pageHeightToWithRelation); while( totalSize > currPos && currPageCount < 100 ) // max 100 pages { currPageCount++; Log.v(LOG_TAG, "Creating page nr. " + currPageCount ); // slice bitmap currPage = Bitmap.createBitmap(screenshot, 0, currPos, sliceWidth, (int) Math.min( sliceHeight, totalSize - currPos )); // save page as png stream = new FileOutputStream( new File(dir, "pdf-page-"+currPageCount+".png") ); currPage.compress(Bitmap.CompressFormat.PNG, 100, stream); stream.close(); // move current position indicator currPos += sliceHeight; currPage.recycle(); } // create pdf Document document = new Document(); File filePdf = new File(sdCard.getAbsolutePath() + "/" + this.tmpPdfName); // change the output name of the pdf here // create dirs if necessary if( this.tmpPdfName.contains("/") ) { File filePdfDir = new File(filePdf.getAbsolutePath().substring(0, filePdf.getAbsolutePath().lastIndexOf("/"))); // get the dir portion filePdfDir.mkdirs(); } PdfWriter.getInstance(document,new FileOutputStream(filePdf)); document.open(); for( int i=1; i<=currPageCount; ++i ) { file = new File(dir, "pdf-page-"+i+".png"); Image image = Image.getInstance (file.getAbsolutePath()); image.scaleToFit( (float)pageWidth, 9999); image.setAlignment(Element.ALIGN_CENTER); document.add(image); document.newPage(); } document.close(); // delete tmp image files for( int i=1; i<=currPageCount; ++i ) { file = new File(dir, "pdf-page-"+i+".png"); file.delete(); } return filePdf; } catch (IOException e) { Log.e(LOG_TAG, "ERROR: " + e.getMessage()); e.printStackTrace(); // return error answer to cordova PluginResult result = new PluginResult(PluginResult.Status.ERROR, e.getMessage()); result.setKeepCallback(false); callbackContext.sendPluginResult(result); } catch (DocumentException e) { Log.e(LOG_TAG, "ERROR: " + e.getMessage()); e.printStackTrace(); // return error answer to cordova PluginResult result = new PluginResult(PluginResult.Status.ERROR, e.getMessage()); result.setKeepCallback(false); callbackContext.sendPluginResult(result); } Log.v(LOG_TAG, "Uncaught ERROR!"); return null; } } class Html2PdfWebView extends WebView { public Html2PdfWebView(Context context) { super(context); } public int getContentWidth() { return this.computeHorizontalScrollRange(); } }